tupelo 0.14 → 0.15

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.
@@ -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