zoidberg 0.0.1 → 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/CONTRIBUTING.md +25 -0
- data/LICENSE +13 -0
- data/README.md +151 -0
- data/lib/zoidberg/future.rb +34 -0
- data/lib/zoidberg/logger.rb +23 -0
- data/lib/zoidberg/pool.rb +100 -0
- data/lib/zoidberg/proxy/confined.rb +175 -0
- data/lib/zoidberg/proxy/liberated.rb +135 -0
- data/lib/zoidberg/proxy.rb +211 -0
- data/lib/zoidberg/registry.rb +7 -0
- data/lib/zoidberg/shell.rb +354 -0
- data/lib/zoidberg/signal.rb +109 -0
- data/lib/zoidberg/supervise.rb +41 -0
- data/lib/zoidberg/supervisor.rb +82 -0
- data/lib/zoidberg/task.rb +147 -0
- data/lib/zoidberg/timer.rb +230 -0
- data/lib/zoidberg/version.rb +2 -1
- data/lib/zoidberg/weak_ref.rb +51 -0
- data/lib/zoidberg.rb +55 -0
- data/zoidberg.gemspec +1 -0
- metadata +31 -2
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'zoidberg'
|
2
|
+
|
3
|
+
module Zoidberg
|
4
|
+
|
5
|
+
# Instance proxy that filters requests to shelled instance
|
6
|
+
class Proxy < BasicObject
|
7
|
+
|
8
|
+
autoload :Confined, 'zoidberg/proxy/confined'
|
9
|
+
autoload :Liberated, 'zoidberg/proxy/liberated'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
@@__registry = ::Hash.new
|
13
|
+
|
14
|
+
# @return [Hash] WeakRef -> Proxy mapping
|
15
|
+
def registry
|
16
|
+
@@__registry
|
17
|
+
end
|
18
|
+
|
19
|
+
# Register the proxy a WeakRef is pointing to
|
20
|
+
#
|
21
|
+
# @param r_id [Integer] object ID of WeakRef
|
22
|
+
# @param proxy [Zoidberg::Proxy] actual proxy instance
|
23
|
+
# @return [Zoidberg::Proxy]
|
24
|
+
def register(r_id, proxy)
|
25
|
+
@@__registry[r_id] = proxy
|
26
|
+
end
|
27
|
+
|
28
|
+
# Destroy the proxy referenced by the WeakRef with the provided
|
29
|
+
# ID
|
30
|
+
#
|
31
|
+
# @param o_id [Integer] Object ID
|
32
|
+
# @return [Truthy, Falsey]
|
33
|
+
def scrub!(o_id)
|
34
|
+
proxy = @@__registry.delete(o_id)
|
35
|
+
if(proxy)
|
36
|
+
proxy._zoidberg_destroy!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Setup proxy for proper scrubbing support
|
42
|
+
def self.inherited(klass)
|
43
|
+
klass.class_eval do
|
44
|
+
# @return [Array] arguments used to build real instance
|
45
|
+
attr_accessor :_build_args
|
46
|
+
# @return [Object] wrapped instance
|
47
|
+
attr_reader :_raw_instance
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Abstract class gets no builder
|
52
|
+
def initialize(*_)
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [TrueClass, FalseClass] currently locked
|
57
|
+
def _zoidberg_locked?
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [TrueClass, FalseClass] currently unlocked
|
62
|
+
def _zoidberg_available?
|
63
|
+
!_zoidberg_locked?
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Object]
|
67
|
+
def _zoidberg_link=(inst)
|
68
|
+
@_zoidberg_link = inst
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Object, NilClass]
|
72
|
+
def _zoidberg_link
|
73
|
+
@_zoidberg_link
|
74
|
+
end
|
75
|
+
|
76
|
+
# Set an optional state signal instance
|
77
|
+
#
|
78
|
+
# @param signal [Signal]
|
79
|
+
# @return [Signal]
|
80
|
+
def _zoidberg_signal=(signal)
|
81
|
+
@_zoidberg_signal = signal
|
82
|
+
end
|
83
|
+
|
84
|
+
# Send a signal if the optional signal instance has been set
|
85
|
+
#
|
86
|
+
# @param sig [Symbol]
|
87
|
+
# @return [TrueClass, FalseClass] signal was sent
|
88
|
+
def _zoidberg_signal(sig)
|
89
|
+
if(@_zoidberg_signal)
|
90
|
+
@_zoidberg_signal.signal(sig)
|
91
|
+
true
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Properly handle an unexpected exception when encountered
|
98
|
+
#
|
99
|
+
# @param e [Exception]
|
100
|
+
def _zoidberg_unexpected_error(e)
|
101
|
+
::Zoidberg.logger.error "Unexpected exception: #{e.class} - #{e}"
|
102
|
+
unless((defined?(Timeout) && e.is_a?(Timeout::Error)) || e.is_a?(::Zoidberg::DeadException))
|
103
|
+
if(_zoidberg_link)
|
104
|
+
if(_zoidberg_link.class.trap_exit)
|
105
|
+
::Zoidberg.logger.warn "Calling linked exit trapper #{@_raw_instance.class.name} -> #{_zoidberg_link.class}: #{e.class} - #{e}"
|
106
|
+
_zoidberg_link.async.send(
|
107
|
+
_zoidberg_link.class.trap_exit, @_raw_instance, e
|
108
|
+
)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
if(@_supervised)
|
112
|
+
::Zoidberg.logger.warn "Unexpected error for supervised class `#{@_raw_instance.class.name}`. Handling error (#{e.class} - #{e})"
|
113
|
+
::Zoidberg.logger.debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
114
|
+
_zoidberg_handle_unexpected_error(e)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# When real instance is being supervised, unexpected exceptions
|
121
|
+
# will force the real instance to be terminated and replaced with
|
122
|
+
# a fresh instance.
|
123
|
+
#
|
124
|
+
# If the real instance provides a #restart
|
125
|
+
# method that will be called instead of forcibly terminating the
|
126
|
+
# current real instance and rebuild a new instance.
|
127
|
+
#
|
128
|
+
# If the real instance provides a #restarted! method, that method
|
129
|
+
# will be called on the newly created instance on replacement
|
130
|
+
#
|
131
|
+
# @param error [Exception] exception that was caught
|
132
|
+
# @return [TrueClass]
|
133
|
+
def _zoidberg_handle_unexpected_error(error)
|
134
|
+
if(_raw_instance.respond_to?(:restart))
|
135
|
+
begin
|
136
|
+
_raw_instance.restart(error)
|
137
|
+
return # short circuit
|
138
|
+
rescue => e
|
139
|
+
end
|
140
|
+
end
|
141
|
+
_zoidberg_destroy!
|
142
|
+
_aquire_lock!
|
143
|
+
args = _build_args.dup
|
144
|
+
@_raw_instance = args.shift.unshelled_new(
|
145
|
+
*args.first,
|
146
|
+
&args.last
|
147
|
+
)
|
148
|
+
_raw_instance._zoidberg_proxy(self)
|
149
|
+
if(_raw_instance.respond_to?(:restarted!))
|
150
|
+
_raw_instance.restarted!
|
151
|
+
end
|
152
|
+
_release_lock!
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
# Destroy the real instance. Will update all methods on real
|
157
|
+
# instance to raise exceptions noting it as terminated rendering
|
158
|
+
# it unusable. This is generally used with the supervise module
|
159
|
+
# but can be used on its own if desired.
|
160
|
+
#
|
161
|
+
# @return [TrueClass]
|
162
|
+
def _zoidberg_destroy!(error=nil, &block)
|
163
|
+
unless(_raw_instance.respond_to?(:_zoidberg_destroyed))
|
164
|
+
if(_raw_instance.respond_to?(:terminate))
|
165
|
+
if(_raw_instance.method(:terminate).arity == 0)
|
166
|
+
_raw_instance.terminate
|
167
|
+
else
|
168
|
+
_raw_instance.terminate(error)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
death_from_above = ::Proc.new do
|
172
|
+
::Kernel.raise ::Zoidberg::DeadException.new('Instance in terminated state!')
|
173
|
+
end
|
174
|
+
death_from_above_display = ::Proc.new do
|
175
|
+
"#<#{self.class.name}:TERMINATED>"
|
176
|
+
end
|
177
|
+
block.call if block
|
178
|
+
_raw_instance.instance_variables.each do |i_var|
|
179
|
+
_raw_instance.remove_instance_variable(i_var)
|
180
|
+
end
|
181
|
+
(
|
182
|
+
_raw_instance.public_methods(false) +
|
183
|
+
_raw_instance.protected_methods(false) +
|
184
|
+
_raw_instance.private_methods(false)
|
185
|
+
).each do |m_name|
|
186
|
+
next if m_name.to_sym == :alive?
|
187
|
+
_raw_instance.send(:define_singleton_method, m_name, &death_from_above)
|
188
|
+
end
|
189
|
+
_raw_instance.send(:define_singleton_method, :to_s, &death_from_above_display)
|
190
|
+
_raw_instance.send(:define_singleton_method, :inspect, &death_from_above_display)
|
191
|
+
_raw_instance.send(:define_singleton_method, :_zoidberg_destroyed, ::Proc.new{ true })
|
192
|
+
_zoidberg_signal(:destroyed)
|
193
|
+
end
|
194
|
+
true
|
195
|
+
end
|
196
|
+
alias_method :terminate, :_zoidberg_destroy!
|
197
|
+
|
198
|
+
# @return [self]
|
199
|
+
def _zoidberg_object
|
200
|
+
self
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# jruby compat [https://github.com/jruby/jruby/pull/2520]
|
207
|
+
if(Zoidberg::Proxy.instance_methods.include?(:object_id))
|
208
|
+
class Zoidberg::Proxy
|
209
|
+
undef_method :object_id
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,354 @@
|
|
1
|
+
require 'zoidberg'
|
2
|
+
|
3
|
+
module Zoidberg
|
4
|
+
|
5
|
+
# Customized exception type used when instance has been terminated
|
6
|
+
class DeadException < RuntimeError; end
|
7
|
+
|
8
|
+
# Librated proxy based shell
|
9
|
+
module SoftShell
|
10
|
+
|
11
|
+
class AsyncProxy
|
12
|
+
attr_reader :target
|
13
|
+
def initialize(instance)
|
14
|
+
@target = instance
|
15
|
+
end
|
16
|
+
def method_missing(*args, &block)
|
17
|
+
target._zoidberg_thread(
|
18
|
+
Thread.new{
|
19
|
+
begin
|
20
|
+
target.send(*args, &block)
|
21
|
+
rescue Exception => e
|
22
|
+
target._zoidberg_proxy.send(:raise, e)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
)
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Unlock current lock on instance and execute given block
|
31
|
+
# without locking
|
32
|
+
#
|
33
|
+
# @yield block to execute without lock
|
34
|
+
# @return [Object] result of block
|
35
|
+
def defer
|
36
|
+
_zoidberg_proxy._release_lock!
|
37
|
+
begin
|
38
|
+
result = yield if block_given?
|
39
|
+
_zoidberg_proxy._aquire_lock!
|
40
|
+
result
|
41
|
+
rescue Exception => e
|
42
|
+
_zoidberg_proxy._aquire_lock!
|
43
|
+
raise e
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Perform an async action
|
48
|
+
#
|
49
|
+
# @param locked [Truthy, Falsey] lock when running
|
50
|
+
# @return [AsyncProxy, NilClass]
|
51
|
+
def async(locked=false, &block)
|
52
|
+
if(block_given?)
|
53
|
+
unless(locked)
|
54
|
+
thread = ::Thread.new do
|
55
|
+
self.instance_exec(&block)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
thread = ::Thread.new{ current_self.instance_exec(&block) }
|
59
|
+
end
|
60
|
+
_zoidberg_thread(thread)
|
61
|
+
nil
|
62
|
+
else
|
63
|
+
::Zoidberg::SoftShell::AsyncProxy.new(locked ? current_self : self)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Register a running thread for this instance. Registered
|
68
|
+
# threads are tracked and killed on cleanup
|
69
|
+
#
|
70
|
+
# @param thread [Thread]
|
71
|
+
# @return [TrueClass]
|
72
|
+
def _zoidberg_thread(thread)
|
73
|
+
_zoidberg_proxy._raw_threads[self.object_id].push(thread)
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Provide a customized sleep behavior which will unlock the real
|
78
|
+
# instance while sleeping
|
79
|
+
#
|
80
|
+
# @param length [Numeric, NilClass]
|
81
|
+
# @return [Float]
|
82
|
+
def sleep(length=nil)
|
83
|
+
if(_zoidberg_proxy._locker == ::Thread.current)
|
84
|
+
defer do
|
85
|
+
start_time = ::Time.now.to_f
|
86
|
+
if(length)
|
87
|
+
::Kernel.sleep(length)
|
88
|
+
else
|
89
|
+
::Kernel.sleep
|
90
|
+
end
|
91
|
+
::Time.now.to_f - start_time
|
92
|
+
end
|
93
|
+
else
|
94
|
+
start_time = ::Time.now.to_f
|
95
|
+
if(length)
|
96
|
+
::Kernel.sleep(length)
|
97
|
+
else
|
98
|
+
::Kernel.sleep
|
99
|
+
end
|
100
|
+
::Time.now.to_f - start_time
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.included(klass)
|
105
|
+
unless(klass.include?(::Zoidberg::Shell))
|
106
|
+
klass.class_eval do
|
107
|
+
include ::Zoidberg::Shell
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
# Confined proxy based shell
|
115
|
+
module HardShell
|
116
|
+
|
117
|
+
class AsyncProxy
|
118
|
+
attr_reader :target, :locked
|
119
|
+
def initialize(instance, locked)
|
120
|
+
@target = instance
|
121
|
+
@locked = locked
|
122
|
+
end
|
123
|
+
def method_missing(*args, &block)
|
124
|
+
target._async_request(locked ? :blocking : :nonblocking, *args, &block)
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Unlock current lock on instance and execute given block
|
130
|
+
# without locking
|
131
|
+
#
|
132
|
+
# @yield block to execute without lock
|
133
|
+
# @return [Object] result of block
|
134
|
+
def defer(&block)
|
135
|
+
Fiber.yield
|
136
|
+
if(block)
|
137
|
+
::Fiber.new(&block).resume
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Perform an async action
|
142
|
+
#
|
143
|
+
# @param locked [Truthy, Falsey] lock when running
|
144
|
+
# @return [AsyncProxy, NilClass]
|
145
|
+
def async(locked=false, &block)
|
146
|
+
if(block)
|
147
|
+
if(locked)
|
148
|
+
current_self.instance_exec(&block)
|
149
|
+
else
|
150
|
+
current_self._async_request(locked ? :blocking : :nonblocking, :instance_exec, &block)
|
151
|
+
end
|
152
|
+
else
|
153
|
+
::Zoidberg::HardShell::AsyncProxy.new(current_self, locked)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Provide a customized sleep behavior which will unlock the real
|
158
|
+
# instance while sleeping
|
159
|
+
#
|
160
|
+
# @param length [Numeric, NilClass]
|
161
|
+
# @return [Float]
|
162
|
+
def sleep(length=nil)
|
163
|
+
start_time = ::Time.now.to_f
|
164
|
+
if(length)
|
165
|
+
::Kernel.sleep(length)
|
166
|
+
else
|
167
|
+
::Thread.current[:root_fiber] == ::Fiber.current ? ::Kernel.sleep : ::Fiber.yield
|
168
|
+
end
|
169
|
+
::Time.now.to_f - start_time
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.included(klass)
|
173
|
+
unless(klass.include?(::Zoidberg::Shell))
|
174
|
+
klass.class_eval do
|
175
|
+
include ::Zoidberg::Shell
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
# Provides a wrapping around a real instance. Including this module
|
183
|
+
# within a class will enable magic.
|
184
|
+
module Shell
|
185
|
+
|
186
|
+
module InstanceMethods
|
187
|
+
|
188
|
+
# Initialize the signal instance if not
|
189
|
+
def _zoidberg_signal_interface
|
190
|
+
unless(@_zoidberg_signal)
|
191
|
+
@_zoidberg_signal = ::Zoidberg::Signal.new(:cache_signals => self.class.option?(:cache_signals))
|
192
|
+
end
|
193
|
+
@_zoidberg_signal
|
194
|
+
end
|
195
|
+
|
196
|
+
# @return [Timer]
|
197
|
+
def timer
|
198
|
+
unless(@_zoidberg_timer)
|
199
|
+
@_zoidberg_timer = Timer.new
|
200
|
+
end
|
201
|
+
@_zoidberg_timer
|
202
|
+
end
|
203
|
+
|
204
|
+
# Register a recurring action
|
205
|
+
#
|
206
|
+
# @param interval [Numeric]
|
207
|
+
# @yield action to run
|
208
|
+
# @return [Timer]
|
209
|
+
def every(interval, &block)
|
210
|
+
timer.every(interval, &block)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Register an action to run after interval
|
214
|
+
#
|
215
|
+
# @param interval [Numeric]
|
216
|
+
# @yield action to run
|
217
|
+
# @return [Timer]
|
218
|
+
def after(interval, &block)
|
219
|
+
timer.after(interval, &block)
|
220
|
+
end
|
221
|
+
|
222
|
+
# Send a signal to single waiter
|
223
|
+
#
|
224
|
+
# @param name [String, Symbol] name of signal
|
225
|
+
# @param arg [Object] optional argument to transmit
|
226
|
+
# @return [TrueClass, FalseClass]
|
227
|
+
def signal(name, arg=nil)
|
228
|
+
current_self._zoidberg_signal_interface.signal(*[name, arg].compact)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Broadcast a signal to all waiters
|
232
|
+
# @param name [String, Symbol] name of signal
|
233
|
+
# @param arg [Object] optional argument to transmit
|
234
|
+
# @return [TrueClass, FalseClass]
|
235
|
+
def broadcast(name, arg=nil)
|
236
|
+
current_self._zoidberg_signal_interface.broadcast(*[name, arg].compact)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Wait for a given signal
|
240
|
+
#
|
241
|
+
# @param name [String, Symbol] name of signal
|
242
|
+
# @return [Object]
|
243
|
+
def wait_for(name)
|
244
|
+
defer{ current_self._zoidberg_signal_interface.wait_for(name) }
|
245
|
+
end
|
246
|
+
alias_method :wait, :wait_for
|
247
|
+
|
248
|
+
# @return [TrueClass, FalseClass]
|
249
|
+
def alive?
|
250
|
+
!respond_to?(:_zoidberg_destroyed)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Provide access to the proxy instance from the real instance
|
254
|
+
#
|
255
|
+
# @param oxy [Zoidberg::Proxy]
|
256
|
+
# @return [NilClass, Zoidberg::Proxy]
|
257
|
+
def _zoidberg_proxy(oxy=nil)
|
258
|
+
if(oxy)
|
259
|
+
@_zoidberg_proxy = oxy
|
260
|
+
end
|
261
|
+
@_zoidberg_proxy
|
262
|
+
end
|
263
|
+
alias_method :current_self, :_zoidberg_proxy
|
264
|
+
alias_method :current_actor, :_zoidberg_proxy
|
265
|
+
|
266
|
+
# Link given shelled instance to current shelled instance to
|
267
|
+
# handle any exceptions raised from async actions
|
268
|
+
#
|
269
|
+
# @param inst [Object]
|
270
|
+
# @return [TrueClass]
|
271
|
+
def link(inst)
|
272
|
+
inst._zoidberg_link = current_self
|
273
|
+
true
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
module ClassMethods
|
279
|
+
|
280
|
+
# Override real instance's .new method to provide a proxy instance
|
281
|
+
def new(*args, &block)
|
282
|
+
if(self.include?(Zoidberg::HardShell))
|
283
|
+
proxy = Zoidberg::Proxy::Confined.new(self, *args, &block)
|
284
|
+
elsif(self.include?(Zoidberg::SoftShell))
|
285
|
+
proxy = Zoidberg::Proxy::Liberated.new(self, *args, &block)
|
286
|
+
else
|
287
|
+
raise TypeError.new "Unable to determine `Shell` type for this class `#{self}`!"
|
288
|
+
end
|
289
|
+
weak_ref = Zoidberg::WeakRef.new(proxy)
|
290
|
+
Zoidberg::Proxy.register(weak_ref.__id__, proxy)
|
291
|
+
ObjectSpace.define_finalizer(weak_ref, Zoidberg::Proxy.method(:scrub!))
|
292
|
+
weak_ref
|
293
|
+
end
|
294
|
+
|
295
|
+
# Trap unhandled exceptions from linked instances and handle via
|
296
|
+
# given method name
|
297
|
+
#
|
298
|
+
# @param m_name [String, Symbol] method handler name
|
299
|
+
# @return [String, Symbol]
|
300
|
+
def trap_exit(m_name=nil)
|
301
|
+
if(m_name)
|
302
|
+
@m_name = m_name
|
303
|
+
end
|
304
|
+
@m_name
|
305
|
+
end
|
306
|
+
|
307
|
+
end
|
308
|
+
|
309
|
+
# Inject Shell magic into given class when included
|
310
|
+
#
|
311
|
+
# @param klass [Class]
|
312
|
+
def self.included(klass)
|
313
|
+
unless(klass.ancestors.include?(Zoidberg::Shell::InstanceMethods))
|
314
|
+
klass.class_eval do
|
315
|
+
|
316
|
+
class << self
|
317
|
+
alias_method :unshelled_new, :new
|
318
|
+
|
319
|
+
# Set an option or multiple options
|
320
|
+
#
|
321
|
+
# @return [Array<Symbol>]
|
322
|
+
def option(*args)
|
323
|
+
@option ||= []
|
324
|
+
unless(args.empty?)
|
325
|
+
@option += args
|
326
|
+
@option.map!(&:to_sym).uniq!
|
327
|
+
end
|
328
|
+
@option
|
329
|
+
end
|
330
|
+
|
331
|
+
# Check if option is available
|
332
|
+
#
|
333
|
+
# @param arg [Symbol]
|
334
|
+
# @return [TrueClass, FalseClass]
|
335
|
+
def option?(arg)
|
336
|
+
option.include?(arg.to_sym)
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
include InstanceMethods
|
342
|
+
extend ClassMethods
|
343
|
+
include Bogo::Memoization
|
344
|
+
end
|
345
|
+
end
|
346
|
+
unless(klass.include?(SoftShell) || klass.include?(HardShell))
|
347
|
+
klass.class_eval do
|
348
|
+
include Zoidberg.default_shell
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'zoidberg'
|
3
|
+
|
4
|
+
module Zoidberg
|
5
|
+
# Wait/send signals
|
6
|
+
class Signal
|
7
|
+
|
8
|
+
# empty value when no object is provided
|
9
|
+
EMPTY_VALUE = :_zoidberg_empty_
|
10
|
+
|
11
|
+
include SoftShell
|
12
|
+
|
13
|
+
# @return [Smash] meta information on current waiters
|
14
|
+
attr_reader :waiters
|
15
|
+
# @return [TrueClass, FalseClass]
|
16
|
+
attr_reader :cache_signals
|
17
|
+
|
18
|
+
# Create a new instance for sending and receiving signals
|
19
|
+
#
|
20
|
+
# @param args [Hash] options
|
21
|
+
# @return [self]
|
22
|
+
def initialize(args={})
|
23
|
+
@cache_signals = args.fetch(:cache_signals, false)
|
24
|
+
@waiters = Smash.new
|
25
|
+
end
|
26
|
+
|
27
|
+
# Set cache behavior
|
28
|
+
#
|
29
|
+
# @param arg [TrueClass, FalseClass] set behavior
|
30
|
+
# @return [TrueClass, FalseClass] behavior
|
31
|
+
def cache_signals(arg=nil)
|
32
|
+
unless(arg.nil?)
|
33
|
+
@cache_signals = !!arg
|
34
|
+
end
|
35
|
+
@cache_signals
|
36
|
+
end
|
37
|
+
|
38
|
+
# Send a signal to _one_ waiter
|
39
|
+
#
|
40
|
+
# @param signal [Symbol] name of signal
|
41
|
+
# @param obj [Object] optional Object to send
|
42
|
+
# @return [TrueClass, FalseClass] if signal was sent
|
43
|
+
def signal(signal, obj=EMPTY_VALUE)
|
44
|
+
if(signal_init(signal, :signal))
|
45
|
+
waiters[signal][:queue].push obj
|
46
|
+
true
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Send a signal to _all_ waiters
|
53
|
+
#
|
54
|
+
# @param signal [Symbol] name of signal
|
55
|
+
# @param obj [Object] optional Object to send
|
56
|
+
# @return [TrueClass, FalseClass] if signal(s) was/were sent
|
57
|
+
def broadcast(signal, obj=EMPTY_VALUE)
|
58
|
+
if(signal_init(signal, :signal))
|
59
|
+
num = waiters[signal][:threads].size
|
60
|
+
num = 1 if num < 1
|
61
|
+
num.times do
|
62
|
+
waiters[signal][:queue].push obj
|
63
|
+
end
|
64
|
+
true
|
65
|
+
else
|
66
|
+
false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Wait for a signal
|
71
|
+
#
|
72
|
+
# @param signal [Symbol] name of signal
|
73
|
+
# @return [Float] number of seconds waiting for signal
|
74
|
+
def wait_for(signal)
|
75
|
+
signal_init(signal, :wait)
|
76
|
+
start_sleep = Time.now.to_f
|
77
|
+
waiters[signal][:threads].push(Thread.current)
|
78
|
+
val = defer{ waiters[signal][:queue].pop }
|
79
|
+
waiters[signal][:threads].delete(Thread.current)
|
80
|
+
val == EMPTY_VALUE ? (Time.now.to_f - start_sleep) : val
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
# Initialize the signal structure data
|
86
|
+
#
|
87
|
+
# @param name [String, Symbol] name of signal
|
88
|
+
# @param origin [String] origin of init
|
89
|
+
# @return [TrueClass, FalseClass]
|
90
|
+
def signal_init(name, origin)
|
91
|
+
if(waiters[name])
|
92
|
+
cache_signals ||
|
93
|
+
origin == :wait ||
|
94
|
+
(origin == :signal && !waiters[name][:threads].empty?)
|
95
|
+
else
|
96
|
+
if(origin == :wait || (origin == :signal && cache_signals))
|
97
|
+
waiters[name] = Smash.new(
|
98
|
+
:queue => Queue.new,
|
99
|
+
:threads => []
|
100
|
+
)
|
101
|
+
true
|
102
|
+
else
|
103
|
+
false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'zoidberg'
|
2
|
+
|
3
|
+
module Zoidberg
|
4
|
+
# Add supervision to instance
|
5
|
+
module Supervise
|
6
|
+
# Customized exception type to wrap allowed errors
|
7
|
+
class AbortException < StandardError
|
8
|
+
attr_accessor :original_exception
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
|
13
|
+
# Customized method for raising exceptions that have been
|
14
|
+
# properly handled (preventing termination)
|
15
|
+
#
|
16
|
+
# @param e [Exception]
|
17
|
+
# @raises [AbortException]
|
18
|
+
def abort(e)
|
19
|
+
new_e = ::Zoidberg::Supervise::AbortException.new
|
20
|
+
new_e.original_exception = e
|
21
|
+
::Kernel.raise new_e
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
# Include supervision into given class when included
|
27
|
+
#
|
28
|
+
# @param klass [Class]
|
29
|
+
def self.included(klass)
|
30
|
+
unless(klass.include?(Zoidberg::Shell))
|
31
|
+
klass.class_eval{ include Zoidberg::Shell }
|
32
|
+
end
|
33
|
+
unless(klass.include?(Zoidberg::Supervise::InstanceMethods))
|
34
|
+
klass.class_eval do
|
35
|
+
include InstanceMethods
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|