smith 0.5.7

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 (50) hide show
  1. data/bin/agency +55 -0
  2. data/bin/smithctl +102 -0
  3. data/lib/smith.rb +237 -0
  4. data/lib/smith/acl_compiler.rb +74 -0
  5. data/lib/smith/agent.rb +207 -0
  6. data/lib/smith/agent_cache.rb +40 -0
  7. data/lib/smith/agent_config.rb +22 -0
  8. data/lib/smith/agent_monitoring.rb +52 -0
  9. data/lib/smith/agent_process.rb +181 -0
  10. data/lib/smith/application/agency.rb +126 -0
  11. data/lib/smith/bootstrap.rb +153 -0
  12. data/lib/smith/cache.rb +61 -0
  13. data/lib/smith/command.rb +128 -0
  14. data/lib/smith/commands/agency/agents.rb +28 -0
  15. data/lib/smith/commands/agency/common.rb +18 -0
  16. data/lib/smith/commands/agency/kill.rb +13 -0
  17. data/lib/smith/commands/agency/list.rb +65 -0
  18. data/lib/smith/commands/agency/logger.rb +56 -0
  19. data/lib/smith/commands/agency/metadata.rb +14 -0
  20. data/lib/smith/commands/agency/restart.rb +39 -0
  21. data/lib/smith/commands/agency/start.rb +62 -0
  22. data/lib/smith/commands/agency/state.rb +14 -0
  23. data/lib/smith/commands/agency/stop.rb +70 -0
  24. data/lib/smith/commands/agency/version.rb +23 -0
  25. data/lib/smith/commands/smithctl/cat.rb +70 -0
  26. data/lib/smith/commands/smithctl/pop.rb +76 -0
  27. data/lib/smith/commands/smithctl/rm.rb +36 -0
  28. data/lib/smith/commands/smithctl/smithctl_version.rb +23 -0
  29. data/lib/smith/commands/smithctl/top.rb +42 -0
  30. data/lib/smith/commands/template.rb +9 -0
  31. data/lib/smith/config.rb +32 -0
  32. data/lib/smith/logger.rb +91 -0
  33. data/lib/smith/messaging/acl/agency_command.proto +5 -0
  34. data/lib/smith/messaging/acl/agent_command.proto +5 -0
  35. data/lib/smith/messaging/acl/agent_config_request.proto +4 -0
  36. data/lib/smith/messaging/acl/agent_config_update.proto +5 -0
  37. data/lib/smith/messaging/acl/agent_keepalive.proto +6 -0
  38. data/lib/smith/messaging/acl/agent_lifecycle.proto +12 -0
  39. data/lib/smith/messaging/acl/agent_stats.proto +14 -0
  40. data/lib/smith/messaging/acl/default.rb +51 -0
  41. data/lib/smith/messaging/acl/search.proto +9 -0
  42. data/lib/smith/messaging/amqp_options.rb +55 -0
  43. data/lib/smith/messaging/endpoint.rb +116 -0
  44. data/lib/smith/messaging/exceptions.rb +7 -0
  45. data/lib/smith/messaging/payload.rb +102 -0
  46. data/lib/smith/messaging/queue_factory.rb +67 -0
  47. data/lib/smith/messaging/receiver.rb +237 -0
  48. data/lib/smith/messaging/responder.rb +15 -0
  49. data/lib/smith/messaging/sender.rb +61 -0
  50. metadata +239 -0
@@ -0,0 +1,126 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'dm-core'
3
+
4
+ module Smith
5
+ class Agency
6
+
7
+ include Logger
8
+
9
+ attr_reader :agents, :agent_processes
10
+
11
+ def initialize(opts={})
12
+ DataMapper.setup(:default, "yaml:///#{Smith.config.agency.cache_path}")
13
+
14
+ @agent_processes = AgentCache.new(:paths => opts.delete(:paths))
15
+ end
16
+
17
+ def setup_queues
18
+ Messaging::Receiver.new('agency.control', :auto_delete => true, :durable => false, :strict => true).ready do |receiver|
19
+ receiver.subscribe do |r|
20
+ r.reply do |responder|
21
+ # Add a logger proc to the responder chain.
22
+ responder.callback { |ret| logger.debug { ret } if ret && !ret.empty? }
23
+
24
+ begin
25
+ Command.run(r.payload.command, r.payload.args, :agency => self, :agents => @agent_processes, :responder => responder)
26
+ rescue Command::UnkownCommandError => e
27
+ responder.value("Unknown command: #{r.payload.command}")
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Messaging::Receiver.new('agent.lifecycle', :auto_delete => true, :durable => false).ready do |receiver|
34
+ receiver.subscribe do |r|
35
+ case r.payload.state
36
+ when 'dead'
37
+ dead(r.payload)
38
+ when 'acknowledge_start'
39
+ acknowledge_start(r.payload)
40
+ when 'acknowledge_stop'
41
+ acknowledge_stop(r.payload)
42
+ else
43
+ logger.warn { "Unkown command received on agent.lifecycle queue: #{r.payload.state}" }
44
+ end
45
+ end
46
+ end
47
+
48
+ Messaging::Receiver.new('agent.keepalive', :auto_delete => true, :durable => false).ready do |receiver|
49
+ receiver.subscribe do |r|
50
+ keep_alive(r.payload)
51
+ end
52
+ end
53
+ end
54
+
55
+ def start_monitoring
56
+ @agent_monitor = AgentMonitoring.new(@agent_processes)
57
+ @agent_monitor.start_monitoring
58
+ end
59
+
60
+ # Stop the agency. This will wait for one second to ensure
61
+ # that any messages are flushed.
62
+ def stop
63
+ Smith.stop(true)
64
+ end
65
+
66
+ private
67
+
68
+ def acknowledge_start(agent_data)
69
+ @agent_processes[agent_data.name].tap do |agent_process|
70
+ if agent_data.pid == agent_process.pid
71
+ agent_process.monitor = agent_data.monitor
72
+ agent_process.singleton = agent_data.singleton
73
+ agent_process.metadata = agent_data.metadata
74
+ agent_process.acknowledge_start
75
+ else
76
+ logger.error { "Agent reports different pid during acknowledge_start: #{agent_data.name}" }
77
+ end
78
+ end
79
+ end
80
+
81
+ def acknowledge_stop(agent_data)
82
+ @agent_processes[agent_data.name].tap do |agent_process|
83
+ if agent_data.pid == agent_process.pid
84
+ agent_process.pid = nil
85
+ agent_process.monitor = nil
86
+ agent_process.singleton = nil
87
+ agent_process.started_at = nil
88
+ agent_process.last_keep_alive = nil
89
+ agent_process.acknowledge_stop
90
+ else
91
+ if agent_process.pid
92
+ logger.error { "Agent reports different pid during acknowledge_stop: #{agent_data.name}" }
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def dead(agent_data)
99
+ @agent_processes[agent_data.name].no_process_running
100
+ logger.fatal { "Agent is dead: #{agent_data.name}" }
101
+ end
102
+
103
+ def keep_alive(agent_data)
104
+ @agent_processes[agent_data.name].tap do |agent_process|
105
+ if agent_data.pid == agent_process.pid
106
+ agent_process.last_keep_alive = agent_data.time
107
+ logger.verbose { "Agent keep alive: #{agent_data.name}: #{agent_data.time}" }
108
+
109
+ # We need to call save explicitly here as the keep alive is not part of
110
+ # the state_machine which is the thing that writes the state to disc.
111
+ agent_process.save
112
+ else
113
+ if agent_process.pid
114
+ logger.error { "Agent reports different pid during acknowledge_stop: #{agent_data.name}" }
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ # FIXME this doesn't work.
121
+ def delete_agent_process(agent_pid)
122
+ @agent_processes.invalidate(agent_pid)
123
+ AgentProcess.first('pid' => agent_pid).destroy!
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,153 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # This should never be run directly it should only ever be run by
4
+ # the agency.
5
+
6
+ $: << File.dirname(__FILE__) + '/..'
7
+
8
+ require 'smith'
9
+
10
+ module Smith
11
+ class AgentBootstrap
12
+
13
+ attr_reader :agent
14
+
15
+ include Logger
16
+
17
+ def initialize(path, agent_name)
18
+ # FIXME
19
+ # This doesn't do what I think it should. If an exception is
20
+ # thrown in setup_control_queue, for example, it just kills
21
+ # the agent without it actually raising the exception.
22
+ Thread.abort_on_exception = true
23
+ @agent_name = agent_name
24
+ @agent_filename = Pathname.new(path).join("#{agent_name.snake_case}.rb").expand_path
25
+ end
26
+
27
+ def signal_handlers
28
+ logger.debug { "Installing default signal handlers" }
29
+ %w{TERM INT QUIT}.each do |sig|
30
+ @agent.install_signal_handler(sig) do |sig|
31
+ logger.error { "Agent received: signal #{sig}: #{agent.name}" }
32
+ terminate!
33
+ end
34
+ end
35
+ end
36
+
37
+ def load_agent
38
+ logger.debug { "Loading #{@agent_name} from: #{@agent_filename.dirname}" }
39
+ add_agent_load_path
40
+ load @agent_filename
41
+ @agent = Kernel.const_get(@agent_name).new
42
+ end
43
+
44
+ def start!
45
+ write_pid_file
46
+ @agent.run
47
+ @agent.started
48
+ end
49
+
50
+ # Exceptional shutdown of the agent. Note. Whenever this is
51
+ # called it almost certain that the reactor is not going to
52
+ # be running. So it must be restarted and then shutdown again
53
+ # See the note at the in main.
54
+ def terminate!(exception=nil)
55
+ logger.error { format_exception(exception) } if exception
56
+ logger.error { "Terminating: #{@agent_name}." }
57
+
58
+ if Smith.running?
59
+ send_dead_message
60
+ unlink_pid_file
61
+ Smith.stop
62
+ else
63
+ logger.debug { "Reconnecting to AMQP Broker." }
64
+ Smith.start do
65
+ send_dead_message
66
+ unlink_pid_file
67
+ Smith.stop
68
+ end
69
+ end
70
+ end
71
+
72
+ # Clean shutdown of the agent.
73
+ def shutdown
74
+ unlink_pid_file
75
+ Smith.stop if Smith.running?
76
+ end
77
+
78
+ private
79
+
80
+ def write_pid_file
81
+ @pid = Daemons::PidFile.new(Daemons::Pid.dir(:normal, Dir::tmpdir, nil), ".rubymas-#{@agent_name.snake_case}", true)
82
+ @pid.pid = Process.pid
83
+ end
84
+
85
+ def send_dead_message
86
+ logger.debug { "Sending dead message to agency: #{@agent_name}" }
87
+ Messaging::Sender.new('agent.lifecycle', :auto_delete => true, :durable => false).ready do |sender|
88
+ sender.publish(ACL::Payload.new(:agent_lifecycle).content(:state => 'dead', :name => @agent_name))
89
+ end
90
+ end
91
+
92
+ def unlink_pid_file
93
+ if @pid && @pid.exist?
94
+ logger.debug { "Cleaning up pid file: #{@pid.filename}" }
95
+ end
96
+ end
97
+
98
+ def format_exception(exception)
99
+ str = "#{exception.class.to_s}: #{exception.message}\n\t"
100
+ if exception.backtrace
101
+ str << exception.backtrace[0..-1].join("\n\t")
102
+ end
103
+ str
104
+ end
105
+
106
+ # Add the ../lib to the load path. This assumes the directory
107
+ # structure is:
108
+ #
109
+ # $ROOT_PATH/agents
110
+ # .../lib
111
+ #
112
+ # where $ROOT_PATH can be anywhere.
113
+ #
114
+ # This needs to be better thought out.
115
+ # TODO think this through some more.
116
+ def add_agent_load_path
117
+ path = @agent_filename.dirname.dirname.join('lib')
118
+ # The load path may be a pathname or a string. Change to strings.
119
+ unless $:.detect { |p| p.to_s == path.to_s }
120
+ logger.debug { "Adding #{path} to load path" }
121
+ $: << path
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ path = ARGV[0]
128
+ agent_name = ARGV[1]
129
+
130
+ exit 1 if agent_name.nil? || path.nil?
131
+
132
+ # Set the running instance name to the name of the agent.
133
+ $0 = "#{agent_name}"
134
+
135
+ # load the acls
136
+ Smith.load_acls
137
+
138
+ bootstrapper = Smith::AgentBootstrap.new(path, agent_name)
139
+
140
+ # I've tried putting the exception handling in the main reactor loog
141
+ # but it doesn't do anything. I know theres a resaon for this but I
142
+ # don't what it is at the moment. Just beware that whenever there
143
+ # is an exception the recator is not going going to be running.
144
+ begin
145
+ Smith.start do
146
+ bootstrapper.load_agent
147
+ bootstrapper.signal_handlers
148
+ bootstrapper.start!
149
+ end
150
+ bootstrapper.shutdown
151
+ rescue Exception => e
152
+ bootstrapper.terminate!(e)
153
+ end
@@ -0,0 +1,61 @@
1
+ # -*- encoding: utf-8 -*-
2
+ module Smith
3
+ class Cache
4
+
5
+ include Enumerable
6
+
7
+ def initialize
8
+ @cache = {}
9
+ end
10
+
11
+ def operator(operator)
12
+ @operator = operator
13
+ end
14
+
15
+ def entry(name, options=nil)
16
+ if @cache[name]
17
+ @cache[name]
18
+ else
19
+ if @operator.respond_to?(:call)
20
+ @cache[name] = @operator.call(name, options)
21
+ else
22
+ nil
23
+ end
24
+ end
25
+ end
26
+
27
+ alias :[] :entry
28
+
29
+ def entries
30
+ @cache.keys.map(&:to_s)
31
+ end
32
+
33
+ def invalidate(name)
34
+ @cache.delete(name)
35
+ end
36
+
37
+ def each
38
+ @cache.each_value { |v| yield v }
39
+ end
40
+
41
+ def empty?
42
+ @cache.empty?
43
+ end
44
+
45
+ def exist?(name)
46
+ !@cache[name].nil?
47
+ end
48
+
49
+ def size
50
+ @cache.size
51
+ end
52
+
53
+ def to_s
54
+ @cache.to_s
55
+ end
56
+
57
+ def update(name, entry)
58
+ @cache[name] = entry
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,128 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'trollop'
4
+
5
+ module Smith
6
+ class Command
7
+ class UnkownCommandError < RuntimeError; end
8
+
9
+ include Logger
10
+
11
+ # Load and run the command specified. This method takes:
12
+ # +target+ which is either a list of agents or the agency
13
+ # +vars+ variables to be passed in to the command. This takes the
14
+ # form of a hash and accessor methods are generated named after the
15
+ # key of the hash.
16
+
17
+ def self.run(command, args, vars)
18
+ # Change _ to - underscores look so ugly as a command name.
19
+ command = command.gsub(/-/, '_')
20
+ logger.debug { "Agency command: #{command}#{(args.empty?) ? '' : " #{args.join(', ')}"}." }
21
+
22
+ load_command(command)
23
+
24
+ clazz = Commands.const_get(Extlib::Inflection.camelize(command)).new
25
+
26
+ begin
27
+ options, target = parse_options(clazz, args)
28
+
29
+ vars.merge(:options => options, :target => target).each do |k,v|
30
+ clazz.instance_eval <<-EOM, __FILE__, __LINE__ + 1
31
+ instance_variable_set(:"@#{k}", v)
32
+ def #{k}=(z); @#{k} = z; end
33
+ def #{k}; @#{k}; end
34
+ EOM
35
+ end
36
+
37
+ clazz.execute
38
+
39
+ rescue Trollop::CommandlineError => e
40
+ vars[:responder].value(parser_help(clazz, :prefix => "Error: #{e.message}.\n"))
41
+ rescue Trollop::HelpNeeded
42
+ vars[:responder].value(parser_help(clazz))
43
+ end
44
+ end
45
+
46
+ # Banner callback. This is called when the parser is called.
47
+ # I'm sure there is a better way of doing this.
48
+ def self.banner(command)
49
+ "smithctl #{command} OPTIONS [Agents]"
50
+ end
51
+
52
+ private
53
+
54
+ # Determine whether the command is an agency or smithctl command and load
55
+ # accordingly.
56
+ def self.load_command(cmd)
57
+ require command_path(cmd)
58
+ end
59
+
60
+ # Check to see if the command is an agency or smithctl command.
61
+ def self.agency?
62
+ Smith.constants.include?(:Agency)
63
+ end
64
+
65
+ # Return the full path of the ruby class.
66
+ def self.command_path(command)
67
+ send("#{command_type(command)}_path").join(command)
68
+ end
69
+
70
+ # What type of command is it?
71
+ def self.command_type(command)
72
+ case
73
+ when agency_command?(command)
74
+ :agency
75
+ when smithctl_command?(command)
76
+ :smithctl
77
+ else
78
+ raise UnkownCommandError, "Unknown command: #{command}"
79
+ end
80
+ end
81
+
82
+ # Is the command an agency command?
83
+ def self.agency_command?(cmd)
84
+ agency_path.join(cmd).sub_ext('.rb').exist?
85
+ end
86
+
87
+ # Is the command a smithctl command?
88
+ def self.smithctl_command?(cmd)
89
+ smithctl_path.join(cmd).sub_ext('.rb').exist?
90
+ end
91
+
92
+ # Return the agency command base path.
93
+ def self.agency_path
94
+ base_path.join('agency')
95
+ end
96
+
97
+ # Return the smithctl command base path.
98
+ def self.smithctl_path
99
+ base_path.join('smithctl')
100
+ end
101
+
102
+ # Return the command base path.
103
+ def self.base_path
104
+ @c64a6f4f ||= Smith.root_path.join('lib').join("smith").join('commands')
105
+ end
106
+
107
+ # Uses the options_parser method in the specific command class to procees
108
+ # any options associated with that command. If no options_parser method
109
+ # exits then an empty Array is returned. Any members of the args array
110
+ # that are not parsed by the options parser are return as the target, i.e.
111
+ # the agent(s) that the command is to operate on.
112
+ def self.parse_options(clazz, args)
113
+ if clazz.respond_to?(:options_parser)
114
+ [clazz.options_parser.parse(args), args]
115
+ else
116
+ [{}, args]
117
+ end
118
+ end
119
+
120
+ def self.parser_help(clazz, opts={})
121
+ StringIO.new.tap do |help|
122
+ help.puts opts[:prefix] if opts[:prefix]
123
+ clazz.options_parser.educate(help)
124
+ help.rewind
125
+ end.read
126
+ end
127
+ end
128
+ end