Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit ce4f2aa

Browse files
committed
Fix exit on hook failure and exit code handling
1 parent a302f24 commit ce4f2aa

3 files changed

Lines changed: 43 additions & 9 deletions

File tree

packages/jumpstarter-cli/jumpstarter_cli/run.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,22 @@ async def signal_handler():
7676
except* Exception as excgroup:
7777
_handle_exporter_exceptions(excgroup)
7878

79+
# Check if exporter set an exit code (e.g., from hook failure with on_failure='exit')
80+
exporter_exit_code = exporter.exit_code
81+
7982
# Cancel the signal handler after exporter completes
8083
signal_tg.cancel_scope.cancel()
8184

82-
# Return signal number if received, otherwise 0 for immediate restart
83-
return received_signal if received_signal else 0
85+
# Return exit code in priority order:
86+
# 1. Signal number if received (for signal-based termination)
87+
# 2. Exporter's exit code if set (for hook failure with on_failure='exit')
88+
# 3. 0 for immediate restart (normal exit without signal or explicit exit code)
89+
if received_signal:
90+
return received_signal
91+
elif exporter_exit_code is not None:
92+
return exporter_exit_code
93+
else:
94+
return 0
8495

8596
sys.exit(anyio.run(serve_with_graceful_shutdown))
8697

packages/jumpstarter/jumpstarter/exporter/exporter.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ class Exporter(AsyncContextManagerMixin, Metadata):
139139
determine when to trigger before-lease and after-lease hooks.
140140
"""
141141

142+
_exit_code: int | None = field(init=False, default=None)
143+
"""Exit code to use when the exporter shuts down.
144+
145+
When set to a non-zero value, the exporter should terminate permanently
146+
(not restart). This is used by hooks with on_failure='exit' to signal
147+
that the exporter should shut down and not be restarted by the CLI.
148+
"""
149+
142150
_lease_context: LeaseContext | None = field(init=False, default=None)
143151
"""Encapsulates all resources associated with the current lease.
144152
@@ -157,13 +165,17 @@ class Exporter(AsyncContextManagerMixin, Metadata):
157165
a reference holder and doesn't manage resource lifecycles directly.
158166
"""
159167

160-
def stop(self, wait_for_lease_exit=False, should_unregister=False):
168+
def stop(self, wait_for_lease_exit=False, should_unregister=False, exit_code: int | None = None):
161169
"""Signal the exporter to stop.
162170
163171
Args:
164172
wait_for_lease_exit (bool): If True, wait for the current lease to exit before stopping.
165173
should_unregister (bool): If True, unregister from controller. Otherwise rely on heartbeat.
174+
exit_code (int | None): If set, the exporter will exit with this code (non-zero means no restart).
166175
"""
176+
# Set exit code if provided
177+
if exit_code is not None:
178+
self._exit_code = exit_code
167179

168180
# Stop immediately if not started yet or if immediate stop is requested
169181
if (not self._started or not wait_for_lease_exit) and self._tg is not None:
@@ -178,6 +190,15 @@ def stop(self, wait_for_lease_exit=False, should_unregister=False):
178190
self._stop_requested = True
179191
logger.info("Exporter marked for stop upon lease exit")
180192

193+
@property
194+
def exit_code(self) -> int | None:
195+
"""Get the exit code for the exporter.
196+
197+
Returns:
198+
The exit code if set, or None if the exporter should restart.
199+
"""
200+
return self._exit_code
201+
181202
async def _get_controller_stub(self) -> jumpstarter_pb2_grpc.ControllerServiceStub:
182203
"""Create and return a controller service stub."""
183204
return jumpstarter_pb2_grpc.ControllerServiceStub(await self.channel_factory())

packages/jumpstarter/jumpstarter/exporter/hooks.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ async def run_before_lease_hook(
274274
self,
275275
lease_scope: "LeaseContext",
276276
report_status: Callable[["ExporterStatus", str], Awaitable[None]],
277-
shutdown: Callable[[], None],
277+
shutdown: Callable[..., None],
278278
) -> None:
279279
"""Execute before-lease hook with full orchestration.
280280
@@ -288,7 +288,7 @@ async def run_before_lease_hook(
288288
Args:
289289
lease_scope: LeaseScope containing session, socket_path, and sync event
290290
report_status: Async callback to report status changes to controller
291-
shutdown: Callback to trigger exporter shutdown on critical failures
291+
shutdown: Callback to trigger exporter shutdown (accepts optional exit_code kwarg)
292292
"""
293293
try:
294294
# Wait for lease scope to be fully populated by handle_lease
@@ -334,7 +334,8 @@ async def run_before_lease_hook(
334334
f"beforeLease hook failed (on_failure=exit, shutting down): {e}",
335335
)
336336
logger.error("Shutting down exporter due to beforeLease hook failure with on_failure='exit'")
337-
shutdown()
337+
# Exit code 1 tells the CLI not to restart the exporter
338+
shutdown(exit_code=1)
338339
else:
339340
# on_failure='endLease' - just block this lease, exporter stays available
340341
logger.error("beforeLease hook failed with on_failure='endLease': %s", e)
@@ -360,7 +361,7 @@ async def run_after_lease_hook(
360361
self,
361362
lease_scope: "LeaseContext",
362363
report_status: Callable[["ExporterStatus", str], Awaitable[None]],
363-
shutdown: Callable[[], None],
364+
shutdown: Callable[..., None],
364365
) -> None:
365366
"""Execute after-lease hook with full orchestration.
366367
@@ -374,7 +375,7 @@ async def run_after_lease_hook(
374375
Args:
375376
lease_scope: LeaseScope containing session, socket_path, and client info
376377
report_status: Async callback to report status changes to controller
377-
shutdown: Callback to trigger exporter shutdown on critical failures
378+
shutdown: Callback to trigger exporter shutdown (accepts optional exit_code kwarg)
378379
"""
379380
try:
380381
# Verify lease scope is ready - for after-lease this should always be true
@@ -412,7 +413,8 @@ async def run_after_lease_hook(
412413
f"afterLease hook failed (on_failure=exit, shutting down): {e}",
413414
)
414415
logger.error("Shutting down exporter due to afterLease hook failure with on_failure='exit'")
415-
shutdown()
416+
# Exit code 1 tells the CLI not to restart the exporter
417+
shutdown(exit_code=1)
416418
else:
417419
# on_failure='endLease' - lease already ended, just report the failure
418420
# The exporter remains available for new leases

0 commit comments

Comments
 (0)