tupelo 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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