Conversation
…actions
When process_command/3 returns {error, Reason} (e.g. {error, noproc}
during Ra initialisation), the catch-all clause 'Ret -> {ok, Ret}'
was wrapping it as {ok, {error, Reason}}. Callers that pattern-match
on {ok, Value} would then silently receive an error tuple as the
transaction result, causing crashes or wrong behaviour downstream.
Add an explicit {error, _} = Error -> Error clause before the
catch-all so infrastructure errors are propagated directly rather
than being mistaken for a successful transaction return value.
58449e6 to
292d419
Compare
Add a regression test asserting that a read-write transaction against a
store that is not running returns {error, noproc} directly, rather than
the wrapped {ok, {error, noproc}} that the catch-all clause previously
produced.
A path-pattern transaction is used so the command reaches
process_command/3 without stand-alone function extraction (which would
require the khepri application to be started). The test fails without the
{error, _} = Error -> Error clause and passes with it.
…transactions
The read-write fix left the same defect in both readonly_transaction/4
clauses. process_query/3 can return an infrastructure error such as
{error, noproc} (store not running) or {error, timeout}, but the catch-all
'Ret -> {ok, Ret}' wrapped it as {ok, {error, Reason}}. A caller matching
on {ok, Value} would then mistake the error for a successful transaction
result.
Add an explicit {error, _} = Error -> Error clause to both read-only
clauses so dispatch errors propagate verbatim, matching the read-write
path and khepri:handle_async_ret/2 (which already returns {error, _}
directly and raises only {exception, _, _, _}). tx_ret() already permits
{error, Reason} via tx_abort(), so the public shape is unchanged.
Document tx_ret() and add a read-only regression test mirroring the
existing read-write one against a non-running store.
|
Hi! Sorry for the delay to get back to you. I still need to think about this. I agree it’s better to return a problem with the Ra server as Perhaps I need to revisit the transaction API a bit to make things clearer. Like:
Perhaps it would be more intuitive this way: it would be like the transaction function was executed directly and the fact it is passed to What do you think? |
|
No problem on the delay, transactions are tricky business :). You're right about the side effect, and I agree: a transaction body that returns a bare As written, the PR doesn't actually satisfy that. I think the gap isn't too wide, though: a Ra-level infrastructure failure ( On the redesign: from the consumer side (reckon-db is an event store on Khepri; we carry So: transparent return, throw for Two requests if you go that way:
Happy to rework this PR along whichever line you prefer, including a test for the "body returns an error-shaped value" case so it stays Thoughts? |
Making the distinction between theses two sources of
Khepri already uses the
I’m strict about versioning (following semver) and breaking changes. In this case, the version would be bumped to e.g. 0.20.0. I’m not bumping the major version yet because the API is still not stable (I have other breaking changes in mind I didn’t get to work on yet, for instance around conditions in path patterns). I would also document the breaking change in the release notes with a "Breaking change" red rectangle. Finally, the machine version would have to be bumped too because all members in the cluster would have to act the same way. I was not aware of other consumers of Khepri beside RabbitMQ and some libraries such as |
|
Frankly, "beaten by bad versioning/releases": not at all, not by khepri/ra. I think the more honest framing is that khepri/ra was never really meant to be the load-bearing substrate for actual data in the first place (at least, as I understand it). ReckonDB builds a real, purpose-built event store on top of it, which is closer to abusing it than using it as intended, I suppose. |
|
Thank you for the feedback! Indeed, the current design of the internal tree is fine for small pieces of data like RabbitMQ metadata. But we also limit out usage internally because we can’t store large data inside it. One of the next big task on Khepri will be to tackle this problem. I know what I want to try and already have most of the details on paper. Hopefully, I should start to work on this before the end of this year. |
|
I pushed the Does it work for your use case? I would like to push this effort further and implement what we discussed for transactions but also stored procedures if they need it too. |
|
I pushed an update to One last thing I need to do is deprecate What do you think? |
|
Thanks for picking this up, and steal away, the tests are better off in your branch :) Short answer: yes, this is the shape we were hoping for. Tagging the fun's return as I built the branch (b53a564, OTP 28) and ran a few cases against it. One gap: the case that started this PR still comes back in the old shape. %% Store never started:
khepri:transaction(never_started, [foo], [], rw).
%% => {error, noproc} (returned, not raised)
%% Running store, the fun itself returns an error tuple:
khepri:transaction(StoreId, fun() -> {error, noproc} end, rw).
%% => {error, noproc}As far as I can tell, The same check probably leaves a short window right after Would it make sense to tag the dispatch errors where they are produced, the same way you tag the fun's return value? Then the caller side wouldn't need to infer anything from the machine version. Either way, here are tests for it, for Tests for the stopped-store casereadwrite_transaction_on_stopped_store_raises_test() ->
%% A store that was never started has no Ra server, so the command never
%% reaches the state machine and the transaction function never runs. The
%% resulting `{error, noproc}' must be raised as a `tx_error', like any
%% other error from Ra, so it can't be mistaken for a value returned by
%% the transaction function.
%%
%% A path-pattern transaction is used so the command reaches
%% `process_command/3' without going through stand-alone function
%% extraction, which would require the khepri application.
?assertError(
?khepri_error(tx_error, #{reason := {error, noproc}}),
khepri:transaction(?FUNCTION_NAME, [foo], [], rw)).
readonly_transaction_on_stopped_store_raises_test() ->
%% Same expectation as the read-write case, for a read-only transaction
%% going through `process_query/3'.
?assertError(
?khepri_error(tx_error, #{reason := {error, noproc}}),
khepri:transaction(?FUNCTION_NAME, [foo], [], ro)).
tx_fun_returning_error_tuple_test_() ->
%% The counterpart of the tests above: when the transaction function
%% itself returns an error-shaped value, it ran, and that value is
%% returned as is.
{setup,
fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end,
fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end,
[?_assertEqual(
{error, noproc},
begin
Fun = fun() -> {error, noproc} end,
khepri:transaction(?FUNCTION_NAME, Fun, rw)
end),
?_assertEqual(
{error, noproc},
begin
Fun = fun() -> {error, noproc} end,
khepri:transaction(?FUNCTION_NAME, Fun, ro)
end)]}.One small thing I noticed while reading: On our side the migration is small: a handful of call sites, and our Thanks again! |
|
Thank you @rgfaber for the feedback! I modified Hopefullly, your example is correctly handled now. I also modified the documentation and comments which I didn’t touch before being sure of what I want from the API. |
…rocs
[Why]
Before this patch, the return value of `khepri:transaction()` was:
* `{ok, TxRet}` or
* an exception if the transaction function aborted or crashed
Any error, including with the communication with the Ra process (such as
a `noproc` error or a timeout), was wrapped in `{ok, Error}` too.
A return value of `{ok, {error, Reason}}` is awkward at best. It also
made its interpretation quite messy: is the error from Khepri or the
transaction function itself?
This problem doesn't exist with an executed stored procedure because it
is already transparent: it acts as if it was executed by the caller
directly, like if Khepri was never involved.
Having the same behaviour with transaction fixes the interpretation of
the return values and makes the global API more consistent.
[How]
The return value of the transaction function is always wrapped in a
`{txfun_ret, TxRet}` tuple internally. This helps distinguish it from
other return values along the chain. Before returning anything to the
caller, the transaction function return value `TxRet` is unwrapped.
Any errors coming from outside the transaction function are thrown as
exception. For instance `error:{error, noproc}`.
`khepri:abort(Reason)` still raises a `throw:Reason` exception. That
said, this API is deprecated because any Erlang exceptions will do the
job just fine.
This is a breaking change in Khepri transaction API. Callers will have
to be adapted because the return value is changed from `{ok, ActualRet}`
to `ActualRet`.
The machine version is bumped to 5 and a new `transparent_tx_fun`
behaviour is introduced to help with backward compatibility. Regardless
of the effective machine version, the caller gets this new API.
References #400.
…procs
[Why]
Before this patch, the return value of `khepri:transaction()` was:
* `{ok, TxRet}` or
* an exception if the transaction function aborted or crashed
Any error, including with the communication with the Ra process (such as
a `noproc` error or a timeout), was wrapped in `{ok, Error}` too.
A return value of `{ok, {error, Reason}}` is awkward at best. It also
made its interpretation quite messy: is the error from Khepri or the
transaction function itself?
This problem doesn't exist with an executed stored procedure because it
is already transparent: it acts as if it was executed by the caller
directly, like if Khepri was never involved.
Having the same behaviour with transaction fixes the interpretation of
the return values and makes the global API more consistent.
[How]
The return value of the transaction function is always wrapped in a
`{txfun_ret, TxRet}` tuple internally. This helps distinguish it from
other return values along the chain. Before returning anything to the
caller, the transaction function return value `TxRet` is unwrapped.
Any errors coming from outside the transaction function are thrown as
exception. For instance `error:{error, noproc}`.
`khepri:abort(Reason)` still raises a `throw:Reason` exception. That
said, this API is deprecated because any Erlang exceptions will do the
job just fine.
This is a breaking change in Khepri transaction API. Callers will have
to be adapted because the return value is changed from `{ok, ActualRet}`
to `ActualRet`.
The machine version is bumped to 5 and a new `transparent_tx_fun`
behaviour is introduced to help with backward compatibility. Regardless
of the effective machine version, the caller gets this new API.
References #400.
…procs
[Why]
Before this patch, the return value of `khepri:transaction()` was:
* `{ok, TxRet}` or
* an exception if the transaction function aborted or crashed
Any error, including with the communication with the Ra process (such as
a `noproc` error or a timeout), was wrapped in `{ok, Error}` too.
A return value of `{ok, {error, Reason}}` is awkward at best. It also
made its interpretation quite messy: is the error from Khepri or the
transaction function itself?
This problem doesn't exist with an executed stored procedure because it
is already transparent: it acts as if it was executed by the caller
directly, like if Khepri was never involved.
Having the same behaviour with transaction fixes the interpretation of
the return values and makes the global API more consistent.
[How]
The return value of the transaction function is always wrapped in a
`{txfun_ret, TxRet}` tuple internally. This helps distinguish it from
other return values along the chain. Before returning anything to the
caller, the transaction function return value `TxRet` is unwrapped.
Any errors coming from outside the transaction function are thrown as
exception. For instance `error:{error, noproc}`.
`khepri:abort(Reason)` still raises a `throw:Reason` exception. That
said, this API is deprecated because any Erlang exceptions will do the
job just fine.
This is a breaking change in Khepri transaction API. Callers will have
to be adapted because the return value is changed from `{ok, ActualRet}`
to `ActualRet`.
The machine version is bumped to 5 and a new `transparent_tx_funs`
behaviour is introduced to help with backward compatibility. Regardless
of the effective machine version, the caller gets this new API.
References #400.
…procs
[Why]
Before this patch, the return value of `khepri:transaction()` was:
* `{ok, TxRet}` or
* an exception if the transaction function aborted or crashed
Any error, including with the communication with the Ra process (such as
a `noproc` error or a timeout), was wrapped in `{ok, Error}` too.
A return value of `{ok, {error, Reason}}` is awkward at best. It also
made its interpretation quite messy: is the error from Khepri or the
transaction function itself?
This problem doesn't exist with an executed stored procedure because it
is already transparent: it acts as if it was executed by the caller
directly, like if Khepri was never involved.
Having the same behaviour with transaction fixes the interpretation of
the return values and makes the global API more consistent.
[How]
The return value of the transaction function is always wrapped in a
`{txfun_ret, TxRet}` tuple internally. This helps distinguish it from
other return values along the chain. Before returning anything to the
caller, the transaction function return value `TxRet` is unwrapped.
Any errors coming from outside the transaction function are thrown as
exception. For instance `error:{error, noproc}`.
`khepri:abort(Reason)` still raises a `throw:Reason` exception. That
said, this API is deprecated because any Erlang exceptions will do the
job just fine.
This is a breaking change in Khepri transaction API. Callers will have
to be adapted because the return value is changed from `{ok, ActualRet}`
to `ActualRet`.
The machine version is bumped to 5 and a new `transparent_tx_funs`
behaviour is introduced to help with backward compatibility. Regardless
of the effective machine version, the caller gets this new API.
References #400.
…procs
[Why]
Before this patch, the return value of `khepri:transaction()` was:
* `{ok, TxRet}` or
* an exception if the transaction function aborted or crashed
Any error, including with the communication with the Ra process (such as
a `noproc` error or a timeout), was wrapped in `{ok, Error}` too.
A return value of `{ok, {error, Reason}}` is awkward at best. It also
made its interpretation quite messy: is the error from Khepri or the
transaction function itself?
This problem doesn't exist with an executed stored procedure because it
is already transparent: it acts as if it was executed by the caller
directly, like if Khepri was never involved.
Having the same behaviour with transaction fixes the interpretation of
the return values and makes the global API more consistent.
[How]
The return value of the transaction function is always wrapped in a
`{txfun_ret, TxRet}` tuple internally. This helps distinguish it from
other return values along the chain. Before returning anything to the
caller, the transaction function return value `TxRet` is unwrapped.
Any errors coming from outside the transaction function are thrown as
exception. For instance `error:{error, noproc}`.
`khepri:abort(Reason)` still raises a `throw:Reason` exception. That
said, this API is deprecated because any Erlang exceptions will do the
job just fine.
This is a breaking change in Khepri transaction API. Callers will have
to be adapted because the return value is changed from `{ok, ActualRet}`
to `ActualRet`.
The machine version is bumped to 5 and a new `transparent_tx_funs`
behaviour is introduced to help with backward compatibility. Regardless
of the effective machine version, the caller gets this new API.
References #400.
…procs
[Why]
Before this patch, the return value of `khepri:transaction()` was:
* `{ok, TxRet}` or
* an exception if the transaction function aborted or crashed
Any error, including with the communication with the Ra process (such as
a `noproc` error or a timeout), was wrapped in `{ok, Error}` too.
A return value of `{ok, {error, Reason}}` is awkward at best. It also
made its interpretation quite messy: is the error from Khepri or the
transaction function itself?
This problem doesn't exist with an executed stored procedure because it
is already transparent: it acts as if it was executed by the caller
directly, like if Khepri was never involved.
Having the same behaviour with transaction fixes the interpretation of
the return values and makes the global API more consistent.
[How]
The return value of the transaction function is always wrapped in a
`{txfun_ret, TxRet}` tuple internally. This helps distinguish it from
other return values along the chain. Before returning anything to the
caller, the transaction function return value `TxRet` is unwrapped.
Any errors coming from outside the transaction function are thrown as
exception. For instance `error:{error, noproc}`.
`khepri:abort(Reason)` still raises a `throw:Reason` exception. That
said, this API is deprecated because any Erlang exceptions will do the
job just fine.
This is a breaking change in Khepri transaction API. Callers will have
to be adapted because the return value is changed from `{ok, ActualRet}`
to `ActualRet`.
The machine version is bumped to 5 and a new `transparent_tx_funs`
behaviour is introduced to help with backward compatibility. Regardless
of the effective machine version, the caller gets this new API.
References #400.
Problem
The sync transaction paths in
khepri_machinewrapped every non-exception result with{ok, Ret}via a catch-all clause. Whenprocess_command/3(read-write) orprocess_query/3(read-only) returned an infrastructure error such as{error, noproc}(store not running) or{error, timeout}, the catch-all turned it into{ok, {error, Reason}}.A caller matching on
{ok, Value}then received an error tuple as the transaction result, mistaking a dispatch failure for a successful transaction and crashing downstream.Fix
Add an explicit
{error, _} = Error -> Errorclause before the catch-all in:readwrite_transaction1/3readonly_transaction/4clauses (function and path-pattern)so infrastructure errors propagate verbatim.
This aligns sync transactions with the convention already used by
khepri:handle_async_ret/2, which propagates{error, _}directly and raises only{exception, _, _, _}.tx_ret()already includes{error, Reason}viatx_abort(), so the public return shape is unchanged.tx_ret()is documented accordingly.Tests
Regression tests for both the read-write and read-only paths, issuing a path-pattern transaction against a store that is never started so the command/query reaches
process_command/3/process_query/3and returns{error, noproc}. Each test fails without the corresponding{error, _}clause (producing{ok, {error, noproc}}) and passes with it.Note for reviewers
A transaction body that returns a bare
{error, Reason}value (rather than aborting) now surfaces as{error, Reason}instead of{ok, {error, Reason}}. This is consistent with howhandle_async_ret/2already treats asynchronous transactions and withtx_abort(). To return an error-shaped value as a successful result, wrap it explicitly ({ok, {error, Reason}}); this is documented ontx_ret().