tupelo 0.17 → 0.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/COPYING +1 -1
- data/README.md +7 -5
- data/bench/tuplespace.rb +30 -0
- data/bin/tup +16 -0
- data/example/barrier.rb +29 -0
- data/example/bingo/bingo-v1.rb +42 -0
- data/example/dphil-optimistic-v2.rb +2 -1
- data/example/map-reduce/prime-factor-balanced.rb +11 -14
- data/example/map-reduce/prime-factor.rb +1 -1
- data/example/nest.rb +27 -0
- data/example/observer.rb +55 -0
- data/example/subspaces/addr-book-v1.rb +106 -0
- data/example/subspaces/addr-book.rb +1 -1
- data/example/subspaces/riemann.rb +103 -0
- data/example/subspaces/sorted-set-space-OLD.rb +130 -0
- data/lib/tupelo/app/trace.rb +45 -14
- data/lib/tupelo/app.rb +5 -1
- data/lib/tupelo/client/subspace.rb +6 -3
- data/lib/tupelo/client/transaction.rb +17 -17
- data/lib/tupelo/client/worker.rb +5 -5
- data/lib/tupelo/client.rb +3 -2
- data/lib/tupelo/version.rb +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60516b564c0329a1544a913f4ec1680bd8308de8
|
4
|
+
data.tar.gz: 6aadbcc4c96e57d762d072e266ff921972f58578
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75b24e537a78c831be55b389457a41fd527bf3fc88cb7e4dd358d03ecc492d439157ce28a3191ae86946c2df12a5ee5f74b5b388aebd8a9da57e0e1cc0af8386
|
7
|
+
data.tar.gz: 53a0848f5b3c30e091fc0bd0d58fced4703b77561099866b032e1d9f7c312a85da01d2a4066c2979f2632caa31a6c97466b21cf4319acca99308f4beddaa25d5
|
data/COPYING
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Tupelo
|
2
2
|
==
|
3
3
|
|
4
|
-
A tuplespace that is fast, scalable, and language agnostic.
|
4
|
+
A tuplespace that is fast, scalable, and language agnostic. Tupelo is designed for distribution of both computation and storage (disk and memory), in a unified language that has both transactional and tuple-operation (read/write/take) semantics.
|
5
5
|
|
6
6
|
This is the reference implementation in ruby. It should be able to communicate with implementations in other languages.
|
7
7
|
|
@@ -12,7 +12,10 @@ Documentation
|
|
12
12
|
* [FAQ](doc/faq.md)
|
13
13
|
* [Comparisons](doc/compare.md)
|
14
14
|
* [Transactions](doc/transactions.md)
|
15
|
+
* [Replication](doc/replication.md)
|
15
16
|
* [Subspaces](doc/subspace.md)
|
17
|
+
* [Causality](doc/causality.md)
|
18
|
+
* [Concurrency](doc/concurrency.md)
|
16
19
|
* [Examples](example/)
|
17
20
|
|
18
21
|
Internals
|
@@ -41,14 +44,13 @@ Getting started
|
|
41
44
|
>> t [nil, nil]
|
42
45
|
=> ["hello", "world"]
|
43
46
|
|
44
|
-
4. Take a look at the [FAQ](doc/faq.md), [tutorial](doc/tutorial.md), and many
|
47
|
+
4. Take a look at the [FAQ](doc/faq.md), [tutorial](doc/tutorial.md), and the many [examples](example/).
|
45
48
|
|
46
49
|
|
47
50
|
Applications
|
48
51
|
=======
|
49
52
|
|
50
|
-
Tupelo is a flexible base layer for various distributed programming paradigms: job queues, dataflow, map-reduce, etc.
|
51
|
-
|
53
|
+
Tupelo is a flexible base layer for various distributed programming paradigms: job queues, dataflow, map-reduce, etc. Using subspaces, it's also a transactional, replicated datastore with pluggable storage providers.
|
52
54
|
|
53
55
|
|
54
56
|
Advantages
|
@@ -148,6 +150,6 @@ Joel VanderWerf, vjoel@users.sourceforge.net, @JoelVanderWerf.
|
|
148
150
|
License and Copyright
|
149
151
|
========
|
150
152
|
|
151
|
-
Copyright (c) 2013, Joel VanderWerf
|
153
|
+
Copyright (c) 2013-2014, Joel VanderWerf
|
152
154
|
|
153
155
|
License for this project is BSD. See the COPYING file for the standard BSD license. The supporting gems developed for this project are similarly licensed.
|
data/bench/tuplespace.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Benchmark the default tuplespace (which is not intended to be fast).
|
2
|
+
|
3
|
+
module Tupelo
|
4
|
+
class Client; end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'tupelo/client/tuplespace'
|
8
|
+
require 'benchmark'
|
9
|
+
|
10
|
+
N_TUPLES = 100_000
|
11
|
+
N_DELETES = 10_000
|
12
|
+
|
13
|
+
Benchmark.bm(20) do |b|
|
14
|
+
ts = Tupelo::Client::SimpleTuplespace.new
|
15
|
+
|
16
|
+
b.report('insert') do
|
17
|
+
N_TUPLES.times do |i|
|
18
|
+
ts.insert i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
b.report('delete') do
|
23
|
+
N_DELETES.times do
|
24
|
+
i = rand(N_TUPLES)
|
25
|
+
if ts.delete_once i
|
26
|
+
ts.insert i
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/bin/tup
CHANGED
@@ -119,12 +119,28 @@ Tupelo.application(
|
|
119
119
|
CMD_ALIASES = %w{ w pl t r ra tr }
|
120
120
|
private *CMD_ALIASES
|
121
121
|
|
122
|
+
def trace *args
|
123
|
+
require 'tupelo/app/trace'
|
124
|
+
if args.empty?
|
125
|
+
if tracing? then stop_trace else start_trace end
|
126
|
+
elsif args.first
|
127
|
+
start_trace
|
128
|
+
else
|
129
|
+
stop_trace
|
130
|
+
end
|
131
|
+
tracing?
|
132
|
+
end
|
133
|
+
|
122
134
|
def help
|
123
135
|
puts "Command aliases:"
|
124
136
|
CMD_ALIASES.each do |m_name|
|
125
137
|
m = method(m_name)
|
126
138
|
printf "%8s -> %s\n", m.name, m.original_name
|
127
139
|
end
|
140
|
+
puts
|
141
|
+
puts "Extra commands:"
|
142
|
+
puts " trace [true|false] # turn on or off tracing"
|
143
|
+
puts " trace # toggle tracing"
|
128
144
|
nil
|
129
145
|
end
|
130
146
|
end
|
data/example/barrier.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Unlike MPI barriers, does not use token ring. This is better for loosely
|
2
|
+
# coupled systems -- a failed process doesn't break the ring, the state is in
|
3
|
+
# the distributed tuplespace rather than just one process, and a replacement
|
4
|
+
# process can be swapped in. Run with --trace to see what's happening.
|
5
|
+
|
6
|
+
require 'tupelo/app'
|
7
|
+
|
8
|
+
N_WORKERS = 5
|
9
|
+
N_STEPS = 3
|
10
|
+
|
11
|
+
Tupelo.application do
|
12
|
+
pids = []
|
13
|
+
N_WORKERS.times do |wi|
|
14
|
+
pids << child do
|
15
|
+
N_STEPS.times do |si|
|
16
|
+
sleep 0.1 # do some work
|
17
|
+
|
18
|
+
write step: si, worker: wi
|
19
|
+
|
20
|
+
N_WORKERS.times do |i|
|
21
|
+
read step: si, worker: i
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
pids.each {|pid| Process.wait pid}
|
28
|
+
puts "done"
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'tupelo/app'
|
2
|
+
|
3
|
+
N_BUYERS = 3
|
4
|
+
MAX_PER_BUYER = 4
|
5
|
+
|
6
|
+
Tupelo.application do
|
7
|
+
child do
|
8
|
+
# stock up on cards to sell
|
9
|
+
10.times do |i|
|
10
|
+
write ["card", i] # todo add card details
|
11
|
+
end
|
12
|
+
|
13
|
+
# start selling
|
14
|
+
write ["selling", true]
|
15
|
+
sleep 1
|
16
|
+
|
17
|
+
# stop selling
|
18
|
+
take ["selling", true]
|
19
|
+
write ["selling", false]
|
20
|
+
|
21
|
+
# run the game
|
22
|
+
p read_all ["player", nil, "card", nil]
|
23
|
+
end
|
24
|
+
|
25
|
+
N_BUYERS.times do
|
26
|
+
child do
|
27
|
+
catch :done_buying do
|
28
|
+
MAX_PER_BUYER.times do
|
29
|
+
transaction do
|
30
|
+
_, selling = read ["selling", nil]
|
31
|
+
if selling
|
32
|
+
_, card_id = take ["card", nil]
|
33
|
+
write ["player", client_id, "card", card_id]
|
34
|
+
else
|
35
|
+
throw :done_buying
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -15,7 +15,8 @@ Tupelo.application do
|
|
15
15
|
|
16
16
|
N_ITER.times do
|
17
17
|
transaction do
|
18
|
-
# lock the resource (in transaction, so optimistically)
|
18
|
+
# lock the resource (in transaction, so optimistically)
|
19
|
+
# this is really just an assertion of facts:
|
19
20
|
read ["chopstick", i]
|
20
21
|
read ["chopstick", (i+1)%N_PHIL]
|
21
22
|
|
@@ -1,7 +1,10 @@
|
|
1
1
|
# Modified prime-factor.rb attempts to balance load better. Improvement
|
2
2
|
# varies--typically around 50% faster with 12 remote hosts.
|
3
|
+
# The key to balancing load in this case is that all tuples can be
|
4
|
+
# read in each worker, so the worker doesn't have to guess so much.
|
3
5
|
|
4
6
|
require 'tupelo/app/remote'
|
7
|
+
require 'set'
|
5
8
|
|
6
9
|
hosts = ARGV.shift or abort "usage: #$0 <ssh-hostname>,<ssh-hostname>,..."
|
7
10
|
hosts = hosts.split(",")
|
@@ -13,7 +16,7 @@ Tupelo.tcp_application do
|
|
13
16
|
class M
|
14
17
|
def initialize nh, hi, excl = []
|
15
18
|
@nh, @hi = nh, hi
|
16
|
-
@excl = excl
|
19
|
+
@excl = Set.new(excl)
|
17
20
|
end
|
18
21
|
def === x
|
19
22
|
Array === x and
|
@@ -37,7 +40,7 @@ Tupelo.tcp_application do
|
|
37
40
|
|
38
41
|
if input
|
39
42
|
begin
|
40
|
-
txn.commit
|
43
|
+
txn.commit # but don't wait yet. Optimistically start working...
|
41
44
|
output = input.prime_division
|
42
45
|
Thread.new do
|
43
46
|
begin
|
@@ -51,6 +54,9 @@ Tupelo.tcp_application do
|
|
51
54
|
rescue TransactionFailure
|
52
55
|
end
|
53
56
|
my_pref = my_pref.exclude input
|
57
|
+
# Some other worker is optimistically working on this item
|
58
|
+
# (stolen from this worker), so don't try to take it again.
|
59
|
+
# Reduces contention.
|
54
60
|
next
|
55
61
|
end
|
56
62
|
|
@@ -60,27 +66,18 @@ Tupelo.tcp_application do
|
|
60
66
|
end
|
61
67
|
break
|
62
68
|
end
|
63
|
-
|
69
|
+
|
70
|
+
# No more preferred items, so fall back to dumb (and contentious) algo.
|
64
71
|
loop do
|
65
72
|
_, input = take(["input", Integer])
|
66
73
|
write ["output", input, input.prime_division]
|
67
74
|
end
|
68
|
-
|
69
|
-
|
70
|
-
# _, input =
|
71
|
-
# begin
|
72
|
-
# take(my_pref, timeout: 1.0) # fewer fails (5.0 -> none at all)
|
73
|
-
# rescue TimeoutError
|
74
|
-
# take(["input", Integer])
|
75
|
-
# end
|
76
|
-
# write ["output", input, input.prime_division]
|
77
|
-
# end
|
78
75
|
}
|
79
76
|
end
|
80
77
|
|
81
78
|
local do
|
82
79
|
t0 = Time.now
|
83
|
-
inputs = 1_000_000_000_000 ..
|
80
|
+
inputs = 1_000_000_000_000 .. 1_000_000_000_200
|
84
81
|
|
85
82
|
inputs.each do |input|
|
86
83
|
write ["input", input]
|
data/example/nest.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'tupelo/app'
|
2
|
+
|
3
|
+
Tupelo.application do
|
4
|
+
|
5
|
+
child do
|
6
|
+
write [1]
|
7
|
+
|
8
|
+
transaction timeout: 1 do |outer|
|
9
|
+
x, _ = outer.read [1]
|
10
|
+
|
11
|
+
transaction do
|
12
|
+
write [x+1]
|
13
|
+
# This is a programming error. The outer txn assumes that [1] exists.
|
14
|
+
# The inner txn assumes that the outer txn executes. But in fact the
|
15
|
+
# outer txn never executes (it times out), and the inner txn does
|
16
|
+
# execute, leaving an unexpected [2] in the space.
|
17
|
+
end
|
18
|
+
|
19
|
+
outer.read ["barrier"]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
child do
|
24
|
+
take [1]
|
25
|
+
write ["barrier"]
|
26
|
+
end
|
27
|
+
end
|
data/example/observer.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# An implementation of the observer pattern using transaction failure.
|
2
|
+
# Note that the failures happen in the pre-commit phase of the transactions,
|
3
|
+
# so they will not be shown with the --trace switch.
|
4
|
+
# Run with `--stream` to show the optional observer implemented using
|
5
|
+
# streaming reads instead of transaction failure.
|
6
|
+
|
7
|
+
require 'tupelo/app'
|
8
|
+
|
9
|
+
Tupelo.application do
|
10
|
+
|
11
|
+
child do
|
12
|
+
log.progname = "counting client"
|
13
|
+
|
14
|
+
write count: 0
|
15
|
+
|
16
|
+
3.times do
|
17
|
+
sleep 1
|
18
|
+
transaction do
|
19
|
+
count = take(count: Numeric)["count"]
|
20
|
+
write count: count + 1
|
21
|
+
log "incrementing counter"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
child passive: true do
|
27
|
+
log.progname = "observing client"
|
28
|
+
|
29
|
+
loop do
|
30
|
+
begin
|
31
|
+
t = transaction
|
32
|
+
counter = t.read count: Numeric
|
33
|
+
log "entering new state: #{counter}"
|
34
|
+
t.wait
|
35
|
+
rescue Tupelo::Client::TransactionFailure => ex
|
36
|
+
log "leaving old state: #{counter}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if argv.include?("--stream")
|
42
|
+
# If you only care about seeing each new values as it arrives, this client
|
43
|
+
# is enough. But it doesn't alert you when a tuple is deleted. In this
|
44
|
+
# example, the delete and insert happen atomically in a transaction, so
|
45
|
+
# it doesn't matter.
|
46
|
+
child passive: true do
|
47
|
+
log.progname = "stream client"
|
48
|
+
|
49
|
+
read count: Numeric do |counter|
|
50
|
+
log "entering new state: #{counter}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
## TODO
|
2
|
+
##
|
3
|
+
## scaling params
|
4
|
+
|
5
|
+
require 'tupelo/app'
|
6
|
+
|
7
|
+
ab_tag = "my address book"
|
8
|
+
ab_sort_field = 1
|
9
|
+
ab_val_field = 2
|
10
|
+
cmd_tag = "#{ab_tag} commands"
|
11
|
+
resp_tag = "#{ab_tag} responses"
|
12
|
+
|
13
|
+
Tupelo.application do
|
14
|
+
local do
|
15
|
+
use_subspaces!
|
16
|
+
|
17
|
+
# Subspace for tuples belonging to the addr book.
|
18
|
+
define_subspace(
|
19
|
+
tag: ab_tag,
|
20
|
+
template: [
|
21
|
+
{value: ab_tag},
|
22
|
+
{type: "string"}, # name <-- ab_sort_field references this field
|
23
|
+
nil # address; can be any object <-- ab_val_field
|
24
|
+
]
|
25
|
+
)
|
26
|
+
|
27
|
+
# Subspace for commands for fetch and delete.
|
28
|
+
# We can't use #read and #take because then the requesting client
|
29
|
+
# would have to subscribe to the ab_tag subspace.
|
30
|
+
define_subspace(
|
31
|
+
tag: cmd_tag,
|
32
|
+
template: [
|
33
|
+
{value: cmd_tag},
|
34
|
+
{type: "string"}, # cmd name
|
35
|
+
{type: "list"} # arguments
|
36
|
+
]
|
37
|
+
)
|
38
|
+
|
39
|
+
# Subspace for responses to commands. Identify the command this is in
|
40
|
+
# response to by copying it (alternately, could use ids).
|
41
|
+
define_subspace(
|
42
|
+
tag: resp_tag,
|
43
|
+
template: [
|
44
|
+
{value: resp_tag},
|
45
|
+
{type: "string"}, # cmd name
|
46
|
+
{type: "list"}, # arguments
|
47
|
+
nil # result of query -- type depends on command
|
48
|
+
]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
## Could set N_SORTED_SET_SPACE > 1, but lookups are so fast it would
|
53
|
+
## just lead to contention and redundant computation. Redundancy is useful
|
54
|
+
## though.
|
55
|
+
|
56
|
+
# Inserts are just writes, which are handled by Worker and SortedSetSpace,
|
57
|
+
# so this child's app loop only needs to handle special commands: fetch and
|
58
|
+
# delete, which are delegated to the SortedSetSpace.
|
59
|
+
child tuplespace: [SortedSetSpace, ab_tag, ab_sort_field, ab_val_field],
|
60
|
+
subscribe: [ab_tag, cmd_tag], passive: true do
|
61
|
+
loop do
|
62
|
+
transaction do
|
63
|
+
_, cmd, args = take(subspace cmd_tag)
|
64
|
+
|
65
|
+
case cmd
|
66
|
+
when "delete"
|
67
|
+
args.each do |name|
|
68
|
+
take [ab_tag, name, nil]
|
69
|
+
end
|
70
|
+
|
71
|
+
when "fetch"
|
72
|
+
name = args[0]
|
73
|
+
_, _, addr = read [ab_tag, name, nil]
|
74
|
+
write [resp_tag, name, args, addr]
|
75
|
+
|
76
|
+
when "next", "prev"
|
77
|
+
name = args[0]
|
78
|
+
_, name2, addr = read SortedSetTemplate[ab_tag, cmd, name]
|
79
|
+
write [resp_tag, name, args, name2, addr]
|
80
|
+
|
81
|
+
when "first", "last"
|
82
|
+
_, name, addr = read SortedSetTemplate[ab_tag, cmd]
|
83
|
+
write [resp_tag, name, args, name, addr]
|
84
|
+
|
85
|
+
else # maybe write an error message in a tuple
|
86
|
+
log.error "bad command: #{cmd}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
child subscribe: resp_tag do
|
93
|
+
# write some ab entries
|
94
|
+
write [ab_tag, "McFirst, Firsty", "123 W. Crescent Terrace"]
|
95
|
+
write [ab_tag, "Secondismus, Deuce", "456 S. West Way"]
|
96
|
+
|
97
|
+
# make some queries
|
98
|
+
write [cmd_tag, "first", []]
|
99
|
+
*, name, addr = take [resp_tag, "first", [], nil, nil]
|
100
|
+
log "first entry: #{name} => #{addr}"
|
101
|
+
|
102
|
+
write [cmd_tag, "next", [name]]
|
103
|
+
*, name, addr = take [resp_tag, "next", [name], nil, nil]
|
104
|
+
log "next entry: #{name} => #{addr}"
|
105
|
+
end
|
106
|
+
end
|
@@ -119,7 +119,7 @@ Tupelo.application do
|
|
119
119
|
name = "Daisy"
|
120
120
|
write [cmd_tag, rqid, "fetch", [name]]
|
121
121
|
addr = take( [resp_tag, rqid, nil, nil, nil] ).last
|
122
|
-
log "found: #{name} => #{addr}"
|
122
|
+
log "Looked up #{name} and found: #{name} => #{addr}"
|
123
123
|
|
124
124
|
rqid = next_rqid.call
|
125
125
|
write [cmd_tag, rqid, "first", []]
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# A toy implementation of Riemann (http://riemann.io)
|
2
|
+
#
|
3
|
+
# Also, this is an example of storing a subspace using different data
|
4
|
+
# structures in different clients, depending on needs: some clients (generic
|
5
|
+
# consumers) need to index by host and service, and others (expiration manager)
|
6
|
+
# need to sort by expiration time.
|
7
|
+
|
8
|
+
require 'tupelo/app'
|
9
|
+
|
10
|
+
N_PRODUCERS = 3
|
11
|
+
N_CONSUMERS = 2
|
12
|
+
|
13
|
+
Tupelo.application do
|
14
|
+
|
15
|
+
local do
|
16
|
+
use_subspaces!
|
17
|
+
|
18
|
+
define_subspace("event", {
|
19
|
+
# field type description
|
20
|
+
# (from http://riemann.io/concepts.html)
|
21
|
+
|
22
|
+
host: String, # A hostname, e.g. "api1", "foo.com"
|
23
|
+
|
24
|
+
service: String, # e.g. "API port 8000 reqs/sec"
|
25
|
+
|
26
|
+
state: String, # Any string less than 255 bytes, e.g. "ok",
|
27
|
+
# "warning", "critical"
|
28
|
+
|
29
|
+
time: Numeric, # The time of the event, in unix epoch seconds
|
30
|
+
|
31
|
+
description: String, # Freeform text
|
32
|
+
|
33
|
+
tags: Array, # Freeform list of strings,
|
34
|
+
# e.g. ["rate", "fooproduct", "transient"]
|
35
|
+
|
36
|
+
metric: Numeric, # A number associated with this event,
|
37
|
+
# e.g. the number of reqs/sec.
|
38
|
+
|
39
|
+
ttl: Numeric # A floating-point time, in seconds, that this
|
40
|
+
# event is considered valid for. Expired states
|
41
|
+
# may be removed from the index.
|
42
|
+
})
|
43
|
+
end
|
44
|
+
|
45
|
+
N_PRODUCERS.times do
|
46
|
+
child subscribe: [] do # N.b., no subscriptions
|
47
|
+
event = {
|
48
|
+
host: `hostname`.chomp,
|
49
|
+
service: "service #{client_id}", # placeholder
|
50
|
+
state: "",
|
51
|
+
time: 0,
|
52
|
+
description: "",
|
53
|
+
tags: [],
|
54
|
+
metric: 0,
|
55
|
+
ttl: 0
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
e_ok = event.merge(
|
59
|
+
state: "ok",
|
60
|
+
time: Time.now.to_f,
|
61
|
+
ttl: 1.0
|
62
|
+
)
|
63
|
+
|
64
|
+
if e_ok[:ttl] == 0.0
|
65
|
+
pulse e_ok # no need to bother with expiration
|
66
|
+
else
|
67
|
+
write e_ok
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
N_CONSUMERS.times do
|
73
|
+
# stores events indexed by host, service
|
74
|
+
child subscribe: "event", passive: true do ### tuplespace: sqlite
|
75
|
+
read subspace("event") do |event|
|
76
|
+
log event ### need filtering, actions, etc.
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# This could be a subspace of the event subspace.
|
82
|
+
critical_event = {
|
83
|
+
host: nil,
|
84
|
+
service: nil,
|
85
|
+
state: /critical|fatal/i,
|
86
|
+
time: nil,
|
87
|
+
description: nil,
|
88
|
+
tags: nil,
|
89
|
+
metric: nil,
|
90
|
+
ttl: nil
|
91
|
+
}
|
92
|
+
|
93
|
+
child subscribe: "event", passive: true do
|
94
|
+
read critical_event do |event|
|
95
|
+
log.error event
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# expirer: stores current events in expiration order
|
100
|
+
child subscribe: "event", passive: true do
|
101
|
+
### use rbtree
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'rbtree'
|
2
|
+
|
3
|
+
class SortedSetTemplate
|
4
|
+
class << self
|
5
|
+
alias [] new
|
6
|
+
end
|
7
|
+
|
8
|
+
# cmd can be "next", "prev", "first", "last"
|
9
|
+
# for next/prev, args is ["name"]
|
10
|
+
# for first/last, args is empty
|
11
|
+
def initialize tag, cmd, *args
|
12
|
+
@tag = tag
|
13
|
+
@cmd = cmd
|
14
|
+
@args = args
|
15
|
+
end
|
16
|
+
|
17
|
+
def === other
|
18
|
+
raise ### should not need this?
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_in rbtree
|
22
|
+
case @cmd
|
23
|
+
when "first"
|
24
|
+
rbtree.first
|
25
|
+
###
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# A tuple store (in-memory) that is optimized for (key_string, object) pairs.
|
30
|
+
# The object may be any serializable object (built up from numbers, booleans,
|
31
|
+
# nil, strings, hashes and arrays).
|
32
|
+
#
|
33
|
+
# Unlike in a key-value store, a given key_string may occur more than once.
|
34
|
+
# It is up to the application to decide whether to enforce key uniqueness or
|
35
|
+
# not (for example, by taking (k,...) before writing (k,v).
|
36
|
+
#
|
37
|
+
# This store should be used only by clients that subscribe to a subspace
|
38
|
+
# that can be represented as pairs. (See memo2.rb.)
|
39
|
+
#
|
40
|
+
# This store also manages meta tuples, which it keeps in an array, just like
|
41
|
+
# the default Tuplespace class does.
|
42
|
+
class SortedSetSpace
|
43
|
+
include Enumerable
|
44
|
+
|
45
|
+
attr_reader :tag, :hash, :metas
|
46
|
+
|
47
|
+
def initialize tag
|
48
|
+
@tag = tag
|
49
|
+
clear
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear
|
53
|
+
@hash = Hash.new {|h,k| h[k] = []}
|
54
|
+
# It's up to the application to enforce that these arrays have size <=1.
|
55
|
+
@metas = []
|
56
|
+
# We are automatically subscribed to tupelo metadata (subspace defs), so
|
57
|
+
# we need to keep them somewhere.
|
58
|
+
end
|
59
|
+
|
60
|
+
def each
|
61
|
+
hash.each do |k, vs|
|
62
|
+
vs.each do |v|
|
63
|
+
yield tag, k, v
|
64
|
+
end
|
65
|
+
end
|
66
|
+
metas.each do |tuple|
|
67
|
+
yield tuple
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def insert tuple
|
72
|
+
if tuple.kind_of? Array
|
73
|
+
# and tuple.size == 3 and tuple[0] == tag and tuple[1].kind_of? String
|
74
|
+
# This is redundant, because of subscribe.
|
75
|
+
t, k, v = tuple
|
76
|
+
hash[k] << v
|
77
|
+
|
78
|
+
else
|
79
|
+
metas << tuple
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def delete_once tuple
|
84
|
+
if tuple.kind_of? Array
|
85
|
+
# and tuple.size == 3 and tuple[0] == tag and tuple[1].kind_of? String
|
86
|
+
# This is redundant, because of subscribe.
|
87
|
+
t, k, v = tuple
|
88
|
+
if hash.key?(k) and hash[k].include? v
|
89
|
+
hash[k].delete v
|
90
|
+
hash.delete k if hash[k].empty?
|
91
|
+
true
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
|
96
|
+
else
|
97
|
+
if i=metas.index(tuple)
|
98
|
+
delete_at i
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def transaction inserts: [], deletes: [], tick: nil
|
104
|
+
deletes.each do |tuple|
|
105
|
+
delete_once tuple or raise "bug"
|
106
|
+
end
|
107
|
+
|
108
|
+
inserts.each do |tuple|
|
109
|
+
insert tuple.freeze ## should be deep_freeze
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def find_distinct_matches_for templates
|
114
|
+
templates.inject([]) do |tuples, template|
|
115
|
+
tuples << find_match_for(template, distinct_from: tuples)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def find_match_for template, distinct_from: []
|
120
|
+
case template
|
121
|
+
when SortedSetTemplate
|
122
|
+
template.find_in rbtree, distinct_from: distinct_from ###
|
123
|
+
else
|
124
|
+
# fall back to linear search
|
125
|
+
find do |tuple|
|
126
|
+
template === tuple and not distinct_from.any? {|t| t.equal? tuple}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/tupelo/app/trace.rb
CHANGED
@@ -1,27 +1,58 @@
|
|
1
1
|
require 'tupelo/app'
|
2
2
|
|
3
|
+
class Tupelo::Client
|
4
|
+
def trace_loop
|
5
|
+
note = notifier
|
6
|
+
log << ( "%6s %6s %6s %s\n" % %w{ tick cid status operation } )
|
7
|
+
loop do
|
8
|
+
status, tick, cid, op, tags = note.wait
|
9
|
+
unless status == :attempt
|
10
|
+
s = status == :failure ? "FAIL" : ""
|
11
|
+
if tags and not tags.empty?
|
12
|
+
log << ( "%6d %6d %6s %p to %p\n" % [tick, cid, s, op, tags] )
|
13
|
+
else
|
14
|
+
log << ( "%6d %6d %6s %p\n" % [tick, cid, s, op] )
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def tracing?
|
21
|
+
!!@trace_thread
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Turn on tracing in this client (performed by a thread).
|
27
|
+
def start_trace
|
28
|
+
@trace_thread ||= Thread.new do
|
29
|
+
begin
|
30
|
+
trace_loop
|
31
|
+
rescue => ex
|
32
|
+
log "trace thread: #{ex}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Turn off tracing in this client.
|
38
|
+
def stop_trace
|
39
|
+
if @trace_thread
|
40
|
+
@trace_thread.kill
|
41
|
+
@trace_thread = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
3
46
|
# displays every transaction in sequence, specially marking failed ones,
|
4
47
|
# until INT signal
|
5
48
|
class Tupelo::AppBuilder
|
6
49
|
def start_trace
|
7
|
-
child passive: true do
|
50
|
+
child passive: true do
|
8
51
|
trap :INT do
|
9
52
|
exit!
|
10
53
|
end
|
11
54
|
|
12
|
-
|
13
|
-
log << ( "%6s %6s %6s %s\n" % %w{ tick cid status operation } )
|
14
|
-
loop do
|
15
|
-
status, tick, cid, op, tags = note.wait
|
16
|
-
unless status == :attempt
|
17
|
-
s = status == :failure ? "FAIL" : ""
|
18
|
-
if tags and not tags.empty?
|
19
|
-
log << ( "%6d %6d %6s %p to %p\n" % [tick, cid, s, op, tags] )
|
20
|
-
else
|
21
|
-
log << ( "%6d %6d %6s %p\n" % [tick, cid, s, op] )
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
55
|
+
trace_loop
|
25
56
|
end
|
26
57
|
end
|
27
58
|
end
|
data/lib/tupelo/app.rb
CHANGED
@@ -67,8 +67,12 @@ module Tupelo
|
|
67
67
|
tunnel_default = !!opts[:tunnel]
|
68
68
|
persist_dir = opts[:persist_dir]
|
69
69
|
|
70
|
+
if not services_file and argv[0] !~ /^-/ ## hacky
|
71
|
+
services_file = argv.shift
|
72
|
+
end
|
73
|
+
|
70
74
|
ez_opts = {
|
71
|
-
services_file: services_file
|
75
|
+
services_file: services_file,
|
72
76
|
interactive: $stdin.isatty
|
73
77
|
}
|
74
78
|
|
@@ -2,7 +2,7 @@ class Tupelo::Client
|
|
2
2
|
module Api
|
3
3
|
def define_subspace tag, template, addr: nil
|
4
4
|
metatuple = {
|
5
|
-
|
5
|
+
TUPELO_META_KEY => "subspace",
|
6
6
|
tag: tag,
|
7
7
|
template: PortableObjectTemplate.spec_from(template),
|
8
8
|
addr: addr
|
@@ -15,7 +15,7 @@ class Tupelo::Client
|
|
15
15
|
def use_subspaces!
|
16
16
|
return if subspace(TUPELO_SUBSPACE_TAG)
|
17
17
|
define_subspace(TUPELO_SUBSPACE_TAG, {
|
18
|
-
|
18
|
+
TUPELO_META_KEY => "subspace",
|
19
19
|
tag: nil,
|
20
20
|
template: nil,
|
21
21
|
addr: nil
|
@@ -26,7 +26,10 @@ class Tupelo::Client
|
|
26
26
|
tag = tag.to_s
|
27
27
|
worker.subspaces.find {|sp| sp.tag == tag} or begin
|
28
28
|
if subscribed_tags.include? tag
|
29
|
-
read
|
29
|
+
read TUPELO_META_KEY => "subspace",
|
30
|
+
tag: tag,
|
31
|
+
template: nil,
|
32
|
+
addr: nil
|
30
33
|
worker.subspaces.find {|sp| sp.tag == tag}
|
31
34
|
end
|
32
35
|
end
|
@@ -211,9 +211,19 @@ class Tupelo::Client
|
|
211
211
|
end
|
212
212
|
end
|
213
213
|
|
214
|
+
def check_open
|
215
|
+
if failed?
|
216
|
+
# checking this here is mostly a courtesy to client code; it is possible
|
217
|
+
# (a benign race condition) for the failure flag to be set later,
|
218
|
+
# even while a #write or #take method still has not returned.
|
219
|
+
raise exception
|
220
|
+
elsif not open?
|
221
|
+
raise TransactionStateError, "not open: #{inspect}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
214
225
|
def write *tuples
|
215
|
-
|
216
|
-
failed?
|
226
|
+
check_open
|
217
227
|
check_tuples tuples
|
218
228
|
blobber = worker.blobber
|
219
229
|
@writes.concat tuples.map {|t| blobber.load(blobber.dump(t))}
|
@@ -223,9 +233,7 @@ class Tupelo::Client
|
|
223
233
|
end
|
224
234
|
|
225
235
|
def pulse *tuples
|
226
|
-
|
227
|
-
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
228
|
-
failed?
|
236
|
+
check_open
|
229
237
|
check_tuples tuples
|
230
238
|
blobber = worker.blobber
|
231
239
|
@pulses.concat tuples.map {|t| blobber.load(blobber.dump(t))}
|
@@ -234,9 +242,7 @@ class Tupelo::Client
|
|
234
242
|
|
235
243
|
# raises TransactionFailure
|
236
244
|
def take template_spec
|
237
|
-
|
238
|
-
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
239
|
-
failed?
|
245
|
+
check_open
|
240
246
|
template = worker.make_template(template_spec)
|
241
247
|
@take_templates << template
|
242
248
|
log.debug {"asking worker to take #{template_spec.inspect}"}
|
@@ -246,9 +252,7 @@ class Tupelo::Client
|
|
246
252
|
end
|
247
253
|
|
248
254
|
def take_nowait template_spec
|
249
|
-
|
250
|
-
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
251
|
-
failed?
|
255
|
+
check_open
|
252
256
|
template = worker.make_template(template_spec)
|
253
257
|
@_take_nowait ||= {}
|
254
258
|
i = @take_templates.size
|
@@ -262,9 +266,7 @@ class Tupelo::Client
|
|
262
266
|
|
263
267
|
# transaction applies only if template has a match
|
264
268
|
def read template_spec
|
265
|
-
|
266
|
-
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
267
|
-
failed?
|
269
|
+
check_open
|
268
270
|
template = worker.make_template(template_spec)
|
269
271
|
@read_templates << template
|
270
272
|
log.debug {"asking worker to read #{template_spec.inspect}"}
|
@@ -274,9 +276,7 @@ class Tupelo::Client
|
|
274
276
|
end
|
275
277
|
|
276
278
|
def read_nowait template_spec
|
277
|
-
|
278
|
-
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
279
|
-
failed?
|
279
|
+
check_open
|
280
280
|
template = worker.make_template(template_spec)
|
281
281
|
@_read_nowait ||= {}
|
282
282
|
i = @read_templates.size
|
data/lib/tupelo/client/worker.rb
CHANGED
@@ -249,9 +249,9 @@ class Tupelo::Client
|
|
249
249
|
|
250
250
|
begin
|
251
251
|
tuplespace.clear
|
252
|
-
##
|
253
|
-
## archiver is not smart enough to send exactly the delta
|
254
|
-
|
252
|
+
## In some cases, we can keep some of it, but the current
|
253
|
+
## archiver is not smart enough to send exactly the delta.
|
254
|
+
## Also, might need to abort some current transactions.
|
255
255
|
|
256
256
|
arc_tick = arc.read[0]
|
257
257
|
log.info "arc says global_tick = #{arc_tick}"
|
@@ -434,8 +434,8 @@ class Tupelo::Client
|
|
434
434
|
|
435
435
|
# Returns true if tuple is subspace metadata.
|
436
436
|
def is_meta_tuple? tuple
|
437
|
-
tuple.kind_of? Hash and tuple.key?
|
438
|
-
tuple[
|
437
|
+
tuple.kind_of? Hash and tuple.key? TUPELO_META_KEY and
|
438
|
+
tuple[TUPELO_META_KEY] == "subspace"
|
439
439
|
end
|
440
440
|
|
441
441
|
def sniff_meta_tuple tuple
|
data/lib/tupelo/client.rb
CHANGED
@@ -11,13 +11,14 @@ module Tupelo
|
|
11
11
|
attr_reader :worker
|
12
12
|
attr_reader :tuplespace
|
13
13
|
|
14
|
-
TUPELO_SUBSPACE_TAG = "tupelo subspace"
|
14
|
+
TUPELO_SUBSPACE_TAG = "tupelo subspace".freeze
|
15
|
+
TUPELO_META_KEY = "__tupelo__".freeze
|
15
16
|
|
16
17
|
def initialize(tuplespace: SimpleTuplespace, subscribe: :all, **opts)
|
17
18
|
super **opts
|
18
19
|
@tuplespace = tuplespace
|
19
20
|
@worker = make_worker
|
20
|
-
@initial_subscriptions = subscribe
|
21
|
+
@initial_subscriptions = subscribe || []
|
21
22
|
end
|
22
23
|
|
23
24
|
def inspect
|
data/lib/tupelo/version.rb
CHANGED
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.
|
4
|
+
version: '0.18'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel VanderWerf
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-02-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: atdo
|
@@ -81,6 +81,7 @@ files:
|
|
81
81
|
- README.md
|
82
82
|
- Rakefile
|
83
83
|
- bench/pipeline.rb
|
84
|
+
- bench/tuplespace.rb
|
84
85
|
- bin/tspy
|
85
86
|
- bin/tup
|
86
87
|
- bugs/read-take.rb
|
@@ -92,6 +93,8 @@ files:
|
|
92
93
|
- example/balance-xfer-locking.rb
|
93
94
|
- example/balance-xfer-retry.rb
|
94
95
|
- example/balance-xfer.rb
|
96
|
+
- example/barrier.rb
|
97
|
+
- example/bingo/bingo-v1.rb
|
95
98
|
- example/boolean-match.rb
|
96
99
|
- example/bounded-retry.rb
|
97
100
|
- example/broker-locking.rb
|
@@ -128,7 +131,9 @@ files:
|
|
128
131
|
- example/multi-tier/memo.rb
|
129
132
|
- example/multi-tier/memo2.rb
|
130
133
|
- example/multi-tier/multi-sinatras.rb
|
134
|
+
- example/nest.rb
|
131
135
|
- example/notify.rb
|
136
|
+
- example/observer.rb
|
132
137
|
- example/optimist.rb
|
133
138
|
- example/parallel.rb
|
134
139
|
- example/pregel/distributed.rb
|
@@ -143,13 +148,16 @@ files:
|
|
143
148
|
- example/small-simplified.rb
|
144
149
|
- example/small.rb
|
145
150
|
- example/socket-broker.rb
|
151
|
+
- example/subspaces/addr-book-v1.rb
|
146
152
|
- example/subspaces/addr-book-v2.rb
|
147
153
|
- example/subspaces/addr-book.rb
|
148
154
|
- example/subspaces/pubsub.rb
|
149
155
|
- example/subspaces/ramp.rb
|
156
|
+
- example/subspaces/riemann.rb
|
150
157
|
- example/subspaces/shop/shop-v1.rb
|
151
158
|
- example/subspaces/shop/shop-v2.rb
|
152
159
|
- example/subspaces/simple.rb
|
160
|
+
- example/subspaces/sorted-set-space-OLD.rb
|
153
161
|
- example/subspaces/sorted-set-space.rb
|
154
162
|
- example/take-many.rb
|
155
163
|
- example/take-nowait-caution.rb
|
@@ -231,7 +239,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
231
239
|
version: '0'
|
232
240
|
requirements: []
|
233
241
|
rubyforge_project:
|
234
|
-
rubygems_version: 2.2.
|
242
|
+
rubygems_version: 2.2.2
|
235
243
|
signing_key:
|
236
244
|
specification_version: 4
|
237
245
|
summary: Distributed tuplespace
|