Skip to content

fix: retry transient gRPC failures in status reports#911

Open
evakhoni wants to merge 9 commits into
jumpstarter-dev:mainfrom
evakhoni:fix/890-report-status-retry
Open

fix: retry transient gRPC failures in status reports#911
evakhoni wants to merge 9 commits into
jumpstarter-dev:mainfrom
evakhoni:fix/890-report-status-retry

Conversation

@evakhoni

@evakhoni evakhoni commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #890 by adding retry logic with exponential backoff to handle transient gRPC failures during status reporting, ensuring exporters recover from network blips instead of becoming permanently stuck.

Problem

When the afterLease hook completes, the exporter calls _report_status(AVAILABLE) to tell the controller it's ready for new leases. If that gRPC call fails transiently (network blip, controller momentarily unreachable), the error is silently swallowed. The controller permanently sees AFTER_LEASE_HOOK, and the exporter is never assigned new leases — even though it locally believes it's AVAILABLE. Only a pod restart recovers.

Solution

Retry mechanism (_send_report_status_rpc)

Extracts the ReportStatus gRPC call into a new private method with bounded retry:

  • Max retries: 3 (4 total attempts per call to this method)
  • Backoff: Exponential — 1s, 2s, 4s
  • Enters retry loop for: UNAVAILABLE, DEADLINE_EXCEEDED
  • Single attempt only: PERMISSION_DENIED, INVALID_ARGUMENT, and other non-transient codes (logs error, returns immediately)
  • Legacy handling: UNIMPLEMENTED logs warning, returns immediately (controllers before Nov 2025)

Two-phase lease release (race condition fix)

To avoid retrying release_lease=true (which could release a newly-assigned lease if the controller processed the request but response was lost), _request_lease_release uses a two-phase approach:

  1. Phase 1: Send release_lease=true once (non-idempotent, not safe to retry)
  2. Phase 2: On any failure except UNIMPLEMENTED, call _report_status(AVAILABLE) which invokes the retry mechanism above

Total attempts by error type:

  • Transient errors (UNAVAILABLE, DEADLINE_EXCEEDED): 5 attempts (1 in Phase 1 + 4 in Phase 2)
  • Non-transient errors (PERMISSION_DENIED, etc.): 2 attempts (1 in Phase 1 + 1 in Phase 2, no retry loop)
  • UNIMPLEMENTED: 1 attempt (Phase 2 not triggered)

Phase 2 is always triggered for failures except UNIMPLEMENTED (conservative approach), giving non-transient errors one more chance with a fresh gRPC stub, but only transient errors enter the full retry loop.

Testing

  • 8 new tests covering retry success, retry exhaustion, non-retryable errors, and two-phase lease release
  • 1 existing test updated for retry behavior
  • All 631 tests pass
  • No new lint or type errors

Test plan

  • Unit tests pass
  • Lint and type check pass

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 80b85246-e9b7-41b7-969f-315ac614922f

📥 Commits

Reviewing files that changed from the base of the PR and between 4a62ca0 and 834fde2.

📒 Files selected for processing (4)
  • controller/internal/service/auth/auth.go
  • controller/internal/service/auth/auth_test.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/controller_service_integration_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • controller/internal/service/controller_service_integration_test.go
  • controller/internal/service/controller_service.go

📝 Walkthrough

Walkthrough

The exporter adds bounded retries for status and lease-release RPCs, supports compatibility fallback for unsupported release APIs, and signals lease completion across outcomes. The controller now authenticates exporter-initiated release requests and handles release operations idempotently.

Changes

Exporter lease release and retry flow

Layer / File(s) Summary
Exporter RPC retry handling
python/packages/jumpstarter/.../exporter.py, python/packages/jumpstarter/.../exporter_test.py
Adds exponential retry handling, terminal error classification, UNIMPLEMENTED handling, and coverage for transient and non-transient failures.
Lease release orchestration
python/packages/jumpstarter/.../exporter.py, python/packages/jumpstarter/.../exporter_test.py
Attempts direct release, uses cached compatibility fallback when unsupported, reports AVAILABLE, preserves failure status where required, and signals lease completion.
Controller exporter release authorization
controller/internal/service/...
Authenticates exporter release requests, verifies ownership, updates release state idempotently, and tests success, denial, and repeated calls.

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

Sequence Diagram(s)

sequenceDiagram
  participant Exporter
  participant Controller
  participant Backoff
  Exporter->>Controller: ReleaseLease request
  Controller-->>Exporter: Success, unsupported, or transient error
  Exporter->>Backoff: Retry transient failure
  Backoff-->>Exporter: Retry delay completed
  Exporter->>Controller: ReportStatus release fallback or AVAILABLE
  Controller-->>Exporter: Status response
Loading

Possibly related issues

  • jumpstarter-dev/jumpstarter#890: The retry and recovery path addresses exporter status remaining stuck after lease hooks.
  • jumpstarter-dev/jumpstarter#912: Both changes modify _send_report_status_rpc() retry behavior; that issue concerns per-attempt deadlines.

Possibly related PRs

Suggested reviewers: mangelajo, bennyz

Poem

A rabbit retries through rain,
Till status shines bright again.
Leases release, hooks depart,
The controller knows each part.
AVAILABLE hops in the chart.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Controller auth and exporter lease-release routing changes go beyond fixing the stuck status-update issue in #890. Remove or justify the lease-release, auth, and controller-service changes if they are not required for the status-retry fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding retries for transient gRPC status-report failures.
Description check ✅ Passed The description is directly about the same retry/backoff fix and its lease-release follow-up.
Linked Issues check ✅ Passed The PR adds retry/backoff to the exporter status update path after afterLease, which addresses issue #890's stuck AfterLeaseHook behavior.
Docstring Coverage ✅ Passed Docstring coverage is 81.25% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

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

541-558: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the promised exponential backoff schedule.

Both tests patch anyio.sleep but do not verify its arguments, so 1,1,1 or another incorrect delay sequence would pass. Assert [1.0, 2.0, 4.0] for exhaustion and [1.0, 2.0] for recovery.

  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py#L541-L558: assert all three retry delays.
  • python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py#L590-L595: assert the two delays before recovery.

As per coding guidelines, “Provide comprehensive package test coverage.”

🤖 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 541 - 558, Update the _report_status retry tests in
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py at lines
541-558 and 590-595 to inspect the patched anyio.sleep calls: assert [1.0, 2.0,
4.0] for exhaustion and [1.0, 2.0] for recovery, preserving the existing warning
and error assertions.

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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter.py`:
- Around line 443-446: Update the lease-release result flow around the success
handling in the exporter to distinguish an unsupported RPC (UNIMPLEMENTED) from
an actual delivery failure. Propagate a distinct unsupported outcome from the
retry/request logic, and only emit the “Failed to request lease release after
retries” error for genuine failed attempts; preserve the existing warning for
unsupported controllers and success logging for delivered requests.
- Around line 436-442: Update the release flow around _send_report_status_rpc
and ReportStatusRequest to include the lease ID being released as an
expected-lease precondition. Propagate this identity through the protocol and
controller release operation, and atomically reject requests whose expected ID
does not match the currently assigned lease. Ensure retries after UNAVAILABLE
still target only the original lease and cannot release a newly assigned one.
- Around line 365-384: The ReportStatus retry loop lacks a per-attempt deadline,
allowing a call to hang indefinitely. Update the ReportStatus invocation inside
the retry loop to pass a short timeout, then handle
grpc.StatusCode.DEADLINE_EXCEEDED explicitly according to the existing retry
policy—retry it as transient while attempts remain, or return False immediately
if deadline failures should not retry.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 541-558: Update the _report_status retry tests in
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py at lines
541-558 and 590-595 to inspect the patched anyio.sleep calls: assert [1.0, 2.0,
4.0] for exhaustion and [1.0, 2.0] for recovery, preserving the existing warning
and error assertions.
🪄 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: 6a90d77c-0769-4675-990a-011d049d8e31

📥 Commits

Reviewing files that changed from the base of the PR and between e0b0cba and da12fa5.

📒 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 thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py Outdated
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py Outdated
evakhoni and others added 2 commits July 22, 2026 22:18
Extract ReportStatus gRPC call into _send_report_status_rpc with retry
logic for transient errors (UNAVAILABLE, DEADLINE_EXCEEDED):
- Max 3 retries (4 total attempts)
- Exponential backoff: 1s, 2s, 4s
- UNIMPLEMENTED handled specially (no retry, warning log)
- Non-transient errors fail fast

Both _report_status and _request_lease_release now use this shared
method, making all status transitions resilient to transient failures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add tests proving retry logic works:
- test_report_status_retries_on_transient_failure: transient error
  resolves on 3rd attempt, status delivered
- test_report_status_does_not_retry_non_transient_grpc_error:
  PERMISSION_DENIED fails fast (no retry)
- test_report_status_does_not_retry_non_grpc_exception:
  ConnectionError fails fast (no retry)
- test_request_lease_release_retries_on_transient_failure:
  _request_lease_release also retries via shared logic

Update existing test for retry behavior:
- test_other_grpc_error_logs_error: patch anyio.sleep, verify 3 retry
  warnings logged before final error

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py
evakhoni and others added 3 commits July 23, 2026 16:59
Retrying release_lease=true is unsafe: if the controller processed it but
the response was lost, a retry could release a newly-assigned lease (no
lease identity in the request). Split _request_lease_release into phases:
1. Send release_lease=true once (no retry)
2. On failure, retry just status=AVAILABLE via _report_status (idempotent)

The controller already ended the lease before afterLease runs, so
release_lease=true is a courtesy - the critical part is AVAILABLE status.

Documentation:
- Fix _send_report_status_rpc docstring (UNIMPLEMENTED returns False)
- Add comments clarifying UNIMPLEMENTED is legacy support for controllers
  before Nov 2025 (b76f6c8), safe to remove in future versions

Tests: update existing + add two new tests for success and UNIMPLEMENTED.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace two boolean flags (released, unimplemented) with one (should_retry)
for clearer intent and simpler future removal of UNIMPLEMENTED legacy support.

Changes:
- Use should_retry flag instead of double-negative condition
  (if should_retry vs if not released and not unimplemented)
- Simplify Phase 2 comment - legacy context is already documented at the
  UNIMPLEMENTED check itself (line 455-458)
- Remove unreachable return False at end of _send_report_status_rpc loop
  (all paths explicitly return inside the loop)

No functional changes - all tests pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Extract test fixture to deduplicate mock setup:
- Add _setup_mock_controller_stub helper to eliminate 40+ lines of
  repeated boilerplate across 8 tests
- Makes test assertions more visible and easier to maintain

Remove single-use success variable:
- Inline _send_report_status_rpc return check in _report_status
- Add comment clarifying error logging is handled by the helper

Simplify duplicate UNIMPLEMENTED comment:
- Replace verbose duplicate in _request_lease_release with cross-reference
  to _send_report_status_rpc where full context is documented

Add missing call_count assertion:
- Validate test_other_grpc_error_logs_error makes exactly 4 RPC attempts
- Catches potential off-by-one errors the backoff schedule alone might miss

Add retry exhaustion test:
- test_request_lease_release_exhausts_retries verifies critical safety:
  even when all retries fail, lease_ended is still set so handle_lease
  can exit (prevents permanent deadlock)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py (3)

519-533: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that UNIMPLEMENTED is not retried.

This test checks logging only, so a retry regression could still pass. Add assert mock_controller.ReportStatus.call_count == 1 to enforce the fail-fast contract.

As per coding guidelines, Python tests should provide comprehensive package coverage; the PR objective requires UNIMPLEMENTED to fail fast.

🤖 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 519 - 533, Update the UNIMPLEMENTED handling test around _report_status to
assert mock_controller.ReportStatus.call_count equals 1 after the call, ensuring
the status report is attempted once and not retried while preserving the
existing logging assertions.

Source: Coding guidelines


575-610: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover DEADLINE_EXCEEDED as well.

Parameterize or duplicate this scenario for both UNAVAILABLE and DEADLINE_EXCEEDED; currently the retry classification and backoff behavior for the second transient code are untested.

As per coding guidelines, Python tests should provide comprehensive package coverage; the PR objective explicitly requires retries for both transient gRPC codes.

🤖 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 575 - 610, Extend test_report_status_retries_on_transient_failure to
exercise both grpc.StatusCode.UNAVAILABLE and grpc.StatusCode.DEADLINE_EXCEEDED,
preferably by parameterizing the test. Preserve the existing
fail-twice-then-succeed assertions, including three calls and backoff values
[1.0, 2.0], for each transient status code.

Source: Coding guidelines


644-686: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent lease-release fallback for terminal errors.

This test covers only UNAVAILABLE. In the supplied _request_lease_release implementation, every non-UNIMPLEMENTED gRPC error and every non-gRPC exception sets should_retry, so PERMISSION_DENIED and ConnectionError still trigger the Phase 2 status call. Add fail-fast cases for both and restrict fallback to UNAVAILABLE/DEADLINE_EXCEEDED only.

As per coding guidelines, Python tests should provide comprehensive package coverage; the PR objective states that other gRPC errors and non-gRPC exceptions are not retried.

🤖 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 644 - 686, Extend the _request_lease_release tests with fail-fast cases
for gRPC PERMISSION_DENIED and non-gRPC ConnectionError, asserting Phase 2
_report_status is not called. Update _request_lease_release so fallback retry
occurs only for UNAVAILABLE and DEADLINE_EXCEEDED; propagate or handle all other
gRPC statuses and non-gRPC exceptions without issuing a status-only request.

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 `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 519-533: Update the UNIMPLEMENTED handling test around
_report_status to assert mock_controller.ReportStatus.call_count equals 1 after
the call, ensuring the status report is attempted once and not retried while
preserving the existing logging assertions.
- Around line 575-610: Extend test_report_status_retries_on_transient_failure to
exercise both grpc.StatusCode.UNAVAILABLE and grpc.StatusCode.DEADLINE_EXCEEDED,
preferably by parameterizing the test. Preserve the existing
fail-twice-then-succeed assertions, including three calls and backoff values
[1.0, 2.0], for each transient status code.
- Around line 644-686: Extend the _request_lease_release tests with fail-fast
cases for gRPC PERMISSION_DENIED and non-gRPC ConnectionError, asserting Phase 2
_report_status is not called. Update _request_lease_release so fallback retry
occurs only for UNAVAILABLE and DEADLINE_EXCEEDED; propagate or handle all other
gRPC statuses and non-gRPC exceptions without issuing a status-only request.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a92aadf3-e336-4fa1-885a-5cdddd40e53a

📥 Commits

Reviewing files that changed from the base of the PR and between 450f966 and 1472f04.

📒 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

Add assertion to verify UNIMPLEMENTED fails fast without retry,
addressing CodeRabbit review finding. Also document the conservative
retry approach for non-transient errors (retries all errors except
UNIMPLEMENTED) with a note about the alternative stricter approach.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@evakhoni
evakhoni requested a review from mangelajo July 23, 2026 21:39
Comment thread python/packages/jumpstarter/jumpstarter/exporter/exporter.py Outdated
evakhoni and others added 2 commits July 27, 2026 14:35
Replace the two-phase ReportStatus(release_lease=true) approach with:
- Primary path: ReleaseLease RPC (semantically correct, retry-safe)
- Fallback: ReportStatus with release_lease=true for old controllers

Changes:
- Add _send_release_lease_rpc() with enum return type for clarity
- Detect auth rejection (PERMISSION_DENIED/UNAUTHENTICATED/UNIMPLEMENTED)
  and cache in _release_lease_unsupported to skip on subsequent leases
- Fallback uses non-AVAILABLE status to prevent race condition where
  retry could release a newly-assigned lease (controller identifies
  lease by exporter.Status.LeaseRef, not request)
- Mark fallback as deprecated with TODO for removal
- Update all tests for new flow

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Extend ReleaseLease to support both client and exporter authentication.
Try authenticateExporter first, then authenticateClient, allowing both
entity types to release their leases via the same RPC.

Add releaseLeaseAsExporter() method with ExporterRef ownership check and
idempotency guards (already-ended/already-releasing). Add same idempotency
guards to client path. Include 4 integration tests for exporter-auth scenarios.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.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.

Actionable comments posted: 1

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

647-973: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared mock/stub helper for the seven _request_lease_release tests.

Each of test_request_lease_release_succeeds_on_first_attempt, test_request_lease_release_retries_on_transient_failure, test_request_lease_release_exhausts_retries, test_request_lease_release_falls_back_on_unsupported, test_request_lease_release_skips_release_lease_when_cached_unsupported, test_request_lease_release_preserves_failure_status_in_fallback, and test_request_lease_release_unimplemented repeats the same ~10-line mock_controller = AsyncMock() / stub_ctx.__aenter__/__aexit__ boilerplate. The existing _setup_mock_controller_stub helper (used for the ReportStatus-only tests above) doesn't cover the dual ReleaseLease+ReportStatus side-effect case these tests need. Extracting a small helper (e.g. _setup_mock_controller_stub(release_lease_effect=None, report_status_effect=None)) would cut this duplication and make future lease-release tests cheaper to add.

Test logic/assertions themselves are correct and consistent with the _send_release_lease_rpc/_request_lease_release retry, fallback, and caching contract.

🤖 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 647 - 973, Extract the repeated AsyncMock controller and async
stub-context setup from the seven _request_lease_release tests into a shared
helper, extending or adding _setup_mock_controller_stub to accept separate
release_lease_effect and report_status_effect arguments. Update each test to use
the helper while preserving its existing side effects, assertions, and
ReleaseLease/ReportStatus 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 `@controller/internal/service/controller_service.go`:
- Around line 1049-1070: Update releaseLeaseAsExporter to return explicit gRPC
status errors for each failure path: map lease retrieval and patch failures to
an appropriate retryable/transient status while preserving the underlying error,
and map the exporter ownership denial to codes.PermissionDenied with the
existing message. Keep successful and idempotent release behavior unchanged so
_send_release_lease_rpc can classify these cases correctly.

---

Nitpick comments:
In `@python/packages/jumpstarter/jumpstarter/exporter/exporter_test.py`:
- Around line 647-973: Extract the repeated AsyncMock controller and async
stub-context setup from the seven _request_lease_release tests into a shared
helper, extending or adding _setup_mock_controller_stub to accept separate
release_lease_effect and report_status_effect arguments. Update each test to use
the helper while preserving its existing side effects, assertions, and
ReleaseLease/ReportStatus 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e7d74f45-9766-4dc2-8b69-d5630b218d13

📥 Commits

Reviewing files that changed from the base of the PR and between bd51658 and 4a62ca0.

📒 Files selected for processing (4)
  • controller/internal/service/controller_service.go
  • controller/internal/service/controller_service_integration_test.go
  • 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 controller/internal/service/controller_service.go Outdated
req *pb.ReleaseLeaseRequest,
) (*pb.ReleaseLeaseResponse, error) {
// Try exporter auth first, then client auth
if exporter, err := s.authenticateExporter(ctx); err == nil {

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.

hmm this might spam the logs with errors in the common flow where the client does the releasing?

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 check like this might work, not super pretty because it returns []string:

  func (s *Auth) IsExporter(ctx context.Context) bool {
      md, ok := metadata.FromIncomingContext(ctx)
      if !ok { return false }
      kinds := md.Get("jumpstarter-kind")
      return len(kinds) == 1 && kinds[0] == "Exporter"
  }

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.

good idea. I actually already asked claude about that one before, and it convinced me that no error is expected 🤦
anyway, implemented in 834fde2

Address code review feedback on PR jumpstarter-dev#911:

1. Route ReleaseLease calls based on jumpstarter-kind metadata instead of
   try-exporter-then-client to eliminate spurious "exporter authentication
   failed" log entries when clients release leases. Add Auth.IsExporter()
   helper and comprehensive tests.

2. Return semantic gRPC status codes from releaseLeaseAsExporter instead of
   fmt.Errorf (which becomes codes.Unknown):
   - NotFound when lease doesn't exist
   - FailedPrecondition when exporter doesn't own the lease
   - Internal for k8s API errors

   These codes improve observability (can distinguish ownership failures from
   API errors in metrics) while remaining safe: none trigger the Python
   exporter's UNSUPPORTED fallback (PERMISSION_DENIED, INVALID_ARGUMENT,
   UNAUTHENTICATED, UNIMPLEMENTED), so all map to FAILURE as intended.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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 AfterLeaseHook status permanently if gRPC status update fails

3 participants