eventbox 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,47 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Eventbox
4
+ module CallContext
5
+ # @private
6
+ def __answer_queue__
7
+ @__answer_queue__
8
+ end
9
+
10
+ # @private
11
+ attr_writer :__answer_queue__
12
+ end
13
+
14
+ class BlockingExternalCallContext
15
+ include CallContext
16
+ end
17
+
18
+ class ActionCallContext
19
+ include CallContext
20
+
21
+ # @private
22
+ def initialize(event_loop)
23
+ answer_queue = Queue.new
24
+ meth = proc do
25
+ event_loop.callback_loop(answer_queue, nil, self.class)
26
+ end
27
+ @action = event_loop.start_action(meth, self.class, [])
28
+
29
+ def answer_queue.gc_stop(object_id)
30
+ close
31
+ end
32
+ ObjectSpace.define_finalizer(self, answer_queue.method(:gc_stop))
33
+
34
+ @__answer_queue__ = answer_queue
35
+ end
36
+
37
+ # The action that drives the call context.
38
+ attr_reader :action
39
+
40
+ # Terminate the call context and the driving action.
41
+ #
42
+ # The method returns immediately and the corresponding action is terminated asynchonously.
43
+ def shutdown!
44
+ @__answer_queue__.close
45
+ end
46
+ end
47
+ end
@@ -10,22 +10,40 @@ class Eventbox
10
10
  class EventLoop
11
11
  def initialize(threadpool, guard_time)
12
12
  @threadpool = threadpool
13
+ @shutdown = false
14
+ @guard_time = guard_time
15
+ _init_variables
16
+ end
17
+
18
+ def marshal_dump
19
+ raise TypeError, "Eventbox objects can't be serialized within event scope" if event_scope?
20
+ @mutex.synchronize do
21
+ raise TypeError, "Eventbox objects can't be serialized while actions are running" unless @running_actions.empty?
22
+ [@threadpool, @shutdown, @guard_time]
23
+ end
24
+ end
25
+
26
+ def marshal_load(array)
27
+ @threadpool, @shutdown, @guard_time = array
28
+ _init_variables
29
+ end
30
+
31
+ def _init_variables
13
32
  @running_actions = []
14
33
  @running_actions_for_gc = []
15
34
  @mutex = Mutex.new
16
- @shutdown = false
17
- @guard_time_proc = case guard_time
35
+ @guard_time_proc = case @guard_time
18
36
  when NilClass
19
37
  nil
20
38
  when Numeric
21
- guard_time and proc do |dt, name|
22
- if dt > guard_time
39
+ @guard_time and proc do |dt, name|
40
+ if dt > @guard_time
23
41
  ecaller = caller.find{|t| !(t=~/lib\/eventbox(\/|\.rb:)/) }
24
- warn "guard time exceeded: #{"%2.3f" % dt} sec (limit is #{guard_time}) in `#{name}' called from `#{ecaller}' - please move blocking tasks to actions"
42
+ warn "guard time exceeded: #{"%2.3f" % dt} sec (limit is #{@guard_time}) in `#{name}' called from `#{ecaller}' - please move blocking tasks to actions"
25
43
  end
26
44
  end
27
45
  when Proc
28
- guard_time
46
+ @guard_time
29
47
  else
30
48
  raise ArgumentError, "guard_time should be Numeric, Proc or nil"
31
49
  end
@@ -53,6 +71,10 @@ class Eventbox
53
71
  nil
54
72
  end
55
73
 
74
+ def inspect
75
+ "#<#{self.class}:#{self.object_id} @threadpool=#{@threadpool.inspect}, @shutdown=#{@shutdown.inspect}, @guard_time=#{@guard_time.inspect}, @running_actions=#{@running_actions.length}>"
76
+ end
77
+
56
78
  def shutdown(&completion_block)
57
79
  send_shutdown
58
80
  if event_scope?
@@ -111,28 +133,47 @@ class Eventbox
111
133
  @latest_answer_queue = nil
112
134
  @latest_call_name = nil
113
135
  @mutex.unlock
136
+ Thread.current.thread_variable_set(:__event_loop__, source_event_loop)
114
137
  diff_time = Time.now - start_time
115
138
  @guard_time_proc&.call(diff_time, name)
116
- Thread.current.thread_variable_set(:__event_loop__, source_event_loop)
117
139
  end
118
140
  source_event_loop
119
141
  end
120
142
 
121
- def async_call(box, name, args, block, wrapper)
143
+ def _latest_call_context
144
+ if @latest_answer_queue
145
+ ctx = BlockingExternalCallContext.new
146
+ ctx.__answer_queue__ = @latest_answer_queue
147
+ end
148
+ ctx
149
+ end
150
+
151
+ def with_call_context(ctx)
152
+ orig_context = @latest_answer_queue
153
+ raise ArgumentError, "invalid argument #{ctx.inspect} instead of Eventbox::CallContext" unless CallContext === ctx
154
+ @latest_answer_queue = ctx.__answer_queue__
155
+ yield
156
+ ensure
157
+ @latest_answer_queue = orig_context
158
+ end
159
+
160
+ def async_call(box, name, args, kwargs, block, wrapper)
122
161
  with_call_frame(name, nil) do |source_event_loop|
123
- args = wrapper.call(source_event_loop, *args) if wrapper
162
+ args, kwargs = wrapper.call(source_event_loop, self, *args, **kwargs) if wrapper
124
163
  args = Sanitizer.sanitize_values(args, source_event_loop, self, name)
164
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, source_event_loop, self, name)
125
165
  block = Sanitizer.sanitize_value(block, source_event_loop, self, name)
126
- box.send("__#{name}__", *args, &block)
166
+ box.send("__#{name}__", *args, **kwargs, &block)
127
167
  end
128
168
  end
129
169
 
130
- def sync_call(box, name, args, block, answer_queue, wrapper)
170
+ def sync_call(box, name, args, kwargs, block, answer_queue, wrapper)
131
171
  with_call_frame(name, answer_queue) do |source_event_loop|
132
- args = wrapper.call(source_event_loop, *args) if wrapper
172
+ args, kwargs = wrapper.call(source_event_loop, self, *args, **kwargs) if wrapper
133
173
  args = Sanitizer.sanitize_values(args, source_event_loop, self, name)
174
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, source_event_loop, self, name)
134
175
  block = Sanitizer.sanitize_value(block, source_event_loop, self, name)
135
- res = box.send("__#{name}__", *args, &block)
176
+ res = box.send("__#{name}__", *args, **kwargs, &block)
136
177
  res = Sanitizer.sanitize_value(res, self, source_event_loop)
137
178
  answer_queue << res
138
179
  end
@@ -140,32 +181,34 @@ class Eventbox
140
181
 
141
182
  def yield_call(box, name, args, kwargs, block, answer_queue, wrapper)
142
183
  with_call_frame(name, answer_queue) do |source_event_loop|
143
- args << _completion_proc(answer_queue, name, source_event_loop)
144
- args << kwargs unless kwargs.empty?
145
- args = wrapper.call(source_event_loop, *args) if wrapper
184
+ args << new_completion_proc(answer_queue, name, source_event_loop)
185
+ args, kwargs = wrapper.call(source_event_loop, self, *args, **kwargs) if wrapper
146
186
  args = Sanitizer.sanitize_values(args, source_event_loop, self, name)
187
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, source_event_loop, self, name)
147
188
  block = Sanitizer.sanitize_value(block, source_event_loop, self, name)
148
- box.send("__#{name}__", *args, &block)
189
+ box.send("__#{name}__", *args, **kwargs, &block)
149
190
  end
150
191
  end
151
192
 
152
193
  # Anonymous version of async_call
153
- def async_proc_call(pr, args, arg_block, wrapper)
194
+ def async_proc_call(pr, args, kwargs, arg_block, wrapper)
154
195
  with_call_frame(AsyncProc, nil) do |source_event_loop|
155
- args = wrapper.call(source_event_loop, *args) if wrapper
196
+ args, kwargs = wrapper.call(source_event_loop, self, *args, **kwargs) if wrapper
156
197
  args = Sanitizer.sanitize_values(args, source_event_loop, self)
198
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, source_event_loop, self)
157
199
  arg_block = Sanitizer.sanitize_value(arg_block, source_event_loop, self)
158
- pr.yield(*args, &arg_block)
200
+ pr.yield(*args, **kwargs, &arg_block)
159
201
  end
160
202
  end
161
203
 
162
204
  # Anonymous version of sync_call
163
- def sync_proc_call(pr, args, arg_block, answer_queue, wrapper)
205
+ def sync_proc_call(pr, args, kwargs, arg_block, answer_queue, wrapper)
164
206
  with_call_frame(SyncProc, answer_queue) do |source_event_loop|
165
- args = wrapper.call(source_event_loop, *args) if wrapper
207
+ args, kwargs = wrapper.call(source_event_loop, self, *args, **kwargs) if wrapper
166
208
  args = Sanitizer.sanitize_values(args, source_event_loop, self)
209
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, source_event_loop, self)
167
210
  arg_block = Sanitizer.sanitize_value(arg_block, source_event_loop, self)
168
- res = pr.yield(*args, &arg_block)
211
+ res = pr.yield(*args, **kwargs, &arg_block)
169
212
  res = Sanitizer.sanitize_value(res, self, source_event_loop)
170
213
  answer_queue << res
171
214
  end
@@ -174,18 +217,20 @@ class Eventbox
174
217
  # Anonymous version of yield_call
175
218
  def yield_proc_call(pr, args, kwargs, arg_block, answer_queue, wrapper)
176
219
  with_call_frame(YieldProc, answer_queue) do |source_event_loop|
177
- args << _completion_proc(answer_queue, pr, source_event_loop)
178
- args << kwargs unless kwargs.empty?
179
- args = wrapper.call(source_event_loop, *args) if wrapper
220
+ args << new_completion_proc(answer_queue, pr, source_event_loop)
221
+ args, kwargs = wrapper.call(source_event_loop, self, *args, **kwargs) if wrapper
180
222
  args = Sanitizer.sanitize_values(args, source_event_loop, self)
223
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, source_event_loop, self)
181
224
  arg_block = Sanitizer.sanitize_value(arg_block, source_event_loop, self)
182
- pr.yield(*args, &arg_block)
225
+ pr.yield(*args, **kwargs, &arg_block)
183
226
  end
184
227
  end
185
228
 
186
- # Called when an external proc finished
187
- def external_proc_result(cbresult, res)
188
- with_call_frame(ExternalProc, nil) do
229
+ # Called when an external object call finished
230
+ def external_call_result(cbresult, res, answer_queue, wrapper)
231
+ with_call_frame(ExternalObject, answer_queue) do |source_event_loop|
232
+ res, _ = wrapper.call(source_event_loop, self, res) if wrapper
233
+ res = Sanitizer.sanitize_value(res, source_event_loop, self)
189
234
  cbresult.yield(*res)
190
235
  end
191
236
  end
@@ -193,13 +238,13 @@ class Eventbox
193
238
  def new_async_proc(name=nil, klass=AsyncProc, &block)
194
239
  raise InvalidAccess, "async_proc outside of the event scope is not allowed" unless event_scope?
195
240
  wrapper = ArgumentWrapper.build(block, "async_proc #{name}")
196
- pr = klass.new do |*args, &arg_block|
241
+ pr = klass.new do |*args, **kwargs, &arg_block|
197
242
  if event_scope?
198
243
  # called in the event scope
199
- block.yield(*args, &arg_block)
244
+ block.yield(*args, **kwargs, &arg_block)
200
245
  else
201
246
  # called externally
202
- async_proc_call(block, args, arg_block, wrapper)
247
+ async_proc_call(block, args, kwargs, arg_block, wrapper)
203
248
  end
204
249
  pr
205
250
  end
@@ -208,15 +253,15 @@ class Eventbox
208
253
  def new_sync_proc(name=nil, &block)
209
254
  raise InvalidAccess, "sync_proc outside of the event scope is not allowed" unless event_scope?
210
255
  wrapper = ArgumentWrapper.build(block, "sync_proc #{name}")
211
- SyncProc.new do |*args, &arg_block|
256
+ SyncProc.new do |*args, **kwargs, &arg_block|
212
257
  if event_scope?
213
258
  # called in the event scope
214
- block.yield(*args, &arg_block)
259
+ block.yield(*args, **kwargs, &arg_block)
215
260
  else
216
261
  # called externally
217
262
  answer_queue = Queue.new
218
- sel = sync_proc_call(block, args, arg_block, answer_queue, wrapper)
219
- callback_loop(answer_queue, sel)
263
+ sel = sync_proc_call(block, args, kwargs, arg_block, answer_queue, wrapper)
264
+ callback_loop(answer_queue, sel, block)
220
265
  end
221
266
  end
222
267
  end
@@ -227,20 +272,19 @@ class Eventbox
227
272
  YieldProc.new do |*args, **kwargs, &arg_block|
228
273
  if event_scope?
229
274
  # called in the event scope
230
- safe_yield_result(args, block)
231
- args << kwargs unless kwargs.empty?
232
- block.yield(*args, &arg_block)
275
+ internal_yield_result(args, block)
276
+ block.yield(*args, **kwargs, &arg_block)
233
277
  nil
234
278
  else
235
279
  # called externally
236
280
  answer_queue = Queue.new
237
281
  sel = yield_proc_call(block, args, kwargs, arg_block, answer_queue, wrapper)
238
- callback_loop(answer_queue, sel)
282
+ callback_loop(answer_queue, sel, block)
239
283
  end
240
284
  end
241
285
  end
242
286
 
243
- def safe_yield_result(args, name)
287
+ def internal_yield_result(args, name)
244
288
  complete = args.last
245
289
  unless Proc === complete
246
290
  if Proc === name
@@ -252,9 +296,9 @@ class Eventbox
252
296
  args[-1] = proc do |*cargs, &cblock|
253
297
  unless complete
254
298
  if Proc === name
255
- raise MultipleResults, "received multiple results for #{name.inspect}"
299
+ raise MultipleResults, "second result yielded for #{name.inspect} that already returned"
256
300
  else
257
- raise MultipleResults, "received multiple results for method `#{name}'"
301
+ raise MultipleResults, "second result yielded for method `#{name}' that already returned"
258
302
  end
259
303
  end
260
304
  res = complete.yield(*cargs, &cblock)
@@ -263,13 +307,15 @@ class Eventbox
263
307
  end
264
308
  end
265
309
 
266
- private def _completion_proc(answer_queue, name, source_event_loop)
267
- new_async_proc(name, CompletionProc) do |*resu|
310
+ private def new_completion_proc(answer_queue, name, source_event_loop)
311
+ pr = new_async_proc(name, CompletionProc) do |*resu|
268
312
  unless answer_queue
313
+ # It could happen, that two threads call the CompletionProc simultanously so that nothing is raised here.
314
+ # In this case the failure is caught in callback_loop instead, but in all other cases the failure is raised early here at the caller side.
269
315
  if Proc === name
270
- raise MultipleResults, "received multiple results for #{name.inspect}"
316
+ raise MultipleResults, "second result yielded for #{name.inspect} that already returned"
271
317
  else
272
- raise MultipleResults, "received multiple results for method `#{name}'"
318
+ raise MultipleResults, "second result yielded for method `#{name}' that already returned"
273
319
  end
274
320
  end
275
321
  resu = Sanitizer.sanitize_values(resu, self, source_event_loop)
@@ -277,29 +323,51 @@ class Eventbox
277
323
  answer_queue << resu
278
324
  answer_queue = nil
279
325
  end
326
+ pr.__answer_queue__ = answer_queue
327
+ pr
280
328
  end
281
329
 
282
- def callback_loop(answer_queue, source_event_loop)
330
+ def callback_loop(answer_queue, source_event_loop, name)
283
331
  loop do
284
332
  rets = answer_queue.deq
285
333
  case rets
286
- when Callback
287
- cbres = rets.block.yield(*rets.args, &rets.arg_block)
334
+ when ExternalObjectCall
335
+ cbres = rets.object.send(rets.method, *rets.args, **rets.kwargs, &rets.arg_block)
288
336
 
289
337
  if rets.cbresult
290
- cbres = Sanitizer.sanitize_value(cbres, source_event_loop, self)
291
- external_proc_result(rets.cbresult, cbres)
338
+ external_call_result(rets.cbresult, cbres, answer_queue, rets.result_wrapper)
292
339
  end
293
340
  when WrappedException
294
- answer_queue.close if answer_queue.respond_to?(:close)
341
+ close_answer_queue(answer_queue, name)
295
342
  raise(*rets.exc)
296
343
  else
297
- answer_queue.close if answer_queue.respond_to?(:close)
344
+ close_answer_queue(answer_queue, name)
298
345
  return rets
299
346
  end
300
347
  end
301
348
  end
302
349
 
350
+ private def close_answer_queue(answer_queue, name)
351
+ answer_queue.close
352
+ unless answer_queue.empty?
353
+ rets = answer_queue.deq
354
+ case rets
355
+ when ExternalObjectCall
356
+ if Proc === name
357
+ raise InvalidAccess, "#{rets.objtype} can't be called through #{name.inspect}, since it already returned"
358
+ else
359
+ raise InvalidAccess, "#{rets.objtype} can't be called through method `#{name}', since it already returned"
360
+ end
361
+ else
362
+ if Proc === name
363
+ raise MultipleResults, "second result yielded for #{name.inspect} that already returned"
364
+ else
365
+ raise MultipleResults, "second result yielded for method `#{name}' that already returned"
366
+ end
367
+ end
368
+ end
369
+ end
370
+
303
371
  # Mark an object as to be shared instead of copied.
304
372
  def shared_object(object)
305
373
  if event_scope?
@@ -310,6 +378,11 @@ class Eventbox
310
378
  object
311
379
  end
312
380
 
381
+ # Wrap an object as ExternalObject.
382
+ def €(object)
383
+ Sanitizer.wrap_object(object, nil, self, nil)
384
+ end
385
+
313
386
  def thread_finished(action)
314
387
  @mutex.synchronize do
315
388
  @running_actions.delete(action) or raise(ArgumentError, "unknown action has finished: #{action}")
@@ -317,20 +390,40 @@ class Eventbox
317
390
  end
318
391
  end
319
392
 
320
- Callback = Struct.new :block, :args, :arg_block, :cbresult
393
+ class ExternalObjectCall < Struct.new :object, :method, :args, :kwargs, :arg_block, :cbresult, :result_wrapper
394
+ def proc?
395
+ Proc === object
396
+ end
321
397
 
322
- def _external_proc_call(block, name, args, arg_block, cbresult, source_event_loop)
323
- if @latest_answer_queue
324
- args = Sanitizer.sanitize_values(args, self, source_event_loop)
325
- arg_block = Sanitizer.sanitize_value(arg_block, self, source_event_loop)
326
- @latest_answer_queue << Callback.new(block, args, arg_block, cbresult)
327
- nil
398
+ def objtype
399
+ proc? ? "closure" : "method `#{method}'"
400
+ end
401
+ end
402
+
403
+ def _external_object_call(object, method, name, args, kwargs, arg_block, cbresult, source_event_loop, call_context)
404
+ result_wrapper = ArgumentWrapper.build(cbresult, name) if cbresult
405
+ args = Sanitizer.sanitize_values(args, self, source_event_loop)
406
+ kwargs = Sanitizer.sanitize_kwargs(kwargs, self, source_event_loop)
407
+ arg_block = Sanitizer.sanitize_value(arg_block, self, source_event_loop)
408
+ cb = ExternalObjectCall.new(object, method, args, kwargs, arg_block, cbresult, result_wrapper)
409
+
410
+ if call_context
411
+ # explicit call_context given
412
+ if call_context.__answer_queue__.closed?
413
+ raise InvalidAccess, "#{cb.objtype} #{"defined by `#{name}' " if name}was called with a call context that already returned"
414
+ end
415
+ call_context.__answer_queue__ << cb
416
+ elsif @latest_answer_queue
417
+ # proc called by a sync or yield call/proc context
418
+ @latest_answer_queue << cb
328
419
  else
329
- raise(InvalidAccess, "closure #{"defined by `#{name}' " if name}was yielded by `#{@latest_call_name}', which must a sync_call, yield_call, sync_proc or yield_proc")
420
+ raise InvalidAccess, "#{cb.objtype} #{"defined by `#{name}' " if name}was called by `#{@latest_call_name}', which must a sync_call, yield_call, sync_proc or yield_proc"
330
421
  end
422
+
423
+ nil
331
424
  end
332
425
 
333
- def start_action(meth, name, args)
426
+ def start_action(meth, name, args, &block)
334
427
  # Actions might not be tagged to a calling event scope
335
428
  source_event_loop = Thread.current.thread_variable_get(:__event_loop__)
336
429
  Thread.current.thread_variable_set(:__event_loop__, nil)
@@ -339,23 +432,27 @@ class Eventbox
339
432
 
340
433
  new_thread = Thread.handle_interrupt(Exception => :never) do
341
434
  @threadpool.new do
435
+ ac = nil
342
436
  begin
343
437
  Thread.handle_interrupt(AbortAction => :on_blocking) do
344
438
  if meth.arity == args.length
345
- meth.call(*args)
439
+ meth.call(*args, &block)
346
440
  else
347
- meth.call(*args, qu.deq)
441
+ ac ||= qu.deq
442
+ meth.call(*args, ac, &block)
348
443
  end
349
444
  end
350
445
  rescue AbortAction
351
- # Do nothing, just exit the action
446
+ ac ||= qu.deq
447
+ ac.terminate
352
448
  rescue WeakRef::RefError
353
449
  # It can happen that the GC already swept the Eventbox instance, before some instance action is in a blocking state.
354
450
  # In this case access to the Eventbox instance raises a RefError.
355
451
  # Since it's now impossible to execute the action up to a blocking state, abort the action prematurely.
356
452
  raise unless @shutdown
357
453
  ensure
358
- thread_finished(qu.deq)
454
+ ac ||= qu.deq
455
+ thread_finished(ac)
359
456
  end
360
457
  end
361
458
  end
@@ -368,8 +465,8 @@ class Eventbox
368
465
  _update_action_threads_for_gc
369
466
  end
370
467
 
371
- # Enqueue the action twice (for call and for finish)
372
- qu << a << a
468
+ # Enqueue the action for passing as action parameter
469
+ qu << a
373
470
 
374
471
  # @shutdown is set without a lock, so that we need to re-check, if it was set while start_action
375
472
  if @shutdown