tupelo 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1f296ad08b928805d512dc88cbdc0f5b1196fde6
4
- data.tar.gz: f25b3ac8ef8fac55f5a9affc1b33cf42409aebc6
3
+ metadata.gz: 1bd2a467d93d42b9237dc027c9ee33160c7f44a3
4
+ data.tar.gz: 16c3d47bc0acd033f1f8be8bc4578310edb3f653
5
5
  SHA512:
6
- metadata.gz: 2a88ccb3f784fc9106fb4d92c522fb26e61ae7ec0ad24fecbb805a94c4f01329ad717e035ed2f9fc8b795076aaec9c840d2601759452afb8502f61b7999ef86f
7
- data.tar.gz: 4d3662603023f504618ea0f3f8e84ef076dce52f86150634ad4d8e540a610279f3604bb5071b6bd5e16c074bf330dcd7914f70397a52397e1966dae9d55ba8f3
6
+ metadata.gz: 1f20d07a716db5de35606c9500562863c64a5f3e74129336bfb9d52221b2751385d3d2c8a6ca873bd9841c403edafcc5911df776a143bf83a743d0d257af7ce6
7
+ data.tar.gz: 40b77909cf54701fabfaf35d189dd15f9ebc8beb4bbfa8a6a55b2cc6ee1eb512eab682c5c26df375e94a939d1a018a1f463e1400ae9131412819ebd09f4c093c
data/README.md CHANGED
@@ -97,7 +97,7 @@ Getting started
97
97
 
98
98
  There is no guarantee that `x_final == x_optimistic`. The block may execute more than once.
99
99
 
100
- Take a tuple matching a template, but only if a local match exist (otherwise return nil):
100
+ Take a tuple matching a template, but only if a local match exists (otherwise return nil):
101
101
 
102
102
  take_nowait <template>
103
103
 
@@ -105,12 +105,11 @@ Getting started
105
105
  ...
106
106
  end
107
107
 
108
- Note that a local match is still not a guarantee of returning that tuple immediately -- another process may take it first, blocking this process.
109
-
108
+ Note that a local match is still not a guarantee of `x_final == x_optimistic`. Another process may take `x_optimistic` first, and the take will be re-executed.
110
109
  Perform a general transaction:
111
110
 
112
111
  result =
113
- tr do |t| # tr is alias for transaction
112
+ transaction do |t|
114
113
  rval = t.read ... # optimistic value
115
114
  t.write ...
116
115
  t.pulse ...
@@ -136,11 +135,12 @@ Getting started
136
135
 
137
136
  6. Debugging: in addition to the --info switch on all bin and example programs, bin/tspy is also really useful. There is also the similar --trace switch that is available to all bin and example programs, for example:
138
137
 
139
- tick cid status operation
140
- 1 2 batch write ["x", 1]
141
- 2 2 batch write ["y", 2]
142
- 3 3 atomic take ["x", 1], ["y", 2]
143
-
138
+ ```
139
+ tick cid status operation
140
+ 1 2 batch write ["x", 1]
141
+ 2 2 batch write ["y", 2]
142
+ 3 3 atomic take ["x", 1], ["y", 2]
143
+ ```
144
144
 
145
145
  The `Tupelo.application` command, provided by `tupelo/app`, is the source of all these options and is available to your programs.
146
146
 
@@ -155,7 +155,7 @@ See https://en.wikipedia.org/wiki/Tuple_space for general information and histor
155
155
  What is a tuple?
156
156
  ----------------
157
157
 
158
- A tuple is the unit of information in a tuplespace. It is immutable in the context of the tuplespace -- you can write a tuple into the space and you can read or take one from the space, but you cannot update a tuple within a space. A tuples does not have an identity other than the data it is made from.
158
+ A tuple is the unit of information in a tuplespace. It is immutable in the context of the tuplespace -- you can write a tuple into the space and you can read or take one from the space, but you cannot update a tuple within a space. A tuple does not have an identity other than the data it contains. A tuplespace can contain multiple copies of the same tuple. (In the ruby client, two tuples are considered the same when they are #==.)
159
159
 
160
160
  A tuple is either an array:
161
161
 
@@ -182,8 +182,6 @@ In other words, a tuple is a fairly general object, though this depends on the s
182
182
 
183
183
  It's kind of like a "JSON object", except that in the json blob case, the hash keys can only be strings. In the msgpack case, keys have no special limitations. In the case of the marshal and yaml modes, tuples can contain many other kinds of objects.
184
184
 
185
- A tuplespace can contain multiple copies of the same tuple.
186
-
187
185
  What is a template?
188
186
  -------------------
189
187
 
@@ -222,7 +220,7 @@ A template doesn't have to be a tuple pattern with wildcards, though. It can be
222
220
  read_all Array
223
221
  read_all Object
224
222
 
225
- An optional library, `tupelo/util/boolean`, provides a #match_any method to construct the boolean or of other templates:
223
+ An optional library, `tupelo/util/boolean`, provides a #match_any method to construct the boolean `or` of other templates:
226
224
 
227
225
  read_all match_any( [1,2,3], {foo: "bar"} )
228
226
 
@@ -237,7 +235,7 @@ What are the operations on tuples?
237
235
 
238
236
  * take - search the space for matching tuples, waiting if none found, removing the tuple if found
239
237
 
240
- * pulse - write and take the tuple; readers see it, but it cannot be taken (nto a classical tuplespace operation)
238
+ * pulse - write and take the tuple; readers see it, but it cannot be taken by other client, and it cannot be read later (this is not a classical tuplespace operation, but is useful for publish-subscribe communication patterns)
241
239
 
242
240
  These operations have a few variations (wait vs nowait) and options (timeouts).
243
241
 
@@ -250,9 +248,9 @@ However, it may take some time to prepare the transaction. This is true in terms
250
248
 
251
249
  Transactions are not just about batching up operations into a more efficient package (though you can do that with the #batch api). A transaction makes the combined operations execute atomically: the transaction finishes only when all of its operations can be successfully performed. Writes and pulses can always succeed, but takes and reads only succeed if the tuples exist.
252
250
 
253
- Transactions give you a means of optimistic locking: the transaction proceeds in a way that depends on preconditions. See example/increment.rb for a very simple example. Not only can you make a transaction depend on the existence of a tuple, you can make the effect of the transaction a function of existing tuples (see example/transaction-logic.rb and example/broker-optimistic.rb).
251
+ Transactions give you a means of optimistic locking: the transaction proceeds in a way that depends on preconditions. See [example/increment.rb](example/increment.rb) for a very simple example. Not only can you make a transaction depend on the existence of a tuple, you can make the effect of the transaction a function of existing tuples (see [example/transaction-logic.rb](example/transaction-logic.rb) and [example/broker-optimistic.rb](example/broker-optimistic.rb)).
254
252
 
255
- If you prefer classical tuplespace locking, you can simply use certain tuples as locks, using take/write to lock/unlock them. See the examples, such as example/broker-locking.rb. If you have a lot of contention and want to avoid the thundering herd, see example/lock-mgr-with-queue.rb.
253
+ If you prefer classical tuplespace locking, you can simply use certain tuples as locks, using take/write to lock/unlock them. See the examples, such as [example/broker-locking.rb](example/broker-locking.rb). If you have a lot of contention and want to avoid the thundering herd, see [example/lock-mgr-with-queue.rb](example/lock-mgr-with-queue.rb).
256
254
 
257
255
  If an optimistic transaction fails (for example, it is trying to take a tuple, but the tuple has just been taken by another transaction), then the transaction block is re-executed, possibly waiting for new matches to the templates. Application code must be aware of the possible re-execution of the block. This is better explained in the examples...
258
256
 
@@ -260,7 +258,7 @@ Transactions have a significant disadvantage compared to lock tuples: a transact
260
258
 
261
259
  Tupelo transactions are ACID in the following sense. They are Atomic and Isolated -- this is enforced by the transaction processing in each client. Consistency is enforced by the underlying message sequencer: each client's copy of the space is the deterministic result of the same sequence of operations. Durability is optional, but can be provided by the archiver (to be implemented) or other clients.
262
260
 
263
- On the CAP spectrum, tupelo tends towards consistency.
261
+ On the CAP spectrum, tupelo tends towards consistency: for all clients, write and take operations are applied in the same order, so the state of the entire system up through a given tick of discrete time is universally agreed upon. Of course, because of the difficulties of distributed systems, one client may not yet have seen the same range of ticks as another.
264
262
 
265
263
  Tupelo transactions do not require two-phase commit, because they are less powerful than general transactions. Each client has enough information to decide (in the same way as all other clients) whether the transaction succeeds or fails. This has performance advantages, but imposes some limitations on transactions over subspaces that are known to one client but not another.
266
264
 
@@ -268,7 +266,7 @@ Tupelo transactions do not require two-phase commit, because they are less power
268
266
  Syntax
269
267
  ======
270
268
 
271
- You can use tupelo with a simplified syntax, like a "domain-specific language". Each construct with a block can be used in either of two forms, with an explicit block param or without. Compare example/add-dsl.rb and example/add.rb.
269
+ You can use tupelo with a simplified syntax, like a "domain-specific language". Each construct with a block can be used in either of two forms, with an explicit block param or without. Compare [example/add-dsl.rb](example/add-dsl.rb) and [example/add.rb](example/add.rb).
272
270
 
273
271
 
274
272
  Advantages
@@ -290,6 +288,8 @@ Speed (latency, throughput):
290
288
 
291
289
  Can use optimal data structure for each subspace of tuplespace.
292
290
 
291
+ Decouples storage from query. (E.g. archiver for storage, optimized for just insert, delete, dump. And in-memory data structure, such as red-black tree, optimized for sorted query.)
292
+
293
293
  Each client can have its own matching agorithms and api -- matching is not part of the comm protocol, which is defined purely in terms of tuples.
294
294
 
295
295
  Data replication is easy--hard to avoid in fact.
@@ -348,7 +348,7 @@ Rinda has a severe bottleneck, though: all matching, waiting, etc. are performed
348
348
 
349
349
  Rinda is rpc-based, which is slower and also more vulnerable due to the extra client-server state; tupelo is imlemented on a message layer, rather than rpc. This also helps with pipelined writes.
350
350
 
351
- Tupelo also supports custom classes in tuples, but only with marshal / yaml; must define #==; see example/custom-class.rb
351
+ Tupelo also supports custom classes in tuples, but only with marshal / yaml; must define #==; see [example/custom-class.rb](example/custom-class.rb)
352
352
 
353
353
  Both: tuples can be arrays or hashes.
354
354
 
@@ -372,10 +372,12 @@ To compare
372
372
 
373
373
  * lmax -- minimal spof
374
374
 
375
- * datomic -- similar distribution of "facts", but not tuplespace
375
+ * datomic -- similar distribution of "facts", but not tuplespace; similar use of pluggable storage managers
376
376
 
377
377
  * job queues: sidekiq, resque, delayedjob, http://queues.io
378
378
 
379
+ * pubsubs: kafka
380
+
379
381
  Architecture
380
382
  ============
381
383
 
@@ -11,20 +11,24 @@ Tupelo.application do
11
11
  # sleep rand / 10 # reduce contention -- could also randomize inserts
12
12
  child do
13
13
  me = client_id
14
+ you = nil
14
15
  write name: me
15
16
 
16
- you = transaction do
17
+ transaction do
17
18
  game = read_nowait(
18
19
  player1: nil,
19
20
  player2: me)
20
- break game["player1"] if game
21
+
22
+ if game
23
+ you = game["player1"]
24
+ break
25
+ end
21
26
 
22
27
  take_nowait(name: me) or fail!
23
28
  you = take(name: nil)["name"]
24
29
  write(
25
30
  player1: me,
26
31
  player2: you)
27
- you
28
32
  end
29
33
 
30
34
  log "now playing with #{you}"
@@ -0,0 +1,35 @@
1
+ # more like how you would do it in redis, except that the queue is not stored in
2
+ # the central server, so operations on it are not a bottleneck, FWIW
3
+
4
+ require 'tupelo/app'
5
+
6
+ N_PLAYERS = 10
7
+
8
+ Tupelo.application do
9
+ N_PLAYERS.times do
10
+ # sleep rand / 10 # reduce contention -- could also randomize inserts
11
+ child do
12
+ me = client_id
13
+ write name: me
14
+
15
+ you = transaction do
16
+ game = read_nowait(
17
+ player1: nil,
18
+ player2: me)
19
+ break game["player1"] if game
20
+
21
+ unless take_nowait name: me
22
+ raise Tupelo::Client::TransactionFailure
23
+ end
24
+
25
+ you = take(name: nil)["name"]
26
+ write(
27
+ player1: me,
28
+ player2: you)
29
+ you
30
+ end
31
+
32
+ log "now playing with #{you}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ require 'tupelo/app'
2
+
3
+ N_WORKERS = 10
4
+ N_CLIENTS = 2
5
+
6
+ Tupelo.application do
7
+
8
+ N_WORKERS.times do |i|
9
+ child passive: true do
10
+ log.progname = "worker #{i}"
11
+ worker_delay = rand 1.0..3.0 # how long it takes this worker to do a task
12
+
13
+ loop do
14
+ _, req_id, req_dat = take ["request", Integer, nil]
15
+ # we could modify this client's tuplespace storage so that
16
+ # the requst are taken in req_id order
17
+
18
+ log.info "handling request #{req_id} for #{req_dat.inspect}"
19
+ sleep worker_delay
20
+ write ["response", req_id]
21
+ log.info "handled request #{req_id} for #{req_dat.inspect}"
22
+ end
23
+ end
24
+ end
25
+
26
+ N_CLIENTS.times do |i|
27
+ child do
28
+ log.progname = "client #{i}"
29
+
30
+ req_data = 1..20 # task spec here
31
+
32
+ req_data.each do |req_dat|
33
+ req_id = nil
34
+ transaction do
35
+ # grouping the following ops in a transaction is not necessary for
36
+ # correctness, but it does reduce latency
37
+ _, req_id = take ["next_req_id", Integer]
38
+ write ["next_req_id", req_id + 1]
39
+ write ["request", req_id, req_dat]
40
+ end
41
+
42
+ take ["response", req_id]
43
+ log "got response for request #{req_id}"
44
+ end
45
+ end
46
+ end
47
+
48
+ local do
49
+ write ["next_req_id", 0]
50
+ end
51
+ end
@@ -0,0 +1,59 @@
1
+ # Asynchronously deliver messages via a shared "data bus" aka "data hub".
2
+ # A message is written to a channel. It remains there until a new value is
3
+ # written to the channel. A reader can pick it up at any time. Compare
4
+ # pubsub.rb.
5
+ #
6
+ # Run with the --show-final-state switch to verify that the last tuple
7
+ # written to a channel does stay there.
8
+
9
+ require 'tupelo/app'
10
+
11
+ show_final_state = ARGV.delete "--show-final-state"
12
+
13
+ N_PUBS = 6
14
+ N_SUBS = 6
15
+ N_CHAN = 3
16
+
17
+ Tupelo.application do
18
+ channels = (0...N_CHAN).map {|i| "channel #{i}"}
19
+
20
+ N_PUBS.times do |pi|
21
+ child do
22
+ read ['start']
23
+ delay = pi
24
+ sleep delay
25
+ ch = channels[ pi % channels.size ]
26
+ transaction do
27
+ take_nowait [ch, nil]
28
+ write [ch, "pub #{pi} slept for #{delay} sec"]
29
+ end
30
+ end
31
+ end
32
+
33
+ N_SUBS.times do |si|
34
+ child passive: true do # passive means process will exit when pubs exit
35
+ log.progname = "sub #{si}"
36
+ ch = channels[ si % channels.size ]
37
+ loop do
38
+ sleep 1
39
+ t = read_nowait [ch, nil]
40
+ log t if t
41
+ end
42
+ end
43
+ end
44
+
45
+ if show_final_state
46
+ child passive: true do
47
+ log.progname = "final"
48
+ def self.stop
49
+ log read_all
50
+ super
51
+ end
52
+ sleep
53
+ end
54
+ end
55
+
56
+ local do
57
+ write ['start']
58
+ end
59
+ end
@@ -0,0 +1 @@
1
+ # like gnu parallel
data/example/pubsub.rb ADDED
@@ -0,0 +1,53 @@
1
+ # Synchronously deliver published messages to subscribers. Subscriber gets the
2
+ # message only if waiting while the message is sent.
3
+ # Compare message-bus.rb.
4
+ #
5
+ # Run with the --show-final-state switch to verify that published tuples
6
+ # don't stay in the space.
7
+
8
+ require 'tupelo/app'
9
+
10
+ show_final_state = ARGV.delete "--show-final-state"
11
+
12
+ N_PUBS = 6
13
+ N_SUBS = 6
14
+ N_CHAN = 3
15
+
16
+ Tupelo.application do
17
+ channels = (0...N_CHAN).map {|i| "channel #{i}"}
18
+
19
+ N_PUBS.times do |pi|
20
+ child do
21
+ read ['start']
22
+ delay = pi/10.0
23
+ sleep delay
24
+ ch = channels[ pi % channels.size ]
25
+ pulse [ch, "pub #{pi} slept for #{delay} sec"]
26
+ end
27
+ end
28
+
29
+ N_SUBS.times do |si|
30
+ child passive: true do # passive means process will exit when pubs exit
31
+ log.progname = "sub #{si}"
32
+ ch = channels[ si % channels.size ]
33
+ loop do
34
+ log read [ch, nil]
35
+ end
36
+ end
37
+ end
38
+
39
+ if show_final_state
40
+ child passive: true do
41
+ log.progname = "final"
42
+ def self.stop
43
+ log read_all
44
+ super
45
+ end
46
+ sleep
47
+ end
48
+ end
49
+
50
+ local do
51
+ write ['start']
52
+ end
53
+ end
data/example/pulse.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # See pubsub.rb for a more interesting use of pulse.
2
+
1
3
  require 'tupelo/app'
2
4
 
3
5
  Tupelo.application do |app|
data/example/remote.rb ADDED
@@ -0,0 +1,28 @@
1
+ # simple use of remote to start process on remote host and connect it to
2
+ # the tupelo app
3
+
4
+ require 'tupelo/app/remote'
5
+
6
+ host = ARGV.shift or abort "usage: #$0 <ssh-hostname>"
7
+
8
+ Tupelo.tcp_application do
9
+ remote host: host do
10
+ write host: `hostname`.chomp, mode: "drb", client: client_id
11
+ # this actually returns local hostname, because the block executes
12
+ # locally -- only the tupelo ops are remote. So this mode is really
13
+ # only for examples and tests, not production.
14
+ end
15
+
16
+ remote host: host, eval: %{
17
+ write host: `hostname`.chomp, mode: "eval", client: client_id
18
+ }
19
+ # rather than embed large chunks of code in the string, it's better to
20
+ # load or require a file and pass self (which is a Client instance) to
21
+ # a method in that file.
22
+
23
+ local do
24
+ 2.times do
25
+ log take host: nil, mode: nil, client: nil
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # This is a simple way to take multiple tuples at once.
2
+
3
+ require 'tupelo/app'
4
+
5
+ Tupelo.application do
6
+ child do
7
+ result = transaction { (1..3).map {|i| take [i]} }
8
+ log result
9
+ end
10
+
11
+ child do
12
+ write [2], [1], [3]
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # Run this with --trace to see that, even in the FAIL case, take_nowait never
2
+ # hangs waiting for a match. The "ready" tuple is just to keep the take
3
+ # requests fairly close in time, increasing the chance of transaction failure.
4
+
5
+ require 'tupelo/app'
6
+
7
+ Tupelo.application do
8
+ 20.times do
9
+ child do
10
+ read ["ready"]
11
+ r = take_nowait [1]
12
+ log "winner! result = #{r.inspect}" if r
13
+ end
14
+ end
15
+
16
+ local do
17
+ write [1]
18
+ write ["ready"]
19
+ end
20
+ end
@@ -0,0 +1,68 @@
1
+ require 'tupelo/app'
2
+
3
+ module Tupelo
4
+ class AppBuilder
5
+ # Perform client operations on another host.
6
+ #
7
+ # There are two modes: drb and eval. In the drb mode, a block executes
8
+ # locally, performing tuplespace operations by proxy to the remote
9
+ # tupelo client instance. Results of the ops are available in the local
10
+ # process. This is very useful for testing and examples.
11
+ #
12
+ # In the eval mode, a string is evaluated on the remote host in the context
13
+ # of a client instance.
14
+ #
15
+ # There are some minor limitations compared to #child:
16
+ #
17
+ # In eval mode, the code string is always treated as in the arity 0 case
18
+ # of #child, in other words "DSL mode", in other words the self is the
19
+ # client.
20
+ #
21
+ # Unlike #child, there is no mode that returns a Client instance.
22
+ #
23
+ def remote client_class = Client,
24
+ client_lib: 'tupelo/client', host: nil, **opts
25
+ require 'easy-serve/remote'
26
+ raise if opts[:passive] ## not supported yet
27
+ ## detach option so that remote process doesn't keep ssh connection
28
+ snames = :seqd, :cseqd, :arcd
29
+
30
+ if opts[:eval]
31
+ ez.remote *snames, host: host, **opts, eval: %{
32
+ require #{client_lib.inspect}
33
+
34
+ seqd, cseqd, arcd = *conns
35
+ client_class = Object.const_get(#{client_class.name.inspect})
36
+
37
+ begin
38
+ log.progname = "client <starting in \#{log.progname}>"
39
+ client = client_class.new(
40
+ seq: seqd, cseq: cseqd, arc: arcd, log: log)
41
+ client.start do
42
+ log.progname = "client \#{client.client_id}"
43
+ end
44
+ client.instance_eval #{opts[:eval].inspect}
45
+ ensure
46
+ client.stop if client
47
+ end
48
+ }
49
+
50
+ elsif block_given?
51
+ block = Proc.new
52
+ ez.remote *snames, host: host, **opts do |seqd, cseqd, arcd|
53
+ run_client client_class,
54
+ seq: seqd, cseq: cseqd, arc: arcd, log: log do |client|
55
+ if block.arity == 0
56
+ client.instance_eval &block
57
+ else
58
+ yield client
59
+ end
60
+ end
61
+ end
62
+
63
+ else
64
+ raise ArgumentError, "cannot select remote mode based on arguments"
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/tupelo/app.rb CHANGED
@@ -75,6 +75,16 @@ module Tupelo
75
75
  end
76
76
  end
77
77
 
78
+ # same as application, but with tcp sockets the default
79
+ def self.tcp_application argv: ARGV,
80
+ servers_file: nil, blob_type: nil,
81
+ seqd_addr: [:tcp, nil, 0],
82
+ cseqd_addr: [:tcp, nil, 0],
83
+ arcd_addr: [:tcp, nil, 0], &block
84
+ application argv: ARGV, servers_file: servers_file, blob_type: blob_type,
85
+ seqd_addr: seqd_addr, cseqd_addr: cseqd_addr, arcd_addr: arcd_addr, &block
86
+ end
87
+
78
88
  #blob_type: 'msgpack' # the default
79
89
  #blob_type: 'marshal' # if you need to pass general ruby objects
80
90
  #blob_type: 'yaml' # less general ruby objects, but cross-language
@@ -1,3 +1,3 @@
1
1
  module Tupelo
2
- VERSION = "0.5"
2
+ VERSION = "0.6"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tupelo
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.5'
4
+ version: '0.6'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel VanderWerf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-07-31 00:00:00.000000000 Z
11
+ date: 2013-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: atdo
@@ -79,6 +79,7 @@ files:
79
79
  - README.md
80
80
  - COPYING
81
81
  - Rakefile
82
+ - lib/tupelo/app/remote.rb
82
83
  - lib/tupelo/app/irb-shell.rb
83
84
  - lib/tupelo/app/trace.rb
84
85
  - lib/tupelo/client.rb
@@ -96,9 +97,11 @@ files:
96
97
  - bench/pipeline.rb
97
98
  - bugs/write-read.rb
98
99
  - bugs/take-write.rb
100
+ - example/pubsub.rb
99
101
  - example/timeout-trans.rb
100
102
  - example/tiny-client.rb
101
103
  - example/add.rb
104
+ - example/parallel.rb
102
105
  - example/app-and-tup.rb
103
106
  - example/small.rb
104
107
  - example/bounded-retry.rb
@@ -124,7 +127,10 @@ files:
124
127
  - example/fail-and-retry.rb
125
128
  - example/dphil-optimistic-v2.rb
126
129
  - example/broker-optimistic-v2.rb
130
+ - example/remote.rb
131
+ - example/take-nowait.rb
127
132
  - example/optimist.rb
133
+ - example/message-bus.rb
128
134
  - example/balance-xfer-locking.rb
129
135
  - example/increment.rb
130
136
  - example/custom-class.rb
@@ -133,8 +139,11 @@ files:
133
139
  - example/broker-optimistic.rb
134
140
  - example/notify.rb
135
141
  - example/small-simplified.rb
142
+ - example/broker-queue.rb
136
143
  - example/async-transaction.rb
137
144
  - example/boolean-match.rb
145
+ - example/load-balancer.rb
146
+ - example/take-many.rb
138
147
  - example/dphil.rb
139
148
  - test/lib/testable-worker.rb
140
149
  - test/lib/mock-seq.rb