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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f12f7686bfce42183b33d6a765027eed64113aee
|
4
|
+
data.tar.gz: 1b12f2ad1e6210a086c60c3dac02599cef678ba1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17f6a8e0cb57573076bbeb852c392699b8bdbde0f88b9818a213704cfb6e1c5c37e80cd6b207821e61f47f4d30e8825177e671136e209e2e197e1f0735063dd9
|
7
|
+
data.tar.gz: f8d4c6aba250fdf670e996bbb8668081cb21aa2ab0c8a049c7c8878f086af766e4525de47e2e1968c0b431985bdf3894ac05627a485b21fe78dbddb70dc048c9
|
data/CHANGELOG.md
CHANGED
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
|