isono 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +202 -0
- data/NOTICE +2 -0
- data/bin/cli +122 -0
- data/isono.gemspec +47 -0
- data/lib/ext/shellwords.rb +172 -0
- data/lib/isono.rb +61 -0
- data/lib/isono/amqp_client.rb +169 -0
- data/lib/isono/daemonize.rb +96 -0
- data/lib/isono/event_delegate_context.rb +56 -0
- data/lib/isono/event_observable.rb +86 -0
- data/lib/isono/logger.rb +48 -0
- data/lib/isono/manifest.rb +161 -0
- data/lib/isono/messaging_client.rb +116 -0
- data/lib/isono/models/event_log.rb +28 -0
- data/lib/isono/models/job_state.rb +35 -0
- data/lib/isono/models/node_state.rb +70 -0
- data/lib/isono/models/resource_instance.rb +35 -0
- data/lib/isono/node.rb +158 -0
- data/lib/isono/node_modules/base.rb +65 -0
- data/lib/isono/node_modules/data_store.rb +57 -0
- data/lib/isono/node_modules/event_channel.rb +72 -0
- data/lib/isono/node_modules/event_logger.rb +39 -0
- data/lib/isono/node_modules/job_channel.rb +86 -0
- data/lib/isono/node_modules/job_collector.rb +47 -0
- data/lib/isono/node_modules/job_worker.rb +152 -0
- data/lib/isono/node_modules/node_collector.rb +87 -0
- data/lib/isono/node_modules/node_heartbeat.rb +26 -0
- data/lib/isono/node_modules/rpc_channel.rb +482 -0
- data/lib/isono/rack.rb +67 -0
- data/lib/isono/rack/builder.rb +40 -0
- data/lib/isono/rack/data_store.rb +20 -0
- data/lib/isono/rack/job.rb +74 -0
- data/lib/isono/rack/map.rb +56 -0
- data/lib/isono/rack/object_method.rb +20 -0
- data/lib/isono/rack/proc.rb +50 -0
- data/lib/isono/rack/thread_pass.rb +22 -0
- data/lib/isono/resource_manifest.rb +273 -0
- data/lib/isono/runner/agent.rb +89 -0
- data/lib/isono/runner/rpc_server.rb +198 -0
- data/lib/isono/serializer.rb +43 -0
- data/lib/isono/thread_pool.rb +169 -0
- data/lib/isono/util.rb +212 -0
- metadata +185 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Isono
|
4
|
+
module Models
|
5
|
+
class ResourceInstance < Sequel::Model
|
6
|
+
include Logger
|
7
|
+
plugin :schema
|
8
|
+
plugin :hook_class_methods
|
9
|
+
plugin :serialization
|
10
|
+
|
11
|
+
set_schema {
|
12
|
+
primary_key :id, :type => Integer, :auto_increment=>true, :unsigned=>true
|
13
|
+
column :agent_id, :varchar, :size=>80, :null=>false
|
14
|
+
column :uuid, :varchar, :size=>50, :null=>false
|
15
|
+
column :resource_type, :varchar, :size=>50, :null=>false
|
16
|
+
column :instance_data, :text
|
17
|
+
column :status, :varchar, :size=>10
|
18
|
+
|
19
|
+
column :created_at, :datetime, :null=>false
|
20
|
+
column :updated_at, :datetime, :null=>false
|
21
|
+
index [:uuid], {:unique=>true}
|
22
|
+
}
|
23
|
+
|
24
|
+
serialize_attributes :yaml, :instance_data
|
25
|
+
|
26
|
+
before_create(:set_created_at) do
|
27
|
+
self.updated_at = self.created_at = Time.now
|
28
|
+
end
|
29
|
+
before_update(:set_updated_at) do
|
30
|
+
self.updated_at = Time.now
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/isono/node.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'isono'
|
4
|
+
require 'digest/sha1'
|
5
|
+
|
6
|
+
module Isono
|
7
|
+
# A node instance which joins AMQP network.
|
8
|
+
#
|
9
|
+
class Node
|
10
|
+
include Logger
|
11
|
+
include AmqpClient
|
12
|
+
include EventObservable
|
13
|
+
|
14
|
+
def self.inherited(klass)
|
15
|
+
klass.class_eval {
|
16
|
+
include Logger
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.instance
|
21
|
+
@instance
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.start(manifest, opts)
|
25
|
+
new_instance = proc {
|
26
|
+
stop
|
27
|
+
@instance = new(manifest)
|
28
|
+
@instance.connect(opts[:amqp_server_uri], *opts)
|
29
|
+
}
|
30
|
+
|
31
|
+
if EventMachine.reactor_running?
|
32
|
+
EventMachine.schedule {
|
33
|
+
new_instance.call
|
34
|
+
}
|
35
|
+
else
|
36
|
+
EventMachine.run new_instance
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.stop(&blk)
|
41
|
+
return if @instance.nil?
|
42
|
+
EventMachine.schedule {
|
43
|
+
begin
|
44
|
+
if @instance.connected?
|
45
|
+
@instance.close {
|
46
|
+
blk.call(@instance) if blk
|
47
|
+
}
|
48
|
+
end
|
49
|
+
ensure
|
50
|
+
@instance = nil
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :manifest, :boot_token, :value_objects
|
56
|
+
|
57
|
+
def initialize(manifest)
|
58
|
+
initialize_event_observable
|
59
|
+
raise ArgumentError unless manifest.is_a? Manifest
|
60
|
+
@manifest = manifest
|
61
|
+
@boot_token = Digest::SHA1.hexdigest(Process.pid.to_s)[0,5]
|
62
|
+
@value_objects = {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def node_id
|
66
|
+
manifest.node_id
|
67
|
+
end
|
68
|
+
|
69
|
+
def on_connect
|
70
|
+
raise "node_id is not set" if node_id.nil?
|
71
|
+
|
72
|
+
#amq.prefetch(1)
|
73
|
+
identity_queue(node_id)
|
74
|
+
init_modules
|
75
|
+
|
76
|
+
fire_event(:node_ready, {:node_id=> self.node_id})
|
77
|
+
logger.info("Started : AMQP Server=#{amqp_server_uri.to_s}, ID=#{node_id}, token=#{boot_token}")
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_close
|
81
|
+
term_modules
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def init_modules
|
87
|
+
manifest.node_modules.each { |modclass, *args|
|
88
|
+
if !@value_objects.has_key?(modclass)
|
89
|
+
@value_objects[modclass] = vo = ValueObject.new(self, modclass)
|
90
|
+
|
91
|
+
if modclass.initialize_hook.is_a?(Proc)
|
92
|
+
vo.instance_eval(&modclass.initialize_hook)
|
93
|
+
end
|
94
|
+
|
95
|
+
logger.debug("Initialized #{modclass.to_s}")
|
96
|
+
end
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def term_modules
|
101
|
+
manifest.node_modules.reverse.each { |modclass, *args|
|
102
|
+
vo = @value_objects[modclass]
|
103
|
+
if vo && modclass.terminate_hook.is_a?(Proc)
|
104
|
+
vo.instance_eval(&modclass.terminate_hook)
|
105
|
+
end
|
106
|
+
logger.info("Terminated #{modclass.to_s}")
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
class ValueObject
|
111
|
+
module DelegateMethods
|
112
|
+
def self_class
|
113
|
+
@_tmp[:modclass]
|
114
|
+
end
|
115
|
+
|
116
|
+
def myinstance
|
117
|
+
@_tmp[:myself] ||= self_class.new(self.node)
|
118
|
+
end
|
119
|
+
|
120
|
+
def node
|
121
|
+
@_tmp[:node]
|
122
|
+
end
|
123
|
+
|
124
|
+
def amq
|
125
|
+
self.node.amq
|
126
|
+
end
|
127
|
+
|
128
|
+
def manifest
|
129
|
+
self.node.manifest
|
130
|
+
end
|
131
|
+
|
132
|
+
def config_section
|
133
|
+
manifest.config.send(@_tmp[:modclass].instance_variable_get(:@config_section_name))
|
134
|
+
end
|
135
|
+
|
136
|
+
def logger
|
137
|
+
@_tmp[:modclass].logger
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def initialize(node, modclass)
|
142
|
+
@_tmp = {:node=>node, :modclass=>modclass}
|
143
|
+
end
|
144
|
+
|
145
|
+
include DelegateMethods
|
146
|
+
|
147
|
+
def copy_instance_variables(vo)
|
148
|
+
self.instance_variables.each { |n|
|
149
|
+
next if n == '@_tmp'
|
150
|
+
vo.instance_variable_set(n, self.instance_variable_get(n))
|
151
|
+
}
|
152
|
+
vo
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Isono
|
4
|
+
module NodeModules
|
5
|
+
class Base
|
6
|
+
|
7
|
+
attr_reader :node
|
8
|
+
|
9
|
+
def initialize(node)
|
10
|
+
@node = node
|
11
|
+
|
12
|
+
raise "Module initializer_hook is not run yet" if self.value_object.nil?
|
13
|
+
value_object.copy_instance_variables(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Delegate methods used in subclass frequently.
|
17
|
+
def manifest
|
18
|
+
node.manifest
|
19
|
+
end
|
20
|
+
|
21
|
+
def amq
|
22
|
+
node.amq
|
23
|
+
end
|
24
|
+
|
25
|
+
def value_object
|
26
|
+
node.value_objects[self.class]
|
27
|
+
end
|
28
|
+
|
29
|
+
# shortcut method to lookup configuration section only which
|
30
|
+
# belongs to this class.
|
31
|
+
def config_section
|
32
|
+
node.manifest.config.send(self.class.instance_variable_get(:@config_section_name))
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def initialize_hook(&blk)
|
37
|
+
@initialize_hook = blk if blk
|
38
|
+
@initialize_hook
|
39
|
+
end
|
40
|
+
|
41
|
+
def terminate_hook(&blk)
|
42
|
+
@terminate_hook = blk if blk
|
43
|
+
@terminate_hook
|
44
|
+
end
|
45
|
+
|
46
|
+
def config_section(name=nil, &blk)
|
47
|
+
@config_section_name = name unless name.nil?
|
48
|
+
@config_section_builder = blk
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
def self.inherited(klass)
|
54
|
+
super
|
55
|
+
klass.extend ClassMethods
|
56
|
+
klass.class_eval {
|
57
|
+
# set the default config section name from its class name.
|
58
|
+
# can be overwritten later.
|
59
|
+
@config_section_name = Util.snake_case(self.to_s.split('::').last)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'sequel'
|
4
|
+
|
5
|
+
module Isono
|
6
|
+
module NodeModules
|
7
|
+
class DataStore < Base
|
8
|
+
include Logger
|
9
|
+
|
10
|
+
config_section do
|
11
|
+
desc "Destination database server to be connected."
|
12
|
+
database_dsn ''
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def self.create_instance(node)
|
17
|
+
@instance = self.new(node)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.pass(&blk)
|
21
|
+
@instance.db_writer_thread.pass {
|
22
|
+
@instance.db.transaction {
|
23
|
+
blk.call
|
24
|
+
}
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.barrier(&blk)
|
29
|
+
@instance.db_writer_thread.barrier {
|
30
|
+
@instance.db.transaction {
|
31
|
+
blk.call
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :db_writer_thread, :db
|
37
|
+
|
38
|
+
initialize_hook do
|
39
|
+
@db_writer_thread = ThreadPool.new(1)
|
40
|
+
|
41
|
+
@db_writer_thread.barrier {
|
42
|
+
#@db = Sequel.connect(config_section.database_dsn, {:logger=>logger})
|
43
|
+
@db = Sequel.connect(config_section.database_dsn)
|
44
|
+
logger.debug("connected to the database: #{config_section.database_dsn}, #{@db}")
|
45
|
+
}
|
46
|
+
|
47
|
+
DataStore.create_instance(node)
|
48
|
+
end
|
49
|
+
|
50
|
+
terminate_hook do
|
51
|
+
@db_writer_thread.shutdown
|
52
|
+
@db.disconnect
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Isono
|
4
|
+
module NodeModules
|
5
|
+
# Setup mq based event dispatch channels.
|
6
|
+
# It prepares a single point topic exchange which receives all the
|
7
|
+
# events from publishers. The event consumers setup queues
|
8
|
+
# respectively to bound to the exchange. The most of queues
|
9
|
+
# have the key string which filters the event. The event publisher
|
10
|
+
# puts the key when it sends each new event.
|
11
|
+
#
|
12
|
+
# The key consists of two parts with dot separated:
|
13
|
+
# event type and the event sender (ie. "event1.sender1").
|
14
|
+
# The event publisher can put any format of data into the message
|
15
|
+
# body part.
|
16
|
+
#
|
17
|
+
# These rules make the message queue broker to work as the event
|
18
|
+
# dispacher.
|
19
|
+
class EventChannel < Base
|
20
|
+
AMQP_EXCHANGE='isono.event'
|
21
|
+
|
22
|
+
initialize_hook do
|
23
|
+
amq.topic(AMQP_EXCHANGE, {:auto_delete=>false})
|
24
|
+
end
|
25
|
+
|
26
|
+
# @example
|
27
|
+
# publish('ev/event_name', :args=>[1, 2, 3])
|
28
|
+
def publish(evname, opts={})
|
29
|
+
opts = {:args=>[], :sender=>manifest.node_id}.merge(opts)
|
30
|
+
|
31
|
+
body = {
|
32
|
+
:event => evname,
|
33
|
+
:published_at=> Time.now,
|
34
|
+
:sender => opts[:sender],
|
35
|
+
:origin_node => manifest.node_id,
|
36
|
+
:args => opts[:args]
|
37
|
+
}
|
38
|
+
|
39
|
+
EventMachine.schedule {
|
40
|
+
amq.topic(AMQP_EXCHANGE).publish(Serializer.instance.marshal(body),
|
41
|
+
{:key=>"#{evname}.#{opts[:sender]}"}
|
42
|
+
)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def subscribe(evname, sender, receiver_id=node.node_id, &blk)
|
47
|
+
amq.queue("#{evname}-#{receiver_id}", {:exclusive=>true}).bind(
|
48
|
+
AMQP_EXCHANGE, :key=>"#{evname}.#{sender}"
|
49
|
+
).subscribe { |data|
|
50
|
+
data = Serializer.instance.unmarshal(data)
|
51
|
+
case blk.arity
|
52
|
+
when 2
|
53
|
+
m = data.delete(:args)
|
54
|
+
blk.call(data, m)
|
55
|
+
when 1
|
56
|
+
blk.call(data[:args])
|
57
|
+
else
|
58
|
+
blk.call
|
59
|
+
end
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def unsubscribe(evname, receiver_id=node.node_id)
|
64
|
+
EventMachine.schedule {
|
65
|
+
q = amq.queue("#{evname}-#{receiver_id}")
|
66
|
+
q.unsubscribe
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Isono
|
4
|
+
module NodeModules
|
5
|
+
class EventLogger < Base
|
6
|
+
|
7
|
+
initialize_hook do
|
8
|
+
amq.topic(EventChannel::AMQP_EXCHANGE, {:auto_delete=>true})
|
9
|
+
|
10
|
+
amq.queue("event-logger.#{manifest.node_id}", {:exclusive=>true}).bind(
|
11
|
+
EventChannel::AMQP_EXCHANGE, {:key=>'*.*'}).subscribe { |data|
|
12
|
+
data = Serializer.instance.unmarshal(data)
|
13
|
+
next unless EventLogger.filter_event(data)
|
14
|
+
|
15
|
+
DataStore.pass {
|
16
|
+
Models::EventLog.create(:event=>data[:event],
|
17
|
+
:sender=>data[:sender],
|
18
|
+
:message=>data[:message].inspect,
|
19
|
+
:publised_at=>data[:published_at])
|
20
|
+
}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
terminate_hook do
|
25
|
+
amq.queue("event-logger.#{manifest.node_id}").delete
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def self.filter_event(data)
|
30
|
+
case data[:event]
|
31
|
+
when 'node_collector/pong'
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
return true
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Isono
|
4
|
+
module NodeModules
|
5
|
+
class JobChannel < Base
|
6
|
+
|
7
|
+
# Send a new job request to the endpoint and get back job ID.
|
8
|
+
#
|
9
|
+
# @param [String] endpoint endpoint name created by JobChannel#register_endpoint()
|
10
|
+
# @param [String] command command name in the endpoint.
|
11
|
+
# @param [any] *args arguments to run the job.
|
12
|
+
# @yieldparam [RpcChannel::RequestContext] modify RPC request before sending. The
|
13
|
+
# method will stop the current thread if it does not exist.
|
14
|
+
# @return [Rack::Request,String] Request object if the block is given.
|
15
|
+
#
|
16
|
+
# @example call job endpoint 'endpoint1' and receive
|
17
|
+
# submit('endpoint1', 'command1', 1, 2, 3) #=> Job ID.
|
18
|
+
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
|
31
|
+
}
|
32
|
+
|
33
|
+
blk ? req : req.wait
|
34
|
+
end
|
35
|
+
|
36
|
+
# Send a new job request and wait until the job finished.
|
37
|
+
# The difference to submit() is that this method will stop the
|
38
|
+
# thread until the called job completed.
|
39
|
+
#
|
40
|
+
# @param [String] endpoint endpoint name created by JobChannel#register_endpoint()
|
41
|
+
# @param [String] command command name in the endpoint.
|
42
|
+
# @param [any] *args arguments to run the job.
|
43
|
+
# @yieldparam [RpcChannel::RequestContext] modify RPC request before sending. The
|
44
|
+
# method will stop the current thread if it does not exist.
|
45
|
+
#
|
46
|
+
# @example wait while the job running.
|
47
|
+
# run('endpoint1', 'command1', 1, 2, 3) #=> result of 'endpoint1/command1'.
|
48
|
+
# @example receive progress messages from long running job.
|
49
|
+
# run('endpoint1', 'command1', 1, 2, 3) do |req|
|
50
|
+
# req.on_progress { |r|
|
51
|
+
# puts r #=> show progress message
|
52
|
+
# }
|
53
|
+
# end
|
54
|
+
def run(endpoint, command, *args, &blk)
|
55
|
+
cur_job_ctx = Thread.current[JobWorker::JOB_CTX_KEY]
|
56
|
+
req = rpc.request("job.#{endpoint}", command, *args) { |req|
|
57
|
+
req.request[:job_request_type]=:run
|
58
|
+
|
59
|
+
# A job is working on this current thread if cur_job_ctx is
|
60
|
+
# not nil. Let the new job know the current job ID
|
61
|
+
# as its parent job ID.
|
62
|
+
if cur_job_ctx
|
63
|
+
req.request[:parent_job_id] = cur_job_ctx.job_id
|
64
|
+
end
|
65
|
+
|
66
|
+
blk ? blk.call(req) : req.synchronize
|
67
|
+
}
|
68
|
+
|
69
|
+
blk ? req : req.wait
|
70
|
+
end
|
71
|
+
|
72
|
+
def cancel()
|
73
|
+
end
|
74
|
+
|
75
|
+
def register_endpoint(endpoint, app)
|
76
|
+
rpc.register_endpoint("job.#{endpoint}", Rack::Job.new(app, JobWorker.new(node)))
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def rpc
|
81
|
+
@rpc ||= RpcChannel.new(@node)
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|