diff --git a/src/luamod.rs b/src/luamod.rs index 463acf276840b3b4b0edab41c4a8dbb9932864a1..9c37830e1122be8759ed95491721007c6bbf0630 100644 --- a/src/luamod.rs +++ b/src/luamod.rs @@ -637,21 +637,26 @@ pub(crate) fn setup(args: &args::Run) { &l, "cas", indoc! {" - pico.cas(dml, predicate) + pico.cas(dml, [predicate]) ======================== Performs a clusterwide compare and swap operation. E.g. it checks the `predicate` on leader and if no conflicting entries were found - appends the `op` to the raft log and returns its index (number). + appends the `op` to the raft log and returns its index (number). If predicate + is not supplied, it will be auto generated with `index` and `term` taken from the + current instance and with empty `ranges`. # Params 1. dml - table. See pico.help(\"Dml\") 2. predicate - table. See pico.help(\"Predicate\") "}, tlua::function2( - |op: op::DmlInLua, predicate: rpc::cas::Predicate| -> traft::Result<RaftIndex> { + |op: op::DmlInLua, + predicate: Option<rpc::cas::PredicateInLua>| + -> traft::Result<RaftIndex> { let op = op::Dml::from_lua_args(op).map_err(traft::error::Error::other)?; + let predicate = rpc::cas::Predicate::from_lua_args(predicate.unwrap_or_default())?; let (index, _) = compare_and_swap(op.into(), predicate)?; Ok(index) }, @@ -676,10 +681,15 @@ pub(crate) fn setup(args: &args::Run) { "Predicate", indoc! {" Predicate that will be checked by the leader, before accepting the proposed op. + Has these fields: - - index (number) - - term (number) - - ranges (table) + - index (optional, number) + - term (optional, number) + - ranges (optional, table) + + If some fields are not supplied, they will be autogenerated. `index` and `term` taken from the + raft state on the instance which sends this operation and `ranges` left as an empty + vector. "}, ); luamod_set( diff --git a/src/rpc/cas.rs b/src/rpc/cas.rs index 9736e625bde1e9952c1430fb30194f7937c11fa5..83a0d9388e1e1e79b4358a3299b0445558906593 100644 --- a/src/rpc/cas.rs +++ b/src/rpc/cas.rs @@ -1,6 +1,7 @@ use crate::storage::Clusterwide; use crate::storage::ClusterwideSpaceId; use crate::tlog; +use crate::traft; use crate::traft::error::Error as TraftError; use crate::traft::node; use crate::traft::op::{Ddl, Dml, Op}; @@ -215,8 +216,22 @@ pub enum Error { KeyTypeMismatch(#[from] TntError), } +/// Represents a lua table describing a [`Predicate`]. +/// +/// This is only used to parse lua arguments from lua api functions such as +/// `pico.cas`. +#[derive(Clone, Debug, Default, ::serde::Serialize, ::serde::Deserialize, tlua::LuaRead)] +pub struct PredicateInLua { + /// CaS sender's current raft index. + pub index: Option<RaftIndex>, + /// CaS sender's current raft term. + pub term: Option<RaftTerm>, + /// Range that the CaS sender have read and expects it to be unmodified. + pub ranges: Option<Vec<Range>>, +} + /// Predicate that will be checked by the leader, before accepting the proposed `op`. -#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize, tlua::LuaRead)] +#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)] pub struct Predicate { /// CaS sender's current raft index. pub index: RaftIndex, @@ -227,6 +242,27 @@ pub struct Predicate { } impl Predicate { + pub fn from_lua_args(predicate: PredicateInLua) -> traft::Result<Self> { + let node = traft::node::global()?; + let (index, term) = if let Some(index) = predicate.index { + if let Some(term) = predicate.term { + (index, term) + } else { + let term = raft::Storage::term(&node.raft_storage, index)?; + (index, term) + } + } else { + let index = node.get_index(); + let term = raft::Storage::term(&node.raft_storage, index)?; + (index, term) + }; + Ok(Self { + index, + term, + ranges: predicate.ranges.unwrap_or_default(), + }) + } + /// Checks if `entry_op` changes anything within the ranges specified in the predicate. pub fn check_entry( &self, diff --git a/test/conftest.py b/test/conftest.py index 3b53ccc868945f64c412b2483e8fa3274c2c9e7b..bd77f02e063e3c652d79fb59bcb26e144155f61e 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -607,6 +607,21 @@ class Instance: return self.call("pico.raft_compact_log", 0) + def space_id(self, space: str | int) -> int: + """ + Get space id by space name. + If id is supplied instead it is just returned back. + + This method is useful in functions which can take both id and space name. + """ + match space: + case int(): + return space + case str(): + return self.eval("return box.space[...].id", space) + case _: + raise TypeError("space must be str or int") + def cas( self, dml_kind: Literal["insert", "replace", "delete"], @@ -614,7 +629,7 @@ class Instance: tuple: Tuple | List, index: int | None = None, term: int | None = None, - range: CasRange | None = None, # TODO better types for bounds + range: CasRange | None = None, ) -> int: """ Performs a clusterwide compare and swap operation. @@ -622,13 +637,6 @@ class Instance: E.g. it checks the `predicate` on leader and if no conflicting entries were found appends the `op` to the raft log and returns its index. - `range` is a tuple of two dictionaries: (key_min, key_max). Each dictionary - is of the following structure: - { - "kind": "included" | "excluded" | "unbounded", - "value": int | None - } - ASSUMPTIONS It is assumed that this operation is called on leader. Failing to do so will result in an error. @@ -639,14 +647,7 @@ class Instance: elif term is None: term = self.raft_term_by_index(index) - space_id = None - match space: - case int(): - space_id = space - case str(): - space_id = self.eval("return box.space[...].id", space) - case _: - raise TypeError("space must be str or int") + space_id = self.space_id(space) predicate_range = None if range is not None: @@ -1077,7 +1078,7 @@ class Cluster: tuple: Tuple | List, index: int | None = None, term: int | None = None, - range: CasRange | None = None, # TODO better types + range: CasRange | None = None, # If specified send CaS through this instance instance: Instance | None = None, ) -> int: @@ -1088,21 +1089,9 @@ class Cluster: appends the `op` to the raft log and returns its index. Calling this operation will route CaS request to a leader. - - `range` is a tuple of two dictionaries: (key_min, key_max). Each dictionary - is of the following structure: - { - "kind": "included" | "excluded" | "unbounded", - "value": int # skip if `None` - } """ if instance is None: instance = self.instances[0] - if index is None: - index = instance.raft_read_index() - term = instance.raft_term_by_index(index) - elif term is None: - term = instance.raft_term_by_index(index) predicate_range = None if range is not None: @@ -1115,7 +1104,7 @@ class Cluster: predicate = dict( index=index, term=term, - ranges=[predicate_range] if predicate_range is not None else [], + ranges=predicate_range, ) if dml_kind in ["insert", "replace", "delete"]: dml = dict(