feat(kv): set_if compare-and-swap for KV (per-app + shared + SDK) (Track A M5)
No atomic CAS existed, so concurrent writers to the same KV key raced. Add set_if(key, expected, new): writes only when the current value equals expected, or (expected absent) only when the key is missing — the primitive for lock-free counters / optimistic concurrency. - KvService + GroupKvService traits gain set_if with a defaulted "unsupported" impl (Noop + other impls untouched); the real services override it. - kv_repo/group_kv_repo: atomic set_if — a single UPDATE ... WHERE value = $expected (JSONB semantic equality) or INSERT ... ON CONFLICT DO NOTHING. Atomic without a transaction; returns whether the write happened. - Services enforce the same value-size cap + (group) writes-authed + row/byte quotas as set; the event fires only on a successful swap. - Executor Rhai SDK: kv::collection(c).set_if(key, expected, new) and the shared_collection variant; Rhai () for expected means "only if absent". Pinned by kv_service/group_kv_service set_if unit tests (CAS + fail-closed) + sdk_kv::kv_set_if_compare_and_swap (through a real script). No migration. docs/sdk-shape.md documents the primitive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
//! widgets.set("k", #{ n: 1 });
|
||||
//! let v = widgets.get("k"); // value or () if absent
|
||||
//! if widgets.has("k") { ... }
|
||||
//! widgets.set_if("k", (), #{ n: 0 }); // insert only if ABSENT -> bool
|
||||
//! widgets.set_if("k", old, new); // swap only if current == old -> bool
|
||||
//! widgets.delete("k"); // bool (was-present)
|
||||
//! let page = widgets.list(); // returns #{ keys: [...], next_cursor: () }
|
||||
//! ```
|
||||
@@ -105,6 +107,7 @@ pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<Sdk
|
||||
|
||||
register_get(engine);
|
||||
register_set(engine);
|
||||
register_set_if(engine);
|
||||
register_has(engine);
|
||||
register_delete(engine);
|
||||
register_list(engine);
|
||||
@@ -114,11 +117,23 @@ pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<Sdk
|
||||
|
||||
register_group_get(engine);
|
||||
register_group_set(engine);
|
||||
register_group_set_if(engine);
|
||||
register_group_has(engine);
|
||||
register_group_delete(engine);
|
||||
register_group_list(engine);
|
||||
}
|
||||
|
||||
/// Map a Rhai `expected` argument to the CAS precondition: Rhai unit `()` means
|
||||
/// "expected ABSENT" (insert-if-absent); any other value is the expected current
|
||||
/// value.
|
||||
fn expected_from_dynamic(expected: &Dynamic) -> Option<serde_json::Value> {
|
||||
if expected.is_unit() {
|
||||
None
|
||||
} else {
|
||||
Some(dynamic_to_json(expected))
|
||||
}
|
||||
}
|
||||
|
||||
fn register_get(engine: &mut RhaiEngine) {
|
||||
engine.register_fn(
|
||||
"get",
|
||||
@@ -145,6 +160,27 @@ fn register_set(engine: &mut RhaiEngine) {
|
||||
);
|
||||
}
|
||||
|
||||
fn register_set_if(engine: &mut RhaiEngine) {
|
||||
// `handle.set_if(key, expected, new)` — CAS. `expected = ()` means "only if
|
||||
// absent". Returns `true` if the swap happened, `false` if the precondition
|
||||
// failed.
|
||||
engine.register_fn(
|
||||
"set_if",
|
||||
|handle: &mut KvHandle,
|
||||
key: &str,
|
||||
expected: Dynamic,
|
||||
new: Dynamic|
|
||||
-> Result<bool, Box<EvalAltResult>> {
|
||||
let h = handle.clone();
|
||||
let exp = expected_from_dynamic(&expected);
|
||||
let new = dynamic_to_json(&new);
|
||||
block_on("kv", async move {
|
||||
h.service.set_if(&h.cx, &h.collection, key, exp, new).await
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn register_has(engine: &mut RhaiEngine) {
|
||||
engine.register_fn(
|
||||
"has",
|
||||
@@ -243,6 +279,24 @@ fn register_group_set(engine: &mut RhaiEngine) {
|
||||
);
|
||||
}
|
||||
|
||||
fn register_group_set_if(engine: &mut RhaiEngine) {
|
||||
engine.register_fn(
|
||||
"set_if",
|
||||
|handle: &mut GroupKvHandle,
|
||||
key: &str,
|
||||
expected: Dynamic,
|
||||
new: Dynamic|
|
||||
-> Result<bool, Box<EvalAltResult>> {
|
||||
let h = handle.clone();
|
||||
let exp = expected_from_dynamic(&expected);
|
||||
let new = dynamic_to_json(&new);
|
||||
block_on("kv", async move {
|
||||
h.service.set_if(&h.cx, &h.collection, key, exp, new).await
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn register_group_has(engine: &mut RhaiEngine) {
|
||||
engine.register_fn(
|
||||
"has",
|
||||
|
||||
Reference in New Issue
Block a user