letify 2.10.1__tar.gz → 2.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {letify-2.10.1 → letify-2.10.2}/PKG-INFO +1 -1
- {letify-2.10.1 → letify-2.10.2}/docs/SPEC.md +1 -1
- {letify-2.10.1 → letify-2.10.2}/letify/__init__.py +1 -1
- {letify-2.10.1 → letify-2.10.2}/letify/protocol/worker.py +12 -9
- {letify-2.10.1 → letify-2.10.2}/letify-core/Cargo.lock +2 -2
- {letify-2.10.1 → letify-2.10.2}/letify-core/Cargo.toml +1 -1
- {letify-2.10.1 → letify-2.10.2}/letify-ext/package-lock.json +2 -2
- {letify-2.10.1 → letify-2.10.2}/letify-ext/package.json +1 -1
- {letify-2.10.1 → letify-2.10.2}/pyproject.toml +1 -1
- {letify-2.10.1 → letify-2.10.2}/tests/test_run_takeover.py +110 -0
- {letify-2.10.1 → letify-2.10.2}/.github/workflows/ci.yml +0 -0
- {letify-2.10.1 → letify-2.10.2}/.github/workflows/docs.yml +0 -0
- {letify-2.10.1 → letify-2.10.2}/.github/workflows/publish-crates.yml +0 -0
- {letify-2.10.1 → letify-2.10.2}/.github/workflows/publish.yml +0 -0
- {letify-2.10.1 → letify-2.10.2}/.github/workflows/reserve-crates.yml +0 -0
- {letify-2.10.1 → letify-2.10.2}/.gitignore +0 -0
- {letify-2.10.1 → letify-2.10.2}/.researchtree +0 -0
- {letify-2.10.1 → letify-2.10.2}/CLAUDE.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/LICENSE +0 -0
- {letify-2.10.1 → letify-2.10.2}/PROJECT.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/COMPONENT.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/INTENT.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/NETWORK.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/01-getting-started.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/02-providers.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/03-execution-modes.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/04-environments-and-data.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/05-concurrency.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/06-cost.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/07-troubleshooting.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/08-releasing.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/locales/README_ko.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/docs/mkdocs.yml +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/.letify.example +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/00_smoke.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/01_sweep.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/02_resume.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/03_watch.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/README_ko.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/common.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/recipe.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/cli.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/config/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/config/inventory.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/config/login.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/config/schema.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/config/secrets.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/config/writer.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/cache.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/env.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/exit.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/function.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/instance.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/reduce.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/declare/replicas.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/errors.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/install.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/launcher.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/paths.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/protocol/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/protocol/codec.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/protocol/driver.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/protocol/handle.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/protocol/wire.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/base.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/colab.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/colab_files.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/colab_keepalive.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/elice.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/kaggle.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/kaggle_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/local.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/modal.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/modal_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/naming.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/shell.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/tunnel.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/providers/usage.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/client.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/cuda.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/executor.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/frames.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/guard.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/tensor.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/trace.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/value.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/render.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/bootstrap.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/broker.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/channel.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/control.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/lease.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/pool.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/session.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/takeover.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/telemetry.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/runtime/workers.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/backends/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/backends/drive.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/backends/filesystem.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/backends/google_auth.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/backends/layout.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/backends/objects.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/cas.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/handoff.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/pathdata.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/sendorder.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/store/volume.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/stubs.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/tools.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/__init__.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/agent.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/announce.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/link.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/nat.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/pipeline.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/portmap.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/probe.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/quic.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/rendezvous.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/setup.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/sshopts.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify/transport/strategies.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/.gitignore +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/build.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/letify/Cargo.toml +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/letify/LICENSE +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/letify/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/letify/src/lib.rs +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/letify-quic/Cargo.toml +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/letify-quic/src/main.rs +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-core/rust-toolchain.toml +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/.vscodeignore +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/LICENSE +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/README.md +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/media/icon.svg +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/src/cli.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/src/extension.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/src/history.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/src/model.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/src/poll.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/src/render.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/test/cli.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/test/manifest.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/test/model.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/test/regressions.test.ts +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify-ext/tsconfig.json +0 -0
- {letify-2.10.1 → letify-2.10.2}/letify_providers.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/scripts/sync-main.sh +0 -0
- {letify-2.10.1 → letify-2.10.2}/scripts/version.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/conftest.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/fake_kaggle_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/fake_modal_adapter.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_cli.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_cli_output.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_colab_keepalive.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_config.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_core.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_data.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_data_streaming.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_declare.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_device.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_device_compile.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_distributed.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_drive_handoff.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_drive_store.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_editor_sessions.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_elice.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_frames.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_install.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_kaggle.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_kaggle_cookie.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_kaggle_link.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_link_log.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_notebook.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_parallel_placement.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_paths.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_portmap.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_protocol.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_providers.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_punch_sockets.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_quic.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_remoting.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_render.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_run_control.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_runtime.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_shell_workers.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_ship.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_store.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_strategies.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_stubs.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_transport.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_tunnel_setup.py +0 -0
- {letify-2.10.1 → letify-2.10.2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: letify
|
|
3
|
-
Version: 2.10.
|
|
3
|
+
Version: 2.10.2
|
|
4
4
|
Summary: Declarations that become infrastructure.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DarkPyonix/letify
|
|
6
6
|
Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
|
|
@@ -1089,7 +1089,7 @@ A call has exactly one owner at a time, and the session still ends with the call
|
|
|
1089
1089
|
|
|
1090
1090
|
1. **The registry keeps an orphan.** The record also holds `project`, the project root the call was sent from, and `lease_deadline`, the Unix time at which the runtime's lease ends unless renewed, rewritten at each renewal. It also holds what a second process needs to reach the call and finish it: `door`, the path of the worker's socket below; `config`, the project `config.toml` that names the account; and `call_dir`, `blobs` and `outputs`, the runtime paths and the output locations to write back, each location stored relative to `project` when it lies inside it. No secret is among them. A record whose owner pid is gone is no longer removed when read while its `lease_deadline` is in the future: `letify runs` lists it with `owner` `gone` and the seconds left, and it can be taken over. After the deadline it is removed as stale.
|
|
1091
1091
|
2. **The worker keeps a second door.** While a call runs, the worker listens on a Unix socket on the runtime, `<workspace root>/.letify/run-<run id>.sock`, mode 0600. When that path is longer than 100 bytes, the limit of a Unix socket address, the worker and the relay reach the socket through `/proc/self/fd/<n>`, a descriptor of its directory, so it stays at that path; where `/proc` is missing the worker reports that the run has no second door. A channel that closes while a call runs, with no shutdown frame before it, does not end the worker: the body keeps running until the lease deadline passes. A shutdown frame is different: it is the owner ending the run on purpose, as [A deliberate shutdown](#run-shutdown) describes. Output the body makes while no channel is open is kept, the last 256 KiB per stream, and is sent to the next owner first. A body that returns while no channel is open keeps its result for a new owner until the lease deadline; on a provider with no lease the worker ends then, as before.
|
|
1092
|
-
3. **The new owner connects.** `letify takeover <run>` reads the record, opens a channel to the same runtime through the record's provider and runtime name, and starts a relay to that socket instead of a new worker. It then sends `adopt` with the run id. The worker answers once with the call's state, which is `running` or `finished`, and from then on sends the call's output and its reply on the new channel, the reply as the last piece of the same answer. The old channel, if still open, is told `moved` on the call's stream
|
|
1092
|
+
3. **The new owner connects.** `letify takeover <run>` reads the record, opens a channel to the same runtime through the record's provider and runtime name, and starts a relay to that socket instead of a new worker. It then sends `adopt` with the run id. The worker answers once with the call's state, which is `running` or `finished`, and from then on sends the call's output and its reply on the new channel, the reply as the last piece of the same answer. The old channel, if still open, is told `moved` on the call's stream, after the worker has switched the call's output to the new channel, so everything the body prints once the old owner knows of the move reaches the new owner. It answers every later request on it with `moved`. A door takes one connection at a time and no request but `adopt` before it, and a second owner can adopt after the first one's connection has closed. The adoption is atomic with respect to the old channel: if the old channel closes while the adoption is under way, the worker decides what that close means only after the adoption has finished, sees that the call has moved, and does nothing. The new owner's channel is then never marked as having lost its input, and no exit hook runs for the close.
|
|
1093
1093
|
4. **The old owner lets go.** When the old owner is alive, takeover first asks it through its control socket to release the run. Its waiting call raises `OwnershipMoved` naming the run and the new owner's pid, it stops renewing that runtime's lease, and it drops the runtime from its pool without stopping it. A `keep_alive` block in the old owner no longer holds that session. An old owner that does not answer within 5 s is treated as gone.
|
|
1094
1094
|
5. **The call ends at the new owner.** `letify takeover` prints the body's output as it arrives. When the call returns it prints the `repr` of the value, or with `--result PATH` writes the value with pickle to `PATH`, and exits 0. When the body raises it prints the traceback and exits 1. With `--json` the last line is one object with `run`, `ok`, `value_repr` or `error`, and `result_path`. The call's outputs are written back under the record's `project`, not the current directory. The session is then ended.
|
|
1095
1095
|
6. **The same from Python.** `letify.takeover(run)` does the same in the calling process and returns the value or raises the body's exception, as a call does. It holds no handle: it returns once the call has ended and the session is gone. A darkpyonix kernel uses this to adopt a run started elsewhere.
|
|
@@ -3549,11 +3549,12 @@ def _reader_ended(door, shutdown=False):
|
|
|
3549
3549
|
Spec "A deliberate shutdown": a shutdown frame on the channel that owns a running call
|
|
3550
3550
|
ends the body and the worker. End of input alone leaves the body for a takeover.
|
|
3551
3551
|
"""
|
|
3552
|
-
if door is None and _TAKE["moved"]:
|
|
3553
|
-
# The call was adopted away; this channel closing is the old owner leaving.
|
|
3554
|
-
return
|
|
3555
3552
|
during = False
|
|
3556
3553
|
with _TAKE_COND:
|
|
3554
|
+
if door is None and _TAKE["moved"]:
|
|
3555
|
+
# The call was adopted away; this channel closing is the old owner leaving. Read
|
|
3556
|
+
# under the lock, because an adoption in progress holds it until it has moved.
|
|
3557
|
+
return
|
|
3557
3558
|
if door is None or (_TAKE["owner"] is door and _TAKE["live"]):
|
|
3558
3559
|
during = _TAKE["run"] is not None
|
|
3559
3560
|
if door is not None or during:
|
|
@@ -3702,12 +3703,6 @@ def _door_adopt(door, stream, request):
|
|
|
3702
3703
|
moved = _moved_outcome()
|
|
3703
3704
|
if _TAKE["owner"] is not None:
|
|
3704
3705
|
moved["last"] = True
|
|
3705
|
-
# Told on a thread, because a former owner that has stopped reading can block the write.
|
|
3706
|
-
told = threading.Thread(
|
|
3707
|
-
target=_answer, args=(former, former_stream, moved), daemon=True
|
|
3708
|
-
)
|
|
3709
|
-
told.start()
|
|
3710
|
-
told.join(2.0)
|
|
3711
3706
|
state = {
|
|
3712
3707
|
"run": run,
|
|
3713
3708
|
"state": "finished" if _TAKE["finished"] else "running",
|
|
@@ -3728,6 +3723,14 @@ def _door_adopt(door, stream, request):
|
|
|
3728
3723
|
)
|
|
3729
3724
|
_SENDER = door.sender
|
|
3730
3725
|
_TAKE_COND.notify_all()
|
|
3726
|
+
# Told last, after the output has been switched, so that nothing the body prints from
|
|
3727
|
+
# the moment the former owner learns of the move is sent to a channel that has stopped
|
|
3728
|
+
# reading. A thread, because a former owner that has stopped reading can block the write.
|
|
3729
|
+
told = threading.Thread(
|
|
3730
|
+
target=_answer, args=(former, former_stream, moved), daemon=True
|
|
3731
|
+
)
|
|
3732
|
+
told.start()
|
|
3733
|
+
told.join(2.0)
|
|
3731
3734
|
|
|
3732
3735
|
|
|
3733
3736
|
def _deliver_call(stream, outcome):
|
|
@@ -138,11 +138,11 @@ dependencies = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "letify"
|
|
141
|
-
version = "2.10.
|
|
141
|
+
version = "2.10.2"
|
|
142
142
|
|
|
143
143
|
[[package]]
|
|
144
144
|
name = "letify-quic"
|
|
145
|
-
version = "2.10.
|
|
145
|
+
version = "2.10.2"
|
|
146
146
|
dependencies = [
|
|
147
147
|
"quinn",
|
|
148
148
|
"rcgen",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "letify-status",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "letify-status",
|
|
9
|
-
"version": "2.10.
|
|
9
|
+
"version": "2.10.2",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@types/node": "^18.19.0",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "letify-status",
|
|
3
3
|
"displayName": "letify Status",
|
|
4
4
|
"description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
|
|
5
|
-
"version": "2.10.
|
|
5
|
+
"version": "2.10.2",
|
|
6
6
|
"publisher": "darkpyonix",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE",
|
|
8
8
|
"repository": {
|
|
@@ -289,6 +289,8 @@ def test_the_new_owner_takes_the_record_and_removes_it_when_the_call_ends(world,
|
|
|
289
289
|
until(lambda: any(r["pid"] == os.getpid() for r in control.list_runs()), "the new record")
|
|
290
290
|
row = next(r for r in control.list_runs() if r["pid"] == os.getpid())
|
|
291
291
|
assert row["run"] == owner.run
|
|
292
|
+
# The body returns only once the old owner has let go, which is after the adoption.
|
|
293
|
+
owner.answer()
|
|
292
294
|
owner.finish_body()
|
|
293
295
|
taking.join()
|
|
294
296
|
assert control.list_runs(orphans=True) == []
|
|
@@ -742,3 +744,111 @@ def test_detaching_a_persistent_channel_sends_no_shutdown(monkeypatch) -> None:
|
|
|
742
744
|
channel.detach()
|
|
743
745
|
assert wire.SHUTDOWN not in sent
|
|
744
746
|
assert sent == []
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
#: The old owner's channel closes while the worker is still adopting the call to the new owner.
|
|
750
|
+
#: The adoption holds the call's lock, so the close has to wait for it and then see the move.
|
|
751
|
+
RACE_PROBE = textwrap.dedent(
|
|
752
|
+
"""
|
|
753
|
+
import json, sys, threading, time
|
|
754
|
+
from pathlib import Path
|
|
755
|
+
|
|
756
|
+
from letify.protocol import worker
|
|
757
|
+
|
|
758
|
+
out = sys.argv[1]
|
|
759
|
+
text = worker.SOURCE.rstrip()
|
|
760
|
+
assert text.endswith("_serve()")
|
|
761
|
+
space = {"__name__": "probe"}
|
|
762
|
+
exec(compile(text[: -len("_serve()")], "worker", "exec"), space)
|
|
763
|
+
hooks = []
|
|
764
|
+
space["_run_exit_hooks"] = lambda reason: hooks.append(reason)
|
|
765
|
+
take, cond = space["_TAKE"], space["_TAKE_COND"]
|
|
766
|
+
take.update(run="abcdef012345", live=True, moved=False)
|
|
767
|
+
new_owner = object()
|
|
768
|
+
with cond:
|
|
769
|
+
closing = threading.Thread(target=space["_reader_ended"], args=(None, False))
|
|
770
|
+
closing.start()
|
|
771
|
+
time.sleep(0.5)
|
|
772
|
+
# The adoption finishes under the lock, after the close began.
|
|
773
|
+
take.update(owner=new_owner, moved=True, live=True, ended_input=False)
|
|
774
|
+
closing.join(30)
|
|
775
|
+
Path(out).write_text(json.dumps({
|
|
776
|
+
"live": take["live"], "ended_input": take["ended_input"], "hooks": hooks,
|
|
777
|
+
}))
|
|
778
|
+
"""
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def test_the_old_owners_channel_closing_during_the_adoption_leaves_the_new_owner_live(
|
|
783
|
+
tmp_path,
|
|
784
|
+
) -> None:
|
|
785
|
+
script = tmp_path / "race.py"
|
|
786
|
+
script.write_text(RACE_PROBE, encoding="utf-8")
|
|
787
|
+
out = tmp_path / "race.out"
|
|
788
|
+
root = str(Path(letify.__file__).resolve().parent.parent)
|
|
789
|
+
subprocess.run(
|
|
790
|
+
[sys.executable, str(script), str(out)],
|
|
791
|
+
check=True, timeout=60, env={**os.environ, "PYTHONPATH": root},
|
|
792
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
793
|
+
)
|
|
794
|
+
assert json.loads(out.read_text(encoding="utf-8")) == {
|
|
795
|
+
"live": True, "ended_input": False, "hooks": [],
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
#: Adopts a call with stand-in channels and records whether the worker already sends output
|
|
800
|
+
#: to the new owner at the moment the old owner is told the call has moved.
|
|
801
|
+
ORDER_PROBE = textwrap.dedent(
|
|
802
|
+
"""
|
|
803
|
+
import json, sys
|
|
804
|
+
from pathlib import Path
|
|
805
|
+
|
|
806
|
+
from letify.protocol import worker
|
|
807
|
+
|
|
808
|
+
out = sys.argv[1]
|
|
809
|
+
text = worker.SOURCE.rstrip()
|
|
810
|
+
assert text.endswith("_serve()")
|
|
811
|
+
space = {"__name__": "probe"}
|
|
812
|
+
exec(compile(text[: -len("_serve()")], "worker", "exec"), space)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class Channel:
|
|
816
|
+
def message(self, *args):
|
|
817
|
+
pass
|
|
818
|
+
|
|
819
|
+
def frame(self, *args):
|
|
820
|
+
pass
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
class Connection:
|
|
824
|
+
def settimeout(self, value):
|
|
825
|
+
pass
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
class Door:
|
|
829
|
+
sender = Channel()
|
|
830
|
+
connection = Connection()
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
door, former = Door(), Channel()
|
|
834
|
+
seen = []
|
|
835
|
+
space["_answer"] = lambda sender, stream, outcome: seen.append(space["_SENDER"] is door.sender)
|
|
836
|
+
space["_SENDER"] = former
|
|
837
|
+
space["_TAKE"].update(run="abcdef012345", live=True, stream=3)
|
|
838
|
+
space["_door_adopt"](door, 7, {"run": "abcdef012345", "owner": 1234})
|
|
839
|
+
Path(out).write_text(json.dumps(seen))
|
|
840
|
+
"""
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def test_output_after_the_old_owner_is_told_the_call_moved_goes_to_the_new_owner(tmp_path) -> None:
|
|
845
|
+
script = tmp_path / "order.py"
|
|
846
|
+
script.write_text(ORDER_PROBE, encoding="utf-8")
|
|
847
|
+
out = tmp_path / "order.out"
|
|
848
|
+
root = str(Path(letify.__file__).resolve().parent.parent)
|
|
849
|
+
subprocess.run(
|
|
850
|
+
[sys.executable, str(script), str(out)],
|
|
851
|
+
check=True, timeout=60, env={**os.environ, "PYTHONPATH": root},
|
|
852
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
853
|
+
)
|
|
854
|
+
assert json.loads(out.read_text(encoding="utf-8")) == [True]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|