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.
- 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
|
|