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