Skip to content

Commit 9d40219

Browse files
authored
Merge pull request #54 from ccamel/fix/improvements
Fix/improvements
2 parents d23f7a9 + cb450e0 commit 9d40219

4 files changed

Lines changed: 57 additions & 14 deletions

File tree

apps/es_kernel/src/es_kernel_aggregate.erl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,11 +319,10 @@ apply_events(PayloadEvents, {AggregateModule, State0, Sequence0}) ->
319319
}
320320
) -> ok.
321321
persist_events(PayloadEvents, {AggregateModule, StoreContext, StreamId, Sequence0, NowFun}) ->
322-
%% Extract aggregate_type from StreamId (which is {aggregate_type, aggregate_id})
323322
{AggregateType, _AggregateId} = StreamId,
324323
{Events, _} =
325-
lists:foldl(
326-
fun(PayloadEvent, {Events, SequenceN}) ->
324+
lists:mapfoldl(
325+
fun(PayloadEvent, SequenceN) ->
327326
Now = NowFun(),
328327
SequenceN1 = SequenceN + 1,
329328
EventType = AggregateModule:event_type(PayloadEvent),
@@ -336,9 +335,9 @@ persist_events(PayloadEvents, {AggregateModule, StoreContext, StreamId, Sequence
336335
Now,
337336
PayloadEvent
338337
),
339-
{[Event | Events], SequenceN1}
338+
{Event, SequenceN1}
340339
end,
341-
{[], Sequence0},
340+
Sequence0,
342341
PayloadEvents
343342
),
344343
lists:foreach(

apps/es_kernel/src/es_kernel_mgr_aggregate.erl

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Dispatches a command to the appropriate aggregate instance.
7777
- Command is the command to dispatch.
7878

7979
Returns `ok` on success, or `{error, Reason}` if routing or execution fails.
80+
If the target aggregate dies during the call, returns `{error, {aggregate_down, Reason}}`
81+
and evicts the stale PID so a subsequent dispatch will start a fresh aggregate.
8082
""".
8183
-spec dispatch(ServerRef, Command) -> ok | {error, Reason} when
8284
ServerRef :: gen_server:server_ref(),
@@ -198,21 +200,34 @@ ensure_and_dispatch(
198200
end;
199201
Pid ->
200202
Result = forward(Pid, Command),
201-
{ok, Result, State}
203+
case Result of
204+
{error, {aggregate_down, _}} ->
205+
NewPids = maps:remove(Target, Pids),
206+
{ok, Result, State#state{pids = NewPids}};
207+
_ ->
208+
{ok, Result, State}
209+
end
202210
end.
203211

204212
-doc """
205213
Forwards a command to an aggregate process.
206214

207215
Returns the result of the aggregate's `execute/2` function:
208216
`ok` on success or `{error, Reason}` on failure.
217+
218+
If the target process dies before replying, returns `{error, {aggregate_down, Reason}}`.
209219
""".
210220
-spec forward(Pid, Command) -> ok | {error, Reason} when
211221
Pid :: pid(),
212222
Command :: es_contract_command:t(),
213223
Reason :: term().
214224
forward(Pid, Command) ->
215-
es_kernel_aggregate:execute(Pid, Command).
225+
try es_kernel_aggregate:execute(Pid, Command) of
226+
Result -> Result
227+
catch
228+
exit:Reason ->
229+
{error, {aggregate_down, Reason}}
230+
end.
216231

217232
-doc """
218233
Starts an aggregate process for a given stream ID.

apps/es_kernel/src/es_kernel_store.erl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,14 @@ This is a convenience wrapper around fold/5 that collects all events into a list
9292
Range :: es_contract_range:range(),
9393
Result :: [es_contract_event:t()].
9494
retrieve_events(StoreContext, StreamId, Range) ->
95-
fold(
96-
StoreContext,
97-
StreamId,
98-
fun(Event, Acc) -> Acc ++ [Event] end,
99-
[],
100-
Range
95+
lists:reverse(
96+
fold(
97+
StoreContext,
98+
StreamId,
99+
fun(Event, Acc) -> [Event | Acc] end,
100+
[],
101+
Range
102+
)
101103
).
102104

103105
-doc """

apps/es_kernel/test/es_kernel_mgr_aggregate_tests.erl

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ suite_test_() ->
77
[
88
{"aggregate_behaviour", fun aggregate_behaviour/0},
99
{"aggregate_passivation", fun aggregate_passivation/0},
10-
{"aggregate_invalid_command", fun aggregate_invalid_command/0}
10+
{"aggregate_invalid_command", fun aggregate_invalid_command/0},
11+
{"aggregate_survives_dead_worker", fun aggregate_survives_dead_worker/0}
1112
],
1213
{foreach, fun setup/0, fun teardown/1, TestCases}.
1314

@@ -173,6 +174,32 @@ aggregate_invalid_command() ->
173174

174175
?assertEqual(ok, es_kernel_mgr_aggregate:stop(Pid)).
175176

177+
aggregate_survives_dead_worker() ->
178+
Pid = start_mgr(5000),
179+
Id = <<"123">>,
180+
181+
?assertEqual(
182+
ok,
183+
es_kernel_mgr_aggregate:dispatch(Pid, cmd(deposit, Id, #{amount => 100}))
184+
),
185+
AggPid = agg_id(Pid, bank_account, Id),
186+
exit(AggPid, kill),
187+
188+
%% Dispatch while the cached pid is dead: should not crash manager and should return an error
189+
?assertMatch(
190+
{error, {aggregate_down, _}},
191+
es_kernel_mgr_aggregate:dispatch(Pid, cmd(deposit, Id, #{amount => 10}))
192+
),
193+
194+
%% Next dispatch should start a fresh aggregate and succeed (rehydrated to previous balance)
195+
?assertEqual(
196+
ok,
197+
es_kernel_mgr_aggregate:dispatch(Pid, cmd(deposit, Id, #{amount => 20}))
198+
),
199+
?assertState(agg_id(Pid, bank_account, Id), Id, #{balance := 120}, 2),
200+
201+
?assertEqual(ok, es_kernel_mgr_aggregate:stop(Pid)).
202+
176203
start_mgr(Timeout) ->
177204
StoreContext = es_kernel_app:get_store_context(),
178205
{ok, Pid} =

0 commit comments

Comments
 (0)