tupelo 0.13 → 0.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8a657d2c56962432faa339d693f6856f409249f6
4
- data.tar.gz: 64a58b3ab627151aeb2fc8965de21a39c1ebf3f6
3
+ metadata.gz: f4eb65df5eb526b3a885d5f0ce41736bb3ccc216
4
+ data.tar.gz: 96f7e2b11e86b299af62b6598081564e98ceb159
5
5
  SHA512:
6
- metadata.gz: 7ea7042705c178a855d4d9ad621c2b65433d0adb4ff7dd75ee855f75d4b53d2bc4301ea91f3491bfaf2847294076a9edffa176290192d004488bdf87caa0b016
7
- data.tar.gz: 7e371d515db7f86bd95c353fcd5153efd5d1f34ba6edb0c75b7602994309509615f602ba7463729326f3c8d3aa55ba290e67c0e24fac07e917d61e72d668b886
6
+ metadata.gz: bd91077b980c38858cb3b4549cec5df43ccb91a58bc209c026fe9db23c9ec6a90486c1cecd8f92c564d420fa26ef00cda93fa6b92e6e9af171d9abd23af78717
7
+ data.tar.gz: b5de5df9f3e7baf6a02a4440b05e0cbb9a3fa04d37d5e46db3e0950ff5d59e29c23ac49809a9810f94fdd1a54916c47c98ce05a651167e72c9cfe8d60bd227ea
@@ -0,0 +1,55 @@
1
+ # Factor numbers using remote hosts. Run with --trace to see contention.
2
+ # This is more "map" than "map-reduce", though you could aggregate the
3
+ # factored numbers, such as by finding the largest prime factor.
4
+
5
+ require 'tupelo/app/remote'
6
+
7
+ hosts = ARGV.shift or abort "usage: #$0 <ssh-hostname>,<ssh-hostname>,..."
8
+ hosts = hosts.split(",")
9
+
10
+ Tupelo.tcp_application do
11
+ hosts.each_with_index do |host, hi|
12
+ remote host: host, passive: true, eval: %{
13
+ require 'prime' # ruby stdlib for prime factorization
14
+ class M
15
+ def initialize nh, hi
16
+ @nh, @hi = nh, hi
17
+ end
18
+ def === x
19
+ Array === x and
20
+ x[0] == "input" and
21
+ x[1] % @nh == @hi
22
+ end
23
+ end
24
+ my_pref = M.new(#{hosts.size}, #{hi})
25
+ loop do
26
+ _, input =
27
+ begin
28
+ take(my_pref, timeout: 1.0) # fewer fails (5.0 -> none at all)
29
+ rescue TimeoutError
30
+ take(["input", Integer])
31
+ end
32
+ write ["output", input, input.prime_division]
33
+ end
34
+ }
35
+ end
36
+
37
+ local do
38
+ t0 = Time.now
39
+ inputs = 1_000_000_000_000 .. 1_000_000_000_050
40
+
41
+ inputs.each do |input|
42
+ write ["input", input]
43
+ end
44
+
45
+ inputs.size.times do
46
+ _, input, outputs = take ["output", Integer, nil]
47
+ output_str = outputs.map {|prime, exp|
48
+ exp == 1 ? prime : "#{prime}**#{exp}"}.join(" * ")
49
+ log "#{input} == #{output_str}"
50
+ end
51
+
52
+ t1 = Time.now
53
+ log "elapsed: %6.2f seconds" % (t1-t0)
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ # Factor numbers using remote hosts. Run with --trace to see contention.
2
+ # This is more "map" than "map-reduce", though you could aggregate the
3
+ # factored numbers, such as by finding the largest prime factor.
4
+
5
+ require 'tupelo/app/remote'
6
+
7
+ hosts = ARGV.shift or abort "usage: #$0 <ssh-hostname>,<ssh-hostname>,..."
8
+ hosts = hosts.split(",")
9
+
10
+ Tupelo.tcp_application do
11
+ hosts.each do |host|
12
+ remote host: host, passive: true, eval: %{
13
+ require 'prime' # ruby stdlib for prime factorization
14
+ loop do
15
+ _, input = take(["input", Integer])
16
+ write ["output", input, input.prime_division]
17
+ end
18
+ }
19
+ end
20
+
21
+ local do
22
+ t0 = Time.now
23
+ inputs = 1_000_000_000_000 .. 1_000_000_000_050
24
+
25
+ inputs.each do |input|
26
+ write ["input", input]
27
+ end
28
+
29
+ inputs.size.times do
30
+ _, input, outputs = take ["output", Integer, nil]
31
+ output_str = outputs.map {|prime, exp|
32
+ exp == 1 ? prime : "#{prime}**#{exp}"}.join(" * ")
33
+ log "#{input} == #{output_str}"
34
+ end
35
+
36
+ t1 = Time.now
37
+ log "elapsed: %6.2f seconds" % (t1-t0)
38
+ end
39
+ end
@@ -32,7 +32,7 @@ class KVSpace
32
32
  def each
33
33
  hash.each do |k, vs|
34
34
  vs.each do |v|
35
- yield tag, k, v
35
+ yield [tag, k, v]
36
36
  end
37
37
  end
38
38
  metas.each do |tuple|
@@ -67,7 +67,7 @@ class KVSpace
67
67
 
68
68
  else
69
69
  if i=metas.index(tuple)
70
- delete_at i
70
+ metas.delete_at i
71
71
  end
72
72
  end
73
73
  end
@@ -0,0 +1,145 @@
1
+ # Example of attaching a data structure to a subspace. In this case, we
2
+ # use an in-memory structure, a red-black tree, to maintain the tuples in
3
+ # sorted order. (For a simpler example, with a hash instead of a tree,
4
+ # see [memo example using subspaces](../multi-tier/memo2.rb). The process(es)
5
+ # that manages the rbtree needs to subscribe to this subspace, so it can
6
+ # apply writes to the rbtree.
7
+ #
8
+ # We also have subspaces for query commands and responses so that other clients
9
+ # can access the sorted structure. The process(es) that host the rbtree also
10
+ # subscribe to the command subspace (and write to, but not subscribe to, the
11
+ # response subspace.) The process that query
12
+ # do so by writing to the command subspace and subscribing to the response
13
+ # subspace.
14
+ #
15
+ # This is kinda like redis, but the data is distributed, not stored on the same
16
+ # process that is managing concurrency. Multiple replicas increase concurrency.
17
+ # Run this example with --show-handlers to see which replicas are responding.
18
+ #
19
+ # Note that a subspace can be sharded to different clients, and different
20
+ # clients can each use their own data structure for these tuples.
21
+
22
+ require 'tupelo/app'
23
+ require_relative 'sorted-set-space'
24
+
25
+ SHOW_HANDLERS = ARGV.delete("--show-handlers")
26
+
27
+ N_REPLICAS = 3
28
+
29
+ ab_tag = "my address book"
30
+ cmd_tag = "#{ab_tag} commands"
31
+ resp_tag = "#{ab_tag} responses"
32
+
33
+ Tupelo.application do
34
+ local do
35
+ use_subspaces!
36
+
37
+ # Subspace for tuples belonging to the addr book.
38
+ define_subspace(
39
+ tag: ab_tag,
40
+ template: [
41
+ {value: ab_tag},
42
+ {type: "string"}, # name
43
+ nil # address; can be any object
44
+ ]
45
+ )
46
+
47
+ # Subspace for commands for fetch, delete, first, last, prev, next.
48
+ # We can't use #read and #take for fetch and delete because then the
49
+ # requesting client would have to subscribe to the ab_tag subspace.
50
+ define_subspace(
51
+ tag: cmd_tag,
52
+ template: [
53
+ {value: cmd_tag},
54
+ nil, # request id, such as [client_id, uniq_id]
55
+ {type: "string"}, # cmd name
56
+ {type: "list"} # arguments
57
+ ]
58
+ )
59
+
60
+ # Subspace for responses to commands. A response identifies the command
61
+ # it is responding to in two ways: by copying it and by an id. The
62
+ # former is so that another client can "spy" on one client's query
63
+ # responses, perhaps saving effort. The latter is to distinguish between
64
+ # iterations of the same command (first, first, ...).
65
+ define_subspace(
66
+ tag: resp_tag,
67
+ template: [
68
+ {value: resp_tag},
69
+ nil, # in response to this request id
70
+ {type: "string"}, # cmd name
71
+ {type: "list"}, # arguments
72
+ nil, # result of query -- type depends on command
73
+ ]
74
+ )
75
+ end
76
+
77
+ N_REPLICAS.times do |i|
78
+ # Inserts are just writes, which are handled by Worker and SortedSetSpace,
79
+ # so this child's app loop only needs to handle the special commands.
80
+ child tuplespace: [SortedSetSpace, ab_tag],
81
+ subscribe: [ab_tag, cmd_tag], passive: true do
82
+
83
+ log.progname = "replica ##{i}"
84
+
85
+ loop do
86
+ _, rqid, cmd, args = take(subspace cmd_tag)
87
+ if SHOW_HANDLERS
88
+ log "handling request for #{cmd} #{args}"
89
+ end
90
+
91
+ case cmd
92
+ when "delete" # handled by one replica
93
+ args.each do |name|
94
+ take [ab_tag, name, nil] # propagates to all replicas
95
+ end
96
+
97
+ when "fetch"
98
+ _, _, addr = read_nowait [ab_tag, args[0], nil] # addr might be nil
99
+ write [resp_tag, rqid, cmd, args, addr]
100
+
101
+ when "next", "prev", "first", "last"
102
+ _, name, addr = read_nowait SortedSetTemplate[ab_tag, cmd, *args]
103
+ write [resp_tag, rqid, cmd, args, [name, addr]]
104
+
105
+ else # maybe write an error message in a tuple
106
+ log.error "bad command: #{cmd}"
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ child subscribe: resp_tag do
113
+ log.progname = "user agent"
114
+
115
+ counter = 0 # this is a bit hacky -- could use prev txn's global tick
116
+ next_rqid = proc { [client_id, counter+=1] }
117
+ # Protect this with a mutex if other threads need it.
118
+
119
+ # write some ab entries
120
+ write [ab_tag, "Eliza", "100 E St."]
121
+ write [ab_tag, "Alice", "100 A St."]
122
+ write [ab_tag, "Daisy", "100 D St."]
123
+ write [ab_tag, "Bob", "100 B St."]
124
+ write [ab_tag, "Charles", "100 C St."]
125
+
126
+ # make some queries
127
+ rqid = next_rqid.call
128
+ name = "Daisy"
129
+ write [cmd_tag, rqid, "fetch", [name]]
130
+ addr = take( [resp_tag, rqid, nil, nil, nil] ).last
131
+ log "found: #{name} => #{addr}"
132
+
133
+ rqid = next_rqid.call
134
+ write [cmd_tag, rqid, "first", []]
135
+ name, addr = take( [resp_tag, rqid, nil, nil, nil] ).last
136
+ log "first entry: #{name} => #{addr}"
137
+
138
+ 5.times do
139
+ rqid = next_rqid.call
140
+ write [cmd_tag, rqid, "next", [name]]
141
+ name, addr = take( [resp_tag, rqid, nil, nil, nil] ).last
142
+ log( name ? "next entry: #{name} => #{addr}" : "no more entries" )
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,146 @@
1
+ # Read-atomic multipartition transactions, as per:
2
+ # http://www.youtube.com/watch?v=_rAdJkAbGls (around minutes 28-30)
3
+ # http://www.bailis.org/blog/non-blocking-transactional-atomicity
4
+ #
5
+ # Example of transacting separately on two subspaces (i.e. shardable subsets of
6
+ # the tuplespace), but hiding intermediate tuples so that the results show up
7
+ # atomically when readers look for them. (Note that this is different from
8
+ # atomically transacting on replicas of the same shard, which is inherent in
9
+ # tupelo transactions.)
10
+ #
11
+ # In tupelo, we could use the classic tuplespace technique of taking a lock
12
+ # tuple to protect the sequence ot two transactions on the two subspaces, but
13
+ # that would reduce concurrency and require a lease mechanism in case the lock
14
+ # holder dies. That's possible, but not scalable. So we use transactions with a
15
+ # trick...
16
+ #
17
+ # Tupelo doesn't allow transactions to cross subspace boundaries (except in the
18
+ # special case of writes outside of a subspace--see
19
+ # [doc/subspace.md](doc/subspace.md)). We can get around this at the application
20
+ # level, with a few extra steps. This adds latency, but preserves effective
21
+ # atomicity from the application's point of view and does not introduce any
22
+ # fragile locks or blocking. The main trick (as in Bailis's talk) is to use a
23
+ # globally unique value -- in his talk he used a transaction id. We could use
24
+ # the global_tick of a successful transaction (same idea) or a unique id based
25
+ # on client_id -- see [example/uniq-id.rb](example/uniq-id.rb).
26
+
27
+ # todo: use a smarter data structure for the x and y subspaces
28
+
29
+ require 'tupelo/app'
30
+
31
+ N_ITER = 6
32
+ X_REPLICATIONS = 1 # number of copies of the shard of X data
33
+ Y_REPLICATIONS = 1 # number of copies of the shard of Y data
34
+
35
+ def next_local_id
36
+ @counter = 0
37
+ @counter += 1
38
+ # Protect this with a mutex or queue if other threads need it, or
39
+ # use the atomic gem. It's ok in a multiprocess app without mutex,
40
+ # because each process has its own copy.
41
+ end
42
+
43
+ Tupelo.application do
44
+
45
+ local do
46
+ use_subspaces!
47
+
48
+ define_subspace(
49
+ tag: "x",
50
+ template: {
51
+ x: {type: "number"}, # data payload
52
+ id: {type: "list"}, # [client_id, local_id]
53
+ final: {type: "boolean"} # false means pending
54
+ }
55
+ )
56
+
57
+ define_subspace(
58
+ tag: "y",
59
+ template: {
60
+ y: {type: "number"}, # data payload
61
+ id: {type: "list"}, # [client_id, local_id]
62
+ final: {type: "boolean"} # false means pending
63
+ }
64
+ )
65
+
66
+ define_subspace(
67
+ tag: "ack", # could make this per-client
68
+ template: {
69
+ ack: {type: "string"}, # state ack-ed: "pending"
70
+ id: {type: "list"} # [client_id, local_id]
71
+ }
72
+ )
73
+ end
74
+
75
+ X_REPLICATIONS.times do |xi|
76
+ child subscribe: ["x"], passive: true do
77
+ log.progname = "x#{xi}"
78
+
79
+ read x: nil, id: nil, final: nil do |t|
80
+ log t
81
+ if t["final"]
82
+ # co-writes are at least pending at this point in global time
83
+ # ("stable"), so remove pending tuple when final tuple exists.
84
+ # First responding replica wins, and the take propagates to others.
85
+ take_nowait t.merge(final: false)
86
+ else
87
+ write ack: "pending", id: t["id"]
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ Y_REPLICATIONS.times do |yi|
94
+ child subscribe: ["y"], passive: true do
95
+ log.progname = "y#{yi}"
96
+
97
+ read y: nil, id: nil, final: nil do |t|
98
+ log t
99
+ if t["final"]
100
+ # co-writes are at least pending at this point in global time
101
+ # ("stable"), so remove pending tuple when final tuple exists.
102
+ # First responding replica wins, and the take propagates to others.
103
+ take_nowait t.merge(final: false)
104
+ else
105
+ write ack: "pending", id: t["id"]
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ child subscribe: ["ack"] do
112
+ # Does not subscribe to x or y, so can only write to those spaces.
113
+ log.progname = "writer"
114
+
115
+ N_ITER.times do |i|
116
+ uniq_id = [client_id, next_local_id]
117
+
118
+ x = {x: i, id: uniq_id}
119
+ y = {y: i, id: uniq_id}
120
+
121
+ write x.merge(final: false), y.merge(final: false) # pending
122
+ (X_REPLICATIONS + Y_REPLICATIONS).times do
123
+ take ack: "pending", id: uniq_id # wait for one to be pending
124
+ end
125
+ write x.merge(final: true), y.merge(final: true)
126
+
127
+ # Note that each of the two above writes is a multi-space transaction
128
+ # which is allowed because it is purely writes (no reads or takes).
129
+ # However, this only guarantees read atomicity for tupelo clients (because
130
+ # of the global transaction ordering). If some processes are accessing the
131
+ # x and y data stores through protocols other than tupelo (such as sql
132
+ # over sockets), this is not enough--they could see inconsistent state.
133
+ # Hence the explicit wait for an ack to truly synchronize the state.
134
+ end
135
+ end
136
+
137
+ # This doesn't test that RAMP is working -- it will always see a consistent
138
+ # view because of tupelo, even without the pending/ack trick. It is more
139
+ # informative to look at the log output from the x and y clients.
140
+ child subscribe: ["x", "y"], passive: true do
141
+ log.progname = "reader"
142
+ read do |t|
143
+ log t
144
+ end
145
+ end
146
+ end
@@ -1,12 +1,17 @@
1
1
  require 'rbtree'
2
2
 
3
+ ## TODO
4
+ ##
5
+ ## generalize SortedSetSpace to accept params that indicate which fields
6
+ ## are key and value
7
+
3
8
  class SortedSetTemplate
4
9
  class << self
5
10
  alias [] new
6
11
  end
7
12
 
8
13
  # cmd can be "next", "prev", "first", "last"
9
- # for next/prev, args is ["name"]
14
+ # for next/prev, args is [key]
10
15
  # for first/last, args is empty
11
16
  def initialize tag, cmd, *args
12
17
  @tag = tag
@@ -14,15 +19,38 @@ class SortedSetTemplate
14
19
  @args = args
15
20
  end
16
21
 
17
- def === other
18
- raise ### should not need this?
19
- end
20
-
21
- def find_in rbtree
22
+ def find_in tree, distinct_from: []
23
+ # for simplicity, ignore distinct_from
24
+ # -- we never take/read multiple keys in the tree
22
25
  case @cmd
23
26
  when "first"
24
- rbtree.first
25
- ###
27
+ k, v = tree.first
28
+ k && [@tag, k, v.first]
29
+ when "last"
30
+ tree.last
31
+ k && [@tag, k, v.last]
32
+ when "prev"
33
+ k = @args[0]
34
+ (k1,v1),(k2,v2) = tree.bound(tree.first[0], k).last(2)
35
+ ## Bad rbtree! This will be much less efficient than "next".
36
+ if k == k2
37
+ k1 && [@tag, k1, v1.last]
38
+ else
39
+ k2 && [@tag, k2, v2.last]
40
+ end
41
+ ## anomaly: can't iterate through multivalues
42
+ when "next"
43
+ k = @args[0]
44
+ (k1,v1),(k2,v2) = tree.bound(k, tree.last[0]).first(2)
45
+ ## Bad rbtree! There is no bounded search with < (rather than <=)
46
+ if k == k1
47
+ k2 && [@tag, k2, v2.first]
48
+ else
49
+ k1 && [@tag, k1, v1.first]
50
+ end
51
+ else
52
+ raise "bad command"
53
+ end
26
54
  end
27
55
  end
28
56
 
@@ -30,19 +58,22 @@ end
30
58
  # The object may be any serializable object (built up from numbers, booleans,
31
59
  # nil, strings, hashes and arrays).
32
60
  #
33
- # Unlike in a key-value store, a given key_string may occur more than once.
61
+ # By default, multiple values per key are allowed. (This differs from a typical
62
+ # key-value store, in wihch a given key_string may occur only once.)
34
63
  # It is up to the application to decide whether to enforce key uniqueness or
35
64
  # not (for example, by taking (k,...) before writing (k,v).
36
65
  #
37
66
  # This store should be used only by clients that subscribe to a subspace
38
- # that can be represented as pairs. (See memo2.rb.)
67
+ # that can be represented as triples (tag, key_string, value), where
68
+ # the tag is a single literal value that is the same for all triples.
69
+ # (See memo2.rb.)
39
70
  #
40
- # This store also manages meta tuples, which it keeps in an array, just like
41
- # the default Tuplespace class does.
71
+ # This store also manages command and meta tuples, which it keeps in an array,
72
+ # just like the default Tuplespace class does.
42
73
  class SortedSetSpace
43
74
  include Enumerable
44
75
 
45
- attr_reader :tag, :hash, :metas
76
+ attr_reader :tag, :tree, :metas
46
77
 
47
78
  def initialize tag
48
79
  @tag = tag
@@ -50,17 +81,17 @@ class SortedSetSpace
50
81
  end
51
82
 
52
83
  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.
84
+ @tree = RBTree.new{|t,k| t[k] = []}
85
+ # It's up to the application to enforce one entry per key.
55
86
  @metas = []
56
87
  # We are automatically subscribed to tupelo metadata (subspace defs), so
57
- # we need to keep them somewhere.
88
+ # we need to keep them somewhere. Also, the command tuples.
58
89
  end
59
90
 
60
91
  def each
61
- hash.each do |k, vs|
92
+ tree.each do |k, vs|
62
93
  vs.each do |v|
63
- yield tag, k, v
94
+ yield [tag, k, v]
64
95
  end
65
96
  end
66
97
  metas.each do |tuple|
@@ -69,11 +100,11 @@ class SortedSetSpace
69
100
  end
70
101
 
71
102
  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
103
+ if tuple.kind_of? Array and tuple.size == 3 and
104
+ tuple[0] == tag and tuple[1].kind_of? String
105
+
106
+ _, k, v = tuple
107
+ tree[k] << v
77
108
 
78
109
  else
79
110
  metas << tuple
@@ -81,13 +112,13 @@ class SortedSetSpace
81
112
  end
82
113
 
83
114
  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?
115
+ if tuple.kind_of? Array and tuple.size == 3 and
116
+ tuple[0] == tag and tuple[1].kind_of? String
117
+
118
+ _, k, v = tuple
119
+ if tree.key?(k) and tree[k].include? v
120
+ tree[k].delete v
121
+ tree.delete k if tree[k].empty?
91
122
  true
92
123
  else
93
124
  false
@@ -95,7 +126,7 @@ class SortedSetSpace
95
126
 
96
127
  else
97
128
  if i=metas.index(tuple)
98
- delete_at i
129
+ metas.delete_at i
99
130
  end
100
131
  end
101
132
  end
@@ -119,7 +150,7 @@ class SortedSetSpace
119
150
  def find_match_for template, distinct_from: []
120
151
  case template
121
152
  when SortedSetTemplate
122
- template.find_in rbtree, distinct_from: distinct_from ###
153
+ template.find_in tree, distinct_from: distinct_from
123
154
  else
124
155
  # fall back to linear search
125
156
  find do |tuple|
@@ -0,0 +1,34 @@
1
+ # The underlying messaging protocol, funl, keeps track of a unique id per
2
+ # message. This id is awailable in the transaction object, but only after
3
+ # commit has succeeded. We can get the id from the transaction object and
4
+ # use it as a globally unique id.
5
+ #
6
+ # Another source of unique ids is the client id, which is unique per client.
7
+ # You can get an id that is uniq per message by combining it with any value
8
+ # that is unique, in that client, to the message, such as a counter.
9
+ # This has the advantage of not requiring a transaction.
10
+
11
+ require 'tupelo/app'
12
+
13
+ Tupelo.application do
14
+ local do
15
+ tr = pulse_wait ["noop"] # returns transaction
16
+ uniq_id = tr.global_tick # available after transaction commits
17
+ log "unique id is #{uniq_id}"
18
+
19
+ # now, we can use that unique id in some other tuples
20
+ write foo: "bar", id: uniq_id
21
+ log take foo: nil, id: nil
22
+
23
+ @counter = 0
24
+ next_local_id = proc { @counter+=1 }
25
+ # Protect this with a mutex or queue if other threads need it, or
26
+ # use the atomic gem.
27
+
28
+ cid = client_id
29
+
30
+ uniq_id2 = [next_local_id.call, cid]
31
+ write foo: "baz", id: uniq_id2
32
+ log take foo: nil, id: nil
33
+ end
34
+ end
@@ -6,6 +6,7 @@ class Tupelo::Client
6
6
  class TransactionStateError < TransactionError; end
7
7
  class TransactionAbort < TransactionError; end
8
8
  class TransactionFailure < TransactionError; end
9
+ class TransactionSubspaceError < TransactionError; end
9
10
 
10
11
  module Api
11
12
  def trans_class
@@ -166,6 +167,10 @@ class Tupelo::Client
166
167
  client.client_id
167
168
  end
168
169
 
170
+ def subspace tag
171
+ client.subspace tag
172
+ end
173
+
169
174
  def log *args
170
175
  if args.empty?
171
176
  @log
@@ -345,8 +350,8 @@ class Tupelo::Client
345
350
 
346
351
  rescue TransactionAbort, Interrupt, TimeoutError => ex ## others?
347
352
  worker_push Unwaiter.new(self)
348
- raise ex.class,
349
- "#{ex.message}: client #{client_id} waiting for #{inspect}"
353
+ cstr = "client #{client_id} (#{log.progname})"
354
+ raise ex.class, "#{ex.message}: #{cstr} waiting for #{inspect}"
350
355
  end
351
356
 
352
357
  def value
@@ -527,15 +527,19 @@ class Tupelo::Client
527
527
  ## in case there is an optimization
528
528
  matcher.fails
529
529
  else
530
- tuple = tuplespace.find_match_for waiter.template
530
+ tuple = tuplespace.find_match_for matcher.template
531
531
  if tuple
532
- waiter.peek tuple
532
+ matcher.peek tuple
533
533
  else
534
534
  matcher.fails
535
535
  end
536
536
  end
537
537
  end
538
538
 
539
+ def collect_tags tuple
540
+ subspaces.select {|subspace| subspace === tuple}.map(&:tag)
541
+ end
542
+
539
543
  def send_transaction transaction
540
544
  msg = message_class.new
541
545
  msg.client_id = client_id
@@ -550,14 +554,33 @@ class Tupelo::Client
550
554
  reads = transaction.read_tuples_for_remote.compact
551
555
 
552
556
  unless msg.tags
553
- tags = []
554
- tuples = [writes, pulses, takes, reads].compact.flatten(1)
555
- subspaces.each do |subspace|
556
- tuples.each do |tuple|
557
- if subspace === tuple
558
- tags << subspace.tag
559
- break
557
+ tags = nil
558
+ [takes, reads].compact.flatten(1).each do |tuple|
559
+ if tags
560
+ tuple_tags = collect_tags(tuple)
561
+ unless tuple_tags == tags
562
+ d = (tuple_tags - tags) + (tags - tuple_tags)
563
+ raise TransactionSubspaceError,
564
+ "tuples crossing subspaces: #{d} in #{transaction.inspect}"
560
565
  end
566
+ else
567
+ tags = collect_tags(tuple)
568
+ end
569
+ end
570
+ tags ||= []
571
+
572
+ write_tags = []
573
+ [writes, pulses].compact.flatten(1).each do |tuple|
574
+ write_tags |= collect_tags(tuple)
575
+ end
576
+
577
+ if takes.empty? and reads.empty?
578
+ tags = write_tags
579
+ else
580
+ d = write_tags - tags
581
+ unless d.empty?
582
+ raise TransactionSubspaceError,
583
+ "writes crossing subspaces: #{d} in #{transaction.inspect}"
561
584
  end
562
585
  end
563
586
 
data/lib/tupelo/client.rb CHANGED
@@ -80,7 +80,13 @@ module Tupelo
80
80
 
81
81
  def subspace tag
82
82
  tag = tag.to_s
83
- worker.subspaces.find {|sp| sp.tag == tag} ## should go thru worker queue
83
+ worker.subspaces.find {|sp| sp.tag == tag} or begin
84
+ if subscribed_tags.include? tag
85
+ read __tupelo__: "subspace", tag: tag, addr: nil, template: nil
86
+ worker.subspaces.find {|sp| sp.tag == tag}
87
+ end
88
+ end
89
+ ## this impl will not be safe with dynamic subspaces
84
90
  end
85
91
  end
86
92
  end
@@ -1,3 +1,3 @@
1
1
  module Tupelo
2
- VERSION = "0.13"
2
+ VERSION = "0.14"
3
3
  end
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.13'
4
+ version: '0.14'
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-12-04 00:00:00.000000000 Z
11
+ date: 2013-12-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: atdo
@@ -84,119 +84,119 @@ files:
84
84
  - lib/tupelo/app/trace.rb
85
85
  - lib/tupelo/app/builder.rb
86
86
  - lib/tupelo/client.rb
87
- - lib/tupelo/app.rb
88
- - lib/tupelo/archiver.rb
89
- - lib/tupelo/client/worker.rb
90
- - lib/tupelo/client/common.rb
91
- - lib/tupelo/client/tuplespace.rb
92
- - lib/tupelo/client/transaction.rb
93
- - lib/tupelo/client/atdo.rb
94
- - lib/tupelo/client/reader.rb
87
+ - lib/tupelo/tuplets/persistent-archiver.rb
95
88
  - lib/tupelo/tuplets/persistent-archiver/worker.rb
96
89
  - lib/tupelo/tuplets/persistent-archiver/tuplespace.rb
97
- - lib/tupelo/tuplets/persistent-archiver.rb
90
+ - lib/tupelo/util/boolean.rb
91
+ - lib/tupelo/app.rb
92
+ - lib/tupelo/archiver.rb
98
93
  - lib/tupelo/archiver/persister.rb
99
94
  - lib/tupelo/archiver/worker.rb
100
95
  - lib/tupelo/archiver/tuplespace.rb
101
96
  - lib/tupelo/archiver/persistent-tuplespace.rb
102
- - lib/tupelo/util/boolean.rb
97
+ - lib/tupelo/client/transaction.rb
98
+ - lib/tupelo/client/worker.rb
99
+ - lib/tupelo/client/reader.rb
100
+ - lib/tupelo/client/tuplespace.rb
101
+ - lib/tupelo/client/atdo.rb
102
+ - lib/tupelo/client/common.rb
103
103
  - lib/tupelo/version.rb
104
104
  - bench/pipeline.rb
105
105
  - bugs/read-take.rb
106
106
  - bugs/take-write.rb
107
- - example/pubsub.rb
108
- - example/timeout-trans.rb
109
- - example/fish01.rb
110
- - example/tiny-client.rb
111
- - example/add.rb
112
- - example/parallel.rb
113
- - example/socket-broker.rb
114
- - example/multi-tier/memo2.rb
115
- - example/multi-tier/drb.rb
116
- - example/multi-tier/memo.rb
117
- - example/multi-tier/kvspace.rb
118
- - example/multi-tier/http.rb
119
- - example/multi-tier/multi-sinatras.rb
120
- - example/app-and-tup.rb
121
- - example/small.rb
122
- - example/bounded-retry.rb
123
- - example/fish.rb
124
- - example/zk/lock.rb
125
- - example/concurrent-transactions.rb
126
- - example/cancel.rb
127
- - example/map-reduce/map-reduce-v2.rb
128
- - example/map-reduce/remote-map-reduce.rb
129
- - example/map-reduce/map-reduce.rb
130
- - example/tiny-server.rb
131
- - example/write-wait.rb
132
- - example/tcp.rb
133
107
  - example/timeout.rb
134
- - example/read-in-trans.rb
135
- - example/subspaces/simple.rb
136
- - example/subspaces/pubsub.rb
137
- - example/subspaces/addr-book-v1.rb
138
- - example/subspaces/addr-book-v2.rb
139
- - example/subspaces/shop/shop-v2.rb
140
- - example/subspaces/shop/shop-v1.rb
141
- - example/subspaces/sorted-set-space.rb
142
- - example/balance-xfer-retry.rb
143
- - example/take-nowait-caution.rb
144
- - example/lock-mgr-with-queue.rb
145
- - example/hash-tuples.rb
146
- - example/pulse.rb
147
- - example/transaction-logic.rb
148
- - example/lease.rb
149
- - example/chat/chat.rb
150
- - example/chat/chat-nohistory.rb
151
- - example/balance-xfer.rb
152
108
  - example/add-dsl.rb
153
- - example/lock-mgr.rb
154
- - example/broker-locking.rb
155
- - example/dphil-optimistic.rb
109
+ - example/remote.rb
110
+ - example/increment.rb
111
+ - example/matching.rb
112
+ - example/dphil.rb
113
+ - example/broker-optimistic.rb
156
114
  - example/fail-and-retry.rb
157
- - example/fish0.rb
115
+ - example/load-balancer.rb
116
+ - example/read-in-trans.rb
117
+ - example/bounded-retry.rb
118
+ - example/pregel/remote.rb
119
+ - example/pregel/pagerank.rb
158
120
  - example/pregel/pregel.rb
159
121
  - example/pregel/distributed.rb
160
- - example/pregel/pagerank.rb
161
122
  - example/pregel/update.rb
162
- - example/pregel/remote.rb
163
- - example/pregel/dist-opt.rb
164
- - example/dphil-optimistic-v2.rb
165
- - example/broker-optimistic-v2.rb
166
- - example/remote.rb
167
123
  - example/take-nowait.rb
168
- - example/wait-interrupt.rb
169
- - example/optimist.rb
124
+ - example/boolean-match.rb
125
+ - example/lease.rb
126
+ - example/broker-locking.rb
127
+ - example/transaction-logic.rb
170
128
  - example/message-bus.rb
129
+ - example/small-simplified.rb
130
+ - example/small.rb
131
+ - example/lock-mgr.rb
132
+ - example/take-nowait-caution.rb
133
+ - example/concurrent-transactions.rb
134
+ - example/tcp.rb
135
+ - example/notify.rb
136
+ - example/pulse.rb
137
+ - example/chat/chat.rb
138
+ - example/chat/chat-nohistory.rb
139
+ - example/hash-tuples.rb
171
140
  - example/balance-xfer-locking.rb
172
- - example/increment.rb
173
- - example/child-of-child.rb
174
- - example/custom-class.rb
175
- - example/matching.rb
141
+ - example/balance-xfer-retry.rb
176
142
  - example/custom-search.rb
177
- - example/broker-optimistic.rb
178
- - example/notify.rb
179
- - example/small-simplified.rb
180
- - example/broker-queue.rb
181
- - example/async-transaction.rb
182
- - example/boolean-match.rb
183
- - example/load-balancer.rb
143
+ - example/app-and-tup.rb
144
+ - example/multi-tier/memo2.rb
145
+ - example/multi-tier/http.rb
146
+ - example/multi-tier/multi-sinatras.rb
147
+ - example/multi-tier/kvspace.rb
148
+ - example/multi-tier/memo.rb
149
+ - example/multi-tier/drb.rb
184
150
  - example/take-many.rb
151
+ - example/subspaces/ramp.rb
152
+ - example/subspaces/sorted-set-space.rb
153
+ - example/subspaces/addr-book.rb
154
+ - example/subspaces/simple.rb
155
+ - example/subspaces/shop/shop-v2.rb
156
+ - example/subspaces/shop/shop-v1.rb
157
+ - example/subspaces/pubsub.rb
158
+ - example/subspaces/addr-book-v2.rb
159
+ - example/dphil-optimistic.rb
160
+ - example/async-transaction.rb
161
+ - example/wait-interrupt.rb
162
+ - example/fish0.rb
163
+ - example/zk/lock.rb
185
164
  - example/deadlock.rb
186
- - example/dphil.rb
187
- - test/lib/testable-worker.rb
165
+ - example/fish.rb
166
+ - example/add.rb
167
+ - example/dphil-optimistic-v2.rb
168
+ - example/parallel.rb
169
+ - example/tiny-client.rb
170
+ - example/map-reduce/map-reduce.rb
171
+ - example/map-reduce/remote-map-reduce.rb
172
+ - example/map-reduce/map-reduce-v2.rb
173
+ - example/map-reduce/prime-factor.rb
174
+ - example/map-reduce/prime-factor-balanced.rb
175
+ - example/lock-mgr-with-queue.rb
176
+ - example/balance-xfer.rb
177
+ - example/cancel.rb
178
+ - example/socket-broker.rb
179
+ - example/timeout-trans.rb
180
+ - example/uniq-id.rb
181
+ - example/optimist.rb
182
+ - example/tiny-server.rb
183
+ - example/pubsub.rb
184
+ - example/broker-optimistic-v2.rb
185
+ - example/write-wait.rb
186
+ - example/custom-class.rb
187
+ - test/stress/archiver-load.rb
188
+ - test/stress/concurrent-transactions.rb
189
+ - test/system/test-archiver.rb
190
+ - test/lib/mock-client.rb
191
+ - test/lib/time-fuzz.rb
192
+ - test/lib/mock-queue.rb
188
193
  - test/lib/mock-seq.rb
194
+ - test/lib/testable-worker.rb
189
195
  - test/lib/mock-msg.rb
190
- - test/lib/mock-queue.rb
191
- - test/lib/time-fuzz.rb
192
- - test/lib/mock-client.rb
193
- - test/system/test-archiver.rb
194
- - test/unit/test-ops.rb
195
- - test/unit/test-mock-client.rb
196
196
  - test/unit/test-mock-seq.rb
197
197
  - test/unit/test-mock-queue.rb
198
- - test/stress/concurrent-transactions.rb
199
- - test/stress/archiver-load.rb
198
+ - test/unit/test-ops.rb
199
+ - test/unit/test-mock-client.rb
200
200
  - bin/tup
201
201
  - bin/tspy
202
202
  homepage: https://github.com/vjoel/tupelo
@@ -231,8 +231,8 @@ signing_key:
231
231
  specification_version: 4
232
232
  summary: Distributed tuplespace
233
233
  test_files:
234
- - test/unit/test-ops.rb
235
- - test/unit/test-mock-client.rb
236
234
  - test/unit/test-mock-seq.rb
237
235
  - test/unit/test-mock-queue.rb
236
+ - test/unit/test-ops.rb
237
+ - test/unit/test-mock-client.rb
238
238
  has_rdoc:
@@ -1,35 +0,0 @@
1
- # more like how you would do it in redis, except that the queue is not stored in
2
- # the central server, so operations on it are not a bottleneck, FWIW
3
-
4
- require 'tupelo/app'
5
-
6
- N_PLAYERS = 10
7
-
8
- Tupelo.application do
9
- N_PLAYERS.times do
10
- # sleep rand / 10 # reduce contention -- could also randomize inserts
11
- child do
12
- me = client_id
13
- write name: me
14
-
15
- you = transaction do
16
- game = read_nowait(
17
- player1: nil,
18
- player2: me)
19
- break game["player1"] if game
20
-
21
- unless take_nowait name: me
22
- raise Tupelo::Client::TransactionFailure
23
- end
24
-
25
- you = take(name: nil)["name"]
26
- write(
27
- player1: me,
28
- player2: you)
29
- you
30
- end
31
-
32
- log "now playing with #{you}"
33
- end
34
- end
35
- end
@@ -1,34 +0,0 @@
1
- require 'tupelo/app'
2
-
3
- ### need a programmatic way to start up clients
4
-
5
- Tupelo.application do |app|
6
-
7
- app.child do ## local still hangs
8
- 3.times do |i|
9
- app.child do
10
- write [i]
11
- log "wrote #{i}"
12
- end
13
- end
14
-
15
- 3.times do
16
- log take [nil]
17
- end
18
- end
19
- end
20
-
21
- __END__
22
-
23
- this hangs sometimes but not always:
24
-
25
- tick cid status operation
26
- A: client 3: wrote 0
27
- A: client 4: wrote 1
28
- 1 3 batch write [0]
29
- 2 4 batch write [1]
30
- A: client 2: [0]
31
- 3 2 atomic take [0]
32
- 4 2 atomic take [1]
33
- A: client 2: [1]
34
- A: client 5: wrote 2
data/example/fish01.rb DELETED
@@ -1,48 +0,0 @@
1
- # This works, but requires a fix-up step.
2
-
3
- require 'tupelo/app'
4
-
5
- Tupelo.application do
6
- 2.times do
7
- child passive: true do
8
- loop do
9
- fish = nil
10
-
11
- transaction do
12
- fish, _ = take([String])
13
- n, _ = take_nowait([Integer, fish])
14
- if n
15
- write [n + 1, fish]
16
- else
17
- write [1, fish] # another process might also write this, so ...
18
- end
19
- end
20
- ### what if both processes die here?
21
- transaction do # ... fix up the two tuples.
22
- n1, _ = take_nowait [Integer, fish]; abort unless n1
23
- n2, _ = take_nowait [Integer, fish]; abort unless n2
24
- #log "fixing: #{[n1 + n2, fish]}"
25
- write [n1 + n2, fish]
26
- end
27
- end
28
- end
29
- end
30
-
31
- local do
32
- seed = 3
33
- srand seed
34
- log "seed = #{seed}"
35
-
36
- fishes = %w{ trout marlin char salmon }
37
-
38
- a = fishes * 10
39
- a.shuffle!
40
- a.each do |fish|
41
- write [fish]
42
- end
43
-
44
- fishes.each do |fish|
45
- log take [10, fish]
46
- end
47
- end
48
- end
@@ -1,15 +0,0 @@
1
- #
2
- # Minor optimization:
3
-
4
- class KeyMatcher
5
- def initialize i, n
6
- @i = i
7
- @n = n
8
- end
9
-
10
- def === id
11
- id % @n == @i
12
- end
13
- end
14
-
15
- vertex = take id: v_id_matcher, step: step, rank: nil, active: true
@@ -1,106 +0,0 @@
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