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