Skip to content

fix: transition exporter to AVAILABLE after afterLease hook failure#871

Open
evakhoni wants to merge 1 commit into
jumpstarter-dev:mainfrom
evakhoni:fix/exporter-status-after-endlease-failure
Open

fix: transition exporter to AVAILABLE after afterLease hook failure#871
evakhoni wants to merge 1 commit into
jumpstarter-dev:mainfrom
evakhoni:fix/exporter-status-after-endlease-failure

Conversation

@evakhoni

@evakhoni evakhoni commented Jul 9, 2026

Copy link
Copy Markdown
Member

Fixes #870

Problem

When an afterLease hook fails with on_failure: endLease or encounters an unexpected error, the exporter gets permanently stuck in AfterLeaseHookFailed status and cannot accept new leases.

Root cause: The run_after_lease_hook() finally block calls request_lease_release(), which sends AVAILABLE status to the controller via gRPC but doesn't update the exporter's internal _exporter_status field.

Solution

Add await report_status(ExporterStatus.AVAILABLE, ...) in the finally block before calling request_lease_release(). This ensures both the internal state and controller are updated.

  • Idempotent for happy/warn paths that already reported AVAILABLE in the try block
  • Fixes endLease and unexpected-error paths that only reported AFTER_LEASE_HOOK_FAILED

Testing

  • Unit tests: 2 new test cases verify status transitions for endLease and unexpected errors
  • All tests pass: 562 tests
  • Manual verification: Exporter now transitions AfterLeaseHookFailedAvailable and accepts subsequent leases

- Report AVAILABLE status in finally block before calling
  request_lease_release() — ensures exporter can accept new leases after
  on_failure=endLease or unexpected errors
- Update comments to reflect that finally block handles the AVAILABLE
  transition, not just lease release
- Add test for afterLease hook failure with on_failure=endLease
- Add test for afterLease hook unexpected error

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Fixes a bug where an exporter permanently stuck in AfterLeaseHookFailed status after an afterLease hook failure (endLease or unexpected exception) by explicitly reporting AVAILABLE in the finally block before requesting lease release, with clarifying comments and two new regression tests.

Changes

AfterLease hook failure recovery

Layer / File(s) Summary
Status transition fix and comments
python/packages/jumpstarter/jumpstarter/exporter/hooks.py
The finally block in run_after_lease_hook now explicitly reports ExporterStatus.AVAILABLE before calling request_lease_release, with comments clarifying AFTER_LEASE_HOOK_FAILED is transient.
Regression tests for recovery paths
python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py
Two new tests verify the endLease and unexpected-exception paths report AFTER_LEASE_HOOK_FAILED then AVAILABLE and call request_lease_release instead of shutdown.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: kirkbrauer

Poem

A hook once failed, and stuck we stayed,
"AfterLease" ghosts refused to fade.
Now finally's touch sets status free,
AVAILABLE hops back, wild and spree! 🐇
Release the lease, no more delay—
This bunny's fix has saved the day!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the requested recovery from AfterLeaseHookFailed for endLease and unexpected errors, with tests covering both.
Out of Scope Changes check ✅ Passed The diff stays focused on the hook failure status fix and matching regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: exporter status now transitions back to AVAILABLE after afterLease hook failure.
Description check ✅ Passed The description matches the changeset and explains the status fix, idempotent behavior, and added tests.
✨ 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.

@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)

1537-1581: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert AFTER_LEASE_HOOK_FAILED is reported before AVAILABLE.

Both tests verify that both statuses appear but don't verify ordering. A future refactor that accidentally swaps the sequence would pass these tests while breaking the recovery contract.

♻️ Proposed addition for both tests

Add after the existing failed_statuses / available_statuses assertions:

+        # Verify FAILED is reported before AVAILABLE
+        status_sequence = [s for s, _ in status_calls]
+        failed_idx = status_sequence.index(ExporterStatus.AFTER_LEASE_HOOK_FAILED)
+        available_idx = status_sequence.index(ExporterStatus.AVAILABLE)
+        assert failed_idx < available_idx, (
+            f"AFTER_LEASE_HOOK_FAILED must be reported before AVAILABLE, got: {status_calls}"
+        )

Also applies to: 1583-1633

🤖 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
1537 - 1581, The after-lease hook tests in HookExecutor.run_after_lease_hook
currently only assert that AFTER_LEASE_HOOK_FAILED and AVAILABLE are both
reported, but they do not verify the required sequence. Update the relevant
tests to assert ordering by checking the relative positions in status_calls so
AFTER_LEASE_HOOK_FAILED is reported before AVAILABLE, covering both the endLease
failure path and the other applicable test in hooks_test.py.
🤖 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 853-857: The cleanup path in the exporter hook should not let a
transient failure in report_status stop lease release. In the finally block
around request_lease_release, wrap the report_status(ExporterStatus.AVAILABLE,
...) call in its own try/except, log or ignore that failure as appropriate, and
then always continue to request_lease_release so the lease is still freed. Use
the existing report_status and request_lease_release symbols in this hook to
locate the change.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/hooks_test.py`:
- Around line 1537-1581: The after-lease hook tests in
HookExecutor.run_after_lease_hook currently only assert that
AFTER_LEASE_HOOK_FAILED and AVAILABLE are both reported, but they do not verify
the required sequence. Update the relevant tests to assert ordering by checking
the relative positions in status_calls so AFTER_LEASE_HOOK_FAILED is reported
before AVAILABLE, covering both the endLease failure path and the other
applicable test in hooks_test.py.
🪄 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: 59ffe5a7-6596-4ca0-8f84-e88132b994a7

📥 Commits

Reviewing files that changed from the base of the PR and between d46e91a and ce15c4d.

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

Comment on lines +853 to 857
# Transition to AVAILABLE to clear AFTER_LEASE_HOOK_FAILED.
# Idempotent if already AVAILABLE from the happy/warn paths.
await report_status(ExporterStatus.AVAILABLE, "Available for new lease")
try:
await request_lease_release()

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap report_status in try/except to guarantee request_lease_release runs.

If report_status raises (e.g., transient gRPC failure), execution exits the finally block before reaching request_lease_release(), leaving the lease unreleased and the exporter stuck — the same class of bug this PR fixes. The request_lease_release call below is already protected; report_status should be too.

🔒 Proposed fix: protect report_status call
             if request_lease_release and not shutdown_called:
                 # Transition to AVAILABLE to clear AFTER_LEASE_HOOK_FAILED.
                 # Idempotent if already AVAILABLE from the happy/warn paths.
-                await report_status(ExporterStatus.AVAILABLE, "Available for new lease")
+                try:
+                    await report_status(ExporterStatus.AVAILABLE, "Available for new lease")
+                except Exception as e:
+                    logger.error("Failed to report AVAILABLE status: %s", e, exc_info=True)
                 try:
                     await request_lease_release()
                 except Exception as e:
                     logger.error("Failed to request lease release: %s", e, exc_info=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Transition to AVAILABLE to clear AFTER_LEASE_HOOK_FAILED.
# Idempotent if already AVAILABLE from the happy/warn paths.
await report_status(ExporterStatus.AVAILABLE, "Available for new lease")
try:
await request_lease_release()
# Transition to AVAILABLE to clear AFTER_LEASE_HOOK_FAILED.
# Idempotent if already AVAILABLE from the happy/warn paths.
try:
await report_status(ExporterStatus.AVAILABLE, "Available for new lease")
except Exception as e:
logger.error("Failed to report AVAILABLE status: %s", e, exc_info=True)
try:
await request_lease_release()
🤖 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 853 -
857, The cleanup path in the exporter hook should not let a transient failure in
report_status stop lease release. In the finally block around
request_lease_release, wrap the report_status(ExporterStatus.AVAILABLE, ...)
call in its own try/except, log or ignore that failure as appropriate, and then
always continue to request_lease_release so the lease is still freed. Use the
existing report_status and request_lease_release symbols in this hook to locate
the change.

@mangelajo

Copy link
Copy Markdown
Member

I am not sure if this was intended behavior

if afterlease is failing, we may want the admin to look at it?
@kirkbrauer

@kirkbrauer

Copy link
Copy Markdown
Member

@mangelajo I'm not sure we should really allow the endLease mode for afterLease hooks because it kind of is a no-op since the lease would end anyways after the lease. We should only force the admin to take a look at this failure in the case that we take the exporter offline due to a failed hook with onFailure = exit.

@mangelajo

Copy link
Copy Markdown
Member

@mangelajo I'm not sure we should really allow the endLease mode for afterLease hooks because it kind of is a no-op since the lease would end anyways after the lease. We should only force the admin to take a look at this failure in the case that we take the exporter offline due to a failed hook with onFailure = exit.

May be this is something we can combine now with the "enabled" flag of the exporters, we could just flip the flag off and add a "disableExporter" mode or similar..

@evakhoni

Copy link
Copy Markdown
Member Author

interesting points. so basically what you @kirkbrauer saying is for being a no-op in this case, this should not even exists as an option? it's a good point, however I'm not sure if we should put a hard restriction on it, or just document it for being a no-op? after al it works as described, even though it's functionally useless, so it's a question of should it be left for the user judgement for not to use it, or restricted it while introducing an inconsistency with beforeLease to achieve that.

@mangelajo , seems like you suggesting to repurpose it into a different functionality 🤔 I'm not sure do we even benefit from keeping an exporter online but disabled for an admin to interact with, while we already have onFailure exit for that? do the admin even need that? if yes, do we also needs the other mode when it actually exits the process, or we can just set onfailure exit to always set offline or just disabled but keeping the process alive for debug? or if we do decide that we need two different modes, then what about beforeLease endlease should it act similarly?

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.

Exporter stuck in AfterLeaseHookFailed status after hook failure with endLease

3 participants