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.
- checksums.yaml +7 -0
- data/COPYING +22 -0
- data/README.md +422 -0
- data/Rakefile +77 -0
- data/bench/pipeline.rb +25 -0
- data/bugs/take-write.rb +19 -0
- data/bugs/write-read.rb +15 -0
- data/example/add.rb +19 -0
- data/example/app-and-tup.rb +30 -0
- data/example/async-transaction.rb +16 -0
- data/example/balance-xfer-locking.rb +50 -0
- data/example/balance-xfer-retry.rb +55 -0
- data/example/balance-xfer.rb +33 -0
- data/example/boolean-match.rb +32 -0
- data/example/bounded-retry.rb +35 -0
- data/example/broker-locking.rb +43 -0
- data/example/broker-optimistic-async.rb +33 -0
- data/example/broker-optimistic.rb +41 -0
- data/example/broker-queue.rb +2 -0
- data/example/cancel.rb +17 -0
- data/example/concurrent-transactions.rb +39 -0
- data/example/custom-class.rb +29 -0
- data/example/custom-search.rb +27 -0
- data/example/fail-and-retry.rb +29 -0
- data/example/hash-tuples.rb +53 -0
- data/example/increment.rb +21 -0
- data/example/lock-mgr-with-queue.rb +75 -0
- data/example/lock-mgr.rb +62 -0
- data/example/map-reduce-v2.rb +96 -0
- data/example/map-reduce.rb +77 -0
- data/example/matching.rb +9 -0
- data/example/notify.rb +35 -0
- data/example/optimist.rb +20 -0
- data/example/pulse.rb +24 -0
- data/example/read-in-trans.rb +56 -0
- data/example/small-simplified.rb +18 -0
- data/example/small.rb +76 -0
- data/example/tcp.rb +35 -0
- data/example/timeout-trans.rb +21 -0
- data/example/timeout.rb +27 -0
- data/example/tiny-client.rb +14 -0
- data/example/tiny-server.rb +12 -0
- data/example/transaction-logic.rb +40 -0
- data/example/write-wait.rb +17 -0
- data/lib/tupelo/app.rb +121 -0
- data/lib/tupelo/archiver/tuplespace.rb +68 -0
- data/lib/tupelo/archiver/worker.rb +87 -0
- data/lib/tupelo/archiver.rb +86 -0
- data/lib/tupelo/client/common.rb +10 -0
- data/lib/tupelo/client/reader.rb +124 -0
- data/lib/tupelo/client/transaction.rb +455 -0
- data/lib/tupelo/client/tuplespace.rb +50 -0
- data/lib/tupelo/client/worker.rb +493 -0
- data/lib/tupelo/client.rb +44 -0
- data/lib/tupelo/version.rb +3 -0
- data/test/lib/mock-client.rb +38 -0
- data/test/lib/mock-msg.rb +47 -0
- data/test/lib/mock-queue.rb +42 -0
- data/test/lib/mock-seq.rb +50 -0
- data/test/lib/testable-worker.rb +24 -0
- data/test/stress/concurrent-transactions.rb +42 -0
- data/test/system/test-archiver.rb +35 -0
- data/test/unit/test-mock-queue.rb +93 -0
- data/test/unit/test-mock-seq.rb +39 -0
- data/test/unit/test-ops.rb +222 -0
- 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
|
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
|
data/example/lock-mgr.rb
ADDED
@@ -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
|