eventbox 0.1.0 → 1.0.0

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