straw-queue 0.1.1__tar.gz → 0.1.3__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.
- {straw_queue-0.1.1 → straw_queue-0.1.3}/CHANGELOG.md +27 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/CHANGELOG_zh.md +14 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/Cargo.lock +1 -1
- {straw_queue-0.1.1 → straw_queue-0.1.3}/Cargo.toml +1 -1
- {straw_queue-0.1.1 → straw_queue-0.1.3}/PKG-INFO +1 -1
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/BUILDING.md +3 -3
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/BUILDING_zh.md +3 -3
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/USAGE.md +49 -9
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/USAGE_zh.md +13 -3
- {straw_queue-0.1.1 → straw_queue-0.1.3}/pyproject.toml +1 -1
- {straw_queue-0.1.1 → straw_queue-0.1.3}/rust/coordinator.rs +5 -123
- {straw_queue-0.1.1 → straw_queue-0.1.3}/rust/gc.rs +5 -8
- {straw_queue-0.1.1 → straw_queue-0.1.3}/rust/journal.rs +5 -6
- {straw_queue-0.1.1 → straw_queue-0.1.3}/rust/lib.rs +1 -0
- straw_queue-0.1.3/rust/payload.rs +40 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/rust/python.rs +75 -39
- {straw_queue-0.1.1 → straw_queue-0.1.3}/rust/store.rs +145 -51
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/protocol.py +10 -5
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/rpc.py +8 -11
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/store.py +7 -1
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/tensor.py +7 -4
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/native.rs +54 -0
- straw_queue-0.1.3/tests/test_extent_visibility.py +126 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_filesystem_queue.py +81 -220
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_packed_store.py +3 -5
- straw_queue-0.1.3/tests/test_streaming_write.py +146 -0
- straw_queue-0.1.3/tests/test_unlimited_capacity.py +119 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/.github/workflows/tests.yml +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/.github/workflows/wheels.yml +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/CONTRIBUTING.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/CONTRIBUTING_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/LICENSE +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/README.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/README_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/APPLICATIONS.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/APPLICATIONS_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/BENCHMARKS.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/BENCHMARKS_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/DESIGN.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/DESIGN_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/FILESYSTEM.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/FILESYSTEM_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/FORMAT.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/FORMAT_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/PROTOCOL_AND_VERIFICATION.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/PROTOCOL_AND_VERIFICATION_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/README.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/README_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/VERIFICATION.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/docs/VERIFICATION_zh.md +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/examples/multiprocess.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/examples/records.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/examples/tensors.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/examples/work_queue.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/__init__.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/backend.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/benchmark.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/coordinator.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/errors.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/inspect.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/journal.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/reporting.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/src/straw/tracing.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_benchmark_profile.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_consumer_rewind.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_gc_extent_validation.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_online_gc.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_read_session.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tests/test_tensor.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/tools/check_wheel.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/verification/crash_campaign.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/verification/gc_cross_client.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/verification/gc_scale.py +0 -0
- {straw_queue-0.1.1 → straw_queue-0.1.3}/verification/ownership_model.py +0 -0
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
[中文版](CHANGELOG_zh.md)
|
|
4
4
|
|
|
5
|
+
## 0.1.3
|
|
6
|
+
|
|
7
|
+
- Retry temporarily zero-filled bytes anywhere in a published extent's header,
|
|
8
|
+
including partial and noncontiguous visibility gaps. Previously, only an
|
|
9
|
+
entirely zero-filled header triggered this retry. The retry limit and checksum
|
|
10
|
+
validation remain unchanged; other incorrect header bytes fail immediately.
|
|
11
|
+
|
|
12
|
+
Existing callers and stored data remain compatible. Packed-record and WAL
|
|
13
|
+
formats are unchanged.
|
|
14
|
+
|
|
15
|
+
## 0.1.2
|
|
16
|
+
|
|
17
|
+
- Stream large record payloads through bounded native write buffers. Records and
|
|
18
|
+
publications no longer need to fit the writer's scratch buffer; existing
|
|
19
|
+
packed-record framing and tensor reads are unchanged.
|
|
20
|
+
- Remove fixed logical quotas on record and dependency counts, result sizes,
|
|
21
|
+
queue work, control messages, and metadata. Legacy limit arguments remain
|
|
22
|
+
accepted for API and queue-identity compatibility; `max_buffer_bytes` still
|
|
23
|
+
bounds temporary payload copies.
|
|
24
|
+
- Retry transient shared-filesystem visibility gaps when a published extent's
|
|
25
|
+
length is visible before its header bytes. The reader reopens the file after
|
|
26
|
+
short delays; persistent failures, nonzero bad headers, and checksum errors
|
|
27
|
+
remain errors.
|
|
28
|
+
|
|
29
|
+
Existing stored data and callers remain compatible. These write and read
|
|
30
|
+
behaviors require 0.1.2; packed-record and WAL formats are unchanged.
|
|
31
|
+
|
|
5
32
|
## 0.1.1
|
|
6
33
|
|
|
7
34
|
- Persistent task scheduling with `TaskSpec.priority` (higher first),
|
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
[English](CHANGELOG.md)
|
|
4
4
|
|
|
5
|
+
## 0.1.3
|
|
6
|
+
|
|
7
|
+
- 已发布 extent 的文件头在任意位置出现暂时零值时都会触发重试,支持部分字节以及不连续位置尚不可见的情况。此前只有整个文件头为零时才会触发这类重试。重试上限和 checksum 校验保持不变;其他错误的文件头字节仍会立即报错。
|
|
8
|
+
|
|
9
|
+
已有调用方和存储数据保持兼容,packed record 和 WAL 格式不变。
|
|
10
|
+
|
|
11
|
+
## 0.1.2
|
|
12
|
+
|
|
13
|
+
- 大记录的 payload 通过有界 native 写入缓冲区分块写入。记录和批量发布不再需要适配 writer 的临时缓冲区;原有 packed record 格式和张量读取方式不变。
|
|
14
|
+
- 移除记录数、依赖数、结果大小、队列任务、控制消息及元数据的固定逻辑配额。旧的限制参数继续保留,以兼容 API 和队列身份;`max_buffer_bytes` 仍约束临时 payload 复制。
|
|
15
|
+
- 共享文件系统上,已发布 extent 的文件长度可能先于文件头内容可见。reader 遇到这种短暂情况会延迟并重新打开文件;持续失败、非零坏文件头和 checksum 错误仍会报错。
|
|
16
|
+
|
|
17
|
+
已有存储数据和调用方保持兼容。上述写入和读取行为需要 0.1.2;packed record 和 WAL 格式不变。
|
|
18
|
+
|
|
5
19
|
## 0.1.1
|
|
6
20
|
|
|
7
21
|
- 持久化任务调度:按 `TaskSpec.priority` 从高到低、`scheduling_key` 从低到高、再按 FIFO 顺序领取任务。两个字段默认均为 0,接受有符号 64 位整数。Rust pending 索引无需读取载荷即可选择任务,WAL 恢复时重建调度顺序。
|
|
@@ -139,11 +139,11 @@ and [publishing guide](https://docs.pypi.org/trusted-publishers/using-a-publishe
|
|
|
139
139
|
|
|
140
140
|
After committing the release changes, ensure that `pyproject.toml`, `Cargo.toml`
|
|
141
141
|
and the `straw` entry in `Cargo.lock` share the release version. For example, for
|
|
142
|
-
version `0.1.
|
|
142
|
+
version `0.1.2`, create and push a new tag on that commit:
|
|
143
143
|
|
|
144
144
|
```sh
|
|
145
|
-
git tag v0.1.
|
|
146
|
-
git push origin v0.1.
|
|
145
|
+
git tag v0.1.2
|
|
146
|
+
git push origin v0.1.2
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
The first successful upload creates the PyPI project; registering a pending
|
|
@@ -90,11 +90,11 @@ python -m pip wheel --no-deps dist/straw_queue-*.tar.gz --wheel-dir rebuilt
|
|
|
90
90
|
|
|
91
91
|
Workflow filename 只填 `wheels.yml`,不带 `.github/workflows/`。发布前,将该文件提交并推送到配置的仓库。发布 job 通过 GitHub OIDC 和 `id-token: write` 认证,无需配置 PyPI API token 或 GitHub secret。参见 [PyPI 配置指南](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)和[发布指南](https://docs.pypi.org/trusted-publishers/using-a-publisher/)。
|
|
92
92
|
|
|
93
|
-
提交发布改动后,确认 `pyproject.toml`、`Cargo.toml` 和 `Cargo.lock` 中 `straw` 条目的版本一致。例如发布 `0.1.
|
|
93
|
+
提交发布改动后,确认 `pyproject.toml`、`Cargo.toml` 和 `Cargo.lock` 中 `straw` 条目的版本一致。例如发布 `0.1.2` 时,在该 commit 上创建并推送新的 tag:
|
|
94
94
|
|
|
95
95
|
```sh
|
|
96
|
-
git tag v0.1.
|
|
97
|
-
git push origin v0.1.
|
|
96
|
+
git tag v0.1.2
|
|
97
|
+
git push origin v0.1.2
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
首次上传成功时才创建 PyPI 项目;仅登记 pending publisher 不会保留包名。后续版本使用新的匹配版本号和 tag。已有 tag 不会包含之后的工作区改动;发布前应检查其指向,不要覆盖已发布版本。构建或测试失败时,应先修复;失败的 job 会阻止发布 job 运行。
|
|
@@ -15,8 +15,8 @@ store = SharedFilesystemStore(
|
|
|
15
15
|
"/shared/new-job", "new-job",
|
|
16
16
|
online_gc=True,
|
|
17
17
|
segment_target_bytes=1024**3, # Rotation target, not a hard file-size limit.
|
|
18
|
-
max_record_bytes=
|
|
19
|
-
max_buffer_bytes=
|
|
18
|
+
max_record_bytes=16 * 1024**3,
|
|
19
|
+
max_buffer_bytes=16 * 1024**2,
|
|
20
20
|
)
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -31,8 +31,37 @@ into a GC-enabled pool and assume its old references have owners. GC is opt-in;
|
|
|
31
31
|
when a catalog exists, all participants must honor it. None of these calls
|
|
32
32
|
create a scheduler or start an automatic GC thread.
|
|
33
33
|
|
|
34
|
+
## Large records and bounded write buffers
|
|
35
|
+
|
|
36
|
+
`max_record_bytes` is a compatibility argument and no longer caps records.
|
|
37
|
+
`max_buffer_bytes` bounds temporary payload copies inside the native writer,
|
|
38
|
+
independently of record/publication size. The writer pins Python input buffers
|
|
39
|
+
and copies at most 4 MiB per chunk; large records pipeline copying and I/O using
|
|
40
|
+
at most two chunks. Applications do not need to split payloads to fit scratch
|
|
41
|
+
memory. Metadata/index allocations, caller-owned inputs, Python serialization
|
|
42
|
+
and full-record reads are outside this payload-copy budget. There are no fixed
|
|
43
|
+
record-count, dependency-count, metadata, manifest or index size quotas.
|
|
44
|
+
|
|
45
|
+
Do not mutate input buffers until publication returns. The writer rechecks each
|
|
46
|
+
payload while writing and rejects detected changes before committing. Chunked
|
|
47
|
+
I/O retains the existing record framing and tensor slice API: it does not expose
|
|
48
|
+
partial records or alter durable publication, retry, ownership or GC ordering.
|
|
49
|
+
A pack rotation target is soft; a large record/publication may exceed it. Reads
|
|
50
|
+
of old files retain their stored checksum chunk sizes. A reader configured with
|
|
51
|
+
an explicit smaller record limit must raise it to read larger records.
|
|
52
|
+
|
|
34
53
|
## Concurrency: processes and threads
|
|
35
54
|
|
|
55
|
+
Published extents are immutable, but a shared-filesystem reader can briefly see
|
|
56
|
+
the new file length before the appended header bytes. The native reader closes
|
|
57
|
+
and reopens the file on a short extent or an all-zero 12-byte header, with delays
|
|
58
|
+
of 100, 250, 500, 1000 and 2000 ms. It still validates the header, index and record
|
|
59
|
+
data normally. Persistent failures remain errors; nonzero bad headers and
|
|
60
|
+
checksum failures are not retried by this visibility handling. This applies to
|
|
61
|
+
store and read-session entry points, including dependency validation. Successful
|
|
62
|
+
reads incur no extra I/O, and applications should not stack another retry loop
|
|
63
|
+
around the resulting corruption error.
|
|
64
|
+
|
|
36
65
|
The API is synchronous; straw does not launch an I/O process pool. Applications
|
|
37
66
|
can read/write from multiple processes and machines using the same pool. Each
|
|
38
67
|
writer needs its own store instance and pack stream. Keep these instances alive
|
|
@@ -125,7 +154,7 @@ requires a leading dimension. `.load()` returns a CPU PyTorch tensor; call
|
|
|
125
154
|
`ref[start:stop]` reads contiguous rows and verifies intersecting 4 MiB chunks.
|
|
126
155
|
Only slices with step 1 are supported. These are file reads, not mmap views.
|
|
127
156
|
`load(pin_memory=True)` requests pinned CPU memory where PyTorch supports it.
|
|
128
|
-
|
|
157
|
+
Large tensors stream through bounded write scratch without record/publication quotas.
|
|
129
158
|
|
|
130
159
|
To restore several tensor descriptors from one publication efficiently:
|
|
131
160
|
|
|
@@ -193,11 +222,22 @@ for durable intermediate inputs while retaining the lease; `yield_task` or
|
|
|
193
222
|
`fail_task` and timeouts use the task's attempt budget. Only publish a
|
|
194
223
|
continuation when all its fields describe one complete, consistent prefix.
|
|
195
224
|
|
|
196
|
-
`Limits`
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
225
|
+
`Limits` retains its fields solely for API and stored queue-identity compatibility.
|
|
226
|
+
They no longer reject pending/in-flight work, result bytes/tokens/records, ready
|
|
227
|
+
batches or control messages. Applications control concurrency through requested
|
|
228
|
+
acquisition sizes and their own schedulers. Accounting metrics remain available;
|
|
229
|
+
large immutable results are accepted by reference without rewriting payloads.
|
|
230
|
+
Pass the same legacy limits, codecs, lease duration, run ID and queue ID when
|
|
231
|
+
recovering an existing queue. In protocol v1 the default queue ID is `rollout`.
|
|
232
|
+
|
|
233
|
+
The removed fixed quotas include 4 GiB per result, 64 GiB of outstanding results,
|
|
234
|
+
100 million tokens, 10,000 records/dependency nodes, 64 KiB record envelopes,
|
|
235
|
+
256 KiB control metadata, 8 MiB indexes/manifests/journal transactions and 64 MiB
|
|
236
|
+
catalog transactions. The existing frame uses a u32 envelope length; that format
|
|
237
|
+
boundary, u64 counters, authenticated file extents, checksums, task/attempt
|
|
238
|
+
ownership, retry counts, leases and writer exclusion remain enforced. Removing
|
|
239
|
+
quotas does not provide unlimited RAM/disk; metadata is still materialized in
|
|
240
|
+
memory and payload writes retain bounded scratch buffers.
|
|
201
241
|
|
|
202
242
|
## Lifetime and online GC
|
|
203
243
|
|
|
@@ -276,7 +316,7 @@ Use one `with store.read_session() as reader:` block for adjacent immutable publ
|
|
|
276
316
|
|
|
277
317
|
The Rust reader retains only the last authenticated index, keyed by the complete extent descriptor. Each manifest and record read still authenticates its frame and payload; tensor reads authenticate touched chunks. `validate` rechecks logical descriptors and task/attempt authorization on every call. Index authentication runs again after an extent switch or in a new session. Keep a durable owner or `store.pin` for the entire operation: a read session is not an ownership pin. Sessions close on context exit and reject all inherited operations after fork.
|
|
278
318
|
|
|
279
|
-
Publication dependency validation, continuation batches, consumer snapshots and GC traversal use the same bounded native reader internally. Formats, WAL commit ordering,
|
|
319
|
+
Publication dependency validation, continuation batches, consumer snapshots and GC traversal use the same bounded native reader internally. Formats, WAL commit ordering, owner-release rules remain unchanged; logical capacity quotas are no longer enforced.
|
|
280
320
|
|
|
281
321
|
`store.read(publication)` also owns a scoped session for the lifetime of its iterator. Exhaust or close the iterator before releasing its data owner. Separate iterators reauthenticate the index.
|
|
282
322
|
|
|
@@ -13,8 +13,8 @@ store = SharedFilesystemStore(
|
|
|
13
13
|
"/shared/new-job", "new-job",
|
|
14
14
|
online_gc=True,
|
|
15
15
|
segment_target_bytes=1024**3, # Rotation target, not a hard file-size limit.
|
|
16
|
-
max_record_bytes=
|
|
17
|
-
max_buffer_bytes=
|
|
16
|
+
max_record_bytes=16 * 1024**3,
|
|
17
|
+
max_buffer_bytes=16 * 1024**2,
|
|
18
18
|
)
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -22,8 +22,16 @@ store = SharedFilesystemStore(
|
|
|
22
22
|
|
|
23
23
|
应从创建池时就启用在线 GC。不能把已有但未跟踪所有权的队列直接切换为 GC 模式,并假定旧引用已经获得保护。GC 是可选能力;catalog 一旦存在,所有参与者都必须遵循它的规则。这些 API 不会创建调度器,也不会启动自动 GC 线程。
|
|
24
24
|
|
|
25
|
+
## 大记录与有界写入缓冲区
|
|
26
|
+
|
|
27
|
+
`max_record_bytes` 只保留为兼容参数,不再限制单条记录。`max_buffer_bytes` 仍限制 native writer 临时复制的 payload 内存,不限制整个 record 或 publication。每块最多复制 4 MiB,大记录采用双缓冲复制/I/O 流水线。调用方不需要自行拆分 sample。元数据/索引、调用方已有输入、Python 序列化和整条读回不包含在 scratch 预算内;记录数、依赖数、元数据和索引不再有固定容量配额。
|
|
28
|
+
|
|
29
|
+
发布返回前不要修改输入 buffer。写入时会再次校验 payload,检测到变化会在提交前拒绝。分块 I/O 保持原有 record 格式及张量切片 API,不会暴露部分记录,也不改变持久化、重试、所有权和 GC 顺序。pack 大小是软轮转目标,大 record/publication 可以超过目标。旧文件仍使用其中保存的 checksum 分块大小;reader 同样不再受旧 record 大小参数限制。
|
|
30
|
+
|
|
25
31
|
## 进程与线程并发
|
|
26
32
|
|
|
33
|
+
已发布的 extent 不可变,但共享文件系统的 reader 可能短暂先看到增长后的文件大小、后看到追加的文件头。对于文件长度尚不足或 12 字节文件头全零的情况,native reader 会关闭文件,依次等待 100、250、500、1000、2000 ms 后重新打开,再进行原有校验。持续失败仍报错;非零的坏文件头和 checksum 错误不走此可见性重试。该处理覆盖 store、read session 和依赖校验等入口,正常读取不增加 I/O,应用无需再叠加另一层 CorruptData 重试。
|
|
34
|
+
|
|
27
35
|
API 是同步的,straw 不会启动 I/O 进程池。应用可从多个进程、多台机器访问同一个池。每个 writer 需要独立的 store 实例和 pack 写入流,并在多次发布之间复用实例。不要将可变 store/coordinator 对象跨 fork 继承后继续使用。
|
|
28
36
|
|
|
29
37
|
Rust 在 native I/O、checksum 和日志操作期间释放 GIL,Python 线程可并行执行这些操作。Python 序列化、张量转换和输入缓冲区复制仍有开销。每个 store 同时只允许一个写操作;同实例的重叠写入会报 `ResourceLimitExceeded`,因此需由调用方串行化,或为每个 writer 创建独立实例。引用受到保护期间,reader 可以并发读取。
|
|
@@ -122,7 +130,9 @@ b.submit_tasks("b-1", [TaskSpec("task-b", input_ref=dependency)])
|
|
|
122
130
|
|
|
123
131
|
通过 `heartbeat` 续租。用 `save_task_progress`/`save_task_progress_many` 在保留 lease 的同时持久化中间输入;`yield_task` 或 `release_tasks` 把未完成任务退回队列,不消耗失败重试预算。`fail_task` 与超时会消耗尝试预算。只有全部字段都对应同一个完整、一致的前缀,才能发布 continuation。
|
|
124
132
|
|
|
125
|
-
`Limits`
|
|
133
|
+
`Limits` 的字段仅用于兼容旧调用方及队列恢复身份,不再限制 pending/in-flight 任务、结果字节/token/记录数、ready batch 或控制消息。应用通过 acquire 请求数量和自己的调度器控制并发,原有使用量统计保留。移除了单结果 4 GiB、累计 64 GiB、1 亿 token、1 万记录/依赖节点、64 KiB envelope、256 KiB 控制元数据、8 MiB 索引/manifest/事务日志以及 64 MiB catalog 事务等配额。恢复旧队列仍应传入与原先相同的 Limits 字段、codec、lease 时长、run ID 和 queue ID。
|
|
134
|
+
|
|
135
|
+
保留真实格式和正确性检查:u32 envelope 长度、u64 计数、文件边界与校验和、task/attempt 归属、lease、失败重试数及同一 writer 不可重入。移除逻辑配额不代表无限内存/磁盘;元数据仍需占用内存,payload 写入继续使用有界 scratch。
|
|
126
136
|
|
|
127
137
|
## 生命周期与在线 GC
|
|
128
138
|
|
|
@@ -54,15 +54,6 @@ fn retry(task: &mut Value) {
|
|
|
54
54
|
});
|
|
55
55
|
task["lease"] = Value::Null;
|
|
56
56
|
}
|
|
57
|
-
fn bounded(value: &Value, limit: u64) -> Result<()> {
|
|
58
|
-
if bytes(value)?.len() as u64 > limit {
|
|
59
|
-
return fail(
|
|
60
|
-
"ResourceLimitExceeded",
|
|
61
|
-
"Control metadata limit exceeded; use a manifest",
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
Ok(())
|
|
65
|
-
}
|
|
66
57
|
fn validate_scheduling(value: &Value) -> Result<()> {
|
|
67
58
|
for key in ["priority", "scheduling_key"] {
|
|
68
59
|
if value.get(key).is_some_and(|v| v.as_i64().is_none()) {
|
|
@@ -697,10 +688,8 @@ impl Coordinator {
|
|
|
697
688
|
"Coordinator unavailable; stop owner and recover",
|
|
698
689
|
);
|
|
699
690
|
}
|
|
700
|
-
let metadata = number(&self.limits, "metadata_bytes");
|
|
701
691
|
match method {
|
|
702
692
|
"release_task_reads" => {
|
|
703
|
-
bounded(args, metadata)?;
|
|
704
693
|
for lease in array(args, "leases")? {
|
|
705
694
|
let prior = &self.state["readers"][field(lease, "attempt_id")?];
|
|
706
695
|
if !prior.is_null() && prior["lease"] != *lease {
|
|
@@ -721,7 +710,7 @@ impl Coordinator {
|
|
|
721
710
|
"submit_tasks" => {
|
|
722
711
|
let tasks = array(args, "tasks")?;
|
|
723
712
|
let producer = args["producer_id"].as_str();
|
|
724
|
-
if producer.is_some()
|
|
713
|
+
if producer.is_some() == args["producer_state"].is_null() {
|
|
725
714
|
return fail("ValueError", "Producer ID/state must be provided together");
|
|
726
715
|
}
|
|
727
716
|
let content = if producer.is_some() {
|
|
@@ -729,7 +718,6 @@ impl Coordinator {
|
|
|
729
718
|
} else {
|
|
730
719
|
json!(tasks)
|
|
731
720
|
};
|
|
732
|
-
bounded(&content, metadata)?;
|
|
733
721
|
let request = self.request("submit", field(args, "request_id")?, &content)?;
|
|
734
722
|
if !request.2.is_null() {
|
|
735
723
|
self.count("duplicate_submissions");
|
|
@@ -744,29 +732,6 @@ impl Coordinator {
|
|
|
744
732
|
"Scheduling paused after storage failure",
|
|
745
733
|
);
|
|
746
734
|
}
|
|
747
|
-
for (control, limit) in [
|
|
748
|
-
(false, number(&self.limits, "pending_tasks")),
|
|
749
|
-
(true, number(&self.limits, "control_tasks")),
|
|
750
|
-
] {
|
|
751
|
-
let existing = vals(&self.state["tasks"])
|
|
752
|
-
.filter(|t| {
|
|
753
|
-
(t["state"] == "pending" || (control && t["state"] == "leased"))
|
|
754
|
-
&& boolean(&t["spec"], "control") == control
|
|
755
|
-
})
|
|
756
|
-
.count();
|
|
757
|
-
if existing as u64
|
|
758
|
-
+ tasks
|
|
759
|
-
.iter()
|
|
760
|
-
.filter(|t| boolean(t, "control") == control)
|
|
761
|
-
.count() as u64
|
|
762
|
-
> limit
|
|
763
|
-
{
|
|
764
|
-
return fail(
|
|
765
|
-
"ResourceLimitExceeded",
|
|
766
|
-
"Pending/control task budget exceeded",
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
735
|
let mut ids = vec![];
|
|
771
736
|
let mut unique = HashSet::new();
|
|
772
737
|
for t in tasks {
|
|
@@ -785,12 +750,6 @@ impl Coordinator {
|
|
|
785
750
|
if boolean(t, "control") && estimate != 0 {
|
|
786
751
|
return fail("ValueError", "Control tasks cannot reserve production");
|
|
787
752
|
};
|
|
788
|
-
if estimate > number(&self.limits, &format!("accepted_{key}")) {
|
|
789
|
-
return fail(
|
|
790
|
-
"ResourceLimitExceeded",
|
|
791
|
-
"Task estimate exceeds output budget",
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
753
|
}
|
|
795
754
|
if !t["input_ref"].is_null() {
|
|
796
755
|
self.validate(&t["input_ref"], None, None)?;
|
|
@@ -803,11 +762,7 @@ impl Coordinator {
|
|
|
803
762
|
"acquire" => {
|
|
804
763
|
let worker = field(args, "worker_id")?;
|
|
805
764
|
let max = args["max_tasks"].as_u64().unwrap_or(1);
|
|
806
|
-
if worker.is_empty()
|
|
807
|
-
|| worker.len() > 1024
|
|
808
|
-
|| max == 0
|
|
809
|
-
|| max > number(&self.limits, "inflight_tasks")
|
|
810
|
-
{
|
|
765
|
+
if worker.is_empty() || worker.len() > 1024 || max == 0 {
|
|
811
766
|
return fail("ValueError", "Invalid worker identity/max tasks");
|
|
812
767
|
}
|
|
813
768
|
let expired = self
|
|
@@ -835,14 +790,8 @@ impl Coordinator {
|
|
|
835
790
|
}
|
|
836
791
|
let control = boolean(args, "control");
|
|
837
792
|
let ids = args["task_ids"].as_array();
|
|
838
|
-
if let Some(ids) = ids {
|
|
839
|
-
bounded(&json!(ids), metadata)?;
|
|
840
|
-
}
|
|
841
793
|
let prefix = args["task_prefix"].as_str();
|
|
842
|
-
let mut usage = self.usage()?;
|
|
843
794
|
let mut assignments = vec![];
|
|
844
|
-
let mut assignment_bytes = 64u64;
|
|
845
|
-
let mut pending = false;
|
|
846
795
|
let candidates: Vec<&String> = if let Some(ids) = ids {
|
|
847
796
|
let mut selected = ids
|
|
848
797
|
.iter()
|
|
@@ -867,61 +816,20 @@ impl Coordinator {
|
|
|
867
816
|
{
|
|
868
817
|
continue;
|
|
869
818
|
}
|
|
870
|
-
pending = true;
|
|
871
|
-
if control {
|
|
872
|
-
if number(&usage, "control_inflight")
|
|
873
|
-
>= number(&self.limits, "control_tasks")
|
|
874
|
-
{
|
|
875
|
-
continue;
|
|
876
|
-
}
|
|
877
|
-
} else if number(&usage, "inflight") >= number(&self.limits, "inflight_tasks")
|
|
878
|
-
|| number(&usage, "ready_bytes") >= number(&self.limits, "ready_bytes")
|
|
879
|
-
|| ["records", "bytes", "tokens"].iter().any(|k| {
|
|
880
|
-
number(&usage, k) >= number(&self.limits, &format!("accepted_{k}"))
|
|
881
|
-
|| number(&usage, k) + number(spec, &format!("estimated_{k}"))
|
|
882
|
-
> number(&self.limits, &format!("accepted_{k}"))
|
|
883
|
-
})
|
|
884
|
-
{
|
|
885
|
-
continue;
|
|
886
|
-
}
|
|
887
819
|
let lease = json!({"run_id":self.store.config.run_id,"queue_id":self.queue_id,"task_id":id,"attempt_id":uid(),"generation":number(task,"generation")+1,"coordinator_epoch":self.epoch,"token":format!("{}{}",uid(),uid()),"worker_id":worker});
|
|
888
820
|
let assignment = json!({"task":spec,"lease":lease});
|
|
889
|
-
let size = bytes(&assignment)?.len() as u64 + 1;
|
|
890
|
-
if assignment_bytes + size > metadata {
|
|
891
|
-
if assignments.is_empty() {
|
|
892
|
-
return fail(
|
|
893
|
-
"ResourceLimitExceeded",
|
|
894
|
-
"Assignment exceeds control metadata limit",
|
|
895
|
-
);
|
|
896
|
-
};
|
|
897
|
-
break;
|
|
898
|
-
}
|
|
899
|
-
assignment_bytes += size;
|
|
900
821
|
assignments.push(assignment);
|
|
901
|
-
let key = if control {
|
|
902
|
-
"control_inflight"
|
|
903
|
-
} else {
|
|
904
|
-
"inflight"
|
|
905
|
-
};
|
|
906
|
-
usage[key] = json!(number(&usage, key) + 1);
|
|
907
|
-
for k in ["records", "bytes", "tokens"] {
|
|
908
|
-
usage[k] =
|
|
909
|
-
json!(number(&usage, k) + number(spec, &format!("estimated_{k}")));
|
|
910
|
-
}
|
|
911
822
|
if assignments.len() as u64 == max {
|
|
912
823
|
break;
|
|
913
824
|
}
|
|
914
825
|
}
|
|
915
826
|
if assignments.is_empty() {
|
|
916
|
-
return Ok(
|
|
917
|
-
json!({"status":if pending{"backpressured"}else{"empty"},"assignments":[]}),
|
|
918
|
-
);
|
|
827
|
+
return Ok(json!({"status":"empty","assignments":[]}));
|
|
919
828
|
}
|
|
920
829
|
let leases = assignments
|
|
921
830
|
.iter()
|
|
922
831
|
.map(|a| a["lease"].clone())
|
|
923
832
|
.collect::<Vec<_>>();
|
|
924
|
-
bounded(&json!(leases), metadata)?;
|
|
925
833
|
self.commit(json!([{"type":"Leased","leases":leases}]), fault)?;
|
|
926
834
|
for lease in leases {
|
|
927
835
|
self.deadlines
|
|
@@ -932,7 +840,6 @@ impl Coordinator {
|
|
|
932
840
|
"save_task_progress_many" | "yield_tasks" => {
|
|
933
841
|
let yielding = method == "yield_tasks";
|
|
934
842
|
let updates = array(args, "updates")?;
|
|
935
|
-
bounded(&json!(updates), metadata)?;
|
|
936
843
|
let request = self.request(
|
|
937
844
|
if yielding {
|
|
938
845
|
"yield_many"
|
|
@@ -987,7 +894,6 @@ impl Coordinator {
|
|
|
987
894
|
}
|
|
988
895
|
"release_tasks" => {
|
|
989
896
|
let leases = array(args, "leases")?;
|
|
990
|
-
bounded(&json!(leases), metadata)?;
|
|
991
897
|
let request =
|
|
992
898
|
self.request("release_tasks", field(args, "request_id")?, &json!(leases))?;
|
|
993
899
|
if !request.2.is_null() {
|
|
@@ -1021,7 +927,6 @@ impl Coordinator {
|
|
|
1021
927
|
}
|
|
1022
928
|
"heartbeat" => {
|
|
1023
929
|
let leases = array(args, "leases")?;
|
|
1024
|
-
bounded(&json!(leases), metadata)?;
|
|
1025
930
|
let mut results = vec![];
|
|
1026
931
|
for lease in leases {
|
|
1027
932
|
match self.valid_lease(lease, now) {
|
|
@@ -1040,7 +945,6 @@ impl Coordinator {
|
|
|
1040
945
|
}
|
|
1041
946
|
"complete_task" => {
|
|
1042
947
|
let started = Instant::now();
|
|
1043
|
-
bounded(args, metadata)?;
|
|
1044
948
|
let lease = &args["lease"];
|
|
1045
949
|
let id = field(args, "submission_id")?;
|
|
1046
950
|
if id.is_empty() || id.len() > 1024 {
|
|
@@ -1070,14 +974,6 @@ impl Coordinator {
|
|
|
1070
974
|
if number(reference, "records") == 0 && !boolean(&task["spec"], "allow_empty") {
|
|
1071
975
|
return fail("InvalidReference", "Task does not allow empty output");
|
|
1072
976
|
}
|
|
1073
|
-
if number(reference, "payload_bytes") > number(&self.limits, "max_result_bytes")
|
|
1074
|
-
|| number(reference, "tokens") > number(&self.limits, "max_result_tokens")
|
|
1075
|
-
{
|
|
1076
|
-
return fail(
|
|
1077
|
-
"ResourceLimitExceeded",
|
|
1078
|
-
"Per-task result upper bound exceeded",
|
|
1079
|
-
);
|
|
1080
|
-
}
|
|
1081
977
|
self.validate(
|
|
1082
978
|
reference,
|
|
1083
979
|
Some(field(lease, "task_id")?),
|
|
@@ -1100,7 +996,6 @@ impl Coordinator {
|
|
|
1100
996
|
let retryable = args["retryable"].as_bool().unwrap_or(true);
|
|
1101
997
|
let failure = &args["failure"];
|
|
1102
998
|
let content = json!({"lease":lease,"failure":failure,"retryable":retryable});
|
|
1103
|
-
bounded(&content, metadata)?;
|
|
1104
999
|
let request = self.request("fail", field(args, "request_id")?, &content)?;
|
|
1105
1000
|
if !request.2.is_null() {
|
|
1106
1001
|
return Ok(request.2);
|
|
@@ -1124,7 +1019,6 @@ impl Coordinator {
|
|
|
1124
1019
|
}
|
|
1125
1020
|
"yield_task" | "save_task_progress" => {
|
|
1126
1021
|
let content = json!({"lease":args["lease"],"input_ref":args["input_ref"]});
|
|
1127
|
-
bounded(&content, metadata)?;
|
|
1128
1022
|
let yielding = method == "yield_task";
|
|
1129
1023
|
let request = self.request(
|
|
1130
1024
|
if yielding { "yield" } else { "task_progress" },
|
|
@@ -1207,10 +1101,10 @@ impl Coordinator {
|
|
|
1207
1101
|
let commits = array(&self.state, "commits")?;
|
|
1208
1102
|
let cursor = number(args, "cursor") as usize;
|
|
1209
1103
|
let limit = args["limit"].as_u64().unwrap_or(100) as usize;
|
|
1210
|
-
if cursor > commits.len() || limit == 0
|
|
1104
|
+
if cursor > commits.len() || limit == 0 {
|
|
1211
1105
|
return fail("ValueError", "Invalid cursor/page limit");
|
|
1212
1106
|
}
|
|
1213
|
-
let
|
|
1107
|
+
let end = cursor.saturating_add(limit).min(commits.len());
|
|
1214
1108
|
if cursor < number(&self.state, "retired_prefix") as usize
|
|
1215
1109
|
|| array(&self.state, "retired_positions")?
|
|
1216
1110
|
.iter()
|
|
@@ -1222,15 +1116,6 @@ impl Coordinator {
|
|
|
1222
1116
|
"Requested history was released; resume from a retained checkpoint or a live cursor",
|
|
1223
1117
|
);
|
|
1224
1118
|
}
|
|
1225
|
-
while bytes(&json!(&commits[cursor..end]))?.len() as u64 > metadata {
|
|
1226
|
-
end = cursor + (end - cursor) / 2;
|
|
1227
|
-
}
|
|
1228
|
-
if end == cursor && cursor < commits.len() {
|
|
1229
|
-
return fail(
|
|
1230
|
-
"ResourceLimitExceeded",
|
|
1231
|
-
"Commit exceeds control message limit",
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
1119
|
Ok(
|
|
1235
1120
|
json!({"commits":&commits[cursor..end],"cursor":end,"end_of_input":self.sealed&&end==commits.len()&&vals(&self.state["tasks"]).all(|t|terminal(&t["state"]))}),
|
|
1236
1121
|
)
|
|
@@ -1324,7 +1209,6 @@ impl Coordinator {
|
|
|
1324
1209
|
}
|
|
1325
1210
|
self.validate(&args["plan_ref"], None, None)?;
|
|
1326
1211
|
let batch = json!({"batch_id":batch_id,"consumer_id":id,"input_positions":positions,"plan_ref":args["plan_ref"],"ready":false});
|
|
1327
|
-
bounded(&batch, metadata)?;
|
|
1328
1212
|
let request = self.request("plan_batch", batch_id, &batch)?;
|
|
1329
1213
|
if !request.2.is_null() {
|
|
1330
1214
|
return Ok(self.state["batches"][batch_id].clone());
|
|
@@ -1350,7 +1234,6 @@ impl Coordinator {
|
|
|
1350
1234
|
self.validate(&args["ready_ref"], None, None)?;
|
|
1351
1235
|
let state = self.consumer_state(args)?;
|
|
1352
1236
|
let content = json!({"batch_id":batch,"ready_ref":args["ready_ref"],"state":state});
|
|
1353
|
-
bounded(&content, metadata)?;
|
|
1354
1237
|
let request = self.request("batch_ready", batch, &content)?;
|
|
1355
1238
|
if !request.2.is_null() {
|
|
1356
1239
|
return Ok(request.2);
|
|
@@ -1396,7 +1279,6 @@ impl Coordinator {
|
|
|
1396
1279
|
}
|
|
1397
1280
|
let mut content = checkpoint;
|
|
1398
1281
|
content["checkpoint_ref"] = reference.clone();
|
|
1399
|
-
bounded(&content, metadata)?;
|
|
1400
1282
|
let request = self.request("checkpoint", id, &content)?;
|
|
1401
1283
|
if request.2.is_null() {
|
|
1402
1284
|
self.commit(
|
|
@@ -98,13 +98,16 @@ impl Catalog {
|
|
|
98
98
|
}
|
|
99
99
|
let sequence = u64::from_le_bytes(header[8..16].try_into().unwrap());
|
|
100
100
|
let n = u64::from_le_bytes(header[16..24].try_into().unwrap());
|
|
101
|
-
if
|
|
101
|
+
if sequence != self.sequence {
|
|
102
102
|
return fail(
|
|
103
103
|
"CorruptData",
|
|
104
104
|
"Storage catalog length/sequence outside bounds",
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
|
-
|
|
107
|
+
let frame_size = n.checked_add(96).ok_or_else(|| {
|
|
108
|
+
Error::new("CorruptData", "Storage catalog frame length overflow")
|
|
109
|
+
})?;
|
|
110
|
+
if remaining < frame_size {
|
|
108
111
|
break;
|
|
109
112
|
}
|
|
110
113
|
let mut raw = vec![0; n as usize];
|
|
@@ -173,12 +176,6 @@ impl Catalog {
|
|
|
173
176
|
fn append(&mut self, config: &Config, mut event: Value, fault: &mut Fault<'_>) -> Result<()> {
|
|
174
177
|
event["run_id"] = json!(config.run_id);
|
|
175
178
|
let raw = bytes(&event)?;
|
|
176
|
-
if raw.len() > 64 * 1024 * 1024 {
|
|
177
|
-
return fail(
|
|
178
|
-
"ResourceLimitExceeded",
|
|
179
|
-
"Storage catalog transaction too large",
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
179
|
let mut header = b"STRGC001".to_vec();
|
|
183
180
|
header.extend_from_slice(&self.sequence.to_le_bytes());
|
|
184
181
|
header.extend_from_slice(&(raw.len() as u64).to_le_bytes());
|
|
@@ -9,7 +9,6 @@ use std::{
|
|
|
9
9
|
};
|
|
10
10
|
const MAGIC: &[u8] = b"SLMTXN01";
|
|
11
11
|
const END: &[u8] = b"SLMTEND1";
|
|
12
|
-
const MAX_BLOCK: usize = 8 * 1024 * 1024;
|
|
13
12
|
pub struct Journal {
|
|
14
13
|
pub path: PathBuf,
|
|
15
14
|
file: Option<File>,
|
|
@@ -89,10 +88,13 @@ impl Journal {
|
|
|
89
88
|
}
|
|
90
89
|
let sequence = u64::from_le_bytes(header[8..16].try_into().unwrap());
|
|
91
90
|
let length = u64::from_le_bytes(header[16..24].try_into().unwrap());
|
|
92
|
-
if &header[..8] != MAGIC || sequence != self.sequence
|
|
91
|
+
if &header[..8] != MAGIC || sequence != self.sequence {
|
|
93
92
|
return fail("CorruptData", "Journal sequence, magic or length mismatch");
|
|
94
93
|
}
|
|
95
|
-
|
|
94
|
+
let frame_size = length
|
|
95
|
+
.checked_add(96)
|
|
96
|
+
.ok_or_else(|| Error::new("CorruptData", "Journal frame length overflow"))?;
|
|
97
|
+
if remaining < frame_size {
|
|
96
98
|
break;
|
|
97
99
|
}
|
|
98
100
|
let mut body = vec![0; (length + 40) as usize];
|
|
@@ -132,9 +134,6 @@ impl Journal {
|
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
let payload = bytes(events)?;
|
|
135
|
-
if payload.len() > MAX_BLOCK {
|
|
136
|
-
return fail("ResourceLimitExceeded", "Journal transaction too large");
|
|
137
|
-
}
|
|
138
137
|
let mut prefix = MAGIC.to_vec();
|
|
139
138
|
prefix.extend_from_slice(&self.sequence.to_le_bytes());
|
|
140
139
|
prefix.extend_from_slice(&(payload.len() as u64).to_le_bytes());
|