tupelo 0.7 → 0.8

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,70 @@
1
+ # A lock using a distributed queue, like the zookeeper lock:
2
+ #
3
+ # http://zookeeper.apache.org/doc/r3.3.1/recipes.html#sc_recipes_Locks
4
+ #
5
+ # Like the zk example:
6
+ #
7
+ # - there is no specialized lock manager process
8
+ #
9
+ # - there is no thundering herd effect when the head of the queue advances
10
+ #
11
+ # - queue state is globally visible (and you could add tuples that record
12
+ # which client_id is holding the lock or waiting for it)
13
+ #
14
+ # Unlike the zk example:
15
+ #
16
+ # - there is no mechanism for dealing with clients that disappear, either while
17
+ # holding the lock, or while waiting. See example/lease.rb for a solution.
18
+
19
+ require 'tupelo/app'
20
+
21
+ N_CLIENT = 3
22
+ N_ITER = 3
23
+
24
+ Tupelo.application do
25
+ local do
26
+ write ["head", nil]
27
+ write ["tail", nil]
28
+ end
29
+
30
+ N_CLIENT.times do
31
+ child do
32
+ N_ITER.times do |iter|
33
+ my_wait_pos = nil
34
+
35
+ transaction do
36
+ _, head = take ["head", nil]
37
+ _, tail = take ["tail", nil]
38
+ if head
39
+ write ["head", head]
40
+ write ["tail", tail + 1]
41
+ my_wait_pos = tail + 1
42
+ else
43
+ write ["head", 0]
44
+ write ["tail", 0]
45
+ end
46
+ end
47
+
48
+ if my_wait_pos
49
+ read ["head", my_wait_pos]
50
+ end
51
+
52
+ log "working on iteration #{iter}..."
53
+ sleep 0.2
54
+ log "done"
55
+
56
+ transaction do
57
+ _, head = take ["head", nil]
58
+ _, tail = take ["tail", nil]
59
+ if head == tail
60
+ write ["head", nil]
61
+ write ["tail", nil]
62
+ else
63
+ write ["head", head + 1]
64
+ write ["tail", tail]
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
data/lib/tupelo/app.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  require 'easy-serve'
2
2
  require 'tupelo/client'
3
3
 
4
- ## this could be unified with the implementation of bin/tup, which is similar
5
-
6
4
  module Tupelo
7
5
  # Not an essential part of the library, but used to build up groups of
8
6
  # processes for use in examples, tests, benchmarks, etc.
@@ -22,18 +20,16 @@ module Tupelo
22
20
  end
23
21
 
24
22
  # Yields a client that runs in this process.
25
- def local client_class = Client, &block
23
+ def local client_class = Client, **opts, &block
26
24
  ez.local :seqd, :cseqd, :arcd do |seqd, cseqd, arcd|
27
- run_client client_class,
28
- seq: seqd, cseq: cseqd, arc: arcd, log: log do |client|
25
+ opts = {seq: seqd, cseq: cseqd, arc: arcd, log: log}.merge(opts)
26
+ run_client client_class, **opts do |client|
29
27
  if block
30
28
  if block.arity == 0
31
29
  client.instance_eval &block
32
30
  else
33
31
  yield client
34
32
  end
35
- else
36
- client
37
33
  end
38
34
  end
39
35
  end
@@ -45,18 +41,16 @@ module Tupelo
45
41
  # the passive flag for processes that wait for tuples and respond in some
46
42
  # way. Then you do not have to manually interrupt the whole application when
47
43
  # the active processes are done. See examples.
48
- def child client_class = Client, passive: false, &block
44
+ def child client_class = Client, passive: false, **opts, &block
49
45
  ez.child :seqd, :cseqd, :arcd, passive: passive do |seqd, cseqd, arcd|
50
- run_client client_class,
51
- seq: seqd, cseq: cseqd, arc: arcd, log: log do |client|
46
+ opts = {seq: seqd, cseq: cseqd, arc: arcd, log: log}.merge(opts)
47
+ run_client client_class, **opts do |client|
52
48
  if block
53
49
  if block.arity == 0
54
50
  client.instance_eval &block
55
51
  else
56
52
  yield client
57
53
  end
58
- else
59
- client
60
54
  end
61
55
  end
62
56
  end
@@ -74,14 +68,48 @@ module Tupelo
74
68
  client.stop if client # gracefully exit the tuplespace management thread
75
69
  end
76
70
  end
71
+
72
+ # Returns [argv, opts], leaving orig_argv unmodified. The opts hash contains
73
+ # switches (and their arguments, if any) recognized by tupelo. The argv array
74
+ # contains all unrecognized arguments.
75
+ def self.parse_args orig_argv
76
+ argv = orig_argv.dup
77
+ opts = {}
78
+
79
+ opts[:log_level] =
80
+ case
81
+ when argv.delete("--debug"); Logger::DEBUG
82
+ when argv.delete("--info"); Logger::INFO
83
+ when argv.delete("--warn"); Logger::WARN
84
+ when argv.delete("--error"); Logger::ERROR
85
+ when argv.delete("--fatal"); Logger::FATAL
86
+ else Logger::WARN
87
+ end
88
+
89
+ opts[:verbose] = argv.delete("-v")
90
+
91
+ if i = argv.index("--persist-dir")
92
+ argv.delete_at(i)
93
+ opts[:persist_dir] = argv.delete_at(i)
94
+ end
95
+
96
+ %w{--marshal --yaml --json --msgpack}.each do |switch|
97
+ s = argv.delete(switch) and
98
+ otps[:blob_type] = s.delete("--")
99
+ end
100
+
101
+ opts[:trace] = argv.delete("--trace")
102
+
103
+ [argv, opts]
104
+ end
77
105
 
78
106
  # same as application, but with tcp sockets the default
79
- def self.tcp_application argv: ARGV,
107
+ def self.tcp_application argv: nil,
80
108
  servers_file: nil, blob_type: nil,
81
109
  seqd_addr: [:tcp, nil, 0],
82
110
  cseqd_addr: [:tcp, nil, 0],
83
- arcd_addr: [:tcp, nil, 0], &block
84
- application argv: ARGV, servers_file: servers_file, blob_type: blob_type,
111
+ arcd_addr: [:tcp, nil, 0], **opts, &block
112
+ application argv: argv, servers_file: servers_file, blob_type: blob_type,
85
113
  seqd_addr: seqd_addr, cseqd_addr: cseqd_addr, arcd_addr: arcd_addr, &block
86
114
  end
87
115
 
@@ -90,35 +118,31 @@ module Tupelo
90
118
  #blob_type: 'yaml' # less general ruby objects, but cross-language
91
119
  #blob_type: 'json' # more portable than yaml, but more restrictive
92
120
 
93
- def self.application argv: ARGV,
121
+ def self.application argv: nil,
94
122
  servers_file: nil, blob_type: nil,
95
- seqd_addr: [], cseqd_addr: [], arcd_addr: [], &block
96
-
97
- log_level = case
98
- when argv.delete("--debug"); Logger::DEBUG
99
- when argv.delete("--info"); Logger::INFO
100
- when argv.delete("--warn"); Logger::WARN
101
- when argv.delete("--error"); Logger::ERROR
102
- when argv.delete("--fatal"); Logger::FATAL
103
- else Logger::WARN
123
+ seqd_addr: [], cseqd_addr: [], arcd_addr: [], **opts, &block
124
+
125
+ unless argv
126
+ argv, h = parse_args(ARGV)
127
+ opts.merge! h
104
128
  end
105
129
 
106
- unless blob_type
107
- %w{--marshal --yaml --json --msgpack}.each do |switch|
108
- s = argv.delete(switch) and
109
- blob_type ||= s.delete("--")
110
- end
111
- blob_type ||= "msgpack"
112
- end
113
-
114
- enable_trace = ARGV.delete("--trace")
130
+ log_level = opts[:log_level]
131
+ verbose = opts[:verbose]
132
+ blob_type = blob_type || "msgpack"
133
+ enable_trace = opts[:trace]
134
+ persist_dir = opts[:persist_dir]
115
135
 
116
- svrs = servers_file || argv.shift || "servers-#$$.yaml"
136
+ ez_opts = {
137
+ servers_file: servers_file || argv.shift,
138
+ interactive: $stdin.isatty
139
+ }
117
140
 
118
- EasyServe.start(servers_file: svrs) do |ez|
141
+ EasyServe.start ez_opts do |ez|
119
142
  log = ez.log
120
143
  log.level = log_level
121
- log.progname = "parent"
144
+ log.formatter = nil if verbose
145
+ log.progname = File.basename($0)
122
146
  owns_servers = false
123
147
 
124
148
  ez.start_servers do
@@ -142,8 +166,17 @@ module Tupelo
142
166
 
143
167
  ez.server :arcd, *arcd_addr do |svr|
144
168
  require 'tupelo/archiver'
145
- arc = Archiver.new svr, seq: arc_to_seq_sock,
146
- cseq: arc_to_cseq_sock, log: log
169
+ if persist_dir
170
+ require 'tupelo/archiver/persistent-tuplespace'
171
+ arc = Archiver.new svr, seq: arc_to_seq_sock,
172
+ tuplespace: Archiver::PersistentTuplespace,
173
+ persist_dir: persist_dir,
174
+ cseq: arc_to_cseq_sock, log: log
175
+ else
176
+ arc = Archiver.new svr, seq: arc_to_seq_sock,
177
+ tuplespace: Archiver::Tuplespace,
178
+ cseq: arc_to_cseq_sock, log: log
179
+ end
147
180
  arc.start
148
181
  end
149
182
  end
@@ -0,0 +1,142 @@
1
+ class Tupelo::Archiver
2
+ class PersistentTuplespace
3
+ include Enumerable
4
+
5
+ attr_reader :zero_tolerance
6
+
7
+ class Rec
8
+ attr_accessor :id
9
+ attr_accessor :count
10
+ attr_accessor :packed
11
+
12
+ # linked list of recs to update to db
13
+ attr_accessor :next_rec_to_save
14
+
15
+ def initialize ts, obj
16
+ @id = ts.next_id
17
+ @count = 0
18
+ @packed = MessagePack.pack(obj)
19
+ @next_rec_to_save = nil
20
+ end
21
+
22
+ def unmark_to_save
23
+ @next_rec_to_save = nil
24
+ end
25
+ end
26
+
27
+ def initialize(
28
+ persist_dir: nil,
29
+ zero_tolerance: Tupelo::Archiver::ZERO_TOLERANCE)
30
+
31
+ @next_id = 0
32
+ @tuple_rec = Hash.new {|h,k| h[k] = Rec.new(self, k)}
33
+ @nzero = 0
34
+ @zero_tolerance = zero_tolerance
35
+ @next_rec_to_save = nil
36
+
37
+ if persist_dir
38
+ require 'tupelo/archiver/persister.rb'
39
+ @persister = Persister.new persist_dir
40
+ read_from_persister
41
+ else
42
+ @persister = nil
43
+ end
44
+ end
45
+
46
+ def read_from_persister
47
+ @persister.each do |tuple_row|
48
+ packed = tuple_row[:packed]
49
+ tuple = MessagePack.unpack(packed)
50
+ rec = @tuple_rec[tuple]
51
+ rec.count = tuple_row[:count]
52
+ rec.id = tuple_row[:id]
53
+ rec.packed = packed
54
+ end
55
+ @next_id = @persister.next_id
56
+ ## @persister.tick # how to send this back to worker? do we need to?
57
+ end
58
+
59
+ def next_id
60
+ @next_id += 1
61
+ end
62
+
63
+ # note: multiple equal tuples are yielded once
64
+ def each
65
+ @tuple_rec.each do |tuple, rec|
66
+ yield tuple, rec.count if rec.count > 0
67
+ end
68
+ end
69
+
70
+ def mark_to_save rec
71
+ unless rec.next_rec_to_save
72
+ rec.next_rec_to_save = @next_rec_to_save
73
+ @next_rec_to_save = rec
74
+ nil
75
+ end
76
+ end
77
+
78
+ def insert tuple
79
+ rec = @tuple_rec[tuple]
80
+ rec.count += 1
81
+ mark_to_save rec if @persister
82
+ end
83
+
84
+ def delete_once tuple
85
+ rec = @tuple_rec[tuple]
86
+ if rec.count > 0
87
+ rec.count -= 1
88
+ if rec.count == 0
89
+ @nzero += 1
90
+ clear_excess_zeros if @nzero > zero_tolerance
91
+ end
92
+ mark_to_save rec if @persister
93
+ true
94
+ else
95
+ false
96
+ end
97
+ end
98
+
99
+ def transaction inserts: [], deletes: [], tick: nil
100
+ deletes.each do |tuple|
101
+ delete_once tuple or raise "bug"
102
+ end
103
+
104
+ inserts.each do |tuple|
105
+ insert tuple.freeze ## freeze recursively
106
+ end
107
+
108
+ flush tick
109
+ end
110
+
111
+ def flush tick
112
+ if @persister
113
+ @persister.flush(@next_rec_to_save, @next_id, tick)
114
+ end
115
+ end
116
+
117
+ def clear_excess_zeros
118
+ nd = (@nzero - zero_tolerance / 2)
119
+ @nzero -= nd
120
+ @tuple_rec.delete_if {|tuple, rec| rec.count == 0 && (nd-=1) >= 0}
121
+ @nzero += nd
122
+
123
+ @persister.clear_excess_zeros if @persister
124
+ end
125
+
126
+ def find_distinct_matches_for tuples
127
+ h = Hash.new(0)
128
+ tuples.map do |tuple|
129
+ if @tuple_rec[tuple].count > h[tuple]
130
+ h[tuple] += 1
131
+ tuple
132
+ else
133
+ nil
134
+ end
135
+ end
136
+ end
137
+
138
+ def find_match_for tuple
139
+ @tuple_rec[tuple].count > 0 && tuple
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,94 @@
1
+ require 'sequel'
2
+
3
+ class Tupelo::Archiver
4
+ class Persister
5
+ include Enumerable
6
+
7
+ attr_reader :dir
8
+
9
+ ## options:
10
+ ## time and size thresholds
11
+ ## sync wal
12
+ ## run in fork, at end of pipe, for parallel case
13
+ ### how to read old wal file in recovery case?
14
+ def initialize dir
15
+ @dir = dir
16
+ unless File.directory?(dir)
17
+ raise "not a dir: #{dir.inspect} -- cannot set up persister"
18
+ end
19
+ ### raise unless blobber is msgpack (or json)?
20
+ end
21
+
22
+ def wal
23
+ @wal ||= begin
24
+ File.open File.join(dir, "wal"), "w"
25
+ end
26
+ end
27
+
28
+ def db
29
+ @db ||= begin
30
+ db = Sequel.sqlite(:database => File.join(dir, "db"))
31
+ #db.loggers << Logger.new($stderr) ## client.log ?
32
+
33
+ db.create_table? :tuples do
34
+ primary_key :id
35
+ String :packed, null: false
36
+ Integer :count, null: false
37
+ end
38
+
39
+ db.create_table? :subspaces do
40
+ foreign_key :tuple_id, :tuples, index: true, null: false
41
+ Integer :tag, null: false
42
+ end
43
+
44
+ db.create_table? :global do # one row
45
+ Integer :tick
46
+ # starts from 0 when system starts, but
47
+ # we persist it in case of crash while 2 arcs running, to
48
+ # determine which is more correct
49
+ Integer :next_id
50
+ # internal state to a single arc worker
51
+ end
52
+
53
+ if db[:global].count == 0
54
+ db[:global] << {tick: 0, next_id: 0}
55
+ end
56
+
57
+ db
58
+ end
59
+ end
60
+
61
+ def each
62
+ db[:tuples].each do |tuple|
63
+ yield tuple
64
+ end
65
+ end
66
+
67
+ # rec points to linked list (via next_rec_to_save) of
68
+ # recs to flush to db but we don't have to do that for every transaction
69
+ # (configurable)
70
+ def flush rec, next_id, tick
71
+ ## if threshold etc.
72
+ db.transaction do
73
+ while rec
74
+ n = db[:tuples].filter(id: rec.id).update(count: rec.count)
75
+ if n == 0
76
+ db[:tuples].insert(id: rec.id, count: rec.count, packed: rec.packed)
77
+ end
78
+ rec.unmark_to_save
79
+ rec = rec.next_rec_to_save
80
+ end
81
+ db[:global].update(next_id: next_id, tick: tick)
82
+ end
83
+ ## rescue ???
84
+ end
85
+
86
+ def next_id
87
+ db[:global].first[:next_id]
88
+ end
89
+
90
+ def clear_excess_zeros
91
+ db[:tuples].filter(count: 0).delete ## limit rows to delete? threshold?
92
+ end
93
+ end
94
+ end
@@ -34,7 +34,7 @@ class Tupelo::Archiver
34
34
  end
35
35
  end
36
36
 
37
- def transaction inserts: [], deletes: []
37
+ def transaction inserts: [], deletes: [], tick: nil
38
38
  deletes.each do |tuple|
39
39
  delete_once tuple or raise "bug"
40
40
  end
@@ -46,7 +46,9 @@ class Tupelo::Archiver
46
46
 
47
47
  def clear_excess_zeros
48
48
  nd = (@nzero - zero_tolerance / 2)
49
+ @nzero -= nd
49
50
  @counts.delete_if {|tuple, count| count == 0 && (nd-=1) >= 0}
51
+ @nzero += nd
50
52
  end
51
53
 
52
54
  def find_distinct_matches_for tuples
@@ -4,9 +4,25 @@ class Tupelo::Archiver
4
4
  class Worker < Tupelo::Client::Worker
5
5
  include Funl::HistoryWorker
6
6
 
7
- def initialize *args
8
- super
7
+ def initialize *args, **opts
8
+ super *args
9
9
  @scheduled_actions = Hash.new {|h,k| h[k] = []}
10
+ @opts = opts
11
+ end
12
+
13
+ def tuplespace
14
+ @tuplespace ||= begin
15
+ if client.tuplespace.respond_to? :new
16
+ client.tuplespace.new **@opts
17
+ else
18
+ client.tuplespace
19
+ end
20
+ end
21
+ end
22
+
23
+ def stop
24
+ super
25
+ tuplespace.flush global_tick
10
26
  end
11
27
 
12
28
  def handle_client_request req
@@ -1,24 +1,33 @@
1
1
  require 'tupelo/client'
2
2
  require 'funl/history-client'
3
3
 
4
+ ## move to tuplet and make optional (there is a use case: static set of clients)
5
+
6
+ ## should manipulate tuples as strings (at least in msgpack/json cases) instead
7
+ ## of objects -- use msgpack extension for #hash and #== on packed objects
8
+
4
9
  class Tupelo::Archiver < Tupelo::Client; end
5
10
 
6
11
  require 'tupelo/archiver/worker'
7
- require 'tupelo/archiver/tuplespace'
12
+ require 'tupelo/archiver/tuplespace' ## unless persistent?
8
13
 
9
14
  module Tupelo
10
15
  class Archiver
11
16
  include Funl::HistoryClient
12
17
 
13
18
  attr_reader :server
19
+ attr_reader :persist_dir
14
20
  attr_reader :server_thread
15
21
 
16
22
  # How many tuples with count=0 do we permit before cleaning up?
17
23
  ZERO_TOLERANCE = 1000
18
24
 
19
- def initialize server, **opts
20
- super arc: nil, tuplespace: Tupelo::Archiver::Tuplespace, **opts
25
+ def initialize server,
26
+ tuplespace: Tupelo::Archiver::Tuplespace,
27
+ persist_dir: nil, **opts
21
28
  @server = server
29
+ @persist_dir = persist_dir
30
+ super arc: nil, tuplespace: tuplespace, **opts
22
31
  end
23
32
 
24
33
  # three kinds of requests:
@@ -43,11 +52,14 @@ module Tupelo
43
52
  end
44
53
 
45
54
  def make_worker
46
- Tupelo::Archiver::Worker.new self
55
+ if persist_dir ## ???
56
+ Tupelo::Archiver::Worker.new self, persist_dir: persist_dir
57
+ else
58
+ Tupelo::Archiver::Worker.new self
59
+ end
47
60
  end
48
61
 
49
62
  def start
50
- ## load from file?
51
63
  super # start worker thread
52
64
  @server_thread = Thread.new do
53
65
  run
@@ -65,8 +77,6 @@ module Tupelo
65
77
  Thread.new(server.accept) do |conn|
66
78
  handle_conn conn
67
79
  end
68
-
69
- ## periodically send worker request to dump space to file?
70
80
  end
71
81
  rescue => ex
72
82
  log.error ex
@@ -0,0 +1,31 @@
1
+ require 'tupelo/client'
2
+ require 'atdo'
3
+
4
+ module Tupelo
5
+ class Client
6
+ class AtDo < ::AtDo
7
+ def initialize client, **opts
8
+ @client = client
9
+ super **opts
10
+ end
11
+
12
+ # Accepts numeric +time+. Logs errors in +action+. Otherwise, same
13
+ # as ::AtDo.
14
+ def at time, &action
15
+ time = Time.at(time) if time.kind_of? Numeric
16
+ super time do
17
+ begin
18
+ action.call
19
+ rescue => ex
20
+ @client.log.error "error in action scheduled for #{time}:" +
21
+ " #{ex.class}: #{ex}\n #{ex.backtrace.join("\n ")}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def make_scheduler **opts
28
+ AtDo.new self, **opts
29
+ end
30
+ end
31
+ end
@@ -35,6 +35,7 @@ class Tupelo::Client
35
35
  retry
36
36
  rescue TransactionAbort
37
37
  log.info {"aborting #{t.inspect}"}
38
+ :abort
38
39
  ensure
39
40
  t.cancel if t and t.open? and block_given?
40
41
  end
@@ -155,6 +156,10 @@ class Tupelo::Client
155
156
  open!
156
157
  end
157
158
 
159
+ def client_id
160
+ client.client_id
161
+ end
162
+
158
163
  def log *args
159
164
  if args.empty?
160
165
  @log
@@ -277,6 +282,10 @@ class Tupelo::Client
277
282
  return read_tuples[i]
278
283
  end
279
284
 
285
+ def abort
286
+ client.abort
287
+ end
288
+
280
289
  # Client may call this before commit. In transaction do...end block,
281
290
  # this causes transaction to be re-executed.
282
291
  def fail!
@@ -289,9 +298,16 @@ class Tupelo::Client
289
298
  # idempotent
290
299
  def commit
291
300
  if open?
292
- closed!
293
- log.info {"committing #{inspect}"}
294
- worker_push self
301
+ if @writes.empty? and @pulses.empty? and
302
+ @take_tuples.empty? and @read_tuples.empty?
303
+ @global_tick = global_tick
304
+ done!
305
+ log.info {"not committing empty transaction"}
306
+ else
307
+ closed!
308
+ log.info {"committing #{inspect}"}
309
+ worker_push self
310
+ end
295
311
  else
296
312
  raise exception if failed?
297
313
  end
@@ -318,7 +334,7 @@ class Tupelo::Client
318
334
  rescue TransactionAbort, Interrupt, TimeoutError => ex ## others?
319
335
  worker_push Unwaiter.new(self)
320
336
  raise ex.class,
321
- "#{ex.message}: client #{client.client_id} waiting for #{inspect}"
337
+ "#{ex.message}: client #{client_id} waiting for #{inspect}"
322
338
  end
323
339
 
324
340
  def value
@@ -9,7 +9,7 @@ class Tupelo::Client
9
9
  end
10
10
  end
11
11
 
12
- def transaction inserts: [], deletes: []
12
+ def transaction inserts: [], deletes: [], tick: nil
13
13
  deletes.each do |tuple|
14
14
  delete_once tuple or raise "bug"
15
15
  end