cognizant 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -0
  4. data/Gemfile +4 -1
  5. data/{LICENSE → License.md} +4 -2
  6. data/Rakefile +5 -0
  7. data/Readme.md +95 -0
  8. data/bin/cognizant +76 -122
  9. data/bin/cognizantd +28 -61
  10. data/cognizant.gemspec +8 -4
  11. data/examples/apps/redis-server.cz +42 -0
  12. data/examples/apps/redis-server.yml +29 -0
  13. data/examples/apps/redis-server_dsl.cz +54 -0
  14. data/examples/apps/resque.cz +17 -0
  15. data/examples/apps/thin.cz +32 -0
  16. data/examples/apps/thin.yml +48 -0
  17. data/examples/cognizantd.yml +18 -47
  18. data/features/child_process.feature +62 -0
  19. data/features/commands.feature +65 -0
  20. data/features/cpu_usage.feature +45 -0
  21. data/features/daemon.feature +12 -0
  22. data/features/flapping.feature +39 -0
  23. data/features/memory_usage.feature +45 -0
  24. data/features/shell.feature +30 -0
  25. data/features/step_definitions/common_steps.rb +14 -0
  26. data/features/step_definitions/daemon_steps.rb +25 -0
  27. data/features/step_definitions/shell_steps.rb +96 -0
  28. data/features/support/env.rb +54 -0
  29. data/lib/cognizant.rb +1 -5
  30. data/lib/cognizant/application.rb +122 -0
  31. data/lib/cognizant/application/dsl_proxy.rb +23 -0
  32. data/lib/cognizant/client.rb +61 -0
  33. data/lib/cognizant/commands.rb +164 -0
  34. data/lib/cognizant/commands/actions.rb +30 -0
  35. data/lib/cognizant/commands/help.rb +10 -0
  36. data/lib/cognizant/commands/load.rb +10 -0
  37. data/lib/cognizant/commands/shutdown.rb +7 -0
  38. data/lib/cognizant/commands/status.rb +11 -0
  39. data/lib/cognizant/commands/use.rb +15 -0
  40. data/lib/cognizant/controller.rb +17 -0
  41. data/lib/cognizant/daemon.rb +279 -0
  42. data/lib/cognizant/interface.rb +17 -0
  43. data/lib/cognizant/log.rb +25 -0
  44. data/lib/cognizant/process.rb +138 -94
  45. data/lib/cognizant/process/actions.rb +30 -41
  46. data/lib/cognizant/process/actions/restart.rb +73 -17
  47. data/lib/cognizant/process/actions/start.rb +35 -12
  48. data/lib/cognizant/process/actions/stop.rb +38 -17
  49. data/lib/cognizant/process/attributes.rb +41 -10
  50. data/lib/cognizant/process/children.rb +36 -0
  51. data/lib/cognizant/process/{condition_check.rb → condition_delegate.rb} +11 -13
  52. data/lib/cognizant/process/conditions.rb +7 -4
  53. data/lib/cognizant/process/conditions/cpu_usage.rb +5 -6
  54. data/lib/cognizant/process/conditions/memory_usage.rb +2 -6
  55. data/lib/cognizant/process/dsl_proxy.rb +23 -0
  56. data/lib/cognizant/process/execution.rb +16 -9
  57. data/lib/cognizant/process/pid.rb +16 -6
  58. data/lib/cognizant/process/status.rb +14 -2
  59. data/lib/cognizant/process/trigger_delegate.rb +57 -0
  60. data/lib/cognizant/process/triggers.rb +19 -0
  61. data/lib/cognizant/process/triggers/flapping.rb +68 -0
  62. data/lib/cognizant/process/triggers/transition.rb +22 -0
  63. data/lib/cognizant/process/triggers/trigger.rb +15 -0
  64. data/lib/cognizant/shell.rb +142 -0
  65. data/lib/cognizant/system.rb +16 -0
  66. data/lib/cognizant/system/ps.rb +1 -1
  67. data/lib/cognizant/system/signal.rb +2 -2
  68. data/lib/cognizant/util/dsl_proxy_methods_handler.rb +25 -0
  69. data/lib/cognizant/util/fixnum_percent.rb +5 -0
  70. data/lib/cognizant/util/transform_hash_keys.rb +33 -0
  71. data/lib/cognizant/validations.rb +142 -142
  72. data/lib/cognizant/version.rb +1 -1
  73. metadata +131 -71
  74. data/README.md +0 -221
  75. data/examples/redis-server.rb +0 -28
  76. data/examples/resque.rb +0 -10
  77. data/images/logo-small.png +0 -0
  78. data/images/logo.png +0 -0
  79. data/images/logo.pxm +0 -0
  80. data/lib/cognizant/logging.rb +0 -33
  81. data/lib/cognizant/process/conditions/flapping.rb +0 -57
  82. data/lib/cognizant/process/conditions/trigger_condition.rb +0 -52
  83. data/lib/cognizant/server.rb +0 -14
  84. data/lib/cognizant/server/commands.rb +0 -80
  85. data/lib/cognizant/server/daemon.rb +0 -277
  86. data/lib/cognizant/server/interface.rb +0 -86
  87. data/lib/cognizant/util/symbolize_hash_keys.rb +0 -19
@@ -1,24 +1,34 @@
1
1
  module Cognizant
2
2
  class Process
3
3
  module PID
4
+ def cached_pid
5
+ if not @process_pid or @process_pid == 0
6
+ read_pid
7
+ else
8
+ @process_pid
9
+ end
10
+ end
11
+
4
12
  def read_pid
5
13
  if self.pid_command
6
14
  str = execute(self.pid_command).stdout.to_i
7
- @process_pid = str unless not str or str.zero?
15
+ process_pid = str unless not str or str.zero?
8
16
  # TODO: Write pid to pidfile, since our source was pid_command instead.
9
17
  elsif self.pidfile and File.exists?(self.pidfile)
10
18
  str = File.read(self.pidfile).to_i
11
- @process_pid = str unless not str or str.zero?
19
+ process_pid = str unless not str or str.zero?
12
20
  end
13
- @process_pid = 0 unless System.pid_running?(@process_pid)
14
- @process_pid
21
+ process_pid = 0 unless Cognizant::System.pid_running?(process_pid) # If the newly fetched pid is not running, reset it.
22
+ @process_pid = process_pid
15
23
  end
16
24
 
25
+ # Note: Expected that this method is not called to overwrite pidfile if the process daemonizes itself (and hence manages the pidfile by itself).
17
26
  def write_pid(pid = nil)
18
- @process_pid = pid if pid
19
- File.open(self.pidfile, "w") { |f| f.write(@process_pid) } if self.pidfile and @process_pid
27
+ @process_pid = pid
28
+ File.open(self.pidfile, "w") { |f| f.write(@process_pid) } if self.pidfile and @process_pid and @process_pid != 0
20
29
  end
21
30
 
31
+ # Note: Expected that this method is not called to unlink pidfile if the process daemonizes itself (and hence manages the pidfile by itself).
22
32
  def unlink_pid
23
33
  File.unlink(self.pidfile) if self.pidfile
24
34
  rescue Errno::ENOENT
@@ -3,12 +3,24 @@ require "cognizant/system"
3
3
  module Cognizant
4
4
  class Process
5
5
  module Status
6
+ def process_running?
7
+ @process_running = begin
8
+ if @ping_command and run(@ping_command).succeeded?
9
+ true
10
+ elsif pid_running?
11
+ true
12
+ else
13
+ false
14
+ end
15
+ end
16
+ end
17
+
6
18
  def pid_running?
7
- Cognizant::System.pid_running?(read_pid)
19
+ Cognizant::System.pid_running?(cached_pid)
8
20
  end
9
21
 
10
22
  def signal(signal, pid = nil)
11
- Cognizant::System.signal(signal, (pid || read_pid))
23
+ Cognizant::System.signal(signal, (pid || cached_pid))
12
24
  end
13
25
  end
14
26
  end
@@ -0,0 +1,57 @@
1
+ require "cognizant/process/triggers"
2
+
3
+ module Cognizant
4
+ class Process
5
+ class TriggerDelegate
6
+ attr_accessor :name, :process, :mutex, :scheduled_events
7
+
8
+ def initialize(name, process, options = {}, &block)
9
+ @name, @process = name, process
10
+ @mutex = Mutex.new
11
+ @scheduled_events = []
12
+
13
+ @trigger = Cognizant::Process::Triggers[@name].new(options, &block)
14
+ # TODO: This is hackish even though it keeps trigger implementations simple.
15
+ @trigger.instance_variable_set(:@delegate, self)
16
+ end
17
+
18
+ def notify(transition)
19
+ @trigger.notify(transition)
20
+ end
21
+
22
+ def reset!
23
+ @trigger.reset!
24
+ self.cancel_all_events
25
+ end
26
+
27
+ def dispatch!(event)
28
+ @process.dispatch!(event, @name)
29
+ end
30
+
31
+ def schedule_event(event, delay)
32
+ # TODO: Maybe wrap this in a ScheduledEvent class with methods like cancel.
33
+ thread = Thread.new(self) do |trigger|
34
+ begin
35
+ sleep(delay)
36
+ trigger.dispatch!(event)
37
+ trigger.mutex.synchronize do
38
+ trigger.scheduled_events.delete_if { |_, thread| thread == Thread.current }
39
+ end
40
+ rescue StandardError => e
41
+ puts(e)
42
+ puts(e.backtrace.join("\n"))
43
+ end
44
+ end
45
+
46
+ @scheduled_events.push([event, thread])
47
+ end
48
+
49
+ def cancel_all_events
50
+ Log[self].debug "Canceling all scheduled events"
51
+ @mutex.synchronize do
52
+ @scheduled_events.each {|_, thread| thread.kill}
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ require "cognizant/process/triggers/trigger"
2
+
3
+ Dir["#{File.dirname(__FILE__)}/triggers/*.rb"].each do |trigger|
4
+ require trigger
5
+ end
6
+
7
+ module Cognizant
8
+ class Process
9
+ module Triggers
10
+ def self.[](name)
11
+ begin
12
+ const_get(name.to_s.camelcase)
13
+ rescue NameError
14
+ nil
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,68 @@
1
+ require "cognizant/util/rotational_array"
2
+
3
+ module Cognizant
4
+ class Process
5
+ module Triggers
6
+ class Flapping < Trigger
7
+ TRIGGER_STATES = [:starting, :restarting]
8
+
9
+ attr_accessor :times, :within, :retry_after, :retries
10
+ attr_reader :timeline
11
+
12
+ def initialize(options = {})
13
+ @times = options[:times] || 5
14
+ @within = options[:within] || 1
15
+ @retry_after = options[:retry_after] || 5
16
+ @retries = options[:retries] || 0
17
+
18
+ @timeline = Util::RotationalArray.new(@times)
19
+ @num_of_tries = 0
20
+ end
21
+
22
+ def notify(transition)
23
+ if TRIGGER_STATES.include?(transition.to_name)
24
+ self.timeline << Time.now.to_i
25
+ self.check_flapping
26
+ end
27
+ end
28
+
29
+ def reset!
30
+ @timeline.clear
31
+ @num_of_tries = 0
32
+ end
33
+
34
+ def within_duration?
35
+ (@timeline.last - @timeline.first) <= self.within
36
+ end
37
+
38
+ def can_retry?
39
+ # retry_after = 0 means do not retry.
40
+ self.retry_after > 0 and
41
+ # retries = 0 means always retry.
42
+ (self.retries == 0 or (self.retries > 0 and @num_of_tries <= self.retries))
43
+ end
44
+
45
+ def check_flapping
46
+ # The process has not flapped if we haven't encountered enough incidents.
47
+ return unless (@timeline.compact.length == self.times)
48
+
49
+ # Check if the incident happend within the timeframe.
50
+ if within_duration?
51
+ @num_of_tries += 1
52
+
53
+ Log[self].debug "Flapping detected (##{@num_of_tries}) for #{@delegate.process.name}(pid:#{@delegate.process.cached_pid})."
54
+
55
+ # 0.1 to ensure the state isn't randomly caught in throw :halt below.
56
+ @delegate.schedule_event(:unmonitor, [0.1, self.retry_after].min)
57
+ @delegate.schedule_event(:start, self.retry_after) if can_retry?
58
+
59
+ @timeline.clear
60
+
61
+ # This will prevent a transition from happening in the process state_machine.
62
+ throw :halt
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ module Cognizant
2
+ class Process
3
+ module Triggers
4
+ class Transition < Trigger
5
+ def initialize(options = {}, &block)
6
+ @from = [*options[:from]]
7
+ @to = [*options[:to]]
8
+ @do = block
9
+ end
10
+
11
+ def notify(transition)
12
+ if @from.include?(transition.from_name) and @to.include?(transition.to_name)
13
+ @do.call(@delegate.process) if @do and @do.respond_to?(:call)
14
+ end
15
+ end
16
+
17
+ def reset!
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module Cognizant
2
+ class Process
3
+ module Triggers
4
+ class Trigger
5
+ def notify(transition)
6
+ raise "Implement in subclass"
7
+ end
8
+
9
+ def reset!
10
+ raise "Implement in subclass"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,142 @@
1
+ require "logger"
2
+ require "optparse"
3
+
4
+ require "readline"
5
+ require "shellwords"
6
+
7
+ require "cognizant/client"
8
+
9
+ module Cognizant
10
+ class Shell
11
+ def initialize(options = {})
12
+ @app = ""
13
+ @app = options[:app] if options.has_key?(:app) and options[:app].to_s.size > 0
14
+
15
+ @path_to_socket = "/var/run/cognizant/cognizantd.sock"
16
+ @path_to_socket = options[:socket] if options.has_key?(:socket) and options[:socket].to_s.size > 0
17
+
18
+ @@is_shell = true
19
+ @@is_shell = options[:shell] if options.has_key?(:shell)
20
+
21
+ @autocomplete_keywords = []
22
+ connect
23
+ end
24
+
25
+ def run(&block)
26
+ Signal.trap("INT") do
27
+ Cognizant::Shell.emit("\nGoodbye!")
28
+ exit(0)
29
+ end
30
+
31
+ emit("Enter 'help' if you're not sure what to do.")
32
+ emit
33
+ emit("Type 'quit' or 'exit' to quit at any time.")
34
+
35
+ setup_readline(&block)
36
+ end
37
+
38
+ def setup_readline(&block)
39
+ Readline.completion_proc = Proc.new do |input|
40
+ case input
41
+ when /^\//
42
+ # Handle file and directory name autocompletion.
43
+ Readline.completion_append_character = "/"
44
+ Dir[input + '*'].grep(/^#{Regexp.escape(input)}/)
45
+ else
46
+ # Handle commands and process name autocompletion.
47
+ Readline.completion_append_character = " "
48
+ (@autocomplete_keywords + ['quit', 'exit']).grep(/^#{Regexp.escape(input)}/)
49
+ end
50
+ end
51
+
52
+ while line = Readline.readline(prompt, true).to_s.strip
53
+ if line.size > 0
54
+ command, args = parse_command(line)
55
+ return emit("Goodbye!") if ['quit', 'exit'].include?(command)
56
+ run_command(command, args, &block)
57
+ end
58
+ end
59
+ end
60
+
61
+ def prompt
62
+ @app.to_s.size > 0 ? "(#{@app})> " : "> "
63
+ end
64
+
65
+ def run_command(command, args, &block)
66
+ command = command.to_s
67
+
68
+ begin
69
+ response = @client.command({'command' => command, 'args' => args, 'app' => @app})
70
+ rescue Errno::EPIPE => e
71
+ emit("cognizant: Error communicating with cognizantd: #{e} (#{e.class})")
72
+ exit(1)
73
+ end
74
+
75
+ @app = response["use"] if response.is_a?(Hash) and response.has_key?("use")
76
+
77
+ if block
78
+ block.call(response, command)
79
+ elsif response.kind_of?(Hash)
80
+ puts response['message']
81
+ else
82
+ puts "Invalid response type #{response.class}: #{response.inspect}"
83
+ end
84
+
85
+ fetch_autocomplete_keywords
86
+ end
87
+
88
+ def parse_command(line)
89
+ command, *args = Shellwords.shellsplit(line)
90
+ [command, args]
91
+ end
92
+
93
+ def connect
94
+ begin
95
+ @client = Cognizant::Client.for_path(@path_to_socket)
96
+ rescue Errno::ENOENT => e
97
+ # TODO: The exit here is a biit of a layering violation.
98
+ Cognizant::Shell.emit(<<EOF, true)
99
+ Could not connect to Cognizant daemon process:
100
+
101
+ #{e}
102
+
103
+ HINT: Are you sure you are running the Cognizant daemon? If so, you
104
+ should pass cognizant the socket argument provided to cognizantd.
105
+ EOF
106
+ exit(1)
107
+ end
108
+ ehlo if interactive?
109
+ fetch_autocomplete_keywords
110
+ end
111
+
112
+ def ehlo
113
+ response = @client.command('command' => '_ehlo', 'user' => ENV['USER'], 'app' => @app)
114
+ @app = response["use"] if response.is_a?(Hash) and response.has_key?("use")
115
+
116
+ emit(response['message'])
117
+ end
118
+
119
+ def fetch_autocomplete_keywords
120
+ return unless @@is_shell
121
+ @autocomplete_keywords = @client.command('command' => '_autocomplete_keywords', 'app' => @app)
122
+ end
123
+
124
+ def self.emit(message = nil, force = false)
125
+ $stdout.puts(message || '') if interactive? || force
126
+ end
127
+
128
+ def self.interactive?
129
+ # TODO: It is not a tty during tests.
130
+ # $stdin.isatty and @@is_shell
131
+ @@is_shell
132
+ end
133
+
134
+ def emit(*args)
135
+ self.class.emit(*args)
136
+ end
137
+
138
+ def interactive?
139
+ self.class.interactive?
140
+ end
141
+ end
142
+ end
@@ -1,3 +1,5 @@
1
+ require "fileutils"
2
+
1
3
  require "cognizant/system/signal"
2
4
  require "cognizant/system/ps"
3
5
 
@@ -24,5 +26,19 @@ module Cognizant
24
26
  # Possibly running.
25
27
  true
26
28
  end
29
+
30
+ def unlink_file(path)
31
+ begin
32
+ File.unlink(path) if path
33
+ rescue Errno::ENOENT
34
+ nil
35
+ end
36
+ end
37
+
38
+ def mkdir(*directories)
39
+ [*directories].each do |directory|
40
+ FileUtils.mkdir_p(File.expand_path(directory))
41
+ end
42
+ end
27
43
  end
28
44
  end
@@ -30,7 +30,7 @@ module Cognizant
30
30
  child_pids
31
31
  end
32
32
 
33
- def reset_data
33
+ def reset_data!
34
34
  store.clear unless store.empty?
35
35
  end
36
36
 
@@ -14,8 +14,8 @@ module Cognizant
14
14
  # Return if the process is not running.
15
15
  return true unless pid_running?(pid)
16
16
 
17
- signals = options[:signals] || ["TERM", "INT", "KILL"]
18
- timeout = options[:timeout] || 10
17
+ signals = options[:signals] || ["TERM", "INT"]
18
+ timeout = options[:timeout] || 30
19
19
 
20
20
  catch :stopped do
21
21
  signals.each do |stop_signal|