Add recovery for orphaned jobs
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
||||
)
|
||||
|
||||
type recordingSender struct {
|
||||
envelopes []api.Envelope
|
||||
}
|
||||
|
||||
func (s *recordingSender) Send(env api.Envelope) error {
|
||||
s.envelopes = append(s.envelopes, env)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCancelCommandReportsUnknownJob(t *testing.T) {
|
||||
d := &dispatcher{}
|
||||
tx := &recordingSender{}
|
||||
env, err := api.Marshal(api.MsgCommandCancel, "request-1", api.CommandCancelPayload{JobID: "missing-job"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal command: %v", err)
|
||||
}
|
||||
|
||||
if err := d.handle(context.Background(), env, tx); err != nil {
|
||||
t.Fatalf("handle cancel: %v", err)
|
||||
}
|
||||
if len(tx.envelopes) != 1 {
|
||||
t.Fatalf("sent %d envelopes, want 1", len(tx.envelopes))
|
||||
}
|
||||
if tx.envelopes[0].Type != api.MsgCommandResult || tx.envelopes[0].ID != env.ID {
|
||||
t.Fatalf("unexpected result envelope: %+v", tx.envelopes[0])
|
||||
}
|
||||
var result api.CommandResultPayload
|
||||
if err := tx.envelopes[0].UnmarshalPayload(&result); err != nil {
|
||||
t.Fatalf("unmarshal result: %v", err)
|
||||
}
|
||||
if result.JobID != "missing-job" || result.Accepted || result.Error != "job_not_found" {
|
||||
t.Fatalf("unexpected result: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelCommandCancelsTrackedJob(t *testing.T) {
|
||||
d := &dispatcher{}
|
||||
cancelled := false
|
||||
d.trackJob("running-job", func() { cancelled = true })
|
||||
tx := &recordingSender{}
|
||||
env, err := api.Marshal(api.MsgCommandCancel, "request-2", api.CommandCancelPayload{JobID: "running-job"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal command: %v", err)
|
||||
}
|
||||
|
||||
if err := d.handle(context.Background(), env, tx); err != nil {
|
||||
t.Fatalf("handle cancel: %v", err)
|
||||
}
|
||||
if !cancelled {
|
||||
t.Fatal("tracked job was not cancelled")
|
||||
}
|
||||
var result api.CommandResultPayload
|
||||
if err := tx.envelopes[0].UnmarshalPayload(&result); err != nil {
|
||||
t.Fatalf("unmarshal result: %v", err)
|
||||
}
|
||||
if !result.Accepted || result.Error != "" {
|
||||
t.Fatalf("unexpected result: %+v", result)
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -276,7 +276,8 @@ func (d *dispatcher) handle(ctx context.Context, env api.Envelope, tx wsclient.S
|
||||
if err := env.UnmarshalPayload(&p); err != nil {
|
||||
return fmt.Errorf("command.cancel: %w", err)
|
||||
}
|
||||
if d.cancelJob(p.JobID) {
|
||||
known := d.cancelJob(p.JobID)
|
||||
if known {
|
||||
slog.Info("ws agent: command.cancel applied", "job_id", p.JobID)
|
||||
} else {
|
||||
// Job already finished or was never seen on this agent.
|
||||
@@ -284,6 +285,17 @@ func (d *dispatcher) handle(ctx context.Context, env api.Envelope, tx wsclient.S
|
||||
// natural completion. Server-side state is authoritative.
|
||||
slog.Info("ws agent: command.cancel for unknown job (already finished?)", "job_id", p.JobID)
|
||||
}
|
||||
result := api.CommandResultPayload{JobID: p.JobID, Accepted: known}
|
||||
if !known {
|
||||
result.Error = "job_not_found"
|
||||
}
|
||||
ack, err := api.Marshal(api.MsgCommandResult, env.ID, result)
|
||||
if err != nil {
|
||||
return fmt.Errorf("command.cancel result: %w", err)
|
||||
}
|
||||
if err := tx.Send(ack); err != nil {
|
||||
return fmt.Errorf("command.cancel result send: %w", err)
|
||||
}
|
||||
|
||||
case api.MsgTreeList:
|
||||
// Synchronous RPC for the restore wizard's tree browser. The
|
||||
|
||||
Reference in New Issue
Block a user