isono 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,6 +7,7 @@ module Isono
7
7
  attr_reader :node
8
8
 
9
9
  def initialize(node)
10
+ raise ArgumentError unless node.is_a?(Node)
10
11
  @node = node
11
12
 
12
13
  raise "Module initializer_hook is not run yet" if self.value_object.nil?
@@ -18,10 +19,6 @@ module Isono
18
19
  node.manifest
19
20
  end
20
21
 
21
- def amq
22
- node.amq
23
- end
24
-
25
22
  def value_object
26
23
  node.value_objects[self.class]
27
24
  end
@@ -34,19 +31,37 @@ module Isono
34
31
 
35
32
  module ClassMethods
36
33
  def initialize_hook(&blk)
37
- @initialize_hook = blk if blk
38
- @initialize_hook
34
+ @node_hooks[:initialize] = blk
39
35
  end
40
36
 
41
37
  def terminate_hook(&blk)
42
- @terminate_hook = blk if blk
43
- @terminate_hook
38
+ @node_hooks[:terminate] = blk
39
+ end
40
+
41
+ def before_connect_hook(&blk)
42
+ @node_hooks[:before_connect] = blk
43
+ end
44
+
45
+ def after_connect_hook(&blk)
46
+ @node_hooks[:after_connect] = blk
47
+ end
48
+
49
+ def before_close_hook(&blk)
50
+ @node_hooks[:before_close] = blk
51
+ end
52
+
53
+ def after_close_hook(&blk)
54
+ @node_hooks[:after_close] = blk
44
55
  end
45
56
 
46
57
  def config_section(name=nil, &blk)
47
58
  @config_section_name = name unless name.nil?
48
59
  @config_section_builder = blk
49
- end
60
+ end
61
+
62
+ def node_hooks
63
+ @node_hooks
64
+ end
50
65
  end
51
66
 
52
67
  protected
@@ -57,6 +72,7 @@ module Isono
57
72
  # set the default config section name from its class name.
58
73
  # can be overwritten later.
59
74
  @config_section_name = Util.snake_case(self.to_s.split('::').last)
75
+ @node_hooks = {}
60
76
  }
61
77
  end
62
78
 
@@ -20,7 +20,18 @@ module Isono
20
20
  AMQP_EXCHANGE='isono.event'
21
21
 
22
22
  initialize_hook do
23
- amq.topic(AMQP_EXCHANGE, {:auto_delete=>false})
23
+ @amq = node.create_channel
24
+ @amq.instance_eval {
25
+ def event_exchange
26
+ self.topic(AMQP_EXCHANGE, {:auto_delete=>false})
27
+ end
28
+ }
29
+
30
+ # create the exchange
31
+ @amq.event_exchange
32
+ end
33
+
34
+ terminate_hook do
24
35
  end
25
36
 
26
37
  # @example
@@ -37,14 +48,14 @@ module Isono
37
48
  }
38
49
 
39
50
  EventMachine.schedule {
40
- amq.topic(AMQP_EXCHANGE).publish(Serializer.instance.marshal(body),
41
- {:key=>"#{evname}.#{opts[:sender]}"}
42
- )
51
+ @amq.event_exchange.publish(Serializer.instance.marshal(body),
52
+ {:key=>"#{evname}.#{opts[:sender]}"}
53
+ )
43
54
  }
44
55
  end
45
56
 
46
- def subscribe(evname, sender, receiver_id=node.node_id, &blk)
47
- amq.queue("#{evname}-#{receiver_id}", {:exclusive=>true}).bind(
57
+ def subscribe(evname, sender='#', receiver_id=node.node_id, &blk)
58
+ @amq.queue("#{evname}-#{receiver_id}", {:exclusive=>true}).bind(
48
59
  AMQP_EXCHANGE, :key=>"#{evname}.#{sender}"
49
60
  ).subscribe { |data|
50
61
  data = Serializer.instance.unmarshal(data)
@@ -62,7 +73,7 @@ module Isono
62
73
 
63
74
  def unsubscribe(evname, receiver_id=node.node_id)
64
75
  EventMachine.schedule {
65
- q = amq.queue("#{evname}-#{receiver_id}")
76
+ q = @amq.queue("#{evname}-#{receiver_id}")
66
77
  q.unsubscribe
67
78
  }
68
79
  end
@@ -4,7 +4,14 @@ module Isono
4
4
  module NodeModules
5
5
  class JobChannel < Base
6
6
 
7
- # Send a new job request to the endpoint and get back job ID.
7
+ config_section do |c|
8
+ desc "default timeout duration in second"
9
+ c.timeout_sec = (60*60*24).to_f
10
+ desc "default job concurrency"
11
+ c.concurrency = 1
12
+ end
13
+
14
+ # Send a new job request to the endpoint.
8
15
  #
9
16
  # @param [String] endpoint endpoint name created by JobChannel#register_endpoint()
10
17
  # @param [String] command command name in the endpoint.
@@ -13,24 +20,14 @@ module Isono
13
20
  # method will stop the current thread if it does not exist.
14
21
  # @return [Rack::Request,String] Request object if the block is given.
15
22
  #
16
- # @example call job endpoint 'endpoint1' and receive
17
- # submit('endpoint1', 'command1', 1, 2, 3) #=> Job ID.
23
+ # @example call job endpoint 'endpoint1'.
24
+ # puts submit('endpoint1', 'command1', 1, 2, 3) # => show Job ID
18
25
  def submit(endpoint, command, *args, &blk)
19
- cur_job_ctx = Thread.current[JobWorker::JOB_CTX_KEY]
20
- req = rpc.request("job.#{endpoint}", command, *args) { |req|
21
- req.request[:job_request_type]=:submit
22
-
23
- # A job is working on this current thread if cur_job_ctx is
24
- # not nil. Let the new job know the current job ID
25
- # as its parent job ID.
26
- if cur_job_ctx
27
- req.request[:parent_job_id] = cur_job_ctx.job_id
28
- end
29
-
30
- blk ? blk.call(req) : req.synchronize
26
+ req = run(endpoint, command, *args) { |req|
27
+ blk.call(req) if blk
31
28
  }
32
29
 
33
- blk ? req : req.wait
30
+ req.request[:job_id]
34
31
  end
35
32
 
36
33
  # Send a new job request and wait until the job finished.
@@ -54,8 +51,9 @@ module Isono
54
51
  def run(endpoint, command, *args, &blk)
55
52
  cur_job_ctx = Thread.current[JobWorker::JOB_CTX_KEY]
56
53
  req = rpc.request("job.#{endpoint}", command, *args) { |req|
57
- req.request[:job_request_type]=:run
58
-
54
+ req.timeout = config_section.timeout_sec
55
+ req.request[:job_id] = Util.gen_id
56
+
59
57
  # A job is working on this current thread if cur_job_ctx is
60
58
  # not nil. Let the new job know the current job ID
61
59
  # as its parent job ID.
@@ -69,11 +67,12 @@ module Isono
69
67
  blk ? req : req.wait
70
68
  end
71
69
 
72
- def cancel()
70
+ def cancel(job_id)
73
71
  end
74
72
 
75
- def register_endpoint(endpoint, app)
76
- rpc.register_endpoint("job.#{endpoint}", Rack::Job.new(app, JobWorker.new(node)))
73
+ def register_endpoint(endpoint, app, opts={})
74
+ opts = {:concurrency=>config_section.concurrency}.merge(opts)
75
+ rpc.register_endpoint("job.#{endpoint}", Rack::Job.new(app, JobWorker.new(node)), {:prefetch=>opts[:concurrency]})
77
76
  end
78
77
 
79
78
  private
@@ -8,9 +8,13 @@ module Isono
8
8
  include Logger
9
9
 
10
10
  JOB_CTX_KEY=:job_worker_ctx
11
+
12
+ config_section do |c|
13
+ c.concurrency = 1
14
+ end
11
15
 
12
16
  initialize_hook do
13
- @thread_pool = ThreadPool.new(10)
17
+ @thread_pool = ThreadPool.new(config_section.concurrency.to_i)
14
18
  @active_jobs = {}
15
19
 
16
20
  RpcChannel.new(node).register_endpoint("job-stats.#{node.node_id}", proc { |req, res|
@@ -29,30 +33,17 @@ module Isono
29
33
 
30
34
  # Start a new long term job.
31
35
  #
32
- # @param [String] parent_id Parent Job ID for new job
33
- # @param [Proc,nil] run_cb
34
- # @param [Proc,nil] fail_cb
35
- # @yield The block used as run_cb
36
+ # @yield The block to setup JobContext object.
36
37
  #
37
- # @example Simply set run block as yield block and parent job ID.
38
- # run('xxxxxx') {
39
- # # do something
38
+ # @example Initialize JobContext within the block.
39
+ # start { |ctx|
40
+ # # setup JobContext
41
+ # ctx.job_id = 'xxxx'
42
+ # ctx.parent_job_id = 'yyyy'
40
43
  # }
41
- # @example Set proc{} to both run and fail block.
42
- # run(proc{
43
- # # do something
44
- # }, proc{
45
- # # do rollback on fail
46
- # })
47
- def run(parent_id=nil, run_cb=nil, fail_cb=nil, &blk)
48
- if run_cb.is_a?(Proc)
49
- job = JobContext.new(run_cb, parent_id)
50
- job.fail_cb = fail_cb if fail_cb.is_a?(Proc)
51
- elsif blk
52
- job = JobContext.new(blk, parent_id)
53
- else
54
- raise ArgumentError, "callbacks were not set propery"
55
- end
44
+ def start(&blk)
45
+ job = JobContext.new()
46
+ blk.call(job)
56
47
  @active_jobs[job.job_id] = job
57
48
  rpc = RpcChannel.new(node)
58
49
 
@@ -87,19 +78,47 @@ module Isono
87
78
  job
88
79
  end
89
80
 
81
+ # Run the block/proc. This is simple utility method for start().
82
+ #
83
+ # @param [Proc,nil] run_cb
84
+ # @param [Proc,nil] fail_cb
85
+ #
86
+ # @example Run
87
+ # run {
88
+ # puts "message"
89
+ # }
90
+ # @example Use proc{} for setting both run and fail block.
91
+ # run(proc{
92
+ # # do something
93
+ # }, proc{
94
+ # # do rollback on fail
95
+ # })
96
+ def run(run_cb=nil, fail_cb=nil, &blk)
97
+ if run_cb.is_a?(Proc)
98
+ start do |job|
99
+ job.run_cb = run_cb
100
+ job.fail_cb = fail_cb if fail_cb.is_a?(Proc)
101
+ end
102
+ elsif blk
103
+ start do |job|
104
+ job.run_cb = blk
105
+ end
106
+ end
107
+ end
108
+
90
109
  class JobContext < OpenStruct
91
110
  include Logger
92
- attr_reader :stm, :run_cb
93
- attr_accessor :fail_cb
111
+ attr_reader :stm
112
+ attr_accessor :run_cb, :fail_cb
94
113
 
95
- def initialize(run_cb, parent_id=nil)
114
+ def initialize()
96
115
  super({:job_id=>Util.gen_id,
97
- :parent_job_id=> parent_id,
116
+ :parent_job_id=>nil,
98
117
  :started_at=>nil,
99
118
  :finished_at=>nil,
100
119
  })
101
120
 
102
- @run_cb=run_cb
121
+ @run_cb=proc{}
103
122
  @fail_cb=nil
104
123
 
105
124
  @stm = Statemachine.build {
@@ -9,8 +9,6 @@ module Isono
9
9
  class RpcChannel < Base
10
10
  include Logger
11
11
 
12
- AMQP_EXCHANGE='isono.rpc'
13
-
14
12
  class RpcError < RuntimeError; end
15
13
  class UnknownEndpointError < RpcError; end
16
14
  class DuplicateEndpointError < RpcError; end
@@ -23,14 +21,12 @@ module Isono
23
21
  initialize_hook do
24
22
  @active_requests = {}
25
23
  @endpoints = {}
26
- amq.direct(AMQP_EXCHANGE, {:auto_delete=>true})
27
- amq.queue("command-recv.#{manifest.node_id}", {:exclusive=>true}).subscribe { |header, data|
28
- event = EventChannel.new(self.node)
24
+ @amq = node.create_channel
25
+ @amq.queue("command-recv.#{manifest.node_id}", {:exclusive=>true}).subscribe { |header, data|
29
26
  req = @active_requests[header.message_id]
30
27
  if req
31
28
  data = Serializer.instance.unmarshal(data)
32
29
  req.process_event(:on_received, data)
33
- event.publish('rpc/response_received', :args=>[header.message_id])
34
30
  begin
35
31
  case data[:type]
36
32
  when :inprogress
@@ -69,9 +65,11 @@ module Isono
69
65
  @endpoints.keys.each { |ns|
70
66
  myinstance.unregister_endpoint(ns)
71
67
  }
72
- amq.queue("command-recv.#{manifest.node_id}", {:exclusive=>true}).delete
68
+ @amq.queue("command-recv.#{manifest.node_id}", {:exclusive=>true}).delete
73
69
  end
74
70
 
71
+ attr_reader :amq
72
+
75
73
  # Make a RPC request to an endpoint.
76
74
  #
77
75
  # @param [String] endpoint
@@ -112,24 +110,11 @@ module Isono
112
110
  # async
113
111
  r = blk.call(req)
114
112
  req = r if r.is_a?(RequestContext)
115
- if req.oneshot
116
- send_request(req)
117
- else
118
- check_endpoint(endpoint) { |result|
119
- if result
120
- send_request(req)
121
- else
122
- e = UnknownEndpointError.new(endpoint)
123
- req.error_cb.call(e) if req.error_cb
124
- end
125
- }
126
- end
127
-
113
+ send_request(req)
128
114
  req
129
115
  else
130
116
  # sync
131
117
  req = req.synchronize
132
- check_endpoint(endpoint) || raise(UnknownEndpointError, endpoint)
133
118
  send_request(req)
134
119
  req.wait
135
120
  end
@@ -141,21 +126,21 @@ module Isono
141
126
  # @param [String] endpoint
142
127
  # @param [has call() method] app
143
128
  # @param [Hash] opts
129
+ # :exclusive
130
+ # :prefetch
144
131
  def register_endpoint(endpoint, app, opts={})
145
132
  raise TypeError unless app.respond_to?(:call)
146
- opts = {:exclusive=>true}.merge(opts)
147
- @endpoints[endpoint]={:app=>app, :opts=>opts}
133
+ opts = {:exclusive=>true, :prefetch=>0}.merge(opts)
148
134
 
149
135
  # create receive queue for new RPC endpoint.
150
136
  endpoint_proc = proc { |header, data|
151
137
 
152
138
  data = Serializer.instance.unmarshal(data)
153
- event.publish('rpc/request_received', :args=>[header.message_id])
154
139
 
155
140
  resctx = if data[:oneshot]
156
- OneshotResponseContext.new(self.node, header)
141
+ OneshotResponseContext.new(@endpoints[endpoint][:ch].response_exchange, header)
157
142
  else
158
- ResponseContext.new(self.node, header)
143
+ ResponseContext.new(@endpoints[endpoint][:ch].response_exchange, header)
159
144
  end
160
145
  begin
161
146
  req = Rack::Request.new({:sender=>header.reply_to['command-recv.'.size..-1],
@@ -163,81 +148,51 @@ module Isono
163
148
  }.merge(data))
164
149
  res = Rack::Response.new(resctx)
165
150
  ret = app.call(req, res)
166
- rescue Exception => e
151
+ rescue ::Exception => e
167
152
  logger.error(e)
168
153
  resctx.response(e) unless resctx.responded?
169
154
  end
170
155
  }
171
156
 
172
- setup_proc = proc {
173
- amq.queue(endpoint_queue_name(endpoint), {:exclusive=>false, :auto_delete=>true}).bind(
174
- AMQP_EXCHANGE, {:key=>endpoint_queue_name(endpoint)}
175
- ).subscribe(:ack=>true, &endpoint_proc)
176
- event.publish('rpc/register', :args=>[endpoint])
177
- }
178
-
179
- dm = Util::DeferedMsg.new(30)
180
157
 
181
158
  EventMachine.schedule {
182
- amq.queue(endpoint_queue_name(endpoint), {:exclusive=>false, :auto_delete=>true}).status { |messages, consumers|
183
- if opts[:exclusive]
184
- if consumers.to_i == 0
185
- setup_proc.call
186
- dm.success
187
- else
188
- dm.error(DuplicateEndpointError.new("Endpoint is already locked: #{endpoint}"))
189
- end
190
- else
191
- setup_proc.call
192
- dm.success
159
+ ch = if opts[:prefetch].to_i > 0
160
+ # create per endpoint channel
161
+ node.create_channel
162
+ else
163
+ # use default channel
164
+ @amq
165
+ end
166
+ ch.instance_eval %Q{
167
+ def endpoint_queue
168
+ self.queue("isono.rpc.endpoint.#{endpoint}", {:exclusive=>false, :auto_delete=>true})
193
169
  end
194
170
 
195
- # expect to raise DuplicateEndpointError if endpoint exists.
196
- # ignore the case of success.
197
- dm.wait
171
+ def response_exchange
172
+ self.direct('')
173
+ end
198
174
  }
199
- }
175
+ ch.prefetch(opts[:prefetch].to_i) if opts[:prefetch].to_i > 0
176
+ # stores hash here that is for thread safety.
177
+ @endpoints[endpoint]={:app=>app, :opts=>opts, :ch=>ch}
200
178
 
201
- dm.wait unless EventMachine.reactor_thread?
179
+ ch.endpoint_queue.subscribe(:ack=>true, &endpoint_proc)
180
+ event.publish('rpc/register', :args=>[endpoint])
181
+ }
202
182
  end
203
183
 
204
184
  # Unregister endpoint.
205
185
  #
206
186
  # @param [String] endpoint endpoint name to be removed
207
187
  def unregister_endpoint(endpoint)
208
- if @endpoints.delete(endpoint)
209
- dm = Util::DeferedMsg.new(30)
188
+ if @endpoints.has_key?(endpoint)
210
189
  EventMachine.schedule {
211
- amq.queue(endpoint_queue_name(endpoint)).delete
190
+ data = @endpoints.delete(endpoint)
191
+ # endpoint_queue is :auto_delete=>true so that it will be deleted
192
+ # in case of zero consumers.
193
+ data[:ch].endpoint_queue.unsubscribe
212
194
  event.publish('rpc/unregister', :args=>[endpoint])
213
- dm.success
214
195
  }
215
- dm.wait unless EventMachine.reactor_thread?
216
- end
217
- end
218
-
219
- # Check if the endpoint exists.
220
- # @param [String] endpoint endpoint name to be checked
221
- def check_endpoint(endpoint, &blk)
222
- if blk
223
- else
224
- dm = Util::DeferedMsg.new(30)
225
- end
226
-
227
- EventMachine.schedule {
228
- amq.queue(endpoint_queue_name(endpoint), {:exclusive=>false, :auto_delete=>true}).status { |messages, consumers|
229
- res = consumers.to_i > 0
230
- if blk
231
- blk.call(res)
232
- else
233
- dm.success(res)
234
- end
235
- }
236
- }
237
-
238
- if blk
239
- else
240
- dm.wait unless EventMachine.reactor_thread?
241
196
  end
242
197
  end
243
198
 
@@ -280,24 +235,22 @@ module Isono
280
235
  if !req.oneshot
281
236
  @active_requests[req.ticket] = req
282
237
  end
283
-
284
- amq.direct(AMQP_EXCHANGE).publish(
285
- Serializer.instance.marshal(req.request_hash),
286
- {:message_id => req.ticket,
287
- :key => endpoint_queue_name(req.endpoint),
288
- :reply_to=>"command-recv.#{manifest.node_id}"}
289
- )
238
+
239
+ amq.direct('').publish(Serializer.instance.marshal(req.request_hash),
240
+ {:message_id => req.ticket,
241
+ :key => endpoint_queue_name(req.endpoint),
242
+ :reply_to=>"command-recv.#{manifest.node_id}"}
243
+ )
290
244
  req.process_event(:on_sent)
291
- event.publish('rpc/request_sent', :args=>[req.hash])
292
245
  }
293
246
  end
294
247
 
295
248
  class ResponseContext
296
- attr_reader :node, :header
249
+ attr_reader :header
297
250
 
298
- def initialize(node, header)
251
+ def initialize(exchange, header)
299
252
  @responded = false
300
- @node = node
253
+ @exchange = exchange
301
254
  @header = header
302
255
  end
303
256
 
@@ -321,15 +274,13 @@ module Isono
321
274
  else
322
275
  publish(:success, ret)
323
276
  end
324
- EventChannel.new(@node).publish('rpc/response_sent', :args=>[@header.message_id])
325
277
  }
326
278
  @responded = true
327
279
  end
328
280
 
329
-
330
281
  private
331
282
  def publish(type, body)
332
- @node.amq.direct('').publish(Serializer.instance.marshal({:type=>type, :msg=>body}),
283
+ @exchange.publish(Serializer.instance.marshal({:type=>type, :msg=>body}),
333
284
  {:key=>@header.reply_to,
334
285
  :message_id=>@header.message_id}
335
286
  )
@@ -347,7 +298,6 @@ module Isono
347
298
 
348
299
  EM.schedule {
349
300
  @header.ack
350
- EventChannel.new(@node).publish('rpc/response_sent', :args=>[@header.message_id])
351
301
  }
352
302
  @responded = true
353
303
  end