zoidberg 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ require 'zoidberg'
2
+
3
+ module Zoidberg
4
+ class Registry < Bogo::Smash
5
+ include Zoidberg::Shell
6
+ end
7
+ 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