tupelo 0.10 → 0.11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -6
- data/bin/tup +19 -0
- data/example/{remote-map-reduce.rb → map-reduce/remote-map-reduce.rb} +1 -1
- data/example/message-bus.rb +1 -0
- data/example/multi-tier/drb.rb +49 -0
- data/example/multi-tier/http.rb +67 -0
- data/example/multi-tier/kvspace.rb +113 -0
- data/example/multi-tier/memo.rb +78 -0
- data/example/multi-tier/memo2.rb +94 -0
- data/example/pubsub.rb +2 -1
- data/example/subspace.rb +8 -0
- data/example/subspaces/pubsub.rb +77 -0
- data/lib/tupelo/app.rb +1 -1
- data/lib/tupelo/archiver/tuplespace.rb +2 -0
- data/lib/tupelo/client/worker.rb +34 -13
- data/lib/tupelo/client.rb +8 -1
- data/lib/tupelo/version.rb +1 -1
- metadata +12 -6
- /data/example/{map-reduce-v2.rb → map-reduce/map-reduce-v2.rb} +0 -0
- /data/example/{map-reduce.rb → map-reduce/map-reduce.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: be780826d017e0ea442fa49d2a6403cc44cfe263
|
4
|
+
data.tar.gz: 133fa6c3218dd1fc4b06a8ea407a35ca3bdea0ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3327df2fc6fd2587dd244b99a4ed64827aeafd3314f7d3f34360e69f5598f294cd1c31010f8729c7ba913f1d1f489ce624735090074976de5aca19f88db5a646
|
7
|
+
data.tar.gz: 9b8c852daa50910f9c0823601a85618000404259893de1bc187393b8f0faeb99321ed06cbf78fb7ad488e9725326efa43202f201ae81c5a6d1e832cb9660cd3b
|
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
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. It is designed for distribution of both computation and storage, in a unified language that has both transactional and tuple-operation (read/write/take) semantics.
|
5
|
+
|
5
6
|
|
6
7
|
This is the reference implementation in ruby. It should be able to communicate with implementations in other languages. Planned implementation languages include C, Python, and Go.
|
7
8
|
|
@@ -191,7 +192,7 @@ A tuplespace is a service for coordination, configuration, and control of concur
|
|
191
192
|
|
192
193
|
See https://en.wikipedia.org/wiki/Tuple_space for general information and history. This project is strongly influenced by Masatoshi Seki's Rinda implementation, part of the Ruby standard library. See http://pragprog.com/book/sidruby/the-druby-book for a good introduction to rinda and druby.
|
193
194
|
|
194
|
-
See http://dbmsmusings.blogspot.com/2010/08/problems-with-acid-and-how-to-fix-them.html for an explanation of the
|
195
|
+
See http://dbmsmusings.blogspot.com/2010/08/problems-with-acid-and-how-to-fix-them.html for an explanation of the importance of determinism in distributed transaction systems.
|
195
196
|
|
196
197
|
What is a tuple?
|
197
198
|
----------------
|
@@ -318,11 +319,11 @@ Another use of transactions: forcing a retry when something changes:
|
|
318
319
|
|
319
320
|
This code waits on the existence of a value, but retries if the step changes while waiting. See example/pregel/distributed.rb for a use of this techinique.
|
320
321
|
|
321
|
-
Tupelo transactions are ACID in the following sense. They are Atomic and Isolated -- this is enforced by the transaction processing in each client. Consistency is enforced by the underlying message sequencer: each client's copy of the space is the deterministic result of the same sequence of operations. Durability is optional, but can be provided by the persistent archiver or other clients.
|
322
|
+
Tupelo transactions are ACID in the following sense. They are Atomic and Isolated -- this is enforced by the transaction processing in each client. Consistency is enforced by the underlying message sequencer: each client's copy of the space is the deterministic result of the same sequence of operations. This is also known as [sequential consistency] (https://en.wikipedia.org/wiki/Sequential_consistency). Durability is optional, but can be provided by the persistent archiver or other clients.
|
322
323
|
|
323
|
-
On the CAP spectrum, tupelo tends towards consistency: for all clients, write and take operations are applied in the same order, so the state of the entire system up through a given tick of discrete time is universally agreed upon. Of course, because of the difficulties of distributed systems, one client may not yet have seen the same range of ticks as another.
|
324
|
+
On the CAP spectrum, tupelo tends towards consistency: for all clients, write and take operations are applied in the same order, so the state of the entire system up through a given tick of discrete time is universally agreed upon. This is known as [state machine replication] (http://en.wikipedia.org/wiki/State%20machine%20replication). Of course, because of the difficulties of distributed systems, one client may not yet have seen the same range of ticks as another. Tupelo's replication model (especially in the use of subspaces) can also be described as [virtual synchrony](https://en.wikipedia.org/wiki/Virtual_synchrony).
|
324
325
|
|
325
|
-
Tupelo transactions do not require two-phase commit, because they are less powerful than general transactions. Each client has enough information to decide (in the same way as all other clients) whether the transaction succeeds or fails. This has performance advantages, but imposes some limitations on transactions over subspaces that are known to one client but not another.
|
326
|
+
Tupelo transactions do not require two-phase commit, because they are less powerful than general transactions. Each client has enough information to decide (in the same way as all other clients) whether the transaction succeeds or fails. This has performance advantages, but imposes some limitations on transactions over subspaces that are known to one client but not another. [Subspaces](doc/subspace.md).
|
326
327
|
|
327
328
|
|
328
329
|
Syntax
|
@@ -532,11 +533,14 @@ Other gems:
|
|
532
533
|
|
533
534
|
* yajl-ruby (only used to support --json option)
|
534
535
|
|
536
|
+
Optional gems for some of the examples:
|
537
|
+
|
538
|
+
* sinatra, http, sequel, sqlite, rbtree, leveldb-native
|
535
539
|
|
536
540
|
Contact
|
537
541
|
=======
|
538
542
|
|
539
|
-
Joel VanderWerf, vjoel@users.sourceforge.net.
|
543
|
+
Joel VanderWerf, vjoel@users.sourceforge.net, @JoelVanderWerf.
|
540
544
|
|
541
545
|
License and Copyright
|
542
546
|
========
|
data/bin/tup
CHANGED
@@ -63,6 +63,12 @@ if ARGV.delete("-h") or ARGV.delete("--help")
|
|
63
63
|
--persist-dir DIR
|
64
64
|
load and save tuplespace to DIR
|
65
65
|
|
66
|
+
--use-subspaces
|
67
|
+
enable subspaces for this tupelo server
|
68
|
+
(only needs to be set on first tup invocation)
|
69
|
+
--subscribe TAG,TAG,...
|
70
|
+
subscribe to specified subspaces; use "" for none
|
71
|
+
|
66
72
|
END
|
67
73
|
exit
|
68
74
|
end
|
@@ -73,6 +79,13 @@ argv, tupelo_opts = Tupelo.parse_args(ARGV)
|
|
73
79
|
|
74
80
|
pubsub = argv.delete("--pubsub") # not a standard tupelo opt
|
75
81
|
|
82
|
+
use_subspaces = argv.delete("--use-subspaces") # not a standard tupelo opt
|
83
|
+
|
84
|
+
if i=argv.index("--subscribe") # default is to subscribe to all
|
85
|
+
argv.delete("--subscribe")
|
86
|
+
subscribed_tags = argv.delete_at(i).split(",")
|
87
|
+
end
|
88
|
+
|
76
89
|
servers_file = argv.shift
|
77
90
|
addr = argv.shift(3)
|
78
91
|
|
@@ -100,8 +113,14 @@ Tupelo.application(
|
|
100
113
|
client_opts[:arc] = nil
|
101
114
|
client_opts[:tuplespace] = TupClient::NullTuplespace
|
102
115
|
end
|
116
|
+
|
117
|
+
if subscribed_tags
|
118
|
+
client_opts[:subscribe] = subscribed_tags
|
119
|
+
end
|
103
120
|
|
104
121
|
local TupClient, **client_opts do
|
122
|
+
use_subspaces! if use_subspaces
|
123
|
+
|
105
124
|
log.info {"cpu time: %.2fs" % Process.times.inject {|s,x|s+x}}
|
106
125
|
log.info {"starting shell. Commands: #{TupClient::CMD_ALIASES.join(", ")}"}
|
107
126
|
|
data/example/message-bus.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
# A tupelo cluster may expose its services using some other protocol so that
|
2
|
+
# process need not be tupelo-aware to use them. This example uses drb to expose
|
3
|
+
# services, but http or plain sockets would work too.
|
4
|
+
|
5
|
+
require 'drb'
|
6
|
+
|
7
|
+
rd, wr = IO.pipe # just for sharing the drb uri (or we could hardcode it)
|
8
|
+
|
9
|
+
fork do
|
10
|
+
rd.close
|
11
|
+
require 'tupelo/app'
|
12
|
+
|
13
|
+
Tupelo.application do
|
14
|
+
child do
|
15
|
+
DRb.start_service("druby://localhost:0", self)
|
16
|
+
wr.puts DRb.uri; wr.close
|
17
|
+
read ["done"]
|
18
|
+
end
|
19
|
+
|
20
|
+
child passive: true do
|
21
|
+
loop do
|
22
|
+
transaction do
|
23
|
+
_, x, y = take ["request", nil, nil]
|
24
|
+
write ["response", x, y, x + y]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
fork do # No tupelo in this process.
|
32
|
+
wr.close
|
33
|
+
uri = rd.gets.chomp; rd.close
|
34
|
+
DRb.start_service(nil, nil)
|
35
|
+
tup_client = DRbObject.new(nil, uri)
|
36
|
+
|
37
|
+
tup_client.write ["request", 3, 4]
|
38
|
+
p tup_client.take ["response", 3, 4, nil]
|
39
|
+
|
40
|
+
tup_client.write ["request", "foo", "bar"]
|
41
|
+
p tup_client.take ["response", "foo", "bar", nil]
|
42
|
+
|
43
|
+
tup_client.write ["request", ["a", "b"], ["c", "d"]]
|
44
|
+
p tup_client.take ["response", ["a", "b"], ["c", "d"], nil]
|
45
|
+
|
46
|
+
tup_client.write ["done"]
|
47
|
+
end
|
48
|
+
|
49
|
+
Process.waitall
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# A tupelo cluster may expose its services using some other protocol so that
|
2
|
+
# process need not be tupelo-aware to use them. This example uses http to expose
|
3
|
+
# services.
|
4
|
+
#
|
5
|
+
# Depends on the sinatra and http gems.
|
6
|
+
|
7
|
+
fork do
|
8
|
+
require 'tupelo/app'
|
9
|
+
|
10
|
+
Tupelo.application do
|
11
|
+
child do |client|
|
12
|
+
require 'sinatra/base'
|
13
|
+
|
14
|
+
Class.new(Sinatra::Base).class_eval do
|
15
|
+
get '/' do
|
16
|
+
"hello, world\n"
|
17
|
+
end
|
18
|
+
|
19
|
+
get '/add' do
|
20
|
+
x = Float(params["x"])
|
21
|
+
y = Float(params["y"])
|
22
|
+
client.write ["request", x, y]
|
23
|
+
resp = client.take ["response", x, y, nil]
|
24
|
+
resp.inspect + "\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
get '/exit' do
|
28
|
+
Thread.new {sleep 1; exit}
|
29
|
+
"bye\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
run!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
child passive: true do
|
37
|
+
loop do
|
38
|
+
transaction do
|
39
|
+
_, x, y = take ["request", nil, nil]
|
40
|
+
write ["response", x, y, x + y]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
fork do # No tupelo in this process.
|
48
|
+
require 'http'
|
49
|
+
|
50
|
+
url = 'http://localhost:4567'
|
51
|
+
|
52
|
+
print "trying server at #{url}"
|
53
|
+
begin
|
54
|
+
print "."
|
55
|
+
HTTP.get url
|
56
|
+
rescue Errno::ECONNREFUSED
|
57
|
+
sleep 0.2
|
58
|
+
retry
|
59
|
+
end
|
60
|
+
|
61
|
+
puts
|
62
|
+
puts HTTP.get "#{url}/add?x=1&y=2"
|
63
|
+
puts HTTP.get "#{url}/add?x=3.14&y=10"
|
64
|
+
HTTP.get "#{url}/exit"
|
65
|
+
end
|
66
|
+
|
67
|
+
Process.waitall
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# A tuple store (in-memory) that is optimized for (key_string, object) pairs.
|
2
|
+
# The object may be any serializable object (built up from numbers, booleans,
|
3
|
+
# nil, strings, hashes and arrays).
|
4
|
+
#
|
5
|
+
# Unlike in a key-value store, a given key_string may occur more than once.
|
6
|
+
# It is up to the application to decide whether to enforce key uniqueness or
|
7
|
+
# not (for example, by taking (k,...) before writing (k,v).
|
8
|
+
#
|
9
|
+
# This store should be used only by clients that subscribe to a subspace
|
10
|
+
# that can be represented as pairs. (See memo2.rb.)
|
11
|
+
#
|
12
|
+
# This store also manages meta tuples, which it keeps in an array, just like
|
13
|
+
# the default Tuplespace class does.
|
14
|
+
class KVSpace
|
15
|
+
include Enumerable
|
16
|
+
|
17
|
+
attr_reader :tag, :hash, :metas
|
18
|
+
|
19
|
+
def initialize tag
|
20
|
+
@tag = tag
|
21
|
+
clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def clear
|
25
|
+
@hash = Hash.new {|h,k| h[k] = []}
|
26
|
+
# It's up to the application to enforce that these arrays have size <=1.
|
27
|
+
@metas = []
|
28
|
+
# We are automatically subscribed to tupelo metadata (subspace defs), so
|
29
|
+
# we need to keep them somewhere.
|
30
|
+
end
|
31
|
+
|
32
|
+
def each
|
33
|
+
hash.each do |k, vs|
|
34
|
+
vs.each do |v|
|
35
|
+
yield tag, k, v
|
36
|
+
end
|
37
|
+
end
|
38
|
+
metas.each do |tuple|
|
39
|
+
yield tuple
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert tuple
|
44
|
+
if tuple.kind_of? Array
|
45
|
+
# and tuple.size == 3 and tuple[0] == tag and tuple[1].kind_of? String
|
46
|
+
# This is redundant, because of subscribe.
|
47
|
+
t, k, v = tuple
|
48
|
+
hash[k] << v
|
49
|
+
|
50
|
+
else
|
51
|
+
metas << tuple
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_once tuple
|
56
|
+
if tuple.kind_of? Array
|
57
|
+
# and tuple.size == 3 and tuple[0] == tag and tuple[1].kind_of? String
|
58
|
+
# This is redundant, because of subscribe.
|
59
|
+
t, k, v = tuple
|
60
|
+
if hash.key?(k) and hash[k].include? v
|
61
|
+
hash[k].delete v
|
62
|
+
hash.delete k if hash[k].empty?
|
63
|
+
true
|
64
|
+
else
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
else
|
69
|
+
if i=metas.index(tuple)
|
70
|
+
delete_at i
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def transaction inserts: [], deletes: [], tick: nil
|
76
|
+
deletes.each do |tuple|
|
77
|
+
delete_once tuple or raise "bug"
|
78
|
+
end
|
79
|
+
|
80
|
+
inserts.each do |tuple|
|
81
|
+
insert tuple.freeze ## should be deep_freeze
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def find_distinct_matches_for templates
|
86
|
+
templates.inject([]) do |tuples, template|
|
87
|
+
tuples << find_match_for(template, distinct_from: tuples)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_match_for template, distinct_from: []
|
92
|
+
# try to optimize if template can be satisfied by hash lookup
|
93
|
+
if template.kind_of? RubyObjectTemplate
|
94
|
+
spec = template.spec
|
95
|
+
if spec.kind_of? Array
|
96
|
+
key = spec[1]
|
97
|
+
if key.kind_of? String and spec[2] == nil
|
98
|
+
if hash.key? key
|
99
|
+
value = hash[key].last # most recently written
|
100
|
+
return [tag, key, value]
|
101
|
+
else
|
102
|
+
return nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# fall back to linear search
|
109
|
+
find do |tuple|
|
110
|
+
template === tuple and not distinct_from.any? {|t| t.equal? tuple}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# A tupelo cluster may expose its services using some other protocol so that
|
2
|
+
# process need not be tupelo-aware to use them. This example uses http to expose
|
3
|
+
# a memcache-like service. See also memo2.rb.
|
4
|
+
#
|
5
|
+
# Depends on the sinatra, json, and http gems.
|
6
|
+
|
7
|
+
require 'json'
|
8
|
+
|
9
|
+
fork do
|
10
|
+
require 'tupelo/app'
|
11
|
+
|
12
|
+
Tupelo.application do
|
13
|
+
child do |client|
|
14
|
+
require 'sinatra/base'
|
15
|
+
|
16
|
+
Class.new(Sinatra::Base).class_eval do
|
17
|
+
get '/' do
|
18
|
+
"hello, world\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/read' do
|
22
|
+
key = params["k"]
|
23
|
+
resp = client.read ["memo", key, nil]
|
24
|
+
resp.to_json + "\n"
|
25
|
+
end
|
26
|
+
|
27
|
+
get '/exit' do
|
28
|
+
Thread.new {sleep 1; exit}
|
29
|
+
"bye\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
run!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# this process does some imaginary super-important work whose results
|
37
|
+
# need to be cached.
|
38
|
+
child passive: true do
|
39
|
+
100.times do |i|
|
40
|
+
sleep 1
|
41
|
+
write ["memo", i.to_s, Time.now.to_s]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
fork do # No tupelo in this process.
|
48
|
+
require 'http'
|
49
|
+
|
50
|
+
url = 'http://localhost:4567'
|
51
|
+
|
52
|
+
print "trying server at #{url}"
|
53
|
+
begin
|
54
|
+
print "."
|
55
|
+
HTTP.get url
|
56
|
+
rescue Errno::ECONNREFUSED
|
57
|
+
sleep 0.2
|
58
|
+
retry
|
59
|
+
end
|
60
|
+
|
61
|
+
puts
|
62
|
+
puts "Getting cached data as soon as available:"
|
63
|
+
5.times do |i|
|
64
|
+
resp = HTTP.get "#{url}/read?k=#{i}"
|
65
|
+
p JSON.parse(resp)
|
66
|
+
end
|
67
|
+
|
68
|
+
puts
|
69
|
+
puts "Reviewing already cached data:"
|
70
|
+
5.times do |i|
|
71
|
+
resp = HTTP.get "#{url}/read?k=#{i}"
|
72
|
+
p JSON.parse(resp)
|
73
|
+
end
|
74
|
+
|
75
|
+
HTTP.get "#{url}/exit"
|
76
|
+
end
|
77
|
+
|
78
|
+
Process.waitall
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# Better, but more complex, implementation of memo.rb. Uses a custom tuplespace
|
2
|
+
# that is optimized for storing key-value data, rather than general tuples.
|
3
|
+
# Also, subscribes to just the relevant subspace. Consequently, this example
|
4
|
+
# should scale up to large memo spaces much better than memo.rb, which uses
|
5
|
+
# linear search.
|
6
|
+
#
|
7
|
+
# Depends on the sinatra, json, and http gems.
|
8
|
+
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
fork do
|
12
|
+
require 'tupelo/app'
|
13
|
+
require_relative 'kvspace.rb'
|
14
|
+
|
15
|
+
Tupelo.application do
|
16
|
+
local do
|
17
|
+
use_subspaces!
|
18
|
+
|
19
|
+
define_subspace(
|
20
|
+
tag: "memo",
|
21
|
+
template: [
|
22
|
+
{value: "memo"}, # tag is encoded in each tuple, for recognizing
|
23
|
+
{type: "string"}, # key in the cache, must be string
|
24
|
+
nil # value, can be any object (e.g. JSON object)
|
25
|
+
]
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
child tuplespace: [KVSpace, "memo"], subscribe: ["memo"] do |client|
|
30
|
+
require 'sinatra/base'
|
31
|
+
|
32
|
+
Class.new(Sinatra::Base).class_eval do
|
33
|
+
get '/' do
|
34
|
+
"hello, world\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/read' do
|
38
|
+
key = params["k"]
|
39
|
+
resp = client.read ["memo", key, nil]
|
40
|
+
resp.to_json + "\n"
|
41
|
+
end
|
42
|
+
|
43
|
+
get '/exit' do
|
44
|
+
Thread.new {sleep 1; exit}
|
45
|
+
"bye\n"
|
46
|
+
end
|
47
|
+
|
48
|
+
run!
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# this process does some imaginary super-important work whose results
|
53
|
+
# need to be cached.
|
54
|
+
child passive: true do
|
55
|
+
100.times do |i|
|
56
|
+
sleep 1
|
57
|
+
write ["memo", i.to_s, Time.now.to_s]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
fork do # No tupelo in this process.
|
64
|
+
require 'http'
|
65
|
+
|
66
|
+
url = 'http://localhost:4567'
|
67
|
+
|
68
|
+
print "trying server at #{url}"
|
69
|
+
begin
|
70
|
+
print "."
|
71
|
+
HTTP.get url
|
72
|
+
rescue Errno::ECONNREFUSED
|
73
|
+
sleep 0.2
|
74
|
+
retry
|
75
|
+
end
|
76
|
+
|
77
|
+
puts
|
78
|
+
puts "Getting cached data as soon as available:"
|
79
|
+
5.times do |i|
|
80
|
+
resp = HTTP.get "#{url}/read?k=#{i}"
|
81
|
+
p JSON.parse(resp)
|
82
|
+
end
|
83
|
+
|
84
|
+
puts
|
85
|
+
puts "Reviewing already cached data:"
|
86
|
+
5.times do |i|
|
87
|
+
resp = HTTP.get "#{url}/read?k=#{i}"
|
88
|
+
p JSON.parse(resp)
|
89
|
+
end
|
90
|
+
|
91
|
+
HTTP.get "#{url}/exit"
|
92
|
+
end
|
93
|
+
|
94
|
+
Process.waitall
|
data/example/pubsub.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Synchronously deliver published messages to subscribers. Subscriber gets the
|
2
2
|
# message only if waiting while the message is sent.
|
3
|
-
# Compare message-bus.rb.
|
3
|
+
# Compare message-bus.rb. Also: subspaces/pubsub.rb.
|
4
4
|
#
|
5
5
|
# Run with the --show-final-state switch to verify that published tuples
|
6
6
|
# don't stay in the space.
|
@@ -18,6 +18,7 @@ Tupelo.application do
|
|
18
18
|
|
19
19
|
N_PUBS.times do |pi|
|
20
20
|
child do
|
21
|
+
log.progname = "pub #{pi}"
|
21
22
|
read ['start']
|
22
23
|
delay = pi/10.0
|
23
24
|
sleep delay
|
data/example/subspace.rb
CHANGED
@@ -2,6 +2,7 @@ require 'tupelo/app'
|
|
2
2
|
|
3
3
|
Tupelo.application do
|
4
4
|
local do
|
5
|
+
log.progname = "before"
|
5
6
|
log [subscribed_all, subscribed_tags]
|
6
7
|
|
7
8
|
use_subspaces!
|
@@ -18,7 +19,13 @@ Tupelo.application do
|
|
18
19
|
log read_all(Object)
|
19
20
|
end
|
20
21
|
|
22
|
+
child subscribe: [], passive: true do
|
23
|
+
log.progname = "not a subscriber"
|
24
|
+
log "should never see this: #{read(subspace "foo")}"
|
25
|
+
end
|
26
|
+
|
21
27
|
cid = child subscribe: ["foo"] do
|
28
|
+
log.progname = "foo subscriber"
|
22
29
|
log [subscribed_all, subscribed_tags]
|
23
30
|
write [1]
|
24
31
|
write_wait ["abc"]
|
@@ -27,6 +34,7 @@ Tupelo.application do
|
|
27
34
|
Process.wait cid
|
28
35
|
|
29
36
|
local do
|
37
|
+
log.progname = "after"
|
30
38
|
log [subscribed_all, subscribed_tags]
|
31
39
|
log read_all(Object)
|
32
40
|
log read_all(subspace "foo")
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# Synchronously deliver published messages to subscribers. Subscriber gets the
|
2
|
+
# message only if waiting while the message is sent. Use subspaces to reduce
|
3
|
+
# total message traffic.
|
4
|
+
# Compare ../pubsub.rb.
|
5
|
+
#
|
6
|
+
# Run with the --show-final-state switch to verify that published tuples
|
7
|
+
# don't stay in the space.
|
8
|
+
|
9
|
+
require 'tupelo/app'
|
10
|
+
|
11
|
+
show_final_state = ARGV.delete "--show-final-state"
|
12
|
+
|
13
|
+
N_PUBS = 6
|
14
|
+
N_SUBS = 6
|
15
|
+
N_CHAN = 3
|
16
|
+
|
17
|
+
Tupelo.application do
|
18
|
+
local do
|
19
|
+
use_subspaces!
|
20
|
+
|
21
|
+
N_CHAN.times do |i|
|
22
|
+
define_subspace(
|
23
|
+
tag: i,
|
24
|
+
template: [
|
25
|
+
{value: i},
|
26
|
+
{type: "string"}
|
27
|
+
]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
define_subspace(
|
32
|
+
tag: "control",
|
33
|
+
template: [
|
34
|
+
{value: "control"},
|
35
|
+
nil
|
36
|
+
]
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
N_PUBS.times do |pi|
|
41
|
+
child subscribe: ["control"] do
|
42
|
+
log.progname = "pub #{pi}"
|
43
|
+
read [nil, 'start'] # first elt can only be 'control'
|
44
|
+
delay = pi/10.0
|
45
|
+
sleep delay
|
46
|
+
tag = pi % N_CHAN
|
47
|
+
pulse [tag, "pub #{pi} slept for #{delay} sec"]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
N_SUBS.times do |si|
|
52
|
+
tag = si % N_CHAN
|
53
|
+
child subscribe: [tag], passive: true do
|
54
|
+
log.progname = "sub #{si} on tag #{tag}"
|
55
|
+
loop do
|
56
|
+
log read [nil, nil]
|
57
|
+
# Note: match any pair, but in fact will only get [tag, ....]
|
58
|
+
# because of subspaces.
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if show_final_state
|
64
|
+
child passive: true do
|
65
|
+
log.progname = "final"
|
66
|
+
def self.stop
|
67
|
+
log read_all
|
68
|
+
super
|
69
|
+
end
|
70
|
+
sleep
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
local do
|
75
|
+
write ['control', 'start']
|
76
|
+
end
|
77
|
+
end
|
data/lib/tupelo/app.rb
CHANGED
data/lib/tupelo/client/worker.rb
CHANGED
@@ -106,6 +106,9 @@ class Tupelo::Client
|
|
106
106
|
@tuplespace ||= begin
|
107
107
|
if client.tuplespace.respond_to? :new
|
108
108
|
client.tuplespace.new
|
109
|
+
elsif client.tuplespace.class == Array # but not subclass of Array
|
110
|
+
tsclass, *args = client.tuplespace
|
111
|
+
tsclass.new(*args)
|
109
112
|
else
|
110
113
|
client.tuplespace
|
111
114
|
end
|
@@ -294,7 +297,7 @@ class Tupelo::Client
|
|
294
297
|
update_to_tick tick: msg.global_tick, all: true
|
295
298
|
when Funl::SUBSCRIBE
|
296
299
|
update_to_tick tick: msg.global_tick,
|
297
|
-
tags: (client.subscribed_tags
|
300
|
+
tags: (client.subscribed_tags | tags)
|
298
301
|
when Funl::UNSUBSCRIBE_ALL
|
299
302
|
update_to_tick tick: msg.global_tick, all: false
|
300
303
|
when Funl::UNSUBSCRIBE
|
@@ -332,18 +335,28 @@ class Tupelo::Client
|
|
332
335
|
read_tuples = op.reads.map {|t| tuplespace.find_match_for(t)}
|
333
336
|
|
334
337
|
succeeded = !op.atomic || (granted_tuples.all? && read_tuples.all?)
|
335
|
-
|
338
|
+
take_tuples = granted_tuples.compact
|
339
|
+
|
340
|
+
if client.subscribed_all
|
341
|
+
write_tuples = op.writes
|
342
|
+
else
|
343
|
+
write_tuples = op.writes.select do |tuple|
|
344
|
+
subspaces.any? {|subspace| subspace === tuple}
|
345
|
+
end
|
346
|
+
end
|
347
|
+
## This is duplicated effort: the sender has already done this.
|
348
|
+
## So maybe the result could be transmitted in the msg protocol?
|
336
349
|
|
337
350
|
if succeeded
|
338
|
-
log.debug {"inserting #{op.writes}; deleting #{
|
339
|
-
tuplespace.transaction inserts:
|
351
|
+
log.debug {"inserting #{op.writes}; deleting #{take_tuples}"}
|
352
|
+
tuplespace.transaction inserts: write_tuples, deletes: take_tuples,
|
340
353
|
tick: @global_tick
|
341
354
|
|
342
355
|
op.writes.each do |tuple|
|
343
356
|
sniff_meta_tuple tuple
|
344
357
|
end
|
345
358
|
|
346
|
-
|
359
|
+
take_tuples.each do |tuple|
|
347
360
|
### abstract this out
|
348
361
|
if tuple.kind_of? Hash and tuple.key? "__tupelo__"
|
349
362
|
if tuple["__tupelo__"] == "subspace" # tuple is subspace metatdata
|
@@ -372,9 +385,9 @@ class Tupelo::Client
|
|
372
385
|
log.debug {"operation belongs to this client: #{trans.inspect}"}
|
373
386
|
end
|
374
387
|
|
375
|
-
if not
|
388
|
+
if not take_tuples.empty?
|
376
389
|
if succeeded
|
377
|
-
|
390
|
+
take_tuples.each do |tuple|
|
378
391
|
prep_waiters.keep_if do |waiter|
|
379
392
|
waiter.unprepare tuple
|
380
393
|
## optimization: track number of instances of tuple, to avoid
|
@@ -387,7 +400,7 @@ class Tupelo::Client
|
|
387
400
|
|
388
401
|
else
|
389
402
|
log.debug {
|
390
|
-
missing = op.takes -
|
403
|
+
missing = op.takes - take_tuples
|
391
404
|
trans ? "failed to take #{missing}" :
|
392
405
|
"client #{msg.client_id} failed to take #{missing}"}
|
393
406
|
end
|
@@ -420,7 +433,7 @@ class Tupelo::Client
|
|
420
433
|
if succeeded
|
421
434
|
trans.done msg.global_tick, granted_tuples # note: tuples not frozen
|
422
435
|
else
|
423
|
-
trans.fail (op.takes -
|
436
|
+
trans.fail (op.takes - take_tuples) + (op.reads - read_tuples)
|
424
437
|
end
|
425
438
|
end
|
426
439
|
end
|
@@ -494,19 +507,27 @@ class Tupelo::Client
|
|
494
507
|
end
|
495
508
|
|
496
509
|
def handle_waiter waiter
|
497
|
-
tuplespace.
|
510
|
+
tuple = tuplespace.find_match_for waiter.template
|
511
|
+
if tuple
|
512
|
+
waiter.peek tuple
|
513
|
+
else
|
498
514
|
read_waiters << waiter
|
499
|
-
|
500
|
-
## but will need to expose waiter.tuple
|
515
|
+
end
|
501
516
|
end
|
502
517
|
|
503
518
|
def handle_matcher matcher
|
504
519
|
if matcher.all
|
505
520
|
tuplespace.each {|tuple| matcher.gloms tuple}
|
521
|
+
## maybe should have tuplespace.find_all_matches_for ...
|
522
|
+
## in case there is an optimization
|
506
523
|
matcher.fails
|
507
524
|
else
|
508
|
-
tuplespace.
|
525
|
+
tuple = tuplespace.find_match_for waiter.template
|
526
|
+
if tuple
|
527
|
+
waiter.peek tuple
|
528
|
+
else
|
509
529
|
matcher.fails
|
530
|
+
end
|
510
531
|
end
|
511
532
|
end
|
512
533
|
|
data/lib/tupelo/client.rb
CHANGED
@@ -36,6 +36,12 @@ module Tupelo
|
|
36
36
|
subscribe_all
|
37
37
|
when Array
|
38
38
|
subscribe @initial_subscriptions | [Tupelo::Client::TUPELO_SUBSPACE_TAG]
|
39
|
+
when String
|
40
|
+
@initial_subscriptions = [@initial_subscriptions]
|
41
|
+
subscribe @initial_subscriptions | [Tupelo::Client::TUPELO_SUBSPACE_TAG]
|
42
|
+
else
|
43
|
+
raise ArgumentError,
|
44
|
+
"bad subscription specifier: #{@initial_subscriptions}"
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
@@ -60,6 +66,7 @@ module Tupelo
|
|
60
66
|
# call this just once at start of first client (it's optional to
|
61
67
|
# preserve behavior of non-subspace-aware code)
|
62
68
|
def use_subspaces!
|
69
|
+
return if subspace(TUPELO_SUBSPACE_TAG)
|
63
70
|
define_subspace(
|
64
71
|
tag: TUPELO_SUBSPACE_TAG,
|
65
72
|
template: {
|
@@ -73,7 +80,7 @@ module Tupelo
|
|
73
80
|
|
74
81
|
def subspace tag
|
75
82
|
tag = tag.to_s
|
76
|
-
worker.subspaces.find {|sp| sp.tag == tag}
|
83
|
+
worker.subspaces.find {|sp| sp.tag == tag} ## should go thru worker queue
|
77
84
|
end
|
78
85
|
end
|
79
86
|
end
|
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.11'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joel VanderWerf
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-11-
|
11
|
+
date: 2013-11-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: atdo
|
@@ -113,7 +113,6 @@ files:
|
|
113
113
|
- example/fail-and-retry.rb
|
114
114
|
- example/load-balancer.rb
|
115
115
|
- example/read-in-trans.rb
|
116
|
-
- example/map-reduce.rb
|
117
116
|
- example/bounded-retry.rb
|
118
117
|
- example/pregel/remote.rb
|
119
118
|
- example/pregel/pagerank.rb
|
@@ -122,7 +121,6 @@ files:
|
|
122
121
|
- example/take-nowait.rb
|
123
122
|
- example/boolean-match.rb
|
124
123
|
- example/lease.rb
|
125
|
-
- example/remote-map-reduce.rb
|
126
124
|
- example/broker-locking.rb
|
127
125
|
- example/transaction-logic.rb
|
128
126
|
- example/message-bus.rb
|
@@ -138,18 +136,26 @@ files:
|
|
138
136
|
- example/balance-xfer-retry.rb
|
139
137
|
- example/custom-search.rb
|
140
138
|
- example/app-and-tup.rb
|
139
|
+
- example/multi-tier/memo2.rb
|
140
|
+
- example/multi-tier/http.rb
|
141
|
+
- example/multi-tier/kvspace.rb
|
142
|
+
- example/multi-tier/memo.rb
|
143
|
+
- example/multi-tier/drb.rb
|
141
144
|
- example/take-many.rb
|
145
|
+
- example/subspaces/pubsub.rb
|
142
146
|
- example/dphil-optimistic.rb
|
143
147
|
- example/async-transaction.rb
|
144
148
|
- example/wait-interrupt.rb
|
145
149
|
- example/zk/lock.rb
|
146
150
|
- example/subspace.rb
|
147
|
-
- example/map-reduce-v2.rb
|
148
151
|
- example/deadlock.rb
|
149
152
|
- example/add.rb
|
150
153
|
- example/dphil-optimistic-v2.rb
|
151
154
|
- example/parallel.rb
|
152
155
|
- example/tiny-client.rb
|
156
|
+
- example/map-reduce/map-reduce.rb
|
157
|
+
- example/map-reduce/remote-map-reduce.rb
|
158
|
+
- example/map-reduce/map-reduce-v2.rb
|
153
159
|
- example/lock-mgr-with-queue.rb
|
154
160
|
- example/balance-xfer.rb
|
155
161
|
- example/cancel.rb
|
@@ -202,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
202
208
|
version: '0'
|
203
209
|
requirements: []
|
204
210
|
rubyforge_project:
|
205
|
-
rubygems_version: 2.1.
|
211
|
+
rubygems_version: 2.1.11
|
206
212
|
signing_key:
|
207
213
|
specification_version: 4
|
208
214
|
summary: Distributed tuplespace
|
File without changes
|
File without changes
|