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 }