tupelo 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +22 -0
  3. data/README.md +422 -0
  4. data/Rakefile +77 -0
  5. data/bench/pipeline.rb +25 -0
  6. data/bugs/take-write.rb +19 -0
  7. data/bugs/write-read.rb +15 -0
  8. data/example/add.rb +19 -0
  9. data/example/app-and-tup.rb +30 -0
  10. data/example/async-transaction.rb +16 -0
  11. data/example/balance-xfer-locking.rb +50 -0
  12. data/example/balance-xfer-retry.rb +55 -0
  13. data/example/balance-xfer.rb +33 -0
  14. data/example/boolean-match.rb +32 -0
  15. data/example/bounded-retry.rb +35 -0
  16. data/example/broker-locking.rb +43 -0
  17. data/example/broker-optimistic-async.rb +33 -0
  18. data/example/broker-optimistic.rb +41 -0
  19. data/example/broker-queue.rb +2 -0
  20. data/example/cancel.rb +17 -0
  21. data/example/concurrent-transactions.rb +39 -0
  22. data/example/custom-class.rb +29 -0
  23. data/example/custom-search.rb +27 -0
  24. data/example/fail-and-retry.rb +29 -0
  25. data/example/hash-tuples.rb +53 -0
  26. data/example/increment.rb +21 -0
  27. data/example/lock-mgr-with-queue.rb +75 -0
  28. data/example/lock-mgr.rb +62 -0
  29. data/example/map-reduce-v2.rb +96 -0
  30. data/example/map-reduce.rb +77 -0
  31. data/example/matching.rb +9 -0
  32. data/example/notify.rb +35 -0
  33. data/example/optimist.rb +20 -0
  34. data/example/pulse.rb +24 -0
  35. data/example/read-in-trans.rb +56 -0
  36. data/example/small-simplified.rb +18 -0
  37. data/example/small.rb +76 -0
  38. data/example/tcp.rb +35 -0
  39. data/example/timeout-trans.rb +21 -0
  40. data/example/timeout.rb +27 -0
  41. data/example/tiny-client.rb +14 -0
  42. data/example/tiny-server.rb +12 -0
  43. data/example/transaction-logic.rb +40 -0
  44. data/example/write-wait.rb +17 -0
  45. data/lib/tupelo/app.rb +121 -0
  46. data/lib/tupelo/archiver/tuplespace.rb +68 -0
  47. data/lib/tupelo/archiver/worker.rb +87 -0
  48. data/lib/tupelo/archiver.rb +86 -0
  49. data/lib/tupelo/client/common.rb +10 -0
  50. data/lib/tupelo/client/reader.rb +124 -0
  51. data/lib/tupelo/client/transaction.rb +455 -0
  52. data/lib/tupelo/client/tuplespace.rb +50 -0
  53. data/lib/tupelo/client/worker.rb +493 -0
  54. data/lib/tupelo/client.rb +44 -0
  55. data/lib/tupelo/version.rb +3 -0
  56. data/test/lib/mock-client.rb +38 -0
  57. data/test/lib/mock-msg.rb +47 -0
  58. data/test/lib/mock-queue.rb +42 -0
  59. data/test/lib/mock-seq.rb +50 -0
  60. data/test/lib/testable-worker.rb +24 -0
  61. data/test/stress/concurrent-transactions.rb +42 -0
  62. data/test/system/test-archiver.rb +35 -0
  63. data/test/unit/test-mock-queue.rb +93 -0
  64. data/test/unit/test-mock-seq.rb +39 -0
  65. data/test/unit/test-ops.rb +222 -0
  66. metadata +134 -0
@@ -0,0 +1,50 @@
1
+ require 'tupelo/app'
2
+
3
+ Tupelo.application do |app|
4
+ app.child do |client|
5
+ client.write(
6
+ {name: "alice", balance: 1000},
7
+ {name: "bob", balance: 200}
8
+ )
9
+ 10.times do |i|
10
+ alice = client.take(name: "alice", balance: Numeric)
11
+ client.log alice
12
+ alice = alice.dup
13
+ alice["balance"] -= 10
14
+ client.write_wait alice
15
+ sleep 0.1
16
+ end
17
+
18
+ client.log client.read_all(name: /^(?:alice|bob)$/, balance: nil)
19
+ end
20
+
21
+ app.child do |client|
22
+ sleep 0.3
23
+
24
+ src = client.take(name: "alice", balance: Numeric)
25
+ dst = client.take(name: "bob", balance: Numeric)
26
+
27
+ if src["balance"] < 500
28
+ abort "insufficient funds -- not attempting transfer"
29
+ end
30
+
31
+ sleep 0.3
32
+ # Even though we are outside of transaction, the delay doesn't matter,
33
+ # since this process possesses the tuples. So there is no failure.
34
+ # However, this has some disadvantages compared to the transaction
35
+ # implementation in the other examples: it's not atomic, tuples might
36
+ # be lost if the client exits, and more network hops (latency).
37
+
38
+ src = src.dup
39
+ dst = dst.dup
40
+
41
+ src["balance"] -= 500
42
+ dst["balance"] += 500
43
+
44
+ w = client.write src, dst
45
+ client.log "attempting to set #{[src, dst]}"
46
+
47
+ w.wait
48
+ client.log client.read_all(name: /^(?:alice|bob)$/, balance: nil)
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ require 'tupelo/app'
2
+
3
+ Tupelo.application do |app|
4
+ app.child do |client|
5
+ client.write(
6
+ {name: "alice", balance: 1000},
7
+ {name: "bob", balance: 200}
8
+ )
9
+ 10.times do |i|
10
+ alice = client.take(name: "alice", balance: Numeric)
11
+ client.log alice
12
+ alice = alice.dup
13
+ alice["balance"] -= 10
14
+ client.write_wait alice
15
+ sleep 0.1
16
+ end
17
+
18
+ client.log client.read_all(name: /^(?:alice|bob)$/, balance: nil)
19
+ end
20
+
21
+ app.child do |client|
22
+ client.transaction do |t|
23
+ src = t.take(name: "alice", balance: Numeric)
24
+ dst = t.take(name: "bob", balance: Numeric)
25
+
26
+ if src["balance"] < 500
27
+ abort "insufficient funds -- not attempting transfer"
28
+ end
29
+
30
+ src = src.dup
31
+ dst = dst.dup
32
+
33
+ src["balance"] -= 500
34
+ dst["balance"] += 500
35
+
36
+ sleep 0.3
37
+ # force fail -- the tuples this client is trying to take
38
+ # will be gone when it wakes up
39
+
40
+ client.log "attempting to set #{[src, dst]}"
41
+ t.write src, dst
42
+
43
+ if false # enable this to see how failures are retried
44
+ begin
45
+ t.commit.wait
46
+ rescue => ex
47
+ client.log "retrying after #{ex}"
48
+ raise
49
+ end
50
+ end
51
+ end
52
+
53
+ client.log client.read_all(name: /^(?:alice|bob)$/, balance: nil)
54
+ end
55
+ end
@@ -0,0 +1,33 @@
1
+ require 'tupelo/app'
2
+
3
+ Tupelo.application do |app|
4
+ app.child do |client|
5
+ client.write(
6
+ {name: "alice", balance: 1000},
7
+ {name: "bob", balance: 200}
8
+ )
9
+ end
10
+
11
+ app.child do |client|
12
+ client.transaction do |t|
13
+ src = t.take(name: "alice", balance: Numeric)
14
+ dst = t.take(name: "bob", balance: Numeric)
15
+
16
+ if src["balance"] < 500
17
+ abort "insufficient funds -- not attempting transfer"
18
+ end
19
+
20
+ src = src.dup
21
+ dst = dst.dup
22
+
23
+ src["balance"] -= 500
24
+ dst["balance"] += 500
25
+
26
+ t.write src, dst
27
+ end
28
+ # transaction will block if balances have changed since the read.
29
+ # see balance-xfer-retry.rb
30
+
31
+ client.log client.read_all(name: /^(?:alice|bob)$/, balance: nil)
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ require 'tupelo/app'
2
+
3
+ class Tupelo::Client
4
+ class Or
5
+ attr_reader :templates
6
+
7
+ def initialize worker, templates
8
+ @templates = templates.map {|template| worker.make_template(template)}
9
+ end
10
+
11
+ def === obj
12
+ templates.any? {|template| template === obj}
13
+ end
14
+ end
15
+
16
+ def or *templates
17
+ Or.new(worker, templates)
18
+ end
19
+ end
20
+
21
+ Tupelo.application do |app|
22
+ app.local do |client|
23
+ tm = client.or [0..2, String], [3..5, Hash]
24
+
25
+ client.write(
26
+ [0, "a"], [1, {b: 0}], [2, "c"],
27
+ [3, "a"], [4, {b: 0}], [5, "c"]
28
+ ).wait
29
+
30
+ client.log client.read_all tm
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ require 'tupelo/app'
2
+
3
+ N = 2
4
+ K = 1
5
+
6
+ Tupelo.application do |app|
7
+ (N+K).times do
8
+ app.child do |client|
9
+ catch :gave_up do
10
+ tries = 0
11
+
12
+ r = client.take [Integer] do |val|
13
+ tries += 1
14
+ if tries >= N
15
+ client.log "giving up on #{val}"
16
+ throw :gave_up
17
+ end
18
+ client.log "trying to take #{val}"
19
+ end
20
+
21
+ client.log "took #{r.inspect}"
22
+ end
23
+ end
24
+ end
25
+
26
+ sleep 0.01
27
+
28
+ app.child do |client|
29
+ N.times do |i|
30
+ client.write [i]
31
+ client.log "wrote #{[i]}"
32
+ sleep 0.1
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ # Clients attempt to pair up, using a distributed broker algorithm and a lock
2
+ # tuple.
3
+
4
+ require 'tupelo/app'
5
+
6
+ N_PLAYERS = 10
7
+
8
+ token = ["token"] # only the holder of the token can arrange games
9
+
10
+ Tupelo.application do |app|
11
+ app.local do |client|
12
+ client.write token
13
+ end
14
+
15
+ N_PLAYERS.times do
16
+ app.child do |client|
17
+ me = client.client_id
18
+
19
+ client.take token # bottleneck and fragile until 'client.write token'
20
+ other_player = client.read_nowait(name: nil)
21
+ # sleep 1 # program takes ~N_PLAYERS sec to finish
22
+
23
+ if other_player
24
+ client.take other_player
25
+ client.write(
26
+ player1: me,
27
+ player2: other_player["name"])
28
+ client.write token
29
+ you = other_player["name"]
30
+
31
+ else
32
+ client.write(name: me)
33
+ client.write token
34
+ game = client.read(
35
+ player1: nil,
36
+ player2: me)
37
+ you = game["player1"]
38
+ end
39
+
40
+ client.log "now playing with #{you}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ require 'tupelo/app'
2
+
3
+ N_PLAYERS = 10
4
+
5
+ Tupelo.application do |app|
6
+ N_PLAYERS.times do
7
+ app.child do |client|
8
+ me = client.client_id
9
+ opponent = nil
10
+ client.write name: me
11
+
12
+ arranging_game = client.transaction
13
+ arranging_game.async do |t|
14
+ t.take name: me
15
+ opponent = t.take name: nil
16
+ t.write(
17
+ player1: me,
18
+ player2: opponent)
19
+ end
20
+
21
+ Thread.new do
22
+ game = client.read(
23
+ player1: nil,
24
+ player2: me)
25
+ opponent = game["player1"]
26
+ end
27
+
28
+ wait
29
+ client.log "now playing with #{opponent}"
30
+ joining_game.cancel
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ # The local control flow in this example is more complex than in
2
+ # broker-locking.rb, but it has far fewer bottlenecks (try inserting some sleep
3
+ # 1 calls), and it is not possible for a token to be lost (leaving the lock in a
4
+ # locked state) if a process dies.
5
+
6
+ require 'tupelo/app'
7
+
8
+ N_PLAYERS = 10
9
+
10
+ Tupelo.application do |app|
11
+ N_PLAYERS.times do
12
+ app.child do |client|
13
+ me = client.client_id
14
+ client.write name: me
15
+ you = nil
16
+
17
+ 1.times do
18
+ begin
19
+ t = client.transaction
20
+ if t.take_nowait name: me
21
+ you = t.take(name: nil)["name"]
22
+ t.write(
23
+ player1: me,
24
+ player2: you)
25
+ t.commit.wait
26
+ break
27
+ end
28
+ rescue Tupelo::Client::TransactionFailure => ex
29
+ end
30
+
31
+ game = client.read_nowait(
32
+ player1: nil,
33
+ player2: me)
34
+ redo unless game
35
+ you = game["player1"]
36
+ end
37
+
38
+ client.log "now playing with #{you}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,2 @@
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
data/example/cancel.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'tupelo/app'
2
+
3
+ Tupelo.application do |app|
4
+ app.child do |client|
5
+ ats = (0..4).map do |i|
6
+ client.transaction.async do |t|
7
+ t.take ["start"]
8
+ t.write [i]
9
+ end
10
+ end
11
+
12
+ [0,1,2,4].each {|i| ats[i].cancel}
13
+
14
+ client.write ["start"]
15
+ p client.take [Integer]
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ require 'tupelo/app'
2
+
3
+ N = 100
4
+
5
+ Tupelo.application do |app|
6
+ app.child do |client|
7
+ client.write [0, 0]
8
+
9
+ t1 = Thread.new do
10
+ N.times do
11
+ client.transaction do |t|
12
+ t.take ["reader ready"]
13
+ x, y = t.take [nil, nil]
14
+ t.write [x+1, y]
15
+ t.write ["data ready"]
16
+ end
17
+ end
18
+ end
19
+
20
+ t2 = Thread.new do
21
+ N.times do
22
+ client.transaction do |t|
23
+ t.take ["reader ready"]
24
+ x, y = t.take [nil, nil]
25
+ t.write [x, y+1]
26
+ t.write ["data ready"]
27
+ end
28
+ end
29
+ end
30
+
31
+ loop do
32
+ client.write ["reader ready"]
33
+ client.take ["data ready"]
34
+ x, y = client.read [nil, nil]
35
+ client.log "%3d %3d" % [x, y]
36
+ break if x == N and y == N
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ class Foo
2
+ attr_accessor :x
3
+
4
+ # This method is necessary for #take to work correctly.
5
+ def == other
6
+ other.class == Foo and other.x == x
7
+ end
8
+ end
9
+
10
+ require 'tupelo/app'
11
+
12
+ # Must use marshal or yaml -- msgpack and json do not support custom classes.
13
+ Tupelo.application blob_type: 'marshal' do |app|
14
+ app.child do |client|
15
+ f = Foo.new; f.x = 3
16
+ p f
17
+
18
+ client.write [f]
19
+
20
+ p client.read [nil]
21
+ p client.read [Foo]
22
+ p client.read [f]
23
+
24
+ p client.take [Foo]
25
+
26
+ client.write [f]
27
+ p client.take [f]
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ require 'tupelo/app'
2
+
3
+ class MyClient < Tupelo::Client
4
+ # A custom search method. Note this is not the same as using a custom
5
+ # data structure for the tuplespace, which can be much more efficient than
6
+ # the default linear search.
7
+ def read_all_diagonal val, &bl
8
+ diag_matcher = proc {|t| t.all? {|v| v == val} }
9
+ # Note that Proc#===(t) calls the proc on t. It's convenient, but not
10
+ # essential to this example. We could also define a custom class with any
11
+ # implementation of #===.
12
+
13
+ read_all diag_matcher, &bl
14
+ end
15
+ end
16
+
17
+ Tupelo.application do |app|
18
+ app.local MyClient do |client|
19
+ client.write [41, 42, 43]
20
+ client.write [42, 42, 42]
21
+ client.write [42, 42]
22
+ client.write_wait [42] # make sure all writes up to this one have completed
23
+
24
+ client.log client.read_all [nil, nil, nil]
25
+ client.log client.read_all_diagonal 42
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ require 'tupelo/app'
2
+
3
+ Tupelo.application do |app|
4
+ 2.times do
5
+ app.child do |client|
6
+ begin
7
+ # the block is re-executed for the client that fails to take [1]
8
+ # this is also true in the transaction do...end construct.
9
+ t = client.transaction
10
+ r = t.take [Integer]
11
+ client.log "trying to take #{r.inspect}"
12
+ t.commit.wait
13
+ client.log "took #{r.inspect}"
14
+ rescue Tupelo::Client::TransactionFailure => ex
15
+ client.log "#{ex} -- retrying"
16
+ retry
17
+ # manually emulate the effect of transaction do...end
18
+ end
19
+ end
20
+ end
21
+
22
+ app.child do |client|
23
+ client.write [1]
24
+ client.log "wrote #{[1]}"
25
+ sleep 0.1
26
+ client.write [2]
27
+ client.log "wrote #{[2]}"
28
+ end
29
+ end
@@ -0,0 +1,53 @@
1
+ # Tuples can be sets of key-value pairs, rather than arrays.
2
+ #
3
+ # Caution:
4
+ #
5
+ # - when blob_type is json, (by using the --json switch, or by passing
6
+ # blob_type: 'json' to #application), the keys of these hashes must be
7
+ # strings. That's just a JSON thing.
8
+ #
9
+ # - ruby has some syntax quirks:
10
+ #
11
+ # - these are the same
12
+ # write foo: 1, bar: 2
13
+ # write({foo: 1, bar: 2})
14
+ # but this is a syntax error:
15
+ # write {foo: 1, bar: 2}
16
+ #
17
+ # - {x: 1} is short for {:x => 1}, rather than {"x" => 1}
18
+ #
19
+ # tupelo kind of hides this issue: you can use {x: 1} as a template
20
+ # to match tuples like {"x" => 1}. And you can write tuples using
21
+ # either notation. However, the tuple will still behave like this:
22
+ #
23
+ # write x: 1
24
+ # t = take x: nil
25
+ # t["x"] == 1 # ==> true
26
+ # t[:x] == 1 # ==> false
27
+ #
28
+ # In future API, it might be possible to access values like this:
29
+ #
30
+ # t.x == 1 # ==> true
31
+ #
32
+ # - matching ops succeed only if the key sets are equal, so
33
+ # you have to read or take using a template that has the same keys as the
34
+ # target tuple -- see below.
35
+
36
+ require 'tupelo/app'
37
+
38
+ Tupelo.application do |app|
39
+ app.child do |client|
40
+ client.write x: 1, y: 2
41
+ end
42
+
43
+ app.child do |client|
44
+ t = client.take x: Numeric, y: Numeric
45
+ client.write x: t["x"], y: t["y"], sum: t["x"] + t["y"]
46
+ client.log "sum result: #{client.read x: nil, y: nil, sum: nil}"
47
+
48
+ # N.B.: these are all empty, for the reason given above.
49
+ client.log client.read_all x: nil
50
+ client.log client.read_all y: nil
51
+ client.log client.read_all x: nil, y: nil, z: nil
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ require 'tupelo/app'
2
+
3
+ N = 5
4
+
5
+ Tupelo.application do |app|
6
+ N.times do |i|
7
+ app.child do |client|
8
+ client.transaction do |t|
9
+ n, s = t.take [Numeric, String]
10
+ #sleep rand # No race conditions here!
11
+ t.write [n + 1, s + "\n incremented by client #{i}"]
12
+ end
13
+ end
14
+ end
15
+
16
+ app.child do |client|
17
+ client.write [0, "started with 0"]
18
+ n, s = client.take [N, String]
19
+ puts s, "result is #{n}"
20
+ end
21
+ end
@@ -0,0 +1,75 @@
1
+ # This is like lock-mgr.rb, but by using a queue, we avoid the thundering herd
2
+ # problem. You can observe this by seeing "FAILED" in the output of the former
3
+ # but not in the latter. This means that competing offers are resolved by the
4
+ # queue, rather than by propagating them to all clients.
5
+
6
+ require 'tupelo/app'
7
+
8
+ N = 3
9
+
10
+ Tupelo.application do |app|
11
+ app.child do |client| # a debugger client, to see what's happening
12
+ note = client.notifier
13
+ puts "%4s %4s %10s %s" % %w{ tick cid status operation }
14
+ loop do
15
+ status, tick, cid, op = note.wait
16
+ unless status == :attempt
17
+ s = status == :failure ? "FAILED" : ""
18
+ puts "%4d %4d %10s %p" % [tick, cid, s, op]
19
+ end
20
+ end
21
+ end
22
+
23
+ app.child do |client| # the lock manager
24
+ client.log.progname << " (lock mgr)"
25
+ waiters = Queue.new
26
+
27
+ Thread.new do
28
+ loop do
29
+ _, _, client_id, duration =
30
+ client.take ["request", "resource", nil, nil]
31
+ waiters << [client_id, duration]
32
+ end
33
+ end
34
+
35
+ loop do
36
+ client_id, duration = waiters.pop
37
+ client.write ["resource", client_id, duration]
38
+ begin
39
+ client.take ["done", "resource", client_id], timeout: duration
40
+ rescue TimeoutError
41
+ client.log "forcing client #{client_id} to stop using resource."
42
+ end
43
+ client.take ["resource", client_id, duration]
44
+ end
45
+ # exercise for reader: make this work with 2 or more resources
46
+ # exercise: rewrite this with hash tuples instead of array tuples
47
+ end
48
+
49
+ N.times do |i|
50
+ app.child do |client|
51
+ client.write ["request", "resource", client.client_id, 0.5]
52
+
53
+ 10.times do |j|
54
+ # Now we are ouside of transaction, but still no other client may use
55
+ # "resource" until lock expires (or is otherwise removed), as long
56
+ # as all clients follow the read protocol below.
57
+ sleep 0.2
58
+
59
+ client.transaction do |t|
60
+ t.read ["resource", client.client_id, nil]
61
+ t.write ["c#{client.client_id}##{j}"]
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ app.child do |client|
68
+ # This client never even tries to lock the resource, so it cannot write.
69
+ client.transaction do |t|
70
+ t.read ["resource", client.client_id, nil]
71
+ t.write ["c#{client.client_id}##{j}"]
72
+ end
73
+ client.log.error "should never get here"
74
+ end
75
+ end
@@ -0,0 +1,62 @@
1
+ require 'tupelo/app'
2
+
3
+ N = 3
4
+
5
+ Tupelo.application do |app|
6
+ app.child do |client| # a debugger client, to see what's happening
7
+ note = client.notifier
8
+ puts "%4s %4s %10s %s" % %w{ tick cid status operation }
9
+ loop do
10
+ status, tick, cid, op = note.wait
11
+ unless status == :attempt
12
+ s = status == :failure ? "FAILED" : ""
13
+ puts "%4d %4d %10s %p" % [tick, cid, s, op]
14
+ end
15
+ end
16
+ end
17
+
18
+ app.child do |client| # the lock manager
19
+ loop do
20
+ client.write ["resource", "none"]
21
+ lock_id, client_id, duration = client.read ["resource", nil, nil]
22
+ sleep duration
23
+ client.take [lock_id, client_id, duration]
24
+ # optimization: combine take and write in a transaction, just to
25
+ # reduce delay
26
+ end
27
+ # exercise for reader: make this work with 2 or more resources
28
+ # exercise: rewrite this with hash tuples instead of array tuples
29
+ end
30
+
31
+ N.times do |i|
32
+ app.child do |client|
33
+ client.transaction do |t|
34
+ t.take ["resource", "none"]
35
+ t.write ["resource", client.client_id, 0.5]
36
+ end
37
+ # Thundering herd -- all N clients can respond at the same time.
38
+ # A better example would have a queue -- see lock-mgr-with-queue.rb.
39
+
40
+ 10.times do |j|
41
+ # Now we are ouside of transaction, but still no other client may use
42
+ # "resource" until lock expires (or is otherwise removed), as long
43
+ # as all clients follow the read protocol below.
44
+ sleep 0.2
45
+
46
+ client.transaction do |t|
47
+ t.read ["resource", client.client_id, nil]
48
+ t.write ["c#{client.client_id}##{j}"]
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ app.child do |client|
55
+ # This client never even tries to lock the resource, so it cannot write.
56
+ client.transaction do |t|
57
+ t.read ["resource", client.client_id, nil]
58
+ t.write ["c#{client.client_id}##{j}"]
59
+ end
60
+ client.log.error "should never get here"
61
+ end
62
+ end