isono 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,89 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'optparse'
4
+ require 'amqp'
5
+ require 'digest/sha1'
6
+
7
+ require 'isono/agent'
8
+
9
+ module Isono
10
+ module Runner
11
+ class Agent
12
+ include Daemonize
13
+
14
+ def initialize(argv)
15
+ @argv = argv.dup
16
+
17
+ @options = {
18
+ :amqp_server_uri => URI.parse('amqp://guest:guest@localhost/'),
19
+ :log_file => nil,
20
+ :pid_file => nil,
21
+ :daemonize => true
22
+ }
23
+
24
+ parser.parse! @argv
25
+ end
26
+
27
+
28
+ def parser
29
+ @parser ||= OptionParser.new do |opts|
30
+ opts.banner = "Usage: agent [options]"
31
+
32
+ opts.separator ""
33
+ opts.separator "Agent options:"
34
+ opts.on( "-i", "--id ID", "Manually specify the Agent ID" ) {|str| @options[:agent_id] = str }
35
+ opts.on( "-p", "--pid PIDFILE", "pid file path" ) {|str| @options[:pid_file] = str }
36
+ opts.on( "-s", "--server AMQP_URI", "amqp broker server to connect" ) {|str|
37
+ begin
38
+ @options[:amqp_server_uri] = URI.parse(str)
39
+ rescue URI::InvalidURIError => e
40
+ abort "#{e}"
41
+ end
42
+ }
43
+ opts.on("-X", "Run in foreground" ) { @options[:daemonize] = false }
44
+ end
45
+ end
46
+
47
+
48
+ def run(manifest_path=nil)
49
+ #%w(QUIT INT TERM).each { |i|
50
+ %w(EXIT).each { |i|
51
+ Signal.trap(i) { Isono::Agent.stop{ remove_pidfile if @options[:daemonize]} }
52
+ }
53
+
54
+
55
+ # load manifest file
56
+ manifest = Manifest.load_file(manifest_path.nil? ? @options[:manifest_path] : manifest_path)
57
+
58
+ if @options[:node_id]
59
+ # force overwrite node_id if the command line arg was given.
60
+ manifest.node_id(@options[:node_id])
61
+ elsif manifest.node_id.nil?
62
+ # nobody specified the node_id then set the ID in the
63
+ # default manner.
64
+ manifest.node_id(default_node_id)
65
+ end
66
+
67
+ @options[:log_file] ||= "/var/log/%s.log" % [manifest.node_name]
68
+ @options[:pid_file] ||= "/var/run/%s.pid" % [manifest.node_name]
69
+
70
+ if @options[:daemonize]
71
+ daemonize(@options[:log_file])
72
+ end
73
+
74
+ EventMachine.epoll
75
+ EventMachine.run {
76
+ Isono::Agent.start(manifest, @options)
77
+ }
78
+ end
79
+
80
+ private
81
+ def default_node_id
82
+ # use the ip address for the default routeas key value
83
+ Digest::SHA1.hexdigest(`/sbin/ip route get 8.8.8.8`.split("\n")[0].split.last)[0, 10]
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,198 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'optparse'
4
+ require 'amqp'
5
+ require 'digest/sha1'
6
+
7
+ require 'isono'
8
+ require 'isono/amqp_client'
9
+
10
+ module Isono
11
+ module Runner
12
+ # Run a new agent which provides RPC endpoint and delayed job.
13
+ #
14
+ # @example
15
+ #
16
+ # class RpcEndpoint1
17
+ # def func1
18
+ # end
19
+ # end
20
+ #
21
+ # class RpcEndpoint2
22
+ # def func2
23
+ # end
24
+ # end
25
+ #
26
+ # Isono::Runner::RpcServer.start do
27
+ # endpoint('xxxx1', RpcEndpoint1.new)
28
+ # endpoint('xxxx2', RpcEndpoint2.new)
29
+ # end
30
+ #
31
+ # mc = MessagingClient.new
32
+ # xxxx1 = mc.rpc('xxxx1')
33
+ # xxxx1.func1
34
+ #
35
+ # xxxx2 = mc.rpc('xxxx2')
36
+ # xxxx2.func2
37
+ module RpcServer
38
+
39
+ class EndpointBuilder
40
+ module BuildMethods
41
+ # @exmaple
42
+ # job 'command1', proc {
43
+ # # do somthing.
44
+ # }, proc {
45
+ # # do somthing on job failure.
46
+ # }
47
+ # @example
48
+ # job 'command1' do
49
+ # response.fail_cb {
50
+ # # do somthing on job failure.
51
+ # }
52
+ # sleep 10
53
+ # end
54
+ def job(command, run_cb=nil, fail_cb=nil, &blk)
55
+ app = if run_cb.is_a?(Proc)
56
+ proc {
57
+ request.fail_cb do
58
+ self.instance_eval(&fail_cb)
59
+ end
60
+
61
+ self.instance_eval(&run_cb)
62
+ }
63
+ elsif blk
64
+ blk
65
+ else
66
+ raise ArgumentError, "callbacks were not set propery"
67
+ end
68
+ add(:job, command, &app)
69
+ end
70
+
71
+ def rpc(command, &blk)
72
+ add(:rpc, command, &blk)
73
+ end
74
+
75
+ def build(endpoint, node)
76
+ helper_context = self.new(node)
77
+
78
+ app_builder = lambda { |builders|
79
+ unless builders.empty?
80
+ map_app = Rack::Map.new
81
+ builders.each { |b|
82
+ b.call(map_app, helper_context)
83
+ }
84
+ map_app
85
+ end
86
+ }
87
+
88
+ app = app_builder.call(@builders[:job])
89
+ if app
90
+ NodeModules::JobChannel.new(node).register_endpoint(endpoint, Rack.build do
91
+ run app
92
+ end)
93
+ end
94
+
95
+ app = app_builder.call(@builders[:rpc])
96
+ if app
97
+ NodeModules::RpcChannel.new(node).register_endpoint(endpoint, Rack.build do
98
+ use Rack::ThreadPass
99
+ run app
100
+ end
101
+ )
102
+ end
103
+ end
104
+
105
+
106
+ protected
107
+ def add(type, command, &blk)
108
+ @builders[type] << lambda { |rack_map, ctx|
109
+ rack_map.map(command, Rack::Proc.new(ctx, &blk))
110
+ }
111
+ end
112
+ end
113
+
114
+ def self.inherited(klass)
115
+ klass.class_eval {
116
+ @builders = {:job=>[], :rpc=>[]}
117
+ extend BuildMethods
118
+ }
119
+ end
120
+
121
+ def initialize(node)
122
+ @node = node
123
+ end
124
+ end
125
+
126
+
127
+ DEFAULT_MANIFEST = Manifest.new(Dir.pwd) do
128
+ load_module NodeModules::EventChannel
129
+ load_module NodeModules::RpcChannel
130
+ load_module NodeModules::JobWorker
131
+ load_module NodeModules::JobChannel
132
+ end
133
+
134
+ def start(manifest=nil, &blk)
135
+ rpcsvr = Server.new(ARGV)
136
+ rpcsvr.run(manifest, &blk)
137
+ end
138
+ module_function :start
139
+
140
+ class Server
141
+ def initialize(argv)
142
+ @argv = argv.dup
143
+
144
+ @options = {
145
+ :amqp_server_uri => URI.parse('amqp://guest:guest@localhost/'),
146
+ }
147
+
148
+ parser.parse! @argv
149
+ end
150
+
151
+ def parser
152
+ @parser ||= OptionParser.new do |opts|
153
+ opts.banner = "Usage: agent [options]"
154
+
155
+ opts.separator ""
156
+ opts.separator "Agent options:"
157
+ opts.on( "-i", "--id ID", "Manually specify the Node ID" ) {|str| @options[:node_id] = str }
158
+ opts.on( "-s", "--server AMQP_URI", "amqp broker server to connect" ) {|str|
159
+ begin
160
+ @options[:amqp_server_uri] = URI.parse(str)
161
+ rescue URI::InvalidURIError => e
162
+ abort "#{e}"
163
+ end
164
+ }
165
+ end
166
+ end
167
+
168
+ def run(manifest=nil, &blk)
169
+ %w(EXIT).each { |i|
170
+ Signal.trap(i) { Isono::Node.stop }
171
+ }
172
+
173
+ # .to_s to avoid nil -> String conversion failure.
174
+ if @options[:node_id]
175
+ manifest.node_instance_id(@options[:node_id])
176
+ elsif manifest.node_instance_id.nil?
177
+ abort("[ERROR]: manifest.node_istance_id is not set")
178
+ end
179
+
180
+ EventMachine.epoll
181
+ EventMachine.run {
182
+ @node = Isono::Node.new(manifest)
183
+ @node.connect(@options[:amqp_server_uri], @options) do
184
+ self.instance_eval(&blk) if blk
185
+ end
186
+ }
187
+ end
188
+
189
+ def endpoint(endpoint, builder)
190
+ raise TypeError unless builder.respond_to?(:build)
191
+ builder.build(endpoint, @node)
192
+ end
193
+
194
+ end
195
+
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,43 @@
1
+
2
+ module Isono
3
+ # AMQP message serializer
4
+ class Serializer
5
+ def self.instance
6
+ @serializer ||= RubySerializer.new
7
+ end
8
+
9
+ def marshal(buf)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def unmarshal(buf)
14
+ raise NotImplementedError
15
+ end
16
+ end
17
+
18
+
19
+ class YamlSerializer < Serializer
20
+ def initialize
21
+ require 'yaml'
22
+ end
23
+
24
+ def marshal(buf)
25
+ YAML.dump(buf)
26
+ end
27
+
28
+ def unmarshal(buf)
29
+ YAML.load(buf)
30
+ end
31
+ end
32
+
33
+ class RubySerializer < Serializer
34
+
35
+ def marshal(buf)
36
+ ::Marshal.dump(buf)
37
+ end
38
+
39
+ def unmarshal(buf)
40
+ ::Marshal.load(buf)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,169 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'thread'
3
+
4
+ module Isono
5
+ class ThreadPool
6
+ include Logger
7
+
8
+ class WorkerTerminateError < StandardError; end
9
+ class TimeoutError < StandardError; end
10
+
11
+ def initialize(worker_num=1, name=nil, opts={})
12
+ set_instance_logger(name)
13
+ @queue = ::Queue.new
14
+ @name = name
15
+ @opts = {:stucked_queue_num=>20}.merge(opts)
16
+ @last_stuck_warn_at = Time.now
17
+
18
+ @worker_threads = {}
19
+ worker_num.times {
20
+ t = Thread.new {
21
+ loop {
22
+ begin
23
+ while op = @queue.pop
24
+ if @queue.size > @opts[:stucked_queue_num] && Time.now - @last_stuck_warn_at > 5.0
25
+ logger.warn("too many stacked workers: #{@queue.size}")
26
+ @last_stuck_warn_at = Time.now
27
+ end
28
+ op.call
29
+ end
30
+ rescue WorkerTerminateError
31
+ # someone indicated to terminate this thread
32
+ # exit from the current loop
33
+ break
34
+ rescue Exception => e
35
+ self.logger.error(e)
36
+ end
37
+ }
38
+ @worker_threads.delete(Thread.current.__id__)
39
+ logger.info("#{Thread.current} is being terminated")
40
+ }
41
+ @worker_threads[t.__id__] = t
42
+ }
43
+ end
44
+
45
+ # Pass a block to a worker thread. The job is queued until the
46
+ # worker thread found.
47
+ # @param [Bool] immediage
48
+ # @param [Proc] blk A block to be proccessed on a worker thread.
49
+ def pass(immediate=true, &blk)
50
+ if immediate && member_thread?
51
+ return blk.call
52
+ end
53
+
54
+ @queue << blk
55
+ end
56
+
57
+ # Send a block to a worker thread similar with pass(). but this
58
+ # get the caller thread waited until the block proceeded in a
59
+ # worker thread.
60
+ # @param [Bool] immediage
61
+ # @param [Float] time_out
62
+ # @param [Proc] blk
63
+ def barrier(immediate=true, time_out=nil, &blk)
64
+ if immediate && member_thread?
65
+ return blk.call
66
+ end
67
+
68
+ q = ::Queue.new
69
+ time_start = ::Time.now
70
+
71
+ self.pass {
72
+ begin
73
+ q << blk.call
74
+ rescue Exception => e
75
+ q << e
76
+ end
77
+ }
78
+
79
+ em_sig = nil
80
+ if time_out
81
+ em_sig = EventMachine.add_timer(time_out) {
82
+ q << TimeoutError.new
83
+ }
84
+ end
85
+
86
+ res = q.shift
87
+ EventMachine.cancel_timer(em_sig)
88
+ time_elapsed = ::Time.now - time_start
89
+ logger.debug("Elapsed time for #{blk}: #{time_elapsed} secs") if time_elapsed > 0.05
90
+ if res.is_a?(Exception)
91
+ raise res
92
+ end
93
+ res
94
+
95
+ end
96
+
97
+ def clear
98
+ @queue.clear
99
+ end
100
+
101
+ # Immediatly shutdown all the worker threads
102
+ def shutdown()
103
+ @worker_threads.each {|t|
104
+ t.raise WorkerTerminateError
105
+ }
106
+ end
107
+
108
+ def member_thread?(thread=Thread.current)
109
+ @worker_threads.has_key?(thread.__id__)
110
+ end
111
+
112
+ def shutdown_graceful(timeout)
113
+ term_sig_q = ::Queue.new
114
+ worker_num = @worker_threads.size
115
+ # enqueue the terminate jobs.
116
+ worker_num.times {
117
+ @queue.push proc {
118
+ term_sig_q.enq(1)
119
+ raise WorkerTerminateError
120
+ }
121
+ }
122
+
123
+ em_sig = nil
124
+ if timeout > 0.0
125
+ em_sig = EventMachine.add_timer(timeout) {
126
+ worker_num.times {
127
+ term_sig_q << TimeoutError.new
128
+ }
129
+ }
130
+ end
131
+
132
+ timeout_workers = 0
133
+ while worker_num > 0
134
+ if term_sig_q.deq.is_a?(TimeoutError)
135
+ timeout_workers += 1
136
+ end
137
+ worker_num -= 1
138
+ end
139
+
140
+ logger.error("#{timeout_workers} of worker threads timed out during the cleanup") if timeout_workers > 0
141
+ ensure
142
+ shutdown
143
+ EventMachine.cancel_timer(em_sig)
144
+ end
145
+
146
+ def graceful_shutdown2
147
+ # make new jobs push to dummy queue.
148
+ old_queue = @queue
149
+ @queue = ::Queue.new
150
+ # wait until @queue becomes empty
151
+ if !old_queue.empty?
152
+ logger.info("Waiting for #{old_queue.size} worker jobs in #{self}")
153
+ while !old_queue.empty?
154
+ sleep 1
155
+ end
156
+ end
157
+
158
+ @worker_threads.each {|t|
159
+ t.raise WorkerTerminateError
160
+ }
161
+ end
162
+
163
+
164
+ private
165
+ def thread_loop
166
+ end
167
+
168
+ end
169
+ end