tupelo 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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