{"record":{"id":"d069a1f443defd3c","repo":"Tencent/WeKnora","slug":"sandbox-orphan-reaper-requires-a-client","errorCode":null,"errorMessage":"sandbox: orphan reaper requires a client","messagePattern":"sandbox: orphan reaper requires a client","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/orphan_reaper.go","lineNumber":60,"sourceCode":"\n\t// ConfigID scopes the sweep to one sandbox config. Empty means the\n\t// deployment default config. Sweeping by tenant alone would delete\n\t// sandboxes belonging to a sibling config on the same provider account.\n\tConfigID string\n\n\t// Grace protects freshly created sandboxes that may not be bound yet.\n\t// Pass 0 when reaping deliberately (e.g. before a backend switch), where\n\t// every sandbox on the old backend is known to be abandoned.\n\tGrace time.Duration\n\n\tNow func() time.Time\n}\n\n// ReapOrphanSandboxes deletes unbound sandboxes for one tenant and reports how\n// many were removed.\nfunc ReapOrphanSandboxes(ctx context.Context, deps OrphanReaperDeps) (int, error) {\n\tif deps.Client == nil {\n\t\treturn 0, fmt.Errorf(\"sandbox: orphan reaper requires a client\")\n\t}\n\tnow := deps.Now\n\tif now == nil {\n\t\tnow = time.Now\n\t}\n\n\tsummaries, err := deps.Client.List(ctx, configSandboxFilter(deps.TenantID, deps.ConfigID))\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\n\t\t\t\"sandbox: list workspace %d config %q sandboxes: %w\",\n\t\t\tdeps.TenantID, NormalizeConfigID(deps.ConfigID), err)\n\t}\n\n\tcutoff := now().Add(-deps.Grace)\n\tdeleted := 0\n\tfor _, summary := range summaries {\n\t\tif _, bound := deps.BoundIDs[summary.ID]; bound {\n\t\t\tcontinue","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/orphan_reaper.go#L42-L78","documentation":"ReapOrphanSandboxes deletes unbound sandboxes for a tenant, but it needs a working sandbox store client to list and delete them. Before doing any work it validates its OrphanReaperDeps; if deps.Client is nil it refuses to run with this sentinel-style error. It is a programming/configuration error, not a runtime failure.","triggerScenarios":"Calling ReapOrphanSandboxes with OrphanReaperDeps{} or any deps value where the Client field was never assigned (constructor skipped, config load failed silently, or a test forgot to stub the client).","commonSituations":"Wiring a reaper cron job before the Redis/store client is initialized; partially-built dependency structs after a config error path; unit tests constructing deps inline without a fake client.","solutions":["Initialize deps.Client with the sandbox store client returned by the library's constructor before calling ReapOrphanSandboxes","Guard the call site: skip or reschedule reaping when the client is not yet connected","In tests, supply a fake/stub client in OrphanReaperDeps instead of a zero-value struct"],"exampleFix":"// before\ndeps := sandbox.OrphanReaperDeps{TenantID: 7, ConfigID: \"cfg\", Grace: time.Hour}\nn, err := sandbox.ReapOrphanSandboxes(ctx, deps)\n// after\ndeps := sandbox.OrphanReaperDeps{Client: client, TenantID: 7, ConfigID: \"cfg\", Grace: time.Hour}\nn, err := sandbox.ReapOrphanSandboxes(ctx, deps)","handlingStrategy":"validation","validationCode":"if deps.Client == nil {\n    return errors.New(\"reaper deps missing client; skipping reap\")\n}\nn, err := sandbox.ReapOrphanSandboxes(ctx, deps)","typeGuard":"func reaperReady(deps sandbox.OrphanReaperDeps) bool { return deps.Client != nil }","tryCatchPattern":null,"preventionTips":["Construct OrphanReaperDeps through a single constructor that always sets Client","Fail fast at startup if the client dependency is nil","In tests, use a shared helper that builds fully-populated deps"],"tags":["go","sandbox","misconfiguration","nil-dependency"],"backgroundTag":"nil-dependency-injection","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}