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