tupelo 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +55 -13
- data/bin/tup +37 -89
- data/example/child-of-child.rb +34 -0
- data/example/deadlock.rb +66 -0
- data/example/lease.rb +103 -0
- data/example/parallel.rb +100 -1
- data/example/remote-map-reduce.rb +2 -1
- data/example/zk/lock.rb +70 -0
- data/lib/tupelo/app.rb +72 -39
- data/lib/tupelo/archiver/persistent-tuplespace.rb +142 -0
- data/lib/tupelo/archiver/persister.rb +94 -0
- data/lib/tupelo/archiver/tuplespace.rb +3 -1
- data/lib/tupelo/archiver/worker.rb +18 -2
- data/lib/tupelo/archiver.rb +17 -7
- data/lib/tupelo/client/atdo.rb +31 -0
- data/lib/tupelo/client/transaction.rb +20 -4
- data/lib/tupelo/client/tuplespace.rb +1 -1
- data/lib/tupelo/client/worker.rb +18 -26
- data/lib/tupelo/tuplets/persistent-archiver/tuplespace.rb +86 -0
- data/lib/tupelo/tuplets/persistent-archiver/worker.rb +114 -0
- data/lib/tupelo/tuplets/persistent-archiver.rb +86 -0
- data/lib/tupelo/version.rb +1 -1
- data/test/lib/mock-client.rb +82 -12
- data/test/lib/mock-queue.rb +2 -2
- data/test/unit/test-mock-client.rb +103 -0
- data/test/unit/test-ops.rb +123 -89
- metadata +15 -3
data/example/zk/lock.rb
ADDED
@@ -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
|
-
|
28
|
-
|
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
|
-
|
51
|
-
|
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:
|
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:
|
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:
|
121
|
+
def self.application argv: nil,
|
94
122
|
servers_file: nil, blob_type: nil,
|
95
|
-
seqd_addr: [], cseqd_addr: [], arcd_addr: [], &block
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
136
|
+
ez_opts = {
|
137
|
+
servers_file: servers_file || argv.shift,
|
138
|
+
interactive: $stdin.isatty
|
139
|
+
}
|
117
140
|
|
118
|
-
EasyServe.start
|
141
|
+
EasyServe.start ez_opts do |ez|
|
119
142
|
log = ez.log
|
120
143
|
log.level = log_level
|
121
|
-
log.
|
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
|
-
|
146
|
-
|
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
|
data/lib/tupelo/archiver.rb
CHANGED
@@ -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,
|
20
|
-
|
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
|
-
|
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
|
-
|
293
|
-
|
294
|
-
|
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 #{
|
337
|
+
"#{ex.message}: client #{client_id} waiting for #{inspect}"
|
322
338
|
end
|
323
339
|
|
324
340
|
def value
|