Skip to content

Commit 32412c3

Browse files
rjmhcoot
authored andcommitted
Add IOSimPOR, an IO simulator that can explore race conditions
1 parent 86b02fc commit 32412c3

File tree

12 files changed

+3406
-675
lines changed

12 files changed

+3406
-675
lines changed

io-classes/io-classes.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ library
4343
Control.Monad.Class.MonadThrow
4444
Control.Monad.Class.MonadTime
4545
Control.Monad.Class.MonadTimer
46+
Control.Monad.Class.MonadTest
4647
default-language: Haskell2010
4748
other-extensions: CPP
4849
TypeFamilies
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Control.Monad.Class.MonadTest ( MonadTest (..) ) where
2+
3+
import Control.Monad.Reader
4+
5+
class Monad m => MonadTest m where
6+
exploreRaces :: m ()
7+
exploreRaces = return ()
8+
9+
instance MonadTest IO
10+
11+
instance MonadTest m => MonadTest (ReaderT e m) where
12+
exploreRaces = lift exploreRaces
13+

io-sim/how-to-use-IOSimPOR.txt

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
How to use Control.Monad.IOSimPOR
2+
=================================
3+
4+
IOSimPOR is a version of IOSim that supports finding race conditions, by
5+
6+
- allowing many alternative schedules to be explored, and
7+
8+
- searching through schedules in an order which (empirically) is
9+
likely to play well with shrinking; when a test case that provokes
10+
a race condition is shrunk to a smaller test case that suffers from
11+
the same race condition, then it is likely that IOSimPOR will
12+
actually provoke the race condition in the smaller test too.
13+
14+
To do so, IOSimPOR detects potential racing steps when a test is run,
15+
and then re-runs the same test many times with one or more pairs of
16+
racing steps reversed. IOSimPOR runs each equivalent schedule at most
17+
once.
18+
19+
IOSimPOR will usually not explore all possible schedules, because
20+
there are too many for this to be feasible (even infinitely many, in
21+
some cases). It prioritises races that occur earlier in a test
22+
execution, and prioritises making a small number of race reversals
23+
over making many.
24+
25+
It can test non-terminating programs with an infinite trace; in such
26+
cases it only reverse races in the part of the trace that the property
27+
under test actually depends on. Thus properties that select, for
28+
example, the first simulated 24 hours of trace events, can be tested
29+
using IOSimPOR; only races that occur in the first 24 hours will be
30+
reversed.
31+
32+
IOSimPOR assumes that execution is much faster than the passage of
33+
time, so steps that occur at different simulated times are considered
34+
not to race, and will not be reversed.
35+
36+
Exploring all possible schedules is possible with the right
37+
parameters, but not usually advisable, because it is so very very
38+
expensive. When IOSimPOR is used in a QuickCheck property, then many
39+
test cases will not even have a possibility of provoking a fault,
40+
whatever the schedule; it usually makes more sense to explore a
41+
limited number of schedules each, for a large number of cases, than to
42+
explore a small number of cases very very thoroughly.
43+
44+
Do not be surprised that tests run slowly. Each "test" is running (by
45+
default) 100 different schedules; each run is more costly than a run
46+
using vanilla IOSim, because race conditions and data dependencies
47+
need to be tracked.
48+
49+
The IOSimPOR API
50+
----------------
51+
52+
The common way to use IOSim is to call
53+
54+
runSimTrace :: forall a. (forall s. IOSim s a) -> Trace a
55+
56+
which returns a trace, and then express the property to test as a
57+
predicate on the trace. Since IOSimPOR explores many traces, then the
58+
property to test is given as a continuation instead, which is called
59+
for each trace generated. The common way to use IOSimPOR is thus
60+
61+
exploreSimTrace :: forall a test. (Testable test, Show a) =>
62+
(ExplorationOptions->ExplorationOptions) ->
63+
(forall s. IOSim s a) ->
64+
(Maybe (Trace a) -> Trace a -> test) ->
65+
Property
66+
67+
The continuation is the last parameter, with type
68+
69+
Maybe (Trace a) -> Trace a -> test
70+
71+
Here the second parameter of the continuation is the trace that the
72+
property should check. The first parameter simply provides information
73+
that may be useful for debugging---if present, it is a similar trace
74+
that passed the same test, from which the failing trace was derived by
75+
reversing one race.
76+
77+
IOSimPOR Options
78+
----------------
79+
80+
exploreSimTrace can be controlled by a number of options, which are
81+
obtained by passing the function passed as the first parameter to a
82+
default set of options. Passing the identity function id as the first
83+
parameter uses the defaults.
84+
85+
The options are:
86+
87+
- the schedule bound, default 100, set by withScheduleBound. This is
88+
an upper bound on the number of schedules with race reversals that
89+
will be explored; a bound of zero means that the default schedule
90+
will be explored, but no others. Setting the bound to zero makes
91+
IOSimPOR behave rather like IOSim, in that only one schedule is
92+
explored, but (a) IOSimPOR is considerably slower, because it still
93+
collects information on potential races, and (b) the IOSimPOR
94+
schedule is different (based on priorities, in contrast to IOSim's
95+
round-robin), and plays better with shrinking.
96+
97+
- the branching factor, default 3, set by withBranching. This is the
98+
number of alternative schedules that IOSimPOR tries to run, per
99+
race reversal. With the default parameters, IOSimPOR will try to
100+
reverse the first 33 (100 `div` 3) races discovered using the
101+
default schedule, then (if 33 or more races are discovered), for
102+
each such reversed race, will run the reversal and try to reverse
103+
two more races in the resulting schedule. A high branching factor
104+
will explore more combinations of reversing fewer races, within the
105+
overall schedule bound. A branching factor of one will explore only
106+
schedules resulting from a single race reversal (unless there are
107+
fewer races available to be reversed than the schedule bound).
108+
109+
- the step time limit in microseconds, default 100000 (100ms), set by
110+
withStepTimeLimit. A simulation step consists of pure computation
111+
followed by an IO action of some sort; we expect in most cases that
112+
the pure computation will be fast. However, a bug might cause the
113+
pure computation to fall into an infinite loop. This can be
114+
detected by applying a time limit to the overall test, for example
115+
using QuickCheck's function within, but if such a time limit is
116+
exceeded, then no information about the trace-so-far is
117+
preserved. Instead IOSimPOR can apply a time limit to each step; if
118+
the time limit is exceeded, then the trace up to that point is
119+
passed to the continuation, terminated by a TraceLoop
120+
constructor. The time limit should be large enough that the
121+
intended pure computations always finish well within that
122+
time. Note that garbage collection pauses are not included in the
123+
time limit (which would otherwise need to be considerably longer
124+
than 100ms to accomodate them), provided the -T flag is supplied to
125+
the run-time system.
126+
127+
- the schedule control to replay, default Nothing, set by
128+
withReplay. When a test fails, exploreSimTrace displays the
129+
schedule control which provoked it. The schedule control specifies
130+
how to modify the default schedule to reproduce the failure; by
131+
saving the schedule control and passing it back to exploreSimTrace
132+
for replay, then it is possible to rerun a failed test with
133+
additional diagnostics, without repeating the search for a schedule
134+
that makes it fail.
135+
136+
IOSimPOR Statistics
137+
-------------------
138+
139+
When a tested property passes, IOSimPOR displays some statistics about
140+
the schedules that were actually run. An example of this output is:
141+
142+
Branching factor (5865 in total):
143+
45.51% 1
144+
43.00% 0
145+
6.58% 2
146+
2.23% 3
147+
0.85% 4
148+
0.46% 30-39
149+
0.38% 5
150+
0.32% 6
151+
0.24% 10-19
152+
0.19% 20-29
153+
0.15% 8
154+
0.07% 7
155+
0.02% 9
156+
157+
Modified schedules explored (100 in total):
158+
22% 100-199
159+
22% 90-99
160+
18% 0
161+
7% 30-39
162+
4% 4
163+
4% 60-69
164+
4% 80-89
165+
3% 20-29
166+
3% 3
167+
3% 50-59
168+
3% 70-79
169+
2% 10-19
170+
2% 2
171+
2% 40-49
172+
1% 1
173+
174+
Race reversals per schedule (5865 in total):
175+
50.04% 1
176+
45.05% 2
177+
3.10% 3
178+
1.71% 0
179+
0.10% 4
180+
181+
The first table shows us the branching factor at each point where
182+
IOSimPOR explored alternative schedules: in this case, in 43% of cases
183+
we were at a leaf of the exploration, so no alternative schedules were
184+
explored; 45% of the time we explored one alternative schedule; in a
185+
few cases larger numbers of schedules were explored.
186+
187+
The second table shows us how many schedules we explored for each
188+
test; in this case we used the default bound of 100 alternative
189+
schedules, and we reached that bound in 22% of tests--in the other
190+
tests, there were fewer than 100 possible schedules to explore. In 18%
191+
of the tests, there were no alternative schedules to explore---since
192+
these tests were randomly generated, then these 18% were probably
193+
rather trivial tests.
194+
195+
The final table shows us how many races were reversed in each
196+
alternative schedule; in this case most alternative schedules just
197+
reversed one or two races, but we did reach a maximum of four in a
198+
small number of tests.
199+
200+
IOSimPOR Scheduling
201+
-------------------
202+
203+
IOSimPOR distinguishes between test threads and system threads; test
204+
threads are intended to be part of the test itself, whereas system
205+
threads are part of the system under test. The main thread in a test
206+
is always a test thread; test threads can fork system threads, but not
207+
vice versa. Test threads always take priority over system threads, and
208+
IOSimPOR does not attempt to reverse races involving test threads. So
209+
when writing a test, one can assume that whatever the test threads do
210+
takes place first (at each simulated time point), and then the system
211+
threads run and any races between them are explored. Likewise, if a
212+
blocked test thread responds to an event in a system thread, then the
213+
test thread's response will take place before the system threads
214+
continue execution. Distinguishing test threads from system threads
215+
drastically reduces the set of races that IOSimPOR needs to consider.
216+
217+
To start a system thread, a test thread may call
218+
219+
exploreRaces :: MonadTest m => m ()
220+
221+
All threads forked (using forkIO) after such a call will be system
222+
threads. If there is no call to exploreRaces in a test, then all the
223+
threads forked will be test threads, and IOSimPOR will not reverse any
224+
races---so it is necessary to include a call to exploreRaces somewhere
225+
in a test.
226+
227+
IOSimPOR ThreadIds contain a list of small integers; the main thread
228+
contains the list [], its children are numbered [1], [2], [3] and so
229+
on, then threads forked by [2] are numbered [2,1], [2,2], [2,3] and so
230+
on. The id of a forked thread always extends its parent's id, and
231+
successively forked children of the same thread are numbered 1, 2, 3
232+
and so on. Thread Ids give us useful information for debugging; it is
233+
easy to see which thread forked which other, and relatively easy to
234+
map thread ids in debugging output to threads in the source code.
235+
236+
Except when reversing races, IOSimPOR schedules threads using
237+
priorities. Test threads take priority over system threads, but
238+
otherwise priorities are determined by the thread ids: the thread with
239+
the lexicographically greatest thread Id has the highest priority. As
240+
a result, threads behave in a very predictable manner: newly forked
241+
threads take priority over their parent thread, and children forked
242+
later take priority over children forked earlier from the same parent
243+
thread. Knowing these priorities helps in understanding traces.
244+
245+
When IOSimPOR reverses races, this is done by using a 'schedule
246+
control'. The schedule control is displayed in the test output when a
247+
test fails. Here is an example:
248+
249+
Schedule control:
250+
ControlAwait [ScheduleMod (ThreadId [4],0)
251+
ControlDefault
252+
[(ThreadId [1],0),
253+
(ThreadId [1],1),
254+
(ThreadId [2],0),
255+
(ThreadId [2],1),
256+
(ThreadId [3],0)]]
257+
258+
ThreadId [4] delayed at time Time 0s
259+
until after:
260+
ThreadId [2]
261+
ThreadId [3]
262+
263+
The schedule control consists of one or more schedule modifications,
264+
each of which specifies that an event should be delayed until after a
265+
sequence of other events. The events consist of a thread id and a
266+
per-thread event number. The interpretation of the schedule
267+
modification above is that when the default scheduler would have
268+
performed the first event in thread [4] (that is, (ThreadId [4],0)),
269+
then it should be delayed, and the list of events in the third field
270+
of the schedule modification should be performed first. In this case
271+
there is only one schedule modification, and so only one event is
272+
delayed, but in general a schedule control may delay a series of
273+
events. A failing test can be replayed by using withReplay to supply
274+
the schedule control as an option to exploreSimTrace.
275+
276+
The final part of the output tells us how the schedule in the failing
277+
test differed from the schedule in the most similar passing test:
278+
namely, thread [4] was delayed (at simulated time 0) until after the
279+
events in threads [2] and [3]. Since the schedule control delays
280+
thread [4] until after steps in threads [1], [2] and [3], then we can
281+
conclude that the last passing test already delayed thread [4] until
282+
after the events in thread [1].
283+
284+
Potential IOSimPOR Errors
285+
-------------------------
286+
287+
Most reported errors are a result of faults in the code under test,
288+
but if you see an error of the following form:
289+
290+
Exception:
291+
Can't follow ControlFollow [(ThreadId [5],0)] []
292+
tid: ThreadId [5]
293+
tstep: 0
294+
runqueue: [ThreadId [3],ThreadId [2],ThreadId [1]]
295+
296+
then the probable cause is a bug in IOSimPOR itself. The message
297+
indicates that IOSimPOR scheduler is trying to follow a schedule
298+
modification that specifies that thread [5] should run next, but this
299+
is impossible because thread [5] is not in the runqueue (the list of
300+
runnable threads). If you supplied a schedule control explicitly,
301+
using withReplay, then you may perhaps have supplied a schedule
302+
control that does not match the version of the code you are running:
303+
in this case the exception is your fault. But if this message appears
304+
while you are exploring races, then it indicates a problem in
305+
IOSimPOR's dependency analysis: IOSimPOR has constructed a schedule as
306+
a result of race reversal that tries to run thread [5] at this point,
307+
because the dependency analysis indicates that thread [5] ought to be
308+
runnable---but it is not. Best to consult Quviq at this point.
309+

io-sim/io-sim.cabal

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: io-sim
22
version: 0.2.0.0
3-
synopsis: A pure simlator for monadic concurrency with STM
3+
synopsis: A pure simulator for monadic concurrency with STM
44
-- description:
55
license: Apache-2.0
66
license-files:
@@ -25,9 +25,13 @@ source-repository head
2525

2626
library
2727
hs-source-dirs: src
28-
exposed-modules: Data.List.Trace
29-
, Control.Monad.IOSim
30-
other-modules: Control.Monad.IOSim.Internal
28+
exposed-modules: Data.List.Trace,
29+
Control.Monad.IOSim,
30+
Control.Monad.IOSim.Types
31+
other-modules: Control.Monad.IOSim.Internal,
32+
Control.Monad.IOSimPOR.Internal,
33+
Control.Monad.IOSimPOR.QuickCheckUtils,
34+
Control.Monad.IOSimPOR.Timeout
3135
default-language: Haskell2010
3236
other-extensions: BangPatterns,
3337
CPP,
@@ -44,9 +48,13 @@ library
4448
io-classes >=0.2 && <0.3,
4549
exceptions >=0.10,
4650
containers,
51+
parallel,
52+
pretty-simple,
4753
psqueues >=0.2 && <0.3,
4854
time >=1.9.1 && <1.11,
49-
quiet
55+
quiet,
56+
QuickCheck,
57+
syb
5058

5159
ghc-options: -Wall
5260
-Wcompat
@@ -55,6 +63,7 @@ library
5563
-Wpartial-fields
5664
-Widentities
5765
-Wredundant-constraints
66+
5867
if flag(asserts)
5968
ghc-options: -fno-ignore-asserts
6069

@@ -64,12 +73,14 @@ test-suite test
6473
main-is: Main.hs
6574
other-modules: Test.IOSim
6675
Test.STM
76+
Test.Control.Monad.IOSimPOR
6777
default-language: Haskell2010
6878
build-depends: base,
6979
array,
7080
containers,
7181
io-classes,
7282
io-sim,
83+
parallel,
7384
QuickCheck,
7485
strict-stm,
7586
tasty,

0 commit comments

Comments
 (0)