tupelo 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|