Skip to content

Add Leader Election module to noobaa core - #9765

Closed
nadavMiz wants to merge 2 commits into
noobaa:masterfrom
nadavMiz:temp-lease
Closed

nadavMiz wants to merge 2 commits into
noobaa:masterfrom
nadavMiz:temp-lease

Conversation

@nadavMiz

@nadavMiz nadavMiz commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

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:

  1. Added a new Lease client in core_lease_utils.js. this client is provides functions to acquire, renew, and release the lease
  2. added config variables CORE_LEASE_ACQUIRE_RETRY_MS and CORE_LEASE_RENEW_DEADLINE_MS to configure lease timings
  3. Unit tests for the lease client

Issues:

  1. part of https://redhat.atlassian.net/browse/RHSTOR-7491

operator side change: noobaa/noobaa-operator#1964

Testing Instructions:

  1. node --allow-natives-syntax ./node_modules/.bin/_mocha \ src/test/unit_tests/util_functions_tests/test_core_lease.js
  • Doc added/updated
  • Tests added

Summary by CodeRabbit

Summary

  • New Features

    • Added Kubernetes Lease–based active/passive leader election with leadership_acquired and leadership_lost events.
    • Introduced core lease timing and Kubernetes API request timeout configuration for the leader-election behavior.
    • Expanded HTTPS trust roots by optionally loading the Kubernetes CA certificate via a new KUBE_CA_CERTS setting.
  • Tests

    • Added a comprehensive leader-election unit test suite covering lease parsing/formatting, expiry/takeability logic, and acquire/renew/release edge cases.
    • Wired the new leader-election tests into the existing unit test runner.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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.

Changes

Leader Election Utility

Layer / File(s) Summary
Lease configuration and state contracts
config.js, src/util/leader_election.js
Adds Lease timing constants, timestamp utilities, Lease predicates, leadership events, environment guards, and elector construction helpers.
Kubernetes HTTPS trust
src/util/http_utils.js
Adds the Kubernetes CA certificate to the shared HTTPS agent CA bundle when readable.
LeaderElector lifecycle
src/util/leader_election.js
Implements Lease acquisition, renewal, loss detection, interruptible shutdown, release handling, retries, and renew deadlines.
Leader-election validation
src/test/unit_tests/util_functions_tests/test_leader_election.js, src/test/utils/index/index.js
Adds unit-test coverage for Lease utilities, lifecycle events, retries, shutdown, loss scenarios, abort handling, and test registration.

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
Loading

Suggested reviewers: jackyalbo, tangledbytes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding a leader election module to the core library.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nadavMiz
nadavMiz force-pushed the temp-lease branch 5 times, most recently from 67a3bb4 to abe5827 Compare June 22, 2026 14:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6852404 and abe5827.

📒 Files selected for processing (6)
  • config.js
  • src/cmd/core_init.js
  • src/test/unit_tests/internal/test_core_init.js
  • src/test/unit_tests/util_functions_tests/test_core_lease.js
  • src/test/utils/index/index.js
  • src/util/core_lease_utils.js

Comment thread src/test/unit_tests/util_functions_tests/test_core_lease.js Outdated
@nadavMiz
nadavMiz marked this pull request as ready for review June 23, 2026 06:30
@nadavMiz
nadavMiz requested a review from dannyzaken June 23, 2026 06:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/util/core_lease_utils.js (1)

191-207: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

acquire_lease retries 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

📥 Commits

Reviewing files that changed from the base of the PR and between abe5827 and ae53709.

📒 Files selected for processing (6)
  • config.js
  • src/cmd/core_init.js
  • src/test/unit_tests/internal/test_core_init.js
  • src/test/unit_tests/util_functions_tests/test_core_lease.js
  • src/test/utils/index/index.js
  • src/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

Comment thread src/cmd/core_init.js Outdated
Comment thread src/util/leader_election.js Outdated
Comment thread src/util/core_lease_utils.js Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ae53709 and 9d86e15.

📒 Files selected for processing (6)
  • config.js
  • src/cmd/core_init.js
  • src/test/unit_tests/internal/test_core_init.js
  • src/test/unit_tests/util_functions_tests/test_core_lease.js
  • src/test/utils/index/index.js
  • src/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

Comment thread src/test/unit_tests/util_functions_tests/test_leader_election.js
Comment thread src/util/leader_election.js Outdated
@nadavMiz
nadavMiz force-pushed the temp-lease branch 2 times, most recently from 3a99178 to 32e842f Compare June 24, 2026 08:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9d86e15 and 3a99178.

📒 Files selected for processing (6)
  • config.js
  • src/cmd/core_init.js
  • src/test/unit_tests/internal/test_core_init.js
  • src/test/unit_tests/util_functions_tests/test_core_lease.js
  • src/test/utils/index/index.js
  • src/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

Comment thread src/cmd/core_init.js Outdated
Comment thread src/test/unit_tests/internal/test_core_init.js Outdated
Comment thread src/util/leader_election.js Outdated
@nadavMiz
nadavMiz force-pushed the temp-lease branch 2 times, most recently from a23b23d to 75a9d4c Compare June 24, 2026 10:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3a99178 and 75a9d4c.

📒 Files selected for processing (6)
  • config.js
  • src/cmd/core_init.js
  • src/test/unit_tests/internal/test_core_init.js
  • src/test/unit_tests/util_functions_tests/test_core_lease.js
  • src/test/utils/index/index.js
  • src/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

Comment thread src/cmd/core_init.js Outdated
Comment thread src/test/unit_tests/internal/test_core_init.js Outdated
Comment thread src/util/core_lease_utils.js Outdated
Comment thread src/util/leader_election.js Outdated
Comment thread src/cmd/core_init.js Outdated
Comment thread src/util/leader_election.js
Comment thread config.js Outdated
@nadavMiz
nadavMiz force-pushed the temp-lease branch 2 times, most recently from 55ff283 to d287fea Compare July 12, 2026 12:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Deadline check still masks the "another holder" step-down path.

client._lease_duration_ms stays at its default 0 here, unlike the sibling renew tests which explicitly set it. In renew_lease_loop, the deadline check runs before read_lease_state, and a 0 duration makes _is_renew_deadline_exceeded() true immediately — so LEADERSHIP_LOST fires via the deadline branch, and the stubbed pod-b/can_take: false response 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3723e4f and d287fea.

📒 Files selected for processing (5)
  • config.js
  • src/test/unit_tests/util_functions_tests/test_leader_election.js
  • src/test/utils/index/index.js
  • src/util/http_utils.js
  • src/util/leader_election.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/utils/index/index.js
  • config.js

Comment thread src/util/leader_election.js Outdated
Comment thread src/util/leader_election.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/test/unit_tests/util_functions_tests/test_leader_election.js (1)

203-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared helper for temporary config overrides.

Five separate tests hand-roll the same save/mutate/try-finally-restore boilerplate against the shared config singleton, 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 manual orig_retry save/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 manual orig_retry save/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

📥 Commits

Reviewing files that changed from the base of the PR and between d287fea and 18efe01.

📒 Files selected for processing (5)
  • config.js
  • src/test/unit_tests/util_functions_tests/test_leader_election.js
  • src/test/utils/index/index.js
  • src/util/http_utils.js
  • src/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

@nadavMiz
nadavMiz force-pushed the temp-lease branch 4 times, most recently from 560a370 to c23a891 Compare July 13, 2026 12:24
@nadavMiz
nadavMiz requested a review from dannyzaken July 13, 2026 12:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Test 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 win

Timing-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_MS to 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's elapsed < 1000 bound, while generous, still depends on real wall-clock behavior. Consider widening the margins (e.g., 50-100ms) or migrating to sinon.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

📥 Commits

Reviewing files that changed from the base of the PR and between d287fea and c23a891.

📒 Files selected for processing (5)
  • config.js
  • src/test/unit_tests/util_functions_tests/test_leader_election.js
  • src/test/utils/index/index.js
  • src/util/http_utils.js
  • src/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

Comment thread src/test/unit_tests/util_functions_tests/test_leader_election.js
Comment thread src/util/leader_election.js
Comment thread src/util/leader_election.js
Comment thread src/util/leader_election.js
@nadavMiz
nadavMiz force-pushed the temp-lease branch 2 times, most recently from 58624fb to 6d37355 Compare July 13, 2026 13:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Require current ownership—not takeability—during renewal.

can_take is 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 unless is_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 down

As 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

📥 Commits

Reviewing files that changed from the base of the PR and between 58624fb and 6d37355.

📒 Files selected for processing (3)
  • config.js
  • src/test/unit_tests/util_functions_tests/test_leader_election.js
  • src/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

Comment thread src/util/leader_election.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Remove the redundant _stop_requested = false in renew_lease_loop(). A stop() that lands after acquire_lease() clears _running_promise but 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6d37355 and 2d3c379.

📒 Files selected for processing (3)
  • config.js
  • src/test/unit_tests/util_functions_tests/test_leader_election.js
  • src/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>
Comment thread src/util/leader_election.js Outdated
Comment thread src/util/leader_election.js Outdated
Comment on lines +490 to +493
return this._lease_duration_ms -
config.CORE_LEASE_RENEW_ERROR_SLEEP_MS -
config.CORE_LEASE_REQUEST_TIMEOUT_MS -
config.LEASE_RENEW_DEADLINE_MARGIN_MS;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding a validation that this is > 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/util/leader_election.js Outdated
if (!lease_name) {
throw new Error('NOOBAA_CORE_LEASE_NAME is not set');
}
const holder = `${process.env.HOSTNAME}_${randomUUID()}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for completeness, validate that HOSTNAME is defined (even though this should be injected by Kubernetes automatically)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/util/leader_election.js Outdated
/** @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();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we needthis._cycle_abort?

@nadavMiz nadavMiz Jul 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nadavMiz nadavMiz Jul 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

however I see that the name is not clear. I will change it to this._renew_deadline_abort

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/util/leader_election.js (1)

227-241: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No guard against concurrent start() calls.

start()/acquire_lease()/renew_lease_loop() all unconditionally overwrite this._running_promise at entry. A second start() invocation while the first is still acquiring or renewing would clobber _running_promise and run two renewal loops concurrently, corrupting the shared _renew_deadline/_renew_deadline_abort timer 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

📥 Commits

Reviewing files that changed from the base of the PR and between aa7d5bc and 13bc5b7.

📒 Files selected for processing (5)
  • config.js
  • src/test/unit_tests/util_functions_tests/test_leader_election.js
  • src/test/utils/index/index.js
  • src/util/http_utils.js
  • src/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

Comment thread src/test/unit_tests/util_functions_tests/test_leader_election.js Outdated
Comment thread src/util/leader_election.js
Signed-off-by: nadav mizrahi <nadav.mizrahi16@gmail.com>
@nadavMiz

nadavMiz commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

was replaced by leader election module in the operator noobaa/noobaa-operator#2036
can reopen if needed

@nadavMiz nadavMiz closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants