-
Notifications
You must be signed in to change notification settings - Fork 1
ENG-822 - Sandbox provisioning failures are transient but unclassified — the in-run retry never covers them #282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Advisory (non-blocking), from the code-review lens: this This may well be intentional — the exception only carries a Filed as a non-blocking follow-up: ENG-866. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,25 +5,42 @@ | |
| from datetime import UTC, datetime, timedelta | ||
| from pathlib import Path | ||
|
|
||
| import asyncssh | ||
| from drukbox_sdk import SandboxAPI, SandboxHost | ||
| from drukbox_sdk.exceptions import ( | ||
| SandboxAPIError, | ||
| SandboxNotFoundError, | ||
| SandboxProvisioningError, | ||
| SandboxUnavailableError, | ||
| ) | ||
| from uuid_utils import uuid7 | ||
|
|
||
| from druks.harnesses.exceptions import HarnessSandboxProvisioningError | ||
| from druks.settings import load_settings | ||
|
|
||
| from .constants import SANDBOX_HOST_LEASE_SECONDS | ||
| from .exceptions import HostGone, SandboxUnreachable | ||
| from .exceptions import HostGone, SandboxError, SandboxUnreachable | ||
| from .host import Sandbox | ||
| from .layout import get_helper_script_path, get_remote_home | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _DRUKS_SANDBOX_LOCAL_SCRIPT = Path(__file__).parent / "druks-sandbox.sh" | ||
|
|
||
| # A freshly created host that we can't reach or set up over SSH is a | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. trim this comment and use simplified english |
||
| # provisioning failure in the same sense as a create-time control-plane one: | ||
| # the VM was never usable. These are the shapes such reachability/setup | ||
| # failures take — SSH connect/auth/channel errors (asyncssh), socket-level | ||
| # failures and connect timeouts (OSError/TimeoutError), and the sandbox | ||
| # layer's own SandboxUnreachable/SandboxError. Cancellation and unrelated | ||
| # local/programming bugs are deliberately excluded so they propagate untouched. | ||
| _ACQUIRE_SETUP_REACHABILITY_ERRORS = ( | ||
| SandboxError, | ||
| asyncssh.Error, | ||
| OSError, | ||
| TimeoutError, | ||
| ) | ||
|
|
||
|
|
||
| class Client: | ||
| """Ambient client for the drukbox control plane. | ||
|
|
@@ -80,13 +97,25 @@ async def acquire( | |
| # Fixed lease: drukbox reaps the host when this lapses, so a run whose | ||
| # worker dies frees its VM without a druks-side reconciler. | ||
| expires_at = datetime.now(UTC) + timedelta(seconds=SANDBOX_HOST_LEASE_SECONDS) | ||
| record = await api.create_host( | ||
| expires_at=expires_at, | ||
| env=sandbox_env, | ||
| idempotency_key=key, | ||
| image=image or None, | ||
| provider=provider, | ||
| ) | ||
| try: | ||
| record = await api.create_host( | ||
| expires_at=expires_at, | ||
| env=sandbox_env, | ||
| idempotency_key=key, | ||
| image=image or None, | ||
| provider=provider, | ||
| ) | ||
| except (SandboxProvisioningError, SandboxUnavailableError) as exc: | ||
| # Transient control-plane failures — a 502 the service raises | ||
| # when the provider/Tailscale/keyscan step fails, or a | ||
| # transport/503 SandboxUnavailableError. Classify them into the | ||
| # in-run retry path so a slow provider window recovers instead | ||
| # of dead-ending the run. Fatal SDK errors (auth, validation, | ||
| # conflict, not-found, generic response) are subclasses of the | ||
| # untouched SandboxAPIError base and fall through unretried. | ||
| raise HarnessSandboxProvisioningError( | ||
| f"sandbox host provisioning failed: {exc}" | ||
| ) from exc | ||
| logger.info("sandbox host created id=%s", record.id) | ||
| key_path = settings.sandbox_keys_dir / record.id | ||
| if record.private_key: | ||
|
|
@@ -96,11 +125,21 @@ async def acquire( | |
| sandbox = Sandbox(record=record) | ||
| try: | ||
| await _upload_helper_script(sandbox) | ||
| except BaseException: | ||
| except BaseException as error: | ||
| # Caller never sees this host; release rather than orphan. | ||
| await sandbox.aclose() | ||
| await self._best_effort_delete(api, record.id) | ||
| key_path.unlink(missing_ok=True) | ||
| # A fresh VM that never became reachable/usable during SSH + | ||
| # helper setup is a provisioning failure just like a create-time | ||
| # one, so classify it into the same transient-retry path — the | ||
| # raw error would otherwise bypass agent retry and record an | ||
| # empty failure code. Cancellation and unrelated | ||
| # local/programming errors keep their own type and propagate. | ||
| if isinstance(error, _ACQUIRE_SETUP_REACHABILITY_ERRORS): | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer except _ACQUIRE_SETUP_REACHABILITY_ERRORS with a shortened version of this comment, and its ok to repeat the sandbox aclose and other stuff, to avoid the isinstance which just does what except does |
||
| raise HarnessSandboxProvisioningError( | ||
| f"sandbox host {record.id} unreachable during setup: {error}" | ||
| ) from error | ||
| raise | ||
| try: | ||
| yield sandbox | ||
|
|
@@ -139,6 +178,14 @@ async def attach(self, *, host_id: str) -> AsyncIterator[Sandbox]: | |
| raise HostGone( | ||
| f"sandbox host {host_id} no longer exists", | ||
| ) from exc | ||
| except SandboxUnavailableError as exc: | ||
| # Transport/503 while looking up an existing host — transient, | ||
| # so classify it the same as a create-time control-plane | ||
| # failure and let the in-run retry re-attach once the service | ||
| # recovers. | ||
| raise HarnessSandboxProvisioningError( | ||
| f"sandbox host {host_id} lookup failed: {exc}" | ||
| ) from exc | ||
| sandbox = Sandbox(record=record) | ||
| try: | ||
| yield sandbox | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
trim this docstring and use simplified english