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