tupelo 0.17 → 0.18
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 +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
|