feat(queue): implement stream finalization
This commit is contained in:
parent
e5547005eb
commit
7d004b37da
|
@ -58,14 +58,18 @@
|
||||||
%% TODO https://emqx.atlassian.net/browse/EMQX-12575
|
%% TODO https://emqx.atlassian.net/browse/EMQX-12575
|
||||||
%% Implement some stats to assign evenly?
|
%% Implement some stats to assign evenly?
|
||||||
stream_progresses := #{
|
stream_progresses := #{
|
||||||
emqx_ds:stream() => emqx_ds:iterator()
|
emqx_ds:stream() => #{
|
||||||
|
iterator => emqx_ds:iterator(),
|
||||||
|
rank => emqx_ds:stream_rank()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
agents := #{
|
agents := #{
|
||||||
emqx_ds_shared_sub_proto:agent() => agent_state()
|
emqx_ds_shared_sub_proto:agent() => agent_state()
|
||||||
},
|
},
|
||||||
stream_owners := #{
|
stream_owners := #{
|
||||||
emqx_ds:stream() => emqx_ds_shared_sub_proto:agent()
|
emqx_ds:stream() => emqx_ds_shared_sub_proto:agent()
|
||||||
}
|
},
|
||||||
|
rank_progress := emqx_ds_shared_sub_leader_rank_progress:t()
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-export_type([
|
-export_type([
|
||||||
|
@ -139,7 +143,8 @@ init([#{topic_filter := #share{group = Group, topic = Topic}} = _Options]) ->
|
||||||
start_time => now_ms() - ?START_TIME_THRESHOLD,
|
start_time => now_ms() - ?START_TIME_THRESHOLD,
|
||||||
stream_progresses => #{},
|
stream_progresses => #{},
|
||||||
stream_owners => #{},
|
stream_owners => #{},
|
||||||
agents => #{}
|
agents => #{},
|
||||||
|
rank_progress => emqx_ds_shared_sub_leader_rank_progress:init()
|
||||||
},
|
},
|
||||||
{ok, ?leader_waiting_registration, Data}.
|
{ok, ?leader_waiting_registration, Data}.
|
||||||
|
|
||||||
|
@ -254,37 +259,87 @@ terminate(_Reason, _State, #{topic := Topic, router_id := RouterId} = _Data) ->
|
||||||
%% * Revoke streams from agents having too many streams
|
%% * Revoke streams from agents having too many streams
|
||||||
%% * Assign streams to agents having too few streams
|
%% * Assign streams to agents having too few streams
|
||||||
|
|
||||||
renew_streams(#{start_time := StartTime, stream_progresses := Progresses, topic := Topic} = Data0) ->
|
renew_streams(
|
||||||
|
#{
|
||||||
|
start_time := StartTime,
|
||||||
|
stream_progresses := Progresses,
|
||||||
|
topic := Topic,
|
||||||
|
rank_progress := RankProgress0
|
||||||
|
} = Data0
|
||||||
|
) ->
|
||||||
TopicFilter = emqx_topic:words(Topic),
|
TopicFilter = emqx_topic:words(Topic),
|
||||||
{_, Streams} = lists:unzip(
|
StreamsWRanks = emqx_ds:get_streams(?PERSISTENT_MESSAGE_DB, TopicFilter, StartTime),
|
||||||
emqx_ds:get_streams(?PERSISTENT_MESSAGE_DB, TopicFilter, StartTime)
|
|
||||||
|
%% Discard streams that are already replayed and init new
|
||||||
|
{NewStreamsWRanks, RankProgress1} = emqx_ds_shared_sub_leader_rank_progress:add_streams(
|
||||||
|
StreamsWRanks, RankProgress0
|
||||||
),
|
),
|
||||||
%% TODO https://emqx.atlassian.net/browse/EMQX-12572
|
{NewProgresses, VanishedProgresses} = update_progresses(
|
||||||
%% Handle stream removal
|
Progresses, NewStreamsWRanks, TopicFilter, StartTime
|
||||||
NewProgresses = lists:foldl(
|
),
|
||||||
fun(Stream, ProgressesAcc) ->
|
Data1 = removed_vanished_streams(Data0, VanishedProgresses),
|
||||||
case ProgressesAcc of
|
Data2 = Data1#{stream_progresses => NewProgresses, rank_progress => RankProgress1},
|
||||||
#{Stream := _} ->
|
Data3 = revoke_streams(Data2),
|
||||||
ProgressesAcc;
|
Data4 = assign_streams(Data3),
|
||||||
|
?SLOG(info, #{
|
||||||
|
msg => leader_renew_streams,
|
||||||
|
topic_filter => TopicFilter,
|
||||||
|
new_streams => length(NewStreamsWRanks)
|
||||||
|
}),
|
||||||
|
Data4.
|
||||||
|
|
||||||
|
update_progresses(Progresses, NewStreamsWRanks, TopicFilter, StartTime) ->
|
||||||
|
lists:foldl(
|
||||||
|
fun({Rank, Stream}, {NewProgressesAcc, OldProgressesAcc}) ->
|
||||||
|
case OldProgressesAcc of
|
||||||
|
#{Stream := StreamData} ->
|
||||||
|
{
|
||||||
|
NewProgressesAcc#{Stream => StreamData},
|
||||||
|
maps:remove(Stream, OldProgressesAcc)
|
||||||
|
};
|
||||||
_ ->
|
_ ->
|
||||||
{ok, It} = emqx_ds:make_iterator(
|
{ok, It} = emqx_ds:make_iterator(
|
||||||
?PERSISTENT_MESSAGE_DB, Stream, TopicFilter, StartTime
|
?PERSISTENT_MESSAGE_DB, Stream, TopicFilter, StartTime
|
||||||
),
|
),
|
||||||
ProgressesAcc#{Stream => It}
|
{NewProgressesAcc#{Stream => #{iterator => It, rank => Rank}}, OldProgressesAcc}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
Progresses,
|
{#{}, Progresses},
|
||||||
Streams
|
NewStreamsWRanks
|
||||||
|
).
|
||||||
|
|
||||||
|
%% We just remove disappeared streams from anywhere.
|
||||||
|
%%
|
||||||
|
%% If streams disappear from DS during leader being in replaying state
|
||||||
|
%% this is an abnormal situation (we should receive `end_of_stream` first),
|
||||||
|
%% but clients clients are unlikely to report any progress on them.
|
||||||
|
%%
|
||||||
|
%% If streams disappear after long leader sleep, it is a normal situation.
|
||||||
|
%% This removal will be a part of initialization before any agents connect.
|
||||||
|
removed_vanished_streams(Data0, VanishedProgresses) ->
|
||||||
|
VanishedStreams = maps:keys(VanishedProgresses),
|
||||||
|
Data1 = lists:foldl(
|
||||||
|
fun(Stream, #{stream_owners := StreamOwners0} = DataAcc) ->
|
||||||
|
case StreamOwners0 of
|
||||||
|
#{Stream := Agent} ->
|
||||||
|
#{streams := Streams0, revoked_streams := RevokedStreams0} =
|
||||||
|
AgentState0 = get_agent_state(Data0, Agent),
|
||||||
|
Streams1 = Streams0 -- [Stream],
|
||||||
|
RevokedStreams1 = RevokedStreams0 -- [Stream],
|
||||||
|
AgentState1 = AgentState0#{
|
||||||
|
streams => Streams1,
|
||||||
|
revoked_streams => RevokedStreams1
|
||||||
|
},
|
||||||
|
set_agent_state(DataAcc, Agent, AgentState1);
|
||||||
|
_ ->
|
||||||
|
DataAcc
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Data0,
|
||||||
|
VanishedStreams
|
||||||
),
|
),
|
||||||
Data1 = Data0#{stream_progresses => NewProgresses},
|
Data2 = unassign_streams(Data1, VanishedStreams),
|
||||||
?SLOG(info, #{
|
Data2.
|
||||||
msg => leader_renew_streams,
|
|
||||||
topic_filter => TopicFilter,
|
|
||||||
streams => length(Streams)
|
|
||||||
}),
|
|
||||||
Data2 = revoke_streams(Data1),
|
|
||||||
Data3 = assign_streams(Data2),
|
|
||||||
Data3.
|
|
||||||
|
|
||||||
%% We revoke streams from agents that have too many streams (> desired_stream_count_per_agent).
|
%% We revoke streams from agents that have too many streams (> desired_stream_count_per_agent).
|
||||||
%% We revoke only from replaying agents.
|
%% We revoke only from replaying agents.
|
||||||
|
@ -528,15 +583,19 @@ update_agent_stream_states(Data0, Agent, AgentStreamProgresses, Version) ->
|
||||||
Data0;
|
Data0;
|
||||||
{?waiting_replaying, AgentVersion} ->
|
{?waiting_replaying, AgentVersion} ->
|
||||||
%% Agent finished updating, now replaying
|
%% Agent finished updating, now replaying
|
||||||
Data1 = update_stream_progresses(Data0, Agent, AgentStreamProgresses),
|
{Data1, AgentState1} = update_stream_progresses(
|
||||||
AgentState1 = update_agent_timeout(AgentState0),
|
Data0, Agent, AgentState0, AgentStreamProgresses
|
||||||
AgentState2 = agent_transition_to_replaying(Agent, AgentState1),
|
),
|
||||||
set_agent_state(Data1, Agent, AgentState2);
|
AgentState2 = update_agent_timeout(AgentState1),
|
||||||
|
AgentState3 = agent_transition_to_replaying(Agent, AgentState2),
|
||||||
|
set_agent_state(Data1, Agent, AgentState3);
|
||||||
{?replaying, AgentVersion} ->
|
{?replaying, AgentVersion} ->
|
||||||
%% Common case, agent is replaying
|
%% Common case, agent is replaying
|
||||||
Data1 = update_stream_progresses(Data0, Agent, AgentStreamProgresses),
|
{Data1, AgentState1} = update_stream_progresses(
|
||||||
AgentState1 = update_agent_timeout(AgentState0),
|
Data0, Agent, AgentState0, AgentStreamProgresses
|
||||||
set_agent_state(Data1, Agent, AgentState1);
|
),
|
||||||
|
AgentState2 = update_agent_timeout(AgentState1),
|
||||||
|
set_agent_state(Data1, Agent, AgentState2);
|
||||||
{OtherState, OtherVersion} ->
|
{OtherState, OtherVersion} ->
|
||||||
?tp(warning, unexpected_update, #{
|
?tp(warning, unexpected_update, #{
|
||||||
agent => Agent,
|
agent => Agent,
|
||||||
|
@ -549,24 +608,63 @@ update_agent_stream_states(Data0, Agent, AgentStreamProgresses, Version) ->
|
||||||
end.
|
end.
|
||||||
|
|
||||||
update_stream_progresses(
|
update_stream_progresses(
|
||||||
#{stream_progresses := StreamProgresses0, stream_owners := StreamOwners} = Data,
|
#{stream_progresses := StreamProgresses0, stream_owners := StreamOwners} = Data0,
|
||||||
Agent,
|
Agent,
|
||||||
|
AgentState0,
|
||||||
ReceivedStreamProgresses
|
ReceivedStreamProgresses
|
||||||
) ->
|
) ->
|
||||||
StreamProgresses1 = lists:foldl(
|
{StreamProgresses1, ReplayedStreams} = lists:foldl(
|
||||||
fun(#{stream := Stream, iterator := It}, ProgressesAcc) ->
|
fun(#{stream := Stream, iterator := It}, {ProgressesAcc, ReplayedStreamsAcc}) ->
|
||||||
case StreamOwners of
|
case StreamOwners of
|
||||||
#{Stream := Agent} ->
|
#{Stream := Agent} ->
|
||||||
ProgressesAcc#{Stream => It};
|
StreamData0 = maps:get(Stream, ProgressesAcc),
|
||||||
|
case It of
|
||||||
|
end_of_stream ->
|
||||||
|
Rank = maps:get(rank, StreamData0),
|
||||||
|
{maps:remove(Stream, ProgressesAcc), ReplayedStreamsAcc#{Stream => Rank}};
|
||||||
_ ->
|
_ ->
|
||||||
ProgressesAcc
|
StreamData1 = StreamData0#{iterator => It},
|
||||||
|
{ProgressesAcc#{Stream => StreamData1}, ReplayedStreamsAcc}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{ProgressesAcc, ReplayedStreamsAcc}
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
StreamProgresses0,
|
{StreamProgresses0, #{}},
|
||||||
ReceivedStreamProgresses
|
ReceivedStreamProgresses
|
||||||
),
|
),
|
||||||
Data#{
|
Data1 = update_rank_progress(Data0, ReplayedStreams),
|
||||||
stream_progresses => StreamProgresses1
|
Data2 = Data1#{stream_progresses => StreamProgresses1},
|
||||||
|
AgentState1 = filter_replayed_streams(AgentState0, ReplayedStreams),
|
||||||
|
{Data2, AgentState1}.
|
||||||
|
|
||||||
|
update_rank_progress(#{rank_progress := RankProgress0} = Data, ReplayedStreams) ->
|
||||||
|
RankProgress1 = maps:fold(
|
||||||
|
fun(Stream, Rank, RankProgressAcc) ->
|
||||||
|
emqx_ds_shared_sub_leader_rank_progress:set_replayed({Rank, Stream}, RankProgressAcc)
|
||||||
|
end,
|
||||||
|
RankProgress0,
|
||||||
|
ReplayedStreams
|
||||||
|
),
|
||||||
|
Data#{rank_progress => RankProgress1}.
|
||||||
|
|
||||||
|
%% No need to revoke fully replayed streams. We do not assign them anymore.
|
||||||
|
%% The agent's session also will drop replayed streams itself.
|
||||||
|
filter_replayed_streams(
|
||||||
|
#{streams := Streams0, revoked_streams := RevokedStreams0} = AgentState0,
|
||||||
|
ReplayedStreams
|
||||||
|
) ->
|
||||||
|
Streams1 = lists:filter(
|
||||||
|
fun(Stream) -> not maps:is_key(Stream, ReplayedStreams) end,
|
||||||
|
Streams0
|
||||||
|
),
|
||||||
|
RevokedStreams1 = lists:filter(
|
||||||
|
fun(Stream) -> not maps:is_key(Stream, ReplayedStreams) end,
|
||||||
|
RevokedStreams0
|
||||||
|
),
|
||||||
|
AgentState0#{
|
||||||
|
streams => Streams1,
|
||||||
|
revoked_streams => RevokedStreams1
|
||||||
}.
|
}.
|
||||||
|
|
||||||
clean_revoked_streams(
|
clean_revoked_streams(
|
||||||
|
@ -613,41 +711,49 @@ update_agent_stream_states(Data0, Agent, AgentStreamProgresses, VersionOld, Vers
|
||||||
case {State, VersionOld, VersionNew} of
|
case {State, VersionOld, VersionNew} of
|
||||||
{?waiting_updating, AgentPrevVersion, AgentVersion} ->
|
{?waiting_updating, AgentPrevVersion, AgentVersion} ->
|
||||||
%% Client started updating
|
%% Client started updating
|
||||||
Data1 = update_stream_progresses(Data0, Agent, AgentStreamProgresses),
|
{Data1, AgentState1} = update_stream_progresses(
|
||||||
AgentState1 = update_agent_timeout(AgentState0),
|
Data0, Agent, AgentState0, AgentStreamProgresses
|
||||||
{AgentState2, Data2} = clean_revoked_streams(
|
|
||||||
Data1, Agent, AgentState1, AgentStreamProgresses
|
|
||||||
),
|
),
|
||||||
AgentState3 =
|
AgentState2 = update_agent_timeout(AgentState1),
|
||||||
case AgentState2 of
|
{AgentState3, Data2} = clean_revoked_streams(
|
||||||
|
Data1, Agent, AgentState2, AgentStreamProgresses
|
||||||
|
),
|
||||||
|
AgentState4 =
|
||||||
|
case AgentState3 of
|
||||||
#{revoked_streams := []} ->
|
#{revoked_streams := []} ->
|
||||||
agent_transition_to_waiting_replaying(Data1, Agent, AgentState2);
|
agent_transition_to_waiting_replaying(Data1, Agent, AgentState3);
|
||||||
_ ->
|
_ ->
|
||||||
agent_transition_to_updating(Agent, AgentState2)
|
agent_transition_to_updating(Agent, AgentState3)
|
||||||
end,
|
end,
|
||||||
set_agent_state(Data2, Agent, AgentState3);
|
set_agent_state(Data2, Agent, AgentState4);
|
||||||
{?updating, AgentPrevVersion, AgentVersion} ->
|
{?updating, AgentPrevVersion, AgentVersion} ->
|
||||||
Data1 = update_stream_progresses(Data0, Agent, AgentStreamProgresses),
|
{Data1, AgentState1} = update_stream_progresses(
|
||||||
AgentState1 = update_agent_timeout(AgentState0),
|
Data0, Agent, AgentState0, AgentStreamProgresses
|
||||||
{AgentState2, Data2} = clean_revoked_streams(
|
|
||||||
Data1, Agent, AgentState1, AgentStreamProgresses
|
|
||||||
),
|
),
|
||||||
AgentState3 =
|
AgentState2 = update_agent_timeout(AgentState1),
|
||||||
case AgentState2 of
|
{AgentState3, Data2} = clean_revoked_streams(
|
||||||
|
Data1, Agent, AgentState2, AgentStreamProgresses
|
||||||
|
),
|
||||||
|
AgentState4 =
|
||||||
|
case AgentState3 of
|
||||||
#{revoked_streams := []} ->
|
#{revoked_streams := []} ->
|
||||||
agent_transition_to_waiting_replaying(Data1, Agent, AgentState2);
|
agent_transition_to_waiting_replaying(Data1, Agent, AgentState3);
|
||||||
_ ->
|
_ ->
|
||||||
AgentState2
|
AgentState3
|
||||||
end,
|
end,
|
||||||
set_agent_state(Data2, Agent, AgentState3);
|
set_agent_state(Data2, Agent, AgentState4);
|
||||||
{?waiting_replaying, _, AgentVersion} ->
|
{?waiting_replaying, _, AgentVersion} ->
|
||||||
Data1 = update_stream_progresses(Data0, Agent, AgentStreamProgresses),
|
{Data1, AgentState1} = update_stream_progresses(
|
||||||
AgentState1 = update_agent_timeout(AgentState0),
|
Data0, Agent, AgentState0, AgentStreamProgresses
|
||||||
set_agent_state(Data1, Agent, AgentState1);
|
),
|
||||||
|
AgentState2 = update_agent_timeout(AgentState1),
|
||||||
|
set_agent_state(Data1, Agent, AgentState2);
|
||||||
{?replaying, _, AgentVersion} ->
|
{?replaying, _, AgentVersion} ->
|
||||||
Data1 = update_stream_progresses(Data0, Agent, AgentStreamProgresses),
|
{Data1, AgentState1} = update_stream_progresses(
|
||||||
AgentState1 = update_agent_timeout(AgentState0),
|
Data0, Agent, AgentState0, AgentStreamProgresses
|
||||||
set_agent_state(Data1, Agent, AgentState1);
|
),
|
||||||
|
AgentState2 = update_agent_timeout(AgentState1),
|
||||||
|
set_agent_state(Data1, Agent, AgentState2);
|
||||||
{OtherState, OtherVersionOld, OtherVersionNew} ->
|
{OtherState, OtherVersionOld, OtherVersionNew} ->
|
||||||
?tp(warning, unexpected_update, #{
|
?tp(warning, unexpected_update, #{
|
||||||
agent => Agent,
|
agent => Agent,
|
||||||
|
@ -798,9 +904,10 @@ desired_stream_count_per_agent(#{stream_progresses := StreamProgresses}, AgentCo
|
||||||
stream_progresses(#{stream_progresses := StreamProgresses} = _Data, Streams) ->
|
stream_progresses(#{stream_progresses := StreamProgresses} = _Data, Streams) ->
|
||||||
lists:map(
|
lists:map(
|
||||||
fun(Stream) ->
|
fun(Stream) ->
|
||||||
|
StreamData = maps:get(Stream, StreamProgresses),
|
||||||
#{
|
#{
|
||||||
stream => Stream,
|
stream => Stream,
|
||||||
iterator => maps:get(Stream, StreamProgresses)
|
iterator => maps:get(iterator, StreamData)
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
Streams
|
Streams
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_ds_shared_sub_leader_rank_progress).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-export([
|
||||||
|
init/0,
|
||||||
|
set_replayed/2,
|
||||||
|
add_streams/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% "shard"
|
||||||
|
-type rank_x() :: emqx_ds:rank_x().
|
||||||
|
|
||||||
|
%% "generation"
|
||||||
|
-type rank_y() :: emqx_ds:rank_y().
|
||||||
|
|
||||||
|
%% shard progress
|
||||||
|
-type x_progress() :: #{
|
||||||
|
%% All streams with given rank_x and rank_y =< min_y are replayed.
|
||||||
|
min_y := rank_y(),
|
||||||
|
|
||||||
|
ys := #{
|
||||||
|
rank_y() => #{
|
||||||
|
emqx_ds:stream() => _IdReplayed :: boolean()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
-type t() :: #{
|
||||||
|
rank_x() => x_progress()
|
||||||
|
}.
|
||||||
|
|
||||||
|
-spec init() -> t().
|
||||||
|
init() -> #{}.
|
||||||
|
|
||||||
|
-spec set_replayed(emqx_ds:stream_rank(), t()) -> t().
|
||||||
|
set_replayed({{RankX, RankY}, Stream}, State) ->
|
||||||
|
case State of
|
||||||
|
#{RankX := #{ys := #{RankY := #{Stream := false} = RankYStreams} = Ys0}} ->
|
||||||
|
Ys1 = Ys0#{RankY => RankYStreams#{Stream => true}},
|
||||||
|
{MinY, Ys2} = update_min_y(maps:to_list(Ys1)),
|
||||||
|
State#{RankX => #{min_y => MinY, ys => Ys2}};
|
||||||
|
_ ->
|
||||||
|
?SLOG(
|
||||||
|
warning,
|
||||||
|
leader_rank_progress_double_or_invalid_update,
|
||||||
|
#{
|
||||||
|
rank_x => RankX,
|
||||||
|
rank_y => RankY,
|
||||||
|
state => State
|
||||||
|
}
|
||||||
|
),
|
||||||
|
State
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec add_streams([{emqx_ds:stream_rank(), emqx_ds:stream()}], t()) -> false | {true, t()}.
|
||||||
|
add_streams(StreamsWithRanks, State) ->
|
||||||
|
SortedStreamsWithRanks = lists:sort(
|
||||||
|
fun({{_RankX1, RankY1}, _Stream1}, {{_RankX2, RankY2}, _Stream2}) ->
|
||||||
|
RankY1 =< RankY2
|
||||||
|
end,
|
||||||
|
StreamsWithRanks
|
||||||
|
),
|
||||||
|
lists:foldl(
|
||||||
|
fun({Rank, Stream} = StreamWithRank, {StreamAcc, StateAcc0}) ->
|
||||||
|
case add_stream({Rank, Stream}, StateAcc0) of
|
||||||
|
{true, StateAcc1} ->
|
||||||
|
{[StreamWithRank | StreamAcc], StateAcc1};
|
||||||
|
false ->
|
||||||
|
{StreamAcc, StateAcc0}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
{[], State},
|
||||||
|
SortedStreamsWithRanks
|
||||||
|
).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
add_stream({{RankX, RankY}, Stream}, State0) ->
|
||||||
|
case State0 of
|
||||||
|
#{RankX := #{min_y := MinY}} when RankY =< MinY ->
|
||||||
|
false;
|
||||||
|
#{RankX := #{ys := #{RankY := #{Stream := true}}}} ->
|
||||||
|
false;
|
||||||
|
_ ->
|
||||||
|
XProgress = maps:get(RankX, State0, #{min_y => RankY - 1, ys => #{}}),
|
||||||
|
Ys0 = maps:get(ys, XProgress),
|
||||||
|
RankYStreams0 = maps:get(RankY, Ys0, #{}),
|
||||||
|
RankYStreams1 = RankYStreams0#{Stream => false},
|
||||||
|
Ys1 = Ys0#{RankY => RankYStreams1},
|
||||||
|
State1 = State0#{RankX => XProgress#{ys => Ys1}},
|
||||||
|
{true, State1}
|
||||||
|
end.
|
||||||
|
|
||||||
|
update_min_y([{RankY, RankYStreams} | Rest] = Ys) ->
|
||||||
|
case {has_unreplayed_streams(RankYStreams), Rest} of
|
||||||
|
{true, _} ->
|
||||||
|
{RankY, maps:from_list(Ys)};
|
||||||
|
{false, []} ->
|
||||||
|
{RankY - 1, #{}};
|
||||||
|
{false, _} ->
|
||||||
|
update_min_y(Rest)
|
||||||
|
end.
|
||||||
|
|
||||||
|
has_unreplayed_streams(RankYStreams) ->
|
||||||
|
lists:any(
|
||||||
|
fun(IsReplayed) -> not IsReplayed end,
|
||||||
|
maps:values(RankYStreams)
|
||||||
|
).
|
Loading…
Reference in New Issue