Repository navigation
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds Kubernetes Lease-based leader election with configurable timing, HTTPS trust, Lease state helpers, acquisition and renewal lifecycle management, environment-based construction, release handling, leadership events, and unit tests. ChangesLeader Election Utility
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant LeaderElector
participant KubernetesLeaseAPI
participant LeadershipEvents
LeaderElector->>KubernetesLeaseAPI: Read Lease state
KubernetesLeaseAPI-->>LeaderElector: Return holder and timing data
LeaderElector->>KubernetesLeaseAPI: Acquire or renew Lease
KubernetesLeaseAPI-->>LeaderElector: Return update result
LeaderElector-->>LeadershipEvents: Emit leadership acquired or lost
LeaderElector->>KubernetesLeaseAPI: Release by clearing holderIdentity
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
67a3bb4 to
abe5827
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/test/unit_tests/util_functions_tests/test_core_lease.js`:
- Around line 8-9: The KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT
environment variable assignments on lines 8-9 are mutating global process state
at module load time without restoration, causing test order dependencies. Move
these assignments into individual test setup/teardown (using
beforeEach/afterEach hooks) so that each test can independently control these
environment variables and restore them to their original state after the test
completes, preventing state leakage between tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3a9bf049-b096-4333-ace6-efbeac347bcc
📒 Files selected for processing (6)
config.jssrc/cmd/core_init.jssrc/test/unit_tests/internal/test_core_init.jssrc/test/unit_tests/util_functions_tests/test_core_lease.jssrc/test/utils/index/index.jssrc/util/core_lease_utils.js
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/util/core_lease_utils.js (1)
191-207: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
acquire_leaseretries indefinitely with no overall timeout.If the API consistently returns non-200 on PUT (e.g., RBAC denial or a 5xx outage), this loops forever, silently blocking core startup with no surfaced failure. Consider an upper bound or at least periodic error-level logging so a stuck pod is observable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util/core_lease_utils.js` around lines 191 - 207, The acquire_lease method loops indefinitely without any timeout or maximum retry limit, which can cause silent blocking of core startup if the API consistently returns non-200 status codes. Add a timeout mechanism to the acquire_lease method by tracking the start time of the acquisition attempt and comparing it against a configured maximum timeout duration (similar to how config.CORE_LEASE_ACQUIRE_RETRY_MS is used). When the timeout is exceeded, throw an error with a descriptive message indicating the lease acquisition failed due to timeout. Optionally, add periodic error-level logging after each failed attempt to make stuck pods observable before the timeout is reached.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cmd/core_init.js`:
- Around line 178-196: The issue is that if run_server_upgrade() throws an
error, the start() function exits without releasing the lease held by
_lease_client, causing the renewal loop from start_core_lease_renewal() to
continue and delaying failover. To fix this, wrap the code starting from after
await _lease_client.acquire_lease() through the run_supervisord() call in a
try/finally block, ensuring that stop_serving_and_release_lease() is called in
the finally clause whenever a lease was acquired, regardless of whether
run_server_upgrade() or any subsequent operation succeeds or fails.
In `@src/util/core_lease_utils.js`:
- Around line 330-349: The 409 conflict handling in the lease renewal loop uses
a continue statement that skips the _interruptible_sleep at the end of the loop,
causing a busy-loop without backoff that hammers the API server. Add an
_interruptible_sleep call with an appropriate retry delay before the continue
statement in the 409 conflict handling block (around line 342). Apply the same
fix to the release_lease method around line 254 where the same unthrottled-retry
pattern exists.
- Around line 116-133: In the _request method, the options object passed to
make_https_request contains rejectUnauthorized: false which disables TLS
certificate verification. Remove this line entirely to enable certificate
validation by default, allowing the make_https_request function to use the CA
certificates from INTERNAL_CA_CERTS or EXTERNAL_CA_CERTS paths automatically.
This will ensure the service account bearer token is protected by proper TLS
verification when communicating with the Kubernetes API server.
---
Nitpick comments:
In `@src/util/core_lease_utils.js`:
- Around line 191-207: The acquire_lease method loops indefinitely without any
timeout or maximum retry limit, which can cause silent blocking of core startup
if the API consistently returns non-200 status codes. Add a timeout mechanism to
the acquire_lease method by tracking the start time of the acquisition attempt
and comparing it against a configured maximum timeout duration (similar to how
config.CORE_LEASE_ACQUIRE_RETRY_MS is used). When the timeout is exceeded, throw
an error with a descriptive message indicating the lease acquisition failed due
to timeout. Optionally, add periodic error-level logging after each failed
attempt to make stuck pods observable before the timeout is reached.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d6d6bc77-5cf0-4299-b55b-3b9c3109a3ca
📒 Files selected for processing (6)
config.jssrc/cmd/core_init.jssrc/test/unit_tests/internal/test_core_init.jssrc/test/unit_tests/util_functions_tests/test_core_lease.jssrc/test/utils/index/index.jssrc/util/core_lease_utils.js
✅ Files skipped from review due to trivial changes (1)
- config.js
🚧 Files skipped from review as they are similar to previous changes (1)
- src/test/utils/index/index.js
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/test/unit_tests/util_functions_tests/test_core_lease.js`:
- Around line 35-49: Clear the cached lease module after restoring the
Kubernetes environment variables in the test setup/teardown around lease_utils.
The issue is that core_lease_utils reads KUBERNETES_SERVICE_HOST and
KUBERNETES_SERVICE_PORT at import time, so resetting process.env alone still
leaves the cached module using test values. Update the mocha.after block in
test_core_lease.js to restore the env first and then delete
require.cache[LEASE_UTILS], so later tests re-require core_lease_utils with the
real environment.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 635ef350-cb80-43c8-9d7e-1f3f6fa14ea6
📒 Files selected for processing (6)
config.jssrc/cmd/core_init.jssrc/test/unit_tests/internal/test_core_init.jssrc/test/unit_tests/util_functions_tests/test_core_lease.jssrc/test/utils/index/index.jssrc/util/core_lease_utils.js
✅ Files skipped from review due to trivial changes (1)
- src/test/utils/index/index.js
🚧 Files skipped from review as they are similar to previous changes (1)
- config.js
3a99178 to
32e842f
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cmd/core_init.js`:
- Around line 74-80: The shutdown fallback in kill_supervisord is using
proc.killed as the guard, but that flag is set as soon as SIGTERM is sent, not
when supervisord actually exits. Update the guard to track real child
termination state via the proc close/exit events (or an equivalent explicit
closed flag) so the SIGKILL fallback still runs if the process remains alive;
keep the change localized to kill_supervisord and the shutdown path that sends
SIGTERM.
In `@src/test/unit_tests/internal/test_core_init.js`:
- Around line 65-68: The supervisord child stub in the test setup does not mimic
real ChildProcess kill() behavior, so branches that check
_supervisord_proc.killed can be wrong. Update the child stub in the test around
the EventEmitter/kill setup so that child.kill() returns true and also flips
child.killed to true, keeping the stub behavior aligned with the code paths
exercised by test_core_init and any logic relying on the killed flag.
In `@src/util/core_lease_utils.js`:
- Around line 116-129: The Lease API request path in the _request method
currently calls make_https_request without any timeout, so stalled GET/PUT calls
can hang indefinitely and block lease renewal. Update _request in
core_lease_utils.js to pass an explicit timeout when invoking
make_https_request, or enforce the timeout inside that helper, and keep the
change scoped to the lease renewal request flow so the renew loop can fail fast
between deadline checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ded62949-6ad7-45c8-a7f0-1f5038b4ef74
📒 Files selected for processing (6)
config.jssrc/cmd/core_init.jssrc/test/unit_tests/internal/test_core_init.jssrc/test/unit_tests/util_functions_tests/test_core_lease.jssrc/test/utils/index/index.jssrc/util/core_lease_utils.js
✅ Files skipped from review due to trivial changes (1)
- src/test/utils/index/index.js
🚧 Files skipped from review as they are similar to previous changes (1)
- config.js
a23b23d to
75a9d4c
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cmd/core_init.js`:
- Around line 21-25: The shutdown flow in start(), run_supervisord(),
step_down_from_lease(), and handle_sigterm() can race so supervisord’s close
event returns before lease cleanup finishes. Add coordination for the active
shutdown path (for example, track a shutdown promise or equivalent) and have
start() await it before resolving. Make sure the lease release logic in
step_down_from_lease() and the SIGTERM handler completes before main() can
proceed to process.exit().
In `@src/test/unit_tests/internal/test_core_init.js`:
- Around line 344-356: The test for start_core_lease_renewal only verifies
process.exit on lost leadership, so it does not cover the release contract.
Update the existing test around core_init.start_core_lease_renewal and
step_down_from_lease to also assert that mock_client.release_lease is called
when renew_lease_loop invokes the on_lost callback, so the test fails if
leadership loss exits without releasing the lease.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ce3ab15b-698c-40ba-aed3-afc95509f72b
📒 Files selected for processing (6)
config.jssrc/cmd/core_init.jssrc/test/unit_tests/internal/test_core_init.jssrc/test/unit_tests/util_functions_tests/test_core_lease.jssrc/test/utils/index/index.jssrc/util/core_lease_utils.js
✅ Files skipped from review due to trivial changes (1)
- src/test/utils/index/index.js
🚧 Files skipped from review as they are similar to previous changes (1)
- config.js
55ff283 to
d287fea
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/test/unit_tests/util_functions_tests/test_leader_election.js (1)
266-276: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDeadline check still masks the "another holder" step-down path.
client._lease_duration_msstays at its default0here, unlike the sibling renew tests which explicitly set it. Inrenew_lease_loop, the deadline check runs beforeread_lease_state, and a0duration makes_is_renew_deadline_exceeded()true immediately — soLEADERSHIP_LOSTfires via the deadline branch, and the stubbedpod-b/can_take: falseresponse is never exercised. The test would still pass even if the "another holder" logic were broken.This was raised in a prior review pass and remains unaddressed at these exact lines.
💚 Proposed fix
mocha.it('steps down when another holder is observed', async function() { const client = make_test_client(); + client._lease_duration_ms = 60000; // large duration so the deadline check isn't the trigger sandbox.stub(client, 'read_lease_state').resolves({ lease: fresh_lease('pod-b'), can_take: false, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/unit_tests/util_functions_tests/test_leader_election.js` around lines 266 - 276, Update the “steps down when another holder is observed” test to configure client._lease_duration_ms to a positive duration before calling renew_lease_loop, ensuring the deadline check does not trigger first. Keep the stubbed read_lease_state response and LEADERSHIP_LOST assertion so the test specifically exercises the another-holder path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/util/leader_election.js`:
- Around line 141-158: Update the renewal deadline calculation and flow around
the GET/PUT requests in the leader-election renewal logic to reserve both
CORE_LEASE_REQUEST_TIMEOUT_MS intervals. Either subtract two request timeouts
from the deadline or recheck the deadline after the GET before issuing the PUT,
while preserving existing request behavior.
- Around line 228-270: Serialize the lifecycle in start(), stop(), and
acquire_lease() so shutdown cancels and awaits any pending acquisition before
releasing the lease. Track the acquisition task with a cancellation mechanism,
prevent stop() from returning until acquisition and renewal tasks are settled,
and ensure a cancelled acquisition cannot emit LEADERSHIP_ACQUIRED or start
renewal. Guard repeated start() calls so they cannot overwrite
_renew_loop_promise or launch concurrent lifecycle operations.
---
Duplicate comments:
In `@src/test/unit_tests/util_functions_tests/test_leader_election.js`:
- Around line 266-276: Update the “steps down when another holder is observed”
test to configure client._lease_duration_ms to a positive duration before
calling renew_lease_loop, ensuring the deadline check does not trigger first.
Keep the stubbed read_lease_state response and LEADERSHIP_LOST assertion so the
test specifically exercises the another-holder path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 22586430-120e-48bf-941b-c8275c8e7f59
📒 Files selected for processing (5)
config.jssrc/test/unit_tests/util_functions_tests/test_leader_election.jssrc/test/utils/index/index.jssrc/util/http_utils.jssrc/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (2)
- src/test/utils/index/index.js
- config.js
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/test/unit_tests/util_functions_tests/test_leader_election.js (1)
203-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract a shared helper for temporary
configoverrides.Five separate tests hand-roll the same save/mutate/try-finally-restore boilerplate against the shared
configsingleton, three of them duplicating an identical 3-variable save/restore block verbatim.
src/test/unit_tests/util_functions_tests/test_leader_election.js#L203-L218: replace the manualorig_retrysave/restore with a shared helper call.src/test/unit_tests/util_functions_tests/test_leader_election.js#L279-L304: replace the 3-variable save/restore block with the shared helper.src/test/unit_tests/util_functions_tests/test_leader_election.js#L306-L328: replace the identical 3-variable save/restore block with the shared helper.src/test/unit_tests/util_functions_tests/test_leader_election.js#L330-L350: replace the manualorig_retrysave/restore with the shared helper.src/test/unit_tests/util_functions_tests/test_leader_election.js#L409-L438: replace the 3-variable save/restore block with the shared helper.♻️ Proposed helper
/** * Temporarily overrides config values for the duration of `fn`, then restores them. * `@param` {Record<string, any>} overrides * `@param` {() => Promise<any>} fn */ async function with_config_overrides(overrides, fn) { const originals = {}; for (const key of Object.keys(overrides)) originals[key] = config[key]; Object.assign(config, overrides); try { await fn(); } finally { Object.assign(config, originals); } }Example usage replacing lines 279-304:
- client._lease_duration_ms = 100; - const orig_error_sleep = config.CORE_LEASE_RENEW_ERROR_SLEEP_MS; - const orig_request_timeout = config.CORE_LEASE_REQUEST_TIMEOUT_MS; - const orig_margin = config.LEASE_RENEW_DEADLINE_MARGIN_MS; - config.CORE_LEASE_RENEW_ERROR_SLEEP_MS = 10; - config.CORE_LEASE_REQUEST_TIMEOUT_MS = 10; - config.LEASE_RENEW_DEADLINE_MARGIN_MS = 10; - try { + client._lease_duration_ms = 100; + await with_config_overrides({ + CORE_LEASE_RENEW_ERROR_SLEEP_MS: 10, + CORE_LEASE_REQUEST_TIMEOUT_MS: 10, + LEASE_RENEW_DEADLINE_MARGIN_MS: 10, + }, async () => { sandbox.stub(client, 'read_lease_state').resolves({ lease: fresh_lease('pod-a'), can_take: true }); sandbox.stub(client, 'update_lease').resolves(500); const on_lost = sandbox.stub(); client.on(lease_utils.EVENTS.LEADERSHIP_LOST, on_lost); await client.renew_lease_loop(); assert.strictEqual(on_lost.calledOnce, true); - } finally { - config.CORE_LEASE_RENEW_ERROR_SLEEP_MS = orig_error_sleep; - config.CORE_LEASE_REQUEST_TIMEOUT_MS = orig_request_timeout; - config.LEASE_RENEW_DEADLINE_MARGIN_MS = orig_margin; - } + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/unit_tests/util_functions_tests/test_leader_election.js` around lines 203 - 218, Extract a shared with_config_overrides helper in test_leader_election.js that applies overrides and restores original config values in a finally block, then use it in the five affected tests: lines 203-218, 279-304, 306-328, 330-350, and 409-438. Replace each manual save/mutate/restore block while preserving each test’s existing overrides and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/test/unit_tests/util_functions_tests/test_leader_election.js`:
- Around line 203-218: Extract a shared with_config_overrides helper in
test_leader_election.js that applies overrides and restores original config
values in a finally block, then use it in the five affected tests: lines
203-218, 279-304, 306-328, 330-350, and 409-438. Replace each manual
save/mutate/restore block while preserving each test’s existing overrides and
assertions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 615a3bf8-3ceb-491a-b6f0-6a2a3d343303
📒 Files selected for processing (5)
config.jssrc/test/unit_tests/util_functions_tests/test_leader_election.jssrc/test/utils/index/index.jssrc/util/http_utils.jssrc/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (3)
- config.js
- src/util/http_utils.js
- src/test/utils/index/index.js
560a370 to
c23a891
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/test/unit_tests/util_functions_tests/test_leader_election.js (2)
178-184: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest name claims log verification but doesn't assert it.
The test title says "logs warning on non-200 status" but only asserts
put_stub.callCount === 1; no spy/stub verifies a warning was actually logged.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/unit_tests/util_functions_tests/test_leader_election.js` around lines 178 - 184, Update the `logs warning on non-200 status without retrying` test to spy on the client’s warning logger before calling `release_lease()`, then assert that it was invoked for the 409 response while preserving the existing single-call assertion for `_put_release_lease`.
305-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTiming-sensitive tests use very tight real-clock margins; risk of CI flakiness.
These three tests configure
CORE_LEASE_RENEW_ERROR_SLEEP_MS/CORE_LEASE_REQUEST_TIMEOUT_MS/LEASE_RENEW_DEADLINE_MARGIN_MSto 10ms each and rely on real timers (setTimeout-based sleeps) rather than a fake clock. Deadlines this narrow (≈30-70ms total) leave little slack for scheduling jitter under loaded CI runners, and line 458'selapsed < 1000bound, while generous, still depends on real wall-clock behavior. Consider widening the margins (e.g., 50-100ms) or migrating tosinon.useFakeTimers()for deterministic execution, if the abort-signal/timeout code paths tolerate a mocked clock.Also applies to: 332-354, 435-464
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/unit_tests/util_functions_tests/test_leader_election.js` around lines 305 - 330, Make the timing-sensitive leader-election tests deterministic or less narrow, including the test around renew_lease_loop and the additional tests at the referenced ranges. Prefer sinon.useFakeTimers() when compatible with the abort-signal and timeout paths; otherwise widen the configured sleep, request-timeout, and deadline-margin values and adjust assertions such as the elapsed-time bound to provide CI scheduling slack while preserving the tested behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/test/unit_tests/util_functions_tests/test_leader_election.js`:
- Around line 222-231: Update the test for acquire_lease to retain a reference
to the read_lease_state stub and assert it is called exactly once after
_stop_requested is set. Keep the existing update_lease assertion, ensuring the
test verifies that the stop request prevents retrying rather than only
preventing lease updates.
In `@src/util/leader_election.js`:
- Around line 415-428: Update _handle_put_conflict and its callers around the
409 renewal path to accept and propagate the renewal deadline or abort signal
into read_lease_state, so an expired renewal deadline interrupts the reread and
triggers leadership-loss handling without waiting for the independent request
timeout.
- Around line 409-413: Update _clear_renew_deadline and the renewal restart path
used by start() so _cycle_abort is recreated before the new renewal loop
accesses its signal. Preserve clearing the existing timer and abort state, while
ensuring every stop()-then-start() cycle has a fresh AbortController.
- Around line 362-388: Update _interruptible_sleep to retain the abort listener
and remove it whenever the sleep resolves, including normal timer completion,
abort handling, and _wake_interruptible_sleep. Ensure cleanup is performed once
and preserve the existing wake-up behavior.
---
Nitpick comments:
In `@src/test/unit_tests/util_functions_tests/test_leader_election.js`:
- Around line 178-184: Update the `logs warning on non-200 status without
retrying` test to spy on the client’s warning logger before calling
`release_lease()`, then assert that it was invoked for the 409 response while
preserving the existing single-call assertion for `_put_release_lease`.
- Around line 305-330: Make the timing-sensitive leader-election tests
deterministic or less narrow, including the test around renew_lease_loop and the
additional tests at the referenced ranges. Prefer sinon.useFakeTimers() when
compatible with the abort-signal and timeout paths; otherwise widen the
configured sleep, request-timeout, and deadline-margin values and adjust
assertions such as the elapsed-time bound to provide CI scheduling slack while
preserving the tested behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9f0adff4-2a1d-43c0-a4f8-768f1f7cfcc8
📒 Files selected for processing (5)
config.jssrc/test/unit_tests/util_functions_tests/test_leader_election.jssrc/test/utils/index/index.jssrc/util/http_utils.jssrc/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (3)
- src/test/utils/index/index.js
- src/util/http_utils.js
- config.js
58624fb to
6d37355
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/util/leader_election.js (1)
423-431: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winRequire current ownership—not takeability—during renewal.
can_takeis also true for vacant or expired Leases. If this holder was cleared, it can continue serving and race another pod to reacquire, creating overlapping leadership until the conflict resolves. Both paths must step down unlessis_lease_held_by(lease, this._holder)is true.Proposed fix
async _handle_put_conflict(abort_signal) { dbg.warn('lease PUT conflict, re-reading lease', this._lease_name); const state = await this.read_lease_state(abort_signal); - if (!state.can_take) { + if (!is_lease_held_by(state.lease, this._holder)) { dbg.error('lost core lease after PUT conflict', this._lease_name, 'holder', state.lease?.spec?.holderIdentity, 'we are', this._holder); this.emit(EVENTS.LEADERSHIP_LOST); return true; } return false; } - const { lease, can_take } = await this.read_lease_state(this._cycle_abort.signal); - if (!lease || !can_take) { + const { lease } = await this.read_lease_state(this._cycle_abort.signal); + if (!is_lease_held_by(lease, this._holder)) { // another pod acquired the lease or lease was deleted, step downAs per coding guidelines, “Review code for race conditions in async code.”
Also applies to: 458-466, 470-471
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util/leader_election.js` around lines 423 - 431, Update _handle_put_conflict and the corresponding renewal paths around the referenced logic to require current ownership via is_lease_held_by(lease, this._holder), rather than state.can_take. When the lease is vacant, expired, or held by another identity, emit LEADERSHIP_LOST and stop serving; preserve renewal only when the current holder still owns the lease.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/util/leader_election.js`:
- Around line 467-469: Update the successful renewal branch in the lease-renewal
flow around _reset_renew_deadline so it refreshes the holder’s lease
duration/deadline from the renewed Lease response, rather than retaining the
acquisition-time _lease_duration_ms. Ensure later reductions to
leaseDurationSeconds are reflected before the next renewal deadline is
calculated.
---
Outside diff comments:
In `@src/util/leader_election.js`:
- Around line 423-431: Update _handle_put_conflict and the corresponding renewal
paths around the referenced logic to require current ownership via
is_lease_held_by(lease, this._holder), rather than state.can_take. When the
lease is vacant, expired, or held by another identity, emit LEADERSHIP_LOST and
stop serving; preserve renewal only when the current holder still owns the
lease.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 71e201f7-3071-4f05-8b34-6316298d80c0
📒 Files selected for processing (3)
config.jssrc/test/unit_tests/util_functions_tests/test_leader_election.jssrc/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (2)
- config.js
- src/test/unit_tests/util_functions_tests/test_leader_election.js
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/util/leader_election.js (1)
432-433: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winRemove the redundant
_stop_requested = falseinrenew_lease_loop(). Astop()that lands afteracquire_lease()clears_running_promisebut before this method sets its own can be overwritten by this reset, letting renewal continue after shutdown was requested.start()already clears the flag, so dropping this line closes the race.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util/leader_election.js` around lines 432 - 433, Remove the redundant _stop_requested = false assignment from renew_lease_loop(), relying on start() to initialize the flag and preserving stop() requests that occur before renewal begins.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/util/leader_election.js`:
- Around line 432-433: Remove the redundant _stop_requested = false assignment
from renew_lease_loop(), relying on start() to initialize the flag and
preserving stop() requests that occur before renewal begins.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 27289489-45a7-489b-9da5-454ae4a8e4e1
📒 Files selected for processing (3)
config.jssrc/test/unit_tests/util_functions_tests/test_leader_election.jssrc/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (2)
- config.js
- src/test/unit_tests/util_functions_tests/test_leader_election.js
…tion Implements a LeaderElector class backed by coordination.k8s.io/v1 Lease. Emits EVENTS.LEADERSHIP_ACQUIRED and EVENTS.LEADERSHIP_LOST, with start() and stop() lifecycle methods following the event-driven pattern of the Go client-go leaderelection module. Signed-off-by: nadav mizrahi <nadav.mizrahi16@gmail.com>
| return this._lease_duration_ms - | ||
| config.CORE_LEASE_RENEW_ERROR_SLEEP_MS - | ||
| config.CORE_LEASE_REQUEST_TIMEOUT_MS - | ||
| config.LEASE_RENEW_DEADLINE_MARGIN_MS; |
There was a problem hiding this comment.
Worth adding a validation that this is > 0
There was a problem hiding this comment.
right I feel like now that we have the abort controller we don't need this calculation anymore. its also incorrect as the CORE_LEASE_REQUEST_TIMEOUT_MS is per request. I will return it to be a config variable. I will still validate this just in case
| if (!lease_name) { | ||
| throw new Error('NOOBAA_CORE_LEASE_NAME is not set'); | ||
| } | ||
| const holder = `${process.env.HOSTNAME}_${randomUUID()}`; |
There was a problem hiding this comment.
Just for completeness, validate that HOSTNAME is defined (even though this should be injected by Kubernetes automatically)
There was a problem hiding this comment.
changed it to use "noobaa-core" if undefined. this is not needed for correctness. I think randomUUID() is enough. its so it would be easier for us to understand who hold the lease. and for debugging. so that the holder would be something like:
noobaa-core-0_8f387f47-653e-44da-b01c-fceffc973c4c
| /** @type {(() => void)|null} */ | ||
| this._wake_sleep = null; | ||
| /** @type {AbortController} single controller reused across renew cycles; clearTimeout prevents abort from firing on success */ | ||
| this._cycle_abort = new AbortController(); |
There was a problem hiding this comment.
Why do we needthis._cycle_abort?
There was a problem hiding this comment.
The reason is that each renewal cycle performs three HTTP requests. In our case, each request has a 5 second timeout, so a single iteration could take up to 1 second of sleep + 3 × 5 seconds for the HTTP requests = 16 seconds before we get a chance to check whether the lease deadline has been exceeded.
The AbortController acts as a timeout for the entire renewal cycle. If the deadline is reached while a request or the sleep is still in progress, the controller aborts it immediately instead of waiting for the current iteration to finish.
I chose AbortController because it can be passed to both HTTP requests and interruptible sleep, so they can be canceled while they are running. It also works well with the 5 second timeout for HTTP requests. whichever timeout fire first aborts the request.
There was a problem hiding this comment.
however I see that the name is not clear. I will change it to this._renew_deadline_abort
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/util/leader_election.js (1)
227-241: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winNo guard against concurrent
start()calls.
start()/acquire_lease()/renew_lease_loop()all unconditionally overwritethis._running_promiseat entry. A secondstart()invocation while the first is still acquiring or renewing would clobber_running_promiseand run two renewal loops concurrently, corrupting the shared_renew_deadline/_renew_deadline_aborttimer state. This is the same gap flagged in a prior review round ("also guard against repeated start() calls overwriting _renew_loop_promise"), which doesn't appear to have a corresponding guard in the current code (only the "stop() cancels in-flight acquisition" half of that comment is implemented).As per coding guidelines, "Review code for race conditions in async code" applies here.
🔒 Proposed fix
async start() { + if (this._running_promise) return; this._stop_requested = false; await this.acquire_lease();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util/leader_election.js` around lines 227 - 241, Guard LeaderElector.start() against concurrent or repeated invocations before resetting _stop_requested or beginning acquisition: if an existing _running_promise represents an active start/acquisition/renewal flow, reuse or return it. Ensure _running_promise is assigned once for the complete lifecycle and cleared only when that lifecycle finishes, preventing concurrent renew_lease_loop executions and preserving the existing stop() cancellation behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/test/unit_tests/util_functions_tests/test_leader_election.js`:
- Line 301: Update the affected leader-election tests to target the current API:
replace client._cycle_abort with client._renew_deadline_abort, and configure
config.LEASE_RENEW_DEADLINE_MS to create the intended short renewal deadline
instead of setting removed lease-duration or margin/timeout fields. Apply this
consistently across the deadline-related cases so each test exercises the timer
path and avoids undefined-property failures.
In `@src/util/leader_election.js`:
- Around line 144-163: Update _request to avoid AbortSignal.any() and enforce
the lease request deadline with an explicit timeout controller compatible with
Node 24.13.0. Combine the caller’s abort signal with the controller’s timeout
while preserving cancellation from either source, and ensure the timeout is
cleaned up after make_https_request completes.
---
Duplicate comments:
In `@src/util/leader_election.js`:
- Around line 227-241: Guard LeaderElector.start() against concurrent or
repeated invocations before resetting _stop_requested or beginning acquisition:
if an existing _running_promise represents an active start/acquisition/renewal
flow, reuse or return it. Ensure _running_promise is assigned once for the
complete lifecycle and cleared only when that lifecycle finishes, preventing
concurrent renew_lease_loop executions and preserving the existing stop()
cancellation behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 46837988-487a-4abb-81f7-f6e898167f91
📒 Files selected for processing (5)
config.jssrc/test/unit_tests/util_functions_tests/test_leader_election.jssrc/test/utils/index/index.jssrc/util/http_utils.jssrc/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (2)
- src/test/utils/index/index.js
- src/util/http_utils.js
Signed-off-by: nadav mizrahi <nadav.mizrahi16@gmail.com>
|
was replaced by leader election module in the operator noobaa/noobaa-operator#2036 |
Describe the Problem
Today the NooBaa core pod runs as a single active instance. If it fails or is rescheduled, there is no built-in way for a standby core pod to take over cleanly. We need active-passive high availability so only one core pod serves at a time, with automatic failover when the leader is lost.
Explain the Changes
This PR adds a new Kubernetes Lease-based leader election module to the core library.
The new module is responsible for acquiring and renewing leadership, releasing it on shutdown, and notifying callers when leadership is gained or lost. It provides a simple event-driven interface that can later be used by core_init and other HA-related flows.
This PR only adds the reusable leader election module and its unit tests.
The actual wiring into core_init is done separately in a follow-up PR.
New pieces:
core_lease_utils.js. this client is provides functions to acquire, renew, and release the leaseIssues:
operator side change: noobaa/noobaa-operator#1964
Testing Instructions:
node --allow-natives-syntax ./node_modules/.bin/_mocha \ src/test/unit_tests/util_functions_tests/test_core_lease.jsSummary by CodeRabbit
Summary
New Features
leadership_acquiredandleadership_lostevents.KUBE_CA_CERTSsetting.Tests