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