Add @tenki compute backend (Tenki Sandbox microVMs) - #3311
Open
AlvaroDeleglise wants to merge 7 commits into
Open
AlvaroDeleglise wants to merge 7 commits into
AlvaroDeleglise wants to merge 7 commits into
Conversation
Run each flow step inside a disposable Tenki Sandbox Linux microVM, analogous to @kubernetes/@Batch. Adds metaflow/plugins/tenki/ (decorator + trampoline CLI + runner + a lazily-imported SDK wrapper with a >=0.4.0 version guard), the TENKI_* config block, backend registration (two lines in plugins/__init__.py), and unit tests. One line adds "tenki" to conda_decorator's remote-backend list so @pypi/@conda treats it like the other remote backends. Closes Netflix#3310.
Contributor
Greptile SummaryThis PR adds Tenki Sandbox microVMs as an opt-in Metaflow compute backend. The main changes are:
Confidence Score: 5/5This looks safe to merge.
|
| Filename | Overview |
|---|---|
| metaflow/plugins/tenki/tenki.py | Adds sandbox launch, remote execution, log streaming, result handling, retry classification, tagging, and cleanup. |
| metaflow/plugins/tenki/tenki_cli.py | Adds the task trampoline and flow-scoped sandbox list and kill commands. |
| metaflow/plugins/tenki/tenki_client.py | Adds a lazily loaded Tenki SDK wrapper with version checks and shared client configuration. |
| metaflow/plugins/tenki/tenki_decorator.py | Adds step lifecycle integration, resource handling, datastore validation, and runtime metadata. |
| metaflow/plugins/init.py | Registers the Tenki decorator and CLI with the plugin system. |
| metaflow/plugins/pypi/conda_decorator.py | Preserves the remote trampoline interpreter for Tenki steps using Conda or PyPI environments. |
Reviews (7): Last reviewed commit: "Classify launch errors even when the Ten..." | Re-trigger Greptile
An unscoped `tenki kill` filtered only on the shared "metaflow" tag, so in a shared Tenki project it could terminate sandboxes of other flows/users. Tag each sandbox with the flow at launch (metaflow-flow:<flow>) and adopt the @kubernetes/@Batch parse_cli_options semantics (flow-scoped, --my-runs, default to the latest run of the current flow). `tenki kill` with no flags now only touches the current flow's latest run — never another flow's or user's sandboxes.
The kill loop broke out after the first teardown failure, so a sandbox could be left running without trying the remaining methods, and it printed a "Failed" line eagerly even when a later method would have succeeded. Extract the loop into _terminate_sandboxes: try all methods, report a failure only when none succeeds (no false alarm), and print an accurate terminated/failed summary. Mirrors tenki.py._cleanup.
The launch path exited with METAFLOW_EXIT_DISALLOW_RETRY on any exception, so a transient Tenki API/network failure aborted the flow even under @Retry — launch_job does synchronous network I/O (Client auth, who_am_i, create), where such blips are a real mode. Add is_permanent_launch_error(): defer to the SDK's own `retryable` flag for SDK errors (UNAVAILABLE / rate-limit are retryable; auth / permission / quota / bad-image are not), treat command/session timeouts and a client-side deadline as transient (consistent with _interpret_result), and default unknown/non-SDK errors to permanent so @Retry never loops on a misconfig. Permanent errors still exit DISALLOW_RETRY; transient ones exit non-zero so the runtime retries.
The Tenki SDK is now published as `tenki` (0.5.4, was `tenki-sandbox`) and the service dropped the project requirement — `Client.create()` no longer accepts a project_id. Import `tenki` instead of `tenki_sandbox`, bump the version guard to `tenki>=0.5.4`, and remove the project_id plumbing (the TENKI_PROJECT_ID config, the who_am_i-based default_project_id resolution, and create's project_id arg). workspace_id stays as an optional passthrough. The exception taxonomy / retryable flag are unchanged, so launch/result classification, tag flow-scoping, the runtime shim, and cleanup all carry over.
The no-flag scope resolved to the flow's latest run without a user filter, so on shared storage a destructive `tenki kill` could target another user's latest run. Default an unscoped invocation to the current user's latest run (apply both the run-id and user tags); an explicit --run-id still targets that run for any owner, and --user / --my-runs are unchanged.
is_permanent_launch_error resolved SDK exception classes only via the client, so if TenkiClient() construction itself raised (self._client never set), a retryable SDK error was misread as permanent and @Retry would not relaunch. Fall back to importing the tenki module to resolve the exception hierarchy when no client is available, so the SDK's retryable flag is honored regardless.
Shriprasad-P
left a comment
There was a problem hiding this comment.
Review
PR: Add @tenki compute backend (Tenki Sandbox microVMs)
Touched: metaflow/metaflow_config.py, metaflow/plugins/__init__.py, metaflow/plugins/pypi/conda_decorator.py, metaflow/plugins/tenki/__init__.py, metaflow/plugins/tenki/tenki.py, metaflow/plugins/tenki/tenki_cli.py
- Security-sensitive change — please double-check edge cases and defaults.
- CI/tooling change — confirm the pipeline still passes on this branch.
- Diff is fairly large (+2256/-0); a short summary of risk areas from the author would help reviewers.
Commenting as a drive-by reviewer after reading the diff. Happy to look again if maintainers want a deeper pass on a specific file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Type
Summary
Add
@tenki, a new compute backend that runs each flow step inside a disposable Tenki Sandbox Linux microVM — the Tenki analogue of@kubernetes/@batch. It reuses Metaflow's existing remote-execution contract (thebash -centrypoint +METAFLOW_*env vars) and only swaps the "where it runs" layer (a K8s Job →sb.exec). Entirely additive and opt-in: datastore, metadata, cards,@retry,@resources,@catch,resume, and@pypi/@condaare unchanged.Issue
Closes #3310 (opened first, per the Core Runtime process).
What's in it
New plugin
metaflow/plugins/tenki/:tenki_decorator.py—TenkiDecorator(StepDecorator): lifecycle hooks,@resourcesmerge viacompute_resource_attributes(the@batchidiom), datastore validation (requires a remote datastore; rejectslocal/@parallel).tenki.py— runner: builds the same bash entrypoint as@kubernetes(get_package_commands+bootstrap_commands+ mflog +save_logs), creates a tagged sandbox, runssb.execon a background thread while tailing logs from the datastore, interprets the result / SDK exceptions into Metaflow's retry semantics, and tears the sandbox down.tenki_cli.py— click trampolinetenki step+ flow-scopedtenki list/tenki killcleanup commands.tenki_client.py— thin, lazily-imported wrapper over thetenkiSDK (a soft dependency, like the k8s/cloud SDKs — not insetup.py), with a>= 0.5.4version guard.Wiring / config:
metaflow/plugins/__init__.py— registertenkiinTRAMPOLINE_CLIS_DESC+STEP_DECORATORS_DESC(2 lines).metaflow/metaflow_config.py— aTENKI_*config block (TENKI_API_KEY,TENKI_BASE_URL,TENKI_WORKSPACE_ID,TENKI_CONTAINER_IMAGE,TENKI_CPU,TENKI_MEMORY,TENKI_SANDBOX_INIT_SCRIPT).metaflow/plugins/pypi/conda_decorator.py— one line (see Core Runtime rationale below).Datastores:
s3(incl. S3-compatible endpoints),azure,gs. The backend forwards only the active datastore's cloud credentials into the microVM (a microVM has no ambient cloud identity); for GS it materializes the service-account JSON inside the sandbox. Secret-backend prefixes,METAFLOW_S3_SERVER_SIDE_ENCRYPTION, and OTEL config are forwarded for parity with@kubernetes.Reproduction
A minimal flow that runs each step on a Tenki microVM (datastore = S3; validated with MinIO reached over a temporary tunnel so the sandbox can read/write it):
Evidence shows up in the parent console (per-step logs streamed from the datastore), in the artifacts (via the Client API), and in
python tenki_flow.py tenki list. On the stock Tenki image each step ran in a fresh microVM, the code package downloaded andmetaflowimported from it, and the artifact persisted across steps (self.x = 21→RESULT 42), ending inDone!.Core Runtime rationale
The only change to a shared core file is one line in
metaflow/plugins/pypi/conda_decorator.py: it keeps a hardcoded allowlist of remote backends for which it leaves the trampoline interpreter alone (interpreter = None). A backend not on that list has its localtenki steptrampoline swapped into the resolved@pypi/@condaenvironment, which then can't import the datastore/metadata dependencies — so@pypi+@tenkiwould fail. Adding"tenki"(alongsidebatch/kubernetes/nvidia) gives it the same treatment; no behavior change for other backends. A unit test drives the realCondaStepDecoratorand assertsinterpreter is Nonefor a@tenkistep (fails without the line, passes with it).Everything else is additive: the new
metaflow/plugins/tenki/package, two registration lines, and theTENKI_*config block.Why the design is sound:
@kubernetesremote-execution contract verbatim (samebash -c "…"entrypoint via the sameshlex.splittransform), so remote behavior matches an established backend; the Tenki-specific surface is small (create / exec / teardown + credential forwarding).exec→ background thread + datastore log tailing, preserving Metaflow's live log streaming despitesb.execblocking.result.ok(exit_code == 0 and not signal), not a partial exit-code check, so a signalled task is never read as success.@retry); one fresh sandbox per attempt.Failure modes considered
foreach(3 parallel microVMs) validated live.tenki list/killare always scoped to the current flow (sandboxes are taggedmetaflow-flow:<flow>at launch), adopting the@kubernetes/@batchparse_cli_optionssemantics (--my-runs, default to the latest run of the current flow). An unscopedtenki killcan never reach another flow's or user's sandboxes — verified live against Tenki.finally+atexit, retried on the atexit pass, warns if it still fails),tenki kill(tries every teardown method, reports an accurate terminated/failed summary), and a server-sidemax_durationcap. A hardSIGKILLof the orchestrator relies ontenki kill/ the cap.launch_jobdoes synchronous network I/O (auth, create), so a transient API/network failure at launch stays retryable under@retry: launch exceptions are classified via the SDK's ownretryableflag (UNAVAILABLE / rate-limit → retry; auth / permission / quota / bad-image → no retry), with unknown errors defaulting to non-retryable so@retrynever loops on a misconfig. A task killed by a signal (exit 0 but signalled) is a failure;signal/reason/errnoare surfaced; both timeout types map to a retryable timeout.python3but nopython; a startup shim provisionspython/pip (no-op on images that already have them) and fails with a clear error if the image has nopython3at all.shlex.splitthe exact way@kubernetesdoes, so the\"-escaped inner quotes resolve throughsb.exec(*argv).Tests
test/unit/test_tenki.py, 48 tests, all green; no regressions to the existing@kubernetes/ resource-merge tests.black-clean.tenkiSDK is a soft dependency, so CI unit tests run against a stubbed SDK; real-SDK integration can't run in CI without a Tenki account, so that validation is manual.Unit coverage includes: resource merge; datastore /
@parallelvalidation; sandbox-name and tag sanitization; the_commandbash contract + runtime-shim ordering + no-python3guard; runner behavior against a stubbed SDK (result.oksuccess, non-zero / OOM(137) / segfault(139) / unknown / signalled results,signal/reason/errnodiagnostics, both timeout types, session-lost, permission-denied non-retryable, stderr surfacing);_cleanupretry + warn-on-final-pass; flow-scopedlist/kill+parse_cli_optionssemantics + teardown fallback/reporting; launch-failure classification (permanent vs transient); per-datastore credential isolation + GCP JSON materialization; secrets/S3-SSE/OTEL forwarding; SDK minimum-version enforcement; and theconda_decoratorregression above.Live (real Tenki microVMs; datastore = MinIO over a tunnel): happy path (linear +
foreach+ join) and plain@tenkisteps on the stock image;@retry(fresh sandbox per attempt); concurrency (--max-workers 3);@timeout;--metadata=service(real Postgres-backed service);@catch;resume;@pypi(ran a real package inside the microVM); flow-scoped cleanup; and the tag / kill-scoping isolation.Not validated live: Azure/GS (forwarding is implemented + unit-tested; needs a real account) and a custom
TENKI_CONTAINER_IMAGE(Tenki serves images only from its own registry, so it needs a Metaflow-ready image pushed there).Non-goals
@parallel/ gang scheduling, GPU, and persistent volumes/snapshots — deferred.kubernetes/azure-*); glad to expose anextras_require={"tenki": ["tenki>=0.5.4"]}extra instead if you prefer.@kubernetes), but structured to move to ametaflow_extensionsplugin if you'd rather keep vendor backends out of core.AI Tool Usage
Tool: Claude Code. Used for: implementation scaffolding, unit tests, and the live-validation harness (MinIO/tunnel setup, e2e probes). Accountability: all code was reviewed and is understood by me; the design decisions, failure modes, and tests reflect my own judgment.