Skip to content

Commit bb6e46b

Browse files
affandarCopilot
andcommitted
Update duroxide-python for duroxide 0.1.25: KV API renames, new bulk APIs
- Bump duroxide dependency to 0.1.25 - Rename KV methods: set_value→set_kv_value, get_value→get_kv_value, etc. - Add bulk KV functions: get_kv_all_values, get_kv_all_keys, get_kv_length - Add prune_kv_values function Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent eb846bf commit bb6e46b

12 files changed

Lines changed: 385 additions & 197 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.1.15] - 2026-03-14
9+
10+
### Changed
11+
- Bumped `duroxide` dependency to 0.1.25.
12+
- Patched local builds to use the sibling `../../providers/duroxide-pg` checkout until a crates.io release includes the KV snapshot changes required by `duroxide` 0.1.25.
13+
- Renamed KV APIs to the new `kv_`-prefixed surface: `ctx.set_kv_value()`, `ctx.get_kv_value()`, `ctx.clear_kv_value()`, `ctx.clear_all_kv_values()`, `ctx.get_kv_value_from_instance()`, `client.get_kv_value()`, and `client.wait_for_kv_value()`.
14+
- Raised `MAX_KV_KEYS` from 10 to 100.
15+
16+
### Added
17+
- New orchestration KV helpers: `ctx.get_kv_all_values()`, `ctx.get_kv_all_keys()`, `ctx.get_kv_length()`, and `ctx.prune_kv_values_updated_before(cutoff_ms)`.
18+
- New typed client conveniences: `client.get_kv_value_typed()` and `client.wait_for_kv_value_typed()`.
19+
820
## [0.1.14] - 2026-03-13
921

1022
- **KV store support:** Durable key-value store for per-instance state

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
[package]
22
name = "duroxide-python"
3-
version = "0.1.14"
3+
version = "0.1.15"
44
edition = "2021"
55

66
[lib]
77
name = "duroxide_python"
88
crate-type = ["cdylib"]
99

1010
[dependencies]
11-
duroxide = { version = "0.1.24", features = ["sqlite"] }
11+
duroxide = { version = "0.1.25", features = ["sqlite"] }
1212
duroxide-pg = { version = "0.1.25" }
1313
pyo3 = { version = "0.23", features = ["extension-module"] }
1414
async-trait = "0.1"
@@ -19,5 +19,8 @@ dotenvy = "0.15"
1919
parking_lot = "0.12"
2020
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter", "json"] }
2121

22+
[patch.crates-io]
23+
duroxide-pg = { path = "../../providers/duroxide-pg" }
24+
2225
[profile.release]
2326
lto = true

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ Write durable workflows as Python generators. The Rust runtime handles replay, p
1616
- **Deterministic replay** — safe resume after crashes
1717
- **SQLite & PostgreSQL** — pluggable storage providers
1818
- **Custom Status**`ctx.set_custom_status()` / `ctx.reset_custom_status()` for orchestration progress reporting, `client.wait_for_status_change()` for efficient polling
19-
- **KV Store** — durable per-instance state via `ctx.set_value()` / `ctx.get_value()` / `ctx.clear_value()` / `ctx.clear_all_values()`, plus `client.get_value()` / `client.wait_for_value()`
19+
- **KV Store** — durable per-instance state via `ctx.set_kv_value()` / `ctx.get_kv_value()` / `ctx.get_kv_all_values()` / `ctx.get_kv_all_keys()` / `ctx.get_kv_length()` / `ctx.clear_kv_value()` / `ctx.clear_all_kv_values()` / `ctx.prune_kv_values_updated_before()`, plus `client.get_kv_value()` / `client.wait_for_kv_value()`
2020
- **Event Queues**`ctx.dequeue_event(queue_name)` for FIFO mailbox-style message passing, `client.enqueue_event()` to send messages
2121
- **Retry on Session**`ctx.schedule_activity_with_retry_on_session()` for retry with session affinity
22-
- **Tag Routing** — worker tags for activity affinity (`MAX_WORKER_TAGS=5`, `MAX_TAG_NAME_BYTES=256`, `MAX_KV_KEYS=10`, `MAX_KV_VALUE_BYTES=16384`)
22+
- **Tag Routing** — worker tags for activity affinity (`MAX_WORKER_TAGS=5`, `MAX_TAG_NAME_BYTES=256`, `MAX_KV_KEYS=100`, `MAX_KV_VALUE_BYTES=16384`)
2323
- **Admin APIs** — instance management, metrics, pruning
2424
- **Activity client access**`ctx.get_client()` lets activities start new orchestrations
2525
- **Runtime metrics**`metrics_snapshot()` for orchestration/activity counters
@@ -186,17 +186,20 @@ Durable per-instance key-value state for orchestration coordination and request/
186186
```python
187187
@runtime.register_orchestration("KvWorkflow")
188188
def kv_workflow(ctx, input):
189-
ctx.set_value("status", "running")
189+
ctx.set_kv_value("status", "running")
190190
result = yield ctx.schedule_activity("Compute", input)
191-
ctx.set_value("result", str(result))
192-
return result
191+
ctx.set_kv_value("result", str(result))
192+
snapshot = ctx.get_kv_all_values()
193+
keys = ctx.get_kv_all_keys()
194+
count = ctx.get_kv_length()
195+
return {"result": result, "snapshot": snapshot, "keys": keys, "count": count}
193196

194197
# External reads
195-
status = client.wait_for_value("instance-1", "status", 10000)
196-
result = client.get_value("instance-1", "result")
198+
status = client.wait_for_kv_value("instance-1", "status", 10000)
199+
result = client.get_kv_value("instance-1", "result")
197200
```
198201

199-
KV entries are scoped to a single orchestration instance and remain readable after completion until the instance is deleted or pruned.
202+
KV entries are scoped to a single orchestration instance and remain readable after completion until the instance is deleted or pruned. Use `ctx.prune_kv_values_updated_before(cutoff_ms)` to deterministically clear stale keys from prior turns when you only want to retain newer state.
200203

201204
## Event Queues
202205

docs/architecture.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -359,30 +359,30 @@ KV operations are fire-and-forget orchestration context calls backed by provider
359359
```
360360
Python Rust (PyO3) Provider (DB)
361361
────── ─────────── ─────────────
362-
ctx.set_value("status", "ready")
362+
ctx.set_kv_value("status", "ready")
363363
364-
└─► orchestration_set_value(instance_id, "status", "ready")
364+
└─► orchestration_set_kv_value(instance_id, "status", "ready")
365365
366366
└─► ORCHESTRATION_CTXS.get(instance_id)
367367
368-
└─► ctx.set_value("status", "ready")
368+
└─► ctx.set_kv_value("status", "ready")
369369
370370
└─► provider.upsert KV row for (instance_id, key)
371371
372-
client.get_value(id, "status")
372+
client.get_kv_value(id, "status")
373373
374-
└─► py.allow_threads(|| TOKIO_RT.block_on(client.get_value(id, "status")))
374+
└─► py.allow_threads(|| TOKIO_RT.block_on(client.get_kv_value(id, "status")))
375375
376-
└─► provider.get_value(id, "status")
376+
└─► provider.get_kv_value(id, "status")
377377
378-
client.wait_for_value(id, "status", timeout_ms)
378+
client.wait_for_kv_value(id, "status", timeout_ms)
379379
380-
└─► py.allow_threads(|| TOKIO_RT.block_on(client.wait_for_value(...)))
380+
└─► py.allow_threads(|| TOKIO_RT.block_on(client.wait_for_kv_value(...)))
381381
382-
└─► repeated provider.get_value(...) polling until key exists or timeout
382+
└─► repeated provider.get_kv_value(...) polling until key exists or timeout
383383
```
384384

385-
Cross-orchestration reads use the built-in `__duroxide_syscall:get_kv_value` activity. In Python this is exposed as `ctx.get_value_from_instance(instance_id, key)` and replays like any other scheduled activity.
385+
Cross-orchestration reads use the built-in `__duroxide_syscall:get_kv_value` activity. In Python this is exposed as `ctx.get_kv_value_from_instance(instance_id, key)` and replays like any other scheduled activity. The local in-memory snapshot also exposes `ctx.get_kv_all_values()`, `ctx.get_kv_all_keys()`, `ctx.get_kv_length()`, and `ctx.prune_kv_values_updated_before(cutoff_ms)` for bulk inspection and pruning without yielding.
386386

387387
## Event Queue Data Flow
388388

docs/user-guide.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -387,29 +387,35 @@ KV entries are durable metadata scoped to a single orchestration instance. They
387387
```python
388388
@runtime.register_orchestration("RequestServer")
389389
def request_server(ctx, input):
390-
ctx.set_value("status", "ready")
390+
ctx.set_kv_value("status", "ready")
391391
request = yield ctx.wait_for_event("request")
392392
response = request["command"][::-1]
393-
ctx.set_value(f"response:{request['op_id']}", response)
393+
ctx.set_kv_value(f"response:{request['op_id']}", response)
394394
return "done"
395395

396-
status = client.wait_for_value("server-1", "status", 10000)
397-
response = client.get_value("server-1", "response:op-1")
396+
status = client.wait_for_kv_value("server-1", "status", 10000)
397+
response = client.get_kv_value("server-1", "response:op-1")
398398
```
399399

400400
**OrchestrationContext methods:**
401-
- `ctx.set_value(key, value)` — set or overwrite a key
402-
- `ctx.get_value(key)` — read the current value for a key in the active instance
403-
- `ctx.clear_value(key)` — remove a single key
404-
- `ctx.clear_all_values()` — clear all keys for the active instance
405-
- `ctx.get_value_from_instance(instance_id, key)` — read another instance's KV via the built-in syscall activity
401+
- `ctx.set_kv_value(key, value)` — set or overwrite a key
402+
- `ctx.get_kv_value(key)` — read the current value for a key in the active instance
403+
- `ctx.get_kv_all_values()` — return a snapshot dict of all current KV entries
404+
- `ctx.get_kv_all_keys()` — return the list of active keys
405+
- `ctx.get_kv_length()` — return the number of active keys
406+
- `ctx.clear_kv_value(key)` — remove a single key
407+
- `ctx.clear_all_kv_values()` — clear all keys for the active instance
408+
- `ctx.prune_kv_values_updated_before(cutoff_ms)` — prune keys older than the provided persisted-update cutoff
409+
- `ctx.get_kv_value_from_instance(instance_id, key)` — read another instance's KV via the built-in syscall activity
406410

407411
**Client methods:**
408-
- `client.get_value(instance_id, key)` — read a key immediately
409-
- `client.wait_for_value(instance_id, key, timeout_ms)` — block until the key exists or timeout
412+
- `client.get_kv_value(instance_id, key)` — read a key immediately
413+
- `client.get_kv_value_typed(instance_id, key)` — read a key and JSON-decode it
414+
- `client.wait_for_kv_value(instance_id, key, timeout_ms)` — block until the key exists or timeout
415+
- `client.wait_for_kv_value_typed(instance_id, key, timeout_ms)` — wait for a key and JSON-decode it
410416

411417
**Limits:**
412-
- `MAX_KV_KEYS = 10`
418+
- `MAX_KV_KEYS = 100`
413419
- `MAX_KV_VALUE_BYTES = 16384`
414420

415421
## Event Queues — Persistent FIFO Message Passing

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "duroxide"
7-
version = "0.1.14"
7+
version = "0.1.15"
88
description = "Python SDK for the Duroxide durable execution runtime"
99
readme = "README.md"
1010
license = { text = "MIT" }

python/duroxide/__init__.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,21 +183,32 @@ def wait_for_status_change(
183183
)
184184
return _parse_status(result)
185185

186-
def get_value(self, instance_id: str, key: str) -> "Optional[str]":
186+
def get_kv_value(self, instance_id: str, key: str) -> "Optional[str]":
187187
"""Read a single KV entry for the given instance."""
188-
return self._native.get_value(instance_id, key)
188+
return self._native.get_kv_value(instance_id, key)
189189

190-
def wait_for_value(self, instance_id: str, key: str, timeout_ms: int = 30000) -> str:
190+
def get_kv_value_typed(self, instance_id: str, key: str):
191+
"""Read a single KV entry for the given instance and JSON-decode it."""
192+
raw = self.get_kv_value(instance_id, key)
193+
if raw is None:
194+
return None
195+
return json.loads(raw)
196+
197+
def wait_for_kv_value(self, instance_id: str, key: str, timeout_ms: int = 30000) -> str:
191198
"""Wait for a KV value to be set on an instance."""
192199
try:
193-
return self._native.wait_for_value(instance_id, key, timeout_ms)
200+
return self._native.wait_for_kv_value(instance_id, key, timeout_ms)
194201
except RuntimeError as exc:
195202
if "timed out" in str(exc).lower():
196203
raise TimeoutError(
197204
f"Timed out waiting for value '{key}' on instance '{instance_id}'"
198205
) from exc
199206
raise
200207

208+
def wait_for_kv_value_typed(self, instance_id: str, key: str, timeout_ms: int = 30000):
209+
"""Wait for a KV value to be set on an instance and JSON-decode it."""
210+
return json.loads(self.wait_for_kv_value(instance_id, key, timeout_ms))
211+
201212
def cancel_instance(self, instance_id: str, reason: str = None):
202213
self._native.cancel_instance(instance_id, reason)
203214

@@ -439,7 +450,7 @@ def metrics_snapshot(self):
439450
# Tag limits
440451
MAX_WORKER_TAGS = 5
441452
MAX_TAG_NAME_BYTES = 256
442-
MAX_KV_KEYS = 10
453+
MAX_KV_KEYS = 100
443454
MAX_KV_VALUE_BYTES = 16384
444455

445456

python/duroxide/context.py

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
orchestration_set_custom_status,
1717
orchestration_reset_custom_status,
1818
orchestration_get_custom_status,
19-
orchestration_set_value,
20-
orchestration_get_value,
21-
orchestration_clear_value,
22-
orchestration_clear_all_values,
19+
orchestration_set_kv_value,
20+
orchestration_get_kv_value,
21+
orchestration_get_kv_all_values,
22+
orchestration_get_kv_all_keys,
23+
orchestration_get_kv_length,
24+
orchestration_clear_kv_value,
25+
orchestration_clear_all_kv_values,
26+
orchestration_prune_kv_values,
2327
activity_trace_log,
2428
activity_is_cancelled,
2529
activity_tag,
@@ -459,23 +463,39 @@ def get_custom_status(self) -> Optional[str]:
459463
"""
460464
return orchestration_get_custom_status(self.instance_id)
461465

462-
def set_value(self, key: str, value: str):
466+
def set_kv_value(self, key: str, value: str):
463467
"""Set a key-value pair scoped to this orchestration instance."""
464-
orchestration_set_value(self.instance_id, str(key), str(value))
468+
orchestration_set_kv_value(self.instance_id, str(key), str(value))
465469

466-
def get_value(self, key: str) -> Optional[str]:
470+
def get_kv_value(self, key: str) -> Optional[str]:
467471
"""Get the current value for a key. Returns None if not set."""
468-
return orchestration_get_value(self.instance_id, str(key))
472+
return orchestration_get_kv_value(self.instance_id, str(key))
469473

470-
def clear_value(self, key: str):
474+
def get_kv_all_values(self) -> dict[str, str]:
475+
"""Return a snapshot of all key-value pairs for this orchestration instance."""
476+
return orchestration_get_kv_all_values(self.instance_id)
477+
478+
def get_kv_all_keys(self) -> list[str]:
479+
"""Return the list of KV keys currently set on this orchestration instance."""
480+
return orchestration_get_kv_all_keys(self.instance_id)
481+
482+
def get_kv_length(self) -> int:
483+
"""Return the number of KV entries currently set on this orchestration instance."""
484+
return orchestration_get_kv_length(self.instance_id)
485+
486+
def clear_kv_value(self, key: str):
471487
"""Remove a single key from the KV store."""
472-
orchestration_clear_value(self.instance_id, str(key))
488+
orchestration_clear_kv_value(self.instance_id, str(key))
473489

474-
def clear_all_values(self):
490+
def clear_all_kv_values(self):
475491
"""Clear ALL key-value pairs for this orchestration instance."""
476-
orchestration_clear_all_values(self.instance_id)
492+
orchestration_clear_all_kv_values(self.instance_id)
493+
494+
def prune_kv_values_updated_before(self, cutoff_ms: int) -> int:
495+
"""Prune KV entries whose last persisted update is older than cutoff_ms."""
496+
return orchestration_prune_kv_values(self.instance_id, cutoff_ms)
477497

478-
def get_value_from_instance(self, instance_id: str, key: str) -> ScheduledTask:
498+
def get_kv_value_from_instance(self, instance_id: str, key: str) -> ScheduledTask:
479499
"""Read a KV value from another orchestration instance via the built-in syscall activity."""
480500
return ScheduledTask({
481501
"type": "activity",

0 commit comments

Comments
 (0)