Skip to content

fix(hooks): release lease when beforeLease hook fails with onFailure=exit#827

Closed
mangelajo wants to merge 1 commit into
mainfrom
fix/hooks-exit-release-lease
Closed

fix(hooks): release lease when beforeLease hook fails with onFailure=exit#827
mangelajo wants to merge 1 commit into
mainfrom
fix/hooks-exit-release-lease

Conversation

@mangelajo

@mangelajo mangelajo commented Jun 23, 2026

Copy link
Copy Markdown
Member

Superseded by #823, which now fixes both #639 (endLease) and #640 (exit) in a single PR.

The exit fix was incorporated into #823 with the correct semantics: shutdown(wait_for_lease_exit=False) for immediate exit without releasing the lease (the lease stays active so the client can observe the failure status)."

…exit

When a beforeLease hook fails with on_failure='exit', the exporter
called shutdown(wait_for_lease_exit=True) which defers the actual exit
until the lease ends. However, nobody released the lease, creating a
deadlock:

- serve() waits for the controller to send leased=False
- The controller never sends leased=False because the lease was never
  released
- handle_lease blocks on lease_ended.wait() which is never signaled
- The exporter hangs forever in a zombie-like state

The fix releases the lease before deferring shutdown by calling
request_lease_release(), which:
1. Tells the controller to release the lease (release_lease=True)
2. Sets lease_ended as a fallback to unblock handle_lease

This allows the serve() loop to receive leased=False from the
controller, check _stop_requested, and proceed with the deferred
shutdown.

Also extracts _safe_release_lease() helper to keep run_before_lease_hook
within the ruff C901 complexity limit.

Fixes: #640
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change updates before-lease hook failure handling for on_failure="exit" to request lease release before shutdown, adds guarded error logging around that release request, and expands tests to verify both the normal release path and a release-request failure path.

Changes

Before-lease exit flow

Layer / File(s) Summary
Exit release orchestration
python/packages/jumpstarter/jumpstarter/exporter/hooks.py, python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py
HookExecutor now safely requests lease release after a before-lease failure with on_failure="exit" and before continuing shutdown handling, and tests assert the release request, shutdown arguments, skip-after-lease state, and continued shutdown when the release request raises an error.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • jumpstarter-dev/jumpstarter#613 — Also changes HookExecutor.run_before_lease_hook and before-lease failure orchestration in hooks.py.

Possibly related PRs

Suggested reviewers

  • evakhoni
  • kirkbrauer

Poem

🐇 I tugged one lease with cautious care,
then logged the stumble in the air.
A hook cried out, “I must now exit!”
The tests all nodded, “Yes, that fits it.”
Hop, release, then rest the warren.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: releasing the lease when beforeLease fails with onFailure=exit.
Description check ✅ Passed The description is directly related to the code changes and bug fix described in the diff.
Linked Issues check ✅ Passed The changes and tests address #640 by releasing the lease before deferred shutdown when beforeLease fails with onFailure=exit.
Out of Scope Changes check ✅ Passed The patch stays focused on the hook failure path and its tests, with no obvious unrelated changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hooks-exit-release-lease

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py (1)

494-509: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Model the release callback’s status side effect in this test.

A bare AsyncMock only proves the callback was invoked; it does not catch the production callback reporting AVAILABLE after the exporter was marked OFFLINE. Add an assertion/fake callback that preserves the expected status sequence for the shutdown path.

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py` around lines
494 - 509, The mock_report_status in the test is a bare AsyncMock that does not
verify the status callback behavior during the shutdown path. Configure
mock_report_status to model or assert the expected status sequence, specifically
that it is called with the appropriate status values (such as OFFLINE and then
AVAILABLE) during the lease hook execution. Add assertions or side effects to
the mock_report_status mock to ensure the production behavior of reporting
status changes during the shutdown flow is properly tested.
🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/hooks.py`:
- Around line 681-688: The shutdown path in Exporter._safe_release_lease /
Exporter._request_lease_release is briefly reporting the exporter as AVAILABLE
after it has already been marked OFFLINE, and _stop_requested is being set too
late. Update the shutdown-specific lease release flow so it preserves OFFLINE
status instead of using the normal release path, and set the deferred stop
intent before unblocking lease cleanup. Make the change in the shutdown callback
path that calls shutdown(..., wait_for_lease_exit=True, should_unregister=True)
so release_lease=True does not advertise AVAILABLE during shutdown.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py`:
- Around line 494-509: The mock_report_status in the test is a bare AsyncMock
that does not verify the status callback behavior during the shutdown path.
Configure mock_report_status to model or assert the expected status sequence,
specifically that it is called with the appropriate status values (such as
OFFLINE and then AVAILABLE) during the lease hook execution. Add assertions or
side effects to the mock_report_status mock to ensure the production behavior of
reporting status changes during the shutdown flow is properly tested.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1db67e8-9ffa-4d71-948d-99939edd8811

📥 Commits

Reviewing files that changed from the base of the PR and between c7c0ec9 and f171862.

📒 Files selected for processing (2)
  • python/packages/jumpstarter/jumpstarter/exporter/hooks.py
  • python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py

Comment on lines +681 to 688
# Release the lease so the controller sends leased=False,
# which unblocks the serve() loop to check _stop_requested.
# Without this, the exporter hangs waiting for a lease end
# that never comes.
await anyio.sleep(1.0)
await self._safe_release_lease(request_lease_release)
# Defer shutdown: sets _stop_requested=True, actual stop after lease cleanup
shutdown(exit_code=1, wait_for_lease_exit=True, should_unregister=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve OFFLINE and set stop intent before releasing the lease.

The runtime callback passed here is Exporter._request_lease_release, which reports ExporterStatus.AVAILABLE with release_lease=True before setting lease_ended; this runs after Lines 677-680 report OFFLINE and before Line 688 sets _stop_requested. That can briefly advertise a shutting-down exporter as available and can let lease cleanup observe the lease end before the deferred stop flag is set.

Use a shutdown-specific release path that keeps the exporter OFFLINE, and set the deferred stop intent before unblocking lease cleanup.

Suggested direction
-                await anyio.sleep(1.0)
-                await self._safe_release_lease(request_lease_release)
                 # Defer shutdown: sets _stop_requested=True, actual stop after lease cleanup
                 shutdown(exit_code=1, wait_for_lease_exit=True, should_unregister=True)
+                await anyio.sleep(1.0)
+                await self._safe_release_lease(request_lease_release)

Also adjust the concrete release callback used for this path so release_lease=True does not report AVAILABLE after the exporter has been marked OFFLINE.

🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/hooks.py` around lines 681 -
688, The shutdown path in Exporter._safe_release_lease /
Exporter._request_lease_release is briefly reporting the exporter as AVAILABLE
after it has already been marked OFFLINE, and _stop_requested is being set too
late. Update the shutdown-specific lease release flow so it preserves OFFLINE
status instead of using the normal release path, and set the deferred stop
intent before unblocking lease cleanup. Make the change in the shutdown callback
path that calls shutdown(..., wait_for_lease_exit=True, should_unregister=True)
so release_lease=True does not advertise AVAILABLE during shutdown.

@mangelajo

Copy link
Copy Markdown
Member Author

Not a bug, see issue comments

@mangelajo mangelajo closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant