Wait for the interrupted operation to clean up before exit (closes #159)
check / check (pull_request) Successful in 1m21s
check / check (pull_request) Successful in 1m21s
On SIGINT/SIGTERM, RunOperation's OnStop hook only cancelled the running command and returned; nothing waited for its goroutine, so the process could exit before restore's cleanup defers ran. That left decrypted data in the temp directory: the blob cache (vaultik-blobcache-*) and the decrypted snapshot-database directory (vaultik-restore-*). StartOperation now returns a stop function that cancels the context and waits for the goroutine to return, bounded by the existing shutdownTimeout; OnStop calls it and warns if it times out. The fix is in the shared runner, so it covers every command, not only restore. Restore's chunk-write and blob-download loops also check the context between steps so the wait ends promptly. Model: opus-4-8
This commit is contained in:
+18
-5
@@ -238,7 +238,10 @@ var errReported = errors.New("operation failed")
|
||||
//
|
||||
// op runs in a goroutine so OnStart returns promptly and an interrupt
|
||||
// can still cancel through OnStop; when it finishes, success or failure,
|
||||
// it triggers shutdown, which is what lets RunWithApp return. report is
|
||||
// it triggers shutdown, which is what lets RunWithApp return. On an
|
||||
// interrupt OnStop cancels op and waits for the goroutine to return, so
|
||||
// op's cleanup (removing decrypted scratch files) runs before the
|
||||
// process exits; the wait is bounded by shutdownTimeout. report is
|
||||
// called with a non-canceled failure so the caller can log it (and
|
||||
// suppress it under --json) before it becomes errReported. A context
|
||||
// cancellation is the interrupt path, not a failure: it is neither
|
||||
@@ -254,9 +257,11 @@ func RunOperation(
|
||||
|
||||
opts.Invokes = append(opts.Invokes,
|
||||
fx.Invoke(func(v *vaultik.Vaultik, lc fx.Lifecycle) {
|
||||
var stop func(context.Context) bool
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(_ context.Context) error {
|
||||
go func() {
|
||||
stop = v.StartOperation(func() {
|
||||
err := op(v)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
report(err)
|
||||
@@ -270,12 +275,20 @@ func RunOperation(
|
||||
if stopErr != nil {
|
||||
log.Error("Failed to shutdown", "error", stopErr)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(_ context.Context) error {
|
||||
v.Cancel()
|
||||
// On an interrupt, cancel the operation and wait for it to
|
||||
// unwind so its cleanup defers (which remove decrypted
|
||||
// scratch files from the temp directory) run before the
|
||||
// process exits. The wait is bounded by ctx, the existing
|
||||
// shutdownTimeout.
|
||||
OnStop: func(ctx context.Context) error {
|
||||
if !stop(ctx) {
|
||||
log.Warn("Shutdown timed out before the operation " +
|
||||
"finished; decrypted temporary files may remain")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user