zoidberg 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|