straw-queue 0.1.0__tar.gz → 0.1.1__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.0 → straw_queue-0.1.1}/CHANGELOG.md +22 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/CHANGELOG_zh.md +9 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/Cargo.lock +1 -1
- {straw_queue-0.1.0 → straw_queue-0.1.1}/Cargo.toml +1 -1
- {straw_queue-0.1.0 → straw_queue-0.1.1}/PKG-INFO +1 -1
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/BUILDING.md +11 -7
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/BUILDING_zh.md +5 -5
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/USAGE.md +20 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/USAGE_zh.md +9 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/pyproject.toml +1 -1
- {straw_queue-0.1.0 → straw_queue-0.1.1}/rust/coordinator.rs +81 -19
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/coordinator.py +14 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/protocol.py +2 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_filesystem_queue.py +307 -56
- {straw_queue-0.1.0 → straw_queue-0.1.1}/.github/workflows/tests.yml +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/.github/workflows/wheels.yml +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/CONTRIBUTING.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/CONTRIBUTING_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/LICENSE +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/README.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/README_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/APPLICATIONS.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/APPLICATIONS_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/BENCHMARKS.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/BENCHMARKS_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/DESIGN.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/DESIGN_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/FILESYSTEM.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/FILESYSTEM_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/FORMAT.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/FORMAT_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/PROTOCOL_AND_VERIFICATION.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/PROTOCOL_AND_VERIFICATION_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/README.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/README_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/VERIFICATION.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/docs/VERIFICATION_zh.md +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/examples/multiprocess.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/examples/records.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/examples/tensors.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/examples/work_queue.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/rust/gc.rs +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/rust/journal.rs +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/rust/lib.rs +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/rust/python.rs +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/rust/store.rs +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/__init__.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/backend.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/benchmark.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/errors.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/inspect.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/journal.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/reporting.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/rpc.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/store.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/tensor.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/src/straw/tracing.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/native.rs +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_benchmark_profile.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_consumer_rewind.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_gc_extent_validation.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_online_gc.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_packed_store.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_read_session.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tests/test_tensor.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/tools/check_wheel.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/verification/crash_campaign.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/verification/gc_cross_client.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/verification/gc_scale.py +0 -0
- {straw_queue-0.1.0 → straw_queue-0.1.1}/verification/ownership_model.py +0 -0
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
[中文版](CHANGELOG_zh.md)
|
|
4
4
|
|
|
5
|
+
## 0.1.1
|
|
6
|
+
|
|
7
|
+
- Persistent task scheduling with `TaskSpec.priority` (higher first),
|
|
8
|
+
`scheduling_key` (lower first), then FIFO. Both keys default to zero and accept
|
|
9
|
+
signed 64-bit integers. The Rust pending index selects tasks without reading
|
|
10
|
+
their payloads and reconstructs scheduling order during WAL recovery.
|
|
11
|
+
- `Coordinator.yield_tasks()` atomically saves a batch of continuation inputs
|
|
12
|
+
and optional scheduling fields, ends their leases, and returns the tasks to
|
|
13
|
+
the pending queue without spending failure retries. Returned tasks join the
|
|
14
|
+
tail of their scheduling key's FIFO order. Identical requests are idempotent;
|
|
15
|
+
a stale lease rejects the entire batch.
|
|
16
|
+
- `Coordinator.pending_tasks()` exposes pending task specifications in
|
|
17
|
+
acquisition order, with optional task-prefix and minimum-priority filters.
|
|
18
|
+
Applications coordinate paused snapshots and retain the referenced inputs.
|
|
19
|
+
- Regression coverage for priority/FIFO recovery, stale leases, invalid
|
|
20
|
+
scheduling keys, and all-or-nothing batch recovery across WAL crash windows.
|
|
21
|
+
|
|
22
|
+
Existing callers may omit the new scheduling fields. Packed-record and WAL
|
|
23
|
+
framing are unchanged; the new APIs and scheduling behavior require 0.1.1.
|
|
24
|
+
Pending-task snapshots alone do not provide a joint application checkpoint or
|
|
25
|
+
arbitrary checkpoint rollback.
|
|
26
|
+
|
|
5
27
|
## 0.1.0
|
|
6
28
|
|
|
7
29
|
Initial release of straw, a filesystem-based durable queue and shared tensor
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
[English](CHANGELOG.md)
|
|
4
4
|
|
|
5
|
+
## 0.1.1
|
|
6
|
+
|
|
7
|
+
- 持久化任务调度:按 `TaskSpec.priority` 从高到低、`scheduling_key` 从低到高、再按 FIFO 顺序领取任务。两个字段默认均为 0,接受有符号 64 位整数。Rust pending 索引无需读取载荷即可选择任务,WAL 恢复时重建调度顺序。
|
|
8
|
+
- 新增 `Coordinator.yield_tasks()`:原子保存一批 continuation 输入及可选调度字段,结束这些任务的 lease 并归还到 pending 队列,不消耗失败重试次数。归还的任务进入相同调度键的 FIFO 队尾;相同请求可幂等重试,任一 lease 失效则拒绝整批操作。
|
|
9
|
+
- 新增 `Coordinator.pending_tasks()`:按领取顺序返回 pending 任务的 spec,支持按任务 ID 前缀及最低优先级过滤。应用负责协调暂停后的快照,并持有所引用的输入。
|
|
10
|
+
- 补充优先级/FIFO 恢复、失效 lease、非法调度键,以及 WAL 崩溃窗口下批量操作全有或全无恢复的回归测试。
|
|
11
|
+
|
|
12
|
+
已有调用方可以不指定新增调度字段。Packed record 与 WAL 的 framing 不变;新增 API 和调度行为需要 0.1.1。仅有 pending 任务快照尚不构成应用联合 checkpoint,也不提供任意 checkpoint 回退。
|
|
13
|
+
|
|
5
14
|
## 0.1.0
|
|
6
15
|
|
|
7
16
|
straw 首次发布:面向 AI 应用、基于文件系统的持久化队列与共享张量存储,以 `straw-queue` 包分发,采用 MIT 许可证。
|
|
@@ -137,17 +137,20 @@ job uses GitHub OIDC with `id-token: write`; no PyPI API token or GitHub secret
|
|
|
137
137
|
is needed. See [PyPI's setup guide](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)
|
|
138
138
|
and [publishing guide](https://docs.pypi.org/trusted-publishers/using-a-publisher/).
|
|
139
139
|
|
|
140
|
-
After the release
|
|
141
|
-
the
|
|
140
|
+
After committing the release changes, ensure that `pyproject.toml`, `Cargo.toml`
|
|
141
|
+
and the `straw` entry in `Cargo.lock` share the release version. For example, for
|
|
142
|
+
version `0.1.1`, create and push a new tag on that commit:
|
|
142
143
|
|
|
143
144
|
```sh
|
|
144
|
-
git tag v0.1.
|
|
145
|
-
git push origin v0.1.
|
|
145
|
+
git tag v0.1.1
|
|
146
|
+
git push origin v0.1.1
|
|
146
147
|
```
|
|
147
148
|
|
|
148
149
|
The first successful upload creates the PyPI project; registering a pending
|
|
149
150
|
publisher alone does not reserve the name. Later releases use a new matching
|
|
150
|
-
version and tag.
|
|
151
|
+
version and tag. An existing tag does not include later working-tree changes;
|
|
152
|
+
check its target before publishing and do not overwrite a published release.
|
|
153
|
+
If a build or test fails, fix it before publishing; failed jobs
|
|
151
154
|
prevent the publishing job from running.
|
|
152
155
|
|
|
153
156
|
Hosted CI uses local Linux filesystems and CPU PyTorch. Multi-client JuiceFS
|
|
@@ -155,8 +158,9 @@ qualification, storage-service faults and GPU training remain separate
|
|
|
155
158
|
[deployment/application checks](VERIFICATION.md). Inspect hosted results
|
|
156
159
|
separately; a local build is not proof that the matrix ran.
|
|
157
160
|
|
|
158
|
-
Before distributing a release, align the versions in `Cargo.toml
|
|
159
|
-
`pyproject.toml
|
|
161
|
+
Before distributing a release, align the versions in `Cargo.toml`,
|
|
162
|
+
`pyproject.toml` and the `straw` entry in `Cargo.lock`, run the
|
|
163
|
+
[verification checks](VERIFICATION.md), inspect wheel
|
|
160
164
|
and sdist contents, run `check_wheel.py`, and preserve the wheel SHA-256 plus
|
|
161
165
|
the exact source revision. The changelog should state protocol/API compatibility
|
|
162
166
|
and unresolved limits. Upload the already tested artifact; rebuilding afterward
|
|
@@ -90,15 +90,15 @@ 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
|
-
|
|
93
|
+
提交发布改动后,确认 `pyproject.toml`、`Cargo.toml` 和 `Cargo.lock` 中 `straw` 条目的版本一致。例如发布 `0.1.1` 时,在该 commit 上创建并推送新的 tag:
|
|
94
94
|
|
|
95
95
|
```sh
|
|
96
|
-
git tag v0.1.
|
|
97
|
-
git push origin v0.1.
|
|
96
|
+
git tag v0.1.1
|
|
97
|
+
git push origin v0.1.1
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
首次上传成功时才创建 PyPI 项目;仅登记 pending publisher 不会保留包名。后续版本使用新的匹配版本号和 tag
|
|
100
|
+
首次上传成功时才创建 PyPI 项目;仅登记 pending publisher 不会保留包名。后续版本使用新的匹配版本号和 tag。已有 tag 不会包含之后的工作区改动;发布前应检查其指向,不要覆盖已发布版本。构建或测试失败时,应先修复;失败的 job 会阻止发布 job 运行。
|
|
101
101
|
|
|
102
102
|
托管 CI 使用本地 Linux 文件系统和 CPU PyTorch。多客户端 JuiceFS 验证、存储服务故障及 GPU 训练属于独立的[部署与应用检查](VERIFICATION_zh.md)。托管结果需要单独检查;本地构建通过不代表矩阵已经执行。
|
|
103
103
|
|
|
104
|
-
分发版本前,确认 `Cargo.toml`
|
|
104
|
+
分发版本前,确认 `Cargo.toml`、`pyproject.toml` 和 `Cargo.lock` 中 `straw` 条目的版本一致,运行[验证检查](VERIFICATION_zh.md),检查 wheel/sdist 内容,运行 `check_wheel.py`,保存 wheel SHA-256 与精确源码版本。更新日志应说明协议/API 兼容性及已知限制。上传已经测试过的那个产物;之后重新构建会产生新产物,需要重新检查。
|
|
@@ -279,3 +279,23 @@ The Rust reader retains only the last authenticated index, keyed by the complete
|
|
|
279
279
|
Publication dependency validation, continuation batches, consumer snapshots and GC traversal use the same bounded native reader internally. Formats, WAL commit ordering, capacity limits and owner-release rules are unchanged.
|
|
280
280
|
|
|
281
281
|
`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
|
+
|
|
283
|
+
|
|
284
|
+
### Persistent task scheduling
|
|
285
|
+
|
|
286
|
+
`TaskSpec.priority` (default 0) is ordered descending, then `scheduling_key`
|
|
287
|
+
(default 0) ascending, then FIFO. Both keys are signed 64-bit integers. Keys and
|
|
288
|
+
submission/return order survive WAL recovery; the Rust coordinator maintains an
|
|
289
|
+
ordered pending index, without loading payloads to select the next task.
|
|
290
|
+
|
|
291
|
+
Publish continuation inputs before calling `yield_tasks(updates, request_id=...)`.
|
|
292
|
+
Each update contains `lease` and `input_ref`, and may replace `priority`,
|
|
293
|
+
`scheduling_key` and `metadata`. The whole batch validates before one WAL commit;
|
|
294
|
+
all leases end together and the tasks become pending at the tail of their key's
|
|
295
|
+
FIFO order. A stale lease rejects the whole batch. Retrying the identical request
|
|
296
|
+
is idempotent. Publish again only if you intend to create a different input.
|
|
297
|
+
This is distinct from `save_task_progress_many`, which keeps leases active.
|
|
298
|
+
`pending_tasks()` returns ordered task specifications for a paused snapshot;
|
|
299
|
+
applications must coordinate the pause and retain the referenced inputs.
|
|
300
|
+
|
|
301
|
+
These APIs require `straw-queue>=0.1.1`.
|
|
@@ -165,3 +165,12 @@ Rust reader 只保留最后一个已认证索引,以完整 extent 描述符区
|
|
|
165
165
|
发布依赖校验、批量续跑进度、消费者快照及 GC 遍历在 Rust 内部也复用这种有界 reader。文件格式、WAL 提交顺序、容量限制和所有权释放规则均未改变。
|
|
166
166
|
|
|
167
167
|
`store.read(publication)` 同样在迭代器生命周期内持有读取会话。释放数据 owner 前应读完或关闭迭代器。不同迭代器会重新认证索引。
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
### 持久化任务排序
|
|
171
|
+
|
|
172
|
+
`TaskSpec.priority`(默认 0)降序,其次 `scheduling_key`(默认 0)升序,再按 FIFO;两个字段均为有符号 64 位整数。排序字段及提交/归还顺序会从 WAL 恢复。Rust coordinator 维护有序 pending 索引,选取任务无需读取 payload。
|
|
173
|
+
|
|
174
|
+
先发布续跑输入,再调用 `yield_tasks(updates, request_id=...)`。每项包含 `lease`、`input_ref`,可同时替换 `priority`、`scheduling_key` 和 `metadata`。整批校验后只提交一次 WAL;所有 lease 同时结束,任务进入其排序键的 FIFO 队尾。任何 stale lease 都会拒绝整批。相同请求重试幂等;只有确实需要另一个输入版本时才重新发布。与此不同,`save_task_progress_many` 保留 lease。
|
|
175
|
+
|
|
176
|
+
`pending_tasks()` 返回按领取顺序排列的任务 spec,可用于暂停后的快照;暂停边界和输入引用持有由应用协调。这些 API 需要 `straw-queue>=0.1.1`。
|
|
@@ -4,6 +4,7 @@ use crate::store::{Config, Reader, Record, Store, mkdir, sync, sync_dir};
|
|
|
4
4
|
use crate::{Error, Fault, Result, array, bytes, digest, fail, field, number, uid};
|
|
5
5
|
use serde_json::{Value, json};
|
|
6
6
|
use std::{
|
|
7
|
+
cmp::Reverse,
|
|
7
8
|
collections::{BTreeMap, HashMap, HashSet},
|
|
8
9
|
fs::OpenOptions,
|
|
9
10
|
io::Write,
|
|
@@ -17,9 +18,9 @@ pub struct Coordinator {
|
|
|
17
18
|
pub limits: Value,
|
|
18
19
|
pub epoch: String,
|
|
19
20
|
pub recovery_seconds: f64,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
pending: BTreeMap<(Reverse<i64>, i64, usize), String>,
|
|
22
|
+
pending_keys: HashMap<String, (Reverse<i64>, i64, usize)>,
|
|
23
|
+
next_pending_order: usize,
|
|
23
24
|
leased: HashSet<String>,
|
|
24
25
|
usage_cache: Option<Value>,
|
|
25
26
|
deadlines: HashMap<String, f64>,
|
|
@@ -62,6 +63,17 @@ fn bounded(value: &Value, limit: u64) -> Result<()> {
|
|
|
62
63
|
}
|
|
63
64
|
Ok(())
|
|
64
65
|
}
|
|
66
|
+
fn validate_scheduling(value: &Value) -> Result<()> {
|
|
67
|
+
for key in ["priority", "scheduling_key"] {
|
|
68
|
+
if value.get(key).is_some_and(|v| v.as_i64().is_none()) {
|
|
69
|
+
return fail(
|
|
70
|
+
"ValueError",
|
|
71
|
+
format!("{key} must be a signed 64-bit integer"),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
Ok(())
|
|
76
|
+
}
|
|
65
77
|
impl Coordinator {
|
|
66
78
|
pub fn open(config: Config, options: &Value, fault: &mut Fault<'_>) -> Result<Self> {
|
|
67
79
|
let started = Instant::now();
|
|
@@ -119,9 +131,9 @@ impl Coordinator {
|
|
|
119
131
|
limits,
|
|
120
132
|
epoch: String::new(),
|
|
121
133
|
recovery_seconds: 0.,
|
|
122
|
-
order: vec![],
|
|
123
|
-
positions: HashMap::new(),
|
|
124
134
|
pending: BTreeMap::new(),
|
|
135
|
+
pending_keys: HashMap::new(),
|
|
136
|
+
next_pending_order: 0,
|
|
125
137
|
leased: HashSet::new(),
|
|
126
138
|
usage_cache: None,
|
|
127
139
|
deadlines: HashMap::new(),
|
|
@@ -243,8 +255,8 @@ impl Coordinator {
|
|
|
243
255
|
_ => vec![],
|
|
244
256
|
};
|
|
245
257
|
for id in &affected {
|
|
246
|
-
if let Some(
|
|
247
|
-
self.pending.remove(
|
|
258
|
+
if let Some(key) = self.pending_keys.remove(id) {
|
|
259
|
+
self.pending.remove(&key);
|
|
248
260
|
}
|
|
249
261
|
self.leased.remove(id);
|
|
250
262
|
if let Some(usage) = self.usage_cache.as_mut() {
|
|
@@ -261,9 +273,9 @@ impl Coordinator {
|
|
|
261
273
|
"Submitted" => {
|
|
262
274
|
for spec in array(e, "tasks")? {
|
|
263
275
|
let id = field(spec, "task_id")?;
|
|
264
|
-
self.positions.insert(id.into(), self.order.len());
|
|
265
|
-
self.order.push(id.into());
|
|
266
276
|
self.state["tasks"][id] = json!({"spec":spec,"state":"pending","generation":0,"failures":0,"lease":null});
|
|
277
|
+
self.state["tasks"][id]["pending_order"] = json!(self.next_pending_order);
|
|
278
|
+
self.next_pending_order += 1;
|
|
267
279
|
}
|
|
268
280
|
if let Some(id) = e["producer_id"].as_str() {
|
|
269
281
|
self.state["producers"][id] = e["producer_state"].clone();
|
|
@@ -336,9 +348,16 @@ impl Coordinator {
|
|
|
336
348
|
}
|
|
337
349
|
let t = &mut self.state["tasks"][id];
|
|
338
350
|
t["spec"]["input_ref"] = e["input_ref"].clone();
|
|
351
|
+
for key in ["priority", "scheduling_key", "metadata"] {
|
|
352
|
+
if let Some(value) = e.get(key) {
|
|
353
|
+
t["spec"][key] = value.clone();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
339
356
|
if e["type"] == "Yielded" {
|
|
340
357
|
t["state"] = json!("pending");
|
|
341
358
|
t["lease"] = Value::Null;
|
|
359
|
+
t["pending_order"] = json!(self.next_pending_order);
|
|
360
|
+
self.next_pending_order += 1;
|
|
342
361
|
self.deadlines.remove(id);
|
|
343
362
|
}
|
|
344
363
|
}
|
|
@@ -411,7 +430,13 @@ impl Coordinator {
|
|
|
411
430
|
for id in &affected {
|
|
412
431
|
let task = &self.state["tasks"][id];
|
|
413
432
|
if task["state"] == "pending" {
|
|
414
|
-
|
|
433
|
+
let key = (
|
|
434
|
+
Reverse(task["spec"]["priority"].as_i64().unwrap_or(0)),
|
|
435
|
+
task["spec"]["scheduling_key"].as_i64().unwrap_or(0),
|
|
436
|
+
number(task, "pending_order") as usize,
|
|
437
|
+
);
|
|
438
|
+
self.pending.insert(key, id.clone());
|
|
439
|
+
self.pending_keys.insert(id.clone(), key);
|
|
415
440
|
}
|
|
416
441
|
if task["state"] == "leased" {
|
|
417
442
|
self.leased.insert(id.clone());
|
|
@@ -745,6 +770,7 @@ impl Coordinator {
|
|
|
745
770
|
let mut ids = vec![];
|
|
746
771
|
let mut unique = HashSet::new();
|
|
747
772
|
for t in tasks {
|
|
773
|
+
validate_scheduling(t)?;
|
|
748
774
|
let id = field(t, "task_id")?;
|
|
749
775
|
if !unique.insert(id) || !self.state["tasks"][id].is_null() {
|
|
750
776
|
return fail("IdempotencyConflict", "Task IDs must be new within queue");
|
|
@@ -821,7 +847,7 @@ impl Coordinator {
|
|
|
821
847
|
let mut selected = ids
|
|
822
848
|
.iter()
|
|
823
849
|
.filter_map(|v| v.as_str())
|
|
824
|
-
.filter_map(|id| self.
|
|
850
|
+
.filter_map(|id| self.pending_keys.get(id))
|
|
825
851
|
.filter_map(|p| self.pending.get_key_value(p))
|
|
826
852
|
.collect::<Vec<_>>();
|
|
827
853
|
selected.sort_by_key(|(position, _)| *position);
|
|
@@ -903,11 +929,19 @@ impl Coordinator {
|
|
|
903
929
|
}
|
|
904
930
|
Ok(json!({"status":"acquired","assignments":assignments}))
|
|
905
931
|
}
|
|
906
|
-
"save_task_progress_many" => {
|
|
932
|
+
"save_task_progress_many" | "yield_tasks" => {
|
|
933
|
+
let yielding = method == "yield_tasks";
|
|
907
934
|
let updates = array(args, "updates")?;
|
|
908
935
|
bounded(&json!(updates), metadata)?;
|
|
909
|
-
let request =
|
|
910
|
-
|
|
936
|
+
let request = self.request(
|
|
937
|
+
if yielding {
|
|
938
|
+
"yield_many"
|
|
939
|
+
} else {
|
|
940
|
+
"progress_many"
|
|
941
|
+
},
|
|
942
|
+
field(args, "request_id")?,
|
|
943
|
+
&json!(updates),
|
|
944
|
+
)?;
|
|
911
945
|
if !request.2.is_null() {
|
|
912
946
|
return Ok(request.2);
|
|
913
947
|
}
|
|
@@ -918,6 +952,7 @@ impl Coordinator {
|
|
|
918
952
|
let mut unique = HashSet::new();
|
|
919
953
|
let mut reader = Reader::new(self.store.config.clone());
|
|
920
954
|
for update in updates {
|
|
955
|
+
validate_scheduling(update)?;
|
|
921
956
|
let lease = &update["lease"];
|
|
922
957
|
let id = field(lease, "task_id")?;
|
|
923
958
|
if !unique.insert(id) {
|
|
@@ -932,16 +967,23 @@ impl Coordinator {
|
|
|
932
967
|
self.storage_failed = true;
|
|
933
968
|
}
|
|
934
969
|
validated?;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
970
|
+
let mut event = json!({"type":if yielding {"Yielded"} else {"TaskProgress"},"task_id":id,"input_ref":update["input_ref"]});
|
|
971
|
+
if yielding {
|
|
972
|
+
for key in ["priority", "scheduling_key", "metadata"] {
|
|
973
|
+
if let Some(value) = update.get(key) {
|
|
974
|
+
event[key] = value.clone();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
events.push(event);
|
|
938
979
|
}
|
|
939
980
|
let last = events.last_mut().unwrap();
|
|
940
981
|
last["request_key"] = json!(request.0);
|
|
941
982
|
last["request_digest"] = json!(request.1);
|
|
942
|
-
|
|
983
|
+
let response = json!(if yielding { "pending" } else { "saved" });
|
|
984
|
+
last["response"] = response.clone();
|
|
943
985
|
self.commit(json!(events), fault)?;
|
|
944
|
-
Ok(
|
|
986
|
+
Ok(response)
|
|
945
987
|
}
|
|
946
988
|
"release_tasks" => {
|
|
947
989
|
let leases = array(args, "leases")?;
|
|
@@ -1139,6 +1181,26 @@ impl Coordinator {
|
|
|
1139
1181
|
"lookup_submission" => {
|
|
1140
1182
|
Ok(self.state["submissions"][field(args, "submission_id")?]["receipt"].clone())
|
|
1141
1183
|
}
|
|
1184
|
+
"pending_tasks" => {
|
|
1185
|
+
let prefix = args["task_prefix"].as_str();
|
|
1186
|
+
let minimum = args["min_priority"].as_i64();
|
|
1187
|
+
Ok(json!(
|
|
1188
|
+
self.pending
|
|
1189
|
+
.values()
|
|
1190
|
+
.filter_map(|id| {
|
|
1191
|
+
let spec = &self.state["tasks"][id]["spec"];
|
|
1192
|
+
if prefix.is_some_and(|p| !id.starts_with(p))
|
|
1193
|
+
|| minimum
|
|
1194
|
+
.is_some_and(|p| spec["priority"].as_i64().unwrap_or(0) < p)
|
|
1195
|
+
{
|
|
1196
|
+
None
|
|
1197
|
+
} else {
|
|
1198
|
+
Some(spec.clone())
|
|
1199
|
+
}
|
|
1200
|
+
})
|
|
1201
|
+
.collect::<Vec<_>>()
|
|
1202
|
+
))
|
|
1203
|
+
}
|
|
1142
1204
|
"task_status" => Ok(self.state["tasks"][field(args, "task_id")?].clone()),
|
|
1143
1205
|
"producer_state" => Ok(self.state["producers"][field(args, "producer_id")?].clone()),
|
|
1144
1206
|
"read_commits" => {
|
|
@@ -112,6 +112,20 @@ class Coordinator:
|
|
|
112
112
|
"""Persist a bounded batch of leased continuation references atomically."""
|
|
113
113
|
return self._call("save_task_progress_many", locals())
|
|
114
114
|
|
|
115
|
+
def yield_tasks(self, updates, *, request_id):
|
|
116
|
+
"""Atomically persist inputs/scheduling fields and return leased tasks to pending.
|
|
117
|
+
|
|
118
|
+
Each update contains lease and input_ref, and may replace priority,
|
|
119
|
+
scheduling_key and metadata. Higher priority is acquired first; within
|
|
120
|
+
a priority, lower scheduling_key precedes FIFO return/submission order.
|
|
121
|
+
Callers must finish using the inputs before yielding their read lifetime.
|
|
122
|
+
"""
|
|
123
|
+
return self._call("yield_tasks", locals())
|
|
124
|
+
|
|
125
|
+
def pending_tasks(self, *, task_prefix=None, min_priority=None):
|
|
126
|
+
"""Snapshot pending task specifications in acquisition order."""
|
|
127
|
+
return [TaskSpec.from_dict(v) for v in self._call("pending_tasks", locals())]
|
|
128
|
+
|
|
115
129
|
def release_tasks(self, leases, *, request_id):
|
|
116
130
|
"""Release existing inputs atomically without spending failure retries."""
|
|
117
131
|
return self._call("release_tasks", locals())
|