-
Notifications
You must be signed in to change notification settings - Fork 13
Milestone
Description
The lease waiting/handling mechanism needs several improvements:
- The timeout waiting for a lease should be configurable
- The lease waiting progress should be informative, not just waiting in silence.
- If timeout happens, a real error message should be provided instead of a huge traceback, this is specially noticeable when using the pytest class.
$ jmp shell -l name=${JETSON_HARDWARE_RUNNER} --duration 01:00:00 ./scripts/testing/test-rhel-bootc-via-new-jumpstarter.sh ${BOOTC_TEST_IMAGE} ${JETSON_HARDWARE_RUNNER}
[09/29/25 10:10:27] INFO INFO:jumpstarter.client.lease:Created lease.py:60
lease request for selector
name=nvidia-jetson-nx-orin-01-khw-eng-b
os2-beaker for duration 1:00:00
╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/_core/_tas │
│ ks.py:120 in fail_after │
│ │
│ 117 │ with get_async_backend().create_cancel_scope( │
│ 118 │ │ deadline=deadline, shield=shield │
│ 119 │ ) as cancel_scope: │
│ ❱ 120 │ │ yield cancel_scope │
│ 121 │ │
│ 122 │ if cancel_scope.cancelled_caught and current_time() >= cancel_scop │
│ 123 │ │ raise TimeoutError │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter/clie │
│ nt/lease.py:137 in _acquire │
│ │
│ 134 │ │ │ │ if condition_present_and_equal(result.conditions, "Rea │
│ 135 │ │ │ │ │ raise LeaseError(f"lease {self.name} released") │
│ 136 │ │ │ │ │
│ ❱ 137 │ │ │ │ await sleep(1) │
│ 138 │ │
│ 139 │ @asynccontextmanager │
│ 140 │ async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/_core/_eve │
│ ntloop.py:87 in sleep │
│ │
│ 84 │ :param delay: the duration, in seconds │
│ 85 │ │
│ 86 │ """ │
│ ❱ 87 │ return await get_async_backend().sleep(delay) │
│ 88 │
│ 89 │
│ 90 async def sleep_forever() -> None: │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/_backends/ │
│ _asyncio.py:2369 in sleep │
│ │
│ 2366 │ │
│ 2367 │ @classmethod │
│ 2368 │ async def sleep(cls, delay: float) -> None: │
│ ❱ 2369 │ │ await sleep(delay) │
│ 2370 │ │
│ 2371 │ @classmethod │
│ 2372 │ def create_cancel_scope( │
│ │
│ /usr/lib64/python3.12/asyncio/tasks.py:665 in sleep │
│ │
│ 662 │ │ │ │ │ │ futures._set_result_unless_cancelled, │
│ 663 │ │ │ │ │ │ future, result) │
│ 664 │ try: │
│ ❱ 665 │ │ return await future │
│ 666 │ finally: │
│ 667 │ │ h.cancel() │
│ 668 │
╰──────────────────────────────────────────────────────────────────────────────╯
CancelledError: Cancelled via cancel scope ffffa76d6660; reason: deadline
exceeded
During handling of the above exception, another exception occurred:
╭───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /tmp/.local/jumpstarter/venv/bin/jmp:8 in <module> │
│ │
│ 5 from jumpstarter_cli.jmp import jmp │
│ 6 if __name__ == '__main__': │
│ 7 │ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) │
│ ❱ 8 │ sys.exit(jmp()) │
│ 9 │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/click/core.py:14 │
│ 62 in __call__ │
│ │
│ 1459 │ │
│ 1460 │ def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: │
│ 1461 │ │ """Alias for :meth:`main`.""" │
│ ❱ 1462 │ │ return self.main(*args, **kwargs) │
│ 1463 │
│ 1464 │
│ 1465 class _FakeSubclassCheck(type): │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/click/core.py:13 │
│ 83 in main │
│ │
│ 1380 │ │ try: │
│ 1381 │ │ │ try: │
│ 1382 │ │ │ │ with self.make_context(prog_name, args, **extra) as c │
│ ❱ 1383 │ │ │ │ │ rv = self.invoke(ctx) │
│ 1384 │ │ │ │ │ if not standalone_mode: │
│ 1385 │ │ │ │ │ │ return rv │
│ 1386 │ │ │ │ │ # it's not safe to `ctx.exit(rv)` here! │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/click/core.py:18 │
│ 50 in invoke │
│ │
│ 1847 │ │ │ │ super().invoke(ctx) │
│ 1848 │ │ │ │ sub_ctx = cmd.make_context(cmd_name, args, parent=ctx │
│ 1849 │ │ │ │ with sub_ctx: │
│ ❱ 1850 │ │ │ │ │ return _process_result(sub_ctx.command.invoke(sub │
│ 1851 │ │ │
│ 1852 │ │ # In chain mode we create the contexts step by step, but afte │
│ 1853 │ │ # base command has been invoked. Because at that point we do │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/click/core.py:12 │
│ 46 in invoke │
│ │
│ 1243 │ │ │ echo(style(message, fg="red"), err=True) │
│ 1244 │ │ │
│ 1245 │ │ if self.callback is not None: │
│ ❱ 1246 │ │ │ return ctx.invoke(self.callback, **ctx.params) │
│ 1247 │ │
│ 1248 │ def shell_complete(self, ctx: Context, incomplete: str) -> list[C │
│ 1249 │ │ """Return a list of completions for the incomplete value. Loo │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/click/core.py:81 │
│ 4 in invoke │
│ │
│ [811](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L811) │ │ │
│ [812](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L812) │ │ with augment_usage_errors(self): │
│ [813](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L813) │ │ │ with ctx: │
│ ❱ [814](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L814) │ │ │ │ return callback(*args, **kwargs) │
│ [815](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L815) │ │
│ [816](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L816) │ def forward(self, cmd: Command, /, *args: t.Any, **kwargs: t.Any) │
│ [817](https://gitlab.com/redhat/rhel/sst/orin-sidecar/nvidia-jetson-sidecar/-/jobs/11531929594#L817) │ │ """Similar to :meth:`invoke` but fills in default keyword │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter_cli_ │
│ common/config.py:94 in wrapper │
│ │
│ 91 │ │ except Exception as e: │
│ 92 │ │ │ raise click.ClickException("Failed to load config: {}".for │
│ 93 │ │ │
│ ❱ 94 │ │ return f(*args, **kwds, config=config) │
│ 95 │ │
│ 96 │ return reduce(lambda w, opt: opt(w), options, wrapper) │
│ 97 │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter_cli_ │
│ common/exceptions.py:56 in wrapped │
│ │
│ 53 │ │ @wraps(func) │
│ 54 │ │ def wrapped(*args, **kwargs): │
│ 55 │ │ │ try: │
│ ❱ 56 │ │ │ │ return func(*args, **kwargs) │
│ 57 │ │ │ except ConnectionError as e: │
│ 58 │ │ │ │ if "expired" in str(e).lower(): │
│ 59 │ │ │ │ │ click.echo(click.style("Token is expired, triggeri │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter_cli/ │
│ shell.py:52 in shell │
│ │
│ 49 │ │ │ │ │ command=command, │
│ 50 │ │ │ │ ) │
│ 51 │ │ │ │
│ ❱ 52 │ │ │ with config.lease(selector=selector, lease_name=lease_name, │
│ 53 │ │ │ │ with lease.serve_unix() as path: │
│ 54 │ │ │ │ │ with lease.monitor(): │
│ 55 │ │ │ │ │ │ if exporter_logs: │
│ │
│ /usr/lib64/python3.12/contextlib.py:137 in __enter__ │
│ │
│ 134 │ │ # they are only needed for recreation, which is not possible a │
│ 135 │ │ del self.args, self.kwds, self.func │
│ 136 │ │ try: │
│ ❱ 137 │ │ │ return next(self.gen) │
│ 138 │ │ except StopIteration: │
│ 139 │ │ │ raise RuntimeError("generator didn't yield") from None │
│ 140 │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter/conf │
│ ig/client.py:130 in lease │
│ │
│ 127 │ │ duration: timedelta = timedelta(minutes=30), │
│ 128 │ ): │
│ 129 │ │ with start_blocking_portal() as portal: │
│ ❱ 130 │ │ │ with portal.wrap_async_context_manager(self.lease_async(se │
│ 131 │ │ │ │ yield lease │
│ 132 │ │
│ 133 │ @_blocking_compat │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/from_threa │
│ d.py:155 in __enter__ │
│ │
│ 152 │ def __enter__(self) -> T_co: │
│ 153 │ │ self._enter_future = Future() │
│ 154 │ │ self._exit_future = self._portal.start_task_soon(self.run_asyn │
│ ❱ 155 │ │ return self._enter_future.result() │
│ 156 │ │
│ 157 │ def __exit__( │
│ 158 │ │ self, │
│ │
│ /usr/lib64/python3.12/concurrent/futures/_base.py:456 in result │
│ │
│ 453 │ │ │ │ if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: │
│ 454 │ │ │ │ │ raise CancelledError() │
│ 455 │ │ │ │ elif self._state == FINISHED: │
│ ❱ 456 │ │ │ │ │ return self.__get_result() │
│ 457 │ │ │ │ else: │
│ 458 │ │ │ │ │ raise TimeoutError() │
│ 459 │ │ finally: │
│ │
│ /usr/lib64/python3.12/concurrent/futures/_base.py:401 in __get_result │
│ │
│ 398 │ def __get_result(self): │
│ 399 │ │ if self._exception: │
│ 400 │ │ │ try: │
│ ❱ 401 │ │ │ │ raise self._exception │
│ 402 │ │ │ finally: │
│ 403 │ │ │ │ # Break a reference cycle with the exception in self._ │
│ 404 │ │ │ │ self = None │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/from_threa │
│ d.py:252 in _call_func │
│ │
│ 249 │ │ │ │ │ else: │
│ 250 │ │ │ │ │ │ future.add_done_callback(callback) │
│ 251 │ │ │ │ │ │
│ ❱ 252 │ │ │ │ │ retval = await retval_or_awaitable │
│ 253 │ │ │ else: │
│ 254 │ │ │ │ retval = retval_or_awaitable │
│ 255 │ │ except self._cancelled_exc_class: │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/from_threa │
│ d.py:131 in run_async_cm │
│ │
│ 128 │ async def run_async_cm(self) -> bool | None: │
│ 129 │ │ try: │
│ 130 │ │ │ self._exit_event = Event() │
│ ❱ 131 │ │ │ value = await self._async_cm.__aenter__() │
│ 132 │ │ except BaseException as exc: │
│ 133 │ │ │ self._enter_future.set_exception(exc) │
│ 134 │ │ │ raise │
│ │
│ /usr/lib64/python3.12/contextlib.py:210 in __aenter__ │
│ │
│ 207 │ │ # they are only needed for recreation, which is not possible a │
│ 208 │ │ del self.args, self.kwds, self.func │
│ 209 │ │ try: │
│ ❱ 210 │ │ │ return await anext(self.gen) │
│ 211 │ │ except StopAsyncIteration: │
│ 212 │ │ │ raise RuntimeError("generator didn't yield") from None │
│ 213 │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter/conf │
│ ig/client.py:249 in lease_async │
│ │
│ 246 │ │ # when no lease name is provided, release the lease on exit │
│ 247 │ │ release_lease = lease_name == "" │
│ 248 │ │ try: │
│ ❱ 249 │ │ │ async with Lease( │
│ 250 │ │ │ │ channel=await self.channel(), │
│ 251 │ │ │ │ namespace=self.metadata.namespace, │
│ 252 │ │ │ │ name=lease_name, │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/_core/_con │
│ textmanagers.py:163 in __aenter__ │
│ │
│ 160 │ │ │ │ f"'yield' statement?" │
│ 161 │ │ │ ) │
│ 162 │ │ │
│ ❱ 163 │ │ value = await cm.__aenter__() │
│ 164 │ │ self.__cm = cm │
│ 165 │ │ return value │
│ 166 │
│ │
│ /usr/lib64/python3.12/contextlib.py:210 in __aenter__ │
│ │
│ 207 │ │ # they are only needed for recreation, which is not possible a │
│ 208 │ │ del self.args, self.kwds, self.func │
│ 209 │ │ try: │
│ ❱ 210 │ │ │ return await anext(self.gen) │
│ 211 │ │ except StopAsyncIteration: │
│ 212 │ │ │ raise RuntimeError("generator didn't yield") from None │
│ 213 │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter/clie │
│ nt/lease.py:141 in __asynccontextmanager__ │
│ │
│ 138 │ │
│ 139 │ @asynccontextmanager │
│ 140 │ async def __asynccontextmanager__(self) -> AsyncGenerator[Self]: │
│ ❱ 141 │ │ value = await self.request_async() │
│ 142 │ │ try: │
│ 143 │ │ │ yield value │
│ 144 │ │ finally: │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter/clie │
│ nt/lease.py:102 in request_async │
│ │
│ 99 │ │ │ │ await self._create() │
│ 100 │ │ else: │
│ 101 │ │ │ await self._create() │
│ ❱ 102 │ │ return await self._acquire() │
│ 103 │ │
│ 104 │ async def _acquire(self): │
│ 105 │ │ """Acquire a lease. │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/jumpstarter/clie │
│ nt/lease.py:109 in _acquire │
│ │
│ 106 │ │ │
│ 107 │ │ Makes sure the lease is ready, and returns the lease object. │
│ 108 │ │ """ │
│ ❱ 109 │ │ with fail_after(300): # TODO: configurable timeout │
│ 110 │ │ │ while True: │
│ 111 │ │ │ │ logger.debug("Polling Lease %s", self.name) │
│ 112 │ │ │ │ result = await self.get() │
│ │
│ /usr/lib64/python3.12/contextlib.py:158 in __exit__ │
│ │
│ 155 │ │ │ │ # tell if we get the same exception back │
│ 156 │ │ │ │ value = typ() │
│ 157 │ │ │ try: │
│ ❱ 158 │ │ │ │ self.gen.throw(value) │
│ 159 │ │ │ except StopIteration as exc: │
│ 160 │ │ │ │ # Suppress StopIteration *unless* it's the same except │
│ 161 │ │ │ │ # was passed to throw(). This prevents a StopIteratio │
│ │
│ /tmp/.local/jumpstarter/venv/lib64/python3.12/site-packages/anyio/_core/_tas │
│ ks.py:123 in fail_after │
│ │
│ 120 │ │ yield cancel_scope │
│ 121 │ │
│ 122 │ if cancel_scope.cancelled_caught and current_time() >= cancel_scop │
│ ❱ 123 │ │ raise TimeoutError │
│ 124 │
│ 125 │
│ 126 def move_on_after(delay: float | None, shield: bool = False) -> Cancel │
╰──────────────────────────────────────────────────────────────────────────────╯
Reactions are currently unavailable