Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions sdks/sandbox/kotlin/sandbox/Module.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The Open Sandbox SDK provides a comprehensive interface for creating and managin
- **🔄 Lifecycle Management**: Create, pause, resume, terminate operations
- **💚 Health Monitoring**: Automatic readiness detection and status tracking
- **🏗️ Fluent API**: Type-safe builder pattern with DSL support
- **📦 Client-Side Sandbox Pool**: Idle-buffer pool for predictable acquire latency (opt-in, see [Sandbox Pool](#sandbox-pool))

## Quick Start

Expand Down Expand Up @@ -100,6 +101,34 @@ sandbox.commands.executeStreaming("long-running-task").collect { event ->
}
```

## Sandbox Pool

Optional client-side pool for acquiring ready sandboxes with lower latency. The pool maintains an idle buffer; use `SandboxPool.builder()` with `stateStore`, `connectionConfig`, and `creationSpec`, then `start()`, `acquire()`, and `shutdown()`.

```kotlin
import com.alibaba.opensandbox.sandbox.pool.SandboxPool
import com.alibaba.opensandbox.sandbox.domain.pool.PoolCreationSpec
import com.alibaba.opensandbox.sandbox.infrastructure.pool.InMemoryPoolStateStore

val pool = SandboxPool.builder()
.poolName("my-pool")
.ownerId("worker-1")
.maxIdle(5)
.stateStore(InMemoryPoolStateStore())
.connectionConfig(connectionConfig)
.creationSpec(PoolCreationSpec.builder().image("ubuntu:22.04").build())
.build()
pool.start()

val sandbox = pool.acquire(sandboxTimeout = Duration.ofMinutes(30))
try {
// use sandbox
} finally {
sandbox.kill()
}
pool.shutdown(graceful = true)
```

## Key Components

### Sandbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,42 @@ class InvalidArgumentException(
error = SandboxError(SandboxError.INVALID_ARGUMENT, message),
)

/**
* Thrown when acquire is called with FAIL_FAST policy and no idle sandbox is available.
*/
class PoolEmptyException(
message: String? = "No idle sandbox available and policy is FAIL_FAST",
cause: Throwable? = null,
) : SandboxException(
message = message,
cause = cause,
error = SandboxError(SandboxError.POOL_EMPTY, message),
)

/**
* Thrown when the pool state store is unavailable during idle take/put/lock operations.
*/
class PoolStateStoreUnavailableException(
message: String? = null,
cause: Throwable? = null,
) : SandboxException(
message = message,
cause = cause,
error = SandboxError(SandboxError.POOL_STATE_STORE_UNAVAILABLE, message),
)

/**
* Thrown when atomic take or lock-update conflicts occur in the state store.
*/
class PoolStateStoreContentionException(
message: String? = null,
cause: Throwable? = null,
) : SandboxException(
message = message,
cause = cause,
error = SandboxError(SandboxError.POOL_STATE_STORE_CONTENTION, message),
)

/**
* Defines standardized common error codes and messages for the Sandbox SDK.
*/
Expand All @@ -101,5 +137,14 @@ data class SandboxError(
const val UNHEALTHY = "UNHEALTHY"
const val INVALID_ARGUMENT = "INVALID_ARGUMENT"
const val UNEXPECTED_RESPONSE = "UNEXPECTED_RESPONSE"

/** Pool-specific: no idle sandbox and policy is FAIL_FAST. */
const val POOL_EMPTY = "POOL_EMPTY"

/** Pool state store unavailable during operations. */
const val POOL_STATE_STORE_UNAVAILABLE = "POOL_STATE_STORE_UNAVAILABLE"

/** Pool state store contention (atomic take or lock conflicts). */
const val POOL_STATE_STORE_CONTENTION = "POOL_STATE_STORE_CONTENTION"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.opensandbox.sandbox.domain.pool

import com.alibaba.opensandbox.sandbox.domain.exceptions.PoolEmptyException

/**
* Policy for acquire when the idle buffer is empty.
*
* - FAIL_FAST: throw [PoolEmptyException] (POOL_EMPTY).
* - DIRECT_CREATE: attempt direct create via lifecycle API, then connect and return.
*/
enum class AcquirePolicy {
/** When no idle sandbox is available, fail immediately with POOL_EMPTY. */
FAIL_FAST,

/** When no idle sandbox is available, create a new sandbox via lifecycle API. */
DIRECT_CREATE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.opensandbox.sandbox.domain.pool

/**
* Behavior when the idle buffer is empty at acquire time.
*
* - FAIL_FAST: throw POOL_EMPTY.
* - DIRECT_CREATE: attempt direct create (default).
*/
enum class EmptyBehavior {
/** Throw POOL_EMPTY when no idle sandbox is available. */
FAIL_FAST,

/** Create a new sandbox via lifecycle API when no idle is available. */
DIRECT_CREATE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright 2025 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.alibaba.opensandbox.sandbox.domain.pool

import com.alibaba.opensandbox.sandbox.config.ConnectionConfig
import kotlin.math.ceil

/**
* Configuration for a client-side sandbox pool.
*
* @property poolName User-defined name and namespace for this logical pool (required).
* @property ownerId Unique process identity for primary lock ownership (required in distributed mode).
* @property maxIdle Standby idle target/cap (required).
* @property warmupConcurrency Max concurrent creation workers during replenish (default: max(1, ceil(maxIdle * 0.2))).
* @property primaryLockTtl Lock TTL for distributed primary ownership (default: 60s).
* @property emptyBehavior Behavior when idle buffer is empty (default: DIRECT_CREATE).
* @property stateStore Injected [PoolStateStore] implementation (required).
* @property connectionConfig Connection config for lifecycle API (required).
* @property creationSpec Template for creating sandboxes (replenish and direct-create) (required).
* @property reconcileInterval Interval between reconcile ticks (default: 30s).
* @property degradedThreshold Consecutive create failures before transitioning to DEGRADED (default: 3).
* @property drainTimeout Max wait during graceful shutdown for in-flight ops (default: 30s).
*/
data class PoolConfig(
val poolName: String,
val ownerId: String,
val maxIdle: Int,
val warmupConcurrency: Int,
val primaryLockTtl: java.time.Duration,
val emptyBehavior: EmptyBehavior,
val stateStore: PoolStateStore,
val connectionConfig: ConnectionConfig,
val creationSpec: PoolCreationSpec,
val reconcileInterval: java.time.Duration,
val degradedThreshold: Int,
val drainTimeout: java.time.Duration,
) {
init {
require(poolName.isNotBlank()) { "poolName must not be blank" }
require(ownerId.isNotBlank()) { "ownerId must not be blank" }
require(maxIdle >= 0) { "maxIdle must be >= 0" }
require(warmupConcurrency > 0) { "warmupConcurrency must be positive" }
require(degradedThreshold > 0) { "degradedThreshold must be positive" }
require(!reconcileInterval.isNegative && !reconcileInterval.isZero) { "reconcileInterval must be positive" }
require(!primaryLockTtl.isNegative && !primaryLockTtl.isZero) { "primaryLockTtl must be positive" }
require(!drainTimeout.isNegative) { "drainTimeout must be non-negative" }
}

companion object {
private val DEFAULT_RECONCILE_INTERVAL = java.time.Duration.ofSeconds(30)
private val DEFAULT_PRIMARY_LOCK_TTL = java.time.Duration.ofSeconds(60)
private const val DEFAULT_DEGRADED_THRESHOLD = 3
private val DEFAULT_DRAIN_TIMEOUT = java.time.Duration.ofSeconds(30)

@JvmStatic
fun builder(): Builder = Builder()
}

class Builder {
private var poolName: String? = null
private var ownerId: String? = null
private var maxIdle: Int? = null
private var warmupConcurrency: Int? = null
private var primaryLockTtl: java.time.Duration = DEFAULT_PRIMARY_LOCK_TTL
private var emptyBehavior: EmptyBehavior = EmptyBehavior.DIRECT_CREATE
private var stateStore: PoolStateStore? = null
private var connectionConfig: ConnectionConfig? = null
private var creationSpec: PoolCreationSpec? = null
private var reconcileInterval: java.time.Duration = DEFAULT_RECONCILE_INTERVAL
private var degradedThreshold: Int = DEFAULT_DEGRADED_THRESHOLD
private var drainTimeout: java.time.Duration = DEFAULT_DRAIN_TIMEOUT

fun poolName(poolName: String): Builder {
this.poolName = poolName
return this
}

fun ownerId(ownerId: String): Builder {
this.ownerId = ownerId
return this
}

fun maxIdle(maxIdle: Int): Builder {
this.maxIdle = maxIdle
return this
}

fun warmupConcurrency(warmupConcurrency: Int): Builder {
this.warmupConcurrency = warmupConcurrency
return this
}

fun primaryLockTtl(primaryLockTtl: java.time.Duration): Builder {
this.primaryLockTtl = primaryLockTtl
return this
}

fun emptyBehavior(emptyBehavior: EmptyBehavior): Builder {
this.emptyBehavior = emptyBehavior
return this
}

fun stateStore(stateStore: PoolStateStore): Builder {
this.stateStore = stateStore
return this
}

fun connectionConfig(connectionConfig: ConnectionConfig): Builder {
this.connectionConfig = connectionConfig
return this
}

fun creationSpec(creationSpec: PoolCreationSpec): Builder {
this.creationSpec = creationSpec
return this
}

fun reconcileInterval(reconcileInterval: java.time.Duration): Builder {
this.reconcileInterval = reconcileInterval
return this
}

fun degradedThreshold(degradedThreshold: Int): Builder {
this.degradedThreshold = degradedThreshold
return this
}

fun drainTimeout(drainTimeout: java.time.Duration): Builder {
this.drainTimeout = drainTimeout
return this
}

fun build(): PoolConfig {
val name = poolName ?: throw IllegalArgumentException("poolName is required")
val owner = ownerId ?: throw IllegalArgumentException("ownerId is required")
val max = maxIdle ?: throw IllegalArgumentException("maxIdle is required")
val store = stateStore ?: throw IllegalArgumentException("stateStore is required")
val conn = connectionConfig ?: throw IllegalArgumentException("connectionConfig is required")
val spec = creationSpec ?: throw IllegalArgumentException("creationSpec is required")

val warmup = warmupConcurrency ?: ceil(max * 0.2).toInt().coerceAtLeast(1)

return PoolConfig(
poolName = name,
ownerId = owner,
maxIdle = max,
warmupConcurrency = warmup,
primaryLockTtl = primaryLockTtl,
emptyBehavior = emptyBehavior,
stateStore = store,
connectionConfig = conn,
creationSpec = spec,
reconcileInterval = reconcileInterval,
degradedThreshold = degradedThreshold,
drainTimeout = drainTimeout,
)
}
}
}
Loading
Loading