-
Notifications
You must be signed in to change notification settings - Fork 928
feat(sdks/kotlin): add acquireMinRemainingTtl to pool to skip near-ex… #986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
d921dfa
8a6e953
3f7dddf
4e4f12c
33ee6f3
a170c1a
8d97a64
e5026d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,12 @@ import kotlin.math.ceil | |
| * ready (default: 200ms). | ||
| * @property acquireHealthCheck Optional custom health check for sandboxes returned by acquire. | ||
| * @property acquireSkipHealthCheck When true, skip readiness checks for sandboxes returned by acquire (default: false). | ||
| * @property acquireMinRemainingTtl Minimum remaining TTL an idle sandbox must have to be returned by acquire. | ||
| * Idle entries closer to expiry than this threshold are discarded so the subsequent ready-check and | ||
| * any user-side renew have time to run before server-side expiry. Defaults to 60s, which covers the | ||
| * default [acquireReadyTimeout] of 30s with comfortable headroom. Must be strictly less than | ||
| * [idleTimeout] — a value at or above [idleTimeout] would reject every freshly warmed entry. Set to | ||
| * [Duration.ZERO] to opt out and restore the pre-existing binary-expiry behavior. | ||
| * @property warmupReadyTimeout Max time to wait for a pool-created sandbox to become ready (default: 30s). | ||
| * @property warmupHealthCheckPollingInterval Poll interval while waiting for a pool-created sandbox to become ready | ||
| * (default: 200ms). | ||
|
|
@@ -65,6 +71,7 @@ class PoolConfig private constructor( | |
| val acquireHealthCheckPollingInterval: Duration, | ||
| val acquireHealthCheck: ((Sandbox) -> Boolean)?, | ||
| val acquireSkipHealthCheck: Boolean, | ||
| val acquireMinRemainingTtl: Duration, | ||
| val warmupReadyTimeout: Duration, | ||
| val warmupHealthCheckPollingInterval: Duration, | ||
| val warmupHealthCheck: ((Sandbox) -> Boolean)?, | ||
|
|
@@ -87,6 +94,11 @@ class PoolConfig private constructor( | |
| require(!acquireHealthCheckPollingInterval.isNegative && !acquireHealthCheckPollingInterval.isZero) { | ||
| "acquireHealthCheckPollingInterval must be positive" | ||
| } | ||
| require(!acquireMinRemainingTtl.isNegative) { "acquireMinRemainingTtl must be non-negative" } | ||
| require(acquireMinRemainingTtl < idleTimeout) { | ||
| "acquireMinRemainingTtl ($acquireMinRemainingTtl) must be strictly less than " + | ||
| "idleTimeout ($idleTimeout); otherwise every warmed idle entry would be rejected" | ||
| } | ||
| require(!warmupReadyTimeout.isNegative && !warmupReadyTimeout.isZero) { "warmupReadyTimeout must be positive" } | ||
| require(!warmupHealthCheckPollingInterval.isNegative && !warmupHealthCheckPollingInterval.isZero) { | ||
| "warmupHealthCheckPollingInterval must be positive" | ||
|
|
@@ -101,6 +113,7 @@ class PoolConfig private constructor( | |
| private const val DEFAULT_DEGRADED_THRESHOLD = 3 | ||
| private val DEFAULT_ACQUIRE_READY_TIMEOUT = Duration.ofSeconds(30) | ||
| private val DEFAULT_ACQUIRE_HEALTH_CHECK_POLLING_INTERVAL = Duration.ofMillis(200) | ||
| private val DEFAULT_ACQUIRE_MIN_REMAINING_TTL: Duration = Duration.ofSeconds(60) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When callers keep the new default but already configure Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right — addressed in 4e4f12c. The default is now derived from
New tests |
||
| private val DEFAULT_WARMUP_READY_TIMEOUT = Duration.ofSeconds(30) | ||
| private val DEFAULT_WARMUP_HEALTH_CHECK_POLLING_INTERVAL = Duration.ofMillis(200) | ||
| private val DEFAULT_IDLE_TIMEOUT = Duration.ofHours(24) | ||
|
|
@@ -126,6 +139,7 @@ class PoolConfig private constructor( | |
| acquireHealthCheckPollingInterval = acquireHealthCheckPollingInterval, | ||
| acquireHealthCheck = acquireHealthCheck, | ||
| acquireSkipHealthCheck = acquireSkipHealthCheck, | ||
| acquireMinRemainingTtl = acquireMinRemainingTtl, | ||
| warmupReadyTimeout = warmupReadyTimeout, | ||
| warmupHealthCheckPollingInterval = warmupHealthCheckPollingInterval, | ||
| warmupHealthCheck = warmupHealthCheck, | ||
|
|
@@ -151,6 +165,7 @@ class PoolConfig private constructor( | |
| private var acquireHealthCheckPollingInterval: Duration = DEFAULT_ACQUIRE_HEALTH_CHECK_POLLING_INTERVAL | ||
| private var acquireHealthCheck: ((Sandbox) -> Boolean)? = null | ||
| private var acquireSkipHealthCheck: Boolean = false | ||
| private var acquireMinRemainingTtl: Duration = DEFAULT_ACQUIRE_MIN_REMAINING_TTL | ||
| private var warmupReadyTimeout: Duration = DEFAULT_WARMUP_READY_TIMEOUT | ||
| private var warmupHealthCheckPollingInterval: Duration = DEFAULT_WARMUP_HEALTH_CHECK_POLLING_INTERVAL | ||
| private var warmupHealthCheck: ((Sandbox) -> Boolean)? = null | ||
|
|
@@ -229,6 +244,19 @@ class PoolConfig private constructor( | |
| return this | ||
| } | ||
|
|
||
| /** | ||
| * Sets the minimum remaining TTL an idle sandbox must have to be returned by acquire. | ||
| * Idle entries closer to expiry than [acquireMinRemainingTtl] are discarded so the subsequent | ||
| * ready-check and any user-side renew have time to run before the server-side expiry kicks in. | ||
| * | ||
| * Must be non-negative and strictly less than [idleTimeout]. Defaults to 60s. Set to | ||
| * [Duration.ZERO] to opt out and restore the pre-existing binary-expiry behavior. | ||
| */ | ||
| fun acquireMinRemainingTtl(acquireMinRemainingTtl: Duration): Builder { | ||
| this.acquireMinRemainingTtl = acquireMinRemainingTtl | ||
| return this | ||
| } | ||
|
|
||
| fun warmupReadyTimeout(warmupReadyTimeout: Duration): Builder { | ||
| this.warmupReadyTimeout = warmupReadyTimeout | ||
| return this | ||
|
|
@@ -293,6 +321,7 @@ class PoolConfig private constructor( | |
| acquireHealthCheckPollingInterval = acquireHealthCheckPollingInterval, | ||
| acquireHealthCheck = acquireHealthCheck, | ||
| acquireSkipHealthCheck = acquireSkipHealthCheck, | ||
| acquireMinRemainingTtl = acquireMinRemainingTtl, | ||
| warmupReadyTimeout = warmupReadyTimeout, | ||
| warmupHealthCheckPollingInterval = warmupHealthCheckPollingInterval, | ||
| warmupHealthCheck = warmupHealthCheck, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,7 +72,7 @@ internal object PoolReconciler { | |
| val ttl = config.primaryLockTtl | ||
| val now = Instant.now() | ||
|
|
||
| stateStore.reapExpiredIdle(poolName, now) | ||
| stateStore.reapExpiredIdle(poolName, now, config.acquireMinRemainingTtl) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same root cause as the acquire-path leak — addressed in 4e4f12c. The Redis Lua script returns the alive list explicitly so the round-trip survives. New tests cover |
||
| val counters = stateStore.snapshotCounters(poolName) | ||
| val excess = (counters.idleCount - config.maxIdle).coerceAtLeast(0) | ||
| val toRemove = minOf(excess, config.warmupConcurrency) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -209,7 +209,7 @@ class SandboxPool internal constructor( | |
| throw PoolNotRunningException("Cannot acquire when pool state is $state") | ||
| } | ||
| val poolName = config.poolName | ||
| val sandboxId = stateStore.tryTakeIdle(poolName) | ||
| val sandboxId = stateStore.tryTakeIdle(poolName, config.acquireMinRemainingTtl) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When callers construct pools via Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — addressed in 8a6e953. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In pools where sandbox creation/readiness or Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Real issue, thanks for flagging. The store stamps Fixing this correctly means reading server-truth expiry (e.g. via The current 60s default still tolerates warmups well below a minute — which is the common case for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an acquire runs with a positive Useful? React with 👍 / 👎.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Real bug, addressed in 4e4f12c. The store API now surfaces the discarded-alive IDs to the caller:
Verified by new unit tests in both Kotlin ( |
||
| var noIdleReason: String? = null // null = got a sandbox from idle; non-null = reason we have no usable idle | ||
| var idleConnectFailure: Exception? = null | ||
| if (sandboxId != null) { | ||
|
|
@@ -724,6 +724,11 @@ class SandboxPool internal constructor( | |
| return this | ||
| } | ||
|
|
||
| fun acquireMinRemainingTtl(acquireMinRemainingTtl: Duration): Builder { | ||
| configBuilder.acquireMinRemainingTtl(acquireMinRemainingTtl) | ||
| return this | ||
| } | ||
|
|
||
| fun warmupReadyTimeout(warmupReadyTimeout: Duration): Builder { | ||
| configBuilder.warmupReadyTimeout(warmupReadyTimeout) | ||
| return this | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
acquireMinRemainingTtlis configured greater than or equal toidleTimeout, every warmed idle entry will fail the store's strictexpiresAt > now + minRemainingTtlcheck, so each acquire drains the pool's idle IDs, leaves the live sandboxes to expire server-side, and forces direct creation while reconcile keeps replenishing. Please validate this value againstidleTimeout(for example requiring it to be less) so a single config mistake cannot make the pool permanently discard all warm sandboxes.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 8a6e953.
PoolConfig.build()now rejectsacquireMinRemainingTtl >= idleTimeoutwith a descriptive message (and the same guard exists inPoolConfig.__post_init__on the Python side). NewPoolConfigTest.build rejects acquireMinRemainingTtl greater than or equal to idleTimeoutand a strict-less-than boundary test cover both sides.