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,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
@@ -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