tupelo 0.14 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -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/example/tcp.rb CHANGED
@@ -13,8 +13,7 @@
13
13
  #
14
14
  # ../bin/tup tcp.yaml
15
15
  #
16
- # Copy tcp.yaml to a remote host, and in the remote copy edit the
17
- # addr field to a hostname (or ip addr) isntead of 0.0.0.0.
16
+ # Copy tcp.yaml to a remote host.
18
17
  #
19
18
  # Then run a client like this:
20
19
  #
@@ -22,21 +21,21 @@
22
21
 
23
22
  require 'tupelo/app'
24
23
 
25
- svr = "tcp.yaml" # copy this file to remote clients, setting host as needed
24
+ sv = "tcp.yaml" # copy this file to remote clients, setting host as needed
26
25
  port = 9901 # Use 0 to let system choose free port
27
26
 
28
- Tupelo.application servers_file: svr,
29
- seqd_addr: [:tcp, '0.0.0.0', port],
30
- cseqd_addr: [:tcp, '0.0.0.0', port + 1],
31
- arcd_addr: [:tcp, '0.0.0.0', port + 2] do
32
- if owns_servers
33
- puts "server started; ^C to stop"
27
+ Tupelo.application services_file: sv,
28
+ seqd_addr: {proto: :tcp, bind_host: '0.0.0.0', port: port},
29
+ cseqd_addr: {proto: :tcp, bind_host: '0.0.0.0', port: port + 1},
30
+ arcd_addr: {proto: :tcp, bind_host: '0.0.0.0', port: port + 2} do
31
+ if owns_services
32
+ puts "service started; ^C to stop"
34
33
  puts "run in another terminal: ../bin/tup tcp.yaml"
35
34
  if log.level > Logger::INFO
36
35
  puts "(run with --info or --trace to see events)"
37
36
  end
38
37
  sleep
39
38
  else
40
- abort "server seems to be running already; check file #{svr.inspect}"
39
+ abort "service seems to be running already; check file #{sv.inspect}"
41
40
  end
42
41
  end
@@ -1,10 +1,10 @@
1
1
  require 'tupelo/app'
2
2
 
3
- svr = "tiny-server.yaml"
3
+ sv = "tiny-service.yaml"
4
4
 
5
- Tupelo.application servers_file: svr do
6
- if owns_servers
7
- abort "server not running"
5
+ Tupelo.application services_file: sv do
6
+ if owns_services
7
+ abort "service not running"
8
8
  end
9
9
 
10
10
  child do
@@ -0,0 +1,12 @@
1
+ require 'tupelo/app'
2
+
3
+ sv = "tiny-service.yaml"
4
+
5
+ Tupelo.application services_file: sv do
6
+ if owns_services
7
+ puts "service started"
8
+ sleep
9
+ else
10
+ abort "service seems to be running already; check file #{sv.inspect}"
11
+ end
12
+ end
@@ -4,16 +4,16 @@ module Tupelo
4
4
  class AppBuilder
5
5
  attr_reader :ez
6
6
 
7
- # Does this app own (as child processes) the seq, cseq, and arc servers?
8
- attr_reader :owns_servers
7
+ # Does this app own (as child processes) the seq, cseq, and arc services?
8
+ attr_reader :owns_services
9
9
 
10
10
  # Arguments available to application after tupelo has parsed out switches
11
11
  # and args that it recognizes.
12
12
  attr_reader :argv
13
13
 
14
- def initialize ez, owns_servers: nil, argv: argv
14
+ def initialize ez, owns_services: nil, argv: argv
15
15
  @ez = ez
16
- @owns_servers = owns_servers
16
+ @owns_services = owns_services
17
17
  @argv = argv
18
18
  end
19
19
 
data/lib/tupelo/app.rb CHANGED
@@ -38,12 +38,10 @@ module Tupelo
38
38
  end
39
39
 
40
40
  # same as application, but with tcp sockets the default
41
- def self.tcp_application argv: nil,
42
- servers_file: nil, blob_type: nil,
43
- seqd_addr: [:tcp, nil, 0],
44
- cseqd_addr: [:tcp, nil, 0],
45
- arcd_addr: [:tcp, nil, 0], **opts, &block
46
- application argv: argv, servers_file: servers_file, blob_type: blob_type,
41
+ def self.tcp_application argv: nil, services_file: nil, blob_type: nil,
42
+ seqd_addr: {}, cseqd_addr: {}, arcd_addr: {}, **opts, &block
43
+ seqd_addr[:proto] = cseqd_addr[:proto] = arcd_addr[:proto] = :tcp
44
+ application argv: argv, services_file: services_file, blob_type: blob_type,
47
45
  seqd_addr: seqd_addr, cseqd_addr: cseqd_addr, arcd_addr: arcd_addr, &block
48
46
  end
49
47
 
@@ -53,8 +51,8 @@ module Tupelo
53
51
  #blob_type: 'json' # more portable than yaml, but more restrictive
54
52
 
55
53
  def self.application argv: nil,
56
- servers_file: nil, blob_type: nil,
57
- seqd_addr: [], cseqd_addr: [], arcd_addr: [], **opts, &block
54
+ services_file: nil, blob_type: nil,
55
+ seqd_addr: {}, cseqd_addr: {}, arcd_addr: {}, **opts, &block
58
56
 
59
57
  unless argv
60
58
  argv, h = parse_args(ARGV)
@@ -68,7 +66,7 @@ module Tupelo
68
66
  persist_dir = opts[:persist_dir]
69
67
 
70
68
  ez_opts = {
71
- servers_file: servers_file || argv.shift,
69
+ services_file: services_file || argv.shift,
72
70
  interactive: $stdin.isatty
73
71
  }
74
72
 
@@ -77,37 +75,37 @@ module Tupelo
77
75
  log.level = log_level
78
76
  log.formatter = nil if verbose
79
77
  log.progname = File.basename($0)
80
- owns_servers = false
78
+ owns_services = false
81
79
 
82
- ez.start_servers do
83
- owns_servers = true
80
+ ez.start_services do
81
+ owns_services = true
84
82
 
85
83
  arc_to_seq_sock, seq_to_arc_sock = UNIXSocket.pair
86
84
  arc_to_cseq_sock, cseq_to_arc_sock = UNIXSocket.pair
87
85
 
88
- ez.server :seqd, *seqd_addr do |svr|
86
+ ez.service :seqd, **seqd_addr do |sv|
89
87
  require 'funl/message-sequencer'
90
- seq = Funl::MessageSequencer.new svr, seq_to_arc_sock, log: log,
88
+ seq = Funl::MessageSequencer.new sv, seq_to_arc_sock, log: log,
91
89
  blob_type: blob_type
92
90
  seq.start
93
91
  end
94
92
 
95
- ez.server :cseqd, *cseqd_addr do |svr|
93
+ ez.service :cseqd, **cseqd_addr do |sv|
96
94
  require 'funl/client-sequencer'
97
- cseq = Funl::ClientSequencer.new svr, cseq_to_arc_sock, log: log
95
+ cseq = Funl::ClientSequencer.new sv, cseq_to_arc_sock, log: log
98
96
  cseq.start
99
97
  end
100
98
 
101
- ez.server :arcd, *arcd_addr do |svr|
99
+ ez.service :arcd, **arcd_addr do |sv|
102
100
  require 'tupelo/archiver'
103
101
  if persist_dir
104
102
  require 'tupelo/archiver/persistent-tuplespace'
105
- arc = Archiver.new svr, seq: arc_to_seq_sock,
103
+ arc = Archiver.new sv, seq: arc_to_seq_sock,
106
104
  tuplespace: Archiver::PersistentTuplespace,
107
105
  persist_dir: persist_dir,
108
106
  cseq: arc_to_cseq_sock, log: log
109
107
  else
110
- arc = Archiver.new svr, seq: arc_to_seq_sock,
108
+ arc = Archiver.new sv, seq: arc_to_seq_sock,
111
109
  tuplespace: Archiver::Tuplespace,
112
110
  cseq: arc_to_cseq_sock, log: log
113
111
  end
@@ -115,7 +113,7 @@ module Tupelo
115
113
  end
116
114
  end
117
115
 
118
- app = AppBuilder.new(ez, owns_servers: owns_servers, argv: argv.dup)
116
+ app = AppBuilder.new(ez, owns_services: owns_services, argv: argv.dup)
119
117
 
120
118
  if enable_trace
121
119
  require 'tupelo/app/trace'
@@ -13,13 +13,12 @@ class Tupelo::Client
13
13
  Transaction
14
14
  end
15
15
 
16
- # Transactions are atomic by default, and are always isolated. In the
17
- # non-atomic case, a "transaction" is really a batch op. Without a block,
18
- # returns the Transaction. In the block form, transaction automatically
19
- # waits for successful completion and returns the value of the block.
20
- def transaction atomic: true, timeout: nil, &block
16
+ # Transactions are atomic and isolated. Without a block, returns the
17
+ # Transaction. In the block form, transaction automatically waits for
18
+ # successful completion and returns the value of the block.
19
+ def transaction timeout: nil, &block
21
20
  deadline = timeout && Time.now + timeout
22
- t = trans_class.new self, atomic: atomic, deadline: deadline
21
+ t = trans_class.new self, deadline: deadline
23
22
  return t unless block_given?
24
23
 
25
24
  val =
@@ -41,17 +40,13 @@ class Tupelo::Client
41
40
  t.cancel if t and t.open? and block_given?
42
41
  end
43
42
 
44
- def batch &bl
45
- transaction atomic: false, &bl
46
- end
47
-
48
43
  def abort
49
44
  raise TransactionAbort
50
45
  end
51
46
 
52
47
  # returns an object whose #wait method waits for write to be ack-ed
53
48
  def write_nowait *tuples
54
- t = transaction atomic: false
49
+ t = transaction
55
50
  t.write *tuples
56
51
  t.commit
57
52
  end
@@ -63,7 +58,7 @@ class Tupelo::Client
63
58
  end
64
59
 
65
60
  def pulse_nowait *tuples
66
- t = transaction atomic: false
61
+ t = transaction
67
62
  t.pulse *tuples
68
63
  t.commit
69
64
  end
@@ -94,7 +89,6 @@ class Tupelo::Client
94
89
  class Transaction
95
90
  attr_reader :client
96
91
  attr_reader :worker
97
- attr_reader :atomic
98
92
  attr_reader :deadline
99
93
  attr_reader :status
100
94
  attr_reader :global_tick
@@ -129,11 +123,10 @@ class Tupelo::Client
129
123
  }
130
124
  end
131
125
 
132
- def initialize client, atomic: true, deadline: nil
126
+ def initialize client, deadline: nil
133
127
  @client = client
134
128
  @worker = client.worker
135
129
  @log = client.log
136
- @atomic = atomic
137
130
  @deadline = deadline
138
131
  @global_tick = nil
139
132
  @exception = nil
@@ -197,15 +190,13 @@ class Tupelo::Client
197
190
  ["#{label} #{tuples.map(&:inspect).join(", ")}"] unless tuples.empty?
198
191
  end
199
192
  ops.compact!
200
-
201
- b = atomic ? "atomic" : "batch"
202
193
  ops << "missing: #{missing}" if missing
203
194
 
204
195
  ## show take/read tuples too?
205
196
  ## show current tick, if open or closed
206
197
  ## show nowait
207
198
 
208
- "<#{self.class} #{stat} #{b} #{ops.join('; ')}>"
199
+ "<#{self.class} #{stat} #{ops.join('; ')}>"
209
200
  end
210
201
 
211
202
  # :section: Client methods
@@ -240,7 +231,6 @@ class Tupelo::Client
240
231
 
241
232
  # raises TransactionFailure
242
233
  def take template_spec
243
- raise "cannot take in batch" unless atomic
244
234
  raise exception if failed?
245
235
  raise TransactionStateError, "not open: #{inspect}" unless open? or
246
236
  failed?
@@ -253,7 +243,6 @@ class Tupelo::Client
253
243
  end
254
244
 
255
245
  def take_nowait template_spec
256
- raise "cannot take in batch" unless atomic
257
246
  raise exception if failed?
258
247
  raise TransactionStateError, "not open: #{inspect}" unless open? or
259
248
  failed?
@@ -270,7 +259,6 @@ class Tupelo::Client
270
259
 
271
260
  # transaction applies only if template has a match
272
261
  def read template_spec
273
- raise "cannot read in batch" unless atomic
274
262
  raise exception if failed?
275
263
  raise TransactionStateError, "not open: #{inspect}" unless open? or
276
264
  failed?
@@ -283,7 +271,6 @@ class Tupelo::Client
283
271
  end
284
272
 
285
273
  def read_nowait template_spec
286
- raise "cannot read in batch" unless atomic
287
274
  raise exception if failed?
288
275
  raise TransactionStateError, "not open: #{inspect}" unless open? or
289
276
  failed?
@@ -486,7 +473,6 @@ class Tupelo::Client
486
473
 
487
474
  ## convert cancelling write/take to pulse
488
475
  ## convert cancelling take/write to read
489
- ## check that remaining take/read tuples do not cross a space boundary
490
476
 
491
477
  if take_tuples_for_local.all? and read_tuples_for_local.all?
492
478
  @queue << true
@@ -29,16 +29,13 @@ class Tupelo::Client
29
29
  GET_TUPLESPACE = "get tuplespace"
30
30
 
31
31
  class Operation
32
- attr_reader :atomic, :writes, :pulses, :takes, :reads
33
- ## "put" or "set" operation to ensure that at least one
34
- ## copy of a tuple exists?
32
+ attr_reader :writes, :pulses, :takes, :reads
35
33
 
36
- def initialize atomic, writes, pulses, takes, reads
37
- @atomic, @writes, @pulses, @takes, @reads =
38
- atomic, writes, pulses, takes, reads
34
+ def initialize writes, pulses, takes, reads
35
+ @writes, @pulses, @takes, @reads = writes, pulses, takes, reads
39
36
  end
40
37
 
41
- NOOP = new([], [], [], [], [])
38
+ NOOP = new([].freeze, [].freeze, [].freeze, [].freeze).freeze
42
39
 
43
40
  def to_s
44
41
  ops = [ ["write", writes], ["pulse", pulses],
@@ -47,8 +44,7 @@ class Tupelo::Client
47
44
  ["#{label} #{tuples.map(&:inspect).join(", ")}"] unless tuples.empty?
48
45
  end
49
46
  ops.compact!
50
-
51
- [atomic ? "atomic" : "batch", ops.join("; ")].join(" ")
47
+ ops.join("; ")
52
48
  end
53
49
  alias inspect to_s
54
50
  end
@@ -331,11 +327,9 @@ class Tupelo::Client
331
327
  waiter << [:attempt, msg.global_tick, msg.client_id, op, msg.tags]
332
328
  end
333
329
 
334
- granted_tuples = tuplespace.find_distinct_matches_for(op.takes)
330
+ take_tuples = tuplespace.find_distinct_matches_for(op.takes)
335
331
  read_tuples = op.reads.map {|t| tuplespace.find_match_for(t)}
336
-
337
- succeeded = !op.atomic || (granted_tuples.all? && read_tuples.all?)
338
- take_tuples = granted_tuples.compact
332
+ succeeded = take_tuples.all? && read_tuples.all?
339
333
 
340
334
  if client.subscribed_all
341
335
  write_tuples = op.writes
@@ -395,12 +389,15 @@ class Tupelo::Client
395
389
  end
396
390
  end
397
391
 
398
- log.debug {trans ? "taking #{granted_tuples}" :
399
- "client #{msg.client_id} takes #{granted_tuples}"}
392
+ log.debug {trans ? "taking #{take_tuples}" :
393
+ "client #{msg.client_id} takes #{take_tuples}"}
400
394
 
401
395
  else
402
396
  log.debug {
403
- missing = op.takes - take_tuples
397
+ missing = []
398
+ take_tuples.each_with_index do |tuple, i|
399
+ missing << op.takes[i] unless tuple
400
+ end
404
401
  trans ? "failed to take #{missing}" :
405
402
  "client #{msg.client_id} failed to take #{missing}"}
406
403
  end
@@ -431,7 +428,7 @@ class Tupelo::Client
431
428
  trans_waiters.delete trans
432
429
 
433
430
  if succeeded
434
- trans.done msg.global_tick, granted_tuples # note: tuples not frozen
431
+ trans.done msg.global_tick, take_tuples # note: tuples not frozen
435
432
  else
436
433
  trans.fail (op.takes - take_tuples) + (op.reads - read_tuples)
437
434
  end
@@ -598,12 +595,9 @@ class Tupelo::Client
598
595
  end
599
596
 
600
597
  begin
601
- msg.blob = blobber.dump([
602
- transaction.atomic,
603
- writes, pulses, takes, reads
604
- ])
598
+ msg.blob = blobber.dump([writes, pulses, takes, reads])
605
599
  ## optimization: use bitfields to identify which ops are present
606
- ## (instead of nils), and combine this with atomic flag in one int
600
+ ## (instead of nils), in one int
607
601
  rescue => ex
608
602
  raise ex, "cannot serialize #{transaction.inspect}: #{ex}"
609
603
  end
data/lib/tupelo/client.rb CHANGED
@@ -19,6 +19,10 @@ module Tupelo
19
19
  @initial_subscriptions = subscribe
20
20
  end
21
21
 
22
+ def inspect
23
+ "#<#{self.class} #{client_id} (#{log.progname}) at tick #{worker.global_tick}>"
24
+ end
25
+
22
26
  def make_worker
23
27
  Worker.new self
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Tupelo
2
- VERSION = "0.14"
2
+ VERSION = "0.15"
3
3
  end