isono 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.
Files changed (43) hide show
  1. data/LICENSE +202 -0
  2. data/NOTICE +2 -0
  3. data/bin/cli +122 -0
  4. data/isono.gemspec +47 -0
  5. data/lib/ext/shellwords.rb +172 -0
  6. data/lib/isono.rb +61 -0
  7. data/lib/isono/amqp_client.rb +169 -0
  8. data/lib/isono/daemonize.rb +96 -0
  9. data/lib/isono/event_delegate_context.rb +56 -0
  10. data/lib/isono/event_observable.rb +86 -0
  11. data/lib/isono/logger.rb +48 -0
  12. data/lib/isono/manifest.rb +161 -0
  13. data/lib/isono/messaging_client.rb +116 -0
  14. data/lib/isono/models/event_log.rb +28 -0
  15. data/lib/isono/models/job_state.rb +35 -0
  16. data/lib/isono/models/node_state.rb +70 -0
  17. data/lib/isono/models/resource_instance.rb +35 -0
  18. data/lib/isono/node.rb +158 -0
  19. data/lib/isono/node_modules/base.rb +65 -0
  20. data/lib/isono/node_modules/data_store.rb +57 -0
  21. data/lib/isono/node_modules/event_channel.rb +72 -0
  22. data/lib/isono/node_modules/event_logger.rb +39 -0
  23. data/lib/isono/node_modules/job_channel.rb +86 -0
  24. data/lib/isono/node_modules/job_collector.rb +47 -0
  25. data/lib/isono/node_modules/job_worker.rb +152 -0
  26. data/lib/isono/node_modules/node_collector.rb +87 -0
  27. data/lib/isono/node_modules/node_heartbeat.rb +26 -0
  28. data/lib/isono/node_modules/rpc_channel.rb +482 -0
  29. data/lib/isono/rack.rb +67 -0
  30. data/lib/isono/rack/builder.rb +40 -0
  31. data/lib/isono/rack/data_store.rb +20 -0
  32. data/lib/isono/rack/job.rb +74 -0
  33. data/lib/isono/rack/map.rb +56 -0
  34. data/lib/isono/rack/object_method.rb +20 -0
  35. data/lib/isono/rack/proc.rb +50 -0
  36. data/lib/isono/rack/thread_pass.rb +22 -0
  37. data/lib/isono/resource_manifest.rb +273 -0
  38. data/lib/isono/runner/agent.rb +89 -0
  39. data/lib/isono/runner/rpc_server.rb +198 -0
  40. data/lib/isono/serializer.rb +43 -0
  41. data/lib/isono/thread_pool.rb +169 -0
  42. data/lib/isono/util.rb +212 -0
  43. metadata +185 -0
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Isono
4
+ module NodeModules
5
+ class JobCollector < Base
6
+
7
+ initialize_hook do
8
+ rpc = RpcChannel.new(node)
9
+
10
+ app = Rack::DataStore.new(Dispatch.new)
11
+
12
+ rpc.register_endpoint('job-collector', app)
13
+ end
14
+
15
+ terminate_hook do
16
+ end
17
+
18
+ class Dispatch
19
+ # Register new job
20
+ def regist
21
+ params = @req.args[0]
22
+ params[:node_id]=@req.sender
23
+ job =Models::JobState.new
24
+ job.set_fields(params, [:job_id, :parent_job_id, :node_id, :state]).save
25
+ end
26
+
27
+ def update
28
+ params = @req.args[0]
29
+ job = Models::JobState.find(:job_id=>params[:job_id])
30
+ raise "Unknown or JOB ID: #{params[:job_id]}" if job.nil?
31
+ job.set_fields(params, [:state, :started_at, :finished_at]).save
32
+ end
33
+
34
+ def call(req, res)
35
+ @req, @res = req, res
36
+ raise Rack::UnknownMethodError if @req.command == 'call'
37
+ m = self.method(@req.command)
38
+ raise Rack::UnknownMethodError if m.nil?
39
+
40
+ ret = m.call
41
+ @res.response(nil) if @res.responded?
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,152 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'statemachine'
4
+
5
+ module Isono
6
+ module NodeModules
7
+ class JobWorker < Base
8
+ include Logger
9
+
10
+ JOB_CTX_KEY=:job_worker_ctx
11
+
12
+ initialize_hook do
13
+ @thread_pool = ThreadPool.new(10)
14
+ @active_jobs = {}
15
+
16
+ RpcChannel.new(node).register_endpoint("job-stats.#{node.node_id}", proc { |req, res|
17
+ case res.command
18
+ when 'get'
19
+ res.response({ :active_jobs => @active_jobs.map {|j| j.to_hash } })
20
+ else
21
+ raise Rack::UnknownMethodError
22
+ end
23
+ })
24
+ end
25
+
26
+ terminate_hook do
27
+ @thread_pool.shutdown
28
+ end
29
+
30
+ # Start a new long term job.
31
+ #
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
+ #
37
+ # @example Simply set run block as yield block and parent job ID.
38
+ # run('xxxxxx') {
39
+ # # do something
40
+ # }
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
56
+ @active_jobs[job.job_id] = job
57
+ rpc = RpcChannel.new(node)
58
+
59
+ rpc.request('job-collector', 'regist', job.to_hash) { |req|
60
+ req.oneshot = true
61
+ }
62
+
63
+ @thread_pool.pass {
64
+ begin
65
+ Thread.current[JOB_CTX_KEY]=job
66
+ job.stm.on_start
67
+ rpc.request('job-collector', 'update', job.to_hash) { |req|
68
+ req.oneshot = true
69
+ }
70
+ job.run_cb.call
71
+ job.stm.on_done
72
+ rescue Exception => e
73
+ job.stm.on_fail(e)
74
+ if job.fail_cb
75
+ job.fail_cb.arity == 1 ? job.fail_cb.call(e) : job.fail_cb.call
76
+ end
77
+ ensure
78
+ Thread.current[JOB_CTX_KEY]=nil
79
+ EventMachine.schedule {
80
+ rpc.request('job-collector', 'update', job.to_hash) { |req|
81
+ req.oneshot = true
82
+ }
83
+ @active_jobs.delete(job.job_id)
84
+ }
85
+ end
86
+ }
87
+ job
88
+ end
89
+
90
+ class JobContext < OpenStruct
91
+ include Logger
92
+ attr_reader :stm, :run_cb
93
+ attr_accessor :fail_cb
94
+
95
+ def initialize(run_cb, parent_id=nil)
96
+ super({:job_id=>Util.gen_id,
97
+ :parent_job_id=> parent_id,
98
+ :started_at=>nil,
99
+ :finished_at=>nil,
100
+ })
101
+
102
+ @run_cb=run_cb
103
+ @fail_cb=nil
104
+
105
+ @stm = Statemachine.build {
106
+ startstate :init
107
+ trans :init, :on_start, :running, :on_start
108
+ trans :running, :on_done, :done, :on_done
109
+ trans :running, :on_fail, :failed, :on_fail
110
+ trans :init, :on_fail, :failed, :on_fail
111
+ }
112
+ @stm.context = self
113
+ end
114
+
115
+ def state
116
+ stm.state
117
+ end
118
+
119
+ def to_hash
120
+ @table.dup.merge({:state=>@stm.state})
121
+ end
122
+
123
+ def elapsed_time
124
+ if finished_at && started_at
125
+ finished_at - started_at
126
+ else
127
+ 0
128
+ end
129
+ end
130
+
131
+ private
132
+ def on_start
133
+ self.started_at = Time.now
134
+ logger.info("Job start #{job_id}")
135
+ end
136
+
137
+ def on_done
138
+ self.finished_at = Time.now
139
+ logger.info("Job complete #{job_id}: #{elapsed_time} sec")
140
+ end
141
+
142
+ def on_fail(e)
143
+ self.finished_at = Time.now
144
+ logger.error("Job failed #{job_id}: #{e}")
145
+ logger.error(e)
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,87 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Isono
4
+ module NodeModules
5
+ class NodeCollector < Base
6
+ include Logger
7
+
8
+ config_section do
9
+ desc "time in second to recognize if the agent is timed out"
10
+ timeout_sec (60*20).to_f
11
+ desc "the agent to be killed from the datasource after the time of second"
12
+ kill_sec (60*20*2).to_f
13
+ desc ""
14
+ gc_period 20.0
15
+ end
16
+
17
+ initialize_hook do
18
+ # GC event trigger for agent timer & status
19
+ @gc_timer = EM::PeriodicTimer.new(config_section.gc_period) {
20
+ event = EventChannel.new(self.node)
21
+ DataStore.pass {
22
+ # Sqlite3 is unlikely to modify table while iterating
23
+ # the result set. the following is the case of the
24
+ # iteration for the opened result set.
25
+ # Models::AgentPool.dataset.each { |row|
26
+ #
27
+ # while Model.dataset.all, it returns a Ruby array
28
+ # containing rows so that i had no table lock exception.
29
+ # see:
30
+ # http://www.mail-archive.com/sqlite-users@sqlite.org/msg03328.html
31
+ # TODO: paging support for the large result set.
32
+ Models::NodeState.dataset.all.each { |row|
33
+
34
+ sm = row.state_machine
35
+ next if sm.state == :offline
36
+
37
+ diff_time = Time.now - row[:last_ping_at]
38
+ if sm.state != :timeout && diff_time > config_section.timeout_sec
39
+ sm.on_timeout
40
+ row.save_changes
41
+ event.publish('node_collector/timedout', :args=>[row.values])
42
+ end
43
+
44
+ if diff_time > config_section.kill_sec
45
+ sm.on_unmonitor
46
+
47
+ event.publish('node_collector/killed', :args=>[row.values])
48
+ row.delete
49
+ end
50
+ }
51
+ }
52
+ }
53
+
54
+ rpc = RpcChannel.new(node)
55
+ app = Rack::ObjectMethod.new(myinstance)
56
+ rpc.register_endpoint('node-collector', Rack.build do
57
+ use Rack::DataStore
58
+ run app
59
+ end)
60
+ end
61
+
62
+ terminate_hook do
63
+ @gc_timer.cancel
64
+ end
65
+
66
+ def list
67
+ Models::NodeState.dataset.all.map{|r| r.values }
68
+ end
69
+
70
+ def notify(node_id, boot_token)
71
+ event = EventChannel.new(node)
72
+
73
+ a = Models::NodeState.find(:node_id=>node_id) || Models::NodeState.new(:node_id=>node_id)
74
+ a.state_machine.on_ping
75
+ if a.new?
76
+ a.boot_token = boot_token
77
+ a.save
78
+ event.publish('node_collector/monitored', :args=>[a.values])
79
+ else
80
+ a.save_changes
81
+ #event.publish('node_collector/pong')
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module Isono
4
+ module NodeModules
5
+ class NodeHeartbeat < Base
6
+
7
+ config_section do |c|
8
+ desc "second(s) to wait until send the next heartbeat signal"
9
+ heartbeat_offset_time 10
10
+ end
11
+
12
+ initialize_hook do
13
+ @timer = EventMachine::PeriodicTimer.new(config_section.heartbeat_offset_time.to_f) {
14
+ rpc = RpcChannel.new(node)
15
+ rpc.request('node-collector', 'notify', manifest.node_id, node.boot_token) do |req|
16
+ req.oneshot = true
17
+ end
18
+ }
19
+ end
20
+
21
+ terminate_hook do
22
+ @timer.cancel
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,482 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'thread'
4
+ require 'statemachine'
5
+ require 'ostruct'
6
+
7
+ module Isono
8
+ module NodeModules
9
+ class RpcChannel < Base
10
+ include Logger
11
+
12
+ AMQP_EXCHANGE='isono.rpc'
13
+
14
+ class RpcError < RuntimeError; end
15
+ class UnknownEndpointError < RpcError; end
16
+ class DuplicateEndpointError < RpcError; end
17
+
18
+ config_section do
19
+ desc "default timeout duration in second until receive response."
20
+ timeout_sec (60*3).to_f
21
+ end
22
+
23
+ initialize_hook do
24
+ @active_requests = {}
25
+ @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)
29
+ req = @active_requests[header.message_id]
30
+ if req
31
+ data = Serializer.instance.unmarshal(data)
32
+ req.process_event(:on_received, data)
33
+ event.publish('rpc/response_received', :args=>[header.message_id])
34
+ begin
35
+ case data[:type]
36
+ when :inprogress
37
+ req.progress_cb.call(data[:msg]) if req.progress_cb
38
+ when :error
39
+ req.process_event(:on_error, data)
40
+ req.error_cb.call(data[:msg]) if req.error_cb
41
+ else
42
+ req.process_event(:on_success, data)
43
+ req.success_cb.call(data[:msg]) if req.success_cb
44
+ end
45
+ rescue => e
46
+ logger.error(e)
47
+ ensure
48
+ if req.state == :done
49
+ @active_requests.delete req.ticket
50
+ end
51
+ end
52
+ end
53
+ }
54
+
55
+ # RPC endpoint for statistics info of this node.
56
+ myinstance.register_endpoint("rpc-stats.#{manifest.node_id}", proc { |req, res|
57
+ case req.command
58
+ when 'get'
59
+ res.response({:active_requests => @active_requests.map {|a| a.hash },
60
+ :endpoints => @endpoints.keys
61
+ })
62
+ else
63
+ raise Rack::UnknownMethodError
64
+ end
65
+ })
66
+ end
67
+
68
+ terminate_hook do
69
+ @endpoints.keys.each { |ns|
70
+ myinstance.unregister_endpoint(ns)
71
+ }
72
+ amq.queue("command-recv.#{manifest.node_id}", {:exclusive=>true}).delete
73
+ end
74
+
75
+ # Make a RPC request to an endpoint.
76
+ #
77
+ # @param [String] endpoint
78
+ # @param [String] command
79
+ # @param [Array] args
80
+ # @param [Proc] &blk Block to setup the request context.
81
+ # @return [RequestContext,any]
82
+ #
83
+ # @example create a sync RPC request.
84
+ # rpc.request('endpoint1', 'func1', xxxx)
85
+ # @example call RPC in async mode.
86
+ # rpc.request('endpoint1', 'func1', xxxx) { |req|
87
+ # req.on_success { |r|
88
+ # puts r
89
+ # }
90
+ # req.on_error { |r|
91
+ # puts r
92
+ # }
93
+ # }
94
+ #
95
+ # @example setup request context and do wait().
96
+ # Note that callbacks are
97
+ # rpc.request('endpoint1', 'func1', xxxx) { |req|
98
+ # # send new attribute
99
+ # req.request[:xxxx] = "sdfsdf"
100
+ # # returns synchronized RequestContext to block caller.
101
+ # req.synchronize
102
+ # }.wait # request() get back the altered RequestCotenxt that has wait().
103
+ #
104
+ # @example Create async oneshot call. (do not expect response)
105
+ # rpc.request('endpoint1', 'func1') { |req|
106
+ # req.oneshot = true
107
+ # }
108
+ def request(endpoint, command, *args, &blk)
109
+ req = RequestContext.new(endpoint, command, args)
110
+ # the block is to setup the request context prior to sending.
111
+ if blk
112
+ # async
113
+ r = blk.call(req)
114
+ 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
+
128
+ req
129
+ else
130
+ # sync
131
+ req = req.synchronize
132
+ check_endpoint(endpoint) || raise(UnknownEndpointError, endpoint)
133
+ send_request(req)
134
+ req.wait
135
+ end
136
+ end
137
+
138
+ # Register a new RPC endpoint.
139
+ #
140
+ # This method works in sync mode if called at non-EM reactor thread.
141
+ # @param [String] endpoint
142
+ # @param [has call() method] app
143
+ # @param [Hash] opts
144
+ def register_endpoint(endpoint, app, opts={})
145
+ raise TypeError unless app.respond_to?(:call)
146
+ opts = {:exclusive=>true}.merge(opts)
147
+ @endpoints[endpoint]={:app=>app, :opts=>opts}
148
+
149
+ # create receive queue for new RPC endpoint.
150
+ endpoint_proc = proc { |header, data|
151
+
152
+ data = Serializer.instance.unmarshal(data)
153
+ event.publish('rpc/request_received', :args=>[header.message_id])
154
+
155
+ resctx = if data[:oneshot]
156
+ OneshotResponseContext.new(self.node, header)
157
+ else
158
+ ResponseContext.new(self.node, header)
159
+ end
160
+ begin
161
+ req = Rack::Request.new({:sender=>header.reply_to['command-recv.'.size..-1],
162
+ :message_id=>header.message_id
163
+ }.merge(data))
164
+ res = Rack::Response.new(resctx)
165
+ ret = app.call(req, res)
166
+ rescue Exception => e
167
+ logger.error(e)
168
+ resctx.response(e) unless resctx.responded?
169
+ end
170
+ }
171
+
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
+
181
+ 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
193
+ end
194
+
195
+ # expect to raise DuplicateEndpointError if endpoint exists.
196
+ # ignore the case of success.
197
+ dm.wait
198
+ }
199
+ }
200
+
201
+ dm.wait unless EventMachine.reactor_thread?
202
+ end
203
+
204
+ # Unregister endpoint.
205
+ #
206
+ # @param [String] endpoint endpoint name to be removed
207
+ def unregister_endpoint(endpoint)
208
+ if @endpoints.delete(endpoint)
209
+ dm = Util::DeferedMsg.new(30)
210
+ EventMachine.schedule {
211
+ amq.queue(endpoint_queue_name(endpoint)).delete
212
+ event.publish('rpc/unregister', :args=>[endpoint])
213
+ dm.success
214
+ }
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
+ end
242
+ end
243
+
244
+ private
245
+ def endpoint_queue_name(ns)
246
+ "isono.rpc.endpoint.#{ns}"
247
+ end
248
+
249
+ def event
250
+ @event ||= EventChannel.new(node)
251
+ end
252
+
253
+ # Publish a RPC request asynchronously.
254
+ # @param [RequestContext] req Request context object to be
255
+ # sent. If the context's state is not :init, it will fail.
256
+ def send_request(req)
257
+ raise TypeError if !req.is_a?(RequestContext)
258
+ raise "Request context seems to be sent already: #{req.state}" if req.state != :init
259
+
260
+ # possible timeout_sec values:
261
+ # timeout_sec == -1.0 : to be overwritten to the default timeout.
262
+ # timeout_sec == 0.0 : never be timed out.
263
+ # timeout_sec > 0.0 : wait for the user set timeout.
264
+ if req.timeout_sec == -1.0
265
+ # set default timeout if no one updated the initial value.
266
+ req.timeout_sec = config_section.timeout_sec
267
+ end
268
+
269
+ if req.timeout_sec > 0.0
270
+ # register the timeout hook.
271
+ req.timer = EventMachine::Timer.new(req.timeout_sec) {
272
+ @active_requests.delete req.ticket
273
+ req.error_cb.call(:timeout) if req.error_cb
274
+ }
275
+ end
276
+
277
+ req.process_event(:on_ready)
278
+
279
+ EventMachine.schedule {
280
+ if !req.oneshot
281
+ @active_requests[req.ticket] = req
282
+ 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
+ )
290
+ req.process_event(:on_sent)
291
+ event.publish('rpc/request_sent', :args=>[req.hash])
292
+ }
293
+ end
294
+
295
+ class ResponseContext
296
+ attr_reader :node, :header
297
+
298
+ def initialize(node, header)
299
+ @responded = false
300
+ @node = node
301
+ @header = header
302
+ end
303
+
304
+ def responded?
305
+ @responded
306
+ end
307
+
308
+ def progress(ret)
309
+ EM.schedule {
310
+ publish(:inprogress, ret)
311
+ }
312
+ end
313
+
314
+ def response(ret)
315
+ raise "" if @responded
316
+
317
+ EM.schedule {
318
+ @header.ack
319
+ if ret.is_a? Exception
320
+ publish(:error, {:message=> ret.message, :error_type => ret.class.to_s})
321
+ else
322
+ publish(:success, ret)
323
+ end
324
+ EventChannel.new(@node).publish('rpc/response_sent', :args=>[@header.message_id])
325
+ }
326
+ @responded = true
327
+ end
328
+
329
+
330
+ private
331
+ def publish(type, body)
332
+ @node.amq.direct('').publish(Serializer.instance.marshal({:type=>type, :msg=>body}),
333
+ {:key=>@header.reply_to,
334
+ :message_id=>@header.message_id}
335
+ )
336
+ end
337
+ end
338
+
339
+ # Do nothing when the endpoint trys to send back in case of
340
+ # oneshot request.
341
+ class OneshotResponseContext < ResponseContext
342
+ def progress(ret)
343
+ end
344
+
345
+ def response(ret)
346
+ raise "" if @responded
347
+
348
+ EM.schedule {
349
+ @header.ack
350
+ EventChannel.new(@node).publish('rpc/response_sent', :args=>[@header.message_id])
351
+ }
352
+ @responded = true
353
+ end
354
+ end
355
+
356
+ class RequestContext < OpenStruct
357
+ # They are not to be appeared in @table so that won't be inspect().
358
+ attr_reader :error_cb, :success_cb, :progress_cb
359
+ attr_accessor :timer
360
+
361
+ def initialize(endpoint, command, args)
362
+ super({:request=>{
363
+ :endpoint=> endpoint,
364
+ :command => command,
365
+ :args => args
366
+ },
367
+ :endpoint=> endpoint,
368
+ :command => command,
369
+ :ticket => Util.gen_id,
370
+ :timeout_sec => -1.0,
371
+ :oneshot => false,
372
+ :sent_at => nil,
373
+ :completed_at => nil,
374
+ :complete_status => nil,
375
+ })
376
+
377
+ @success_cb = nil
378
+ @progress_cb = nil
379
+ @error_cb = nil
380
+ @timer = nil
381
+
382
+ @stm = Statemachine.build {
383
+ trans :init, :on_ready, :ready
384
+ trans :ready, :on_sent, :waiting, proc {
385
+ self.sent_at=Time.now
386
+ # freeze request hash not to be modified after sending.
387
+ self.request.freeze
388
+ }
389
+ trans :waiting, :on_received, :waiting
390
+ trans :waiting, :on_error, :done, proc {
391
+ self.completed_at=Time.now
392
+ @timer.cancel if @timer
393
+ self.complete_status = :fail
394
+ }
395
+ trans :waiting, :on_success, :done, proc {
396
+ self.completed_at=Time.now
397
+ @timer.cancel if @timer
398
+ self.complete_status = :success
399
+ }
400
+ }
401
+ @stm.context = self
402
+ end
403
+
404
+ def state
405
+ @stm.state
406
+ end
407
+
408
+ def process_event(ev, *args)
409
+ @stm.process_event(ev, *args)
410
+ end
411
+
412
+ def elapsed_time
413
+ self.completed_at.nil? ? nil : (self.completed_at - self.sent_at)
414
+ end
415
+
416
+ def hash
417
+ # state, sent_at received_at are readonly values so they are
418
+ # not pushed in @table.
419
+ @table.dup.merge({:state=>self.state})
420
+ end
421
+
422
+ def request_hash
423
+ request.merge({:oneshot=>oneshot})
424
+ end
425
+
426
+ def on_success(&blk)
427
+ raise ArgumentError unless blk
428
+ @success_cb = blk
429
+ end
430
+
431
+ def on_progress(&blk)
432
+ raise ArgumentError unless blk
433
+ @progress_cb = blk
434
+ end
435
+
436
+ def on_error(&blk)
437
+ raise ArgumentError unless blk
438
+ @error_cb = blk
439
+ end
440
+
441
+ def synchronize
442
+ self.extend RequestSynchronize
443
+ self
444
+ end
445
+
446
+ module RequestSynchronize
447
+ def self.extended(mod)
448
+ raise TypeError, "This module is applicable only for RequestContext" unless mod.is_a?(RequestContext)
449
+ # overwrite callbacks
450
+ mod.instance_eval {
451
+ @q = ::Queue.new
452
+
453
+ on_success { |r|
454
+ @q << [:success, r]
455
+ }
456
+ on_error { |r|
457
+ @q << [:error, r]
458
+ }
459
+ }
460
+ end
461
+
462
+ public
463
+ def wait()
464
+ raise "response was received already." if state == :done
465
+ raise "wait() has to be called at outside of the EventMachine's main loop." if EventMachine.reactor_thread?
466
+
467
+ r = @q.deq
468
+
469
+ case r[0]
470
+ when :success
471
+ r[1]
472
+ when :error
473
+ raise RpcError, r[1]
474
+ end
475
+ end
476
+ end
477
+
478
+ end
479
+
480
+ end
481
+ end
482
+ end