isono 0.1.0 → 0.2.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.
@@ -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