Tencent/WeKnora · error

destroy remote sandbox for session: %w

Error message

destroy remote sandbox for session: %w

What it means

Destroy wraps any failure while removing the session's bound remote sandbox under the lifecycle lock — reading the binding, or destroyBindingLocked (provider destroy plus compare-delete of the binding). The library throws it so callers know cleanup did not complete; Destroy is intended to be idempotent for already-deleted sandboxes, so this error indicates a real failure (provider error, Redis error, or lock failure), not a missing sandbox.

Source

Thrown at internal/sandbox/session_lifecycle.go:139

// Destroy removes the bound remote sandbox and then compare-deletes its
// binding. It is idempotent for absent and already-deleted sandboxes.
func (l *remoteSessionLifecycle) Destroy(
	ctx context.Context,
	key SessionSandboxKey,
) error {
	if err := key.Validate(); err != nil {
		return err
	}
	err := l.bindings.WithLifecycleLock(ctx, key, func(lockCtx context.Context) error {
		binding, err := l.readBinding(lockCtx, key)
		if err != nil || binding == nil {
			return err
		}
		return l.destroyBindingLocked(lockCtx, key, *binding)
	})
	if err != nil {
		return fmt.Errorf("destroy remote sandbox for session: %w", err)
	}
	return nil
}

func (l *remoteSessionLifecycle) resolveLocked(
	ctx context.Context,
	key SessionSandboxKey,
) (RemoteSandboxHandle, error) {
	binding, err := l.readBinding(ctx, key)
	if err != nil {
		return nil, err
	}

	exists, err := l.sessionChecker.SessionExists(ctx, key)
	if err != nil {
		return nil, fmt.Errorf("check owning session: %w", err)
	}
	if !exists {

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Retry Destroy with backoff — it is idempotent; transient provider/Redis failures resolve on re-run.
  2. Unwrap the cause: if the provider destroy failed, check the provider console/API for the sandbox ID and delete it manually to avoid orphaned billing.
  3. Verify Redis/binding store health if the wrapped cause is a store error.
  4. If it is a lock timeout, retry later or investigate the node holding the lifecycle lock.

Example fix

// before
if err := lifecycle.Destroy(ctx, key); err != nil {
    log.Printf("destroy failed: %v", err)
}
// after
if err := lifecycle.Destroy(ctx, key); err != nil {
    log.Printf("destroy failed: %v; retrying", err)
    if retryErr := lifecycle.Destroy(ctxWithTimeout(30*time.Second), key); retryErr != nil {
        log.Printf("destroy still failing, orphan risk: %v", retryErr) // page/queue for manual cleanup
    }
}
Defensive patterns

Strategy: retry

Validate before calling

if err := key.Validate(); err != nil {
    return err
}
if err := rdb.Ping(ctx).Err(); err != nil {
    return fmt.Errorf("redis down; defer destroy: %w", err)
}

Try / catch

err := lifecycle.Destroy(ctx, key)
if err != nil {
    // Destroy is idempotent — retry with backoff before surfacing
    for i := 0; i < 3 && err != nil; i++ {
        time.Sleep(time.Duration(1<<i) * time.Second)
        err = lifecycle.Destroy(ctx, key)
    }
}
if err != nil {
    queueOrphanCleanup(key) // provider console sweep to avoid billed orphans
}

Prevention

When it happens

Trigger: remoteSessionLifecycle.Destroy is called for a session key and WithLifecycleLock fails, readBinding returns an error, or destroyBindingLocked fails (provider Destroy API error, or the binding compare-delete fails).

Common situations: Session teardown during shutdown while Redis is down; provider API outage leaving an orphaned sandbox still billed; lock contention with an in-flight resolve; context cancelled during cleanup.

Related errors


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