tupelo 0.1
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 +7 -0
- data/COPYING +22 -0
- data/README.md +422 -0
- data/Rakefile +77 -0
- data/bench/pipeline.rb +25 -0
- data/bugs/take-write.rb +19 -0
- data/bugs/write-read.rb +15 -0
- data/example/add.rb +19 -0
- data/example/app-and-tup.rb +30 -0
- data/example/async-transaction.rb +16 -0
- data/example/balance-xfer-locking.rb +50 -0
- data/example/balance-xfer-retry.rb +55 -0
- data/example/balance-xfer.rb +33 -0
- data/example/boolean-match.rb +32 -0
- data/example/bounded-retry.rb +35 -0
- data/example/broker-locking.rb +43 -0
- data/example/broker-optimistic-async.rb +33 -0
- data/example/broker-optimistic.rb +41 -0
- data/example/broker-queue.rb +2 -0
- data/example/cancel.rb +17 -0
- data/example/concurrent-transactions.rb +39 -0
- data/example/custom-class.rb +29 -0
- data/example/custom-search.rb +27 -0
- data/example/fail-and-retry.rb +29 -0
- data/example/hash-tuples.rb +53 -0
- data/example/increment.rb +21 -0
- data/example/lock-mgr-with-queue.rb +75 -0
- data/example/lock-mgr.rb +62 -0
- data/example/map-reduce-v2.rb +96 -0
- data/example/map-reduce.rb +77 -0
- data/example/matching.rb +9 -0
- data/example/notify.rb +35 -0
- data/example/optimist.rb +20 -0
- data/example/pulse.rb +24 -0
- data/example/read-in-trans.rb +56 -0
- data/example/small-simplified.rb +18 -0
- data/example/small.rb +76 -0
- data/example/tcp.rb +35 -0
- data/example/timeout-trans.rb +21 -0
- data/example/timeout.rb +27 -0
- data/example/tiny-client.rb +14 -0
- data/example/tiny-server.rb +12 -0
- data/example/transaction-logic.rb +40 -0
- data/example/write-wait.rb +17 -0
- data/lib/tupelo/app.rb +121 -0
- data/lib/tupelo/archiver/tuplespace.rb +68 -0
- data/lib/tupelo/archiver/worker.rb +87 -0
- data/lib/tupelo/archiver.rb +86 -0
- data/lib/tupelo/client/common.rb +10 -0
- data/lib/tupelo/client/reader.rb +124 -0
- data/lib/tupelo/client/transaction.rb +455 -0
- data/lib/tupelo/client/tuplespace.rb +50 -0
- data/lib/tupelo/client/worker.rb +493 -0
- data/lib/tupelo/client.rb +44 -0
- data/lib/tupelo/version.rb +3 -0
- data/test/lib/mock-client.rb +38 -0
- data/test/lib/mock-msg.rb +47 -0
- data/test/lib/mock-queue.rb +42 -0
- data/test/lib/mock-seq.rb +50 -0
- data/test/lib/testable-worker.rb +24 -0
- data/test/stress/concurrent-transactions.rb +42 -0
- data/test/system/test-archiver.rb +35 -0
- data/test/unit/test-mock-queue.rb +93 -0
- data/test/unit/test-mock-seq.rb +39 -0
- data/test/unit/test-ops.rb +222 -0
- metadata +134 -0
@@ -0,0 +1,455 @@
|
|
1
|
+
require 'tupelo/client/common'
|
2
|
+
require 'timeout' # just for TimeoutError
|
3
|
+
|
4
|
+
class Tupelo::Client
|
5
|
+
class TransactionError < StandardError; end
|
6
|
+
class TransactionStateError < TransactionError; end
|
7
|
+
class TransactionAbort < TransactionError; end
|
8
|
+
class TransactionFailure < TransactionError; end
|
9
|
+
|
10
|
+
module Api
|
11
|
+
# Transactions are atomic by default, and are always isolated. In the
|
12
|
+
# non-atomic case, a "transaction" is really a batch op. Without a block,
|
13
|
+
# returns the Transaction. In the block form, transaction automatically
|
14
|
+
# waits for successful completion and returns the value of the block.
|
15
|
+
def transaction atomic: true, timeout: nil
|
16
|
+
deadline = timeout && Time.now + timeout
|
17
|
+
begin
|
18
|
+
t = Transaction.new self, atomic: atomic, deadline: deadline
|
19
|
+
return t unless block_given?
|
20
|
+
val = yield t
|
21
|
+
t.commit.wait
|
22
|
+
return val
|
23
|
+
rescue TransactionFailure => ex
|
24
|
+
log.info {"retrying #{t.inspect}: #{ex}"}
|
25
|
+
retry
|
26
|
+
rescue TransactionAbort
|
27
|
+
log.info {"aborting #{t.inspect}"}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def batch &bl
|
32
|
+
transaction atomic: false, &bl
|
33
|
+
end
|
34
|
+
|
35
|
+
def abort
|
36
|
+
raise TransactionAbort
|
37
|
+
end
|
38
|
+
|
39
|
+
# returns an object whose #wait method waits for write to be ack-ed
|
40
|
+
def write_nowait *tuples
|
41
|
+
t = transaction atomic: false
|
42
|
+
t.write *tuples
|
43
|
+
t.commit
|
44
|
+
end
|
45
|
+
alias write write_nowait
|
46
|
+
|
47
|
+
# waits for write to be ack-ed
|
48
|
+
def write_wait *tuples
|
49
|
+
write_nowait(*tuples).wait
|
50
|
+
end
|
51
|
+
|
52
|
+
def pulse_nowait *tuples
|
53
|
+
t = transaction atomic: false
|
54
|
+
t.pulse *tuples
|
55
|
+
t.commit
|
56
|
+
end
|
57
|
+
alias pulse pulse_nowait
|
58
|
+
|
59
|
+
def pulse_wait *tuples
|
60
|
+
pulse_nowait(*tuples).wait
|
61
|
+
end
|
62
|
+
|
63
|
+
def take template, timeout: nil
|
64
|
+
transaction timeout: timeout do |t|
|
65
|
+
tuple = t.take template
|
66
|
+
yield tuple if block_given?
|
67
|
+
tuple
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def take_nowait template
|
72
|
+
transaction do |t|
|
73
|
+
tuple = t.take_nowait template
|
74
|
+
return nil if tuple.nil?
|
75
|
+
yield tuple if block_given?
|
76
|
+
tuple
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Transaction
|
82
|
+
attr_reader :client
|
83
|
+
attr_reader :worker
|
84
|
+
attr_reader :log
|
85
|
+
attr_reader :atomic
|
86
|
+
attr_reader :deadline
|
87
|
+
attr_reader :status
|
88
|
+
attr_reader :global_tick
|
89
|
+
attr_reader :local_tick
|
90
|
+
attr_reader :exception
|
91
|
+
attr_reader :writes
|
92
|
+
attr_reader :pulses
|
93
|
+
attr_reader :take_templates
|
94
|
+
attr_reader :read_templates
|
95
|
+
attr_reader :take_tuples
|
96
|
+
attr_reader :read_tuples
|
97
|
+
attr_reader :granted_tuples
|
98
|
+
attr_reader :missing
|
99
|
+
|
100
|
+
STATES = [
|
101
|
+
OPEN = :open, # initial state
|
102
|
+
CLOSED = :closed, # client thread changes open -> closed
|
103
|
+
# after closed, client cannot touch any state
|
104
|
+
PENDING = :pending, # worker thread changes closed -> pending | failed
|
105
|
+
DONE = :done, # worker thread changes pending -> done (terminal)
|
106
|
+
FAILED = :failed # worker thread changes pending -> failed (terminal)
|
107
|
+
]
|
108
|
+
|
109
|
+
STATES.each do |s|
|
110
|
+
class_eval %{
|
111
|
+
def #{s}?; @status == #{s.inspect}; end
|
112
|
+
def #{s}!; @status = #{s.inspect}; end
|
113
|
+
private :#{s}!
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def initialize client, atomic: true, deadline: nil
|
118
|
+
@client = client
|
119
|
+
@worker = client.worker
|
120
|
+
@log = client.log
|
121
|
+
@atomic = atomic
|
122
|
+
@deadline = deadline
|
123
|
+
@global_tick = nil
|
124
|
+
@exception = nil
|
125
|
+
@local_tick = nil
|
126
|
+
@queue = client.make_queue
|
127
|
+
@mutex = Mutex.new
|
128
|
+
@writes = []
|
129
|
+
@pulses = []
|
130
|
+
@take_templates = []
|
131
|
+
@read_templates = []
|
132
|
+
@take_tuples = []
|
133
|
+
@read_tuples = []
|
134
|
+
@granted_tuples = nil
|
135
|
+
@missing = nil
|
136
|
+
@_take_nowait = nil
|
137
|
+
|
138
|
+
if deadline
|
139
|
+
worker.at deadline do
|
140
|
+
cancel(TimeoutError) if open?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
open!
|
145
|
+
end
|
146
|
+
|
147
|
+
def inspect
|
148
|
+
stat_extra =
|
149
|
+
case
|
150
|
+
when pending?
|
151
|
+
"at local_tick: #{local_tick}"
|
152
|
+
when done?
|
153
|
+
"at global_tick: #{global_tick}"
|
154
|
+
end
|
155
|
+
|
156
|
+
stat = [status, stat_extra].compact.join(" ")
|
157
|
+
|
158
|
+
ops = [ ["write", writes], ["pulse", pulses],
|
159
|
+
["take", take_templates], ["read", read_templates] ]
|
160
|
+
ops.map! do |label, tuples|
|
161
|
+
["#{label} #{tuples.map(&:inspect).join(", ")}"] unless tuples.empty?
|
162
|
+
end
|
163
|
+
ops.compact!
|
164
|
+
|
165
|
+
b = atomic ? "atomic" : "batch"
|
166
|
+
ops << "missing: #{missing}" if missing
|
167
|
+
|
168
|
+
## show take/read tuples too?
|
169
|
+
## show current tick, if open or closed
|
170
|
+
## show nowait
|
171
|
+
|
172
|
+
"<#{self.class} #{stat} #{b} #{ops.join('; ')}>"
|
173
|
+
end
|
174
|
+
|
175
|
+
# :section: Client methods
|
176
|
+
|
177
|
+
def check_tuples tuples
|
178
|
+
tuples.each do |tuple|
|
179
|
+
tuple.respond_to?(:size) and tuple.respond_to?(:fetch) or
|
180
|
+
raise ArgumentError, "Not a tuple: #{tuple.inspect}"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def write *tuples
|
185
|
+
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
186
|
+
failed?
|
187
|
+
check_tuples tuples
|
188
|
+
@writes.concat tuples
|
189
|
+
nil
|
190
|
+
end
|
191
|
+
|
192
|
+
def pulse *tuples
|
193
|
+
raise exception if failed?
|
194
|
+
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
195
|
+
failed?
|
196
|
+
check_tuples tuples
|
197
|
+
@pulses.concat tuples
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
|
201
|
+
# raises TransactionFailure
|
202
|
+
def take template_spec
|
203
|
+
raise "cannot take in batch" unless atomic
|
204
|
+
raise exception if failed?
|
205
|
+
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
206
|
+
failed?
|
207
|
+
template = worker.make_template(template_spec)
|
208
|
+
@take_templates << template
|
209
|
+
log.debug {"asking worker to take #{template_spec.inspect}"}
|
210
|
+
worker << self
|
211
|
+
wait
|
212
|
+
return take_tuples.last
|
213
|
+
end
|
214
|
+
|
215
|
+
def take_nowait template_spec
|
216
|
+
raise "cannot take in batch" unless atomic
|
217
|
+
raise exception if failed?
|
218
|
+
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
219
|
+
failed?
|
220
|
+
template = worker.make_template(template_spec)
|
221
|
+
@_take_nowait ||= {}
|
222
|
+
i = @take_templates.size
|
223
|
+
@_take_nowait[i] = true
|
224
|
+
@take_templates << template
|
225
|
+
log.debug {"asking worker to take_nowait #{template_spec.inspect}"}
|
226
|
+
worker << self
|
227
|
+
wait
|
228
|
+
return take_tuples[i]
|
229
|
+
end
|
230
|
+
|
231
|
+
# transaction applies only if template has a match
|
232
|
+
def read template_spec
|
233
|
+
raise "cannot read in batch" unless atomic
|
234
|
+
raise exception if failed?
|
235
|
+
raise TransactionStateError, "not open: #{inspect}" unless open? or
|
236
|
+
failed?
|
237
|
+
template = worker.make_template(template_spec)
|
238
|
+
@read_templates << template
|
239
|
+
log.debug {"asking worker to read #{template_spec.inspect}"}
|
240
|
+
worker << self
|
241
|
+
wait
|
242
|
+
return read_tuples.last
|
243
|
+
end
|
244
|
+
|
245
|
+
# idempotent
|
246
|
+
def commit
|
247
|
+
if open?
|
248
|
+
closed!
|
249
|
+
log.info {"committing #{inspect}"}
|
250
|
+
worker << self
|
251
|
+
else
|
252
|
+
raise exception if failed?
|
253
|
+
end
|
254
|
+
return self
|
255
|
+
end
|
256
|
+
|
257
|
+
def wait
|
258
|
+
return self if done?
|
259
|
+
raise exception if failed?
|
260
|
+
|
261
|
+
log.debug {"waiting for #{inspect}"}
|
262
|
+
@queue.pop
|
263
|
+
log.debug {"finished waiting for #{inspect}"}
|
264
|
+
|
265
|
+
return self if done? or open?
|
266
|
+
raise exception if failed?
|
267
|
+
log.error inspect
|
268
|
+
raise "bug: #{inspect}"
|
269
|
+
|
270
|
+
rescue TransactionAbort, Interrupt, TimeoutError => ex ## others?
|
271
|
+
worker << Unwaiter.new(self)
|
272
|
+
raise ex.class,
|
273
|
+
"#{ex.message}: client #{client.client_id} waiting for #{inspect}"
|
274
|
+
end
|
275
|
+
|
276
|
+
def value
|
277
|
+
wait
|
278
|
+
granted_tuples
|
279
|
+
end
|
280
|
+
|
281
|
+
class TransactionThread < Thread
|
282
|
+
def initialize t, *args
|
283
|
+
super(*args)
|
284
|
+
@transaction = t
|
285
|
+
end
|
286
|
+
def cancel
|
287
|
+
@transaction.cancel
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def async
|
292
|
+
raise ArgumentError, "must provide block" unless block_given?
|
293
|
+
TransactionThread.new(self) do ## Fiber?
|
294
|
+
begin
|
295
|
+
val = yield self
|
296
|
+
commit.wait
|
297
|
+
val
|
298
|
+
rescue TransactionFailure => ex
|
299
|
+
log.info {"retrying #{t.inspect}: #{ex}"}
|
300
|
+
retry
|
301
|
+
rescue TransactionAbort
|
302
|
+
log.info {"aborting #{t.inspect}"}
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# :section: Worker methods
|
308
|
+
|
309
|
+
def in_worker_thread?
|
310
|
+
worker.in_thread?
|
311
|
+
end
|
312
|
+
|
313
|
+
def prepare new_tuple = nil
|
314
|
+
return false if closed? or failed? # might change during this method
|
315
|
+
raise unless in_worker_thread?
|
316
|
+
|
317
|
+
if new_tuple
|
318
|
+
return true if take_tuples.all? and read_tuples.all?
|
319
|
+
|
320
|
+
take_tuples.each_with_index do |tuple, i|
|
321
|
+
if not tuple and take_templates[i] === new_tuple
|
322
|
+
take_tuples[i] = new_tuple
|
323
|
+
log.debug {"prepared #{inspect} with #{new_tuple}"}
|
324
|
+
break
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
read_tuples.each_with_index do |tuple, i|
|
329
|
+
if not tuple and read_templates[i] === new_tuple
|
330
|
+
read_tuples[i] = new_tuple
|
331
|
+
log.debug {"prepared #{inspect} with #{new_tuple}"}
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
else
|
336
|
+
## optimization: use tuple cache
|
337
|
+
skip = nil
|
338
|
+
(take_tuples.size...take_templates.size).each do |i|
|
339
|
+
take_tuples[i] = worker.tuplespace.find_match_for(
|
340
|
+
take_templates[i], distinct_from: take_tuples)
|
341
|
+
if take_tuples[i]
|
342
|
+
log.debug {"prepared #{inspect} with #{take_tuples[i]}"}
|
343
|
+
else
|
344
|
+
if @_take_nowait and @_take_nowait[i]
|
345
|
+
(skip ||= []) << i
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
skip and skip.reverse_each do |i|
|
351
|
+
take_tuples.delete_at i
|
352
|
+
take_templates.delete_at i
|
353
|
+
@_take_nowait.delete i
|
354
|
+
end
|
355
|
+
|
356
|
+
(read_tuples.size...read_templates.size).each do |i|
|
357
|
+
read_tuples[i] = worker.tuplespace.find_match_for(
|
358
|
+
read_templates[i])
|
359
|
+
if read_tuples[i]
|
360
|
+
log.debug {"prepared #{inspect} with #{read_tuples[i]}"}
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
## convert cancelling write/take to pulse
|
366
|
+
## convert cancelling take/write to read
|
367
|
+
## check that remaining take/read tuples do not cross a space boundary
|
368
|
+
|
369
|
+
if take_tuples.all? and read_tuples.all?
|
370
|
+
@queue << true
|
371
|
+
log.debug {
|
372
|
+
"prepared #{inspect}, " +
|
373
|
+
"take tuples: #{take_tuples}, read tuples: #{read_tuples}"}
|
374
|
+
end
|
375
|
+
|
376
|
+
return true
|
377
|
+
end
|
378
|
+
|
379
|
+
def unprepare missing_tuple
|
380
|
+
return false if closed? or failed? # might change during this method
|
381
|
+
raise unless in_worker_thread?
|
382
|
+
|
383
|
+
@take_tuples.each do |tuple|
|
384
|
+
if tuple == missing_tuple ## might be false positive, but ok
|
385
|
+
fail [missing_tuple]
|
386
|
+
## optimization: manage tuple cache
|
387
|
+
return false
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
@read_tuples.each do |tuple|
|
392
|
+
if tuple == missing_tuple ## might be false positive, but ok
|
393
|
+
fail [missing_tuple]
|
394
|
+
return false
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
## redo the conversions etc
|
399
|
+
return true
|
400
|
+
end
|
401
|
+
|
402
|
+
def submit
|
403
|
+
raise TransactionStateError, "must be closed" unless closed?
|
404
|
+
raise unless in_worker_thread?
|
405
|
+
|
406
|
+
@local_tick = worker.send_transaction self
|
407
|
+
pending!
|
408
|
+
end
|
409
|
+
|
410
|
+
def done global_tick, granted_tuples
|
411
|
+
raise TransactionStateError, "must be pending" unless pending?
|
412
|
+
raise unless in_worker_thread?
|
413
|
+
raise if @global_tick or @exception
|
414
|
+
|
415
|
+
@global_tick = global_tick
|
416
|
+
done!
|
417
|
+
@granted_tuples = granted_tuples
|
418
|
+
log.info {"done with #{inspect}"}
|
419
|
+
@queue << true
|
420
|
+
end
|
421
|
+
|
422
|
+
def fail missing
|
423
|
+
raise unless in_worker_thread?
|
424
|
+
raise if @global_tick or @exception
|
425
|
+
|
426
|
+
@missing = missing
|
427
|
+
@exception = TransactionFailure
|
428
|
+
failed!
|
429
|
+
@queue << false
|
430
|
+
end
|
431
|
+
|
432
|
+
def error ex
|
433
|
+
raise unless in_worker_thread?
|
434
|
+
raise if @global_tick or @exception
|
435
|
+
|
436
|
+
@exception = ex
|
437
|
+
failed!
|
438
|
+
@queue << false
|
439
|
+
end
|
440
|
+
|
441
|
+
# Called by another thread to cancel a waiting transaction.
|
442
|
+
def cancel err = TransactionAbort
|
443
|
+
worker << proc do
|
444
|
+
raise unless in_worker_thread?
|
445
|
+
if @global_tick or @exception
|
446
|
+
log.info {"cancel was applied too late: #{inspect}"}
|
447
|
+
else
|
448
|
+
@exception = err.new
|
449
|
+
failed!
|
450
|
+
@queue << false
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Tupelo::Client
|
2
|
+
# Simplest fully functional tuplespace. Not efficient for large spaces.
|
3
|
+
class SimpleTuplespace < Array
|
4
|
+
alias insert <<
|
5
|
+
|
6
|
+
def delete_once elt
|
7
|
+
if i=index(elt)
|
8
|
+
delete_at i
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def transaction inserts: [], deletes: []
|
13
|
+
deletes.each do |tuple|
|
14
|
+
delete_once tuple or raise "bug"
|
15
|
+
end
|
16
|
+
|
17
|
+
inserts.each do |tuple|
|
18
|
+
insert tuple.freeze ## freeze recursively
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_distinct_matches_for templates
|
23
|
+
templates.inject([]) do |tuples, template|
|
24
|
+
tuples << find_match_for(template, distinct_from: tuples)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def find_match_for template, distinct_from: []
|
29
|
+
find do |tuple|
|
30
|
+
template === tuple and not distinct_from.any? {|t| t.equal? tuple}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Tuplespace that stores nothing. Very efficient for large spaces!
|
36
|
+
# Useful for clients that don't need to take or read the stored tuples.
|
37
|
+
# The write, pulse, and blocking read operations all work correctly.
|
38
|
+
# The client is essentially a pub/sub client, then. See the
|
39
|
+
# --pubsub switch in tup for an example.
|
40
|
+
class NullTuplespace
|
41
|
+
include Enumerable
|
42
|
+
def each(*); end
|
43
|
+
def delete_once(*); end
|
44
|
+
def insert(*); self; end
|
45
|
+
def find_distinct_matches_for(*); raise; end ##?
|
46
|
+
def find_match_for(*); raise; end ##?
|
47
|
+
|
48
|
+
## should store space metadata, so outgoing writes can be tagged
|
49
|
+
end
|
50
|
+
end
|