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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1086df260c250afd0d140b888020efbff2f2a776
4
- data.tar.gz: 350c9110094de3fb707ecfb12261c3899f9ca3d7
3
+ metadata.gz: f12f7686bfce42183b33d6a765027eed64113aee
4
+ data.tar.gz: 1b12f2ad1e6210a086c60c3dac02599cef678ba1
5
5
  SHA512:
6
- metadata.gz: 9f53d61bb8dd1c6733322e9a29b6e2d049e517136b1b6f226ff1b129b2206c588e53d336cdac1fdc51c5299ce507a81e6d94f8923880dfae8df7b84cc695fe77
7
- data.tar.gz: 9d2c5e02c1c2bae12bb5f37b4cc714f510880b002ed9397321dc8e230a09803f65dda44b917bf4ac66f7aaf5247aa6bbf5938bae0cc65228aa51940155cd0553
6
+ metadata.gz: 17f6a8e0cb57573076bbeb852c392699b8bdbde0f88b9818a213704cfb6e1c5c37e80cd6b207821e61f47f4d30e8825177e671136e209e2e197e1f0735063dd9
7
+ data.tar.gz: f8d4c6aba250fdf670e996bbb8668081cb21aa2ab0c8a049c7c8878f086af766e4525de47e2e1968c0b431985bdf3894ac05627a485b21fe78dbddb70dc048c9
data/CHANGELOG.md CHANGED
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release
data/CONTRIBUTING.md CHANGED
@@ -0,0 +1,25 @@
1
+ # Contributing
2
+
3
+ ## Branches
4
+
5
+ ### `master` branch
6
+
7
+ The master branch is the current stable released version.
8
+
9
+ ### `develop` branch
10
+
11
+ The develop branch is the current edge of development.
12
+
13
+ ## Pull requests
14
+
15
+ * https://github.com/spox/zoidberg/pulls
16
+
17
+ Please base all pull requests of the `develop` branch. Merges to
18
+ `master` only occur through the `develop` branch. Pull requests
19
+ based on `master` will likely be cherry picked.
20
+
21
+ ## Issues
22
+
23
+ Need to report an issue? Use the github issues:
24
+
25
+ * https://github.com/spox/zoidberg/issues
data/LICENSE CHANGED
@@ -0,0 +1,13 @@
1
+ Copyright 2015 Chris Roberts
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md CHANGED
@@ -0,0 +1,151 @@
1
+ # Zoidberg
2
+
3
+ > Why not Zoidberg?
4
+
5
+ ## About
6
+
7
+ Zoidberg is a small library attempting to provide synchronization
8
+ and supervision without requiring any modifications to existing
9
+ implementations. It is heavily inspired by Celluloid and while some
10
+ APIs may look familiar they do not share a familiar implementation.
11
+
12
+ ## Usage
13
+
14
+ Zoidberg provides a `Shell` which can be loaded into a class. After
15
+ it has been loaded, new instances will provide implicit synchronization,
16
+ which is nifty. For example, lets take a simple `Fubar` class that does
17
+ a simple thing:
18
+
19
+ ```ruby
20
+ class Fubar
21
+
22
+ attr_reader :string
23
+
24
+ def initialize
25
+ @string = ''
26
+ @chars = []
27
+ end
28
+
29
+ def append
30
+ string << char
31
+ end
32
+
33
+ private
34
+
35
+ def char
36
+ if(@chars.empty?)
37
+ @chars.replace (A..Z).to_a
38
+ end
39
+ @chars.shift
40
+ end
41
+
42
+ end
43
+ ```
44
+
45
+ Pretty simple class whose only purpose is to add characters to a string.
46
+ And it does just that:
47
+
48
+ ```ruby
49
+ inst = Fubar.new
50
+ 20.times{ inst.append }
51
+ inst.string
52
+
53
+ # => "ABCDEFGHIJKLMNOPQRST"
54
+ ```
55
+
56
+ So this does exactly what we expect it to. Now, lets update this example and
57
+ toss some threads into the mix:
58
+
59
+ ```ruby
60
+ inst = Fubar.new
61
+ 20.times.map{ Thread.new{ inst.append } }.map(&:join)
62
+ inst.string
63
+
64
+ # => "ABCDEFGHIJKLMNOPQRST"
65
+ ```
66
+
67
+ Cool, we get the same results! Looks like everything is great. Lets run it
68
+ again to bask in this multi-threaded awesomeness!
69
+
70
+ ```ruby
71
+ # => "AABCDEFGHIJKLMNOPQRS"
72
+ ```
73
+
74
+ Hrm, that doesn't look quite right. It looks like there's an extra 'A' at the start. Maybe
75
+ everything isn't so great. Lets try a few more:
76
+
77
+ ```ruby
78
+ inst = Fubar.new
79
+ 100.times.map do
80
+ 20.times.map{ Thread.new{ inst.append } }.map(&:join)
81
+ end.uniq
82
+
83
+ # => ["ABCDEFGHIJKLMNOPQRST", "ABCDEDGHIJKLMNOPQRST", "ACDEFGHIJKLMNOPQRST", "BCDEFGHIJKLMNOPQRST", "AABCDEFGHIJKLMNOPQRS", "ABCDEFHGIJKLMNOPQRST"]
84
+ ```
85
+
86
+ Whelp, I don't even know what that is supposed to be, but it's certainly
87
+ not what we are expecting. Well, we _are_ expecting it because this is
88
+ an example on synchronization, but lets just pretend at this point we are
89
+ amazed at this turn of events.
90
+
91
+ To fix this, we need to add some synchronization so multiple threads aren't
92
+ attempting to mutate state at the same time. But, instead of modifying the
93
+ class and explicitly adding synchronization, lets see what happens when
94
+ we toss `Zoidberg::Shell` into the mix (cause it's why everyone is here
95
+ in the first place). We can just continue on with our previous examples
96
+ and open up our defined class to inject the shell and re-run the example:
97
+
98
+ ```ruby
99
+ require 'zoidberg'
100
+
101
+ class Fubar
102
+ include Zoidberg::Shell
103
+ end
104
+
105
+ inst = Fubar.new
106
+ 20.times.map{ Thread.new{ inst.append } }.map(&:join)
107
+ inst.string
108
+
109
+ # => "ABCDEFGHIJKLMNOPQRST"
110
+ ```
111
+
112
+ and running it lots of times we get:
113
+
114
+ ```ruby
115
+ 100.times.map{20.times.map{ Thread.new{ inst.append } }.map(&:join)}.uniq
116
+
117
+ # => ["ABCDEFGHIJKLMNOPQRST"]
118
+ ```
119
+
120
+ So this is pretty neat. We had a class that was shown to not be thread
121
+ safe. We tossed a module into that class. Now that class is thread safe.
122
+
123
+ ## Features
124
+
125
+ ### Implicit Locking
126
+
127
+ Zoidberg automatically synchronizes requests made to an instance. This
128
+ behavior can be short circuited if the actual instance creates a thread
129
+ and calls a method on itself. Otherwise, all external access to the
130
+ instance will be automatically synchronized. Nifty.
131
+
132
+ ### Supervision
133
+
134
+ Zoidberg provides lazy supervision. There is no single supervisor. Instead
135
+ supervision is handled by the proxy which wraps the class. Failure of an
136
+ instance will result in termination + reinstantiation. When externally
137
+ accessing the instance nothing requires modification.
138
+
139
+ ### Pools
140
+
141
+ Zoidberg allows pooling lazy supervised instances. Unexpected failures will
142
+ cause the instance to be terminated and re-initialized as usual. The pool
143
+ will deliver requests to free instances, or queue them until a free instance
144
+ is available.
145
+
146
+ ### Garbage Collection
147
+
148
+ Garbage collection happens as usual with Zoidberg. When an instance is created
149
+ the result may look like the instance but really it is proxy wrapping the
150
+ raw instance. When the proxy falls out of scope and is garbage collected the
151
+ raw instance it wrapped will also fall out of scope and be garbage collected.
@@ -0,0 +1,34 @@
1
+ require 'zoidberg'
2
+
3
+ module Zoidberg
4
+ # Perform action and fetch result in the future
5
+ class Future
6
+
7
+ # @return [Thread] underlying thread running task
8
+ attr_reader :thread
9
+
10
+ # Create a new instance
11
+ #
12
+ # @yield block to execute
13
+ # @return [self]
14
+ def initialize(&block)
15
+ @thread = Thread.new(&block)
16
+ end
17
+
18
+ # @return [Object] result value
19
+ def value
20
+ unless(@value)
21
+ @value = @thread.value
22
+ end
23
+ @value
24
+ end
25
+
26
+ # Check if value is available
27
+ #
28
+ # @return [TrueClass, FalseClass]
29
+ def available?
30
+ !thread.alive?
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ require 'zoidberg'
2
+ require 'mono_logger'
3
+
4
+ module Zoidberg
5
+ # Logger
6
+ class Logger < MonoLogger
7
+
8
+ # Quick override to ensure destination has append mode enabled if
9
+ # file io type
10
+ def initialize(logdev, *args)
11
+ if(logdev.respond_to?(:path))
12
+ begin
13
+ require 'fcntl'
14
+ unless(logdev.fcntl(Fcntl::F_GETFL) & Fcntl::O_APPEND == Fcntl::O_APPEND)
15
+ logdev = File.open(logdev.path, (File::WRONLY | File::APPEND))
16
+ end
17
+ rescue; end
18
+ end
19
+ super(logdev, *args)
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,100 @@
1
+ require 'zoidberg'
2
+
3
+ module Zoidberg
4
+ # Populate a collection of instances and proxy requests to free
5
+ # instances within the pool
6
+ class Pool
7
+
8
+ include Zoidberg::Shell
9
+
10
+ # @return [Array<Object>] workers within pool
11
+ attr_reader :_workers
12
+ # @return [Signal] common signal for state updates
13
+ attr_reader :_signal
14
+
15
+ # Create a new pool instance. Provide class + instance
16
+ # initialization arguments when creating the pool. These will be
17
+ # used to build all instances within the pool.
18
+ #
19
+ # @return [self]
20
+ def initialize(*args, &block)
21
+ _validate_worker_class!(args.first)
22
+ @_signal = Signal.new
23
+ @_worker_count = 1
24
+ @_workers = []
25
+ @builder = lambda do
26
+ inst = args.first.new(
27
+ *args.slice(1, args.size),
28
+ &block
29
+ )
30
+ inst._zoidberg_signal = _signal
31
+ inst
32
+ end
33
+ _zoidberg_balance
34
+ end
35
+
36
+ # Validate worker class is properly supervised
37
+ #
38
+ # @raise [TypeError]
39
+ def _validate_worker_class!(klass)
40
+ unless(klass.ancestors.include?(Zoidberg::Supervise))
41
+ raise TypeError.new "Worker class `#{klass}` must include the `Zoidberg::Supervise` module!"
42
+ end
43
+ end
44
+
45
+ # Set or get the number of workers within the pool
46
+ #
47
+ # @param num [Integer]
48
+ # @return [Integer]
49
+ def _worker_count(num=nil)
50
+ if(num)
51
+ @_worker_count = num.to_i
52
+ _zoidberg_balance
53
+ end
54
+ @_worker_count
55
+ end
56
+
57
+ # Balance the pool to ensure the correct number of workers are
58
+ # available
59
+ #
60
+ # @return [TrueClass]
61
+ def _zoidberg_balance
62
+ unless(_workers.size == _worker_count)
63
+ if(_workers.size < _worker_count)
64
+ (_worker_count - _workers.size).times do
65
+ _workers << @builder.call
66
+ end
67
+ else
68
+ (_workers.size - _worker_count).times do
69
+ worker = _zoidberg_free_worker
70
+ worker._zoidberg_destroy!
71
+ _workers.delete(worker)
72
+ end
73
+ end
74
+ end
75
+ true
76
+ end
77
+
78
+ # Used to proxy request to worker
79
+ def method_missing(*args, &block)
80
+ worker = _zoidberg_free_worker
81
+ defer{ worker.send(*args, &block) }
82
+ end
83
+
84
+ # Find or wait for a free worker
85
+ #
86
+ # @return [Object]
87
+ def _zoidberg_free_worker
88
+ until(worker = @_workers.detect(&:_zoidberg_available?))
89
+ defer{ _signal.wait_for(:unlocked) }
90
+ end
91
+ worker
92
+ end
93
+
94
+ # Force termination of all workers when terminated
95
+ def terminate
96
+ @_workers.map(&:_zoidberg_destroy!)
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,175 @@
1
+ require 'fiber'
2
+ require 'zoidberg'
3
+
4
+ module Zoidberg
5
+ class Proxy
6
+ class Confined < Proxy
7
+
8
+ # @return [Thread] container thread
9
+ attr_reader :_source_thread
10
+ # @return [Queue] current request queue
11
+ attr_reader :_requests
12
+ # @return [TrueClass, FalseClass] blocked running task
13
+ attr_reader :_blocked
14
+
15
+ # Create a new isolation wrapper
16
+ #
17
+ # @param object [Object] object to wrap
18
+ # @return [self]
19
+ def initialize(klass, *args, &block)
20
+ @_requests = ::Queue.new
21
+ @_blocked = false
22
+ @_source_thread = ::Thread.new do
23
+ ::Zoidberg.logger.debug 'Starting the isolation request processor'
24
+ ::Thread.current[:root_fiber] = ::Fiber.current
25
+ _isolate!
26
+ end
27
+ @_build_args = [klass, *args, block]
28
+ @_raw_instance = klass.unshelled_new(*args, &block)
29
+ @_raw_instance._zoidberg_proxy(self)
30
+ if(@_raw_instance.class.include?(::Zoidberg::Supervise))
31
+ @_supervised = true
32
+ end
33
+ ::Zoidberg.logger.debug "Zoidberg object isolation wrap: #{@_build_args.inspect}"
34
+ end
35
+
36
+ # Call into instance asynchronously
37
+ #
38
+ # @note use caution with shared data using this method
39
+ def _async_request(blocking, method_name, *args, &block)
40
+ ::Zoidberg.logger.debug "Received async request from remote thread. Added to queue: #{_raw_instance.class}##{method_name}(#{args.map(&:inspect).join(', ')})"
41
+ _requests << ::Smash.new(
42
+ :uuid => ::Zoidberg.uuid,
43
+ :arguments => [method_name, *args],
44
+ :block => block,
45
+ :response => nil,
46
+ :async => true,
47
+ :blocking => blocking == :blocking
48
+ )
49
+ nil
50
+ end
51
+
52
+ # Wrapping for provided object
53
+ def method_missing(*args, &block)
54
+ res = nil
55
+ begin
56
+ if(::ENV['ZOIDBERG_TESTING'])
57
+ ::Kernel.require 'timeout'
58
+ ::Timeout.timeout(::ENV.fetch('ZOIDBERG_TESTING_TIMEOUT', 5).to_i) do
59
+ res = _isolated_request(*args, &block)
60
+ end
61
+ else
62
+ res = _isolated_request(*args, &block)
63
+ end
64
+ rescue ::Zoidberg::Supervise::AbortException => e
65
+ ::Kernel.raise e.original_exception
66
+ rescue ::Exception => e
67
+ _zoidberg_unexpected_error(e)
68
+ ::Zoidberg.logger.debug "Exception on: #{_raw_instance.class}##{args.first}(#{args.slice(1, args.size).map(&:inspect).join(', ')})"
69
+ ::Kernel.raise e
70
+ end
71
+ res
72
+ end
73
+
74
+ # Send the method request to the wrapped instance
75
+ #
76
+ # @param method_name [String, Symbol] method to call on instance
77
+ # @param args [Object] arguments for call
78
+ # @yield block for call
79
+ # @return [Object] result
80
+ def _isolated_request(method_name, *args, &block)
81
+ if(_source_thread == ::Thread.current)
82
+ ::Zoidberg.logger.debug "Received request from source thread: #{_raw_instance.class}##{method_name}(#{args.map(&:inspect).join(', ')})"
83
+ _raw_instance.__send__(method_name, *args, &block)
84
+ else
85
+ unless(_source_thread.alive?)
86
+ ::Kernel.raise ::Zoidberg::DeadException.new('Instance in terminated state!')
87
+ end
88
+ ::Zoidberg.logger.debug "Received request from remote thread. Added to queue: #{_raw_instance.class}##{method_name}(#{args.map(&:inspect).join(', ')})"
89
+ response_queue = ::Queue.new
90
+ _requests << ::Smash.new(
91
+ :uuid => ::Zoidberg.uuid,
92
+ :arguments => [method_name, *args],
93
+ :block => block,
94
+ :response => response_queue
95
+ )
96
+ result = response_queue.pop
97
+ if(result.is_a?(::Exception))
98
+ ::Kernel.raise result
99
+ else
100
+ result
101
+ end
102
+ end
103
+ end
104
+
105
+ def _zoidberg_available?
106
+ !_blocked
107
+ end
108
+
109
+ protected
110
+
111
+ # Process requests
112
+ def _isolate!
113
+ begin
114
+ ::Kernel.loop do
115
+ begin
116
+ _process_request(_requests.pop)
117
+ rescue => e
118
+ ::Zoidberg.logger.error "Unexpected looping error! (#{e.class}: #{e})"
119
+ ::Zoidberg.logger.error "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
120
+ ::Thread.main.raise e
121
+ end
122
+ end
123
+ ensure
124
+ until(_requests.empty)
125
+ requests.pop[:response] << ::Zoidberg::DeadException.new('Instance in terminated state!')
126
+ end
127
+ end
128
+ end
129
+
130
+ # Process a request
131
+ #
132
+ # @param request [Hash]
133
+ # @return [self]
134
+ def _process_request(request)
135
+ begin
136
+ @_blocked = !request[:async]
137
+ ::Zoidberg.logger.debug "Processing received request: #{request.inspect}"
138
+ unless(request[:task])
139
+ request[:task] = ::Zoidberg::Task.new(request[:async] ? :async : :serial, _raw_instance, [request]) do |req|
140
+ begin
141
+ result = origin.__send__(
142
+ *req[:arguments],
143
+ &req[:block]
144
+ )
145
+ if(req[:response])
146
+ req[:response] << result
147
+ end
148
+ rescue ::Exception => exception
149
+ if(req[:response])
150
+ req[:response] << exception
151
+ end
152
+ end
153
+ end
154
+ end
155
+ if(request[:task].waiting?)
156
+ if(_raw_instance.alive?)
157
+ request[:task].proceed
158
+ request[:task].value if request[:blocking]
159
+ else
160
+ request[:response] << ::Zoidberg::DeadException.new('Instance in terminated state!')
161
+ request[:task].halt!
162
+ end
163
+ end
164
+ _requests.push(request) unless request[:task].complete? || request[:async]
165
+ ::Zoidberg.logger.debug "Request processing completed. #{request.inspect}"
166
+ ensure
167
+ @_blocked = false
168
+ end
169
+ self
170
+ end
171
+
172
+ end
173
+
174
+ end
175
+ end
@@ -0,0 +1,135 @@
1
+ require 'zoidberg'
2
+
3
+ module Zoidberg
4
+ class Proxy
5
+
6
+ class Liberated < Proxy
7
+
8
+ # @return [Thread] current owner of lock
9
+ attr_reader :_locker
10
+ # @return [Hash<Integer:Thread>]
11
+ attr_reader :_raw_threads
12
+
13
+ # Create a new proxy instance, new real instance, and link them
14
+ #
15
+ # @return [self]
16
+ def initialize(klass, *args, &block)
17
+ @_build_args = [klass, args, block]
18
+ @_lock = ::Mutex.new
19
+ @_count_lock = ::Mutex.new
20
+ @_accessing_threads = []
21
+ @_locker = nil
22
+ @_locker_count = 0
23
+ @_zoidberg_signal = nil
24
+ @_raw_instance = klass.unshelled_new(*args, &block)
25
+ @_raw_instance._zoidberg_proxy(self)
26
+ @_raw_threads = ::Smash.new{ ::Array.new }
27
+ if(@_raw_instance.class.ancestors.include?(::Zoidberg::Supervise))
28
+ @_supervised = true
29
+ end
30
+ end
31
+
32
+ # Used to proxy request to real instance
33
+ def method_missing(*args, &block)
34
+ res = nil
35
+ @_accessing_threads << ::Thread.current
36
+ begin
37
+ _aquire_lock!
38
+ if(::ENV['ZOIDBERG_TESTING'])
39
+ ::Kernel.require 'timeout'
40
+ ::Timeout.timeout(::ENV.fetch('ZOIDBERG_TESTING_TIMEOUT', 5).to_i) do
41
+ res = @_raw_instance.__send__(*args, &block)
42
+ end
43
+ else
44
+ res = @_raw_instance.__send__(*args, &block)
45
+ end
46
+ rescue ::Zoidberg::Supervise::AbortException => e
47
+ ::Kernel.raise e.original_exception
48
+ rescue ::Exception => e
49
+ ::Zoidberg.logger.debug "Exception on: #{_raw_instance.class.name}##{args.first}(#{args.slice(1, args.size).map(&:inspect).join(', ')})"
50
+ _zoidberg_unexpected_error(e)
51
+ if(e.class.to_s == 'fatal' && !@_fatal_retry)
52
+ @_fatal_retry = true
53
+ retry
54
+ else
55
+ ::Kernel.raise e
56
+ end
57
+ ensure
58
+ _release_lock!
59
+ t_idx = @_accessing_threads.index(::Thread.current)
60
+ @_accessing_threads.delete_at(t_idx) if t_idx
61
+ end
62
+ res
63
+ end
64
+
65
+ # @return [TrueClass, FalseClass] currently locked
66
+ def _zoidberg_locked?
67
+ @_lock && @_lock.locked?
68
+ end
69
+
70
+ # @return [TrueClass, FalseClass] currently unlocked
71
+ def _zoidberg_available?
72
+ !_zoidberg_locked?
73
+ end
74
+
75
+ # Aquire the lock to access real instance. If already locked, will
76
+ # wait until lock can be aquired.
77
+ #
78
+ # @return [TrueClas]
79
+ def _aquire_lock!
80
+ if(@_lock)
81
+ @_lock.lock unless @_locker == ::Thread.current
82
+ @_locker = ::Thread.current
83
+ @_locker_count += 1
84
+ _zoidberg_signal(:locked)
85
+ end
86
+ true
87
+ end
88
+
89
+ # Release the lock to access real instance
90
+ #
91
+ # @return [TrueClass]
92
+ def _release_lock!
93
+ if(@_lock)
94
+ if(@_locker == ::Thread.current)
95
+ @_locker_count -= 1
96
+ if(@_locker_count < 1)
97
+ @_locker = nil
98
+ @_lock.unlock if @_lock.locked?
99
+ end
100
+ end
101
+ _zoidberg_signal(:unlocked)
102
+ end
103
+ true
104
+ end
105
+
106
+ # Ensure any async threads are killed and accessing threads are
107
+ # forced into error state.
108
+ #
109
+ # @return [TrueClass]
110
+ def _zoidberg_destroy!(error=nil)
111
+ super do
112
+ _raw_threads[_raw_instance.object_id].map do |thread|
113
+ thread.raise ::Zoidberg::DeadException.new('Instance in terminated state!')
114
+ end.map do |thread|
115
+ thread.join(2)
116
+ end.find_all(&:alive?).map(&:kill)
117
+ _raw_threads.delete(_raw_instance.object_id)
118
+ @_accessing_threads.each do |thr|
119
+ if(thr.alive?)
120
+ begin
121
+ thr.raise ::Zoidberg::DeadException.new('Instance in terminated state!')
122
+ rescue
123
+ end
124
+ end
125
+ end
126
+ @_accessing_threads.clear
127
+ end
128
+ end
129
+
130
+ end
131
+
132
+
133
+ end
134
+
135
+ end