Skip to content

refactor: replace _previous_leased with derived LeaseState - #948

Open
bennyz wants to merge 1 commit into
mainfrom
bz/restart-1b
Open

refactor: replace _previous_leased with derived LeaseState#948
bennyz wants to merge 1 commit into
mainfrom
bz/restart-1b

Conversation

@bennyz

@bennyz bennyz commented Aug 3, 2026

Copy link
Copy Markdown
Member

Introduce LeaseState enum and _lease_state property derived from
_lease_context, eliminating a class of state-synchronization bugs
where _previous_leased could drift from _lease_context.

Key changes:

  • Wrap handle_lease body in try/finally so early returns (stale lease,
    session setup race) always clean up _lease_context and log context
  • Reject overlapping leases in _apply_status instead of silently
    replacing _lease_context, preventing concurrent handle_lease tasks
  • Move before-lease hook spawn into _on_lease_acquired for cleaner
    ownership of lease startup
  • Remove stale _previous_leased from test fixtures

Depends on #947
Next: #949

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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

The exporter now uses the public LeaseState enum for lease tracking. It handles conflicting and completed leases, starts acquisition hooks during lease creation, and guarantees cleanup through finally. Tests cover transitions, connections, hooks, replay, and context cleanup.

Changes

Exporter lease lifecycle

Layer / File(s) Summary
Lease state transitions
python/packages/jumpstarter/jumpstarter/exporter/exporter.py
The exporter exposes LeaseState, tracks completed and deferred statuses, processes lease transitions, suppresses conflicting or completed updates, and updates release handling.
Lease session cleanup
python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Lease sessions skip stale leases and clean streams, hooks, lease contexts, logging contexts, completed-lease state, and deferred status through finally.
Lease lifecycle validation
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
Tests cover lease transitions, reassignment replay, completed-lease suppression, connection handling, stale-session cleanup, hook fallback, and final context cleanup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Exporter
  participant LeaseContext
  participant BeforeLeaseHook
  participant ConnectionStream
  Exporter->>LeaseContext: create and register lease context
  Exporter->>BeforeLeaseHook: start before-lease hook
  Exporter->>ConnectionStream: process lease connections
  ConnectionStream-->>Exporter: lease end or interruption
  Exporter->>ConnectionStream: close listen stream
  Exporter->>BeforeLeaseHook: run after-lease cleanup
  Exporter->>LeaseContext: record completed lease and clear contexts
  Exporter->>Exporter: replay deferred status
Loading

Possibly related PRs

Suggested reviewers: kirkbrauer, mangelajo

Poem

A rabbit tracks each lease state,
Hooks start when leases activate.
Stale updates lose their way,
Streams and contexts clear away.
Finally keeps the trail in place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% 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
Title check ✅ Passed The title clearly identifies the main change: replacing _previous_leased with the derived LeaseState.
Description check ✅ Passed The description directly explains the lease-state refactor, cleanup behavior, overlapping lease handling, and related changes.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bz/restart-1b

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.

@bennyz
bennyz force-pushed the bz/restart-1b branch 2 times, most recently from d21d21a to 9b72161 Compare August 3, 2026 12:56
@bennyz
bennyz force-pushed the bz/restart-1b branch 2 times, most recently from 209efdd to 117bb70 Compare August 3, 2026 14:26
Base automatically changed from bz/restart-1a to main August 3, 2026 14:33

@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: 3

🧹 Nitpick comments (3)
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py (3)

1135-1153: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert that the rejected lease does not update client state.

The test proves the return value and the retained lease_name. The rejection path also skips _on_lease_update. Add an assertion on client_name so a future change that moves _on_lease_update before the overlap check fails this test.

🧪 Proposed assertion
         assert result is False
         assert exporter._lease_context.lease_name == "lease-A"
+        assert exporter._lease_context.client_name != "other-client"
🤖 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/exporter_test.py` around
lines 1135 - 1153, Extend test_overlap_rejection_returns_false to assert that
the rejected lease does not alter the existing lease context’s client_name,
alongside the current lease_name assertion. Use the original lease context value
and keep the assertion after _apply_status completes.

1341-1349: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace the fixed sleeps with event-based synchronisation.

This test depends on two anyio.sleep(0.1) calls to let handle_lease reach its finally block. On a loaded CI runner those windows can expire before the cleanup runs, which makes the test flaky. test_handle_lease_processes_connections already uses an Event plus fail_after. Apply the same pattern here, for example by wrapping _cleanup_after_lease in a side effect that sets an event and waiting on that event under fail_after.

🤖 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/exporter_test.py` around
lines 1341 - 1349, Replace the fixed anyio.sleep calls in the handle_lease test
with event-based synchronization: have the _cleanup_after_lease mock side effect
set an Event, then wait for that event within fail_after before cancelling the
task group. Follow the pattern used by test_handle_lease_processes_connections
while preserving the existing assertions.

1120-1133: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing one exporter test factory.

_make_idle_exporter repeats most of _make_serve_exporter at line 1366. Both build an Exporter through __new__ and set the same private fields. A shared helper with keyword arguments would keep the two fixtures in sync when new init=False fields appear.

🤖 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/exporter_test.py` around
lines 1120 - 1133, Refactor the exporter test factories by introducing one
shared helper for constructing the Exporter via __new__ and initializing the
common private fields. Update _make_idle_exporter and _make_serve_exporter to
call that helper with keyword arguments for their differing state, keeping both
fixtures synchronized as new init=False fields are added.
🤖 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/exporter_test.py`:
- Around line 1232-1248: Add assertions to
test_leased_to_idle_calls_on_lease_released verifying the observable effects of
_on_lease_released, including that the lease context’s release signalling is
triggered and, when exit_on_lease_end is enabled, _stop_requested is set.
Configure the exporter flag as needed while preserving the existing
LEASED-to-IDLE setup.

In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 1000-1009: In the finally cleanup guarded by self._lease_context
is lease_scope, keep the lease context set while awaiting the 0.2-second session
settle delay, then clear _lease_context and call clear_log_context afterward.
Ensure cancellation cannot bypass the required delay and subsequent context
cleanup, using the surrounding task framework’s shielded cancellation mechanism
if necessary.
- Around line 1051-1071: Update the lease reacquisition logic around
_apply_status and handle_lease so a completed lease remains a boundary after
_lease_context is cleared and _lease_state becomes IDLE. Track the
just-completed lease name/status or require a different non-empty lease status
before calling _on_lease_acquired, while preserving acquisition of genuinely new
leases and existing lease-update behavior.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 1135-1153: Extend test_overlap_rejection_returns_false to assert
that the rejected lease does not alter the existing lease context’s client_name,
alongside the current lease_name assertion. Use the original lease context value
and keep the assertion after _apply_status completes.
- Around line 1341-1349: Replace the fixed anyio.sleep calls in the handle_lease
test with event-based synchronization: have the _cleanup_after_lease mock side
effect set an Event, then wait for that event within fail_after before
cancelling the task group. Follow the pattern used by
test_handle_lease_processes_connections while preserving the existing
assertions.
- Around line 1120-1133: Refactor the exporter test factories by introducing one
shared helper for constructing the Exporter via __new__ and initializing the
common private fields. Update _make_idle_exporter and _make_serve_exporter to
call that helper with keyword arguments for their differing state, keeping both
fixtures synchronized as new init=False fields are added.
🪄 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 Plus

Run ID: 9458b628-e270-4794-b975-726807f28e7d

📥 Commits

Reviewing files that changed from the base of the PR and between 821bbf0 and 799ebbf.

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

Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py

@mangelajo mangelajo left a comment

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.

A few notes, nothing major.

Comment on lines 1053 to +1061

if self._lease_context is None and status.lease_name != "" and current_leased:
self._on_lease_acquired(status, tg)

if current_leased:
if previous_state == LeaseState.IDLE and status.lease_name != "":
self._on_lease_acquired(status, tg)
elif (
previous_state == LeaseState.LEASED
and self._lease_context
and self._lease_context.lease_name != status.lease_name
):

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.

Nice addition — prevents concurrent handle_lease tasks for different leases.

Worth noting there's a small timing window here: after _on_lease_released returns (having waited for after_lease_hook_done), handle_lease's outer finally may not have run yet (session teardown still in progress). During that gap _lease_context is still set, so a new lease arriving here would be rejected. It gets picked up on the next status tick once the finally clears _lease_context.

A brief comment would help future readers understand this is intentional:

# Note: this may briefly reject new leases while handle_lease
# finishes session teardown; the next status tick will acquire them.

Comment on lines +1235 to +1250
"""LEASED → IDLE transitions through _on_lease_released."""
exporter = self._make_idle_exporter()
lease_ctx = make_lease_context(lease_name="ending-lease")
lease_ctx.after_lease_hook_done.set()
exporter._lease_context = lease_ctx
exporter._started = True

status = MagicMock()
status.leased = False
status.lease_name = ""
status.client_name = ""
status.context = {}

async with create_task_group() as tg:
await exporter._apply_status(status, tg)
tg.cancel_scope.cancel()

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.

This test runs the LEASED→IDLE transition but has no assertions — it only checks that it doesn't raise. Consider verifying the side effects, e.g.:

assert lease_ctx.lease_ended.is_set()

This would confirm the signal actually reached handle_lease (the core purpose of the transition).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ack


async def _on_lease_released(self, previous_leased: bool) -> None:
async def _on_lease_released(self, previous_state: LeaseState) -> None:
"""Handle not-leased status: signal handle_lease on transition, clean up context."""

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.

nit: The docstring still says "clean up context" but this method no longer clears _lease_context, clear_log_context(), or does the sleep(0.2) — that responsibility moved to handle_lease's finally block. Consider updating to match the reduced scope:

"""Handle not-leased status: signal handle_lease on transition, check exit_on_lease_end."""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ack

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

♻️ Duplicate comments (1)
python/packages/jumpstarter/jumpstarter/exporter/exporter.py (1)

1001-1011: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cancellation during the settle delay skips the remaining cleanup.

The ordering fix is correct: the delay now runs before the context is cleared. One gap remains. await sleep(0.2) is a cancellation checkpoint. If the surrounding task group is cancelled while this finally runs, the sleep raises immediately and Lines 1008-1011 never execute. _lease_context then stays set and _lease_state keeps reporting LEASED. Shield the delay so the state cleanup always completes.

🔧 Proposed fix
         finally:
             if self._lease_context is lease_scope:
                 session_was_created = lease_scope.session is not None
-                if session_was_created:
-                    # Brief delay to ensure session is fully closed before next lease.
-                    # Prevents SSL corruption from overlapping connections.
-                    await sleep(0.2)
-                self._last_completed_lease = lease_scope.lease_name
-                self._lease_context = None
-                clear_log_context()
-                logger.debug("Ready for next lease")
+                with CancelScope(shield=True):
+                    if session_was_created:
+                        # Brief delay to ensure session is fully closed before next lease.
+                        # Prevents SSL corruption from overlapping connections.
+                        await sleep(0.2)
+                    self._last_completed_lease = lease_scope.lease_name
+                    self._lease_context = None
+                    clear_log_context()
+                    logger.debug("Ready for next lease")

This was raised as a follow-up note on the previous ordering comment.

🤖 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/exporter.py` around lines
1001 - 1011, Shield the settle delay in the finally block of the lease cleanup
flow so cancellation during await sleep(0.2) cannot skip the subsequent state
reset. Ensure _last_completed_lease is assigned, _lease_context is cleared,
clear_log_context() runs, and the “Ready for next lease” log remains executed
even when the surrounding task group is cancelled.
🧹 Nitpick comments (5)
python/packages/jumpstarter/jumpstarter/exporter/exporter.py (3)

200-200: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document _last_completed_lease like the neighboring state fields.

Every other internal state field in this dataclass carries a docstring. The lifecycle of this field is non-obvious: handle_lease sets it in the finally block, _apply_status clears it on a leased=false tick, and the guard at Line 1061 uses it to drop trailing ticks.

📝 Proposed docstring
     _last_completed_lease: str | None = field(init=False, default=None)
+    """Name of the lease whose handle_lease task most recently completed.
+
+    Set in handle_lease()'s finally block, cleared when the controller reports
+    leased=false. Used to ignore trailing leased=true status ticks that still
+    reference an already-finished lease.
+    """
     _lease_context: LeaseContext | None = field(init=False, default=None)
🤖 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/exporter.py` at line 200,
Add a docstring for the `_last_completed_lease` dataclass field matching the
neighboring internal state-field documentation, describing that `handle_lease`
sets it on completion, `_apply_status` clears it when `leased=false`, and the
trailing-tick guard uses it to discard late ticks.

893-918: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

The stale-lease return at Line 918 leaves listen_tx/listen_rx unclosed.

listen_tx and listen_rx are created at Line 896, but the return at Line 918 exits before the inner finally at Line 984 that calls await listen_tx.aclose(). No producer or consumer task is attached yet, so nothing hangs; anyio can still emit a ResourceWarning for the unclosed streams. Create the streams after the second stale-lease check.

♻️ Proposed reorder
             logger.info("Listening for incoming connection requests on lease %s", lease_name)
 
-            # Buffer Listen responses to avoid blocking when responses arrive before
-            # process_connections starts iterating. This prevents a race condition where
-            # the client dials immediately after lease acquisition but before the session is ready.
-            listen_tx, listen_rx = create_memory_object_stream[jumpstarter_pb2.ListenResponse](max_buffer_size=10)
-
             # Create session for the lease duration and populate lease_scope

Then create the streams immediately after the _skip_stale_lease(..., "during session setup") check at Line 917.

🤖 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/exporter.py` around lines
893 - 918, Move the listen_tx/listen_rx creation out of the pre-session setup
and place it immediately after the _skip_stale_lease(..., "during session
setup") check in the lease-serving flow. Preserve the existing stream
configuration and all later uses, ensuring the stale-lease return occurs before
either stream is allocated.

1053-1082: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Collapse the duplicate current_leased branching and skip the update on an empty lease name.

Two points in this block:

  1. Line 1056 tests not current_leased, and Line 1059 tests current_leased again. Move the _last_completed_lease = None reset into the else branch so the state has one branch per outcome.
  2. If current_leased is true and status.lease_name is "" while the state is IDLE, neither inner branch runs and control reaches _on_lease_update(status) at Line 1079. _lease_context is None there, so the only effect is the log line "Currently leased by under ". Return early instead.
♻️ Proposed restructure
         previous_state = self._lease_state
         current_leased = status.leased
 
-        if not current_leased:
-            self._last_completed_lease = None
-
-        if current_leased:
-            if previous_state == LeaseState.IDLE and status.lease_name != "":
+        if current_leased:
+            if previous_state == LeaseState.IDLE:
+                if status.lease_name == "":
+                    logger.warning("Received leased status without a lease name; ignoring")
+                    return False
                 if status.lease_name == self._last_completed_lease:
                     logger.debug("Ignoring trailing status for completed lease %s", status.lease_name)
                     return False
                 self._on_lease_acquired(status, tg)
             elif (
                 previous_state == LeaseState.LEASED
                 and self._lease_context
                 and self._lease_context.lease_name != status.lease_name
             ):
                 # May briefly reject new leases while handle_lease finishes
                 # session teardown; the next status tick will acquire them.
                 logger.error(
                     "Received lease %s while still handling %s; ignoring",
                     status.lease_name,
                     self._lease_context.lease_name,
                 )
                 return False
 
             self._on_lease_update(status)
         else:
+            self._last_completed_lease = None
             await self._on_lease_released(previous_state)
🤖 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/exporter.py` around lines
1053 - 1082, Refactor the current_leased handling in the status-processing
method to use a single if/else: keep lease release handling in the false branch
and move _last_completed_lease = None there before awaiting
_on_lease_released(previous_state). In the true branch, return early when
previous_state is LeaseState.IDLE and status.lease_name is empty, before
_on_lease_update(status); preserve the existing acquisition,
duplicate-completion, and conflicting-lease behavior.
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py (2)

1370-1410: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fallback path is not actually exercised by this test.

With hook_executor=None, handle_lease's main "no hook" branch inside the try block runs await self._report_status(...) then lease_scope.before_lease_hook.set() before the finally fallback check (not lease_scope.before_lease_hook.is_set()) is reached. In this test, lease_ctx.lease_ended.set() happens only after await anyio.sleep(0.1), well after that primary branch already completes and sets the event.

As a result, the assertion assert lease_ctx.before_lease_hook.is_set() passes through the primary code path, not through the finally fallback. Deleting the fallback line in handle_lease would not make this test fail, so it does not protect against a regression of the deadlock-prevention fallback described in the comment above that code.

To exercise the fallback, trigger the lease-end cancellation before the "no hook" branch reaches its checkpoint, for example by setting lease_ctx.lease_ended before spawning handle_lease, or by having fake_retry_stream call lease_ctx.lease_ended.set() synchronously as its first action.

🧪 Proposed fix to exercise the fallback path
     async def fake_retry_stream(name, factory, tx, **kwargs):
+        lease_ctx.lease_ended.set()
         await tx.aclose()

     exporter._retry_stream = fake_retry_stream
     exporter._listen_stream_factory = MagicMock(return_value=MagicMock())

     async with create_task_group() as tg:
         tg.start_soon(exporter.handle_lease, "fallback-lease", tg, lease_ctx)
-        await anyio.sleep(0.1)
-        lease_ctx.lease_ended.set()
         await anyio.sleep(0.1)
         tg.cancel_scope.cancel()
🤖 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/exporter_test.py` around
lines 1370 - 1410, Update
test_handle_lease_finally_sets_before_lease_hook_fallback so
lease_ctx.lease_ended is triggered before the no-hook branch can set
before_lease_hook, preferably by setting it before spawning handle_lease or as
the first action in fake_retry_stream. Keep the assertion and cleanup
verification, ensuring the test fails if the finally fallback in handle_lease is
removed.

1191-1199: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixed-duration sleeps used as test synchronization barriers.

Three tests use await anyio.sleep(0.05) to wait for a background task to reach a certain point before asserting. Under CI load, a fixed sleep can complete before the background task finishes, causing intermittent, hard-to-reproduce test failures. The shared root cause is the absence of an explicit completion signal for the spawned background work.

  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py#L1191-L1199: replace await anyio.sleep(0.05) with an explicit wait on an event set inside fake_handle_lease once it records lease_name.
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py#L1226-L1229: replace await anyio.sleep(0.05) with an explicit wait on an event set by fake_handle_lease (or a second event set by fake_before_hook), since fake_before_hook already sets lease_scope.before_lease_hook.
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py#L1359-L1365: replace await anyio.sleep(0.05) with an explicit wait on an event set by a fake _cleanup_after_lease wrapper before delegating to the AsyncMock.
🤖 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/exporter_test.py` around
lines 1191 - 1199, Replace the fixed-duration sleeps in exporter_test.py at
lines 1191-1199, 1226-1229, and 1359-1365 with explicit AnyIO event
synchronization. In the tests around fake_handle_lease, await an event set after
lease_name is recorded; at 1226-1229, use fake_handle_lease’s event or an event
from fake_before_hook after setting lease_scope.before_lease_hook; at 1359-1365,
await an event set by a fake _cleanup_after_lease wrapper before it delegates to
the AsyncMock.
🤖 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.

Duplicate comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 1001-1011: Shield the settle delay in the finally block of the
lease cleanup flow so cancellation during await sleep(0.2) cannot skip the
subsequent state reset. Ensure _last_completed_lease is assigned, _lease_context
is cleared, clear_log_context() runs, and the “Ready for next lease” log remains
executed even when the surrounding task group is cancelled.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 1370-1410: Update
test_handle_lease_finally_sets_before_lease_hook_fallback so
lease_ctx.lease_ended is triggered before the no-hook branch can set
before_lease_hook, preferably by setting it before spawning handle_lease or as
the first action in fake_retry_stream. Keep the assertion and cleanup
verification, ensuring the test fails if the finally fallback in handle_lease is
removed.
- Around line 1191-1199: Replace the fixed-duration sleeps in exporter_test.py
at lines 1191-1199, 1226-1229, and 1359-1365 with explicit AnyIO event
synchronization. In the tests around fake_handle_lease, await an event set after
lease_name is recorded; at 1226-1229, use fake_handle_lease’s event or an event
from fake_before_hook after setting lease_scope.before_lease_hook; at 1359-1365,
await an event set by a fake _cleanup_after_lease wrapper before it delegates to
the AsyncMock.

In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Line 200: Add a docstring for the `_last_completed_lease` dataclass field
matching the neighboring internal state-field documentation, describing that
`handle_lease` sets it on completion, `_apply_status` clears it when
`leased=false`, and the trailing-tick guard uses it to discard late ticks.
- Around line 893-918: Move the listen_tx/listen_rx creation out of the
pre-session setup and place it immediately after the _skip_stale_lease(...,
"during session setup") check in the lease-serving flow. Preserve the existing
stream configuration and all later uses, ensuring the stale-lease return occurs
before either stream is allocated.
- Around line 1053-1082: Refactor the current_leased handling in the
status-processing method to use a single if/else: keep lease release handling in
the false branch and move _last_completed_lease = None there before awaiting
_on_lease_released(previous_state). In the true branch, return early when
previous_state is LeaseState.IDLE and status.lease_name is empty, before
_on_lease_update(status); preserve the existing acquisition,
duplicate-completion, and conflicting-lease behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f4fe91a-dff6-4a8f-89b3-13c5ef50a73c

📥 Commits

Reviewing files that changed from the base of the PR and between 799ebbf and d8ea4f7.

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

@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: 2

♻️ Duplicate comments (1)
python/packages/jumpstarter/jumpstarter/exporter/exporter.py (1)

1001-1011: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Shield the settle delay so the cleanup still runs on cancellation.

The delay ordering fix is correct. The remaining gap is cancellation. await sleep(0.2) at Line 1007 is not shielded. When the surrounding task group is cancelled, that await raises immediately and Lines 1008-1010 never run. _lease_context then stays set, so _lease_state stays LEASED and a later lease is rejected by the LEASED branch in _apply_status. A prior review raised this same sub-point.

🔧 Proposed fix
         finally:
             if self._lease_context is lease_scope:
                 session_was_created = lease_scope.session is not None
-                if session_was_created:
-                    # Brief delay to ensure session is fully closed before next lease.
-                    # Prevents SSL corruption from overlapping connections.
-                    await sleep(0.2)
-                self._last_completed_lease = lease_scope.lease_name
-                self._lease_context = None
-                clear_log_context()
-                logger.debug("Ready for next lease")
+                with CancelScope(shield=True):
+                    if session_was_created:
+                        # Brief delay to ensure session is fully closed before next lease.
+                        # Prevents SSL corruption from overlapping connections.
+                        await sleep(0.2)
+                    self._last_completed_lease = lease_scope.lease_name
+                    self._lease_context = None
+                    clear_log_context()
+                    logger.debug("Ready for next 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py` around lines
1001 - 1011, Shield the settle delay in the cleanup block of the
lease-management method so task cancellation cannot skip the remaining
finalization. Wrap the await of sleep(0.2) in the project’s supported
cancellation-shield mechanism, while preserving the existing session_was_created
condition and ensuring _last_completed_lease, _lease_context,
clear_log_context(), and the debug log always execute.
🧹 Nitpick comments (1)
python/packages/jumpstarter/jumpstarter/exporter/exporter.py (1)

200-201: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document _last_completed_lease.

Every other internal state field in this class has a docstring. The purpose of this field is not obvious from its name. Add a short docstring that states it suppresses trailing leased=true ticks for a lease that handle_lease already finished, and that _apply_status clears it on the first leased=false tick.

♻️ Proposed docstring
     _last_completed_lease: str | None = field(init=False, default=None)
+    """Name of the last lease whose handle_lease task completed.
+
+    Set in handle_lease's finally block and cleared on the first not-leased
+    status. Used by _apply_status to ignore trailing leased=true ticks that
+    refer to an already-finished lease.
+    """
     _lease_context: LeaseContext | None = field(init=False, default=None)
🤖 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/exporter.py` around lines
200 - 201, Add a concise docstring to the `_last_completed_lease` field
documenting that it suppresses trailing `leased=true` ticks for leases already
completed by `handle_lease`, and that `_apply_status` clears it on the first
`leased=false` tick. Leave the field behavior unchanged.
🤖 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/exporter.py`:
- Line 896: Ensure the Listen memory streams created by
create_memory_object_stream in the exporter method are closed on the stale-lease
return path. Wrap the remaining listen-processing flow, including the
session_for_lease block and stale-lease check, in an async context manager for
listen_tx and listen_rx, while preserving the existing inner cleanup behavior.
- Around line 1070-1073: Update the comment in handle_lease to replace the
nonexistent _finalize_lease_context reference with the handle_lease finally
block that clears _lease_context.

---

Duplicate comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 1001-1011: Shield the settle delay in the cleanup block of the
lease-management method so task cancellation cannot skip the remaining
finalization. Wrap the await of sleep(0.2) in the project’s supported
cancellation-shield mechanism, while preserving the existing session_was_created
condition and ensuring _last_completed_lease, _lease_context,
clear_log_context(), and the debug log always execute.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 200-201: Add a concise docstring to the `_last_completed_lease`
field documenting that it suppresses trailing `leased=true` ticks for leases
already completed by `handle_lease`, and that `_apply_status` clears it on the
first `leased=false` tick. Leave the field behavior unchanged.
🪄 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 Plus

Run ID: 0cd97282-f41d-41d5-8dc2-afd7f1360899

📥 Commits

Reviewing files that changed from the base of the PR and between d8ea4f7 and 68e7e82.

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

Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
Comment on lines +1070 to +1073
# Controller reassigned the exporter to a different lease.
# Signal the old lease to tear down; the next status tick
# will acquire the new lease once _finalize_lease_context
# clears _lease_context.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the stale method name in the comment.

The comment refers to _finalize_lease_context. No such method exists in this file. handle_lease's finally block at Lines 1001-1011 clears _lease_context. Update the comment so readers can find the code that performs the clear.

🔧 Proposed fix
                 # Controller reassigned the exporter to a different lease.
                 # Signal the old lease to tear down; the next status tick
-                # will acquire the new lease once _finalize_lease_context
-                # clears _lease_context.
+                # will acquire the new lease once handle_lease's finally
+                # block clears _lease_context.
📝 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
# Controller reassigned the exporter to a different lease.
# Signal the old lease to tear down; the next status tick
# will acquire the new lease once _finalize_lease_context
# clears _lease_context.
# Controller reassigned the exporter to a different lease.
# Signal the old lease to tear down; the next status tick
# will acquire the new lease once handle_lease's finally
# block clears _lease_context.
🤖 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/exporter.py` around lines
1070 - 1073, Update the comment in handle_lease to replace the nonexistent
_finalize_lease_context reference with the handle_lease finally block that
clears _lease_context.

raballew pushed a commit to raballew/jumpstarter that referenced this pull request Aug 4, 2026
- serve() → serve / _run_control_plane / _apply_status
 - Transition handlers: _on_lease_acquired, _on_lease_update,
   _on_lease_released, _check_stop_requested
 - Removes the C901 suppression
- Status transitions are now unit-testable without task-group
scaffolding"

Next: jumpstarter-dev#948

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>

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

🧹 Nitpick comments (2)
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py (2)

1486-1498: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert the completed-lease guard.

The finally block in handle_lease sets _last_completed_lease together with clearing _lease_context. _apply_status uses _last_completed_lease to suppress trailing ticks. Asserting it here covers both halves of the finalization contract.

🧪 Suggested assertion
         assert exporter._lease_context is None
+        assert exporter._last_completed_lease == "cleanup-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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py` around
lines 1486 - 1498, Extend test_handle_lease_finally_clears_lease_context to also
assert that exporter._last_completed_lease is set to the completed lease context
after handle_lease finishes, covering the finalization guard alongside the
existing _lease_context assertion.

1444-1484: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider bounding this test with fail_after.

This test relies on fixed anyio.sleep calls to advance handle_lease. If the fallback logic regresses and blocks, the test hangs instead of failing fast. The neighbouring test at Line 1435 already uses fail_after(5). Wrapping the task group in fail_after makes the failure mode consistent.

♻️ Suggested change
-        async with create_task_group() as tg:
-            tg.start_soon(exporter.handle_lease, "fallback-lease", tg, lease_ctx)
-            await anyio.sleep(0.1)
-            lease_ctx.lease_ended.set()
-            await anyio.sleep(0.1)
-            tg.cancel_scope.cancel()
+        with fail_after(5):
+            async with create_task_group() as tg:
+                tg.start_soon(exporter.handle_lease, "fallback-lease", tg, lease_ctx)
+                await anyio.sleep(0.1)
+                lease_ctx.lease_ended.set()
+                await anyio.sleep(0.1)
+                tg.cancel_scope.cancel()
🤖 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/exporter_test.py` around
lines 1444 - 1484, Wrap the task-group execution in
test_handle_lease_finally_sets_before_lease_hook_fallback with
anyio.fail_after(5), matching the neighboring test’s timeout pattern. Keep the
existing sleeps, lease-ended signaling, assertions, and cancellation behavior
unchanged.
🤖 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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 1486-1498: Extend test_handle_lease_finally_clears_lease_context
to also assert that exporter._last_completed_lease is set to the completed lease
context after handle_lease finishes, covering the finalization guard alongside
the existing _lease_context assertion.
- Around line 1444-1484: Wrap the task-group execution in
test_handle_lease_finally_sets_before_lease_hook_fallback with
anyio.fail_after(5), matching the neighboring test’s timeout pattern. Keep the
existing sleeps, lease-ended signaling, assertions, and cancellation behavior
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 637bd7f9-c115-4c23-a7af-6bd85e283800

📥 Commits

Reviewing files that changed from the base of the PR and between 68e7e82 and ab45ea1.

📒 Files selected for processing (2)
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py

@mangelajo mangelajo left a comment

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.

Comment on lines +1012 to +1016
pending = self._pending_lease_status
if pending is not None:
self._pending_lease_status = None
if self._status_replay_tx is not None:
await self._status_replay_tx.send(pending)

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.

If the task group is being cancelled during shutdown, _status_replay_tx may already be closed. send() on a closed stream would raise ClosedResourceError. Replaying a stashed status during shutdown is pointless anyway, so this should be guarded:

pending = self._pending_lease_status
if pending is not None:
    self._pending_lease_status = None
    if self._status_replay_tx is not None:
        try:
            await self._status_replay_tx.send(pending)
        except (ClosedResourceError, EndOfStream):
            logger.debug("Status channel closed, skipping replay for %s", pending.lease_name)

Comment on lines +200 to +202
_last_completed_lease: str | None = field(init=False, default=None)
_pending_lease_status: jumpstarter_pb2.StatusResponse | None = field(init=False, default=None)
_status_replay_tx: MemoryObjectSendStream | None = field(init=False, default=None)

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.

nit: I would document those attributes to make them more evident (also other fields have docstrings), also docstrings need maintenance so it's a double side sword... since a nit, I leave it up to your criteria :D

_last_completed_lease: str | None = field(init=False, default=None)
"""Name of the most recently completed lease, used to filter trailing status ticks
after handle_lease's finally has cleaned up. Cleared on the next leased=False tick."""

_pending_lease_status: jumpstarter_pb2.StatusResponse | None = field(init=False, default=None)
"""Stashed status for a reassigned lease. When the controller reassigns the exporter
to a new lease while the old one is still tearing down, the new status is held here
and replayed by handle_lease's finally block after cleanup completes."""

_status_replay_tx: MemoryObjectSendStream | None = field(init=False, default=None)
"""Reference to the status channel TX end, set by _run_control_plane. Used by
handle_lease's finally to replay stashed reassignment statuses into the status loop."""

@@ -847,7 +853,7 @@ async def _skip_stale_lease(self, lease_name: str, lease_scope: LeaseContext, co
lease_scope.after_lease_hook_done.set()
return True

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.

oh no the noqa is back :'(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

temporarily, needed the lint to pass in this PR 😅

@mangelajo

Copy link
Copy Markdown
Member

There is a failure in E2E related to lease status: https://github.com/jumpstarter-dev/jumpstarter/actions/runs/30887610848/job/91922876687?pr=948#step:10:1773

I am trying to figure out if it could be related.

Comment on lines +1001 to +1011
finally:
if self._lease_context is lease_scope:
session_was_created = lease_scope.session is not None
if session_was_created:
# Brief delay to ensure session is fully closed before next lease.
# Prevents SSL corruption from overlapping connections.
await sleep(0.2)
self._last_completed_lease = lease_scope.lease_name
self._lease_context = None
clear_log_context()
logger.debug("Ready for next lease")

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.

_lease_state derives from _lease_context, but this finally block clears _lease_context = None concurrently with _apply_status. When handle_lease completes before the controller's leased=False tick arrives, _apply_status sees previous_state == IDLE and skips the exit_on_lease_end check in _on_lease_released. The exporter keeps running instead of shutting down.

@mangelajo

mangelajo commented Aug 4, 2026

Copy link
Copy Markdown
Member

This is what claude determined, when I saw it was a status transition I thought it could be related, I am looking at the code now.

E2E Failure Analysis: test-exporter-oidc stuck in LeaseReady

Failed test: paginated exporter listing returns all exporters — timed out after 300s waiting for test-exporter-oidc to reach Available status. It was stuck in LeaseReady.

What the logs show

The exporter gets a new lease at 07:41:36, creates a session, reaches LEASE_READY, and then stops producing any further log output. No "Currently not leased", no "Lease ended", no Available transition.

Why this is related to the PR

In the old code, _on_lease_released was the single owner of _lease_context cleanup — it would signal lease_ended, wait for after_lease_hook_done, clear _lease_context, and move on. This meant repeated leased=False ticks were harmless: after the first one cleared _lease_context, subsequent ones would see _lease_context is None and skip the signaling/wait path.

In the new code, _on_lease_released no longer clears _lease_context — that responsibility moved to handle_lease's outer finally block. This creates a window where multiple leased=False ticks arrive while _lease_context is still set (pointing at the old lease scope). The logs show exactly this pattern: three consecutive "Lease ended, signaling handle_lease" entries for the same old lease right before the new lease starts.

The likely deadlock scenario: a leased=False tick arrives and _on_lease_released calls await lease_ctx.after_lease_hook_done.wait() (shielded) on a _lease_context that now belongs to the new lease (which hasn't run _cleanup_after_lease yet because it just started). The status loop blocks waiting for an event that won't be set until the new lease ends, so the exporter never processes any more status updates and stays stuck in LeaseReady.

Suggested fix

_on_lease_released should either:

  • Snapshot _lease_context atomically and guard against waiting on a context it didn't originate, or
  • Continue to clear _lease_context itself (as the old code did) so repeated ticks are idempotent, and let handle_lease's finally be the fallback rather than the primary cleanup path.

@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 (2)
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py (2)

1445-1485: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test does not exercise the finally fallback.

handle_lease sets before_lease_hook in the no-hook branch, right after starting conn_tg, before the finally block runs. exporter.hook_executor is None here, so the event is already set by that normal path. The assertion at line 1484 passes even if the finally fallback is removed.

To cover the fallback, prevent the no-hook branch from reaching before_lease_hook.set(), for example by making _report_status raise or by cancelling conn_tg before that line, and then assert the event is still set after handle_lease returns.

🤖 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/exporter_test.py` around
lines 1445 - 1485, Update
test_handle_lease_finally_sets_before_lease_hook_fallback so execution cannot
reach the normal no-hook before_lease_hook.set() path, such as by making
_report_status raise or cancelling conn_tg before that point. Ensure
handle_lease completes and assert before_lease_hook is set only because of the
finally fallback, while retaining the cleanup assertion.

1353-1377: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Let the production code perform the replay send.

The test assigns _status_replay_tx, but nothing in production code reads it here. Lines 1368-1374 hand-roll the cleanup and the replay send that handle_lease's finally block performs. The test therefore proves only that _apply_status accepts a replayed status; it does not prove that the pending status reaches _status_replay_tx.

Drive the real finalization path so the send is exercised, for example by calling exporter._finalize_lease_context(lease_ctx_a) (or completing a stubbed handle_lease) and then receiving from status_rx without sending manually.

🤖 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/exporter_test.py` around
lines 1353 - 1377, Update the test to exercise the production replay path
instead of manually clearing lease state and sending the pending status. After
applying status_b, invoke exporter._finalize_lease_context(lease_ctx_a) or
complete a stubbed handle_lease so its finalization logic sends through
_status_replay_tx, then receive the replayed status from status_rx and pass it
to _apply_status.
🤖 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/exporter_test.py`:
- Around line 1636-1656: The test test_finalize_skips_when_already_cleared calls
the removed Exporter._finalize_lease_context method and will raise
AttributeError. Remove or relocate this stale direct call and assert the
equivalent no-op behavior through the current _apply_status and lease-release
flow, unless the method is intentionally restored on Exporter.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 1445-1485: Update
test_handle_lease_finally_sets_before_lease_hook_fallback so execution cannot
reach the normal no-hook before_lease_hook.set() path, such as by making
_report_status raise or cancelling conn_tg before that point. Ensure
handle_lease completes and assert before_lease_hook is set only because of the
finally fallback, while retaining the cleanup assertion.
- Around line 1353-1377: Update the test to exercise the production replay path
instead of manually clearing lease state and sending the pending status. After
applying status_b, invoke exporter._finalize_lease_context(lease_ctx_a) or
complete a stubbed handle_lease so its finalization logic sends through
_status_replay_tx, then receive the replayed status from status_rx and pass it
to _apply_status.
🪄 Autofix

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 Plus

Run ID: fa8818e5-0cf2-4a00-ab6a-227ff7013404

📥 Commits

Reviewing files that changed from the base of the PR and between ab45ea1 and 02c1db1.

📒 Files selected for processing (2)
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • python/packages/jumpstarter/jumpstarter/exporter/exporter.py

Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py Outdated
@bennyz
bennyz force-pushed the bz/restart-1b branch 3 times, most recently from e6db6cd to 970d7bf Compare August 5, 2026 10:34

@bkhizgiy bkhizgiy left a comment

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.

I don't see any issues, good from my side.

Introduce LeaseState enum and _lease_state property derived from
_lease_context, eliminating a class of state-synchronization bugs
where _previous_leased could drift from _lease_context.

Key changes:
- Wrap handle_lease body in try/finally so early returns (stale lease,
  session setup race) always clean up _lease_context and log context
- Reject overlapping leases in _apply_status instead of silently
  replacing _lease_context, preventing concurrent handle_lease tasks
- Move before-lease hook spawn into _on_lease_acquired for cleaner
  ownership of lease startup
- Remove stale _previous_leased from test fixtures

Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com>
Assited-by: claude-opus-4.6
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.

4 participants