Expose orchestrator_lock_timeout in JsRuntimeOptions (Rust supports it, Node bindings do not)
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 72/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bien especificado
- Estado de actividad
- Tranquilo
- Stack tecnológico
- javascript, node.js, rust
Línea de trabajo
Localiza la definición de JsRuntimeOptions y el mapeo existente de workerLockTimeoutMs, y compáralo con RuntimeOptions en src/runtime/mod.rs. Sigue las pruebas existentes de opciones o las comprobaciones de build del binding; se considera terminado cuando los consumidores de Node puedan configurar orchestratorLockTimeoutMs y la validación del binding se supere.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
RuntimeOptions.orchestrator_lock_timeout is configurable in the Rust crate but is not surfaced through JsRuntimeOptions. Node consumers are pinned to the 5s default with no override.
The asymmetry is the tell: JsRuntimeOptions already exposes workerLockTimeoutMs → worker_lock_timeout, but its orchestrator twin was never mapped. This looks like an oversight rather than a deliberate omission.
Current state
Rust crate (duroxide 0.1.29, src/runtime/mod.rs):
/// Lock timeout for orchestrator queue items.
/// When an orchestration message is dequeued, it's locked for this duration.
/// Default: 5 seconds
pub orchestrator_lock_timeout: Duration,
Default Duration::from_secs(5), consumed in src/runtime/dispatchers/orchestration.rs, and explicitly covered by tests/lock_timeout_tests.rs (which exercises Duration::from_secs(10), from_secs(1), from_secs(60), …). The crate's own doc example even shows orchestrator_lock_timeout: Duration::from_secs(10).
Node bindings (duroxide npm 0.1.27) — JsRuntimeOptions exposes:
orchestrationConcurrency, workerConcurrency, dispatcherPollIntervalMs, workerLockTimeoutMs, logFormat, logLevel, serviceName, serviceVersion, maxSessionsPerRuntime, sessionIdleTimeoutMs, workerNodeId, workerTagFilter, workerTagFilterTags
Not mapped: orchestrator_lock_timeout, orchestrator_lock_renewal_buffer, worker_lock_renewal_buffer, dispatcher_long_poll_timeout, max_attempts, session_lock_timeout, session_lock_renewal_buffer, session_cleanup_interval, activity_cancellation_grace_period, unregistered_backoff, supported_replay_versions.
The only DUROXIDE_* environment variable in the native binary is DUROXIDE_PG_POOL_MAX, so there is no env-var escape hatch either.
Motivation: a 5-day production livelock with no available mitigation
A PilotSwarm deployment (8 worker pods × orchestrationConcurrency: 4 = 32 orchestration dispatchers against a single Postgres-backed queue) entered a self-sustaining livelock:
- Under contention,
fetch_orchestration_itembegan taking ~6.4s — longer than the 5sorchestrator_lock_timeout. - The lock token was therefore already expired by the time the turn tried to ack:
ack_orchestration_item: Invalid lock token. - Because the ack failed, retry bookkeeping never committed — including the poison-marking after
max_attempts. The runtime loggedOrchestration message exceeded max attempts, marking as poisonon every single pass, but the marking could never land. - With no backoff committed either, the row's lock simply expired after 5s and the message became visible again. The effective retry interval was pinned at exactly the lock lease.
- A permanently non-empty queue kept all 32 dispatchers in constant contention — which is precisely what made the fetch slow in step 1.
The loop is stable rather than self-correcting, because step 5 causes step 1.
Observed at the point of intervention:
| Metric | Value |
|---|---|
| Max attempts on a single instance | 78,987 (against max_attempts: 10) |
| Total accumulated attempts | 781,446 across 12 instances |
fetch_orchestration_item latency |
~6.4s, very tightly clustered |
| Effective throughput | zero |
| Duration before manual intervention | 5 days |
Latency clustered extremely tightly (6.40, 6.39, 6.39, 6.26, 6.27, 6.45s), which is the signature of queueing/serialization rather than scan cost.
Note that lock renewal cannot help here. The documented strategy renews at 0.5 × timeout when orchestrator_lock_timeout < 15s (so 2.5s at the default), but the 6.4s is spent inside fetch_orchestration_item — before any item exists to renew.
Recovery required manually deleting the affected orchestration instances out-of-band. With orchestrator_lock_timeout exposed, raising it to 10s would have let the ack — and therefore the poison-marking — commit, allowing the runtime's own poison handling to drain the queue without operator intervention.
Ask
Map orchestrator_lock_timeout into JsRuntimeOptions as orchestratorLockTimeoutMs, exactly as workerLockTimeoutMs already maps to worker_lock_timeout.
Ideally also expose orchestratorLockRenewalBufferMs and maxAttempts, which are part of the same tuning surface.
Secondary observation: default divergence
Two values differ notably between the Rust defaults and what Node consumers run, and they compound the contention that triggers this:
| Option | Rust default | Observed in Node consumers |
|---|---|---|
dispatcher_min_poll_interval |
100ms | 10ms |
orchestration_concurrency |
2 | 4 (× N pods) |
At 32 dispatchers polling every 10ms, that is up to ~3,200 fetch_orchestration_item calls/sec against one queue table.
It would be worth documenting the interaction explicitly: total dispatchers × poll frequency determines contention on the orchestrator queue, and orchestrator_lock_timeout must exceed worst-case fetch_orchestration_item latency under that load, or the system cannot make forward progress.
Related
The underlying reason a too-short lease is unrecoverable rather than merely slow — the poison-marking path depending on a lock token that has already expired — is filed separately against the Rust crate.
Environment
duroxide(npm) 0.1.27duroxide(crate) 0.1.29- Postgres-backed provider
- Lenguaje dominante
- JavaScript
- Estrellas
- 36
- Forks
- 20
- Merge medio
- 2 d 11 h
- PR fusionados (30 d)
- 4
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de microsoft/duroxide-node
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 28/100
microsoft/duroxide-node#16 ·
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
Todos los issues de microsoft/duroxide-node
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
HarperFast/skills#96 ·
-
[Block] Latest Posts [Type] Bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
-
Daemon passes --experimental-wasm-jspi unconditionally on Node >= 24; Node 26 rejects the flag Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Automattic/studio#4908 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
sugarlabs/musicblocks#8847 ·