-module(lightspeed@ops@failover_harness).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lightspeed/ops/failover_harness.gleam").
-export([run_scenario/1, run_matrix/0, scenario_label/1, pass_fail_label/1, signature/1, scenario/1, deterministic/1, outcomes/1, failed_scenarios/1, nondeterministic_failures/1, report_signature/1, snapshot_signature/0, snapshot_report_markdown/0]).
-export_type([scenario/0, scenario_outcome/0, report/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(" Deterministic failover certification harness for M27.\n").
-type scenario() :: supported_backends |
ownership_fencing |
crash_restart_failover |
resume_policy_matrix |
reconnect_continuity_slo.
-type scenario_outcome() :: {scenario_outcome,
scenario(),
boolean(),
boolean(),
binary()}.
-type report() :: {report, list(scenario_outcome()), integer(), integer()}.
-file("src/lightspeed/ops/failover_harness.gleam", 470).
-spec count_nondeterministic(list(scenario_outcome())) -> integer().
count_nondeterministic(Outcomes) ->
case Outcomes of
[] ->
0;
[Outcome | Rest] ->
case erlang:element(4, Outcome) of
true ->
count_nondeterministic(Rest);
false ->
1 + count_nondeterministic(Rest)
end
end.
-file("src/lightspeed/ops/failover_harness.gleam", 459).
-spec count_failed(list(scenario_outcome())) -> integer().
count_failed(Outcomes) ->
case Outcomes of
[] ->
0;
[Outcome | Rest] ->
case erlang:element(3, Outcome) of
true ->
count_failed(Rest);
false ->
1 + count_failed(Rest)
end
end.
-file("src/lightspeed/ops/failover_harness.gleam", 488).
-spec join_with(binary(), list(binary())) -> binary().
join_with(Separator, Values) ->
case Values of
[] ->
<<""/utf8>>;
[Value] ->
Value;
[Value@1 | Rest] ->
<<<<Value@1/binary, Separator/binary>>/binary,
(join_with(Separator, Rest))/binary>>
end.
-file("src/lightspeed/ops/failover_harness.gleam", 449).
-spec entries_all_met(list({boolean(), binary()})) -> boolean().
entries_all_met(Entries) ->
case Entries of
[] ->
true;
[Entry | Rest] ->
{Met, _} = Entry,
Met andalso entries_all_met(Rest)
end.
-file("src/lightspeed/ops/failover_harness.gleam", 360).
-spec evaluate_reconnect_continuity_slo() -> {boolean(), binary()}.
evaluate_reconnect_continuity_slo() ->
Entries = gleam@list:map(
lightspeed@cluster@durable_session:supported_backends(),
fun(Backend) ->
Base = begin
_pipe = lightspeed@cluster@durable_session:start(
<<"s-4"/utf8>>,
<<"node-a"/utf8>>,
<<"/chat"/utf8>>,
Backend,
rehydrate,
0,
25,
1
),
_pipe@1 = lightspeed@cluster@durable_session:append_counter_delta(
_pipe,
1,
2
),
_pipe@2 = lightspeed@cluster@durable_session:append_counter_delta(
_pipe@1,
1,
3
),
_pipe@3 = lightspeed@cluster@durable_session:append_counter_delta(
_pipe@2,
1,
4
),
_pipe@4 = lightspeed@cluster@durable_session:crash(
_pipe@3,
<<"boom"/utf8>>
),
lightspeed@cluster@durable_session:restart(_pipe@4, 200)
end,
Recovered = case lightspeed@cluster@durable_session:request_takeover(
Base,
<<"node-b"/utf8>>,
2,
lightspeed@cluster@durable_session:expected_token(
<<"s-4"/utf8>>,
<<"node-b"/utf8>>,
2
),
210
) of
{ok, Updated} ->
Updated;
{error, _} ->
Base
end,
Reconnected = lightspeed@cluster@durable_session:reconnect(
Recovered,
rehydrate,
<<"/chat"/utf8>>,
220
),
Continuity = lightspeed@cluster@durable_session:continuity_report(
Reconnected,
200,
220
),
{erlang:element(4, Continuity),
<<<<(lightspeed@cluster@durable_session:backend_label(Backend))/binary,
":"/utf8>>/binary,
(lightspeed@cluster@durable_session:continuity_signature(
Continuity
))/binary>>}
end
),
Passed = entries_all_met(Entries),
Signatures = gleam@list:map(
Entries,
fun(Entry) ->
{_, Signature} = Entry,
Signature
end
),
{Passed, join_with(<<";"/utf8>>, Signatures)}.
-file("src/lightspeed/ops/failover_harness.gleam", 322).
-spec evaluate_resume_policy_matrix() -> {boolean(), binary()}.
evaluate_resume_policy_matrix() ->
Baseline = begin
_pipe = lightspeed@cluster@durable_session:start(
<<"s-3"/utf8>>,
<<"node-a"/utf8>>,
<<"/todos"/utf8>>,
{journal_only, <<"journal"/utf8>>},
resume,
0,
25,
0
),
lightspeed@cluster@durable_session:append_counter_delta(_pipe, 2, 6)
end,
Resumed = lightspeed@cluster@durable_session:reconnect(
Baseline,
resume,
<<"/todos"/utf8>>,
40
),
Rehydrated = lightspeed@cluster@durable_session:reconnect(
Baseline,
rehydrate,
<<"/todos"/utf8>>,
40
),
Remounted = lightspeed@cluster@durable_session:reconnect(
Baseline,
remount,
<<"/todos"/utf8>>,
40
),
Resumed_counter = erlang:element(
4,
lightspeed@cluster@durable_session:snapshot(Resumed)
),
Rehydrated_counter = erlang:element(
4,
lightspeed@cluster@durable_session:snapshot(Rehydrated)
),
Remounted_counter = erlang:element(
4,
lightspeed@cluster@durable_session:snapshot(Remounted)
),
Passed = ((Resumed_counter =:= 2) andalso (Rehydrated_counter =:= 2))
andalso (Remounted_counter =:= 0),
{Passed,
<<<<<<<<<<"resume="/utf8,
(erlang:integer_to_binary(Resumed_counter))/binary>>/binary,
"|rehydrate="/utf8>>/binary,
(erlang:integer_to_binary(Rehydrated_counter))/binary>>/binary,
"|remount="/utf8>>/binary,
(erlang:integer_to_binary(Remounted_counter))/binary>>}.
-file("src/lightspeed/ops/failover_harness.gleam", 267).
-spec evaluate_crash_restart_failover() -> {boolean(), binary()}.
evaluate_crash_restart_failover() ->
Durable = begin
_pipe = lightspeed@cluster@durable_session:start(
<<"s-2"/utf8>>,
<<"node-a"/utf8>>,
<<"/counter"/utf8>>,
{snapshot_journal, <<"snap"/utf8>>, <<"journal"/utf8>>, 2},
rehydrate,
0,
25,
1
),
_pipe@1 = lightspeed@cluster@durable_session:append_counter_delta(
_pipe,
3,
5
),
_pipe@2 = lightspeed@cluster@durable_session:crash(
_pipe@1,
<<"boom"/utf8>>
),
lightspeed@cluster@durable_session:restart(_pipe@2, 100)
end,
Recovered = case lightspeed@cluster@durable_session:request_takeover(
Durable,
<<"node-b"/utf8>>,
2,
lightspeed@cluster@durable_session:expected_token(
<<"s-2"/utf8>>,
<<"node-b"/utf8>>,
2
),
110
) of
{ok, Updated} ->
Updated;
{error, _} ->
Durable
end,
Reconnected = lightspeed@cluster@durable_session:reconnect(
Recovered,
rehydrate,
<<"/counter"/utf8>>,
118
),
Continuity = lightspeed@cluster@durable_session:continuity_report(
Reconnected,
100,
118
),
Snapshot = lightspeed@cluster@durable_session:snapshot(Reconnected),
Passed = ((erlang:element(4, Snapshot) =:= 3) andalso (lightspeed@cluster@durable_session:lifecycle_label(
erlang:element(5, Snapshot)
)
=:= <<"live"/utf8>>))
andalso erlang:element(4, Continuity),
{Passed,
<<<<(lightspeed@cluster@durable_session:signature(Reconnected))/binary,
"|continuity="/utf8>>/binary,
(lightspeed@cluster@durable_session:continuity_signature(Continuity))/binary>>}.
-file("src/lightspeed/ops/failover_harness.gleam", 440).
-spec ownership_result_label(
{ok, lightspeed@cluster@durable_session:durable_session()} |
{error, lightspeed@cluster@durable_session:ownership_error()}
) -> binary().
ownership_result_label(Value) ->
case Value of
{ok, _} ->
<<"ok"/utf8>>;
{error, Error} ->
lightspeed@cluster@durable_session:ownership_error_label(Error)
end.
-file("src/lightspeed/ops/failover_harness.gleam", 194).
-spec evaluate_ownership_fencing() -> {boolean(), binary()}.
evaluate_ownership_fencing() ->
Baseline = lightspeed@cluster@durable_session:start(
<<"s-1"/utf8>>,
<<"node-a"/utf8>>,
<<"/counter"/utf8>>,
{snapshot_journal, <<"snap"/utf8>>, <<"journal"/utf8>>, 2},
rehydrate,
0,
25,
1
),
Takeover_token = lightspeed@cluster@durable_session:expected_token(
<<"s-1"/utf8>>,
<<"node-b"/utf8>>,
2
),
Takeover = lightspeed@cluster@durable_session:request_takeover(
Baseline,
<<"node-b"/utf8>>,
2,
Takeover_token,
10
),
{Takeover_label, Takeover_signature} = case Takeover of
{ok, Updated} ->
{<<"ok"/utf8>>,
lightspeed@cluster@durable_session:fence_signature(
lightspeed@cluster@durable_session:fence(Updated)
)};
{error, Error} ->
{lightspeed@cluster@durable_session:ownership_error_label(Error),
<<"none"/utf8>>}
end,
Stale_label = case Takeover of
{ok, Updated@1} ->
_pipe = lightspeed@cluster@durable_session:request_takeover(
Updated@1,
<<"node-a"/utf8>>,
1,
lightspeed@cluster@durable_session:expected_token(
<<"s-1"/utf8>>,
<<"node-a"/utf8>>,
1
),
11
),
ownership_result_label(_pipe);
{error, Error@1} ->
lightspeed@cluster@durable_session:ownership_error_label(Error@1)
end,
Split_label = case Takeover of
{ok, Updated@2} ->
_pipe@1 = lightspeed@cluster@durable_session:request_takeover(
Updated@2,
<<"node-c"/utf8>>,
2,
lightspeed@cluster@durable_session:expected_token(
<<"s-1"/utf8>>,
<<"node-c"/utf8>>,
2
),
12
),
ownership_result_label(_pipe@1);
{error, Error@2} ->
lightspeed@cluster@durable_session:ownership_error_label(Error@2)
end,
Passed = ((Takeover_label =:= <<"ok"/utf8>>) andalso (Stale_label =:= <<"stale_owner:2:1"/utf8>>))
andalso (Split_label =:= <<"split_brain:node-b:node-c:2"/utf8>>),
{Passed,
<<<<<<<<<<<<<<"takeover="/utf8, Takeover_label/binary>>/binary,
"|fence="/utf8>>/binary,
Takeover_signature/binary>>/binary,
"|stale="/utf8>>/binary,
Stale_label/binary>>/binary,
"|split="/utf8>>/binary,
Split_label/binary>>}.
-file("src/lightspeed/ops/failover_harness.gleam", 420).
-spec backends_valid(
list(lightspeed@cluster@durable_session:persistence_backend())
) -> boolean().
backends_valid(Backends) ->
case Backends of
[] ->
true;
[Backend | Rest] ->
Contract = lightspeed@cluster@durable_session:start(
<<"s-check"/utf8>>,
<<"node-a"/utf8>>,
<<"/counter"/utf8>>,
Backend,
rehydrate,
0,
25,
1
),
lightspeed@cluster@durable_session:valid(Contract) andalso backends_valid(
Rest
)
end.
-file("src/lightspeed/ops/failover_harness.gleam", 186).
-spec evaluate_supported_backends() -> {boolean(), binary()}.
evaluate_supported_backends() ->
Backends = lightspeed@cluster@durable_session:supported_backends(),
Labels = gleam@list:map(
Backends,
fun lightspeed@cluster@durable_session:backend_label/1
),
Passed = (erlang:length(Backends) =:= 3) andalso backends_valid(Backends),
{Passed, join_with(<<","/utf8>>, Labels)}.
-file("src/lightspeed/ops/failover_harness.gleam", 176).
-spec evaluate(scenario()) -> {boolean(), binary()}.
evaluate(Scenario) ->
case Scenario of
supported_backends ->
evaluate_supported_backends();
ownership_fencing ->
evaluate_ownership_fencing();
crash_restart_failover ->
evaluate_crash_restart_failover();
resume_policy_matrix ->
evaluate_resume_policy_matrix();
reconnect_continuity_slo ->
evaluate_reconnect_continuity_slo()
end.
-file("src/lightspeed/ops/failover_harness.gleam", 57).
?DOC(" Run one scenario twice and require deterministic parity.\n").
-spec run_scenario(scenario()) -> scenario_outcome().
run_scenario(Scenario) ->
{First_passed, First_signature} = evaluate(Scenario),
{Second_passed, Second_signature} = evaluate(Scenario),
Deterministic = (First_passed =:= Second_passed) andalso (First_signature
=:= Second_signature),
Passed = (First_passed andalso Second_passed) andalso Deterministic,
{scenario_outcome, Scenario, Passed, Deterministic, First_signature}.
-file("src/lightspeed/ops/failover_harness.gleam", 38).
?DOC(" Run all M27 scenarios.\n").
-spec run_matrix() -> report().
run_matrix() ->
Outcomes = begin
_pipe = [supported_backends,
ownership_fencing,
crash_restart_failover,
resume_policy_matrix,
reconnect_continuity_slo],
gleam@list:map(_pipe, fun run_scenario/1)
end,
{report, Outcomes, count_failed(Outcomes), count_nondeterministic(Outcomes)}.
-file("src/lightspeed/ops/failover_harness.gleam", 73).
?DOC(" Scenario label.\n").
-spec scenario_label(scenario()) -> binary().
scenario_label(Scenario) ->
case Scenario of
supported_backends ->
<<"supported_backends"/utf8>>;
ownership_fencing ->
<<"ownership_fencing"/utf8>>;
crash_restart_failover ->
<<"crash_restart_failover"/utf8>>;
resume_policy_matrix ->
<<"resume_policy_matrix"/utf8>>;
reconnect_continuity_slo ->
<<"reconnect_continuity_slo"/utf8>>
end.
-file("src/lightspeed/ops/failover_harness.gleam", 84).
?DOC(" Stable pass/fail label.\n").
-spec pass_fail_label(scenario_outcome()) -> binary().
pass_fail_label(Outcome) ->
case erlang:element(3, Outcome) of
true ->
<<"pass"/utf8>>;
false ->
<<"fail"/utf8>>
end.
-file("src/lightspeed/ops/failover_harness.gleam", 92).
?DOC(" Scenario signature.\n").
-spec signature(scenario_outcome()) -> binary().
signature(Outcome) ->
erlang:element(5, Outcome).
-file("src/lightspeed/ops/failover_harness.gleam", 97).
?DOC(" Scenario accessor.\n").
-spec scenario(scenario_outcome()) -> scenario().
scenario(Outcome) ->
erlang:element(2, Outcome).
-file("src/lightspeed/ops/failover_harness.gleam", 102).
?DOC(" Scenario determinism accessor.\n").
-spec deterministic(scenario_outcome()) -> boolean().
deterministic(Outcome) ->
erlang:element(4, Outcome).
-file("src/lightspeed/ops/failover_harness.gleam", 107).
?DOC(" Report outcomes.\n").
-spec outcomes(report()) -> list(scenario_outcome()).
outcomes(Report) ->
erlang:element(2, Report).
-file("src/lightspeed/ops/failover_harness.gleam", 112).
?DOC(" Failed scenario count.\n").
-spec failed_scenarios(report()) -> integer().
failed_scenarios(Report) ->
erlang:element(3, Report).
-file("src/lightspeed/ops/failover_harness.gleam", 117).
?DOC(" Nondeterministic scenario count.\n").
-spec nondeterministic_failures(report()) -> integer().
nondeterministic_failures(Report) ->
erlang:element(4, Report).
-file("src/lightspeed/ops/failover_harness.gleam", 481).
-spec bool_label(boolean()) -> binary().
bool_label(Value) ->
case Value of
true ->
<<"true"/utf8>>;
false ->
<<"false"/utf8>>
end.
-file("src/lightspeed/ops/failover_harness.gleam", 122).
?DOC(" Stable report signature.\n").
-spec report_signature(report()) -> binary().
report_signature(Report) ->
Entries = gleam@list:map(
erlang:element(2, Report),
fun(Outcome) ->
<<<<<<<<<<<<(scenario_label(erlang:element(2, Outcome)))/binary,
"="/utf8>>/binary,
(pass_fail_label(Outcome))/binary>>/binary,
":deterministic="/utf8>>/binary,
(bool_label(erlang:element(4, Outcome)))/binary>>/binary,
":"/utf8>>/binary,
(erlang:element(5, Outcome))/binary>>
end
),
join_with(<<";"/utf8>>, Entries).
-file("src/lightspeed/ops/failover_harness.gleam", 138).
?DOC(" Deterministic snapshot signature for fixture drift gates.\n").
-spec snapshot_signature() -> binary().
snapshot_signature() ->
<<<<<<"m27.snapshot.v"/utf8, (erlang:integer_to_binary(1))/binary>>/binary,
"|"/utf8>>/binary,
(report_signature(run_matrix()))/binary>>.
-file("src/lightspeed/ops/failover_harness.gleam", 146).
?DOC(" Deterministic markdown report for fixture scripts.\n").
-spec snapshot_report_markdown() -> binary().
snapshot_report_markdown() ->
Report = run_matrix(),
Failed = failed_scenarios(Report),
Nondeterministic = nondeterministic_failures(Report),
Status = case (Failed =:= 0) andalso (Nondeterministic =:= 0) of
true ->
<<"OK"/utf8>>;
false ->
<<"FAIL"/utf8>>
end,
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"# Failover Fixture Report\n\n"/utf8,
"snapshot_version: "/utf8>>/binary,
(erlang:integer_to_binary(
1
))/binary>>/binary,
"\n"/utf8>>/binary,
"status: "/utf8>>/binary,
Status/binary>>/binary,
"\n"/utf8>>/binary,
"failed_scenarios: "/utf8>>/binary,
(erlang:integer_to_binary(
Failed
))/binary>>/binary,
"\n"/utf8>>/binary,
"nondeterministic_failures: "/utf8>>/binary,
(erlang:integer_to_binary(Nondeterministic))/binary>>/binary,
"\n\n"/utf8>>/binary,
"snapshot_signature: "/utf8>>/binary,
(snapshot_signature())/binary>>/binary,
"\n\n"/utf8>>/binary,
"report_signature: "/utf8>>/binary,
(report_signature(Report))/binary>>/binary,
"\n"/utf8>>.