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