cognizant 0.0.1 → 0.0.2

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.
@@ -1,6 +1,7 @@
1
1
  require "cognizant/process/actions/start"
2
2
  require "cognizant/process/actions/stop"
3
3
  require "cognizant/process/actions/restart"
4
+ require "cognizant/system"
4
5
 
5
6
  module Cognizant
6
7
  class Process
@@ -55,7 +56,7 @@ module Cognizant
55
56
  end
56
57
  sleep 1
57
58
  end
58
-
59
+
59
60
  # Kill the nested thread.
60
61
  thread.kill
61
62
 
@@ -73,22 +74,7 @@ module Cognizant
73
74
  def send_signals(options = {})
74
75
  # Return if the process is already stopped.
75
76
  return true unless pid_running?
76
-
77
- signals = options[:signals] || ["TERM", "INT", "KILL"]
78
- timeout = options[:timeout] || 10
79
-
80
- catch :stopped do
81
- signals.each do |stop_signal|
82
- # Send the stop signal and wait for it to stop.
83
- signal(stop_signal, @process_pid)
84
-
85
- # Poll to see if it's stopped yet. Minimum 2 so that we check at least once again.
86
- ([timeout / signals.size, 2].max).times do
87
- throw :stopped unless pid_running?
88
- sleep 1
89
- end
90
- end
91
- end
77
+ Cognizant::System.send_signals(@process_pid, options)
92
78
  not pid_running?
93
79
  end
94
80
  end
@@ -0,0 +1,59 @@
1
+ require "cognizant/process/conditions"
2
+ require "cognizant/util/rotational_array"
3
+
4
+ module Cognizant
5
+ class Process
6
+ class ConditionCheck
7
+ class HistoryValue < Struct.new(:value, :critical); end
8
+
9
+ # No need to recreate one every tick.
10
+ EMPTY_ARRAY = [].freeze
11
+
12
+ attr_accessor :condition_name
13
+ def initialize(condition_name, options = {}, &block)
14
+ @condition_name = condition_name
15
+
16
+ if block
17
+ @do = Array(block)
18
+ else
19
+ @do = options.has_key?(:do) ? Array(options.delete(:do)) : [:restart]
20
+ end
21
+
22
+ @every = options.delete(:every).to_i
23
+ @times = options.delete(:times) || [1, 1]
24
+ @times = [@times, @times] unless @times.is_a?(Array) # handles :times => 5
25
+ @times.map(&:to_i)
26
+
27
+ clear_history!
28
+
29
+ @condition = Cognizant::Process::Conditions[@condition_name].new(options)
30
+ end
31
+
32
+ def run(pid, tick_number = Time.now.to_i)
33
+ if @last_ran_at.nil? || (@last_ran_at + @every) <= tick_number
34
+ @last_ran_at = tick_number
35
+
36
+ value = @condition.run(pid)
37
+ @history << HistoryValue.new(@condition.format_value(value), @condition.check(value))
38
+ # puts self.to_s
39
+
40
+ return @do if failed_check?
41
+ end
42
+ EMPTY_ARRAY
43
+ end
44
+
45
+ def clear_history!
46
+ @history = Util::RotationalArray.new(@times.last)
47
+ end
48
+
49
+ def failed_check?
50
+ @history.count { |v| v and v.critical } >= @times.first
51
+ end
52
+
53
+ def to_s
54
+ data = @history.collect { |v| v and "#{v.value}#{'*' unless v.critical}" }.join(", ")
55
+ "#{@condition_name}: [#{data}]\n"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ module Cognizant
2
+ class Process
3
+ module Conditions
4
+ class AlwaysTrue < PollCondition
5
+ def run(pid)
6
+ 1
7
+ end
8
+
9
+ def check(value)
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ require "cognizant/system"
2
+
3
+ module Cognizant
4
+ class Process
5
+ module Conditions
6
+ class CpuUsage < PollCondition
7
+ def initialize(options = {})
8
+ @above = options[:above].to_f
9
+ end
10
+
11
+ def run(pid)
12
+ System.cpu_usage(pid).to_f
13
+ end
14
+
15
+ def check(value)
16
+ value > @above
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ require "cognizant/util/rotational_array"
2
+
3
+ module Cognizant
4
+ class Process
5
+ module Conditions
6
+ class Flapping < TriggerCondition
7
+ TRIGGER_STATES = [:starting, :restarting]
8
+
9
+ attr_accessor :times, :within, :retry_after
10
+ attr_reader :timeline
11
+
12
+ def initialize(process, options = {})
13
+ options = { :times => 5, :within => 1, :retry_after => 5 }.merge(options)
14
+
15
+ options.each_pair do |name, value|
16
+ self.send("#{name}=", value) if self.respond_to?("#{name}=")
17
+ end
18
+
19
+ @timeline = Util::RotationalArray.new(@times)
20
+ super
21
+ end
22
+
23
+ def notify(transition)
24
+ if TRIGGER_STATES.include?(transition.to_name)
25
+ self.timeline << Time.now.to_i
26
+ self.check_flapping
27
+ end
28
+ end
29
+
30
+ def reset!
31
+ @timeline.clear
32
+ super
33
+ end
34
+
35
+ def check_flapping
36
+ # The process has not flapped if we haven't encountered enough incidents.
37
+ return unless (@timeline.compact.length == self.times)
38
+
39
+ # Check if the incident happend within the timeframe.
40
+ duration = (@timeline.last - @timeline.first) <= self.within
41
+
42
+ if duration
43
+ puts "Flapping detected: retrying in #{self.retry_after} seconds"
44
+
45
+ self.schedule_event(:start, self.retry_after) unless self.retry_after == 0 # retry_after zero means "do not retry, ever".
46
+ self.schedule_event(:unmonitor, 0)
47
+
48
+ @timeline.clear
49
+
50
+ # This will prevent a transition from happening in the process state_machine.
51
+ throw :halt
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,32 @@
1
+ module Cognizant
2
+ class Process
3
+ module Conditions
4
+ class MemoryUsage < PollCondition
5
+ MB = 1024 ** 2
6
+ FORMAT_STR = "%d%s"
7
+ MB_LABEL = "MB"
8
+ KB_LABEL = "KB"
9
+
10
+ def initialize(options = {})
11
+ @above = options[:above].to_f
12
+ end
13
+
14
+ def run(pid)
15
+ System.memory_usage(pid).to_f
16
+ end
17
+
18
+ def check(value)
19
+ value.kilobytes > @above
20
+ end
21
+
22
+ def format_value(value)
23
+ if value.kilobytes >= MB
24
+ FORMAT_STR % [(value / 1024).round, MB_LABEL]
25
+ else
26
+ FORMAT_STR % [value, KB_LABEL]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,23 @@
1
+ module Cognizant
2
+ class Process
3
+ module Conditions
4
+ class PollCondition
5
+ def initialize(options = {})
6
+ @options = options
7
+ end
8
+
9
+ def run(pid)
10
+ raise "Implement in subclass!"
11
+ end
12
+
13
+ def check(value)
14
+ raise "Implement in subclass!"
15
+ end
16
+
17
+ def format_value(value)
18
+ value
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ module Cognizant
2
+ class Process
3
+ module Conditions
4
+ class TriggerCondition
5
+ attr_accessor :process, :mutex, :scheduled_events
6
+
7
+ def initialize(process, options = {})
8
+ self.process = process
9
+ self.mutex = Mutex.new
10
+ self.scheduled_events = []
11
+ end
12
+
13
+ def reset!
14
+ self.cancel_all_events
15
+ end
16
+
17
+ def notify(transition)
18
+ raise "Implement in subclass"
19
+ end
20
+
21
+ def dispatch!(event)
22
+ self.process.dispatch!(event, self.class.name.split("::").last)
23
+ end
24
+
25
+ def schedule_event(event, delay)
26
+ # TODO: Maybe wrap this in a ScheduledEvent class with methods like cancel.
27
+ thread = Thread.new(self) do |trigger|
28
+ begin
29
+ sleep delay.to_f
30
+ trigger.dispatch!(event)
31
+ trigger.mutex.synchronize do
32
+ trigger.scheduled_events.delete_if { |_, thread| thread == Thread.current }
33
+ end
34
+ rescue StandardError => e
35
+ puts(e)
36
+ puts(e.backtrace.join("\n"))
37
+ end
38
+ end
39
+
40
+ self.scheduled_events.push([event, thread])
41
+ end
42
+
43
+ def cancel_all_events
44
+ puts "Canceling all scheduled events"
45
+ self.mutex.synchronize do
46
+ self.scheduled_events.each {|_, thread| thread.kill}
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ require "cognizant/process/conditions/poll_condition"
2
+ require "cognizant/process/conditions/trigger_condition"
3
+
4
+ Dir["#{File.dirname(__FILE__)}/conditions/*.rb"].each do |c|
5
+ require c
6
+ end
7
+
8
+ module Cognizant
9
+ class Process
10
+ module Conditions
11
+ def self.[](name)
12
+ const_get(name.to_s.camelcase)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -65,11 +65,11 @@ module Cognizant
65
65
  end
66
66
 
67
67
  # Merge spawn options.
68
- spawn_options = construct_spawn_options(options, {
69
- :in => stdin,
70
- :out => stdout,
71
- :err => stderr
72
- })
68
+ spawn_options = construct_spawn_options(options, {
69
+ :in => stdin,
70
+ :out => stdout,
71
+ :err => stderr
72
+ })
73
73
 
74
74
  # Spawn a process to execute the command.
75
75
  process_pid = ::Process.spawn(options[:env], command, spawn_options)
@@ -4,11 +4,13 @@ module Cognizant
4
4
  def read_pid
5
5
  if self.pid_command
6
6
  str = execute(self.pid_command).stdout.to_i
7
- @process_pid = str unless not str or str.zero? # TODO: Also check if the pid is alive.
7
+ @process_pid = str unless not str or str.zero?
8
+ # TODO: Write pid to pidfile, since our source was pid_command instead.
8
9
  elsif self.pidfile and File.exists?(self.pidfile)
9
10
  str = File.read(self.pidfile).to_i
10
- @process_pid = str unless not str or str.zero? # TODO: Also check if the pid is alive.
11
+ @process_pid = str unless not str or str.zero?
11
12
  end
13
+ @process_pid = 0 unless System.pid_running?(@process_pid)
12
14
  @process_pid
13
15
  end
14
16
 
@@ -1,26 +1,14 @@
1
+ require "cognizant/system"
2
+
1
3
  module Cognizant
2
4
  class Process
3
5
  module Status
4
6
  def pid_running?
5
- pid = read_pid
6
- return false unless pid and pid != 0
7
- signal(0, pid)
8
- # It's running since no exception was raised.
9
- true
10
- rescue Errno::ESRCH
11
- # No such process.
12
- false
13
- rescue Errno::EPERM
14
- # Probably running, but we're not allowed to pass signals.
15
- # TODO: Is this a sign of problems ahead?
16
- true
17
- else
18
- # Possibly running.
19
- true
7
+ Cognizant::System.pid_running?(read_pid)
20
8
  end
21
9
 
22
10
  def signal(signal, pid = nil)
23
- ::Process.kill(signal, (pid || read_pid))
11
+ Cognizant::System.signal(signal, (pid || read_pid))
24
12
  end
25
13
  end
26
14
  end
@@ -1,10 +1,15 @@
1
- require 'state_machine'
1
+ require "monitor"
2
+ require "thread"
3
+
4
+ require "state_machine"
2
5
 
3
6
  require "cognizant/process/pid"
4
7
  require "cognizant/process/status"
5
8
  require "cognizant/process/execution"
6
9
  require "cognizant/process/attributes"
7
10
  require "cognizant/process/actions"
11
+ require "cognizant/process/condition_check"
12
+ require "cognizant/util/symbolize_hash_keys"
8
13
 
9
14
  module Cognizant
10
15
  class Process
@@ -66,21 +71,30 @@ module Cognizant
66
71
  before_transition any => :restarting, :do => lambda { |p| p.autostart = true }
67
72
  after_transition any => :restarting, :do => :restart_process
68
73
 
69
- before_transition any => any, :do => :record_transition_start
70
- after_transition any => any, :do => :record_transition_end
74
+ before_transition any => any, :do => :notify_triggers
75
+ after_transition any => any, :do => :record_transition
71
76
  end
72
77
 
73
78
  def initialize(process_name = nil, options = {})
74
- # Default.
75
- self.autostart = true
76
- self.name = process_name if process_name
79
+ @ticks_to_skip = 0
80
+ @checks = []
81
+ @triggers = []
82
+ @action_mutex = Monitor.new
83
+
84
+ self.name = process_name.to_s if process_name
85
+ self.autostart = true # Default.
86
+
87
+ if options.has_key?(:checks) and options[:checks].kind_of?(Hash)
88
+ options[:checks].each do |condition_name, args|
89
+ self.check(condition_name, args)
90
+ end
91
+ end
92
+ options.delete(:checks)
77
93
 
78
94
  options.each do |attribute_name, value|
79
95
  self.send("#{attribute_name}=", value) if self.respond_to?("#{attribute_name}=")
80
96
  end
81
97
 
82
- @ticks_to_skip = 0
83
-
84
98
  yield(self) if block_given?
85
99
 
86
100
  # Let state_machine initialize as well.
@@ -93,14 +107,80 @@ module Cognizant
93
107
 
94
108
  # Invoke the state_machine event.
95
109
  super
110
+
111
+ self.run_checks if self.running?
112
+ end
113
+
114
+ def record_transition(transition)
115
+ unless transition.loopback?
116
+ @transitioned = true
117
+ @last_transition_time = Time.now.to_i
118
+
119
+ # When a process changes state, we should clear the memory of all the checks.
120
+ @checks.each { |check| check.clear_history! }
121
+ puts "#{name} changing from #{transition.from_name} => #{transition.to_name}"
122
+ end
123
+ end
124
+
125
+ def last_transition_time
126
+ @last_transition_time || 0
127
+ end
128
+
129
+ def handle_user_command(command)
130
+ if command == :unmonitor
131
+ # When the user issues an unmonitor command, reset any
132
+ # triggers so that scheduled events gets cleared.
133
+ @triggers.each { |trigger| trigger.reset! }
134
+ end
135
+ dispatch!(command, "user initiated")
136
+ end
137
+
138
+ def dispatch!(action, reason = nil)
139
+ @action_mutex.synchronize do
140
+ if action.respond_to?(:call)
141
+ action.call(self)
142
+ else
143
+ self.send("#{action}")
144
+ end
145
+ end
96
146
  end
97
147
 
98
- def record_transition_start
99
- print "#{name}: changing state from `#{state}`"
148
+ def check(condition_name, options, &block)
149
+ klass = Cognizant::Process::Conditions[condition_name]
150
+ case klass.superclass.name.split("::").last
151
+ when "TriggerCondition"
152
+ @triggers << klass.new(self, options.deep_symbolize_keys!)
153
+ when "PollCondition"
154
+ @checks << ConditionCheck.new(condition_name, options.deep_symbolize_keys!, &block)
155
+ end
100
156
  end
101
157
 
102
- def record_transition_end
103
- puts " to `#{state}`"
158
+ def notify_triggers(transition)
159
+ @triggers.each { |trigger| trigger.notify(transition) }
160
+ end
161
+
162
+ def run_checks
163
+ now = Time.now.to_i
164
+
165
+ threads = @checks.collect do |check|
166
+ [check, Thread.new { Thread.current[:actions] = check.run(read_pid, now) }]
167
+ end
168
+
169
+ @transitioned = false
170
+
171
+ threads.inject([]) do |actions, (check, thread)|
172
+ thread.join
173
+ if thread[:actions].size > 0
174
+ puts "#{check.condition_name} dispatched: #{thread[:actions].join(',')}"
175
+ thread[:actions].each do |action|
176
+ actions << [action, check.to_s]
177
+ end
178
+ end
179
+ actions
180
+ end.each do |(action, reason)|
181
+ break if @transitioned
182
+ self.dispatch!(action, reason)
183
+ end
104
184
  end
105
185
 
106
186
  def process_running?
@@ -1,52 +1,79 @@
1
+ require "json"
2
+ require "cognizant/system"
3
+
1
4
  module Cognizant
2
5
  module Server
3
6
  module Commands
4
7
  def self.load(config_file)
5
8
  Cognizant::Server.daemon.load(config_file)
6
- yield("OK")
9
+ # yield("OK")
7
10
  end
8
11
 
9
12
  def self.status(*args)
10
- if process_name = args.shift
11
- Cognizant::Server.daemon.processes.each do |name, process|
12
- if process.name.eql?(process_name)
13
- yield("#{process.name}: #{process.state}")
14
- return yield("OK")
15
- end
16
- end
17
- yield("ERR: No such process")
18
- return yield("OK")
13
+ output_processes = []
14
+ if args.size > 0
15
+ Cognizant::Server.daemon.processes.values.each do |process|
16
+ output_processes << process if args.include?(process.name) or args.include?(process.group)
17
+ end
18
+ if output_processes.size == 0
19
+ raise("ERROR: No such process")
20
+ # yield("OK")
21
+ end
22
+ else
23
+ output_processes = Cognizant::Server.daemon.processes.values
19
24
  end
20
- yield("OK")
25
+
26
+ output = []
27
+ output_processes.each do |process|
28
+ pid = process.read_pid
29
+ output << {
30
+ "Process" => process.name,
31
+ "PID" => pid,
32
+ "Group" => process.group,
33
+ "State" => process.state,
34
+ "Since" => process.last_transition_time,
35
+ "% CPU" => System.cpu_usage(pid).to_f,
36
+ "Memory" => System.memory_usage(pid).to_f # in KBs.
37
+ }
38
+ end
39
+ yield(output.to_json)
40
+ # yield("OK")
21
41
  end
22
42
 
23
43
  %w(monitor start stop restart unmonitor).each do |action|
24
44
  class_eval <<-END
25
45
  def self.#{action}(*args)
26
- unless process_name = args.shift
27
- yield("ERR: Missing process name")
28
- return yield("OK")
46
+ unless args.size > 0
47
+ raise("ERROR: Missing process name")
48
+ return # yield("OK")
49
+ end
50
+ output_processes = []
51
+ Cognizant::Server.daemon.processes.values.each do |process|
52
+ if args.include?(process.name) or args.include?(process.group)
53
+ output_processes << process
54
+ end
29
55
  end
30
- Cognizant::Server.daemon.processes.each do |name, process|
31
- if process.name.eql?(process_name)
32
- process.#{action}
33
- return yield("OK")
56
+
57
+ if output_processes.size == 0
58
+ raise("ERROR: No such process")
59
+ # yield("OK")
60
+ else
61
+ output_processes.each do |process|
62
+ process.handle_user_command(:#{action})
34
63
  end
35
64
  end
36
- yield("ERR: No such process")
37
- yield("OK")
38
65
  end
39
66
  END
40
67
  end
41
68
 
42
69
  def self.shutdown
43
70
  Cognizant::Server.daemon.shutdown
44
- yield("OK")
71
+ # yield("OK")
45
72
  end
46
73
 
47
74
  def self.method_missing(command, *args)
48
- yield("ERR: Unknown command '#{command}'")
49
- yield("OK")
75
+ raise("ERROR: Unknown command '#{command}'")
76
+ # yield("OK")
50
77
  end
51
78
  end
52
79
  end