Skip to content

Commit fc6f54b

Browse files
committed
better restart button
1 parent 6b16e02 commit fc6f54b

5 files changed

Lines changed: 100 additions & 19 deletions

File tree

cmd/curio/rpc/rpc.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,17 @@ func CurioHandler(
9898
return ah
9999
}
100100

101+
// TaskEngineRestarter begins an in-memory graceful restart: it cordons the
102+
// node (without persisting to the DB), drains running tasks, then restarts.
103+
type TaskEngineRestarter interface {
104+
RequestRestart()
105+
}
106+
101107
type CurioAPI struct {
102108
*deps.Deps
103109
paths.SectorIndex
104110
ShutdownChan chan struct{}
111+
restarter TaskEngineRestarter
105112
}
106113

107114
func (p *CurioAPI) Version(context.Context) ([]int, error) {
@@ -226,8 +233,14 @@ func (p *CurioAPI) Shutdown(context.Context) error {
226233
return gracehttpsvc.TriggerShutdown()
227234
}
228235

229-
// Restart triggers a zero-downtime process restart via SIGUSR2.
236+
// Restart initiates a graceful restart. When a task engine is wired in, it
237+
// cordons the node in-memory and drains running tasks before triggering the
238+
// zero-downtime restart; otherwise it triggers the restart immediately.
230239
func (p *CurioAPI) Restart(context.Context) error {
240+
if p.restarter != nil {
241+
p.restarter.RequestRestart()
242+
return nil
243+
}
231244
return gracehttpsvc.TriggerRestart()
232245
}
233246

@@ -457,7 +470,7 @@ func (p *CurioAPI) IndexSamples(ctx context.Context, pcid cid.Cid) ([]multihash.
457470
return p.IndexStore.GetPieceHashRange(ctx, pcid, firstHash, chunk.NumberOfBlocks, true)
458471
}
459472

460-
func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan chan struct{}, extraServers ...*http.Server) error {
473+
func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan chan struct{}, restarter TaskEngineRestarter, extraServers ...*http.Server) error {
461474
fh := &paths.FetchHandler{Local: dependencies.LocalStore, PfHandler: &paths.DefaultPartialFileHandler{}}
462475
remoteHandler := func(w http.ResponseWriter, r *http.Request) {
463476
if !auth.HasPerm(r.Context(), nil, lapi.PermAdmin) {
@@ -489,7 +502,7 @@ func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan c
489502
Handler: CurioHandler(
490503
authVerify,
491504
remoteHandler,
492-
&CurioAPI{dependencies, dependencies.Si, shutdownChan},
505+
&CurioAPI{dependencies, dependencies.Si, shutdownChan, restarter},
493506
prometheusServiceDiscovery(ctx, dependencies),
494507
permissioned),
495508
ReadHeaderTimeout: time.Minute * 3,

cmd/curio/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ var runCmd = &cli.Command{
168168
extraServers = append(extraServers, marketServer)
169169
}
170170

171-
err = rpc.ListenAndServe(ctx, dependencies, shutdownChan, extraServers...)
171+
err = rpc.ListenAndServe(ctx, dependencies, shutdownChan, taskEngine, extraServers...)
172172
if err != nil {
173173
return err
174174
}

harmony/harmonytask/harmonytask.go

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

itests/grace_restart_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestGraceRestartDuringPDPPing(t *testing.T) {
7171
}()
7272

7373
require.Eventually(t, func() bool {
74-
return pingSuccesses.Load() >= 20
74+
return pingSuccesses.Load() >= 5
7575
}, 30*time.Second, 25*time.Millisecond, "expected initial successful PDP pings before restart")
7676

7777
require.NoError(t, gracehttpsvc.RestartFromPIDFile(pidPath))
@@ -85,7 +85,7 @@ func TestGraceRestartDuringPDPPing(t *testing.T) {
8585
}, 2*time.Minute, 100*time.Millisecond, "expected curio.pid to reflect a new process after restart")
8686

8787
require.Eventually(t, func() bool {
88-
return pingSuccesses.Load() >= 40
88+
return pingSuccesses.Load() >= 10
8989
}, 2*time.Minute, 25*time.Millisecond, "expected continued successful PDP pings through restart")
9090

9191
close(stopPinger)

itests/helpers/harness.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func StartCurioHarness(
100100
if marketServer != nil {
101101
extra = append(extra, marketServer)
102102
}
103-
err := rpc.ListenAndServe(ctx, dependencies, shutdownChan, extra...)
103+
err := rpc.ListenAndServe(ctx, dependencies, shutdownChan, taskEngine, extra...)
104104
if err != nil {
105105
t.Errorf("failed to start the Curio RPC server: %v", err)
106106
}

0 commit comments

Comments
 (0)