grpc 0.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grpc might be problematic. Click here for more details.

Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +10 -0
  5. data/.rubocop_todo.yml +52 -0
  6. data/Gemfile +4 -0
  7. data/README.md +82 -0
  8. data/Rakefile +54 -0
  9. data/bin/apis/google/protobuf/empty.rb +44 -0
  10. data/bin/apis/pubsub_demo.rb +267 -0
  11. data/bin/apis/tech/pubsub/proto/pubsub.rb +174 -0
  12. data/bin/apis/tech/pubsub/proto/pubsub_services.rb +103 -0
  13. data/bin/interop/README.md +8 -0
  14. data/bin/interop/interop_client.rb +334 -0
  15. data/bin/interop/interop_server.rb +192 -0
  16. data/bin/interop/test/cpp/interop/empty.rb +44 -0
  17. data/bin/interop/test/cpp/interop/messages.rb +89 -0
  18. data/bin/interop/test/cpp/interop/test.rb +43 -0
  19. data/bin/interop/test/cpp/interop/test_services.rb +60 -0
  20. data/bin/math.proto +80 -0
  21. data/bin/math.rb +61 -0
  22. data/bin/math_client.rb +147 -0
  23. data/bin/math_server.rb +190 -0
  24. data/bin/math_services.rb +56 -0
  25. data/bin/noproto_client.rb +108 -0
  26. data/bin/noproto_server.rb +112 -0
  27. data/ext/grpc/extconf.rb +76 -0
  28. data/ext/grpc/rb_byte_buffer.c +241 -0
  29. data/ext/grpc/rb_byte_buffer.h +54 -0
  30. data/ext/grpc/rb_call.c +569 -0
  31. data/ext/grpc/rb_call.h +59 -0
  32. data/ext/grpc/rb_channel.c +264 -0
  33. data/ext/grpc/rb_channel.h +49 -0
  34. data/ext/grpc/rb_channel_args.c +154 -0
  35. data/ext/grpc/rb_channel_args.h +52 -0
  36. data/ext/grpc/rb_completion_queue.c +185 -0
  37. data/ext/grpc/rb_completion_queue.h +50 -0
  38. data/ext/grpc/rb_credentials.c +281 -0
  39. data/ext/grpc/rb_credentials.h +50 -0
  40. data/ext/grpc/rb_event.c +361 -0
  41. data/ext/grpc/rb_event.h +53 -0
  42. data/ext/grpc/rb_grpc.c +274 -0
  43. data/ext/grpc/rb_grpc.h +74 -0
  44. data/ext/grpc/rb_metadata.c +215 -0
  45. data/ext/grpc/rb_metadata.h +53 -0
  46. data/ext/grpc/rb_server.c +278 -0
  47. data/ext/grpc/rb_server.h +50 -0
  48. data/ext/grpc/rb_server_credentials.c +210 -0
  49. data/ext/grpc/rb_server_credentials.h +50 -0
  50. data/grpc.gemspec +41 -0
  51. data/lib/grpc.rb +39 -0
  52. data/lib/grpc/core/event.rb +44 -0
  53. data/lib/grpc/core/time_consts.rb +71 -0
  54. data/lib/grpc/errors.rb +61 -0
  55. data/lib/grpc/generic/active_call.rb +536 -0
  56. data/lib/grpc/generic/bidi_call.rb +221 -0
  57. data/lib/grpc/generic/client_stub.rb +413 -0
  58. data/lib/grpc/generic/rpc_desc.rb +150 -0
  59. data/lib/grpc/generic/rpc_server.rb +404 -0
  60. data/lib/grpc/generic/service.rb +235 -0
  61. data/lib/grpc/logconfig.rb +40 -0
  62. data/lib/grpc/version.rb +33 -0
  63. data/spec/alloc_spec.rb +44 -0
  64. data/spec/byte_buffer_spec.rb +67 -0
  65. data/spec/call_spec.rb +163 -0
  66. data/spec/channel_spec.rb +181 -0
  67. data/spec/client_server_spec.rb +372 -0
  68. data/spec/completion_queue_spec.rb +74 -0
  69. data/spec/credentials_spec.rb +71 -0
  70. data/spec/event_spec.rb +53 -0
  71. data/spec/generic/active_call_spec.rb +373 -0
  72. data/spec/generic/client_stub_spec.rb +519 -0
  73. data/spec/generic/rpc_desc_spec.rb +357 -0
  74. data/spec/generic/rpc_server_pool_spec.rb +139 -0
  75. data/spec/generic/rpc_server_spec.rb +404 -0
  76. data/spec/generic/service_spec.rb +342 -0
  77. data/spec/metadata_spec.rb +64 -0
  78. data/spec/server_credentials_spec.rb +69 -0
  79. data/spec/server_spec.rb +212 -0
  80. data/spec/spec_helper.rb +51 -0
  81. data/spec/testdata/README +1 -0
  82. data/spec/testdata/ca.pem +15 -0
  83. data/spec/testdata/server1.key +16 -0
  84. data/spec/testdata/server1.pem +16 -0
  85. data/spec/time_consts_spec.rb +89 -0
  86. metadata +353 -0
@@ -0,0 +1,150 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ require 'grpc/grpc'
31
+
32
+ # GRPC contains the General RPC module.
33
+ module GRPC
34
+ # RpcDesc is a Descriptor of an RPC method.
35
+ class RpcDesc < Struct.new(:name, :input, :output, :marshal_method,
36
+ :unmarshal_method)
37
+ include Core::StatusCodes
38
+
39
+ # Used to wrap a message class to indicate that it needs to be streamed.
40
+ class Stream
41
+ attr_accessor :type
42
+
43
+ def initialize(type)
44
+ @type = type
45
+ end
46
+ end
47
+
48
+ # @return [Proc] { |instance| marshalled(instance) }
49
+ def marshal_proc
50
+ proc { |o| o.class.method(marshal_method).call(o).to_s }
51
+ end
52
+
53
+ # @param [:input, :output] target determines whether to produce the an
54
+ # unmarshal Proc for the rpc input parameter or
55
+ # its output parameter
56
+ #
57
+ # @return [Proc] An unmarshal proc { |marshalled(instance)| instance }
58
+ def unmarshal_proc(target)
59
+ fail ArgumentError unless [:input, :output].include?(target)
60
+ unmarshal_class = method(target).call
61
+ unmarshal_class = unmarshal_class.type if unmarshal_class.is_a? Stream
62
+ proc { |o| unmarshal_class.method(unmarshal_method).call(o) }
63
+ end
64
+
65
+ def run_server_method(active_call, mth)
66
+ # While a server method is running, it might be cancelled, its deadline
67
+ # might be reached, the handler could throw an unknown error, or a
68
+ # well-behaved handler could throw a StatusError.
69
+ if request_response?
70
+ req = active_call.remote_read
71
+ resp = mth.call(req, active_call.single_req_view)
72
+ active_call.remote_send(resp)
73
+ elsif client_streamer?
74
+ resp = mth.call(active_call.multi_req_view)
75
+ active_call.remote_send(resp)
76
+ elsif server_streamer?
77
+ req = active_call.remote_read
78
+ replys = mth.call(req, active_call.single_req_view)
79
+ replys.each { |r| active_call.remote_send(r) }
80
+ else # is a bidi_stream
81
+ active_call.run_server_bidi(mth)
82
+ end
83
+ send_status(active_call, OK, 'OK')
84
+ rescue BadStatus => e
85
+ # this is raised by handlers that want GRPC to send an application
86
+ # error code and detail message.
87
+ logger.debug("app err: #{active_call}, status:#{e.code}:#{e.details}")
88
+ send_status(active_call, e.code, e.details)
89
+ rescue Core::CallError => e
90
+ # This is raised by GRPC internals but should rarely, if ever happen.
91
+ # Log it, but don't notify the other endpoint..
92
+ logger.warn("failed call: #{active_call}\n#{e}")
93
+ rescue OutOfTime
94
+ # This is raised when active_call#method.call exceeeds the deadline
95
+ # event. Send a status of deadline exceeded
96
+ logger.warn("late call: #{active_call}")
97
+ send_status(active_call, DEADLINE_EXCEEDED, 'late')
98
+ rescue Core::EventError => e
99
+ # This is raised by GRPC internals but should rarely, if ever happen.
100
+ # Log it, but don't notify the other endpoint..
101
+ logger.warn("failed call: #{active_call}\n#{e}")
102
+ rescue StandardError => e
103
+ # This will usuaally be an unhandled error in the handling code.
104
+ # Send back a UNKNOWN status to the client
105
+ logger.warn("failed handler: #{active_call}; sending status:UNKNOWN")
106
+ logger.warn(e)
107
+ send_status(active_call, UNKNOWN, 'no reason given')
108
+ end
109
+
110
+ def assert_arity_matches(mth)
111
+ if request_response? || server_streamer?
112
+ if mth.arity != 2
113
+ fail arity_error(mth, 2, "should be #{mth.name}(req, call)")
114
+ end
115
+ else
116
+ if mth.arity != 1
117
+ fail arity_error(mth, 1, "should be #{mth.name}(call)")
118
+ end
119
+ end
120
+ end
121
+
122
+ def request_response?
123
+ !input.is_a?(Stream) && !output.is_a?(Stream)
124
+ end
125
+
126
+ def client_streamer?
127
+ input.is_a?(Stream) && !output.is_a?(Stream)
128
+ end
129
+
130
+ def server_streamer?
131
+ !input.is_a?(Stream) && output.is_a?(Stream)
132
+ end
133
+
134
+ def bidi_streamer?
135
+ input.is_a?(Stream) && output.is_a?(Stream)
136
+ end
137
+
138
+ def arity_error(mth, want, msg)
139
+ "##{mth.name}: bad arg count; got:#{mth.arity}, want:#{want}, #{msg}"
140
+ end
141
+
142
+ def send_status(active_client, code, details)
143
+ details = 'Not sure why' if details.nil?
144
+ active_client.send_status(code, details)
145
+ rescue StandardError => e
146
+ logger.warn("Could not send status #{code}:#{details}")
147
+ logger.warn(e)
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,404 @@
1
+ # Copyright 2015, Google Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions are
6
+ # met:
7
+ #
8
+ # * Redistributions of source code must retain the above copyright
9
+ # notice, this list of conditions and the following disclaimer.
10
+ # * Redistributions in binary form must reproduce the above
11
+ # copyright notice, this list of conditions and the following disclaimer
12
+ # in the documentation and/or other materials provided with the
13
+ # distribution.
14
+ # * Neither the name of Google Inc. nor the names of its
15
+ # contributors may be used to endorse or promote products derived from
16
+ # this software without specific prior written permission.
17
+ #
18
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22
+ # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ require 'grpc/grpc'
31
+ require 'grpc/generic/active_call'
32
+ require 'grpc/generic/service'
33
+ require 'thread'
34
+ require 'xray/thread_dump_signal_handler'
35
+
36
+ # GRPC contains the General RPC module.
37
+ module GRPC
38
+ # RpcServer hosts a number of services and makes them available on the
39
+ # network.
40
+ class RpcServer
41
+ include Core::CompletionType
42
+ include Core::TimeConsts
43
+ extend ::Forwardable
44
+
45
+ def_delegators :@server, :add_http2_port
46
+
47
+ # Default thread pool size is 3
48
+ DEFAULT_POOL_SIZE = 3
49
+
50
+ # Default max_waiting_requests size is 20
51
+ DEFAULT_MAX_WAITING_REQUESTS = 20
52
+
53
+ # Creates a new RpcServer.
54
+ #
55
+ # The RPC server is configured using keyword arguments.
56
+ #
57
+ # There are some specific keyword args used to configure the RpcServer
58
+ # instance, however other arbitrary are allowed and when present are used
59
+ # to configure the listeninng connection set up by the RpcServer.
60
+ #
61
+ # * server_override: which if passed must be a [GRPC::Core::Server]. When
62
+ # present.
63
+ #
64
+ # * poll_period: when present, the server polls for new events with this
65
+ # period
66
+ #
67
+ # * pool_size: the size of the thread pool the server uses to run its
68
+ # threads
69
+ #
70
+ # * completion_queue_override: when supplied, this will be used as the
71
+ # completion_queue that the server uses to receive network events,
72
+ # otherwise its creates a new instance itself
73
+ #
74
+ # * creds: [GRPC::Core::ServerCredentials]
75
+ # the credentials used to secure the server
76
+ #
77
+ # * max_waiting_requests: the maximum number of requests that are not
78
+ # being handled to allow. When this limit is exceeded, the server responds
79
+ # with not available to new requests
80
+ def initialize(pool_size:DEFAULT_POOL_SIZE,
81
+ max_waiting_requests:DEFAULT_MAX_WAITING_REQUESTS,
82
+ poll_period:INFINITE_FUTURE,
83
+ completion_queue_override:nil,
84
+ creds:nil,
85
+ server_override:nil,
86
+ **kw)
87
+ if completion_queue_override.nil?
88
+ cq = Core::CompletionQueue.new
89
+ else
90
+ cq = completion_queue_override
91
+ unless cq.is_a? Core::CompletionQueue
92
+ fail(ArgumentError, 'not a CompletionQueue')
93
+ end
94
+ end
95
+ @cq = cq
96
+
97
+ if server_override.nil?
98
+ if creds.nil?
99
+ srv = Core::Server.new(@cq, kw)
100
+ elsif !creds.is_a? Core::ServerCredentials
101
+ fail(ArgumentError, 'not a ServerCredentials')
102
+ else
103
+ srv = Core::Server.new(@cq, kw, creds)
104
+ end
105
+ else
106
+ srv = server_override
107
+ fail(ArgumentError, 'not a Server') unless srv.is_a? Core::Server
108
+ end
109
+ @server = srv
110
+
111
+ @pool_size = pool_size
112
+ @max_waiting_requests = max_waiting_requests
113
+ @poll_period = poll_period
114
+ @run_mutex = Mutex.new
115
+ @run_cond = ConditionVariable.new
116
+ @pool = Pool.new(@pool_size)
117
+ end
118
+
119
+ # stops a running server
120
+ #
121
+ # the call has no impact if the server is already stopped, otherwise
122
+ # server's current call loop is it's last.
123
+ def stop
124
+ return unless @running
125
+ @stopped = true
126
+ @pool.stop
127
+ end
128
+
129
+ # determines if the server is currently running
130
+ def running?
131
+ @running ||= false
132
+ end
133
+
134
+ # Is called from other threads to wait for #run to start up the server.
135
+ #
136
+ # If run has not been called, this returns immediately.
137
+ #
138
+ # @param timeout [Numeric] number of seconds to wait
139
+ # @result [true, false] true if the server is running, false otherwise
140
+ def wait_till_running(timeout = 0.1)
141
+ end_time, sleep_period = Time.now + timeout, (1.0 * timeout) / 100
142
+ while Time.now < end_time
143
+ @run_mutex.synchronize { @run_cond.wait(@run_mutex) } unless running?
144
+ sleep(sleep_period)
145
+ end
146
+ running?
147
+ end
148
+
149
+ # determines if the server is currently stopped
150
+ def stopped?
151
+ @stopped ||= false
152
+ end
153
+
154
+ # handle registration of classes
155
+ #
156
+ # service is either a class that includes GRPC::GenericService and whose
157
+ # #new function can be called without argument or any instance of such a
158
+ # class.
159
+ #
160
+ # E.g, after
161
+ #
162
+ # class Divider
163
+ # include GRPC::GenericService
164
+ # rpc :div DivArgs, DivReply # single request, single response
165
+ # def initialize(optional_arg='default option') # no args
166
+ # ...
167
+ # end
168
+ #
169
+ # srv = GRPC::RpcServer.new(...)
170
+ #
171
+ # # Either of these works
172
+ #
173
+ # srv.handle(Divider)
174
+ #
175
+ # # or
176
+ #
177
+ # srv.handle(Divider.new('replace optional arg'))
178
+ #
179
+ # It raises RuntimeError:
180
+ # - if service is not valid service class or object
181
+ # - its handler methods are already registered
182
+ # - if the server is already running
183
+ #
184
+ # @param service [Object|Class] a service class or object as described
185
+ # above
186
+ def handle(service)
187
+ fail 'cannot add services if the server is running' if running?
188
+ fail 'cannot add services if the server is stopped' if stopped?
189
+ cls = service.is_a?(Class) ? service : service.class
190
+ assert_valid_service_class(cls)
191
+ add_rpc_descs_for(service)
192
+ end
193
+
194
+ # runs the server
195
+ #
196
+ # - if no rpc_descs are registered, this exits immediately, otherwise it
197
+ # continues running permanently and does not return until program exit.
198
+ #
199
+ # - #running? returns true after this is called, until #stop cause the
200
+ # the server to stop.
201
+ def run
202
+ if rpc_descs.size == 0
203
+ logger.warn('did not run as no services were present')
204
+ return
205
+ end
206
+ @run_mutex.synchronize do
207
+ @running = true
208
+ @run_cond.signal
209
+ end
210
+ @pool.start
211
+ @server.start
212
+ server_tag = Object.new
213
+ until stopped?
214
+ @server.request_call(server_tag)
215
+ ev = @cq.pluck(server_tag, @poll_period)
216
+ next if ev.nil?
217
+ if ev.type != SERVER_RPC_NEW
218
+ logger.warn("bad evt: got:#{ev.type}, want:#{SERVER_RPC_NEW}")
219
+ ev.close
220
+ next
221
+ end
222
+ c = new_active_server_call(ev.call, ev.result)
223
+ unless c.nil?
224
+ mth = ev.result.method.to_sym
225
+ ev.close
226
+ @pool.schedule(c) do |call|
227
+ rpc_descs[mth].run_server_method(call, rpc_handlers[mth])
228
+ end
229
+ end
230
+ end
231
+ @running = false
232
+ end
233
+
234
+ def new_active_server_call(call, new_server_rpc)
235
+ # Accept the call. This is necessary even if a status is to be sent
236
+ # back immediately
237
+ finished_tag = Object.new
238
+ call_queue = Core::CompletionQueue.new
239
+ call.metadata = new_server_rpc.metadata # store the metadata
240
+ call.server_accept(call_queue, finished_tag)
241
+ call.server_end_initial_metadata
242
+
243
+ # Send UNAVAILABLE if there are too many unprocessed jobs
244
+ jobs_count, max = @pool.jobs_waiting, @max_waiting_requests
245
+ logger.info("waiting: #{jobs_count}, max: #{max}")
246
+ if @pool.jobs_waiting > @max_waiting_requests
247
+ logger.warn("NOT AVAILABLE: too many jobs_waiting: #{new_server_rpc}")
248
+ noop = proc { |x| x }
249
+ c = ActiveCall.new(call, call_queue, noop, noop,
250
+ new_server_rpc.deadline,
251
+ finished_tag: finished_tag)
252
+ c.send_status(StatusCodes::UNAVAILABLE, '')
253
+ return nil
254
+ end
255
+
256
+ # Send NOT_FOUND if the method does not exist
257
+ mth = new_server_rpc.method.to_sym
258
+ unless rpc_descs.key?(mth)
259
+ logger.warn("NOT_FOUND: #{new_server_rpc}")
260
+ noop = proc { |x| x }
261
+ c = ActiveCall.new(call, call_queue, noop, noop,
262
+ new_server_rpc.deadline,
263
+ finished_tag: finished_tag)
264
+ c.send_status(StatusCodes::NOT_FOUND, '')
265
+ return nil
266
+ end
267
+
268
+ # Create the ActiveCall
269
+ rpc_desc = rpc_descs[mth]
270
+ logger.info("deadline is #{new_server_rpc.deadline}; (now=#{Time.now})")
271
+ ActiveCall.new(call, call_queue,
272
+ rpc_desc.marshal_proc, rpc_desc.unmarshal_proc(:input),
273
+ new_server_rpc.deadline, finished_tag: finished_tag)
274
+ end
275
+
276
+ # Pool is a simple thread pool for running server requests.
277
+ class Pool
278
+ def initialize(size)
279
+ fail 'pool size must be positive' unless size > 0
280
+ @jobs = Queue.new
281
+ @size = size
282
+ @stopped = false
283
+ @stop_mutex = Mutex.new
284
+ @stop_cond = ConditionVariable.new
285
+ @workers = []
286
+ end
287
+
288
+ # Returns the number of jobs waiting
289
+ def jobs_waiting
290
+ @jobs.size
291
+ end
292
+
293
+ # Runs the given block on the queue with the provided args.
294
+ #
295
+ # @param args the args passed blk when it is called
296
+ # @param blk the block to call
297
+ def schedule(*args, &blk)
298
+ fail 'already stopped' if @stopped
299
+ return if blk.nil?
300
+ logger.info('schedule another job')
301
+ @jobs << [blk, args]
302
+ end
303
+
304
+ # Starts running the jobs in the thread pool.
305
+ def start
306
+ fail 'already stopped' if @stopped
307
+ until @workers.size == @size.to_i
308
+ next_thread = Thread.new do
309
+ catch(:exit) do # allows { throw :exit } to kill a thread
310
+ loop do
311
+ begin
312
+ blk, args = @jobs.pop
313
+ blk.call(*args)
314
+ rescue StandardError => e
315
+ logger.warn('Error in worker thread')
316
+ logger.warn(e)
317
+ end
318
+ end
319
+ end
320
+
321
+ # removes the threads from workers, and signal when all the
322
+ # threads are complete.
323
+ @stop_mutex.synchronize do
324
+ @workers.delete(Thread.current)
325
+ @stop_cond.signal if @workers.size == 0
326
+ end
327
+ end
328
+ @workers << next_thread
329
+ end
330
+ end
331
+
332
+ # Stops the jobs in the pool
333
+ def stop
334
+ logger.info('stopping, will wait for all the workers to exit')
335
+ @workers.size.times { schedule { throw :exit } }
336
+ @stopped = true
337
+
338
+ # TODO: allow configuration of the keepalive period
339
+ keep_alive = 5
340
+ @stop_mutex.synchronize do
341
+ @stop_cond.wait(@stop_mutex, keep_alive) if @workers.size > 0
342
+ end
343
+
344
+ # Forcibly shutdown any threads that are still alive.
345
+ if @workers.size > 0
346
+ logger.warn("forcibly terminating #{@workers.size} worker(s)")
347
+ @workers.each do |t|
348
+ next unless t.alive?
349
+ begin
350
+ t.exit
351
+ rescue StandardError => e
352
+ logger.warn('error while terminating a worker')
353
+ logger.warn(e)
354
+ end
355
+ end
356
+ end
357
+
358
+ logger.info('stopped, all workers are shutdown')
359
+ end
360
+ end
361
+
362
+ protected
363
+
364
+ def rpc_descs
365
+ @rpc_descs ||= {}
366
+ end
367
+
368
+ def rpc_handlers
369
+ @rpc_handlers ||= {}
370
+ end
371
+
372
+ private
373
+
374
+ def assert_valid_service_class(cls)
375
+ unless cls.include?(GenericService)
376
+ fail "#{cls} should 'include GenericService'"
377
+ end
378
+ if cls.rpc_descs.size == 0
379
+ fail "#{cls} should specify some rpc descriptions"
380
+ end
381
+ cls.assert_rpc_descs_have_methods
382
+ end
383
+
384
+ def add_rpc_descs_for(service)
385
+ cls = service.is_a?(Class) ? service : service.class
386
+ specs = rpc_descs
387
+ handlers = rpc_handlers
388
+ cls.rpc_descs.each_pair do |name, spec|
389
+ route = "/#{cls.service_name}/#{name}".to_sym
390
+ if specs.key? route
391
+ fail "Cannot add rpc #{route} from #{spec}, already registered"
392
+ else
393
+ specs[route] = spec
394
+ if service.is_a?(Class)
395
+ handlers[route] = cls.new.method(name.to_s.underscore.to_sym)
396
+ else
397
+ handlers[route] = service.method(name.to_s.underscore.to_sym)
398
+ end
399
+ logger.info("handling #{route} with #{handlers[route]}")
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end