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