letify 2.5.3__tar.gz → 2.5.4__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.
Files changed (210) hide show
  1. {letify-2.5.3 → letify-2.5.4}/PKG-INFO +1 -1
  2. {letify-2.5.3 → letify-2.5.4}/docs/SPEC.md +1 -0
  3. {letify-2.5.3 → letify-2.5.4}/letify/__init__.py +1 -1
  4. {letify-2.5.3 → letify-2.5.4}/letify/protocol/worker.py +44 -1
  5. {letify-2.5.3 → letify-2.5.4}/letify/runtime/session.py +1 -1
  6. {letify-2.5.3 → letify-2.5.4}/letify/store/pathdata.py +30 -6
  7. {letify-2.5.3 → letify-2.5.4}/letify-core/Cargo.lock +3 -3
  8. {letify-2.5.3 → letify-2.5.4}/letify-core/Cargo.toml +1 -1
  9. {letify-2.5.3 → letify-2.5.4}/letify-ext/package-lock.json +2 -2
  10. {letify-2.5.3 → letify-2.5.4}/letify-ext/package.json +1 -1
  11. {letify-2.5.3 → letify-2.5.4}/pyproject.toml +1 -1
  12. {letify-2.5.3 → letify-2.5.4}/tests/test_data_streaming.py +43 -0
  13. {letify-2.5.3 → letify-2.5.4}/.github/workflows/ci.yml +0 -0
  14. {letify-2.5.3 → letify-2.5.4}/.github/workflows/docs.yml +0 -0
  15. {letify-2.5.3 → letify-2.5.4}/.github/workflows/publish.yml +0 -0
  16. {letify-2.5.3 → letify-2.5.4}/.gitignore +0 -0
  17. {letify-2.5.3 → letify-2.5.4}/.researchtree +0 -0
  18. {letify-2.5.3 → letify-2.5.4}/CLAUDE.md +0 -0
  19. {letify-2.5.3 → letify-2.5.4}/LICENSE +0 -0
  20. {letify-2.5.3 → letify-2.5.4}/PROJECT.md +0 -0
  21. {letify-2.5.3 → letify-2.5.4}/README.md +0 -0
  22. {letify-2.5.3 → letify-2.5.4}/docs/COMPONENT.md +0 -0
  23. {letify-2.5.3 → letify-2.5.4}/docs/INTENT.md +0 -0
  24. {letify-2.5.3 → letify-2.5.4}/docs/NETWORK.md +0 -0
  25. {letify-2.5.3 → letify-2.5.4}/docs/guide/01-getting-started.md +0 -0
  26. {letify-2.5.3 → letify-2.5.4}/docs/guide/02-providers.md +0 -0
  27. {letify-2.5.3 → letify-2.5.4}/docs/guide/03-execution-modes.md +0 -0
  28. {letify-2.5.3 → letify-2.5.4}/docs/guide/04-environments-and-data.md +0 -0
  29. {letify-2.5.3 → letify-2.5.4}/docs/guide/05-concurrency.md +0 -0
  30. {letify-2.5.3 → letify-2.5.4}/docs/guide/06-cost.md +0 -0
  31. {letify-2.5.3 → letify-2.5.4}/docs/guide/07-troubleshooting.md +0 -0
  32. {letify-2.5.3 → letify-2.5.4}/docs/guide/08-releasing.md +0 -0
  33. {letify-2.5.3 → letify-2.5.4}/docs/guide/README.md +0 -0
  34. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  35. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  36. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  37. {letify-2.5.3 → letify-2.5.4}/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
  38. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  39. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  40. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  41. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  42. {letify-2.5.3 → letify-2.5.4}/docs/guide/ko/README.md +0 -0
  43. {letify-2.5.3 → letify-2.5.4}/docs/locales/README_ko.md +0 -0
  44. {letify-2.5.3 → letify-2.5.4}/docs/mkdocs.yml +0 -0
  45. {letify-2.5.3 → letify-2.5.4}/examples/README.md +0 -0
  46. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/.letify.example +0 -0
  47. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/00_smoke.py +0 -0
  48. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/01_sweep.py +0 -0
  49. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/02_resume.py +0 -0
  50. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/03_watch.py +0 -0
  51. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/README.md +0 -0
  52. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/README_ko.md +0 -0
  53. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/common.py +0 -0
  54. {letify-2.5.3 → letify-2.5.4}/examples/nvfp4-lora/recipe.py +0 -0
  55. {letify-2.5.3 → letify-2.5.4}/letify/cli.py +0 -0
  56. {letify-2.5.3 → letify-2.5.4}/letify/config/__init__.py +0 -0
  57. {letify-2.5.3 → letify-2.5.4}/letify/config/inventory.py +0 -0
  58. {letify-2.5.3 → letify-2.5.4}/letify/config/login.py +0 -0
  59. {letify-2.5.3 → letify-2.5.4}/letify/config/schema.py +0 -0
  60. {letify-2.5.3 → letify-2.5.4}/letify/config/secrets.py +0 -0
  61. {letify-2.5.3 → letify-2.5.4}/letify/config/writer.py +0 -0
  62. {letify-2.5.3 → letify-2.5.4}/letify/declare/__init__.py +0 -0
  63. {letify-2.5.3 → letify-2.5.4}/letify/declare/cache.py +0 -0
  64. {letify-2.5.3 → letify-2.5.4}/letify/declare/env.py +0 -0
  65. {letify-2.5.3 → letify-2.5.4}/letify/declare/function.py +0 -0
  66. {letify-2.5.3 → letify-2.5.4}/letify/declare/instance.py +0 -0
  67. {letify-2.5.3 → letify-2.5.4}/letify/declare/reduce.py +0 -0
  68. {letify-2.5.3 → letify-2.5.4}/letify/declare/replicas.py +0 -0
  69. {letify-2.5.3 → letify-2.5.4}/letify/errors.py +0 -0
  70. {letify-2.5.3 → letify-2.5.4}/letify/install.py +0 -0
  71. {letify-2.5.3 → letify-2.5.4}/letify/launcher.py +0 -0
  72. {letify-2.5.3 → letify-2.5.4}/letify/paths.py +0 -0
  73. {letify-2.5.3 → letify-2.5.4}/letify/protocol/__init__.py +0 -0
  74. {letify-2.5.3 → letify-2.5.4}/letify/protocol/codec.py +0 -0
  75. {letify-2.5.3 → letify-2.5.4}/letify/protocol/driver.py +0 -0
  76. {letify-2.5.3 → letify-2.5.4}/letify/protocol/handle.py +0 -0
  77. {letify-2.5.3 → letify-2.5.4}/letify/protocol/wire.py +0 -0
  78. {letify-2.5.3 → letify-2.5.4}/letify/providers/__init__.py +0 -0
  79. {letify-2.5.3 → letify-2.5.4}/letify/providers/base.py +0 -0
  80. {letify-2.5.3 → letify-2.5.4}/letify/providers/colab.py +0 -0
  81. {letify-2.5.3 → letify-2.5.4}/letify/providers/colab_files.py +0 -0
  82. {letify-2.5.3 → letify-2.5.4}/letify/providers/colab_keepalive.py +0 -0
  83. {letify-2.5.3 → letify-2.5.4}/letify/providers/elice.py +0 -0
  84. {letify-2.5.3 → letify-2.5.4}/letify/providers/kaggle.py +0 -0
  85. {letify-2.5.3 → letify-2.5.4}/letify/providers/kaggle_adapter.py +0 -0
  86. {letify-2.5.3 → letify-2.5.4}/letify/providers/local.py +0 -0
  87. {letify-2.5.3 → letify-2.5.4}/letify/providers/modal.py +0 -0
  88. {letify-2.5.3 → letify-2.5.4}/letify/providers/modal_adapter.py +0 -0
  89. {letify-2.5.3 → letify-2.5.4}/letify/providers/naming.py +0 -0
  90. {letify-2.5.3 → letify-2.5.4}/letify/providers/shell.py +0 -0
  91. {letify-2.5.3 → letify-2.5.4}/letify/providers/tunnel.py +0 -0
  92. {letify-2.5.3 → letify-2.5.4}/letify/providers/usage.py +0 -0
  93. {letify-2.5.3 → letify-2.5.4}/letify/remoting/__init__.py +0 -0
  94. {letify-2.5.3 → letify-2.5.4}/letify/remoting/capability.py +0 -0
  95. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/__init__.py +0 -0
  96. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/client.py +0 -0
  97. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/cuda.py +0 -0
  98. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/executor.py +0 -0
  99. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/frames.py +0 -0
  100. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/guard.py +0 -0
  101. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/tensor.py +0 -0
  102. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/trace.py +0 -0
  103. {letify-2.5.3 → letify-2.5.4}/letify/remoting/device/value.py +0 -0
  104. {letify-2.5.3 → letify-2.5.4}/letify/remoting/loader.py +0 -0
  105. {letify-2.5.3 → letify-2.5.4}/letify/remoting/probe.py +0 -0
  106. {letify-2.5.3 → letify-2.5.4}/letify/render.py +0 -0
  107. {letify-2.5.3 → letify-2.5.4}/letify/runtime/__init__.py +0 -0
  108. {letify-2.5.3 → letify-2.5.4}/letify/runtime/bootstrap.py +0 -0
  109. {letify-2.5.3 → letify-2.5.4}/letify/runtime/broker.py +0 -0
  110. {letify-2.5.3 → letify-2.5.4}/letify/runtime/channel.py +0 -0
  111. {letify-2.5.3 → letify-2.5.4}/letify/runtime/lease.py +0 -0
  112. {letify-2.5.3 → letify-2.5.4}/letify/runtime/pool.py +0 -0
  113. {letify-2.5.3 → letify-2.5.4}/letify/runtime/telemetry.py +0 -0
  114. {letify-2.5.3 → letify-2.5.4}/letify/store/__init__.py +0 -0
  115. {letify-2.5.3 → letify-2.5.4}/letify/store/backends/__init__.py +0 -0
  116. {letify-2.5.3 → letify-2.5.4}/letify/store/backends/filesystem.py +0 -0
  117. {letify-2.5.3 → letify-2.5.4}/letify/store/backends/google_auth.py +0 -0
  118. {letify-2.5.3 → letify-2.5.4}/letify/store/backends/layout.py +0 -0
  119. {letify-2.5.3 → letify-2.5.4}/letify/store/backends/objects.py +0 -0
  120. {letify-2.5.3 → letify-2.5.4}/letify/store/cas.py +0 -0
  121. {letify-2.5.3 → letify-2.5.4}/letify/store/sendorder.py +0 -0
  122. {letify-2.5.3 → letify-2.5.4}/letify/store/volume.py +0 -0
  123. {letify-2.5.3 → letify-2.5.4}/letify/stubs.py +0 -0
  124. {letify-2.5.3 → letify-2.5.4}/letify/tools.py +0 -0
  125. {letify-2.5.3 → letify-2.5.4}/letify/transport/__init__.py +0 -0
  126. {letify-2.5.3 → letify-2.5.4}/letify/transport/agent.py +0 -0
  127. {letify-2.5.3 → letify-2.5.4}/letify/transport/announce.py +0 -0
  128. {letify-2.5.3 → letify-2.5.4}/letify/transport/link.py +0 -0
  129. {letify-2.5.3 → letify-2.5.4}/letify/transport/nat.py +0 -0
  130. {letify-2.5.3 → letify-2.5.4}/letify/transport/pipeline.py +0 -0
  131. {letify-2.5.3 → letify-2.5.4}/letify/transport/portmap.py +0 -0
  132. {letify-2.5.3 → letify-2.5.4}/letify/transport/probe.py +0 -0
  133. {letify-2.5.3 → letify-2.5.4}/letify/transport/quic.py +0 -0
  134. {letify-2.5.3 → letify-2.5.4}/letify/transport/rendezvous.py +0 -0
  135. {letify-2.5.3 → letify-2.5.4}/letify/transport/setup.py +0 -0
  136. {letify-2.5.3 → letify-2.5.4}/letify/transport/sshopts.py +0 -0
  137. {letify-2.5.3 → letify-2.5.4}/letify/transport/strategies.py +0 -0
  138. {letify-2.5.3 → letify-2.5.4}/letify-core/.gitignore +0 -0
  139. {letify-2.5.3 → letify-2.5.4}/letify-core/build.py +0 -0
  140. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-agent/Cargo.toml +0 -0
  141. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-agent/src/driver.rs +0 -0
  142. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-agent/src/main.rs +0 -0
  143. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/Cargo.toml +0 -0
  144. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/src/client.rs +0 -0
  145. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/src/exports.rs +0 -0
  146. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/src/image.rs +0 -0
  147. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/src/lib.rs +0 -0
  148. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/src/status.rs +0 -0
  149. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-driver/src/table.rs +0 -0
  150. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-quic/Cargo.toml +0 -0
  151. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-quic/src/main.rs +0 -0
  152. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-wire/Cargo.toml +0 -0
  153. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-wire/src/lib.rs +0 -0
  154. {letify-2.5.3 → letify-2.5.4}/letify-core/letify-wire/tests/throughput.rs +0 -0
  155. {letify-2.5.3 → letify-2.5.4}/letify-core/rust-toolchain.toml +0 -0
  156. {letify-2.5.3 → letify-2.5.4}/letify-ext/.vscodeignore +0 -0
  157. {letify-2.5.3 → letify-2.5.4}/letify-ext/LICENSE +0 -0
  158. {letify-2.5.3 → letify-2.5.4}/letify-ext/README.md +0 -0
  159. {letify-2.5.3 → letify-2.5.4}/letify-ext/media/icon.svg +0 -0
  160. {letify-2.5.3 → letify-2.5.4}/letify-ext/src/cli.ts +0 -0
  161. {letify-2.5.3 → letify-2.5.4}/letify-ext/src/extension.ts +0 -0
  162. {letify-2.5.3 → letify-2.5.4}/letify-ext/src/history.ts +0 -0
  163. {letify-2.5.3 → letify-2.5.4}/letify-ext/src/model.ts +0 -0
  164. {letify-2.5.3 → letify-2.5.4}/letify-ext/src/poll.ts +0 -0
  165. {letify-2.5.3 → letify-2.5.4}/letify-ext/src/render.ts +0 -0
  166. {letify-2.5.3 → letify-2.5.4}/letify-ext/test/cli.test.ts +0 -0
  167. {letify-2.5.3 → letify-2.5.4}/letify-ext/test/manifest.test.ts +0 -0
  168. {letify-2.5.3 → letify-2.5.4}/letify-ext/test/model.test.ts +0 -0
  169. {letify-2.5.3 → letify-2.5.4}/letify-ext/test/regressions.test.ts +0 -0
  170. {letify-2.5.3 → letify-2.5.4}/letify-ext/tsconfig.json +0 -0
  171. {letify-2.5.3 → letify-2.5.4}/letify_providers.py +0 -0
  172. {letify-2.5.3 → letify-2.5.4}/scripts/sync-main.sh +0 -0
  173. {letify-2.5.3 → letify-2.5.4}/scripts/version.py +0 -0
  174. {letify-2.5.3 → letify-2.5.4}/tests/conftest.py +0 -0
  175. {letify-2.5.3 → letify-2.5.4}/tests/fake_kaggle_adapter.py +0 -0
  176. {letify-2.5.3 → letify-2.5.4}/tests/fake_modal_adapter.py +0 -0
  177. {letify-2.5.3 → letify-2.5.4}/tests/test_cli.py +0 -0
  178. {letify-2.5.3 → letify-2.5.4}/tests/test_cli_output.py +0 -0
  179. {letify-2.5.3 → letify-2.5.4}/tests/test_colab_keepalive.py +0 -0
  180. {letify-2.5.3 → letify-2.5.4}/tests/test_config.py +0 -0
  181. {letify-2.5.3 → letify-2.5.4}/tests/test_core.py +0 -0
  182. {letify-2.5.3 → letify-2.5.4}/tests/test_data.py +0 -0
  183. {letify-2.5.3 → letify-2.5.4}/tests/test_declare.py +0 -0
  184. {letify-2.5.3 → letify-2.5.4}/tests/test_device.py +0 -0
  185. {letify-2.5.3 → letify-2.5.4}/tests/test_distributed.py +0 -0
  186. {letify-2.5.3 → letify-2.5.4}/tests/test_editor_sessions.py +0 -0
  187. {letify-2.5.3 → letify-2.5.4}/tests/test_elice.py +0 -0
  188. {letify-2.5.3 → letify-2.5.4}/tests/test_frames.py +0 -0
  189. {letify-2.5.3 → letify-2.5.4}/tests/test_install.py +0 -0
  190. {letify-2.5.3 → letify-2.5.4}/tests/test_kaggle.py +0 -0
  191. {letify-2.5.3 → letify-2.5.4}/tests/test_kaggle_cookie.py +0 -0
  192. {letify-2.5.3 → letify-2.5.4}/tests/test_kaggle_link.py +0 -0
  193. {letify-2.5.3 → letify-2.5.4}/tests/test_link_log.py +0 -0
  194. {letify-2.5.3 → letify-2.5.4}/tests/test_notebook.py +0 -0
  195. {letify-2.5.3 → letify-2.5.4}/tests/test_parallel_placement.py +0 -0
  196. {letify-2.5.3 → letify-2.5.4}/tests/test_paths.py +0 -0
  197. {letify-2.5.3 → letify-2.5.4}/tests/test_portmap.py +0 -0
  198. {letify-2.5.3 → letify-2.5.4}/tests/test_protocol.py +0 -0
  199. {letify-2.5.3 → letify-2.5.4}/tests/test_providers.py +0 -0
  200. {letify-2.5.3 → letify-2.5.4}/tests/test_quic.py +0 -0
  201. {letify-2.5.3 → letify-2.5.4}/tests/test_remoting.py +0 -0
  202. {letify-2.5.3 → letify-2.5.4}/tests/test_render.py +0 -0
  203. {letify-2.5.3 → letify-2.5.4}/tests/test_runtime.py +0 -0
  204. {letify-2.5.3 → letify-2.5.4}/tests/test_ship.py +0 -0
  205. {letify-2.5.3 → letify-2.5.4}/tests/test_store.py +0 -0
  206. {letify-2.5.3 → letify-2.5.4}/tests/test_strategies.py +0 -0
  207. {letify-2.5.3 → letify-2.5.4}/tests/test_stubs.py +0 -0
  208. {letify-2.5.3 → letify-2.5.4}/tests/test_transport.py +0 -0
  209. {letify-2.5.3 → letify-2.5.4}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.5.3 → letify-2.5.4}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.5.3
3
+ Version: 2.5.4
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
@@ -1239,6 +1239,7 @@ A `stat` cannot take that snapshot first, because it is on a data loader's hot p
1239
1239
  #### When a blob does not arrive <!-- id: project-data-streaming-failures -->
1240
1240
 
1241
1241
  - **A blob that never arrives.** A `data_want` unanswered for `data_wait_timeout` seconds on the account, 600 by default, fails that open with `RuntimeFailure` naming the relative path and the digest. The call is not killed, because the body may handle it; the failure is infrastructure, so a retry of the call is allowed.
1242
+ - **A sender that stops.** The background sender raising leaves every blob still queued unsendable. letify says so once, `letify: data sending stopped after <n> of <n> files, <n> not sent: <error>`, and tells the runtime, which wakes every reader waiting on one of those blobs with `RuntimeFailure` naming the file and that reason, and fails a reader that arrives later at once. Waiting it out instead costs `data_wait_timeout` once for every blob still queued, which is how a hundred queued shards become an hour of silence.
1242
1243
  - **A client that disconnects mid stream.** The session ends as any lost channel ends it. Every blocked reader is woken with `RuntimeFailure`, the call ends with it, and no partial file is ever placed or read. A `.partial` file is removed when the worker next starts.
1243
1244
  - **A call that finishes before the background transfer.** The transfer is cancelled, both the channel sender and any bucket upload not yet started. A blob already committed to the cache stays and counts as held for the next call, and a partial one is removed. The write-back and the `data_evict` request follow the cancellation, in that order.
1244
1245
  - **A bucket-backed ephemeral provider.** A pull that fails is retried twice with the same downscoped headers, then a third time with headers the local process derives again, and a pull that still fails fails the open with `RuntimeFailure`. A `data_want` for a digest not yet uploaded moves that object to the front of the upload queue, and the worker pulls it when the local process sends its `data_pull`.
@@ -66,7 +66,7 @@ distributed: Final = Host.distributed
66
66
  # The enum class stays importable from letify.declare.instance for letify's own use.
67
67
  del Host
68
68
 
69
- __version__ = "2.5.3"
69
+ __version__ = "2.5.4"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -364,6 +364,8 @@ def _op_call(request):
364
364
  import shutil
365
365
  _data_unregister(state)
366
366
  _data_check_cache(data, state["placed"])
367
+ _DATA_FAILED.pop(data["dir"], None)
368
+ _DATA_FAILED.pop("", None)
367
369
  _DATA_POLL_SEEN.pop(data["dir"], None)
368
370
  _DATA_POLL_SENT.pop(data["dir"], None)
369
371
  shutil.rmtree(data["dir"], ignore_errors=True)
@@ -1047,6 +1049,9 @@ def _data_register(data):
1047
1049
  "linked": 0,
1048
1050
  "order": {},
1049
1051
  "arrived": 0,
1052
+ # Spec "When a blob does not arrive": set when the local sender stops, so a
1053
+ # reader fails at once with its reason rather than waiting out the timeout.
1054
+ "failed": _DATA_FAILED.get(data["dir"]) or _DATA_FAILED.get(""),
1050
1055
  "wait": float(data.get("wait") or 600.0),
1051
1056
  "observe": bool(data.get("observe", True)),
1052
1057
  "prefetch": int(data.get("prefetch") or 64),
@@ -1238,6 +1243,32 @@ def _data_arrived(blobs, digest):
1238
1243
  _DATA_READY.notify_all()
1239
1244
 
1240
1245
 
1246
+ #: Reasons a sender stopped, by call directory, for a call that registers afterwards.
1247
+ _DATA_FAILED = {}
1248
+
1249
+
1250
+ def _op_data_failed(request):
1251
+ """The local sender stopped, so the blobs it had left will never arrive.
1252
+
1253
+ Spec "When a blob does not arrive": every reader waiting on one of them is woken with
1254
+ the reason, and a reader that arrives later fails on it at once, because waiting out
1255
+ the per blob timeout costs that timeout once for every blob still queued.
1256
+ """
1257
+ reason = request.get("reason") or "the local process stopped sending"
1258
+ where = request.get("dir") or ""
1259
+ with _DATA_READY:
1260
+ # Remembered by directory as well as set on the live call, because the sender can
1261
+ # stop before the call request has been read, and the call then registers after
1262
+ # this and would otherwise inherit nothing. Spec "When a blob does not arrive".
1263
+ _DATA_FAILED[where] = reason
1264
+ for state in _DATA_CALLS.values():
1265
+ if where and state["dir"] != where:
1266
+ continue
1267
+ state["failed"] = reason
1268
+ _DATA_READY.notify_all()
1269
+ return {"ok": True, "value": True}
1270
+
1271
+
1241
1272
  def _data_want(digests, blocking=False):
1242
1273
  """Ask the local process for these blobs, in this order. Nothing replies to it."""
1243
1274
  if not digests:
@@ -1304,6 +1335,10 @@ def _data_wait(path):
1304
1335
  entry = state["pending"].get(path)
1305
1336
  if entry is None:
1306
1337
  return
1338
+ if state["failed"]:
1339
+ raise _DataFailure(
1340
+ "%s was never sent: %s" % (os.path.basename(path), state["failed"])
1341
+ )
1307
1342
  digest, size = entry
1308
1343
  index = state["order"].get(digest)
1309
1344
  if index is not None and index > state["arrived"] + _DATA_MISS_AHEAD:
@@ -1321,6 +1356,11 @@ def _data_wait(path):
1321
1356
  try:
1322
1357
  with _DATA_READY:
1323
1358
  while path in state["pending"] and state["dir"] in _DATA_CALLS:
1359
+ if state["failed"]:
1360
+ raise _DataFailure(
1361
+ "%s was never sent: %s"
1362
+ % (os.path.basename(path), state["failed"])
1363
+ )
1324
1364
  left = deadline - time.monotonic()
1325
1365
  if left <= 0:
1326
1366
  raise _DataFailure(
@@ -2289,6 +2329,7 @@ _OPS = {
2289
2329
  "data_put": _op_data_put,
2290
2330
  "data_begin": _op_data_begin,
2291
2331
  "data_drop": _op_data_drop,
2332
+ "data_failed": _op_data_failed,
2292
2333
  "data_put_at": _op_data_put_at,
2293
2334
  "data_seal": _op_data_seal,
2294
2335
  "data_pull": _op_data_pull,
@@ -2314,7 +2355,9 @@ _OPS = {
2314
2355
  }
2315
2356
 
2316
2357
  #: Answered by the frame reader at once, so they work while a call runs.
2317
- _LIGHT = ("stat", "lease", "reduce_peers")
2358
+ # ``data_failed`` is light because it must not queue behind the call it is about: the call
2359
+ # is blocked on a blob the sender will never send, so a queued answer would deadlock.
2360
+ _LIGHT = ("stat", "lease", "reduce_peers", "data_failed")
2318
2361
 
2319
2362
  #: Answered by several replies, the last one marked. These send their own replies.
2320
2363
  _STREAMING = ("data_stream",)
@@ -419,7 +419,7 @@ class Runtime:
419
419
  needed = [d for d in plan.missing if d in ship_digests and d not in wave]
420
420
  if needed:
421
421
  wave = needed + wave
422
- stream = pathdata.Stream(self, blobs, plan)
422
+ stream = pathdata.Stream(self, blobs, plan, collector.call_dir)
423
423
  if not plan.missing:
424
424
  # Nothing to stream: the runtime holds every file, so the call places them all
425
425
  # before the body starts. It needs no manifest, no patch and no observation.
@@ -23,7 +23,7 @@ from importlib import import_module
23
23
  from pathlib import Path
24
24
  from typing import TYPE_CHECKING, Any
25
25
 
26
- from ..errors import ConfigError, RemoteError, RuntimeFailure
26
+ from ..errors import ConfigError, ProtocolError, RemoteError, RuntimeFailure
27
27
 
28
28
  if TYPE_CHECKING:
29
29
  from ..providers.base import Provider
@@ -608,10 +608,12 @@ class Stream:
608
608
  order after the call starts comes from the runtime rather than from the prediction.
609
609
  """
610
610
 
611
- def __init__(self, runtime: Runtime, blobs: str, plan: Plan) -> None:
611
+ def __init__(self, runtime: Runtime, blobs: str, plan: Plan, call_dir: str = "") -> None:
612
612
  self.runtime = runtime
613
613
  self.blobs = blobs
614
614
  self.plan = plan
615
+ #: The call this stream feeds, so a stopped sender fails that call's readers only.
616
+ self.call_dir = call_dir
615
617
  #: Digests whose send finished. ``_sent`` holds the ones it began, so a blob the
616
618
  #: sender was carrying when the call ended is in one and not the other.
617
619
  self.done: set[str] = set()
@@ -662,13 +664,35 @@ class Stream:
662
664
  break
663
665
  # The pieces still in flight are answered before the stream is released.
664
666
  self._flush()
665
- except Exception:
666
- # The call owns the failure: a blob that never arrives fails the read that
667
- # waits for it, with the message spec "When a blob does not arrive" names.
668
- pass
667
+ except Exception as exc:
668
+ # Spec "When a blob does not arrive": the sender stopping leaves every blob
669
+ # still queued unsendable, so it is said once and the readers waiting on one
670
+ # are failed with the reason. Waiting it out costs the per blob timeout once
671
+ # for each, which is how 131 queued shards became a silent hour.
672
+ if not self._cancelled.is_set():
673
+ self._stopped(exc)
669
674
  finally:
670
675
  self.during_seconds = time.monotonic() - started
671
676
 
677
+ def _stopped(self, exc: BaseException) -> None:
678
+ """Say the sender stopped, and tell the runtime so its readers stop waiting."""
679
+ from .. import install
680
+
681
+ left = self.dropped
682
+ reason = f"{type(exc).__name__}: {exc}"
683
+ install.log(
684
+ f"data sending stopped after {len(self.done)} of "
685
+ f"{len(self.plan.missing)} files, {len(left)} not sent: {reason}"
686
+ )
687
+ try:
688
+ _worker(
689
+ self.runtime,
690
+ {"op": "data_failed", "dir": self.call_dir, "reason": reason},
691
+ )
692
+ except (RuntimeFailure, RemoteError, ProtocolError):
693
+ # The runtime is gone, which fails every reader on its own.
694
+ pass
695
+
672
696
  def _pump(self) -> bool:
673
697
  """Send the next blob of the queue. False once nothing is left to send."""
674
698
  self.reorder()
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.5.3"
141
+ version = "2.5.4"
142
142
  dependencies = [
143
143
  "letify-wire",
144
144
  "libloading",
@@ -146,7 +146,7 @@ dependencies = [
146
146
 
147
147
  [[package]]
148
148
  name = "letify-driver"
149
- version = "2.5.3"
149
+ version = "2.5.4"
150
150
  dependencies = [
151
151
  "letify-wire",
152
152
  ]
@@ -164,7 +164,7 @@ dependencies = [
164
164
 
165
165
  [[package]]
166
166
  name = "letify-wire"
167
- version = "2.5.3"
167
+ version = "2.5.4"
168
168
 
169
169
  [[package]]
170
170
  name = "libc"
@@ -8,7 +8,7 @@ resolver = "3"
8
8
  members = ["letify-wire", "letify-driver", "letify-agent", "letify-quic"]
9
9
 
10
10
  [workspace.package]
11
- version = "2.5.3"
11
+ version = "2.5.4"
12
12
  edition = "2024"
13
13
  license = "MIT"
14
14
  repository = "https://github.com/DarkPyonix/letify"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "letify-status",
3
- "version": "2.5.3",
3
+ "version": "2.5.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.5.3",
9
+ "version": "2.5.4",
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.5.3",
5
+ "version": "2.5.4",
6
6
  "publisher": "darkpyonix",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "letify"
3
- version = "2.5.3"
3
+ version = "2.5.4"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1641,3 +1641,46 @@ def test_a_cancelled_send_leaves_no_partial_in_the_blob_cache(
1641
1641
  root = Path.home() / ".letify" / "runtime" / "data" / "blobs"
1642
1642
  leftovers = [p for p in root.rglob("*") if p.is_file() and ".letify-" in p.name]
1643
1643
  assert leftovers == [], leftovers
1644
+
1645
+
1646
+ def test_a_sender_that_stops_fails_the_readers_waiting_on_it(
1647
+ let, cpu, project, capsys, monkeypatch
1648
+ ) -> None:
1649
+ """Spec "When a blob does not arrive": waiting it out costs the timeout per blob.
1650
+
1651
+ KICT sent 126.9 GB as 132 shards over the first wave budget. The sender raised on the
1652
+ second one, _run swallowed it, and the body's readers waited in silence: 540 s and
1653
+ counting on one shard, with 131 behind it.
1654
+ """
1655
+ from letify.store import pathdata
1656
+
1657
+ first = project / "a.bin"
1658
+ second = project / "b.bin"
1659
+ first.write_bytes(b"a" * (1 << 20))
1660
+ second.write_bytes(b"b" * (1 << 20))
1661
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
1662
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
1663
+
1664
+ real_send = pathdata.Stream._send
1665
+ sent: list[str] = []
1666
+
1667
+ def one_then_raise(self, digest, meter):
1668
+ if sent:
1669
+ raise RuntimeFailure("the transfer stream failed")
1670
+ sent.append(digest)
1671
+ return real_send(self, digest, meter)
1672
+
1673
+ monkeypatch.setattr(pathdata.Stream, "_send", one_then_raise)
1674
+
1675
+ @let.function(device=cpu, host=letify.remote)
1676
+ def read_both(one, two) -> int:
1677
+ return len(one.read_bytes()) + len(two.read_bytes())
1678
+
1679
+ started = time.monotonic()
1680
+ with pytest.raises(RuntimeFailure) as failure:
1681
+ read_both(one=first, two=second)
1682
+ elapsed = time.monotonic() - started
1683
+ assert elapsed < 60, f"waited {elapsed:.0f} s, so it waited out a timeout"
1684
+ assert "was never sent" in str(failure.value), failure.value
1685
+ err = capsys.readouterr().err
1686
+ assert "data sending stopped after" in err, err
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