Tencent/WeKnora · error

sandbox: orphan reaper requires a client

Error message

sandbox: orphan reaper requires a client

What it means

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.

Source

Thrown at internal/sandbox/orphan_reaper.go:60

	// ConfigID scopes the sweep to one sandbox config. Empty means the
	// deployment default config. Sweeping by tenant alone would delete
	// sandboxes belonging to a sibling config on the same provider account.
	ConfigID string

	// Grace protects freshly created sandboxes that may not be bound yet.
	// Pass 0 when reaping deliberately (e.g. before a backend switch), where
	// every sandbox on the old backend is known to be abandoned.
	Grace time.Duration

	Now func() time.Time
}

// ReapOrphanSandboxes deletes unbound sandboxes for one tenant and reports how
// many were removed.
func ReapOrphanSandboxes(ctx context.Context, deps OrphanReaperDeps) (int, error) {
	if deps.Client == nil {
		return 0, fmt.Errorf("sandbox: orphan reaper requires a client")
	}
	now := deps.Now
	if now == nil {
		now = time.Now
	}

	summaries, err := deps.Client.List(ctx, configSandboxFilter(deps.TenantID, deps.ConfigID))
	if err != nil {
		return 0, fmt.Errorf(
			"sandbox: list workspace %d config %q sandboxes: %w",
			deps.TenantID, NormalizeConfigID(deps.ConfigID), err)
	}

	cutoff := now().Add(-deps.Grace)
	deleted := 0
	for _, summary := range summaries {
		if _, bound := deps.BoundIDs[summary.ID]; bound {
			continue

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Initialize deps.Client with the sandbox store client returned by the library's constructor before calling ReapOrphanSandboxes
  2. Guard the call site: skip or reschedule reaping when the client is not yet connected
  3. In tests, supply a fake/stub client in OrphanReaperDeps instead of a zero-value struct

Example fix

// before
deps := sandbox.OrphanReaperDeps{TenantID: 7, ConfigID: "cfg", Grace: time.Hour}
n, err := sandbox.ReapOrphanSandboxes(ctx, deps)
// after
deps := sandbox.OrphanReaperDeps{Client: client, TenantID: 7, ConfigID: "cfg", Grace: time.Hour}
n, err := sandbox.ReapOrphanSandboxes(ctx, deps)
Defensive patterns

Strategy: validation

Validate before calling

if deps.Client == nil {
    return errors.New("reaper deps missing client; skipping reap")
}
n, err := sandbox.ReapOrphanSandboxes(ctx, deps)

Type guard

func reaperReady(deps sandbox.OrphanReaperDeps) bool { return deps.Client != nil }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/d069a1f443defd3c. Report an issue: GitHub.