zookeeper 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.dotfiles/rvmrc CHANGED
@@ -1 +1,2 @@
1
1
  rvm ruby-1.9.3@zookeeper --create
2
+ export ZK_DEV=1
data/.travis.yml CHANGED
@@ -20,3 +20,8 @@ rvm:
20
20
 
21
21
  bundler_args: --without development docs coverage
22
22
 
23
+ # blacklist
24
+ branches:
25
+ except:
26
+ # - 'dev/zookeeper-st'
27
+
data/CHANGELOG CHANGED
@@ -1,3 +1,16 @@
1
+ v1.1.0 Rewrite C backend to use zookeeper_st, the async library
2
+
3
+ * In order to ensure fork safety, a rewrite of the backend was necessary.
4
+ It was impossible to guarantee with the mt lib that a lock would not
5
+ be held by a thread when fork() was called, which opened up the possibility
6
+ for corruption and other badness.
7
+
8
+ This version contains a Continuation class, which allows us to present a
9
+ synchronous front-end to the asynchronous backend. All features are still
10
+ supported, no special action is necessary to prepare for a fork, and the
11
+ post-fork procedure is the same as before: call reopen() in the child,
12
+ continue on in the parent like nothing happened.
13
+
1
14
  v1.0.6 Only include backports if RUBY_VERSION is 1.8.x
2
15
 
3
16
  * 'backports' pollutes too much, use sparingly
data/Gemfile CHANGED
@@ -30,6 +30,9 @@ end
30
30
 
31
31
  group :development do
32
32
  gem 'pry'
33
+ gem 'guard', :require => false
34
+ gem 'guard-rspec', :require => false
35
+ gem 'guard-shell', :require => false
33
36
  end
34
37
 
35
38
  # vim:ft=ruby
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard 'rspec' do
2
+ watch(%r{^spec/.+_spec.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| %w[spec/zookeeper_spec.rb spec/chrooted_connection_spec.rb] }
4
+ watch(%r{^ext/zookeeper_c.bundle}) { %w[spec/c_zookeeper_spec.rb] }
5
+ end
6
+
data/README.markdown CHANGED
@@ -6,6 +6,10 @@ An interface to the Zookeeper cluster coordination server.
6
6
 
7
7
  For a higher-level interface with a more convenient API and features such as locks, have a look at [ZK](https://github.com/slyphon/zk) (also available is [ZK-EventMachine](https://github.com/slyphon/zk-eventmachine) for those who prefer async).
8
8
 
9
+ ## Fork Safety! ##
10
+
11
+ As of 1.1.0, this library is fork-safe (which was not easy to accomplish). This means you can use it without worry in unicorn, resque, and whatever other fork-philic frameworks you sick little monkeys are using this week. The only rule is that after a fork(), you need to call `#reopen` on the client ASAP, because if you try to peform any other action, an exception will be raised. Other than that, there is no special action that is needed in the parent.
12
+
9
13
  ## Big Plans for 1.0 ##
10
14
 
11
15
  The 1.0 release will feature a reorganization of the heirarchy. There will be a single top-level `Zookeeper` namespace (as opposed to the current layout, with 5-6 different top-level constants), and for the next several releases, there will be a backwards compatible require for users that still need to use the old names.
@@ -36,18 +40,18 @@ Connect to a server:
36
40
  ## Idioms
37
41
 
38
42
  The following methods are initially supported:
39
- * get
40
- * set
41
- * get\_children
42
- * stat
43
- * create
44
- * delete
45
- * get\_acl
46
- * set\_acl
47
-
48
- All support async callbacks. get, get\_children and stat support both watchers and callbacks.
49
-
50
- Calls take a dictionary of parameters. With the exception of set\_acl, the only required parameter is :path. Each call returns a dictionary with at minimum two keys :req\_id and :rc.
43
+ * `get`
44
+ * `set`
45
+ * `get_children`
46
+ * `stat`
47
+ * `create`
48
+ * `delete`
49
+ * `get_acl`
50
+ * `set_acl`
51
+
52
+ All support async callbacks. `get`, `get_children` and `stat` support both watchers and callbacks.
53
+
54
+ Calls take a dictionary of parameters. With the exception of set\_acl, the only required parameter is `:path`. Each call returns a dictionary with at minimum two keys :req\_id and :rc.
51
55
 
52
56
  ### A Bit about this repository ###
53
57
 
data/Rakefile CHANGED
@@ -100,6 +100,11 @@ end
100
100
 
101
101
  task 'spec:run' => 'build:clean' unless defined?(::JRUBY_VERSION)
102
102
 
103
+ task 'ctags' do
104
+ sh 'bundle-ctags'
105
+ end
106
+
103
107
  # because i'm a creature of habit
104
108
  task 'mb:test_all' => 'zk:test_all'
105
109
 
110
+
data/cause-abort.rb ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'zookeeper'
4
+ require File.expand_path('../spec/support/zookeeper_spec_helpers', __FILE__)
5
+
6
+ class CauseAbort
7
+ include Zookeeper::Logger
8
+ include Zookeeper::SpecHelpers
9
+
10
+ attr_reader :path, :pids_root, :data
11
+
12
+ def initialize
13
+ @path = "/_zktest_"
14
+ @pids_root = "#{@path}/pids"
15
+ @data = 'underpants'
16
+ end
17
+
18
+ def before
19
+ @zk = Zookeeper.new('localhost:2181')
20
+ rm_rf(@zk, path)
21
+ logger.debug { "----------------< BEFORE: END >-------------------" }
22
+ end
23
+
24
+ def process_alive?(pid)
25
+ Process.kill(0, @pid)
26
+ true
27
+ rescue Errno::ESRCH
28
+ false
29
+ end
30
+
31
+ def wait_for_child_safely(pid, timeout=5)
32
+ time_to_stop = Time.now + timeout
33
+
34
+ until Time.now > time_to_stop
35
+ if a = Process.wait2(@pid, Process::WNOHANG)
36
+ return a.last
37
+ else
38
+ sleep(0.01)
39
+ end
40
+ end
41
+
42
+ nil
43
+ end
44
+
45
+ def try_pause_and_resume
46
+ @zk.pause
47
+ logger.debug { "paused" }
48
+ @zk.resume
49
+ logger.debug { "resumed" }
50
+ @zk.close
51
+ logger.debug { "closed" }
52
+ end
53
+
54
+ def run_test
55
+ logger.debug { "----------------< TEST: BEGIN >-------------------" }
56
+ @zk.wait_until_connected
57
+
58
+ mkdir_p(@zk, pids_root)
59
+
60
+ # the parent's pid path
61
+ @zk.create(:path => "#{pids_root}/#{$$}", :data => $$.to_s)
62
+
63
+ @latch = Zookeeper::Latch.new
64
+ @event = nil
65
+
66
+ cb = proc do |h|
67
+ logger.debug { "watcher called back: #{h.inspect}" }
68
+ @event = h
69
+ @latch.release
70
+ end
71
+
72
+ @zk.stat(:path => "#{pids_root}/child", :watcher => cb)
73
+
74
+ logger.debug { "-------------------> FORK <---------------------------" }
75
+
76
+ @pid = fork do
77
+ rand_sleep = rand()
78
+
79
+ $stderr.puts "sleeping for rand_sleep: #{rand_sleep}"
80
+ sleep(rand_sleep)
81
+
82
+ logger.debug { "reopening connection in child: #{$$}" }
83
+ @zk.reopen
84
+ logger.debug { "creating path" }
85
+ rv = @zk.create(:path => "#{pids_root}/child", :data => $$.to_s)
86
+ logger.debug { "created path #{rv[:path]}" }
87
+ @zk.close
88
+
89
+ logger.debug { "close finished" }
90
+ exit!(0)
91
+ end
92
+
93
+ event_waiter_th = Thread.new do
94
+ @latch.await(5) unless @event
95
+ @event
96
+ end
97
+
98
+ logger.debug { "waiting on child #{@pid}" }
99
+
100
+ status = wait_for_child_safely(@pid)
101
+ raise "Child process did not exit, likely hung" unless status
102
+
103
+ if event_waiter_th.join(5) == event_waiter_th
104
+ logger.warn { "event waiter has not received events" }
105
+ end
106
+
107
+ exit(@event.nil? ? 1 : 0)
108
+ end
109
+
110
+ def run
111
+ before
112
+ run_test
113
+ # try_pause_and_resume
114
+ end
115
+ end
116
+
117
+ CauseAbort.new.run
data/ext/Rakefile CHANGED
@@ -1,20 +1,41 @@
1
+ require 'rbconfig'
1
2
 
2
- task :clean do
3
- if File.exists?('Makefile')
4
- sh 'make clean'
5
- else
6
- $stderr.puts "nothing to clean, no Makefile"
3
+ LIB_ZK_SO = 'lib/libzookeeper_mt_gem.la'
4
+
5
+ TARBALL = FileList['zkc-*.tar.gz'].first
6
+
7
+ raise "Where is the zkc tarball!?" unless TARBALL
8
+
9
+ namespace :zkrb do
10
+ task :clean do
11
+ if File.exists?('Makefile')
12
+ sh 'make clean'
13
+ rm 'Makefile' # yep, regenerate this
14
+ else
15
+ $stderr.puts "nothing to clean, no Makefile"
16
+ end
17
+ end
18
+
19
+ task :clobber => :clean do
20
+ rm_rf %w[Makefile c lib bin include]
7
21
  end
8
22
  end
9
23
 
24
+ task :clean => 'zkrb:clean'
25
+ task :clobber => 'zkrb:clobber'
26
+
10
27
  GENERATE_GVL_CODE_RB = 'generate_gvl_code.rb'
11
28
 
12
- file 'c' do
13
- if tarball = Dir['zkc-*.tar.gz'].first
14
- sh "tar -zxf #{tarball}"
15
- else
16
- raise "couldn't find the tarball! wtf?!"
17
- end
29
+ # file 'c' do
30
+ # if tarball = Dir['zkc-*.tar.gz'].first
31
+ # sh "tar -zxf #{tarball}"
32
+ # else
33
+ # raise "couldn't find the tarball! wtf?!"
34
+ # end
35
+ # end
36
+
37
+ file 'c' => TARBALL do
38
+ sh "tar -zxf #{TARBALL}"
18
39
  end
19
40
 
20
41
  file GENERATE_GVL_CODE_RB => 'c'
@@ -29,20 +50,12 @@ end
29
50
 
30
51
  ZKRB_WRAPPER = %w[zkrb_wrapper.c zkrb_wrapper.h]
31
52
 
32
-
33
53
  task :wrappers => ZKRB_WRAPPER
34
54
 
35
-
36
- task :clobber => :clean do
37
- rm_rf %w[Makefile c lib bin include]
55
+ file 'Makefile' do
56
+ sh "ruby extconf.rb"
38
57
  end
39
58
 
40
- task :build_zkc do
41
- sh 'ruby extconf.rb'
42
- end
43
-
44
- file 'Makefile' => :build_zkc
45
-
46
59
  task :build => [ZKRB_WRAPPER, 'Makefile'].flatten do
47
60
  sh 'make'
48
61
  end
data/ext/c_zookeeper.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  require_relative '../lib/zookeeper/logger'
2
2
  require_relative '../lib/zookeeper/common'
3
3
  require_relative '../lib/zookeeper/constants'
4
+ require_relative '../lib/zookeeper/exceptions' # zookeeper_c depends on exceptions defined in here
4
5
  require_relative 'zookeeper_c'
5
6
 
6
7
  # require File.expand_path('../zookeeper_c', __FILE__)
7
8
 
8
- # TODO: see if we can get the destructor to handle thread/event queue teardown
9
- # when we're garbage collected
10
9
  module Zookeeper
10
+ # NOTE: this class extending (opening) the class defined in zkrb.c
11
11
  class CZookeeper
12
12
  include Forked
13
13
  include Constants
@@ -30,6 +30,15 @@ class CZookeeper
30
30
  set_zkrb_debug_level(value)
31
31
  end
32
32
 
33
+ # wrap these calls in our sync->async special sauce
34
+ %w[get set exists create delete get_acl set_acl get_children].each do |sym|
35
+ class_eval(<<-EOS, __FILE__, __LINE__+1)
36
+ def #{sym}(*args)
37
+ submit_and_block(:#{sym}, *args)
38
+ end
39
+ EOS
40
+ end
41
+
33
42
  def initialize(host, event_queue, opts={})
34
43
  @host = host
35
44
  @event_queue = event_queue
@@ -40,7 +49,7 @@ class CZookeeper
40
49
  # used by the C layer. CZookeeper sets this to true when the init method
41
50
  # has completed. once this is set to true, it stays true.
42
51
  #
43
- # you should grab the @start_stop_mutex before messing with this flag
52
+ # you should grab the @mutex before messing with this flag
44
53
  @_running = nil
45
54
 
46
55
  # This is set to true after destroy_zkrb_instance has been called and all
@@ -55,40 +64,40 @@ class CZookeeper
55
64
 
56
65
  @_session_timeout_msec = DEFAULT_SESSION_TIMEOUT_MSEC
57
66
 
58
- if cid = opts[:client_id]
59
- raise ArgumentError, "opts[:client_id] must be a CZookeeper::ClientId" unless cid.kind_of?(ClientId)
60
- raise ArgumentError, "session_id must not be nil" if cid.session_id.nil?
61
- end
62
-
63
- @_client_id = opts[:client_id]
64
-
65
- @start_stop_mutex = Monitor.new
67
+ @mutex = Monitor.new
66
68
 
67
69
  # used to signal that we're running
68
- @running_cond = @start_stop_mutex.new_cond
70
+ @running_cond = @mutex.new_cond
69
71
 
70
72
  # used to signal we've received the connected event
71
- @connected_cond = @start_stop_mutex.new_cond
73
+ @connected_cond = @mutex.new_cond
74
+
75
+ @pipe_read, @pipe_write = IO.pipe
72
76
 
73
77
  @event_thread = nil
74
78
 
75
- setup_event_thread!
79
+ # hash of in-flight Continuation instances
80
+ @reg = Continuation::Registry.new
81
+
82
+ log_level = ENV['ZKC_DEBUG'] ? ZOO_LOG_LEVEL_DEBUG : ZOO_LOG_LEVEL_ERROR
83
+
84
+ zkrb_init(@host)#, :zkc_log_level => log_level)
76
85
 
77
- zkrb_init(@host)
86
+ start_event_thread
78
87
 
79
88
  logger.debug { "init returned!" }
80
89
  end
81
90
 
82
91
  def closed?
83
- @start_stop_mutex.synchronize { !!@_closed }
92
+ @mutex.synchronize { !!@_closed }
84
93
  end
85
94
 
86
95
  def running?
87
- @start_stop_mutex.synchronize { !!@_running }
96
+ @mutex.synchronize { !!@_running }
88
97
  end
89
98
 
90
99
  def shutting_down?
91
- @start_stop_mutex.synchronize { !!@_shutting_down }
100
+ @mutex.synchronize { !!@_shutting_down }
92
101
  end
93
102
 
94
103
  def connected?
@@ -116,13 +125,34 @@ class CZookeeper
116
125
  if forked?
117
126
  fn_close.call
118
127
  else
119
- shut_down!
120
- stop_event_thread!
121
- @start_stop_mutex.synchronize(&fn_close)
128
+ stop_event_thread
129
+ @mutex.synchronize(&fn_close)
122
130
  end
123
131
 
132
+ [@pipe_read, @pipe_write].each { |io| io.close unless io.closed? }
133
+
124
134
  nil
125
135
  end
136
+
137
+ # call this to stop the event loop, you can resume with the
138
+ # resume method
139
+ #
140
+ # requests may still be added during this time, but they will not be
141
+ # processed until you call resume
142
+ def pause
143
+ logger.debug { "#{self.class}##{__method__}" }
144
+ @mutex.synchronize { stop_event_thread }
145
+ end
146
+
147
+ # call this if 'pause' was previously called to start the event loop again
148
+ def resume
149
+ logger.debug { "#{self.class}##{__method__}" }
150
+
151
+ @mutex.synchronize do
152
+ @_shutting_down = nil
153
+ start_event_thread
154
+ end
155
+ end
126
156
 
127
157
  def state
128
158
  return ZOO_CLOSED_STATE if closed?
@@ -137,107 +167,188 @@ class CZookeeper
137
167
  # if timeout is nil, we never time out, and wait forever for CONNECTED state
138
168
  #
139
169
  def wait_until_connected(timeout=10)
140
- @start_stop_mutex.synchronize do
141
- wait_until_running(timeout)
142
- @connected_cond.wait(timeout) unless connected?
143
- end
170
+ # this begin/ensure/end style is recommended by tarceri
171
+ # no need to create a context for every mutex grab
144
172
 
173
+ wait_until_running(timeout)
174
+
175
+ Thread.pass until connected? or is_unrecoverable
145
176
  connected?
146
177
  end
147
178
 
179
+
148
180
  private
181
+ # submits a job for processing
182
+ # blocks the caller until result has returned
183
+ def submit_and_block(meth, *args)
184
+ cnt = Continuation.new(meth, *args)
185
+ @reg.synchronized { |r| r.pending << cnt }
186
+ wake_event_loop!
187
+ cnt.value
188
+ end
189
+
190
+ # this method is part of the reopen/close code, and is responsible for
191
+ # shutting down the dispatch thread.
192
+ #
193
+ # @event_thread will be nil when this method exits
194
+ #
195
+ def stop_event_thread
196
+ if @event_thread
197
+ logger.debug { "#{self.class}##{__method__}" }
198
+ shut_down!
199
+ wake_event_loop!
200
+ @event_thread.join
201
+ @event_thread = nil
202
+ end
203
+ end
204
+
205
+ # starts the event thread running if not already started
206
+ # returns false if already running
207
+ def start_event_thread
208
+ return false if @event_thread
209
+ @event_thread = Thread.new(&method(:event_thread_body))
210
+ end
211
+
149
212
  # will wait until the client has entered the running? state
150
213
  # or until timeout seconds have passed.
151
214
  #
152
215
  # returns true if we're running, false if we timed out
153
- def wait_until_running(timeout=5)
154
- @start_stop_mutex.synchronize do
216
+ def wait_until_running(timeout=5)
217
+ @mutex.lock
218
+ begin
155
219
  return true if @_running
156
220
  @running_cond.wait(timeout)
157
221
  !!@_running
222
+ ensure
223
+ @mutex.unlock
158
224
  end
159
225
  end
160
226
 
161
- def setup_event_thread!
162
- @event_thread ||= Thread.new(&method(:_event_thread_body))
163
- end
227
+ def event_thread_body
228
+ Thread.current.abort_on_exception = true
229
+ logger.debug { "#{self.class}##{__method__} starting event thread" }
164
230
 
165
- def _event_thread_body
166
- logger.debug { "event_thread waiting until running: #{@_running}" }
231
+ event_thread_await_running
232
+
233
+ # this is the main loop
234
+ until (@_shutting_down or @_closed or is_unrecoverable)
235
+ submit_pending_calls if @reg.pending?
236
+ # log_realtime("zkrb_iterate_event_loop") do
237
+ zkrb_iterate_event_loop # XXX: check rc here
238
+ # end
239
+ iterate_event_delivery
240
+ end
167
241
 
168
- @start_stop_mutex.synchronize do
169
- @running_cond.wait_until { @_running }
242
+ # ok, if we're exiting the event loop, and we still have a valid connection
243
+ # and there's still completions we're waiting to hear about, then we
244
+ # should pump the handle before leaving this loop
245
+ if @_shutting_down and not (@_closed or is_unrecoverable or @reg.in_flight.empty?)
246
+ logger.debug { "we're in shutting down state, ensuring we have no in-flight completions" }
170
247
 
171
- if @_shutting_down
172
- logger.error { "event thread saw @_shutting_down, bailing without entering loop" }
173
- return
248
+ until @reg.in_flight.empty?
249
+ zkrb_iterate_event_loop
250
+ iterate_event_delivery
174
251
  end
252
+
253
+ logger.debug { "finished completions" }
175
254
  end
176
255
 
177
- logger.debug { "event_thread running: #{@_running}" }
256
+ rescue ShuttingDownException
257
+ logger.error { "event thread saw @_shutting_down, bailing without entering loop" }
258
+ ensure
259
+ logger.debug { "#{self.class}##{__method__} exiting" }
260
+ end
178
261
 
179
- until @_shutting_down
180
- begin
181
- _iterate_event_delivery
182
- rescue GotNilEventException
183
- logger.debug { "#{self.class}##{__method__}: event delivery thread is exiting" }
184
- break
185
- end
262
+ def submit_pending_calls
263
+ # this is ok, because the calling thread only ever *adds* to this hash,
264
+ # and the keys are always unique
265
+
266
+ pending = nil
267
+
268
+ @reg.lock
269
+ begin
270
+ pending, @reg.pending = @reg.pending, []
271
+ ensure
272
+ @reg.unlock
186
273
  end
274
+
275
+ return if pending.empty?
276
+
277
+ logger.debug { "#{self.class}##{__method__} " }
278
+
279
+ while cntn = pending.shift
280
+ cntn.submit(self)
281
+ @reg.in_flight[cntn.req_id] = cntn # in_flight is only ever touched by us
282
+ end
283
+ end
284
+
285
+ def wake_event_loop!
286
+ logger.debug { "#{self.class}##{__method__}" }
287
+ @pipe_write && @pipe_write.write("\001")
187
288
  end
188
289
 
189
- def _iterate_event_delivery
190
- get_next_event(true).tap do |hash|
191
- raise GotNilEventException if hash.nil?
290
+ def iterate_event_delivery
291
+ while hash = zkrb_get_next_event_st()
292
+ logger.debug { "#{self.class}##{__method__} got #{hash.inspect} " }
192
293
 
193
- # TODO: should push notify_connected! down so that it's common to both java and C impl.
194
- if hash.values_at(:req_id, :type, :state) == CONNECTED_EVENT_VALUES
294
+ # notify when we get this event so we know we're connected
295
+ if hash.values_at(:req_id, :type, :state) == CONNECTED_EVENT_VALUES[1..2]
195
296
  notify_connected!
196
297
  end
197
298
 
299
+ cntn = @reg.in_flight.delete(hash[:req_id])
300
+
301
+ if cntn and not cntn.user_callback? # this is one of "our" continuations
302
+ cntn.call(hash) # so we handle delivering it
303
+ next # and skip handing it to the dispatcher
304
+ end
305
+
306
+ # otherwise, the event was a session event (ZKRB_GLOBAL_CB_REQ)
307
+ # or a user-provided callback
198
308
  @event_queue.push(hash)
199
309
  end
200
310
  end
201
311
 
202
- # use this method to set the @_shutting_down flag to true
203
- def shut_down!
204
- logger.debug { "#{self.class}##{__method__}" }
312
+ def event_thread_await_running
313
+ logger.debug { "event_thread waiting until running: #{@_running}" }
205
314
 
206
- @start_stop_mutex.synchronize do
207
- @_shutting_down = true
315
+ @mutex.lock
316
+ begin
317
+ @running_cond.wait_until { @_running or @_shutting_down }
318
+ logger.debug { "event_thread running: #{@_running}" }
319
+
320
+ raise ShuttingDownException if @_shutting_down
321
+ ensure
322
+ @mutex.unlock
208
323
  end
209
324
  end
210
325
 
211
- # this method is part of the reopen/close code, and is responsible for
212
- # shutting down the dispatch thread.
213
- #
214
- # @dispatch will be nil when this method exits
215
- #
216
- def stop_event_thread!
326
+ # use this method to set the @_shutting_down flag to true
327
+ def shut_down!
217
328
  logger.debug { "#{self.class}##{__method__}" }
218
329
 
219
- if @event_thread
220
- unless @_closed
221
- wake_event_loop! # this is a C method
222
- end
223
- @event_thread.join
224
- @event_thread = nil
225
- end
330
+ @mutex.synchronize { @_shutting_down = true }
226
331
  end
227
332
 
228
333
  # called by underlying C code to signal we're running
229
334
  def zkc_set_running_and_notify!
230
335
  logger.debug { "#{self.class}##{__method__}" }
231
336
 
232
- @start_stop_mutex.synchronize do
337
+ @mutex.lock
338
+ begin
233
339
  @_running = true
234
340
  @running_cond.broadcast
341
+ ensure
342
+ @mutex.unlock
235
343
  end
236
344
  end
237
345
 
238
346
  def notify_connected!
239
- @start_stop_mutex.synchronize do
347
+ @mutex.lock
348
+ begin
240
349
  @connected_cond.broadcast
350
+ ensure
351
+ @mutex.unlock
241
352
  end
242
353
  end
243
354
  end