Skip to content

Feat/go sdk pool#1198

Open
jianpingpei wants to merge 7 commits into
opensandbox-group:mainfrom
jianpingpei:feat/go-sdk-pool
Open

Feat/go sdk pool#1198
jianpingpei wants to merge 7 commits into
opensandbox-group:mainfrom
jianpingpei:feat/go-sdk-pool

Conversation

@jianpingpei

Copy link
Copy Markdown
Contributor

feat(sdk/go): add client-side sandbox pool (OSEP-0005)

Summary

Implement OSEP-0005 client-side sandbox pool for the Go SDK, enabling millisecond-level sandbox acquisition through pre-warmed idle instances. Fully aligned with the existing Python and Kotlin SDK pool implementations.

Features

  • SandboxPool interface with DefaultSandboxPool implementation
  • PoolBuilder with fluent API, validation, and sensible defaults
  • Acquire with DirectCreate (default) / FailFast policies
  • Background reconcile loop with leader election, fill-deficit, and shrink-excess
  • Exponential backoff on consecutive warmup failures (base 30s, max 24h)
  • InMemoryPoolStateStore for single-process deployments
  • RedisPoolStateStore with Lua scripts for atomic distributed operations
  • TryTakeIdleWithMinTTL / ReapExpiredIdleWithMinTTL for near-expiry-aware idle management
  • Custom PooledSandboxCreator and WarmupSandboxPreparer callbacks
  • Separate warmup / acquire health check and timeout configurations
  • Resize, ReleaseAllIdle, Snapshot, SnapshotIdleEntries operations
  • Graceful and non-graceful shutdown with configurable drain timeout
  • Store error graceful degradation: DirectCreate policy falls through to direct create on store outage
  • Structured logging via slog.Logger

Cross-language alignment

The implementation is fully aligned with the Python (pool_async.py, pool_redis.py) and Kotlin (SandboxPool.kt, RedisPoolStateStore.kt) SDKs in:

  • Public API surface (start, acquire, resize, releaseAllIdle, snapshot, shutdown)
  • PoolStateStore interface (including TryTakeIdleWithMinTTL, SetMaxIdle, SetIdleEntryTTL)
  • TakeIdleResult with DiscardedAliveSandboxIDs for near-expiry cleanup
  • Reconcile loop phases (leader gate, reap, snapshot, shrink, fill)
  • Lock renewal before each PutIdle in reconcile
  • Post-warmup Renew to compensate for creation time
  • ReconcileState with degraded threshold and exponential backoff
  • Configuration defaults (30s reconcile interval, 60s lock TTL, 24h idle timeout, etc.)
  • Error types (PoolEmptyError, PoolAcquireFailedError, PoolNotRunningError, PoolStateStoreUnavailableError, PoolStateStoreContentionError)

File structure

sdks/sandbox/go/
  pool.go                   (593 lines)  - Pool interface, DefaultSandboxPool, Acquire, Shutdown
  pool_types.go             (221 lines)  - Enums, config structs, type definitions
  pool_builder.go           (295 lines)  - Builder pattern with defaults and validation
  pool_errors.go            (78 lines)   - Pool-specific error types
  pool_store.go             (71 lines)   - PoolStateStore interface
  pool_store_memory.go      (383 lines)  - InMemoryPoolStateStore
  pool_store_redis.go       (471 lines)  - RedisPoolStateStore (Lua scripts)
  pool_reconciler.go        (295 lines)  - Reconcile loop, backoff, leader election
  pool_test.go              (1065 lines) - Pool unit tests
  pool_store_memory_test.go (507 lines)  - InMemory store contract tests
  pool_store_redis_test.go  (500 lines)  - Redis store integration tests
  pool_reconciler_test.go   (386 lines)  - Reconciler unit tests

tests/go/
  pool_e2e_test.go          (916 lines)  - E2E tests (InMemory + Redis)
  base_e2e_test.go          (238 lines)  - Shared e2e helpers

Closes #1166

Testing

  • Not run (explain why)
  • Unit tests
  • Integration tests
  • e2e / manual verification

Test coverage:

Test suite Count Status
Pool unit tests (pool_test.go) 30 tests Pass
InMemory store contract tests (pool_store_memory_test.go) 12 tests Pass
Redis store integration tests (pool_store_redis_test.go, -tags integration) 10 tests Pass
Reconciler unit tests (pool_reconciler_test.go) 10 tests Pass
E2E tests - InMemory (tests/go/pool_e2e_test.go) 11 tests Pass
E2E tests - Redis (tests/go/pool_e2e_test.go) 8 tests Pass

All existing Go SDK and e2e tests continue to pass (go test ./...).

Breaking Changes

  • None
  • Yes (describe impact and migration path)

Pool is a new opt-in API. Existing SDK usage is completely unaffected.

Checklist

  • Linked Issue or clearly described motivation
  • Added/updated docs (if needed)
  • Added/updated tests (if needed)
  • Security impact considered
  • Backward compatibility considered

jianpingpei and others added 7 commits July 6, 2026 15:14
Implement OSEP-0005 client-side sandbox pool with:
- SandboxPool interface + DefaultSandboxPool (acquire/release/resize/shutdown)
- PoolStateStore abstraction with InMemory and Redis implementations
- Reconcile loop with leader election, exponential backoff, concurrent warmup
- Builder pattern with validation and sensible defaults
- 48 unit tests (including race-safe concurrency tests)

Closes opensandbox-group#1166

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ython

- Add missing Renew call in createOneSandbox when using SandboxCreator
  path, so idle TTL is properly reset after warmup (matches Kotlin/Python)
- Kill orphaned sandbox when Renew fails in directCreate, preventing
  server-side resource leak
- Add primaryLockTTL vs warmupReadyTimeout warning on Start (parity with
  Kotlin/Python safety check)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align Go SDK sandbox pool with Kotlin and Python reference
implementations per OSEP-0005 review (issue opensandbox-group#1166):

1. Shutdown no longer releases idle sandboxes. In distributed mode,
   killing shared idle entries on one node's shutdown could disrupt
   other nodes. Users can still call ReleaseAllIdle() explicitly.

2. Add defer sb.Close() after warmup sandbox creation so only the
   sandbox ID is retained, matching Kotlin's finally { sandbox.close() }
   and Python's finally: await sandbox.close(). Prevents future resource
   leaks when transport pooling is added.

3. Add sb.Close() on renew failure in directCreate, matching
   Kotlin/Python kill-then-close cleanup pattern.

4. Allow Start() after Shutdown() (restart from STOPPED state),
   matching Kotlin/Python behavior. Previously Go returned
   PoolNotRunningError; now users can reuse pool instances.

Also includes infrastructure improvements:
- Structured logger support via PoolLogger builder method
- Reconciler accepts logger parameter for structured logging
- Atomic RemoveIdle via Lua script in Redis store
- InMemoryStore queue compaction to prevent unbounded growth
- Reconciler shrink-excess-idle phase with leader lock renewal
…rent lifecycle bugs

- Split PoolStateStoreError into PoolStateStoreUnavailableError and
  PoolStateStoreContentionError to align with OSEP-0005 error matrix
  and Python/Kotlin SDKs.
- Export PrimaryLockKey on RedisPoolStateStore for e2e test access.
- Fix reconcile tick using ReconcileInterval as context timeout; use
  context.Background() instead (tick operations have their own
  timeouts via WarmupReadyTimeout).
- Fix immediate first tick not syncing healthState back to pool struct;
  extract syncHealthState() helper called by both paths.
- Fix concurrent Start/Shutdown panic from double-closing done channel;
  add doneClosed guard flag.
- Fix concurrent Start/Shutdown deadlock from WaitGroup reuse; add
  shutdownDone channel so Start waits for previous Shutdown to fully
  complete before creating new goroutines.
Add 17 pool e2e tests (9 single-node InMemory + 8 Redis distributed)
covering warmup, acquire policies, stale idle fallback, lifecycle
idempotency, concurrent acquire/shutdown, warmup preparer, warmup
concurrency, broken connection degradation, cross-node acquire,
primary lock failover, shared resize, atomic take contention, idle
TTL expiry, and resize jitter convergence.

Add pool helper functions to base_e2e_test.go: createTestPool,
eventually, cleanupPool, cleanupBorrowed, cleanupTaggedSandboxes,
countTaggedSandboxes, brokenConnectionConfig, etc.

Redis tests are gated by OPENSANDBOX_TEST_REDIS_URL env var and
automatically skipped when not set.
…te policy

When the state store is unavailable (e.g., Redis outage), Acquire now
falls through to direct sandbox creation under the DirectCreate policy
instead of returning a PoolStateStoreUnavailableError. This keeps the
pool at least as available as raw SDK usage during store outages, per
the OSEP-0005 error-code action matrix.

Under FailFast policy, the store error is still propagated immediately.

Added two tests:
- TestPool_Acquire_StoreError_DirectCreate_FallsThrough
- TestPool_Acquire_StoreError_FailFast_ReturnsError
Add client-side pool usage documentation aligned with the existing
Python and Kotlin SDK docs pages, covering:
- InMemory pool quick start with builder API
- Redis distributed deployment example
- Pool lifecycle semantics (info box)
- Distributed deployment notes (tip box)
@github-actions github-actions Bot added documentation Improvements or additions to documentation sdk/go sdks labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b5226d705

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread sdks/sandbox/go/go.mod
module github.com/alibaba/OpenSandbox/sdks/sandbox/go

go 1.20
go 1.24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the Go SDK on the CI-supported toolchain

The SDK workflow I checked sets up Go 1.20 and runs go vet/go test in sdks/sandbox/go (.github/workflows/sdk-tests.yml:373-396), but this module now declares Go 1.24 and the new pool files also import log/slog, which is unavailable to that Go 1.20 job. In that CI and documented go 1.20+ install context, the Go SDK no longer builds unless the workflow/docs/support policy are updated together or the new code stays within the supported toolchain.

Useful? React with 👍 / 👎.

Comment thread sdks/sandbox/go/pool.go
return nil, fmt.Errorf("opensandbox: pool direct create: renew failed: %w", err)
}
}
return sb, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply acquire health checks to direct-created sandboxes

When the pool is empty or the store falls back to DirectCreate, this path returns the sandbox after creation and optional renewal without running p.config.AcquireHealthCheck, even though the idle-acquire path does. For pools that configure an app-specific acquire check and do not set SkipHealthCheck, cold starts or store outages can hand callers a sandbox before that app-specific readiness condition is satisfied; run the same acquire check before returning here.

Useful? React with 👍 / 👎.

Comment thread sdks/sandbox/go/pool.go
Timeout: p.config.AcquireReadyTimeout,
PollingInterval: p.config.AcquireHealthCheckPollingInterval,
}
sb, err := ConnectSandbox(ctx, p.config.ConnectionConfig, sandboxID, readyOpts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor SkipHealthCheck when taking an idle sandbox

When AcquireOptions.SkipHealthCheck is true for an idle sandbox, this still passes ReadyOptions into ConnectSandbox; ConnectSandbox calls WaitUntilReady whenever options are supplied, so the idle path still performs the execd readiness check while the direct-create path skips it. Callers using SkipHealthCheck to avoid readiness polling can still fail or block on idle acquisition, so skip passing ready options in that scenario.

Useful? React with 👍 / 👎.


// WarmupConcurrency sets the maximum number of sandboxes created per reconcile tick.
func (b *SandboxPoolBuilder) WarmupConcurrency(n int) *SandboxPoolBuilder {
b.config.WarmupConcurrency = n

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject negative warmup concurrency

WarmupConcurrency(-1) is accepted by Build because only zero gets defaulted; on a later reconcile with a deficit, toCreate := min(deficit, cfg.WarmupConcurrency) becomes negative and make([]createResult, toCreate) panics, taking down the process. Validate n > 0 or normalize non-positive values before the pool can start.

Useful? React with 👍 / 👎.

Comment thread sdks/sandbox/go/pool.go
Volumes: spec.Volumes,
Extensions: spec.Extensions,
Platform: spec.Platform,
ManualCleanup: spec.ManualCleanup,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject ManualCleanup for pooled sandboxes

If PoolCreationSpec.ManualCleanup is true, CreateSandbox omits the runtime TTL while the pool store still expires the idle record; after the pool is down longer than IdleTimeout or misses the near-expiry reap window, the expired record is dropped without returning an ID to kill, leaving an untracked sandbox running indefinitely. Reject this option for pool-created sandboxes or avoid forwarding it here.

Useful? React with 👍 / 👎.

Comment on lines +251 to +252
if b.config.IdleTimeout == 0 {
b.config.IdleTimeout = DefaultIdleTimeout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject non-positive idle timeouts

Only the zero value is defaulted here, so IdleTimeout(-1 * time.Hour) is accepted and later produces a negative sandbox timeout plus already-expired in-memory idle entries (Redis clamps the store TTL to 1ms). Validate that the post-default IdleTimeout is positive before initializing the store or deriving AcquireMinRemainingTTL.

Useful? React with 👍 / 👎.

Comment thread sdks/sandbox/go/pool.go
// Do not use ReconcileInterval as context timeout — the interval
// controls how often ticks fire, not how long each tick may run.
// Sandbox creation has its own timeouts (WarmupReadyTimeout).
p.runReconcileTick(context.Background())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel in-flight reconcile work on non-graceful shutdown

Reconcile ticks are run with context.Background(), while Shutdown(false) only closes done and then waits on the worker group; if a warmup create or preparer is blocked until a long WarmupReadyTimeout, non-graceful shutdown still blocks for that work instead of returning promptly. Use a cancellable reconcile context and cancel it from the non-graceful shutdown path.

Useful? React with 👍 / 👎.

Comment on lines +122 to +124
if err != nil {
logger.Warn("reconcile: lock acquire error", slog.String("pool_name", poolName), slog.Any("error", err))
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark the pool degraded when the state store fails

When the Redis/state store is unavailable during lock acquisition, this path logs and returns without calling state.recordFailure, so Snapshot().HealthState can remain HEALTHY forever even though the pool cannot reconcile or warm any idle sandboxes. Record these store errors as reconcile failures so operators can detect the degraded pool state.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation sdk/go sdks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support client-side sandbox pool in Go SDK

1 participant