This repository was archived by the owner on Mar 2, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTicketDispenser.hs
347 lines (279 loc) · 12.8 KB
/
TicketDispenser.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
module TicketDispenser where
import Control.Distributed.Process
(Process, ProcessId, expect, getSelfPid, liftIO,
send, spawnLocal)
import Control.Monad
(forM_, unless)
import Control.Monad.Reader
(ask)
import Control.Monad.Trans
(lift)
import Data.Binary
(Binary)
import GHC.Generics
(Generic)
import Prelude hiding
(readFile)
import System.Directory
(removePathForcibly)
import System.IO.Strict
(readFile)
import System.IO.Temp
(emptySystemTempFile)
import Test.QuickCheck
(Gen, Property, expectFailure, forAllShrink,
frequency)
import Text.Printf
(printf)
import Linearisability
import QuickCheckHelpers
import Scheduler
import StateMachine
------------------------------------------------------------------------
-- In this example we will implement and test a ticket dispenser; think
-- of one of those machines in the pharmacy which gives you a piece of
-- paper with your number in line on it.
--
-- This example also appears in the "Testing a Database for Race
-- Conditions with QuickCheck" and "Testing the Hard Stuff and Staying
-- Sane" papers.
-- Our ticket dispenser support two actions; you can take a ticket and
-- reset the dispenser (refill it with new paper tickets).
data Request
= TakeTicket
| Reset
deriving (Eq, Show, Generic)
instance Binary Request
-- If you take a ticket the dispenser will return you a number, and if
-- you reset it you get an acknowledgement.
data Response
= Number Int
| Ok
deriving (Eq, Show, Generic)
instance Binary Response
------------------------------------------------------------------------
-- We use a maybe integer as a model, where nothing means that the
-- dispenser hasn't been reset or filled with paper yet.
type Model = Maybe Int
initModel :: Model
initModel = Nothing
-- The transition function describes how actions advance the model. When
-- we take a ticket the counter is incremented by one, and when we reset
-- the counter is set to zero.
transition :: Model -> Either Request Response -> Model
transition m (Left req) = case req of
TakeTicket -> succ <$> m
Reset -> Just 0
transition m (Right _resp) = m
-- Pre-conditions describe in what state an action is allowed, and is
-- used to generate well-formed (or legal) programs.
precondition :: Model -> Request -> Bool
precondition Nothing TakeTicket = False
precondition (Just _) TakeTicket = True
precondition _ Reset = True
-- The assertions about our ticket dispenser are made by
-- post-conditions. Post-conditions provide a way to make sure that the
-- responses given by the implementation match the model.
postcondition :: Model -> Request -> Response -> Bool
postcondition m TakeTicket (Number i) = Just i == (succ <$> m)
postcondition _ Reset Ok = True
postcondition _ _ _ = False
-- We can also make assertions using a global invariant of the model,
-- but in the ticket dispenser case we don't need this.
invariant :: Model -> Bool
invariant _ = True
------------------------------------------------------------------------
-- To generate programs (lists of requests) we need to describe what
-- actions can occure with what frequency for each state.
generator :: Model -> Gen Request
generator Nothing = return Reset
generator (Just _) = frequency
[ (1, return Reset)
, (8, return TakeTicket)
]
-- From that the library can, using the state machine model and the
-- precondition, generate whole well-formed programs.
genRequests :: Gen [Request]
genRequests = generateRequests generator precondition transition initModel
-- The library also provides a way of shrinking whole programs given how
-- to shrink individual requests.
shrink1 :: Model -> Request -> [Request]
shrink1 _ _ = []
shrRequests :: [Request] -> [[Request]]
shrRequests = shrinkRequests shrink1 precondition transition initModel
------------------------------------------------------------------------
-- We are now done with the modelling and QuickCheck specific parts, and
-- can proceed with the actual implementation of the ticket dispenser.
-- Haskell's typed actors, distributed-processes, are used to implement
-- the programs we want to test, or as a thin layer on top of the
-- programs we want to test (which may we written without using
-- distributed-processes).
-- We don't write our programs using distributed-process' Process monad
-- directly, but instead we use a StateMachine monad which is
-- essentially a RWS (reader writer state) as described in the following
-- blog post:
--
-- http://yager.io/Distributed/Distributed.html
--
-- The reason for this is that it allows us to run the distributed
-- process in "testing mode", where we drop in a deterministic user-land
-- message scheduler in place of distributed-process' scheduler. By
-- being able to deterministically control the flow of messages between
-- our actors we can easier test for race conditions. See the paper
-- "Finding Race Conditions in Erlang with QuickCheck and PULSE" for
-- more information about deterministic scheduling.
clientP :: ProcessId -> FilePath -> Process ()
clientP pid dbFile = stateMachineProcess_ (pid, dbFile) () True clientSM
-- The True above indicates that we want to run in "testing mode". After
-- we are done with testing we can deploy the distributed process using
-- the real scheduler, i.e. passing False instead.
-- The actual implementation uses a file to store the next ticket
-- number. The reader part of the state machine monad contains the
-- process id to which we send the responses and a file path to the
-- ticket database.
clientSM :: Request -> StateMachine (ProcessId, FilePath) () Request Response ()
clientSM req = case req of
Reset -> do
(pid, db) <- ask
liftIO (reset db)
pid ! Ok
TakeTicket -> do
(pid, db) <- ask
n <- liftIO (takeTicket db)
pid ! Number n
where
reset :: FilePath -> IO ()
reset db = writeFile db (show (0 :: Int))
takeTicket :: FilePath -> IO Int
takeTicket db = do
n <- read <$> readFile db
writeFile db (show (n + 1))
return (n + 1)
-- For testing, we will set up two clients (so we can test for race
-- conditions) and a deterministic scheduler.
setup :: ProcessId -> Int -> Process (ProcessId, ProcessId, ProcessId, FilePath)
setup self seed = do
schedulerPid <- spawnLocal (schedulerP (SchedulerEnv transition invariant)
(makeSchedulerState seed initModel))
dbFile <- liftIO (emptySystemTempFile "ticket-dispenser")
client1Pid <- spawnLocal (clientP self dbFile)
client2Pid <- spawnLocal (clientP self dbFile)
mapM_ (`send` SchedulerPid schedulerPid) [client1Pid, client2Pid]
return (client1Pid, client2Pid, schedulerPid, dbFile)
-- Now we have everything we need to write QuickCheck properties.
-- We start with a sequential property, which assures that using a
-- single client the implementation matches the model.
-- We use QuickCheck's @forAllShrink@ combinator together with the
-- generator and shrinker for requests that we defined above. The
-- library provides a new combinator for writing properties involving
-- @Process@es, called @monadicProcess :: Testable a => PropertyM
-- Process a -> Property@. It's analogue to QuickCheck's @monadicIO@
-- combinator.
prop_ticketDispenser :: Property
prop_ticketDispenser =
forAllShrink genRequests shrRequests $ \reqs -> monadicProcess $ lift $ do
-- The tests themselves will have a process id, which will be the
-- used to communicate with the implementation of the ticket
-- dispenser.
self <- getSelfPid
(client1Pid, _, schedulerPid, dbFile) <- setup self 25
-- Next we tell the scheduler where to send the result, how many
-- messages it should expect, and which messages should be sent in a
-- sequential fashion (all messages will be sent sequentially in
-- this property, as we only are using one client).
send schedulerPid (SchedulerSupervisor self)
send schedulerPid (SchedulerCount (length reqs * 2))
send schedulerPid (SchedulerSequential [])
-- Send the generated requests from the tests, via the scheduler, to
-- the client.
forM_ reqs $ \req ->
send schedulerPid (SchedulerRequest self req client1Pid
:: SchedulerMessage Request Response)
-- Once all requests have been processed the scheduler will send us
-- a trace/history of the messages.
SchedulerHistory hist <- expect
-- Clean up after ourselves.
liftIO (removePathForcibly dbFile)
-- Finally we check if the model agrees with the responeses from the
-- implementation. The way this is done is by starting from the
-- initial model, for each request and response pair we advance the
-- model and check the post-condition for that pair.
unless (linearisable transition postcondition initModel hist) $
fail (printf "Can't linearise:\n%s\n"
(trace transition initModel hist))
------------------------------------------------------------------------
-- The above property passes, even though there's a bug in the
-- implementation. Have a look at how @takeTicket@ is implemented! It
-- first reads a file and then writes to it. If two of those actions
-- happen concurrently one write might overwrite the other causing a
-- race condition!
-- We shall now see how the state machine approach can catch race
-- conditions for nearly no extra work.
-- First we need to explain how to generate and shrink
-- parallel/concurrent programs. We will model concurrent programs as a
-- pair of normal programs, where the first component is a sequential
-- prefix (run on one thread) that sets some state up (in our case
-- resets the ticket dispenser). The second component is the concurrent
-- part which will be run using multiple threads.
genParallelRequests :: Gen ([Request], [Request])
genParallelRequests = generateParallelRequests generator precondition transition initModel
shrParallelRequests :: ([Request], [Request]) -> [([Request], [Request])]
shrParallelRequests = shrinkParallelRequests shrink1 precondition transition initModel
-- The concurrent/parallel property looks quite similar to the
-- sequential one, except we generate and shrink parallel programs.
prop_ticketDispenserParallel :: Property
prop_ticketDispenserParallel = expectFailure $
forAllShrink genParallelRequests shrParallelRequests $ \(prefix, suffix) -> monadicProcess $ lift $ do
self <- getSelfPid
-- First difference, is that we will use two clients this time.
(client1Pid, client2Pid, schedulerPid, dbFile) <- setup self 15
send schedulerPid (SchedulerSupervisor self)
let count = (length prefix + length suffix) * 2
send schedulerPid (SchedulerCount count)
-- We need to tell the scheduler to run the prefix sequentially.
let seqPairs = foldr (\_ ih -> (self, client1Pid) : (self, client1Pid) : ih) [] prefix
send schedulerPid (SchedulerSequential seqPairs)
-- Send the generates prefix requests from the tests, via the
-- scheduler, to the first client.
forM_ prefix $ \req ->
send schedulerPid (SchedulerRequest self req client1Pid
:: SchedulerMessage Request Response)
-- Send the generated parallel suffix from an alternation between
-- the two clients.
forM_ (zip (cycle [True, False]) suffix) $ \(client, req) ->
send schedulerPid (SchedulerRequest self req (if client then client1Pid else client2Pid)
:: SchedulerMessage Request Response)
SchedulerHistory hist <- expect
liftIO (removePathForcibly dbFile)
-- When we have a concurrent history, we try to find a possible
-- sequential interleaving of requests and responses that respects
-- the post-conditions. This technique was first described in the
-- paper "Linearizability: A Correctness Condition for Concurrent
-- Objects".
unless (linearisable transition postcondition initModel hist) $
fail (printf "Can't linearise:\n%s\n"
(trace transition initModel hist))
------------------------------------------------------------------------
-- Example output:
{-
> quickCheck prop_ticketDispenserParallel
+++ OK, failed as expected. (after 3 tests and 3 shrinks):
Exception:
user error (Can't linearise:
Nothing
==> Reset [8]
Just 0
<== Ok [8]
Just 0
==> TakeTicket [8]
Just 1
==> TakeTicket [8]
Just 2
<== Number 2 [8]
Just 2
<== Number 1 [8]
)
([Reset],[TakeTicket,TakeTicket])
-}