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