@@ -146,6 +146,11 @@ type TaskEngine struct {
146146 // runtime flags
147147 yieldBackground atomic.Bool
148148
149+ // restartRequested is an in-memory cordon set when a graceful restart is
150+ // requested via RPC. It is intentionally not persisted to the DB so that
151+ // the node returns to its previous cordon state after restarting.
152+ restartRequested atomic.Bool
153+
149154 // synchronous to the single-threaded poller
150155 lastFollowTime time.Time
151156 lastCleanup atomic.Value
@@ -289,6 +294,11 @@ func (e *TaskEngine) poller() {
289294 continue
290295 }
291296
297+ // An in-memory restart request cordons the node without touching the DB.
298+ if e .restartRequested .Load () {
299+ schedulable = false
300+ }
301+
292302 e .yieldBackground .Store (! schedulable )
293303
294304 accepted := e .pollerTryAllWork (schedulable )
@@ -519,28 +529,86 @@ func (e *TaskEngine) checkNodeFlags() (bool, error) {
519529 return ! unschedulable , nil
520530}
521531
522- func (e * TaskEngine ) restartIfNoTasksPending (pendingSince time.Time ) {
523- var tasksPending int
524- err := e .db .QueryRow (e .ctx , `SELECT COUNT(*) FROM harmony_task WHERE owner_id=$1` , e .ownerID ).Scan (& tasksPending )
525- if err != nil {
526- log .Error ("Unable to check for tasks pending: " , err )
532+ func (e * TaskEngine ) activeTaskCount () int {
533+ count := 0
534+ for _ , h := range e .handlers {
535+ count += h .Max .ActiveThis ()
536+ }
537+ return count
538+ }
539+
540+ // RequestRestart begins an in-memory graceful restart. The node immediately
541+ // stops accepting new tasks (the same effect as cordoning), waits for the
542+ // currently-running tasks to drain, then triggers a zero-downtime restart.
543+ //
544+ // The cordon is kept in-memory only and is never written to harmony_machines,
545+ // so after the restart the node returns to whatever cordon state was persisted
546+ // in the database before the restart was requested.
547+ func (e * TaskEngine ) RequestRestart () {
548+ if ! e .restartRequested .CompareAndSwap (false , true ) {
549+ // a restart is already in progress
527550 return
528551 }
529- if tasksPending == 0 {
530- log .Infow ("no tasks pending, restarting" , "ownerID" , e .ownerID , "pendingSince" , pendingSince , "took" , time .Since (pendingSince ))
552+ log .Infow ("restart requested; cordoning in-memory and draining tasks" , "ownerID" , e .ownerID )
553+ go e .drainAndRestart ()
554+ }
531555
532- // unset the flags first
533- _ , err = e .db .Exec (e .ctx , `UPDATE harmony_machines SET restart_request=NULL, unschedulable=FALSE WHERE host_and_port=$1` , e .hostAndPort )
534- if err != nil {
535- log .Error ("Unable to unset restart request: " , err )
556+ func (e * TaskEngine ) drainAndRestart () {
557+ ticker := time .NewTicker (time .Second )
558+ defer ticker .Stop ()
559+
560+ for {
561+ select {
562+ case <- e .ctx .Done ():
536563 return
564+ case <- ticker .C :
565+ }
566+
567+ if active := e .activeTaskCount (); active > 0 {
568+ log .Infow ("restart waiting for tasks to drain" , "ownerID" , e .ownerID , "activeTasks " , active )
569+ continue
537570 }
538571
539- // zero-downtime restart via gracehttp; fall back to exit 100 for systemd
572+ log . Infow ( "no tasks running, triggering graceful restart" , "ownerID" , e . ownerID )
540573 if err := gracehttpsvc .TriggerRestart (); err != nil {
541574 log .Errorw ("graceful restart failed, falling back to exit" , "error" , err )
542575 os .Exit (ExitStatusRestartRequest )
543576 }
577+ return
578+ }
579+ }
580+
581+ func (e * TaskEngine ) restartIfNoTasksPending (pendingSince time.Time ) {
582+ var tasksPending int
583+ err := e .db .QueryRow (e .ctx , `SELECT COUNT(*) FROM harmony_task WHERE owner_id=$1` , e .ownerID ).Scan (& tasksPending )
584+ if err != nil {
585+ log .Error ("Unable to check for tasks pending: " , err )
586+ return
587+ }
588+
589+ activeTasks := e .activeTaskCount ()
590+ if tasksPending > 0 || activeTasks > 0 {
591+ log .Infow ("restart waiting for tasks to finish" ,
592+ "ownerID" , e .ownerID ,
593+ "pendingSince" , pendingSince ,
594+ "tasksPending" , tasksPending ,
595+ "activeTasks" , activeTasks )
596+ return
597+ }
598+
599+ log .Infow ("no tasks pending, restarting" , "ownerID" , e .ownerID , "pendingSince" , pendingSince , "took" , time .Since (pendingSince ))
600+
601+ // Clear restart_request only; stay cordoned until the operator uncordons after restart.
602+ _ , err = e .db .Exec (e .ctx , `UPDATE harmony_machines SET restart_request=NULL WHERE host_and_port=$1` , e .hostAndPort )
603+ if err != nil {
604+ log .Error ("Unable to unset restart request: " , err )
605+ return
606+ }
607+
608+ // zero-downtime restart via gracehttp; fall back to exit 100 for systemd
609+ if err := gracehttpsvc .TriggerRestart (); err != nil {
610+ log .Errorw ("graceful restart failed, falling back to exit" , "error" , err )
611+ os .Exit (ExitStatusRestartRequest )
544612 }
545613}
546614
0 commit comments