Expose orchestrator_lock_timeout in JsRuntimeOptions (Rust supports it, Node bindings do not)

Đang mở Phù hợp với người mới
#11 3 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

Đánh giá

Độ khó
2/5
Thời gian dự kiến
1-3 giờ
Mức phù hợp với người mới
72/100
Loại issue
Tính năng
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Ít trao đổi
Công nghệ
javascript, node.js, rust
Lĩnh vực
api, backend

Hướng nghiên cứu

Tìm định nghĩa của JsRuntimeOptions và ánh xạ workerLockTimeoutMs hiện có, sau đó so sánh nó với RuntimeOptions trong src/runtime/mod.rs. Truy vết các bài kiểm thử tùy chọn hiện có hoặc các bước kiểm tra build của binding; công việc được xem là hoàn tất khi các consumer Node có thể cấu hình orchestratorLockTimeoutMs và quá trình validation của binding đạt.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

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 workerLockTimeoutMsworker_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:

  1. Under contention, fetch_orchestration_item began taking ~6.4s — longer than the 5s orchestrator_lock_timeout.
  2. The lock token was therefore already expired by the time the turn tried to ack: ack_orchestration_item: Invalid lock token.
  3. Because the ack failed, retry bookkeeping never committed — including the poison-marking after max_attempts. The runtime logged Orchestration message exceeded max attempts, marking as poison on every single pass, but the marking could never land.
  4. 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.
  5. 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.27
  • duroxide (crate) 0.1.29
  • Postgres-backed provider
Ngôn ngữ chính
JavaScript
Star
36
Fork
20
Merge trung bình
2 ngày 11 giờ
Pull request đã merge (30 ngày)
4

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của microsoft/duroxide-node

Tất cả issue của microsoft/duroxide-node

Issue tương tự

Thêm issue về JavaScript

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.