tupelo 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +22 -0
  3. data/README.md +422 -0
  4. data/Rakefile +77 -0
  5. data/bench/pipeline.rb +25 -0
  6. data/bugs/take-write.rb +19 -0
  7. data/bugs/write-read.rb +15 -0
  8. data/example/add.rb +19 -0
  9. data/example/app-and-tup.rb +30 -0
  10. data/example/async-transaction.rb +16 -0
  11. data/example/balance-xfer-locking.rb +50 -0
  12. data/example/balance-xfer-retry.rb +55 -0
  13. data/example/balance-xfer.rb +33 -0
  14. data/example/boolean-match.rb +32 -0
  15. data/example/bounded-retry.rb +35 -0
  16. data/example/broker-locking.rb +43 -0
  17. data/example/broker-optimistic-async.rb +33 -0
  18. data/example/broker-optimistic.rb +41 -0
  19. data/example/broker-queue.rb +2 -0
  20. data/example/cancel.rb +17 -0
  21. data/example/concurrent-transactions.rb +39 -0
  22. data/example/custom-class.rb +29 -0
  23. data/example/custom-search.rb +27 -0
  24. data/example/fail-and-retry.rb +29 -0
  25. data/example/hash-tuples.rb +53 -0
  26. data/example/increment.rb +21 -0
  27. data/example/lock-mgr-with-queue.rb +75 -0
  28. data/example/lock-mgr.rb +62 -0
  29. data/example/map-reduce-v2.rb +96 -0
  30. data/example/map-reduce.rb +77 -0
  31. data/example/matching.rb +9 -0
  32. data/example/notify.rb +35 -0
  33. data/example/optimist.rb +20 -0
  34. data/example/pulse.rb +24 -0
  35. data/example/read-in-trans.rb +56 -0
  36. data/example/small-simplified.rb +18 -0
  37. data/example/small.rb +76 -0
  38. data/example/tcp.rb +35 -0
  39. data/example/timeout-trans.rb +21 -0
  40. data/example/timeout.rb +27 -0
  41. data/example/tiny-client.rb +14 -0
  42. data/example/tiny-server.rb +12 -0
  43. data/example/transaction-logic.rb +40 -0
  44. data/example/write-wait.rb +17 -0
  45. data/lib/tupelo/app.rb +121 -0
  46. data/lib/tupelo/archiver/tuplespace.rb +68 -0
  47. data/lib/tupelo/archiver/worker.rb +87 -0
  48. data/lib/tupelo/archiver.rb +86 -0
  49. data/lib/tupelo/client/common.rb +10 -0
  50. data/lib/tupelo/client/reader.rb +124 -0
  51. data/lib/tupelo/client/transaction.rb +455 -0
  52. data/lib/tupelo/client/tuplespace.rb +50 -0
  53. data/lib/tupelo/client/worker.rb +493 -0
  54. data/lib/tupelo/client.rb +44 -0
  55. data/lib/tupelo/version.rb +3 -0
  56. data/test/lib/mock-client.rb +38 -0
  57. data/test/lib/mock-msg.rb +47 -0
  58. data/test/lib/mock-queue.rb +42 -0
  59. data/test/lib/mock-seq.rb +50 -0
  60. data/test/lib/testable-worker.rb +24 -0
  61. data/test/stress/concurrent-transactions.rb +42 -0
  62. data/test/system/test-archiver.rb +35 -0
  63. data/test/unit/test-mock-queue.rb +93 -0
  64. data/test/unit/test-mock-seq.rb +39 -0
  65. data/test/unit/test-ops.rb +222 -0
  66. 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