tractor.dev/toolkit-go@v0.0.0-20241010005851-214d91207d07/engine/daemon/daemon.go (about)

     1  package daemon
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"log/slog"
     7  	"reflect"
     8  	"sync"
     9  	"sync/atomic"
    10  	"time"
    11  )
    12  
    13  // Initializer is initialized before services are started. Returning
    14  // an error will cancel the start of daemon services.
    15  type Initializer interface {
    16  	InitializeDaemon() error
    17  }
    18  
    19  // Terminator is terminated when the daemon gets a stop signal.
    20  type Terminator interface {
    21  	TerminateDaemon(ctx context.Context) error
    22  }
    23  
    24  // Service is run after the daemon is initialized.
    25  type Service interface {
    26  	Serve(ctx context.Context)
    27  }
    28  
    29  // Framework is a top-level daemon lifecycle manager runs services given to it.
    30  type Framework struct {
    31  	Initializers []Initializer
    32  	Services     []Service
    33  	Terminators  []Terminator
    34  	Context      context.Context
    35  	OnFinished   func()
    36  	Log          *slog.Logger
    37  
    38  	running    int32
    39  	cancel     context.CancelFunc
    40  	terminated chan bool
    41  }
    42  
    43  // New builds a daemon configured to run a set of services. The services
    44  // are populated with each other if they have fields that match anything
    45  // that was passed in.
    46  func New(services ...Service) *Framework {
    47  	d := &Framework{}
    48  	d.Add(services...)
    49  	return d
    50  }
    51  
    52  // Add appends Services, Initializers, Terminators to daemon
    53  func (d *Framework) Add(services ...Service) {
    54  	for _, s := range services {
    55  		d.Services = append(d.Services, s)
    56  		if i, ok := s.(Initializer); ok {
    57  			d.Initializers = append(d.Initializers, i)
    58  		}
    59  		if t, ok := s.(Terminator); ok {
    60  			d.Terminators = append(d.Terminators, t)
    61  		}
    62  	}
    63  }
    64  
    65  // Run executes the daemon lifecycle
    66  func (d *Framework) Run(ctx context.Context) error {
    67  	if !atomic.CompareAndSwapInt32(&d.running, 0, 1) {
    68  		return errors.New("already running")
    69  	}
    70  
    71  	// call initializers
    72  	for _, i := range d.Initializers {
    73  		d.Log.Debug("initializing", "service", ptrName(i))
    74  		if err := i.InitializeDaemon(); err != nil {
    75  			return err
    76  		}
    77  	}
    78  
    79  	// finish if no services
    80  	if len(d.Services) == 0 {
    81  		return errors.New("no services to run")
    82  	}
    83  
    84  	if ctx == nil {
    85  		ctx = context.Background()
    86  	}
    87  	ctx, cancelFunc := context.WithCancel(ctx)
    88  	d.Context = ctx
    89  	d.cancel = cancelFunc
    90  	d.terminated = make(chan bool, 1)
    91  
    92  	// setup terminators on stop signals
    93  	go TerminateOnSignal(d)
    94  	go TerminateOnContextDone(d)
    95  
    96  	var wg sync.WaitGroup
    97  	var running sync.Map
    98  	for _, service := range d.Services {
    99  		running.Store(service, nil)
   100  		wg.Add(1)
   101  		go func(s Service) {
   102  			defer func() {
   103  				if r := recover(); r != nil {
   104  					d.Log.Info("serve panic from ", ptrName(s))
   105  					d.Log.Debug("serve panic:", r)
   106  				}
   107  			}()
   108  			defer wg.Done()
   109  			s.Serve(d.Context)
   110  			running.Delete(s)
   111  		}(service)
   112  	}
   113  
   114  	finished := make(chan bool)
   115  	go func() {
   116  		wg.Wait()
   117  		close(finished)
   118  	}()
   119  
   120  	select {
   121  	case <-finished:
   122  		<-d.terminated
   123  	case <-d.terminated:
   124  		<-time.After(10 * time.Millisecond)
   125  		var waiting []string
   126  		running.Range(func(k, v interface{}) bool {
   127  			waiting = append(waiting, ptrName(k))
   128  			return true
   129  		})
   130  		if len(waiting) > 0 {
   131  			d.Log.Info("waiting on serve for:", waiting)
   132  		}
   133  		select {
   134  		case <-finished:
   135  		case <-time.After(2 * time.Second):
   136  			var waiting []string
   137  			running.Range(func(k, v interface{}) bool {
   138  				waiting = append(waiting, ptrName(k))
   139  				return true
   140  			})
   141  			d.Log.Info("warning: unfinished services:", waiting)
   142  		}
   143  	}
   144  
   145  	if d.OnFinished != nil {
   146  		d.OnFinished()
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  // Terminate cancels the daemon context and calls Terminators in reverse order
   153  func (d *Framework) Terminate() {
   154  	if d == nil {
   155  		// find these cases and prevent them!
   156  		panic("daemon reference used to Terminate but daemon pointer is nil")
   157  	}
   158  
   159  	if !atomic.CompareAndSwapInt32(&d.running, 1, 0) {
   160  		return
   161  	}
   162  
   163  	d.Log.Info("shutting down")
   164  
   165  	if d.cancel != nil {
   166  		d.cancel()
   167  	}
   168  
   169  	// hmmm..
   170  	ctx := context.Background()
   171  
   172  	var wg sync.WaitGroup
   173  	for i := len(d.Terminators) - 1; i >= 0; i-- {
   174  		wg.Add(1)
   175  		go func(t Terminator) {
   176  			d.Log.Debug("terminating", ptrName(t))
   177  			// TODO: timeout
   178  			if err := t.TerminateDaemon(ctx); err != nil {
   179  				d.Log.Info("terminate error:", err)
   180  			}
   181  			wg.Done()
   182  		}(d.Terminators[i])
   183  	}
   184  	wg.Wait()
   185  	d.Log.Debug("finished termination")
   186  	d.terminated <- true
   187  }
   188  
   189  // TerminateOnContextDone waits for the deamon's context to be canceled.
   190  func TerminateOnContextDone(d *Framework) {
   191  	<-d.Context.Done()
   192  	d.Terminate()
   193  }
   194  
   195  func ptrName(v any) string {
   196  	return reflect.ValueOf(v).Elem().Type().String()
   197  }