fed-sx-m2: Step 8b-pure — retry-time bookkeeping + 11 tests + 2 Blockers
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 18s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 18s
delivery_worker state shape gains :next_retry proplist alongside
the existing :attempts:
[{peer, _}, {pending, _}, {attempts, [{Cid, N}]},
{next_retry, [{Cid, NextRetryAt}]}, {dead_letter, _},
{dispatch_fn, _}]
New pure-functional exports:
record_failure_pure/3(Cid, Now, State)
Bumps :attempts for Cid. On the 6th failure
(backoff_for returns dead_letter) moves the matching
activity from :pending to :dead_letter and clears the
:next_retry entry. Otherwise sets next_retry to
Now + backoff_for(NewAttempts).
record_success_pure/2(Cid, State)
Clears both :attempts and :next_retry for Cid.
next_due_pure/2(Now, State)
Returns cids whose retry time has passed (insertion
order preserved so the worker drains in FIFO retry
order).
attempts_for/2, next_retry_at/2, dead_letter_list/1
Read-side accessors.
Internal helper move_to_dead_letter/2 + take_by_cid/4 walks
:pending to find the matching activity by cid.
11/11 in next/tests/delivery_retry.sh covering:
- fresh state: 0 attempts / undefined retry / [] dead_letter
- record_failure bumps to 1
- record_failure sets next_retry_at = Now + 30 (slot 1)
- second failure: attempts=2, NextRetryAt = Now + 300 (slot 2)
- record_success clears both
- next_due returns due cids
- next_due empty before due
- 6th failure -> dead-letter; activity out of :pending
- dead-lettered cid removed from :next_retry
- per-cid isolation: success on one doesn't disturb another
delivery_worker.sh 17/17 unchanged (new exports are additive).
Blockers added:
#2 — Native http-request primitive missing in bin/sx_server.ml
(briefing assumed it existed; only http-listen exists).
Belongs to loops/fed-prims. Step 8e wrapper waits for
the native.
#3 — erlang:send_after-style timer primitive missing. Needed
for the real retry loop. Belongs to loops/erlang. 8b-pure
captures the semantics so 8b-timer is a 1-shot wiring
when the primitive lands.
Conformance preserved at 761/761.
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
-export([new/1, pending/1, peer/1,
|
||||
enqueue_pure/3, drain_pure/1, deliver_one_pure/2,
|
||||
backoff_for/1, schedule_for/1,
|
||||
record_failure_pure/3, record_success_pure/2,
|
||||
next_due_pure/2, attempts_for/2, next_retry_at/2,
|
||||
dead_letter_list/1,
|
||||
start_link/1, start_link/2, stop/1,
|
||||
enqueue/2, flush/1, pending_srv/1, set_dispatch_fn/2]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
|
||||
@@ -31,6 +34,7 @@
|
||||
%% [{peer, PeerId},
|
||||
%% {pending, [Activity, ...]}, %% FIFO; head delivered first
|
||||
%% {attempts, [{Cid, AttemptCount}, ...]},
|
||||
%% {next_retry, [{Cid, NextRetryAt}, ...]}, %% Step 8b-pure
|
||||
%% {dead_letter, [Activity, ...]},
|
||||
%% {dispatch_fn, fun/1 | undefined}]
|
||||
%%
|
||||
@@ -43,6 +47,7 @@ new(PeerId) ->
|
||||
[{peer, PeerId},
|
||||
{pending, []},
|
||||
{attempts, []},
|
||||
{next_retry, []},
|
||||
{dead_letter, []},
|
||||
{dispatch_fn, undefined}].
|
||||
|
||||
@@ -118,6 +123,85 @@ schedule_for(Attempts) ->
|
||||
Seconds -> {retry_in, Seconds}
|
||||
end.
|
||||
|
||||
%% ── Step 8b-pure: retry-time bookkeeping ───────────────────────
|
||||
%%
|
||||
%% `record_failure_pure/3(Cid, Now, State)` — call after a failed
|
||||
%% deliver_one. Bumps the per-cid attempt counter; if the new
|
||||
%% attempt is past the dead-letter threshold, moves the matching
|
||||
%% activity from :pending to :dead_letter. Otherwise records the
|
||||
%% next retry time as Now + backoff_for(NewAttempt).
|
||||
%%
|
||||
%% Real timer wiring (erlang:send_after self-cast on the worker
|
||||
%% pid) needs substrate support — Step 8b-timer when that lands.
|
||||
%%
|
||||
%% `record_success_pure/2(Cid, State)` — clears :attempts and
|
||||
%% :next_retry entries for the cid; called after a successful
|
||||
%% deliver_one.
|
||||
%%
|
||||
%% `next_due_pure/2(Now, State)` — returns the list of Cids whose
|
||||
%% NextRetryAt has passed, in insertion order.
|
||||
|
||||
record_failure_pure(Cid, Now, State) ->
|
||||
Attempts = field(attempts, State),
|
||||
Current = case find_keyed(Cid, Attempts) of
|
||||
{ok, N} -> N;
|
||||
_ -> 0
|
||||
end,
|
||||
New = Current + 1,
|
||||
State1 = set_field(attempts, set_keyed(Cid, New, Attempts), State),
|
||||
case backoff_for(New) of
|
||||
dead_letter ->
|
||||
move_to_dead_letter(Cid, State1);
|
||||
Seconds ->
|
||||
NextAt = Now + Seconds,
|
||||
NR = field(next_retry, State1),
|
||||
set_field(next_retry, set_keyed(Cid, NextAt, NR), State1)
|
||||
end.
|
||||
|
||||
record_success_pure(Cid, State) ->
|
||||
A1 = del_keyed(Cid, field(attempts, State)),
|
||||
NR1 = del_keyed(Cid, field(next_retry, State)),
|
||||
set_field(attempts, A1, set_field(next_retry, NR1, State)).
|
||||
|
||||
%% next_due_pure/2 — Cids whose NextRetryAt <= Now. Preserves
|
||||
%% insertion order so the worker drains them in FIFO retry order.
|
||||
|
||||
next_due_pure(Now, State) ->
|
||||
[Cid || {Cid, At} <- field(next_retry, State), At =< Now].
|
||||
|
||||
attempts_for(Cid, State) ->
|
||||
case find_keyed(Cid, field(attempts, State)) of
|
||||
{ok, N} -> N;
|
||||
_ -> 0
|
||||
end.
|
||||
|
||||
next_retry_at(Cid, State) ->
|
||||
case find_keyed(Cid, field(next_retry, State)) of
|
||||
{ok, At} -> At;
|
||||
_ -> undefined
|
||||
end.
|
||||
|
||||
dead_letter_list(State) -> field(dead_letter, State).
|
||||
|
||||
move_to_dead_letter(Cid, State) ->
|
||||
Pending = field(pending, State),
|
||||
{Match, Rest} = take_by_cid(Cid, Pending, [], []),
|
||||
DL = field(dead_letter, State),
|
||||
State1 = set_field(pending, Rest, State),
|
||||
State2 = case Match of
|
||||
none -> State1;
|
||||
Act -> set_field(dead_letter, DL ++ [Act], State1)
|
||||
end,
|
||||
NR = field(next_retry, State2),
|
||||
set_field(next_retry, del_keyed(Cid, NR), State2).
|
||||
|
||||
take_by_cid(_, [], Acc, _) -> {none, lists:reverse(Acc)};
|
||||
take_by_cid(Cid, [A | Rest], Acc, _) ->
|
||||
case activity_cid(A) of
|
||||
Cid -> {A, lists:reverse(Acc) ++ Rest};
|
||||
_ -> take_by_cid(Cid, Rest, [A | Acc], 0)
|
||||
end.
|
||||
|
||||
%% ── gen_server wrapper ──────────────────────────────────────────
|
||||
|
||||
start_link(PeerId) ->
|
||||
@@ -196,3 +280,7 @@ find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
||||
set_keyed(K, V, []) -> [{K, V}];
|
||||
set_keyed(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
||||
set_keyed(K, V, [P | Rest]) -> [P | set_keyed(K, V, Rest)].
|
||||
|
||||
del_keyed(_, []) -> [];
|
||||
del_keyed(K, [{K, _} | Rest]) -> Rest;
|
||||
del_keyed(K, [P | Rest]) -> [P | del_keyed(K, Rest)].
|
||||
|
||||
Reference in New Issue
Block a user