Skip to content

Add Dask Gateway support to lc run (JupyterHub deployments)#159

Draft
EiffL wants to merge 4 commits into
mainfrom
gateway-cluster-support
Draft

Add Dask Gateway support to lc run (JupyterHub deployments)#159
EiffL wants to merge 4 commits into
mainfrom
gateway-cluster-support

Conversation

@EiffL

@EiffL EiffL commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary

lc run now works on Dask Gateway deployments (our JupyterHub-on-GKE playground) with zero user configuration: on a hub pod, cd /shared/<project> && lc run requests an adaptive Gateway cluster, runs the pipeline on the spot pool, and shuts the cluster down afterwards. Setting LIGHTCONE_GATEWAY_CLUSTER=<name> instead attaches to an existing cluster (e.g. one created from the JupyterLab Dask sidebar, dashboard panels docked) and leaves it running on exit.

Why the rendezvous changes shape

Gateway scheduler addresses are not dialable: they use a custom gateway:// comm scheme (traefik multiplexing + per-cluster TLS + SNI) that only the dask-gateway client library understands — verified on the staging cluster (unknown address scheme 'gateway' from a bare Client). Since lc run and the executor plugin live in different processes, the fix is the same rendezvous-by-ambient-context pattern the SLURM branch already uses: the parent passes the cluster name through the env, and the executor rejoins by name through the authenticated Gateway API.

Concretely, cluster_for_run now yields the env overlay the child snakemake needs ({"DASK_SCHEDULER_ADDRESS": ...} on existing branches, {"LIGHTCONE_GATEWAY_CLUSTER": name} on the new one) instead of a bare address string — making the actual parent→child contract explicit.

Changes

  • engine/dask_cluster.py — fourth branch (priority: explicit address → gateway → SLURM → local): creates an adaptive cluster (minimum=1, bounded by --jobs) or attaches by name; owned clusters are shut down on exit, attached ones untouched — mirroring the address-branch convention. Fails fast when workers don't advertise the cpus/memory/gpus resource contract (otherwise every task hangs with no error).
  • snakemake_executor_plugin_dask/executor.py — client resolution extracted to _connect_client(): Gateway name → rejoin via Gateway API; else Client(DASK_SCHEDULER_ADDRESS) as before.
  • engine/site_registry.pylightcone-hub site entry detected via env markers (JUPYTERHUB_USER + DASK_GATEWAY__ADDRESS; pods have generated hostnames), checked ahead of hostname patterns in detect_current_site.
  • pyproject.toml — optional extra lightcone-cli[gateway]; mypy override for dask_gateway.
  • Docs — api/user/contributing pages updated; full design rationale + GCP deployment procedure + staging findings in docs/design/jupyterhub-dask-gateway-gcp.md.

Testing

  • 346 passed (11 new: branch routing/precedence, owned-vs-attached lifecycle, missing-dependency and resource-contract failure modes, executor rejoin, site env-marker detection); ruff and strict mypy clean.
  • The deployment-side wiring (worker resource injection, /shared RWX + flock, hub-proxied gateway) was validated end to end on the lightcone-hub GKE staging cluster. End-to-end lc run on the hub additionally needs the worker image (lc + snakemake baked in) — tracked in lightcone-hub.

🤖 Generated with Claude Code

cluster_for_run gains a fourth branch, selected when a Gateway
environment is ambient (DASK_GATEWAY__ADDRESS, as on a lightcone
JupyterHub pod) or a cluster is named via LIGHTCONE_GATEWAY_CLUSTER.
Gateway scheduler addresses use a gateway:// comm scheme that a bare
distributed.Client cannot dial, so the parent/child rendezvous changes
shape: cluster_for_run now yields the env overlay the child snakemake
needs ({DASK_SCHEDULER_ADDRESS: ...} on existing branches,
{LIGHTCONE_GATEWAY_CLUSTER: name} on the new one), and the executor
rejoins the cluster by name through the authenticated Gateway API —
the same rendezvous-by-ambient-context pattern as the SLURM branch.

Ownership mirrors the address branch: clusters we create are adaptive
(minimum=1, bounded by --jobs) and shut down on exit; clusters named
by the user (e.g. created from the JupyterLab Dask sidebar) are left
running with their scaling untouched. Startup fails fast when gateway
workers don't advertise the cpus/memory/gpus resource contract, which
otherwise hangs every task silently.

Also: a lightcone-hub site entry detected via env markers (pods have
generated hostnames, so detect_current_site now checks declared env
markers before hostname patterns), and a [gateway] optional extra for
the dask-gateway client.

Validated against the lightcone-hub GKE staging deployment; design
notes and the deployment procedure live in
docs/design/jupyterhub-dask-gateway-gcp.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
lightcone-cli 4b39e63 Commit Preview URL

Branch Preview URL
Jul 14 2026, 05:17 AM

@github-actions

Copy link
Copy Markdown

✅ Eval Results

Metric Value
Score 1.00
Build complete
Cost $2.30
Turns 84
Duration 1049s
lightcone-cli 0.3.8.dev3+g924c833cc (924c833c)
Results Download

Graders

✅ spec_valid (1.00)
✅ all_materialized (1.00)

Full output
 pygments-2.20.0 pyparsing-3.3.2 pypdf-6.14.2 pystow-0.8.21 pytest-9.1.1 pytest-logging-2015.11.4 pyyaml-6.0.3 rapidfuzz-3.14.5 rdflib-7.6.0 referencing-0.37.0 requests-2.34.2 rich-15.0.0 rpds-py-2026.6.3 smart-open-7.7.1 smmap-5.0.3 snakemake-9.23.1 snakemake-interface-common-1.23.0 snakemake-interface-executor-plugins-9.4.0 snakemake-interface-logger-plugins-2.1.0 snakemake-interface-report-plugins-1.3.0 snakemake-interface-scheduler-plugins-2.0.2 snakemake-interface-storage-plugins-4.4.1 sortedcontainers-2.4.0 sqlmodel-0.0.37 tabulate-0.10.0 tblib-3.2.2 tenacity-9.1.4 throttler-1.2.3 toolz-1.1.0 tornado-6.5.7 tqdm-4.68.4 traitlets-5.15.1 typing-extensions-4.16.0 typing-inspection-0.4.2 urllib3-2.7.0 wrapt-2.2.2 yte-1.9.4 zict-3.0.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
12:21:21 lightcone.eval.sandbox [sandbox build] #13 [10/11] WORKDIR /home/evaluser
12:21:21 lightcone.eval.sandbox [sandbox build] #14 [11/11] RUN mkdir -p ~/.claude && echo '{"hasCompletedOnboarding": true}' > ~/.claude.json
12:21:21 lightcone.eval.sandbox [sandbox build] #15 exporting to image
12:21:45 lightcone.eval.sandbox Created sandbox a3696165-44cd-45aa-847a-d6932dd8635d for trial build-snae-0
12:21:45 httpx HTTP Request: POST https://proxy.app.daytona.io/toolbox/a3696165-44cd-45aa-847a-d6932dd8635d/files/bulk-upload "HTTP/1.1 200 OK"
12:21:46 lightcone.eval.sandbox Installed wheels: ['lightcone_cli-0.3.8.dev3+g924c833cc-py3-none-any.whl']
12:21:46 httpx HTTP Request: POST https://proxy.app.daytona.io/toolbox/a3696165-44cd-45aa-847a-d6932dd8635d/files/bulk-upload "HTTP/1.1 200 OK"
12:21:46 httpx HTTP Request: POST https://proxy.app.daytona.io/toolbox/a3696165-44cd-45aa-847a-d6932dd8635d/files/bulk-upload "HTTP/1.1 200 OK"
12:21:47 httpx HTTP Request: POST https://proxy.app.daytona.io/toolbox/a3696165-44cd-45aa-847a-d6932dd8635d/files/bulk-upload "HTTP/1.1 200 OK"
12:21:47 httpx HTTP Request: POST https://proxy.app.daytona.io/toolbox/a3696165-44cd-45aa-847a-d6932dd8635d/files/bulk-upload "HTTP/1.1 200 OK"
12:39:18 lightcone.eval.sandbox Deleted sandbox for trial build-snae-0
  snae trial 0: score=1.00 complete

lightcone-cli: 0.3.8.dev3+g924c833cc (HEAD 924c833c)
ASTRA: 0.2.10

  Eval Results: Scores  
┏━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Task ┃     Score     ┃
┡━━━━━━╇━━━━━━━━━━━━━━━┩
│ snae │ 1.00 +/- 0.00 │
│      │ pass@k: 100%  │
└──────┴───────────────┘

   Eval Results: Cost &   
         Duration         
┏━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Task ┃ Cost / Duration ┃
┡━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ snae │      $2.31      │
│      │      1049s      │
└──────┴─────────────────┘

Total: 1 trials, $2.31, 1049s

Results saved to: eval-results/build-924c833c/results.json

@EiffL EiffL marked this pull request as draft July 12, 2026 12:45
EiffL and others added 3 commits July 12, 2026 23:51
Reworks the Dask Gateway integration around one idea: on a hub
deployment, the worker pod IS the container. The Containerfile keeps
defining the recipe environment on every path; only its realization
differs (OCI wrap locally/SLURM, pod image on the hub).

- container.py: new "kubernetes" runtime — wrap_recipe no-ops, a
  site-declared non-OCI runtime resolves as explicit in load_runtime
  (kills the false "no container runtime found" warning on the hub;
  Perlmutter's OCI hint semantics unchanged). LIGHTCONE_REGISTRY env
  names the deployment registry; registry_image_ref maps the local
  lc-<project>-<hash> tag to <registry>/lc-<project>:<hash> (same
  content hash — same artifact). resolve_image_for_run stays
  site-agnostic so code_version is stable across laptop<->hub.
  Best-effort registry probe via the GKE metadata token.

- dask_cluster.py: the Gateway branch is attach-only. lc run never
  creates clusters; it discovers the user's running cluster through
  the user-scoped Gateway API (one -> attach; zero -> error with a
  copy-pasteable new_cluster snippet, image pre-filled and
  shutdown_on_close=False; several -> LIGHTCONE_GATEWAY_CLUSTER
  disambiguates). Scaling untouched, cluster left running — the
  Gateway flavor of the DASK_SCHEDULER_ADDRESS convention. Verifies
  the resource contract on live workers (now cpus+memory together;
  cpus-only would still hang mem_mb rules) and the worker image via
  the deployment-injected LIGHTCONE_WORKER_IMAGE, warning on
  mismatch. Gateway()/connect failures translate to guided errors.

- executor.py: _connect_client now mirrors the parent's branch
  priority (address first), and lc run strips the losing variable
  from the child env — a stale shell-exported gateway name can no
  longer redirect the child to an unverified cluster.

- commands.py: lc build on kubernetes checks the registry and prints
  publish instructions that reproduce the hash-attested build context
  (lc build + docker tag/push, not a raw docker build). lc run
  cross-checks runtime vs cluster branch and warns on both mismatch
  directions. Manifests record the pod's actual image (worker_image).

- tests: attach-only gateway suite (discovery, lifecycle, contract,
  image verification, stale-name guidance), registry helpers, runtime
  precedence; conftest neutralizes ambient hub env markers so the
  suite passes inside a hub pod.

Validated end-to-end under a simulated hub environment; reviewed by a
25-finding multi-agent workflow with adversarial verification, all
confirmed findings fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRVPnWVUtpumnFsCzEEA8U
Validated live on the lightcone-hub GKE deployment; a two-rule pipeline
now runs end to end on a Gateway cluster (attach, contract+image
verification, DAG on spot workers, manifests with worker_image ground
truth, lc verify clean, idempotent re-run). Three real-world fixes, all
the same root cause — Gateway worker pods share the project volume with
the driver but not its HOME, install prefix, or cwd, unlike local/SLURM
workers which inherit the driver's environment by process ancestry:

- Executor overrides get_python_executable(): the driver's
  sys.executable (conda notebook path) doesn't exist in a slim worker
  image — exit 127 before snakemake starts. The worker's own PATH
  resolves the interpreter; the worker image is the software deployment.

- Executor pins --directory on the child snakemake: worker pods start
  in their own HOME, so relative outputs "succeeded" into the pod's
  ephemeral filesystem while the driver saw nothing.

- lc run passes --shared-fs-usage input-output/persistence/sources/
  storage-local-copies (not source-cache: the child mkdir'd the
  driver's ~/.cache path and died on PermissionError) and
  --latency-wait 60 (NFS attribute caching hid freshly written worker
  outputs from the driver past snakemake's 5s default) — gateway
  branch only; local/SLURM keep snakemake defaults.

Also: _run_shell forwards a bounded stderr tail when the child fails
(exit 127 with zero diagnostics is a debugging dead end), and
gateway_branch_active() exposes the branch decision so lc run can
shape the snakemake invocation before entering the cluster context.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRVPnWVUtpumnFsCzEEA8U
Found live on the hub: an ASTRA analysis named "Union2.1 Flat ΛCDM
MAP Fit" produced the image name lc-union2.1-flat-λcdm-map-fit — not
a valid OCI reference (registries and docker/podman require lowercase
ASCII), and the λ reached http.client's ASCII-only request line, so
`lc build` crashed with UnicodeEncodeError instead of reporting.

- compute_image_tag routes through image_name_slug(): NFKD
  transliteration for accented latin, other non-ASCII dropped,
  invalid chars collapsed to "-". Already-valid names (gwtest,
  union2.1) pass through unchanged, so existing tags keep their
  names.
- registry_image_exists percent-encodes the URL path and treats
  ValueError/UnicodeError as "cannot tell" (None) — the probe's
  contract is tri-state, never a traceback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HRVPnWVUtpumnFsCzEEA8U
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.

1 participant