Packages

Concuerror: Stateless Model Checking tool for Erlang programs

Current section

Files

Jump to
concuerror src concuerror_scheduler.erl
Raw

src/concuerror_scheduler.erl

%%% @private
%%% @doc Concuerror's scheduler component
%%%
%%% concuerror_scheduler is the main driver of interleaving
%%% exploration. A rough trace through it is the following:
%%% The entry point is `concuerror_scheduler:run/1` which takes the
%%% options and initializes the exploration, spawning the main
%%% process. There are plenty of state info that are kept in the
%%% `#scheduler_state` record the most important of which being a list
%%% of `#trace_state` records, recording events in the exploration.
%%% This list corresponds more or less to "E" in the various DPOR
%%% papers (representing the execution trace).
%%% The logic of the exploration goes through `explore_scheduling/1`,
%%% which in turn calls 'explore/1'. Both functions are fairly clean:
%%% as long as there are more processes that can be executed and yield
%%% events, `get_next_event/1` will be returning `ok`, after executing
%%% one of them and doing all necessary updates to the state (adding
%%% new `#trace_state`s, etc). If `get_next_event/1` returns `none`,
%%% we are at the end of an interleaving (either due to no more
%%% enabled processes or due to "sleep set blocking") and can do race
%%% analysis and report any errors found in the interleaving. Race
%%% analysis is contained in `plan_more_interleavings/1`, reporting
%%% whether the current interleaving was buggy is contained in
%%% `log_trace/1` and resetting most parts to continue exploration is
%%% contained in `has_more_to_explore/1`.
%%% Focusing on `plan_more_interleavings`, it is composed out of two
%%% steps: first (`assign_happens_before/3`) we assign a
%%% happens-before relation to all events to be able to detect when
%%% races are reversible or not (if two events are dependent not only
%%% directly but also via a chain of dependent events then the race is
%%% not reversible) and then (`plan_more_interleavings/3`) for each
%%% event (`more_interleavings_for_event/6`) we do an actual race
%%% analysis, adding initials or wakeup sequences in appropriate
%%% places in the list of `#trace_state`s.
-module(concuerror_scheduler).
%% User interface
-export([run/1, explain_error/1]).
-export_type([ interleaving_error/0,
interleaving_error_tag/0,
interleaving_result/0,
unique_id/0
]).
%% =============================================================================
%% DATA STRUCTURES & TYPES
%% =============================================================================
-include("concuerror.hrl").
%%------------------------------------------------------------------------------
-type interleaving_id() :: pos_integer().
-ifdef(BEFORE_OTP_17).
-type clock_map() :: dict().
-type message_event_queue() :: queue().
-else.
-type vector_clock() :: #{actor() => index()}.
-type clock_map() :: #{actor() => vector_clock()}.
-type message_event_queue() :: queue:queue(#message_event{}).
-endif.
-record(backtrack_entry, {
conservative = false :: boolean(),
event :: event(),
origin = 1 :: interleaving_id(),
wakeup_tree = [] :: event_tree()
}).
-type event_tree() :: [#backtrack_entry{}].
-type channel_actor() :: {channel(), message_event_queue()}.
-type unique_id() :: {interleaving_id(), index()}.
-record(trace_state, {
actors :: [pid() | channel_actor()],
clock_map = empty_map():: clock_map(),
done = [] :: [event()],
enabled = [] :: [pid() | channel_actor()],
index :: index(),
unique_id :: unique_id(),
previous_actor = 'none' :: 'none' | actor(),
scheduling_bound :: concuerror_options:bound(),
sleep_set = [] :: [event()],
wakeup_tree = [] :: event_tree()
}).
-type trace_state() :: #trace_state{}.
-type interleaving_result() ::
'ok' | 'sleep_set_block' | {[interleaving_error()], [event()]}.
-type interleaving_error_tag() ::
'abnormal_exit' |
'abnormal_halt' |
'deadlock' |
'depth_bound'.
-type interleaving_error() ::
{'abnormal_exit', {index(), pid(), term(), [term()]}} |
{'abnormal_halt', {index(), pid(), term()}} |
{'deadlock', [pid()]} |
{'depth_bound', concuerror_options:bound()} |
'fatal'.
-type scope() :: 'all' | [pid()].
%% DO NOT ADD A DEFAULT VALUE IF IT WILL ALWAYS BE OVERWRITTEN.
%% Default values for fields should be specified in ONLY ONE PLACE.
%% For e.g., user options this is normally in the _options module.
-record(scheduler_state, {
assertions_only :: boolean(),
assume_racing :: assume_racing_opt(),
depth_bound :: pos_integer(),
dpor :: concuerror_options:dpor(),
entry_point :: mfargs(),
estimator :: concuerror_estimator:estimator(),
first_process :: pid(),
ignore_error :: [{interleaving_error_tag(), scope()}],
interleaving_bound :: concuerror_options:bound(),
interleaving_errors :: [interleaving_error()],
interleaving_id :: interleaving_id(),
keep_going :: boolean(),
logger :: pid(),
last_scheduled :: pid(),
need_to_replay :: boolean(),
non_racing_system :: [atom()],
origin :: interleaving_id(),
print_depth :: pos_integer(),
processes :: processes(),
receive_timeout_total :: non_neg_integer(),
report_error :: [{interleaving_error_tag(), scope()}],
scheduling :: concuerror_options:scheduling(),
scheduling_bound_type :: concuerror_options:scheduling_bound_type(),
show_races :: boolean(),
strict_scheduling :: boolean(),
timeout :: timeout(),
trace :: [trace_state()],
treat_as_normal :: [atom()],
use_receive_patterns :: boolean(),
use_sleep_sets :: boolean(),
use_unsound_bpor :: boolean()
}).
%% =============================================================================
%% LOGIC (high level description of the exploration algorithm)
%% =============================================================================
-spec run(concuerror_options:options()) -> ok.
run(Options) ->
process_flag(trap_exit, true),
put(bound_exceeded, false),
FirstProcess = concuerror_callback:spawn_first_process(Options),
EntryPoint = ?opt(entry_point, Options),
Timeout = ?opt(timeout, Options),
ok =
concuerror_callback:start_first_process(FirstProcess, EntryPoint, Timeout),
SchedulingBound = ?opt(scheduling_bound, Options, infinity),
InitialTrace =
#trace_state{
actors = [FirstProcess],
enabled = [E || E <- [FirstProcess], enabled(E)],
index = 1,
scheduling_bound = SchedulingBound,
unique_id = {1, 1}
},
Logger = ?opt(logger, Options),
{SchedulingBoundType, UnsoundBPOR} =
case ?opt(scheduling_bound_type, Options) of
ubpor -> {bpor, true};
Else -> {Else, false}
end,
{IgnoreError, ReportError} =
generate_filtering_rules(Options, FirstProcess),
InitialState =
#scheduler_state{
assertions_only = ?opt(assertions_only, Options),
assume_racing = {?opt(assume_racing, Options), Logger},
depth_bound = ?opt(depth_bound, Options) + 1,
dpor = ?opt(dpor, Options),
entry_point = EntryPoint,
estimator = ?opt(estimator, Options),
first_process = FirstProcess,
ignore_error = IgnoreError,
interleaving_bound = ?opt(interleaving_bound, Options),
interleaving_errors = [],
interleaving_id = 1,
keep_going = ?opt(keep_going, Options),
last_scheduled = FirstProcess,
logger = Logger,
need_to_replay = false,
non_racing_system = ?opt(non_racing_system, Options),
origin = 1,
print_depth = ?opt(print_depth, Options),
processes = Processes = ?opt(processes, Options),
receive_timeout_total = 0,
report_error = ReportError,
scheduling = ?opt(scheduling, Options),
scheduling_bound_type = SchedulingBoundType,
show_races = ?opt(show_races, Options),
strict_scheduling = ?opt(strict_scheduling, Options),
trace = [InitialTrace],
treat_as_normal = ?opt(treat_as_normal, Options),
timeout = Timeout,
use_receive_patterns = ?opt(use_receive_patterns, Options),
use_sleep_sets = not ?opt(disable_sleep_sets, Options),
use_unsound_bpor = UnsoundBPOR
},
case SchedulingBound =:= infinity of
true ->
?unique(Logger, ?ltip, msg(scheduling_bound_tip), []);
false ->
ok
end,
concuerror_logger:plan(Logger),
?time(Logger, "Exploration start"),
Ret = explore_scheduling(InitialState),
concuerror_callback:cleanup_processes(Processes),
Ret.
%%------------------------------------------------------------------------------
explore_scheduling(State) ->
UpdatedState = explore(State),
LogState = log_trace(UpdatedState),
RacesDetectedState = plan_more_interleavings(LogState),
{HasMore, NewState} = has_more_to_explore(RacesDetectedState),
case HasMore of
true -> explore_scheduling(NewState);
false -> ok
end.
explore(State) ->
{Status, UpdatedState} =
try
get_next_event(State)
catch
C:R ->
S = [],
{{crash, C, R, S}, State}
end,
case Status of
ok -> explore(UpdatedState);
none -> UpdatedState;
{crash, Class, Reason, Stack} ->
FatalCrashState =
add_error(fatal, discard_last_trace_state(UpdatedState)),
catch log_trace(FatalCrashState),
erlang:raise(Class, Reason, Stack)
end.
%%------------------------------------------------------------------------------
log_trace(#scheduler_state{logger = Logger} = State) ->
Log =
case filter_errors(State) of
[] -> none;
Errors ->
case proplists:get_value(sleep_set_block, Errors) of
{Origin, Sleep} ->
case State#scheduler_state.dpor =:= optimal of
true -> ?crash({optimal_sleep_set_block, Origin, Sleep});
false -> ok
end,
sleep_set_block;
undefined ->
#scheduler_state{trace = Trace} = State,
Fold =
fun(#trace_state{done = [A|_], index = I}, Acc) ->
[{I, A}|Acc]
end,
TraceInfo = lists:foldl(Fold, [], Trace),
{lists:reverse(Errors), TraceInfo}
end
end,
concuerror_logger:complete(Logger, Log),
case Log =/= none andalso Log =/= sleep_set_block of
true when not State#scheduler_state.keep_going ->
?unique(Logger, ?lerror, msg(stop_first_error), []),
State#scheduler_state{trace = []};
Other ->
case Other of
true ->
?unique(Logger, ?linfo, "Continuing after error (-k)~n", []);
false ->
ok
end,
InterleavingId = State#scheduler_state.interleaving_id,
NextInterleavingId = InterleavingId + 1,
NextState =
State#scheduler_state{
interleaving_errors = [],
interleaving_id = NextInterleavingId,
receive_timeout_total = 0
},
case NextInterleavingId =< State#scheduler_state.interleaving_bound of
true -> NextState;
false ->
UniqueMsg = "Reached interleaving bound (~p)~n",
?unique(Logger, ?lwarning, UniqueMsg, [InterleavingId]),
NextState#scheduler_state{trace = []}
end
end.
%%------------------------------------------------------------------------------
generate_filtering_rules(Options, FirstProcess) ->
IgnoreErrors = ?opt(ignore_error, Options),
OnlyFirstProcessErrors = ?opt(first_process_errors_only, Options),
case OnlyFirstProcessErrors of
false -> {[{IE, all} || IE <- IgnoreErrors], []};
true ->
AllCategories = [abnormal_exit, abnormal_halt, deadlock],
Ignored = [{IE, all} || IE <- AllCategories],
Reported = [{IE, [FirstProcess]} || IE <- AllCategories -- IgnoreErrors],
{Ignored, Reported}
end.
filter_errors(State) ->
#scheduler_state{
ignore_error = Ignored,
interleaving_errors = UnfilteredErrors,
logger = Logger,
report_error = Reported
} = State,
TaggedErrors = [{true, E} || E <- UnfilteredErrors],
IgnoredErrors = update_all_tags(TaggedErrors, Ignored, false),
ReportedErrors = update_all_tags(IgnoredErrors, Reported, true),
FinalErrors = [E || {true, E} <- ReportedErrors],
case FinalErrors =/= UnfilteredErrors of
true ->
UniqueMsg = "Some errors were ignored ('--ignore_error').~n",
?unique(Logger, ?lwarning, UniqueMsg, []);
false -> ok
end,
FinalErrors.
update_all_tags([], _, _) -> [];
update_all_tags(TaggedErrors, [], _) -> TaggedErrors;
update_all_tags(TaggedErrors, Rules, Value) ->
[update_tag(E, Rules, Value) || E <- TaggedErrors].
update_tag({OldTag, Error}, Rules, NewTag) ->
RuleAppliesPred = fun(Rule) -> rule_applies(Rule, Error) end,
case lists:any(RuleAppliesPred, Rules) of
true -> {NewTag, Error};
false -> {OldTag, Error}
end.
rule_applies({Tag, Scope}, {Tag, _} = Error) ->
scope_applies(Scope, Error);
rule_applies(_, _) -> false.
scope_applies(all, _) -> true;
scope_applies(Pids, ErrorInfo) ->
case ErrorInfo of
{deadlock, Deadlocked} ->
DPids = [element(1, D) || D <- Deadlocked],
DPids -- Pids =/= DPids;
{abnormal_exit, {_, Pid, _, _}} -> lists:member(Pid, Pids);
{abnormal_halt, {_, Pid, _}} -> lists:member(Pid, Pids);
_ -> false
end.
discard_last_trace_state(State) ->
#scheduler_state{trace = [_|Trace]} = State,
State#scheduler_state{trace = Trace}.
add_error(Error, State) ->
add_errors([Error], State).
add_errors(Errors, State) ->
#scheduler_state{interleaving_errors = OldErrors} = State,
State#scheduler_state{interleaving_errors = Errors ++ OldErrors}.
%%------------------------------------------------------------------------------
get_next_event(
#scheduler_state{
depth_bound = Bound,
logger = Logger,
trace = [#trace_state{index = Bound}|_]} = State) ->
?unique(Logger, ?lwarning, msg(depth_bound_reached), []),
NewState =
add_error({depth_bound, Bound - 1}, discard_last_trace_state(State)),
{none, NewState};
get_next_event(#scheduler_state{logger = _Logger, trace = [Last|_]} = State) ->
#trace_state{index = _I, wakeup_tree = WakeupTree} = Last,
case WakeupTree of
[] ->
Event = #event{label = make_ref()},
get_next_event(Event, State);
[#backtrack_entry{event = Event, origin = N}|_] ->
?debug(
_Logger,
"New interleaving detected in ~p (diverge @ ~p)~n",
[N, _I]),
get_next_event(Event, State#scheduler_state{origin = N})
end.
get_next_event(Event, MaybeNeedsReplayState) ->
State = replay(MaybeNeedsReplayState),
#scheduler_state{trace = [Last|_]} = State,
#trace_state{actors = Actors, sleep_set = SleepSet} = Last,
SortedActors = schedule_sort(Actors, State),
#event{actor = Actor, label = Label} = Event,
case Actor =:= undefined of
true ->
AvailableActors = filter_sleep_set(SleepSet, SortedActors),
free_schedule(Event, AvailableActors, State);
false ->
#scheduler_state{print_depth = PrintDepth} = State,
#trace_state{index = I} = Last,
false = lists:member(Actor, SleepSet),
OkUpdatedEvent =
case Label =/= undefined of
true ->
NewEvent = get_next_event_backend(Event, State),
try {ok, Event} = NewEvent
catch
_:_ ->
New =
case NewEvent of
{ok, E} -> E;
_ -> NewEvent
end,
Reason = {replay_mismatch, I, Event, New, PrintDepth},
?crash(Reason)
end;
false ->
%% Last event = Previously racing event = Result may differ.
ResetEvent = reset_event(Event),
get_next_event_backend(ResetEvent, State)
end,
case OkUpdatedEvent of
{ok, UpdatedEvent} ->
update_state(UpdatedEvent, State);
retry ->
BReason = {blocked_mismatch, I, Event, PrintDepth},
?crash(BReason)
end
end.
filter_sleep_set([], AvailableActors) -> AvailableActors;
filter_sleep_set([#event{actor = Actor}|SleepSet], AvailableActors) ->
NewAvailableActors =
case ?is_channel(Actor) of
true -> lists:keydelete(Actor, 1, AvailableActors);
false -> lists:delete(Actor, AvailableActors)
end,
filter_sleep_set(SleepSet, NewAvailableActors).
schedule_sort([], _State) -> [];
schedule_sort(Actors, State) ->
#scheduler_state{
last_scheduled = LastScheduled,
scheduling = Scheduling,
strict_scheduling = StrictScheduling
} = State,
Sorted =
case Scheduling of
oldest -> Actors;
newest -> lists:reverse(Actors);
round_robin ->
Split = fun(E) -> E =/= LastScheduled end,
{Pre, Post} = lists:splitwith(Split, Actors),
Post ++ Pre
end,
case StrictScheduling of
true when Scheduling =:= round_robin ->
[LastScheduled|Rest] = Sorted,
Rest ++ [LastScheduled];
false when Scheduling =/= round_robin ->
[LastScheduled|lists:delete(LastScheduled, Sorted)];
_ -> Sorted
end.
free_schedule(Event, Actors, State) ->
#scheduler_state{
dpor = DPOR,
estimator = Estimator,
logger = Logger,
scheduling_bound_type = SchedulingBoundType,
trace = [Last|Prev]
} = State,
case DPOR =/= none of
true -> free_schedule_1(Event, Actors, State);
false ->
Enabled = [A || A <- Actors, enabled(A)],
ToBeExplored =
case SchedulingBoundType =:= delay of
false -> Enabled;
true ->
#trace_state{scheduling_bound = SchedulingBound} = Last,
?debug(Logger, "Select ~p of ~p~n", [SchedulingBound, Enabled]),
lists:sublist(Enabled, SchedulingBound + 1)
end,
case ToBeExplored < Enabled of
true -> bound_reached(Logger);
false -> ok
end,
Eventify = [maybe_prepare_channel_event(E, #event{}) || E <- ToBeExplored],
FullBacktrack = [#backtrack_entry{event = Ev} || Ev <- Eventify],
case FullBacktrack of
[] -> ok;
[_|L] ->
_ = [concuerror_logger:plan(Logger) || _ <- L],
Index = Last#trace_state.index,
_ = [concuerror_estimator:plan(Estimator, Index) || _ <- L],
ok
end,
NewLast = Last#trace_state{wakeup_tree = FullBacktrack},
NewTrace = [NewLast|Prev],
NewState = State#scheduler_state{trace = NewTrace},
free_schedule_1(Event, Actors, NewState)
end.
enabled({_,_}) -> true;
enabled(P) -> concuerror_callback:enabled(P).
free_schedule_1(Event, [Actor|_], State) when ?is_channel(Actor) ->
%% Pending messages can always be sent
PrepEvent = maybe_prepare_channel_event(Actor, Event),
{ok, FinalEvent} = get_next_event_backend(PrepEvent, State),
update_state(FinalEvent, State);
free_schedule_1(Event, [P|ActiveProcesses], State) ->
case get_next_event_backend(Event#event{actor = P}, State) of
retry -> free_schedule_1(Event, ActiveProcesses, State);
{ok, UpdatedEvent} -> update_state(UpdatedEvent, State)
end;
free_schedule_1(_Event, [], State) ->
%% Nothing to do, trace is completely explored
#scheduler_state{logger = _Logger, trace = [Last|_]} = State,
#trace_state{actors = Actors, sleep_set = SleepSet} = Last,
NewErrors =
case SleepSet =/= [] of
true ->
?debug(_Logger, "Sleep set block:~n ~p~n", [SleepSet]),
[{sleep_set_block, {State#scheduler_state.origin, SleepSet}}];
false ->
case concuerror_callback:collect_deadlock_info(Actors) of
[] -> [];
Info ->
?debug(_Logger, "Deadlock: ~p~n", [[element(1, I) || I <- Info]]),
[{deadlock, Info}]
end
end,
{none, add_errors(NewErrors, discard_last_trace_state(State))}.
maybe_prepare_channel_event(Actor, Event) ->
case ?is_channel(Actor) of
false -> Event#event{actor = Actor};
true ->
{Channel, Queue} = Actor,
MessageEvent = queue:get(Queue),
Event#event{actor = Channel, event_info = MessageEvent}
end.
reset_event(#event{actor = Actor, event_info = EventInfo}) ->
ResetEventInfo =
case ?is_channel(Actor) of
true -> EventInfo;
false -> undefined
end,
#event{
actor = Actor,
event_info = ResetEventInfo,
label = make_ref()
}.
%%------------------------------------------------------------------------------
update_state(#event{actor = Actor} = Event, State) ->
#scheduler_state{
estimator = Estimator,
logger = Logger,
scheduling_bound_type = SchedulingBoundType,
trace = [Last|Prev],
use_sleep_sets = UseSleepSets
} = State,
#trace_state{
actors = Actors,
done = RawDone,
index = Index,
previous_actor = PreviousActor,
scheduling_bound = SchedulingBound,
sleep_set = SleepSet,
unique_id = {InterleavingId, Index} = UID,
wakeup_tree = WakeupTree
} = Last,
?debug(Logger, "~s~n", [?pretty_s(Index, Event)]),
concuerror_logger:graph_new_node(Logger, UID, Index, Event),
Done = reset_receive_done(RawDone, State),
NextSleepSet =
case UseSleepSets of
true ->
AllSleepSet =
case WakeupTree of
[#backtrack_entry{conservative = true}|_] ->
concuerror_logger:plan(Logger),
concuerror_estimator:plan(Estimator, Index),
SleepSet;
_ -> ordsets:union(ordsets:from_list(Done), SleepSet)
end,
update_sleep_set(Event, AllSleepSet, State);
false -> []
end,
{NewLastWakeupTree, NextWakeupTree} =
case WakeupTree of
[] -> {[], []};
[#backtrack_entry{wakeup_tree = NWT}|Rest] -> {Rest, NWT}
end,
NewSchedulingBound =
next_bound(SchedulingBoundType, Done, PreviousActor, SchedulingBound),
?trace(Logger, " Next bound: ~p~n", [NewSchedulingBound]),
NewLastDone = [Event|Done],
NextIndex = Index + 1,
InitNextTrace =
#trace_state{
actors = Actors,
index = NextIndex,
previous_actor = Actor,
scheduling_bound = NewSchedulingBound,
sleep_set = NextSleepSet,
unique_id = {InterleavingId, NextIndex},
wakeup_tree = NextWakeupTree
},
NewLastTrace =
Last#trace_state{
done = NewLastDone,
wakeup_tree = NewLastWakeupTree
},
UpdatedSpecialNextTrace =
update_special(Event#event.special, InitNextTrace),
NextTrace =
maybe_update_enabled(SchedulingBoundType, UpdatedSpecialNextTrace),
InitNewState =
State#scheduler_state{trace = [NextTrace, NewLastTrace|Prev]},
NewState = maybe_log(Event, InitNewState, Index),
{ok, NewState}.
maybe_log(#event{actor = P} = Event, State0, Index) ->
#scheduler_state{
assertions_only = AssertionsOnly,
logger = Logger,
receive_timeout_total = ReceiveTimeoutTotal,
treat_as_normal = Normal
} = State0,
State =
case is_pid(P) of
true -> State0#scheduler_state{last_scheduled = P};
false -> State0
end,
case Event#event.event_info of
#builtin_event{mfargs = {erlang, halt, [Status|_]}}
when Status =/= 0 ->
#event{actor = Actor} = Event,
add_error({abnormal_halt, {Index, Actor, Status}}, State);
#exit_event{reason = Reason} = Exit when Reason =/= normal ->
{Tag, WasTimeout} =
case is_tuple(Reason) andalso (tuple_size(Reason) > 0) of
true ->
T = element(1, Reason),
{T, T =:= timeout};
false -> {Reason, false}
end,
case is_atom(Tag) andalso lists:member(Tag, Normal) of
true ->
?unique(Logger, ?lwarning, msg(treat_as_normal), []),
State;
false ->
case {WasTimeout, Tag} of
{true, _} -> ?unique(Logger, ?ltip, msg(timeout), []);
{_, shutdown} -> ?unique(Logger, ?ltip, msg(shutdown), []);
_ -> ok
end,
IsAssertLike =
case Tag of
{MaybeAssert, _} when is_atom(MaybeAssert) ->
case atom_to_list(MaybeAssert) of
"assert" ++ _ -> true;
_ -> false
end;
_ -> false
end,
Report =
case {IsAssertLike, AssertionsOnly} of
{false, true} ->
?unique(Logger, ?lwarning, msg(assertions_only_filter), []),
false;
{true, false} ->
?unique(Logger, ?ltip, msg(assertions_only_use), []),
true;
_ -> true
end,
case Report of
true ->
#event{actor = Actor} = Event,
Stacktrace = Exit#exit_event.stacktrace,
add_error({abnormal_exit, {Index, Actor, Reason, Stacktrace}}, State);
false -> State
end
end;
#receive_event{message = 'after'} ->
NewReceiveTimeoutTotal = ReceiveTimeoutTotal + 1,
Threshold = 50,
case NewReceiveTimeoutTotal =:= Threshold of
true ->
?unique(Logger, ?ltip, msg(maybe_receive_loop), [Threshold]);
false -> ok
end,
State#scheduler_state{receive_timeout_total = NewReceiveTimeoutTotal};
_ -> State
end.
update_sleep_set(NewEvent, SleepSet, State) ->
#scheduler_state{logger = _Logger} = State,
Pred =
fun(OldEvent) ->
V = concuerror_dependencies:dependent_safe(NewEvent, OldEvent),
?debug(_Logger, " Awaking (~p): ~s~n", [V,?pretty_s(OldEvent)]),
V =:= false
end,
lists:filter(Pred, SleepSet).
update_special(List, TraceState) when is_list(List) ->
lists:foldl(fun update_special/2, TraceState, List);
update_special(Special, #trace_state{actors = Actors} = TraceState) ->
NewActors =
case Special of
halt -> [];
{message, Message} ->
add_message(Message, Actors);
{message_delivered, MessageEvent} ->
remove_message(MessageEvent, Actors);
{message_received, _Message} ->
Actors;
{new, SpawnedPid} ->
Actors ++ [SpawnedPid];
{system_communication, _} ->
Actors
end,
TraceState#trace_state{actors = NewActors}.
add_message(MessageEvent, Actors) ->
#message_event{recipient = Recipient, sender = Sender} = MessageEvent,
Channel = {Sender, Recipient},
Update = fun(Queue) -> queue:in(MessageEvent, Queue) end,
Initial = queue:from_list([MessageEvent]),
insert_message(Channel, Update, Initial, Actors).
insert_message(Channel, Update, Initial, Actors) ->
insert_message(Channel, Update, Initial, Actors, false, []).
insert_message(Channel, _Update, Initial, [], Found, Acc) ->
case Found of
true -> lists:reverse(Acc, [{Channel, Initial}]);
false -> [{Channel, Initial}|lists:reverse(Acc)]
end;
insert_message(Channel, Update, _Initial, [{Channel, Queue}|Rest], true, Acc) ->
NewQueue = Update(Queue),
lists:reverse(Acc, [{Channel, NewQueue}|Rest]);
insert_message({From, _} = Channel, Update, Initial, [Other|Rest], Found, Acc) ->
case Other of
{{_,_},_} ->
insert_message(Channel, Update, Initial, Rest, Found, [Other|Acc]);
From ->
insert_message(Channel, Update, Initial, Rest, true, [Other|Acc]);
_ ->
case Found of
false ->
insert_message(Channel, Update, Initial, Rest, Found, [Other|Acc]);
true ->
lists:reverse(Acc, [{Channel, Initial},Other|Rest])
end
end.
remove_message(#message_event{recipient = Recipient, sender = Sender}, Actors) ->
Channel = {Sender, Recipient},
remove_message(Channel, Actors, []).
remove_message(Channel, [{Channel, Queue}|Rest], Acc) ->
NewQueue = queue:drop(Queue),
case queue:is_empty(NewQueue) of
true -> lists:reverse(Acc, Rest);
false -> lists:reverse(Acc, [{Channel, NewQueue}|Rest])
end;
remove_message(Channel, [Other|Rest], Acc) ->
remove_message(Channel, Rest, [Other|Acc]).
maybe_update_enabled(bpor, TraceState) ->
#trace_state{actors = Actors} = TraceState,
Enabled = [E || E <- Actors, enabled(E)],
TraceState#trace_state{enabled = Enabled};
maybe_update_enabled(_, TraceState) ->
TraceState.
%%------------------------------------------------------------------------------
plan_more_interleavings(#scheduler_state{dpor = none} = State) ->
#scheduler_state{logger = _Logger} = State,
?debug(_Logger, "Skipping race detection~n", []),
State;
plan_more_interleavings(State) ->
#scheduler_state{
dpor = DPOR,
logger = Logger,
trace = RevTrace,
use_receive_patterns = UseReceivePatterns
} = State,
?time(Logger, "Assigning happens-before..."),
{RE, UntimedLate} = split_trace(RevTrace),
{RevEarly, Late} =
case UseReceivePatterns of
false ->
{RE, lists:reverse(assign_happens_before(UntimedLate, RE, State))};
true ->
RevUntimedLate = lists:reverse(UntimedLate),
{ObsLate, Dict} = fix_receive_info(RevUntimedLate),
{ObsEarly, _} = fix_receive_info(RE, Dict),
case lists:reverse(ObsEarly) =:= RE of
true ->
{RE, lists:reverse(assign_happens_before(ObsLate, RE, State))};
false ->
RevHBEarly = assign_happens_before(ObsEarly, [], State),
RevHBLate = assign_happens_before(ObsLate, RevHBEarly, State),
{[], lists:reverse(RevHBLate ++ RevHBEarly)}
end
end,
?time(Logger, "Planning more interleavings..."),
NewRevTrace =
case DPOR =:= optimal of
true ->
plan_more_interleavings(lists:reverse(RevEarly, Late), [], State);
false ->
plan_more_interleavings(Late, RevEarly, State)
end,
State#scheduler_state{trace = NewRevTrace}.
%%------------------------------------------------------------------------------
split_trace(RevTrace) ->
split_trace(RevTrace, []).
split_trace([], UntimedLate) ->
{[], UntimedLate};
split_trace([#trace_state{clock_map = ClockMap} = State|RevEarlier] = RevEarly,
UntimedLate) ->
case is_empty_map(ClockMap) of
true -> split_trace(RevEarlier, [State|UntimedLate]);
false -> {RevEarly, UntimedLate}
end.
%%------------------------------------------------------------------------------
assign_happens_before(UntimedLate, RevEarly, State) ->
assign_happens_before(UntimedLate, [], RevEarly, State).
assign_happens_before([], RevLate, _RevEarly, _State) ->
RevLate;
assign_happens_before([TraceState|Later], RevLate, RevEarly, State) ->
%% We will calculate two separate clocks for each state:
%% - ops unavoidably needed to reach the state will make up the 'state' clock
%% - ops that happened before the state, will make up the 'Actor' clock
#trace_state{done = [Event|_], index = Index} = TraceState,
#scheduler_state{logger = _Logger} = State,
#event{actor = Actor, special = Special} = Event,
?debug(_Logger, "HB: ~s~n", [?pretty_s(Index, Event)]),
%% Start from the latest vector clock of the actor itself
ClockMap = get_base_clock_map(RevLate, RevEarly),
ActorLastClock = lookup_clock(Actor, ClockMap),
%% Add the step itself:
ActorNewClock = clock_store(Actor, Index, ActorLastClock),
%% And add all irreversible edges
IrreversibleClock =
add_pre_message_clocks(Special, ClockMap, ActorNewClock),
%% Apart from those, for the Actor clock we need all the ops that
%% affect the state. That is, anything dependent with the step:
HappenedBeforeClock =
update_clock(RevLate ++ RevEarly, Event, IrreversibleClock, State),
%% The 'state' clock contains the irreversible clock or
%% 'independent' if no other dependencies were found
StateClock =
case IrreversibleClock =:= HappenedBeforeClock of
true -> independent;
false -> IrreversibleClock
end,
BaseNewClockMap = map_store(state, StateClock, ClockMap),
NewClockMap = map_store(Actor, HappenedBeforeClock, BaseNewClockMap),
%% The HB clock should also be added to anything else stemming
%% from the step (spawns, sends and deliveries)
FinalClockMap =
add_new_and_messages(Special, HappenedBeforeClock, NewClockMap),
?trace(_Logger, " SC: ~w~n", [StateClock]),
?trace(_Logger, " AC: ~w~n", [HappenedBeforeClock]),
NewTraceState = TraceState#trace_state{clock_map = FinalClockMap},
assign_happens_before(Later, [NewTraceState|RevLate], RevEarly, State).
get_base_clock_map(RevLate, RevEarly) ->
case get_base_clock_map(RevLate) of
{ok, V} -> V;
none ->
case get_base_clock_map(RevEarly) of
{ok, V} -> V;
none -> empty_map()
end
end.
get_base_clock_map([#trace_state{clock_map = ClockMap}|_]) -> {ok, ClockMap};
get_base_clock_map([]) -> none.
add_pre_message_clocks([], _, Clock) -> Clock;
add_pre_message_clocks([Special|Specials], ClockMap, Clock) ->
NewClock =
case Special of
{message_delivered, #message_event{message = #message{id = Id}}} ->
max_cv(Clock, lookup_clock({Id, sent}, ClockMap));
_ -> Clock
end,
add_pre_message_clocks(Specials, ClockMap, NewClock).
add_new_and_messages([], _Clock, ClockMap) ->
ClockMap;
add_new_and_messages([Special|Rest], Clock, ClockMap) ->
NewClockMap =
case Special of
{new, SpawnedPid} ->
map_store(SpawnedPid, Clock, ClockMap);
{message, #message_event{message = #message{id = Id}}} ->
map_store({Id, sent}, Clock, ClockMap);
_ -> ClockMap
end,
add_new_and_messages(Rest, Clock, NewClockMap).
update_clock([], _Event, Clock, _State) ->
Clock;
update_clock([TraceState|Rest], Event, Clock, State) ->
#trace_state{
done = [#event{actor = EarlyActor} = EarlyEvent|_],
index = EarlyIndex
} = TraceState,
EarlyClock = lookup_clock_value(EarlyActor, Clock),
NewClock =
case EarlyIndex > EarlyClock of
false -> Clock;
true ->
#scheduler_state{assume_racing = AssumeRacing} = State,
Dependent =
concuerror_dependencies:dependent(EarlyEvent, Event, AssumeRacing),
?debug(State#scheduler_state.logger,
" ~s ~s~n",
begin
Star = fun(false) -> " "; (_) -> "->" end,
[Star(Dependent), ?pretty_s(EarlyIndex, EarlyEvent)]
end),
case Dependent =:= false of
true -> Clock;
false ->
#trace_state{clock_map = ClockMap} = TraceState,
EarlyActorClock = lookup_clock(EarlyActor, ClockMap),
max_cv(Clock, clock_store(EarlyActor, EarlyIndex, EarlyActorClock))
end
end,
update_clock(Rest, Event, NewClock, State).
%%------------------------------------------------------------------------------
plan_more_interleavings([], RevEarly, _SchedulerState) ->
RevEarly;
plan_more_interleavings([TraceState|Later], RevEarly, State) ->
case skip_planning(TraceState, State) of
true ->
plan_more_interleavings(Later, [TraceState|RevEarly], State);
false ->
#scheduler_state{logger = _Logger} = State,
#trace_state{
clock_map = ClockMap,
done = [#event{actor = Actor} = _Event|_],
index = _Index
} = TraceState,
StateClock = lookup_clock(state, ClockMap),
%% If no dependencies were found skip this altogether
case StateClock =:= independent of
true ->
plan_more_interleavings(Later, [TraceState|RevEarly], State);
false ->
?debug(_Logger, "~s~n", [?pretty_s(_Index, _Event)]),
ActorClock = lookup_clock(Actor, ClockMap),
%% Otherwise we zero-down to the latest op that happened before
LatestHBIndex = find_latest_hb_index(ActorClock, StateClock),
?trace(_Logger, " SC: ~w~n", [StateClock]),
?trace(_Logger, " AC: ~w~n", [ActorClock]),
?debug(_Logger, " Nearest race @ ~w~n", [LatestHBIndex]),
NewRevEarly =
more_interleavings_for_event(
TraceState, RevEarly, LatestHBIndex, StateClock, Later, State),
plan_more_interleavings(Later, NewRevEarly, State)
end
end.
skip_planning(TraceState, State) ->
#scheduler_state{non_racing_system = NonRacingSystem} = State,
#trace_state{done = [Event|_]} = TraceState,
#event{special = Special} = Event,
case proplists:lookup(system_communication, Special) of
{system_communication, System} -> lists:member(System, NonRacingSystem);
none -> false
end.
more_interleavings_for_event(TraceState, RevEarly, NextIndex, Clock, Later, State) ->
more_interleavings_for_event(TraceState, RevEarly, NextIndex, Clock, Later, State, []).
more_interleavings_for_event(TraceState, RevEarly, -1, _Clock, _Later,
_State, UpdEarly) ->
?trace(_State#scheduler_state.logger, " Finished checking races for event~n", []),
[TraceState|lists:reverse(UpdEarly, RevEarly)];
more_interleavings_for_event(TraceState, [], _NextIndex, _Clock, _Later,
_State, UpdEarly) ->
?trace(
_State#scheduler_state.logger,
" Finished checking races for event (NOT FAST)~n",
[]),
[TraceState|lists:reverse(UpdEarly)];
more_interleavings_for_event(TraceState, [EarlyTraceState|RevEarly], NextIndex,
Clock, Later, State, UpdEarly) ->
#trace_state{
clock_map = ClockMap,
done = [#event{actor = Actor} = Event|_]
} = TraceState,
#trace_state{
clock_map = EarlyClockMap,
done = [#event{actor = EarlyActor} = EarlyEvent|_],
index = EarlyIndex
} = EarlyTraceState,
Action =
case NextIndex =:= EarlyIndex of
false -> none;
true ->
Dependent =
case concuerror_dependencies:dependent_safe(EarlyEvent, Event) of
true -> {true, no_observer};
Other -> Other
end,
case Dependent of
false -> none;
irreversible -> update_clock;
{true, ObserverInfo} ->
?debug(State#scheduler_state.logger,
" races with ~s~n",
[?pretty_s(EarlyIndex, EarlyEvent)]),
case
update_trace(
EarlyEvent, Event, Clock, EarlyTraceState,
Later, UpdEarly, RevEarly, ObserverInfo, State)
of
skip -> update_clock;
{UpdatedNewEarly, ConservativeCandidates} ->
{update, UpdatedNewEarly, ConservativeCandidates}
end
end
end,
{NewClock, NewNextIndex} =
case Action =:= none of
true -> {Clock, NextIndex};
false ->
NC = max_cv(lookup_clock(EarlyActor, EarlyClockMap), Clock),
ActorClock = lookup_clock(Actor, ClockMap),
NI = find_latest_hb_index(ActorClock, NC),
?debug(State#scheduler_state.logger, " Next nearest race @ ~w~n", [NI]),
{NC, NI}
end,
{NewUpdEarly, NewRevEarly} =
case Action of
none -> {[EarlyTraceState|UpdEarly], RevEarly};
update_clock -> {[EarlyTraceState|UpdEarly], RevEarly};
{update, S, CC} ->
maybe_log_race(EarlyTraceState, TraceState, State),
EarlyClock = lookup_clock_value(EarlyActor, Clock),
NRE = add_conservative(RevEarly, EarlyActor, EarlyClock, CC, State),
{S, NRE}
end,
more_interleavings_for_event(TraceState, NewRevEarly, NewNextIndex, NewClock,
Later, State, NewUpdEarly).
update_trace(
EarlyEvent, Event, Clock, TraceState,
Later, NewOldTrace, Rest, ObserverInfo, State
) ->
#scheduler_state{
dpor = DPOR,
estimator = Estimator,
interleaving_id = Origin,
logger = Logger,
scheduling_bound_type = SchedulingBoundType,
use_unsound_bpor = UseUnsoundBPOR,
use_receive_patterns = UseReceivePatterns
} = State,
#trace_state{
done = [#event{actor = EarlyActor} = EarlyEvent|Done] = AllDone,
index = EarlyIndex,
previous_actor = PreviousActor,
scheduling_bound = BaseBound,
sleep_set = BaseSleepSet,
wakeup_tree = Wakeup
} = TraceState,
Bound = next_bound(SchedulingBoundType, AllDone, PreviousActor, BaseBound),
DPORInfo =
{DPOR,
case DPOR =:= persistent of
true -> Clock;
false -> {EarlyActor, EarlyIndex}
end},
RevEvent = update_context(Event, EarlyEvent),
FastSkip =
case Bound < 0 of
true ->
CI =
case SchedulingBoundType =:= bpor of
true ->
ND = not_dep(NewOldTrace, Later, DPORInfo, RevEvent),
get_initials(ND);
false ->
false
end,
{true, {over_bound, [], CI}};
false -> false
end,
{MaybeNewWakeup, VSeq, ConservativeInfo} =
case FastSkip of
{true, FastSkipReason} -> FastSkipReason;
false ->
SleepSet = BaseSleepSet ++ Done,
NotDep = not_dep(NewOldTrace, Later, DPORInfo, RevEvent),
case DPOR =:= optimal of
true ->
case UseReceivePatterns of
false ->
V = NotDep,
NW = insert_wakeup_optimal(SleepSet, Wakeup, V, Bound, Origin),
{NW, V, false};
true ->
ExtV =
case ObserverInfo =:= no_observer of
true -> NotDep;
false ->
NotObsRaw =
not_obs_raw(NewOldTrace, Later, ObserverInfo, Event),
NotObs = NotObsRaw -- NotDep,
ResetEvent = EarlyEvent#event{label = undefined},
NotDep ++ [ResetEvent] ++ NotObs
end,
RevExtV = lists:reverse(ExtV),
{V, ReceiveInfoDict} = fix_receive_info(RevExtV),
{FixedRest, _} = fix_receive_info(Rest, ReceiveInfoDict),
debug_show_sequence("v sequence", Logger, 1, V),
RevFixedRest = lists:reverse(FixedRest),
case has_weak_initial_before(RevFixedRest, V, Logger) of
true -> {skip, V, false};
false ->
NW = insert_wakeup_optimal(Done, Wakeup, V, Bound, Origin),
{NW, V, false}
end
end;
false ->
Initials = get_initials(NotDep),
V = Initials,
AddCons =
case SchedulingBoundType =:= bpor of
true -> Initials;
false -> false
end,
NW = insert_wakeup_non_optimal(SleepSet, Wakeup, V, false, Origin),
{NW, V, AddCons}
end
end,
case MaybeNewWakeup of
skip ->
?debug(Logger, " SKIP~n",[]),
skip;
over_bound ->
bound_reached(Logger),
case UseUnsoundBPOR of
true -> ok;
false -> put(bound_exceeded, true)
end,
{[TraceState|NewOldTrace], ConservativeInfo};
NewWakeup ->
debug_show_sequence("PLAN", Logger, EarlyIndex, VSeq),
NS = TraceState#trace_state{wakeup_tree = NewWakeup},
concuerror_logger:plan(Logger),
concuerror_estimator:plan(Estimator, EarlyIndex),
{[NS|NewOldTrace], ConservativeInfo}
end.
not_dep(Trace, Later, DPORInfo, RevEvent) ->
NotDep = not_dep1(Trace, Later, DPORInfo, []),
lists:reverse([RevEvent|NotDep]).
not_dep1([], [], _DPORInfo, NotDep) ->
NotDep;
not_dep1([], T, {DPOR, _} = DPORInfo, NotDep) ->
KeepLooking =
case DPOR =:= optimal of
true -> T;
false -> []
end,
not_dep1(KeepLooking, [], DPORInfo, NotDep);
not_dep1([TraceState|Rest], Later, {DPOR, Info} = DPORInfo, NotDep) ->
#trace_state{
clock_map = ClockMap,
done = [#event{actor = LaterActor} = LaterEvent|_],
index = LateIndex
} = TraceState,
NewNotDep =
case DPOR =:= persistent of
true ->
Clock = Info,
LaterActorClock = lookup_clock_value(LaterActor, Clock),
case LateIndex > LaterActorClock of
true -> NotDep;
false -> [LaterEvent|NotDep]
end;
false ->
{Actor, Index} = Info,
LaterClock = lookup_clock(LaterActor, ClockMap),
ActorLaterClock = lookup_clock_value(Actor, LaterClock),
case Index > ActorLaterClock of
false -> NotDep;
true -> [LaterEvent|NotDep]
end
end,
not_dep1(Rest, Later, DPORInfo, NewNotDep).
update_context(Event, EarlyEvent) ->
NewEventInfo =
case Event#event.event_info of
%% A receive statement...
#receive_event{message = Msg} = Info when Msg =/= 'after' ->
%% ... in race with a message.
case is_process_info_related(EarlyEvent) of
true -> Info;
false -> Info#receive_event{message = 'after'}
end;
Info -> Info
end,
%% The racing event's effect should differ, so new label.
Event#event{
event_info = NewEventInfo,
label = undefined
}.
is_process_info_related(Event) ->
case Event#event.event_info of
#builtin_event{mfargs = {erlang, process_info, _}} -> true;
_ -> false
end.
not_obs_raw(NewOldTrace, Later, ObserverInfo, Event) ->
lists:reverse(not_obs_raw(NewOldTrace, Later, ObserverInfo, Event, [])).
not_obs_raw([], [], _ObserverInfo, _Event, _NotObs) ->
[];
not_obs_raw([], Later, ObserverInfo, Event, NotObs) ->
not_obs_raw(Later, [], ObserverInfo, Event, NotObs);
not_obs_raw([TraceState|Rest], Later, ObserverInfo, Event, NotObs) ->
#trace_state{done = [#event{special = Special} = E|_]} = TraceState,
case [Id || {message_received, Id} <- Special, Id =:= ObserverInfo] =:= [] of
true ->
not_obs_raw(Rest, Later, ObserverInfo, Event, [E|NotObs]);
false ->
#event{special = NewSpecial} = Event,
ObsNewSpecial =
case lists:keyfind(message_delivered, 1, NewSpecial) of
{message_delivered, #message_event{message = #message{id = NewId}}} ->
lists:keyreplace(ObserverInfo, 2, Special, {message_received, NewId});
_ -> exit(impossible)
end,
[E#event{label = undefined, special = ObsNewSpecial}|NotObs]
end.
has_weak_initial_before([], _, _Logger) ->
?debug(_Logger, " No earlier weak initials found~n",[]),
false;
has_weak_initial_before([TraceState|Rest], V, Logger) ->
#trace_state{done = [EarlyEvent|Done]} = TraceState,
case has_initial(Done, [EarlyEvent|V]) of
true ->
?debug(
Logger,
" Has weak initial in: ~s~n",
[?join([?pretty_s(0, D) || D <- Done], "~n")]
),
debug_show_sequence("if seen as", Logger, 1, [EarlyEvent|V]),
true;
false ->
has_weak_initial_before(Rest, [EarlyEvent|V], Logger)
end.
debug_show_sequence(_Type, _Logger, _Index, _NotDep) ->
?debug(
_Logger, " ~s:~n~s",
begin
Indices = lists:seq(_Index, _Index + length(_NotDep) - 1),
IndexedNotDep = lists:zip(Indices, _NotDep),
Format = " ~s~n",
[_Type] ++
[lists:append(
[io_lib:format(Format, [?pretty_s(I,S)])
|| {I,S} <- IndexedNotDep])]
end).
maybe_log_race(EarlyTraceState, TraceState, State) ->
#scheduler_state{logger = Logger} = State,
case State#scheduler_state.show_races of
true ->
#trace_state{
done = [EarlyEvent|_],
index = EarlyIndex,
unique_id = EarlyUID
} = EarlyTraceState,
#trace_state{
done = [Event|_],
index = Index,
unique_id = UID
} = TraceState,
concuerror_logger:graph_race(Logger, EarlyUID, UID),
IndexedEarly = {EarlyIndex, EarlyEvent#event{location = []}},
IndexedLate = {Index, Event#event{location = []}},
concuerror_logger:race(Logger, IndexedEarly, IndexedLate);
false ->
?unique(Logger, ?linfo, msg(show_races), [])
end.
insert_wakeup_non_optimal(SleepSet, Wakeup, Initials, Conservative, Origin) ->
case existing(SleepSet, Initials) of
true -> skip;
false -> add_or_make_compulsory(Wakeup, Initials, Conservative, Origin)
end.
add_or_make_compulsory(Wakeup, Initials, Conservative, Origin) ->
add_or_make_compulsory(Wakeup, Initials, Conservative, Origin, []).
add_or_make_compulsory([], [E|_], Conservative, Origin, Acc) ->
Entry =
#backtrack_entry{
conservative = Conservative,
event = E, origin = Origin,
wakeup_tree = []
},
lists:reverse([Entry|Acc]);
add_or_make_compulsory([Entry|Rest], Initials, Conservative, Origin, Acc) ->
#backtrack_entry{conservative = C, event = E, wakeup_tree = []} = Entry,
#event{actor = A} = E,
Pred = fun(#event{actor = B}) -> A =:= B end,
case lists:any(Pred, Initials) of
true ->
case C andalso not Conservative of
true ->
NewEntry = Entry#backtrack_entry{conservative = false},
lists:reverse(Acc, [NewEntry|Rest]);
false -> skip
end;
false ->
NewAcc = [Entry|Acc],
add_or_make_compulsory(Rest, Initials, Conservative, Origin, NewAcc)
end.
insert_wakeup_optimal(SleepSet, Wakeup, V, Bound, Origin) ->
case has_initial(SleepSet, V) of
true -> skip;
false -> insert_wakeup(Wakeup, V, Bound, Origin)
end.
has_initial([Event|Rest], V) ->
case check_initial(Event, V) =:= false of
true -> has_initial(Rest, V);
false -> true
end;
has_initial([], _) -> false.
insert_wakeup( _, _NotDep, Bound, _Origin) when Bound < 0 ->
over_bound;
insert_wakeup( [], NotDep, _Bound, Origin) ->
backtrackify(NotDep, Origin);
insert_wakeup([Node|Rest], NotDep, Bound, Origin) ->
#backtrack_entry{event = Event, origin = M, wakeup_tree = Deeper} = Node,
case check_initial(Event, NotDep) of
false ->
NewBound =
case is_integer(Bound) of
true -> Bound - 1;
false -> Bound
end,
case insert_wakeup(Rest, NotDep, NewBound, Origin) of
Special
when
Special =:= skip;
Special =:= over_bound -> Special;
NewTree -> [Node|NewTree]
end;
NewNotDep ->
case Deeper =:= [] of
true -> skip;
false ->
case insert_wakeup(Deeper, NewNotDep, Bound, Origin) of
Special
when
Special =:= skip;
Special =:= over_bound -> Special;
NewTree ->
Entry =
#backtrack_entry{
event = Event,
origin = M,
wakeup_tree = NewTree},
[Entry|Rest]
end
end
end.
backtrackify(Seq, Cause) ->
Fold =
fun(Event, Acc) ->
[#backtrack_entry{event = Event, origin = Cause, wakeup_tree = Acc}]
end,
lists:foldr(Fold, [], Seq).
check_initial(Event, NotDep) ->
check_initial(Event, NotDep, []).
check_initial(_Event, [], Acc) ->
lists:reverse(Acc);
check_initial(Event, [E|NotDep], Acc) ->
#event{actor = EventActor} = Event,
#event{actor = EActor} = E,
case EventActor =:= EActor of
true -> lists:reverse(Acc,NotDep);
false ->
case concuerror_dependencies:dependent_safe(E, Event) =:= false of
true -> check_initial(Event, NotDep, [E|Acc]);
false -> false
end
end.
get_initials(NotDeps) ->
get_initials(NotDeps, [], []).
get_initials([], Initials, _) -> lists:reverse(Initials);
get_initials([Event|Rest], Initials, All) ->
Fold =
fun(Initial, Acc) ->
Acc andalso
concuerror_dependencies:dependent_safe(Initial, Event) =:= false
end,
NewInitials =
case lists:foldr(Fold, true, All) of
true -> [Event|Initials];
false -> Initials
end,
get_initials(Rest, NewInitials, [Event|All]).
existing([], _) -> false;
existing([#event{actor = A}|Rest], Initials) ->
Pred = fun(#event{actor = B}) -> A =:= B end,
lists:any(Pred, Initials) orelse existing(Rest, Initials).
add_conservative(Rest, _Actor, _Clock, false, _State) ->
Rest;
add_conservative(Rest, Actor, Clock, Candidates, State) ->
case add_conservative(Rest, Actor, Clock, Candidates, State, []) of
abort ->
?debug(State#scheduler_state.logger, " aborted~n",[]),
Rest;
NewRest -> NewRest
end.
add_conservative([], _Actor, _Clock, _Candidates, _State, _Acc) ->
abort;
add_conservative([TraceState|Rest], Actor, Clock, Candidates, State, Acc) ->
#scheduler_state{
interleaving_id = Origin,
logger = _Logger
} = State,
#trace_state{
done = [#event{actor = EarlyActor} = _EarlyEvent|Done],
enabled = Enabled,
index = EarlyIndex,
previous_actor = PreviousActor,
sleep_set = BaseSleepSet,
wakeup_tree = Wakeup
} = TraceState,
?debug(_Logger,
" conservative check with ~s~n",
[?pretty_s(EarlyIndex, _EarlyEvent)]),
case (EarlyActor =/= Actor) orelse (EarlyIndex < Clock) of
true -> abort;
false ->
case PreviousActor =:= Actor of
true ->
NewAcc = [TraceState|Acc],
add_conservative(Rest, Actor, Clock, Candidates, State, NewAcc);
false ->
EnabledCandidates =
[C ||
#event{actor = A} = C <- Candidates,
lists:member(A, Enabled)],
case EnabledCandidates =:= [] of
true -> abort;
false ->
SleepSet = BaseSleepSet ++ Done,
case
insert_wakeup_non_optimal(
SleepSet, Wakeup, EnabledCandidates, true, Origin
)
of
skip -> abort;
NewWakeup ->
NS = TraceState#trace_state{wakeup_tree = NewWakeup},
lists:reverse(Acc, [NS|Rest])
end
end
end
end.
%%------------------------------------------------------------------------------
has_more_to_explore(State) ->
#scheduler_state{
estimator = Estimator,
scheduling_bound_type = SchedulingBoundType,
trace = Trace
} = State,
TracePrefix = find_prefix(Trace, SchedulingBoundType),
case TracePrefix =:= [] of
true -> {false, State#scheduler_state{trace = []}};
false ->
NewState =
State#scheduler_state{need_to_replay = true, trace = TracePrefix},
[Last|_] = TracePrefix,
TopIndex = Last#trace_state.index,
concuerror_estimator:restart(Estimator, TopIndex),
{true, NewState}
end.
find_prefix([], _SchedulingBoundType) -> [];
find_prefix(Trace, SchedulingBoundType) ->
[#trace_state{wakeup_tree = Tree} = TraceState|Rest] = Trace,
case SchedulingBoundType =/= 'bpor' orelse get(bound_exceeded) of
false ->
case [B || #backtrack_entry{conservative = false} = B <- Tree] of
[] -> find_prefix(Rest, SchedulingBoundType);
WUT -> [TraceState#trace_state{wakeup_tree = WUT}|Rest]
end;
true ->
case Tree =:= [] of
true -> find_prefix(Rest, SchedulingBoundType);
false -> Trace
end
end.
replay(#scheduler_state{need_to_replay = false} = State) ->
State;
replay(State) ->
#scheduler_state{interleaving_id = N, logger = Logger, trace = Trace} = State,
[#trace_state{index = I, unique_id = Sibling} = Last|
[#trace_state{unique_id = Parent}|_] = Rest] = Trace,
concuerror_logger:graph_set_node(Logger, Parent, Sibling),
NewTrace =
[Last#trace_state{unique_id = {N, I}, clock_map = empty_map()}|Rest],
S = io_lib:format("New interleaving ~p. Replaying...", [N]),
?time(Logger, S),
NewState = replay_prefix(NewTrace, State#scheduler_state{trace = NewTrace}),
?debug(Logger, "~s~n",["Replay done."]),
NewState#scheduler_state{need_to_replay = false}.
%% =============================================================================
reset_receive_done([Event|Rest], #scheduler_state{use_receive_patterns = true}) ->
NewSpecial =
[patch_message_delivery(S, empty_map()) || S <- Event#event.special],
[Event#event{special = NewSpecial}|Rest];
reset_receive_done(Done, _) ->
Done.
fix_receive_info(RevTraceOrEvents) ->
fix_receive_info(RevTraceOrEvents, empty_map()).
fix_receive_info(RevTraceOrEvents, ReceiveInfoDict) ->
fix_receive_info(RevTraceOrEvents, ReceiveInfoDict, []).
fix_receive_info([], ReceiveInfoDict, TraceOrEvents) ->
{TraceOrEvents, ReceiveInfoDict};
fix_receive_info([#trace_state{} = TraceState|RevTrace], ReceiveInfoDict, Trace) ->
[Event|Rest] = TraceState#trace_state.done,
{[NewEvent], NewDict} = fix_receive_info([Event], ReceiveInfoDict, []),
NewTraceState = TraceState#trace_state{done = [NewEvent|Rest]},
fix_receive_info(RevTrace, NewDict, [NewTraceState|Trace]);
fix_receive_info([#event{} = Event|RevEvents], ReceiveInfoDict, Events) ->
case has_delivery_or_receive(Event#event.special) of
true ->
#event{event_info = EventInfo, special = Special} = Event,
NewReceiveInfoDict = store_receive_info(EventInfo, Special, ReceiveInfoDict),
NewSpecial = [patch_message_delivery(S, NewReceiveInfoDict) || S <- Special],
NewEventInfo =
case EventInfo of
#message_event{} ->
{_, NI} =
patch_message_delivery({message_delivered, EventInfo}, NewReceiveInfoDict),
NI;
_ -> EventInfo
end,
NewEvent = Event#event{event_info = NewEventInfo, special = NewSpecial},
fix_receive_info(RevEvents, NewReceiveInfoDict, [NewEvent|Events]);
false ->
fix_receive_info(RevEvents, ReceiveInfoDict, [Event|Events])
end.
has_delivery_or_receive([]) -> false;
has_delivery_or_receive([{M,_}|_])
when M =:= message_delivered; M =:= message_received ->
true;
has_delivery_or_receive([_|R]) -> has_delivery_or_receive(R).
store_receive_info(EventInfo, Special, ReceiveInfoDict) ->
case [ID || {message_received, ID} <- Special] of
[] -> ReceiveInfoDict;
IDs ->
ReceiveInfo =
case EventInfo of
#receive_event{receive_info = RI} -> RI;
_ -> {system, fun(_) -> true end}
end,
Fold = fun(ID,Dict) -> map_store(ID, ReceiveInfo, Dict) end,
lists:foldl(Fold, ReceiveInfoDict, IDs)
end.
patch_message_delivery({message_delivered, MessageEvent}, ReceiveInfoDict) ->
#message_event{message = #message{id = Id}} = MessageEvent,
ReceiveInfo =
case map_find(Id, ReceiveInfoDict) of
{ok, RI} -> RI;
error -> not_received
end,
{message_delivered, MessageEvent#message_event{receive_info = ReceiveInfo}};
patch_message_delivery(Other, _ReceiveInfoDict) ->
Other.
%% =============================================================================
%% ENGINE (manipulation of the Erlang processes under the scheduler)
%% =============================================================================
replay_prefix(Trace, State) ->
#scheduler_state{
entry_point = EntryPoint,
first_process = FirstProcess,
processes = Processes,
timeout = Timeout
} = State,
concuerror_callback:reset_processes(Processes),
ok =
concuerror_callback:start_first_process(FirstProcess, EntryPoint, Timeout),
NewState = State#scheduler_state{last_scheduled = FirstProcess},
replay_prefix_aux(lists:reverse(Trace), NewState).
replay_prefix_aux([_], State) ->
%% Last state has to be properly replayed.
State;
replay_prefix_aux([#trace_state{done = [Event|_], index = I}|Rest], State) ->
#scheduler_state{logger = _Logger, print_depth = PrintDepth} = State,
?debug(_Logger, "~s~n", [?pretty_s(I, Event)]),
{ok, #event{actor = Actor} = NewEvent} = get_next_event_backend(Event, State),
try
true = Event =:= NewEvent
catch
_:_ ->
#scheduler_state{print_depth = PrintDepth} = State,
?crash({replay_mismatch, I, Event, NewEvent, PrintDepth})
end,
NewLastScheduled =
case is_pid(Actor) of
true -> Actor;
false -> State#scheduler_state.last_scheduled
end,
NewState = State#scheduler_state{last_scheduled = NewLastScheduled},
replay_prefix_aux(Rest, maybe_log(Event, NewState, I)).
%% =============================================================================
%% INTERNAL INTERFACES
%% =============================================================================
%% Between scheduler and an instrumented process
%%------------------------------------------------------------------------------
get_next_event_backend(#event{actor = Channel} = Event, State)
when ?is_channel(Channel) ->
#scheduler_state{timeout = Timeout} = State,
#event{event_info = MessageEvent} = Event,
assert_no_messages(),
UpdatedEvent =
concuerror_callback:deliver_message(Event, MessageEvent, Timeout),
{ok, UpdatedEvent};
get_next_event_backend(#event{actor = Pid} = Event, State) when is_pid(Pid) ->
#scheduler_state{timeout = Timeout} = State,
assert_no_messages(),
Pid ! Event,
concuerror_callback:wait_actor_reply(Event, Timeout).
assert_no_messages() ->
receive
Msg -> error({pending_message, Msg})
after
0 -> ok
end.
%%%----------------------------------------------------------------------
%%% Helper functions
%%%----------------------------------------------------------------------
-ifdef(BEFORE_OTP_17).
empty_map() ->
dict:new().
map_store(K, V, Map) ->
dict:store(K, V, Map).
map_find(K, Map) ->
dict:find(K, Map).
is_empty_map(Map) ->
dict:size(Map) =:= 0.
lookup_clock(P, ClockMap) ->
case dict:find(P, ClockMap) of
{ok, Clock} -> Clock;
error -> clock_new()
end.
clock_new() ->
orddict:new().
clock_store(_, 0, VectorClock) ->
VectorClock;
clock_store(Actor, Index, VectorClock) ->
orddict:store(Actor, Index, VectorClock).
lookup_clock_value(Actor, VectorClock) ->
case orddict:find(Actor, VectorClock) of
{ok, Value} -> Value;
error -> 0
end.
max_cv(D1, D2) ->
Merger = fun(_Key, V1, V2) -> max(V1, V2) end,
orddict:merge(Merger, D1, D2).
find_latest_hb_index(ActorClock, StateClock) ->
%% This is the max index that is in the Actor clock but not in the
%% corresponding state clock.
Fold =
fun(K, V, Next) ->
case orddict:find(K, StateClock) =:= {ok, V} of
true -> Next;
false -> max(V, Next)
end
end,
orddict:fold(Fold, -1, ActorClock).
-else.
empty_map() ->
#{}.
map_store(K, V, Map) ->
maps:put(K, V, Map).
map_find(K, Map) ->
maps:find(K, Map).
is_empty_map(Map) ->
maps:size(Map) =:= 0.
lookup_clock(P, ClockMap) ->
maps:get(P, ClockMap, clock_new()).
clock_new() ->
#{}.
clock_store(_, 0, VectorClock) ->
VectorClock;
clock_store(Actor, Index, VectorClock) ->
maps:put(Actor, Index, VectorClock).
lookup_clock_value(Actor, VectorClock) ->
maps:get(Actor, VectorClock, 0).
max_cv(VC1, VC2) ->
ODVC1 = orddict:from_list(maps:to_list(VC1)),
ODVC2 = orddict:from_list(maps:to_list(VC2)),
Merger = fun(_Key, V1, V2) -> max(V1, V2) end,
MaxVC = orddict:merge(Merger, ODVC1, ODVC2),
maps:from_list(MaxVC).
find_latest_hb_index(ActorClock, StateClock) ->
%% This is the max index that is in the Actor clock but not in the
%% corresponding state clock.
Fold =
fun(K, V, Next) ->
case maps:find(K, StateClock) =:= {ok, V} of
true -> Next;
false -> max(V, Next)
end
end,
maps:fold(Fold, -1, ActorClock).
-endif.
next_bound(SchedulingBoundType, Done, PreviousActor, Bound) ->
case SchedulingBoundType of
none -> Bound;
bpor ->
NonPreemptExplored =
[E || #event{actor = PA} = E <- Done, PA =:= PreviousActor] =/= [],
case NonPreemptExplored of
true -> Bound - 1;
false -> Bound
end;
delay ->
%% Every reschedule costs.
Bound - length(Done)
end.
bound_reached(Logger) ->
?unique(Logger, ?lwarning, msg(scheduling_bound_warning), []),
?debug(Logger, "OVER BOUND~n",[]),
concuerror_logger:bound_reached(Logger).
%% =============================================================================
-spec explain_error(term()) -> string().
explain_error({blocked_mismatch, I, Event, Depth}) ->
EString = concuerror_io_lib:pretty_s(Event, Depth),
io_lib:format(
"On step ~p, replaying a built-in returned a different result than"
" expected:~n"
" original:~n"
" ~s~n"
" new:~n"
" blocked~n"
?notify_us_msg,
[I,EString]
);
explain_error({optimal_sleep_set_block, Origin, Who}) ->
io_lib:format(
"During a run of the optimal algorithm, the following events were left in~n"
"a sleep set (the race was detected at interleaving #~p)~n~n"
" ~p~n"
?notify_us_msg,
[Origin, Who]
);
explain_error({replay_mismatch, I, Event, NewEvent, Depth}) ->
[EString, NEString] =
[concuerror_io_lib:pretty_s(E, Depth) || E <- [Event, NewEvent]],
[Original, New] =
case EString =/= NEString of
true -> [EString, NEString];
false ->
[io_lib:format("~p",[E]) || E <- [Event, NewEvent]]
end,
io_lib:format(
"On step ~p, replaying a built-in returned a different result than"
" expected:~n"
" original:~n"
" ~s~n"
" new:~n"
" ~s~n"
?notify_us_msg,
[I,Original,New]
).
%%==============================================================================
msg(after_timeout_tip) ->
"You can use e.g. '--after_timeout 5000' to treat after timeouts that exceed"
" some threshold (here 4999ms) as 'infinity'.~n";
msg(assertions_only_filter) ->
"Only assertion failures are considered abnormal exits ('--assertions_only').~n";
msg(assertions_only_use) ->
"A process exited with reason '{{assert*,_}, _}'. If you want to see only"
" this kind of error you can use the '--assertions_only' option.~n";
msg(depth_bound_reached) ->
"An interleaving reached the depth bound. This can happen if a test has an"
" infinite execution. Concuerror is not sound for testing programs with"
" infinite executions. Consider limiting the size of the test or increasing"
" the bound ('-h depth_bound').~n";
msg(maybe_receive_loop) ->
"The trace contained more than ~w receive timeout events"
" (receive statements that executed their 'after' clause). Concuerror by"
" default treats 'after' clauses as always possible, so a 'receive loop'"
" using a timeout can lead to an infinite execution. "
++ msg(after_timeout_tip);
msg(scheduling_bound_tip) ->
"Running without a scheduling_bound corresponds to verification and"
" may take a long time.~n";
msg(scheduling_bound_warning) ->
"Some interleavings will not be explored because they exceed the scheduling"
" bound.~n";
msg(show_races) ->
"You can see pairs of racing instructions (in the report and"
" '--graph') with '--show_races true'~n";
msg(shutdown) ->
"A process exited with reason 'shutdown'. This may happen when a"
" supervisor is terminating its children. You can use '--treat_as_normal"
" shutdown' if this is expected behaviour.~n";
msg(stop_first_error) ->
"Stop testing on first error. (Check '-h keep_going').~n";
msg(timeout) ->
"A process exited with reason '{timeout, ...}'. This may happen when a"
" call to a gen_server (or similar) does not receive a reply within some"
" timeout (5000ms by default). "
++ msg(after_timeout_tip);
msg(treat_as_normal) ->
"Some abnormal exit reasons were treated as normal ('--treat_as_normal').~n".