isono 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Rakefile +38 -0
- data/isono.gemspec +13 -12
- data/lib/isono.rb +5 -4
- data/lib/isono/amqp_client.rb +71 -42
- data/lib/isono/logger.rb +19 -9
- data/lib/isono/manifest.rb +9 -10
- data/lib/isono/node.rb +41 -25
- data/lib/isono/node_modules/base.rb +25 -9
- data/lib/isono/node_modules/event_channel.rb +18 -7
- data/lib/isono/node_modules/job_channel.rb +20 -21
- data/lib/isono/node_modules/job_worker.rb +47 -28
- data/lib/isono/node_modules/rpc_channel.rb +46 -96
- data/lib/isono/rack/job.rb +19 -32
- data/lib/isono/runner/base.rb +150 -0
- data/lib/isono/runner/cli.rb +28 -0
- data/lib/isono/runner/rpc_server.rb +21 -53
- data/lib/isono/thread_pool.rb +24 -19
- data/lib/isono/util.rb +12 -4
- data/lib/isono/version.rb +5 -0
- data/spec/amqp_client_spec.rb +71 -0
- data/spec/event_observable_spec.rb +6 -0
- data/spec/file_channel_spec.rb +263 -0
- data/spec/job_channel_spec.rb +47 -0
- data/spec/logger_spec.rb +45 -0
- data/spec/manifest_spec.rb +43 -0
- data/spec/node_spec.rb +64 -0
- data/spec/resource_loader_spec.rb +113 -0
- data/spec/rpc_channel_spec.rb +172 -0
- data/spec/spec_helper.rb +62 -0
- data/spec/thread_pool_spec.rb +35 -0
- data/spec/util_spec.rb +38 -0
- data/tasks/load_resource_manifest.rake +7 -0
- metadata +79 -43
- data/lib/isono/runner/agent.rb +0 -89
data/lib/isono/rack/job.rb
CHANGED
@@ -5,17 +5,9 @@ module Rack
|
|
5
5
|
class Job < Decorator
|
6
6
|
include Logger
|
7
7
|
|
8
|
-
# Response class for nothing response.
|
9
|
-
# It is used when the job request type is :submit.
|
10
|
-
class NullResponse < Response
|
11
|
-
def progress(ret)
|
12
|
-
end
|
13
|
-
|
14
|
-
def response(ret)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
8
|
class JobResponse < Response
|
9
|
+
attr_reader :job
|
10
|
+
|
19
11
|
# @param [NodeModules::RpcChannel::ResponseContext] ctx
|
20
12
|
# @param [NodeModules::JobWorker::JobContext] jobctx
|
21
13
|
def initialize(ctx, jobctx)
|
@@ -30,6 +22,8 @@ module Rack
|
|
30
22
|
end
|
31
23
|
|
32
24
|
class JobRequest < Request
|
25
|
+
attr_reader :job
|
26
|
+
|
33
27
|
# @param [Hash] request_hash
|
34
28
|
# @param [NodeModules::JobWorker::JobContext] jobctx
|
35
29
|
def initialize(request_hash, jobctx)
|
@@ -44,30 +38,23 @@ module Rack
|
|
44
38
|
end
|
45
39
|
|
46
40
|
def call(req, res)
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
job = @job_worker.start do |job|
|
42
|
+
job.job_id = req.r[:job_id]
|
43
|
+
job.parent_job_id = req.r[:parent_job_id]
|
44
|
+
job.run_cb = proc {
|
45
|
+
begin
|
46
|
+
@app.call(JobRequest.new(req.r, job), JobResponse.new(res.ctx, job))
|
47
|
+
res.response(nil) unless res.responded?
|
48
|
+
rescue Exception => e
|
49
|
+
res.response(e) unless res.responded?
|
50
|
+
raise e
|
51
|
+
end
|
52
|
+
}
|
51
53
|
end
|
52
|
-
|
53
|
-
job = @job_worker.run(req.r[:parent_job_id]){
|
54
|
-
begin
|
55
|
-
@app.call(JobRequest.new(req.r, job), JobResponse.new(res.ctx, job))
|
56
|
-
res.response(nil) unless res.responded?
|
57
|
-
rescue Exception => e
|
58
|
-
res.response(e) unless res.responded?
|
59
|
-
raise e
|
60
|
-
end
|
61
|
-
}
|
62
54
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
else
|
67
|
-
# send job context info back at the first progress message.
|
68
|
-
# following progress messages to be handled as usual.
|
69
|
-
res.progress(job.to_hash)
|
70
|
-
end
|
55
|
+
# send the new job context info back as the first progress message.
|
56
|
+
# the progress messages that follow will be handled as usual.
|
57
|
+
res.progress(job.to_hash)
|
71
58
|
end
|
72
59
|
end
|
73
60
|
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'etc'
|
6
|
+
|
7
|
+
require 'isono'
|
8
|
+
require 'isono/amqp_client'
|
9
|
+
|
10
|
+
module Isono
|
11
|
+
module Runner
|
12
|
+
|
13
|
+
module Daemonize
|
14
|
+
# Change privileges of the process
|
15
|
+
# to the specified user and group.
|
16
|
+
def self.change_privilege(user, group=user)
|
17
|
+
logger.info("Changing process privilege to #{user}:#{group}")
|
18
|
+
|
19
|
+
uid, gid = Process.euid, Process.egid
|
20
|
+
target_uid = Etc.getpwnam(user).uid
|
21
|
+
target_gid = Etc.getgrnam(group).gid
|
22
|
+
|
23
|
+
if uid != target_uid || gid != target_gid
|
24
|
+
# Change process ownership
|
25
|
+
Process.initgroups(user, target_gid)
|
26
|
+
Process::GID.change_privilege(target_gid)
|
27
|
+
Process::UID.change_privilege(target_uid)
|
28
|
+
end
|
29
|
+
rescue Errno::EPERM => e
|
30
|
+
logger.error("Couldn't change user and group to #{user}:#{group}: #{e}")
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.daemonize(log_io=STDOUT)
|
34
|
+
exit if fork
|
35
|
+
srand
|
36
|
+
trap 'SIGHUP', 'DEFAULT'
|
37
|
+
|
38
|
+
STDIN.reopen('/dev/null')
|
39
|
+
|
40
|
+
STDOUT.reopen(log_io)
|
41
|
+
STDERR.reopen(log_io)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Base
|
46
|
+
include Logger
|
47
|
+
|
48
|
+
def initialize()
|
49
|
+
@options = {
|
50
|
+
:amqp_server_uri => URI.parse('amqp://guest:guest@localhost/'),
|
51
|
+
:log_file => nil,
|
52
|
+
:pid_file => nil,
|
53
|
+
:daemonize => false,
|
54
|
+
:config_path => nil,
|
55
|
+
:manifest_path => nil,
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def optparse(args)
|
60
|
+
optparse = OptionParser.new do |opts|
|
61
|
+
opts.banner = "Usage: #{$0} [options]"
|
62
|
+
|
63
|
+
opts.separator ""
|
64
|
+
opts.separator "Options:"
|
65
|
+
opts.on( "-i", "--id ID", "Manually specify the Agent ID" ) {|str| @options[:node_id] = str }
|
66
|
+
opts.on( "-p", "--pid PIDFILE", "pid file path" ) {|str| @options[:pid_file] = str }
|
67
|
+
opts.on( "--log LOGFILE", "log file path" ) {|str| @options[:log_file] = str }
|
68
|
+
opts.on( "--config CONFFILE", "config file path" ) {|str| @options[:config_file] = str }
|
69
|
+
opts.on( "-s", "--server AMQP_URI", "amqp broker server to connect" ) {|str|
|
70
|
+
begin
|
71
|
+
@options[:amqp_server_uri] = URI.parse(str)
|
72
|
+
rescue URI::InvalidURIError => e
|
73
|
+
abort "#{e}"
|
74
|
+
end
|
75
|
+
}
|
76
|
+
opts.on("-b", "Run in background" ) { @options[:daemonize] = false }
|
77
|
+
end
|
78
|
+
|
79
|
+
optparse.parse!(args)
|
80
|
+
end
|
81
|
+
|
82
|
+
def run(manifest=nil, opts={}, &blk)
|
83
|
+
@options = @options.merge(opts)
|
84
|
+
optparse(ARGV.dup)
|
85
|
+
|
86
|
+
if manifest.is_a?(String)
|
87
|
+
# load manifest file
|
88
|
+
manifest = Manifest.load_file(manifest)
|
89
|
+
elsif manifest.nil? && @options[:manifest_path]
|
90
|
+
manifest = Manifest.load_file(@options[:manifest_path])
|
91
|
+
end
|
92
|
+
|
93
|
+
if @options[:node_id]
|
94
|
+
# force overwrite node_id if the command line arg was given.
|
95
|
+
manifest.node_instance_id(@options[:node_id])
|
96
|
+
elsif manifest.node_id.nil?
|
97
|
+
# nobody specified the node_id then set the ID in the
|
98
|
+
# default manner.
|
99
|
+
manifest.node_id(default_node_id)
|
100
|
+
end
|
101
|
+
|
102
|
+
@options[:log_file] ||= "/var/log/%s.log" % [manifest.node_name]
|
103
|
+
@options[:pid_file] ||= "/var/run/%s.pid" % [manifest.node_name]
|
104
|
+
|
105
|
+
if @options[:daemonize]
|
106
|
+
if @options[:log_file]
|
107
|
+
logio = File.open(@options[:log_file], "a")
|
108
|
+
end
|
109
|
+
Daemonize.daemonize(logio || STDOUT)
|
110
|
+
end
|
111
|
+
|
112
|
+
# EM's reactor is shutdown already when EXIT signal is
|
113
|
+
# caught. so that set handler for TERM, INT
|
114
|
+
%w(TERM INT).each { |i|
|
115
|
+
Signal.trap(i) {
|
116
|
+
if @node
|
117
|
+
# force the block to push next loop.
|
118
|
+
# EM.schedule gets the current thread stucked.
|
119
|
+
EventMachine.next_tick {
|
120
|
+
@node.close { EventMachine.stop }
|
121
|
+
@node = nil
|
122
|
+
}
|
123
|
+
end
|
124
|
+
}
|
125
|
+
}
|
126
|
+
Signal.trap(:EXIT) { remove_pidfile if @options[:daemonize] }
|
127
|
+
|
128
|
+
EventMachine.epoll
|
129
|
+
EventMachine.run {
|
130
|
+
@node = run_main(manifest, &blk)
|
131
|
+
raise "run_main() must return Isono::Node object: #{@node.class}" unless @node.is_a?(Isono::Node)
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
protected
|
136
|
+
def run_main(manifest, &blk)
|
137
|
+
Isono::Node.new(manifest).connect(@options[:amqp_server_uri]) { |n|
|
138
|
+
@node = n
|
139
|
+
self.instance_eval &blk
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def default_node_id
|
144
|
+
raise NotImplementedError
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'isono'
|
4
|
+
|
5
|
+
module Isono
|
6
|
+
module Runner
|
7
|
+
module CLI
|
8
|
+
|
9
|
+
def run_cli(manifest=nil, &blk)
|
10
|
+
Oneshot.new.run(manifest, &blk)
|
11
|
+
end
|
12
|
+
module_function :run_cli
|
13
|
+
|
14
|
+
class Oneshot < Runner::Base
|
15
|
+
include Logger
|
16
|
+
|
17
|
+
def run_main(manifest, &blk)
|
18
|
+
Isono::Node.new(manifest).connect(@options[:amqp_server_uri]) do |n|
|
19
|
+
exit(1) unless n.connected?
|
20
|
+
@node = n
|
21
|
+
self.instance_eval &blk
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -54,8 +54,10 @@ module Isono
|
|
54
54
|
def job(command, run_cb=nil, fail_cb=nil, &blk)
|
55
55
|
app = if run_cb.is_a?(Proc)
|
56
56
|
proc {
|
57
|
-
|
58
|
-
|
57
|
+
if fail_cb.is_a?(Proc)
|
58
|
+
response.fail_cb do
|
59
|
+
self.instance_eval(&fail_cb)
|
60
|
+
end
|
59
61
|
end
|
60
62
|
|
61
63
|
self.instance_eval(&run_cb)
|
@@ -131,66 +133,32 @@ module Isono
|
|
131
133
|
load_module NodeModules::JobChannel
|
132
134
|
end
|
133
135
|
|
134
|
-
def start(manifest=nil, &blk)
|
135
|
-
rpcsvr = Server.new(
|
136
|
-
rpcsvr.run(manifest
|
136
|
+
def start(manifest=nil, opts={}, &blk)
|
137
|
+
rpcsvr = Server.new(blk)
|
138
|
+
rpcsvr.run(manifest)
|
137
139
|
end
|
138
140
|
module_function :start
|
139
141
|
|
140
|
-
class Server
|
141
|
-
def initialize(
|
142
|
-
|
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
|
-
}
|
142
|
+
class Server < Base
|
143
|
+
def initialize(builder_block)
|
144
|
+
super()
|
145
|
+
@builder_block = builder_block
|
187
146
|
end
|
188
|
-
|
147
|
+
|
148
|
+
# DSL method
|
189
149
|
def endpoint(endpoint, builder)
|
190
150
|
raise TypeError unless builder.respond_to?(:build)
|
191
151
|
builder.build(endpoint, @node)
|
192
152
|
end
|
193
153
|
|
154
|
+
protected
|
155
|
+
def run_main(manifest=nil)
|
156
|
+
@node = Isono::Node.new(manifest)
|
157
|
+
@node.connect(@options[:amqp_server_uri]) do
|
158
|
+
self.instance_eval(&@builder_block) if @builder_block
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
194
162
|
end
|
195
163
|
|
196
164
|
end
|
data/lib/isono/thread_pool.rb
CHANGED
@@ -18,25 +18,29 @@ module Isono
|
|
18
18
|
@worker_threads = {}
|
19
19
|
worker_num.times {
|
20
20
|
t = Thread.new {
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@last_stuck_warn_at = Time.now
|
27
|
-
end
|
28
|
-
op.call
|
21
|
+
begin
|
22
|
+
while op = @queue.pop
|
23
|
+
if @queue.size > @opts[:stucked_queue_num] && Time.now - @last_stuck_warn_at > 5.0
|
24
|
+
logger.warn("too many stacked jobs: #{@queue.size}")
|
25
|
+
@last_stuck_warn_at = Time.now
|
29
26
|
end
|
30
|
-
|
31
|
-
|
32
|
-
# exit from the current loop
|
33
|
-
break
|
34
|
-
rescue Exception => e
|
35
|
-
self.logger.error(e)
|
27
|
+
|
28
|
+
op.call
|
36
29
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
30
|
+
rescue WorkerTerminateError
|
31
|
+
# someone indicated to terminate this thread
|
32
|
+
# exit from the current loop
|
33
|
+
break
|
34
|
+
rescue Exception => e
|
35
|
+
logger.error(e)
|
36
|
+
retry
|
37
|
+
ensure
|
38
|
+
EM.schedule {
|
39
|
+
@worker_threads.delete(Thread.current.__id__)
|
40
|
+
logger.debug("#{Thread.current} is being terminated")
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
40
44
|
}
|
41
45
|
@worker_threads[t.__id__] = t
|
42
46
|
}
|
@@ -100,8 +104,9 @@ module Isono
|
|
100
104
|
|
101
105
|
# Immediatly shutdown all the worker threads
|
102
106
|
def shutdown()
|
103
|
-
@worker_threads.each {|t|
|
104
|
-
t.raise WorkerTerminateError
|
107
|
+
@worker_threads.each {|id, t|
|
108
|
+
t.__send__(:raise, WorkerTerminateError)
|
109
|
+
Thread.pass
|
105
110
|
}
|
106
111
|
end
|
107
112
|
|
data/lib/isono/util.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
3
|
require 'digest/sha1'
|
4
|
-
require 'hmac-sha1'
|
5
4
|
require 'thread'
|
6
5
|
require 'stringio'
|
7
6
|
|
@@ -44,7 +43,7 @@ module Isono
|
|
44
43
|
when 'Linux'
|
45
44
|
`/sbin/ip route get 8.8.8.8`.split("\n")[0].split.last
|
46
45
|
when 'SunOS'
|
47
|
-
`/sbin/ifconfig $(route get 1.1.1.1 | awk '$1 == "interface:" {print $2}') | awk '$1 == "inet" { print $2 }'`
|
46
|
+
`/sbin/ifconfig $(/usr/sbin/route -n get 1.1.1.1 | awk '$1 == "interface:" {print $2}') | awk '$1 == "inet" { print $2 }'`
|
48
47
|
else
|
49
48
|
raise "Unsupported platform to detect gateway IP address: #{`/bin/uname`}"
|
50
49
|
end
|
@@ -171,6 +170,7 @@ module Isono
|
|
171
170
|
super()
|
172
171
|
@thread_wait = th
|
173
172
|
@timer_sig = EventMachine.add_timer(timeout) {
|
173
|
+
@on_timeout_hook.call if @on_timeout_hook
|
174
174
|
error(TimeoutError.new)
|
175
175
|
}
|
176
176
|
end
|
@@ -180,13 +180,21 @@ module Isono
|
|
180
180
|
@thread_called = Thread.current
|
181
181
|
end
|
182
182
|
|
183
|
-
|
184
183
|
def error(ex)
|
185
184
|
raise TypeError unless ex.is_a?(Exception)
|
186
185
|
self.enq(ex)
|
187
186
|
@thread_called = Thread.current
|
188
187
|
end
|
189
188
|
|
189
|
+
def on_timeout(&blk)
|
190
|
+
@on_timeout_hook = blk
|
191
|
+
end
|
192
|
+
|
193
|
+
def cancel
|
194
|
+
EventMachine.cancel_timer(@timer_sig) rescue nil
|
195
|
+
@on_timeout_hook = nil
|
196
|
+
end
|
197
|
+
|
190
198
|
def wait
|
191
199
|
if (@thread_called == @thread_wait || @thread_called == Thread.current ) &&
|
192
200
|
self.empty?
|
@@ -203,7 +211,7 @@ module Isono
|
|
203
211
|
end
|
204
212
|
ensure
|
205
213
|
@thread_wait = nil
|
206
|
-
|
214
|
+
self.cancel
|
207
215
|
end
|
208
216
|
end
|
209
217
|
|