diff --git a/services/core/internal/sandbox/node/agent.go b/services/core/internal/sandbox/node/agent.go index 7390a8114..6508929cd 100644 --- a/services/core/internal/sandbox/node/agent.go +++ b/services/core/internal/sandbox/node/agent.go @@ -388,17 +388,18 @@ func (a *agent) executeWork(ctx context.Context, task work) { operationCtx, cancel := context.WithDeadline(context.Background(), task.request.deadline) a.active.Add(1) started := time.Now() - result := execute(operationCtx, task.provider, task.request) + result, err := execute(operationCtx, task.provider, task.request) finished := time.Now() a.active.Add(-1) cancel() _ = task.connection.write(frame{Type: "response", Response: &result}) - logOperation(ctx, task, started, finished, result.ErrorCode) + logOperation(ctx, task, started, finished, result.ErrorCode, err) } // Queue wait and execution share this node's monotonic clock. Read-only // observations repeat on every reconciliation tick and log at debug level. -func logOperation(ctx context.Context, task work, started, finished time.Time, code string) { +// The local error stays in this host's journal; it may name host paths. +func logOperation(ctx context.Context, task work, started, finished time.Time, code string, err error) { level := slog.LevelInfo switch task.request.Operation { case "info", "renew", "initial", "new_compute", "compute", "observe": @@ -406,6 +407,10 @@ func logOperation(ctx context.Context, task work, started, finished time.Time, c level = slog.LevelDebug } } - log.Bg().Log(ctx, level, "sandbox node operation", "operation", task.request.Operation, "allocation_id", task.request.Reference.AllocationID, - "queue_ms", started.Sub(task.queued).Milliseconds(), "duration_ms", finished.Sub(started).Milliseconds(), "error_code", code) + attrs := []any{"operation", task.request.Operation, "allocation_id", task.request.Reference.AllocationID, + "queue_ms", started.Sub(task.queued).Milliseconds(), "duration_ms", finished.Sub(started).Milliseconds(), "error_code", code} + if err != nil { + attrs = append(attrs, "error", err) + } + log.Bg().Log(ctx, level, "sandbox node operation", attrs...) } diff --git a/services/core/internal/sandbox/node/observations_test.go b/services/core/internal/sandbox/node/observations_test.go index 7ba468fd4..9f389f11a 100644 --- a/services/core/internal/sandbox/node/observations_test.go +++ b/services/core/internal/sandbox/node/observations_test.go @@ -152,7 +152,7 @@ func TestObservationWirePreservesUnavailableAndRejectsMismatchedIdentity(t *test } for _, test := range providers { t.Run(test.name, func(t *testing.T) { - out := execute(t.Context(), test.provider, q) + out, _ := execute(t.Context(), test.provider, q) if !errors.Is(responseError(out), test.want) || out.Sample != nil { t.Fatal("observation error or missing sample was fabricated", out) } @@ -163,7 +163,7 @@ func TestObservationWirePreservesUnavailableAndRejectsMismatchedIdentity(t *test invalid := target mutate(&invalid) q.Observation = &invalid - if out := execute(t.Context(), p, q); !errors.Is(responseError(out), sandbox.ErrInvalid) { + if out, _ := execute(t.Context(), p, q); !errors.Is(responseError(out), sandbox.ErrInvalid) { t.Fatal("mismatched observation reached provider", out) } } diff --git a/services/core/internal/sandbox/node/operations_test.go b/services/core/internal/sandbox/node/operations_test.go index e508988b5..b5a83f0d7 100644 --- a/services/core/internal/sandbox/node/operations_test.go +++ b/services/core/internal/sandbox/node/operations_test.go @@ -15,7 +15,7 @@ import ( func TestUnsupportedWireIsExplicitAndDoesNotInvokeProvider(t *testing.T) { p := &fakeProvider{} q := request{ID: uuid.NewString(), ConnectionID: uuid.NewString(), Reference: reference(), TimeoutMillis: 1000, Operation: "initial"} - out := execute(t.Context(), p, q) + out, _ := execute(t.Context(), p, q) if p.creates != 0 || p.reads != 0 || p.kills != 0 { t.Fatal("unsupported operation reached provider") } diff --git a/services/core/internal/sandbox/node/wire.go b/services/core/internal/sandbox/node/wire.go index a8ac9993f..59324ec8e 100644 --- a/services/core/internal/sandbox/node/wire.go +++ b/services/core/internal/sandbox/node/wire.go @@ -289,12 +289,14 @@ func (q request) validate() error { } return sandbox.ErrInvalid } -func execute(ctx context.Context, p sandbox.SandboxProvider, q request) response { + +// execute also returns the local error; only its code crosses the transport. +func execute(ctx context.Context, p sandbox.SandboxProvider, q request) (response, error) { out := response{ID: q.ID, ConnectionID: q.ConnectionID} err := q.validate() if err != nil { out.ErrorCode = errorCode(err) - return out + return out, err } if err := providercontract.Require(p, operationMethod(q.Operation)); err != nil { out.ErrorCode = errorCode(err) @@ -302,7 +304,7 @@ func execute(ctx context.Context, p sandbox.SandboxProvider, q request) response if errors.As(err, &unsupported) { out.Unsupported = unsupported } - return out + return out, err } var info sandbox.Info var command sandbox.CommandResult @@ -374,7 +376,7 @@ func execute(ctx context.Context, p sandbox.SandboxProvider, q request) response out.Command = nil out.Compute = nil } - return out + return out, err } func requiresReady(q request) bool { diff --git a/services/core/internal/sandbox/node/workspace_test.go b/services/core/internal/sandbox/node/workspace_test.go index 87b81fea9..0623cfce6 100644 --- a/services/core/internal/sandbox/node/workspace_test.go +++ b/services/core/internal/sandbox/node/workspace_test.go @@ -78,7 +78,10 @@ func TestActualWorkspaceRefusalKeepsSettledAbsenceOnWire(t *testing.T) { bootstrap := sandbox.Bootstrap{Reference: ref, SessionID: ref.TenantID, DeviceID: ref.EnvironmentID, CoreURL: "https://core.example/api/v1", Credential: "fixture", Workspace: binding} ctx, cancel := context.WithTimeout(t.Context(), time.Second) defer cancel() - out := execute(ctx, provider, request{ID: ref.AllocationID, ConnectionID: ref.TenantID, Reference: ref, Operation: "create", TimeoutMillis: 1000, Bootstrap: &bootstrap}) + out, local := execute(ctx, provider, request{ID: ref.AllocationID, ConnectionID: ref.TenantID, Reference: ref, Operation: "create", TimeoutMillis: 1000, Bootstrap: &bootstrap}) + if !errors.Is(local, workspacefs.ErrUnsupported) { + t.Fatal("the node journal lost the local error", local) + } raw, err := json.Marshal(frame{Version: ProtocolVersion, Type: "response", Response: &out}) if err != nil { t.Fatal(err)