bluepill 0.1.2 → 0.1.4

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. checksums.yaml +5 -5
  2. data/CHANGELOG.md +820 -0
  3. data/CONTRIBUTING.md +31 -0
  4. data/LICENSE +1 -1
  5. data/README.md +69 -30
  6. data/bin/bluepill +2 -2
  7. data/bluepill.gemspec +17 -6
  8. data/lib/bluepill/application.rb +311 -43
  9. data/lib/bluepill/condition_watch.rb +105 -10
  10. data/lib/bluepill/controller.rb +125 -14
  11. data/lib/bluepill/dsl/app_proxy.rb +103 -4
  12. data/lib/bluepill/dsl/process_factory.rb +185 -62
  13. data/lib/bluepill/dsl/process_proxy.rb +132 -14
  14. data/lib/bluepill/dsl.rb +27 -5
  15. data/lib/bluepill/group.rb +142 -9
  16. data/lib/bluepill/logger.rb +122 -7
  17. data/lib/bluepill/process.rb +582 -211
  18. data/lib/bluepill/process_commands.rb +262 -0
  19. data/lib/bluepill/process_conditions/always_true.rb +18 -4
  20. data/lib/bluepill/process_conditions/cpu_usage.rb +34 -0
  21. data/lib/bluepill/process_conditions/file_time.rb +40 -4
  22. data/lib/bluepill/process_conditions/http.rb +81 -23
  23. data/lib/bluepill/process_conditions/mem_usage.rb +64 -5
  24. data/lib/bluepill/process_conditions/process_condition.rb +45 -2
  25. data/lib/bluepill/process_conditions/running_time.rb +36 -1
  26. data/lib/bluepill/process_conditions/zombie_process.rb +26 -5
  27. data/lib/bluepill/process_conditions.rb +15 -1
  28. data/lib/bluepill/process_journal.rb +256 -104
  29. data/lib/bluepill/process_statistics.rb +33 -3
  30. data/lib/bluepill/socket.rb +63 -16
  31. data/lib/bluepill/system/commands.rb +286 -0
  32. data/lib/bluepill/system.rb +193 -229
  33. data/lib/bluepill/trigger.rb +111 -15
  34. data/lib/bluepill/triggers/flapping.rb +86 -5
  35. data/lib/bluepill/util/rotational_array.rb +25 -2
  36. data/lib/bluepill/version.rb +71 -19
  37. data/lib/bluepill.rb +25 -2
  38. data/sig/bluepill.rbs +926 -0
  39. data/sig/manifest.yaml +7 -0
  40. metadata +16 -35
  41. data/bin/sample_forking_server +0 -66
  42. data/lib/bluepill/application/client.rb +0 -6
  43. data/lib/bluepill/application/server.rb +0 -21
@@ -1,41 +1,159 @@
1
1
  module Bluepill
2
+ # The self of a process block in a config file
3
+ #
4
+ # Any of the process's attributes can be set with `name = value`,
5
+ # `name value`, or `name!` for true, and read back with `name`. See
6
+ # {Process::CONFIGURABLE_ATTRIBUTES} for the attributes a process has.
7
+ #
8
+ # @example
9
+ # app.process('web') do |process|
10
+ # process.start_command = '/usr/bin/my_server'
11
+ # process.daemonize!
12
+ # process.checks :mem_usage, every: 10.seconds, below: 100.megabytes
13
+ # end
14
+ #
15
+ # @api public
2
16
  class ProcessProxy
3
- attr_reader :attributes, :watches, :name
17
+ # Return the attributes that the block set, by name
18
+ #
19
+ # @return [Hash{Symbol => Object}] the attributes
20
+ #
21
+ # @api private
22
+ attr_reader :attributes
23
+
24
+ # Return the checks that the block added, with their options, by name
25
+ #
26
+ # @return [Hash{Symbol => Hash{Symbol => Object}}] the checks
27
+ #
28
+ # @api private
29
+ attr_reader :watches
30
+
31
+ # Return the process's name
32
+ #
33
+ # @return [String] the name
34
+ #
35
+ # @api private
36
+ attr_reader :name
37
+
38
+ # Create the proxy of a process, and run its block
39
+ #
40
+ # @param [String] process_name the process's name
41
+ # @param [Hash{Symbol => Object}] attributes the attributes the process
42
+ # starts with, which the block adds to
43
+ # @param [Proc] process_block the block that configures the process
44
+ #
45
+ # @return [void]
46
+ #
47
+ # @api private
4
48
  def initialize(process_name, attributes, process_block)
5
49
  @name = process_name
6
50
  @attributes = attributes
7
51
  @watches = {}
8
52
 
9
- if process_block.arity.zero?
10
- instance_eval(&process_block)
11
- else
12
- instance_exec(self, &process_block)
13
- end
53
+ # instance_eval also passes the proxy to a block that takes an argument
54
+ instance_eval(&process_block)
14
55
  end
15
56
 
57
+ # Set or read an attribute of the process
58
+ #
59
+ # A name ending in `=`, or one given a value, sets the attribute. A name
60
+ # ending in `!` sets it to true. A name alone reads an attribute that's
61
+ # set.
62
+ #
63
+ # @example
64
+ # process.start_command = '/usr/bin/my_server'
65
+ # process.stop_grace_time 10.seconds
66
+ # process.daemonize!
67
+ # process.start_command # => "/usr/bin/my_server"
68
+ #
69
+ # @param [Symbol] name the attribute's name
70
+ # @param [Array<Object>] args the value, if it's set
71
+ #
72
+ # @return [Object] the value that was set or read
73
+ #
74
+ # @raise [NoMethodError] for an attribute that isn't set, or more than one
75
+ # value
76
+ #
77
+ # @api public
16
78
  def method_missing(name, *args)
17
- if args.size == 1 && name.to_s =~ /^(.*)=$/
18
- @attributes[Regexp.last_match[1].to_sym] = args.first
19
- elsif args.size == 1
20
- @attributes[name.to_sym] = args.first
21
- elsif args.size == 0 && name.to_s =~ /^(.*)!$/
22
- @attributes[Regexp.last_match[1].to_sym] = true
23
- elsif args.empty? && @attributes.key?(name.to_sym)
24
- @attributes[name.to_sym]
79
+ attribute = name.to_s
80
+ case args.size
81
+ when 1
82
+ value, = args
83
+ @attributes[attribute.chomp('=').to_sym] = value
84
+ when 0
85
+ if attribute.end_with?('!')
86
+ @attributes[attribute.chomp('!').to_sym] = true
87
+ elsif @attributes.key?(name)
88
+ @attributes.fetch(name)
89
+ else
90
+ super
91
+ end
25
92
  else
26
93
  super
27
94
  end
28
95
  end
29
96
 
97
+ # Return whether the proxy answers a name through method_missing
98
+ #
99
+ # Ruby turns a String name into a Symbol before calling this.
100
+ #
101
+ # @param [Symbol] name the method's name
102
+ # @param [Boolean, nil] _include_private ignored
103
+ #
104
+ # @return [Boolean] true for a setter, a `name!`, or an attribute that's
105
+ # set
106
+ #
107
+ # @api private
108
+ def respond_to_missing?(name, _include_private)
109
+ ['=', '!'].include?(name[-1]) || @attributes.key?(name)
110
+ end
111
+
112
+ # Add a check of the process, or a trigger such as :flapping
113
+ #
114
+ # @example
115
+ # process.checks :cpu_usage, every: 10.seconds, below: 50, times: [3, 5]
116
+ # process.checks :flapping, times: 2, within: 30.seconds, retry_in: 7.seconds
117
+ #
118
+ # @param [Symbol] name the check's name, such as :mem_usage
119
+ # @param [Hash{Symbol => Object}] options the check's options, such as
120
+ # :every, :below and :times
121
+ #
122
+ # @return [Hash{Symbol => Object}] the options
123
+ #
124
+ # @api public
30
125
  def checks(name, options = {})
31
126
  @watches[name] = options
32
127
  end
33
128
 
129
+ # Monitor the process's children too
130
+ #
131
+ # The block configures each child the way a process block configures a
132
+ # process. It must set a stop_command.
133
+ #
134
+ # @example
135
+ # process.monitor_children do |child|
136
+ # child.stop_command = 'kill -QUIT {{PID}}'
137
+ # child.checks :mem_usage, every: 10.seconds, below: 200.megabytes
138
+ # end
139
+ #
140
+ # @yield [child] configures each child
141
+ # @yieldparam [ProcessProxy] child the child's proxy, which is also self
142
+ # @yieldreturn [void]
143
+ #
144
+ # @return [Proc, nil] the block
145
+ #
146
+ # @api public
34
147
  def monitor_children(&child_process_block)
35
148
  @attributes[:monitor_children] = true
36
149
  @attributes[:child_process_block] = child_process_block
37
150
  end
38
151
 
152
+ # Create the process that the block described
153
+ #
154
+ # @return [Process] the process
155
+ #
156
+ # @api private
39
157
  def to_process
40
158
  Process.new(@name, @watches, @attributes)
41
159
  end
data/lib/bluepill/dsl.rb CHANGED
@@ -1,11 +1,33 @@
1
1
  module Bluepill
2
+ # Load an application from a config file
3
+ #
4
+ # The block describes the application's processes. It runs with self set
5
+ # to an {AppProxy}, and also gets the proxy as an argument. Bluepill then
6
+ # loads the application, which starts its daemon.
7
+ #
8
+ # @example
9
+ # Bluepill.application(:my_app, log_file: '/var/log/bluepill.log') do |app|
10
+ # app.process('web') do |process|
11
+ # process.start_command = '/usr/bin/my_server'
12
+ # process.pid_file = '/var/run/my_server.pid'
13
+ # end
14
+ # end
15
+ #
16
+ # @param [#to_s] app_name the application's name
17
+ # @param [Hash{Symbol => Object}] options the application's options, such
18
+ # as :log_file, :foreground and :base_dir
19
+ #
20
+ # @yield [app] describes the application's processes
21
+ # @yieldparam [AppProxy] app the application's proxy, which is also self
22
+ # @yieldreturn [void]
23
+ #
24
+ # @return [void]
25
+ #
26
+ # @api public
2
27
  def self.application(app_name, options = {}, &block)
3
28
  app_proxy = AppProxy.new(app_name, options)
4
- if block.arity == 0
5
- app_proxy.instance_eval(&block)
6
- else
7
- app_proxy.instance_exec(app_proxy, &block)
8
- end
29
+ # instance_eval also passes the proxy to a block that takes an argument
30
+ app_proxy.instance_eval(&block)
9
31
  app_proxy.app.load
10
32
  end
11
33
  end
@@ -1,30 +1,149 @@
1
1
  module Bluepill
2
+ # The processes of an application that share a group name
3
+ #
4
+ # A config puts a process in a group with `process.group = "name"`.
5
+ # Processes without one are in the application's nameless group. A
6
+ # command such as `bluepill restart name` acts on every process in the
7
+ # group.
8
+ #
9
+ # @api private
2
10
  class Group
3
- attr_accessor :name, :processes, :logger
11
+ # Return the group's name
12
+ #
13
+ # @return [String, nil] the name, or nil for the application's nameless
14
+ # group
15
+ #
16
+ # @api private
17
+ attr_accessor :name
18
+
19
+ # Return the group's processes
20
+ #
21
+ # @return [Array<Process>] the processes
22
+ #
23
+ # @api private
24
+ attr_accessor :processes
25
+
26
+ # Return the group's logger, which its processes' loggers prefix
27
+ #
28
+ # @return [Logger] the logger
29
+ #
30
+ # @api private
31
+ attr_accessor :logger
32
+
33
+ # Return a logger for processes, which bluepill doesn't use
34
+ #
35
+ # @return [Object] whatever was set
36
+ #
37
+ # @api private
4
38
  attr_accessor :process_logger
5
39
 
40
+ # Create an empty group
41
+ #
42
+ # @param [String, nil] name the group's name, or nil for the nameless
43
+ # group
44
+ # @param [Hash{Symbol => Object}] options the group's options, with
45
+ # :logger
46
+ #
47
+ # @return [void]
48
+ #
49
+ # @api private
6
50
  def initialize(name, options = {})
7
51
  self.name = name
8
52
  self.processes = []
9
- self.logger = options[:logger]
53
+ logger = options[:logger] #: Logger
54
+ self.logger = logger
10
55
  end
11
56
 
57
+ # Add a process, giving it a logger prefixed with its name
58
+ #
59
+ # @param [Process] process the process
60
+ #
61
+ # @return [Array<Process>] the group's processes
62
+ #
63
+ # @api private
12
64
  def add_process(process)
13
65
  process.logger = logger.prefix_with(process.name)
14
66
  processes << process
15
67
  end
16
68
 
69
+ # Tick every process in the group
70
+ #
71
+ # @return [void]
72
+ #
73
+ # @api private
17
74
  def tick
18
75
  processes.each(&:tick)
19
76
  end
20
77
 
78
+ # Set each process's state from whether it's running
79
+ #
80
+ # Bluepill does this once, when it loads the config.
81
+ #
82
+ # @return [void]
83
+ #
84
+ # @api private
21
85
  def determine_initial_state
22
86
  processes.each(&:determine_initial_state)
23
87
  end
24
88
 
25
- # proxied events
89
+ # @!method start(process_name = nil)
90
+ # Start the group's processes, or one of them
91
+ #
92
+ # Each process runs the command on a thread of its own. Unless it's
93
+ # set to run it without blocking (group_start_noblock), the group
94
+ # waits for it before going on to the next process.
95
+ #
96
+ # @param [String, nil] process_name the process to start, or nil for
97
+ # every process in the group
98
+ #
99
+ # @return [Array<String>] the processes it started, as group:process
100
+ #
101
+ # @api private
102
+ #
103
+ # @!method unmonitor(process_name = nil)
104
+ # Stop monitoring the group's processes, or one of them
105
+ #
106
+ # Each process runs the command on a thread of its own. Unless it's
107
+ # set to run it without blocking (group_unmonitor_noblock), the group
108
+ # waits for it before going on to the next process.
109
+ #
110
+ # @param [String, nil] process_name the process to unmonitor, or nil
111
+ # for every process in the group
112
+ #
113
+ # @return [Array<String>] the processes it unmonitored, as
114
+ # group:process
115
+ #
116
+ # @api private
117
+ #
118
+ # @!method stop(process_name = nil)
119
+ # Stop the group's processes, or one of them
120
+ #
121
+ # Each process runs the command on a thread of its own. Unless it's
122
+ # set to run it without blocking (group_stop_noblock), the group waits
123
+ # for it before going on to the next process.
124
+ #
125
+ # @param [String, nil] process_name the process to stop, or nil for
126
+ # every process in the group
127
+ #
128
+ # @return [Array<String>] the processes it stopped, as group:process
129
+ #
130
+ # @api private
131
+ #
132
+ # @!method restart(process_name = nil)
133
+ # Restart the group's processes, or one of them
134
+ #
135
+ # Each process runs the command on a thread of its own. Unless it's
136
+ # set to run it without blocking (group_restart_noblock), the group
137
+ # waits for it before going on to the next process.
138
+ #
139
+ # @param [String, nil] process_name the process to restart, or nil for
140
+ # every process in the group
141
+ #
142
+ # @return [Array<String>] the processes it restarted, as group:process
143
+ #
144
+ # @api private
26
145
  [:start, :unmonitor, :stop, :restart].each do |event|
27
- class_eval <<-END
146
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
28
147
  def #{event}(process_name = nil)
29
148
  threads = []
30
149
  affected = []
@@ -44,18 +163,31 @@ module Bluepill
44
163
  threads.each { |t| t.join } unless threads.nil?
45
164
  affected
46
165
  end
47
- END
166
+ RUBY
48
167
  end
49
168
 
169
+ # Return the status lines that bluepill status shows
170
+ #
171
+ # Without a process name, it's a line for each process (and each of its
172
+ # children, for one with monitor_children), under the group's name. For
173
+ # one process, it's its line and its event history.
174
+ #
175
+ # @param [String, nil] process_name the process to show, or nil for
176
+ # every process in the group
177
+ #
178
+ # @return [Array<String>] the lines, ending with an empty one
179
+ #
180
+ # @api private
50
181
  def status(process_name = nil)
51
- lines = []
182
+ lines = [] #: Array[String]
52
183
  if process_name.nil?
53
- prefix = name ? ' ' : ''
184
+ prefix = (' ' if name)
54
185
  lines << "#{name}:" if name
55
186
 
56
187
  processes.each do |process|
57
188
  lines << format('%s%s(pid:%s): %s', prefix, process.name, process.actual_pid, process.state)
58
189
  next unless process.monitor_children?
190
+
59
191
  process.children.each do |child|
60
192
  lines << format(' %s%s: %s', prefix, child.name, child.state)
61
193
  end
@@ -63,8 +195,9 @@ module Bluepill
63
195
 
64
196
  else
65
197
  processes.each do |process|
66
- next if process_name != process.name
67
- lines << format('%s%s(pid:%s): %s', prefix, process.name, process.actual_pid, process.state)
198
+ next unless process_name.eql?(process.name)
199
+
200
+ lines << format('%s(pid:%s): %s', process.name, process.actual_pid, process.state)
68
201
  lines << process.statistics.to_s
69
202
  end
70
203
  end
@@ -1,7 +1,27 @@
1
1
  module Bluepill
2
+ # Write bluepill's log, to syslog or a file, with prefixes
3
+ #
4
+ # A logger made with prefix_with writes through its parent, which adds
5
+ # its own prefix, so a process's messages are tagged with its app's name
6
+ # and its own, as in "[app:web] Going from up => down".
7
+ #
8
+ # @api private
2
9
  class Logger
3
- LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug]
10
+ # The syslog priorities, each a method that logs a message
11
+ #
12
+ # @api private
13
+ LOG_METHODS = [:emerg, :alert, :crit, :err, :warning, :notice, :info, :debug].freeze
4
14
 
15
+ # Make a logger
16
+ #
17
+ # @param [Hash{Symbol => Object}] options :logger, a parent logger to
18
+ # write through, :prefix, :stdout to echo messages there too,
19
+ # :log_file to write to instead of syslog, and :identity, the name
20
+ # syslog shows
21
+ #
22
+ # @return [void]
23
+ #
24
+ # @api private
5
25
  def initialize(options = {})
6
26
  @options = options
7
27
  @logger = options[:logger] || create_logger
@@ -10,8 +30,56 @@ module Bluepill
10
30
  @prefixes = {}
11
31
  end
12
32
 
33
+ # @!method emerg(msg, prefix = [])
34
+ # Log a message at the emergency priority
35
+ # @param [String] msg the message
36
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
37
+ # @return [void]
38
+ # @api private
39
+ # @!method alert(msg, prefix = [])
40
+ # Log a message at the alert priority
41
+ # @param [String] msg the message
42
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
43
+ # @return [void]
44
+ # @api private
45
+ # @!method crit(msg, prefix = [])
46
+ # Log a message at the critical priority
47
+ # @param [String] msg the message
48
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
49
+ # @return [void]
50
+ # @api private
51
+ # @!method err(msg, prefix = [])
52
+ # Log a message at the error priority
53
+ # @param [String] msg the message
54
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
55
+ # @return [void]
56
+ # @api private
57
+ # @!method warning(msg, prefix = [])
58
+ # Log a message at the warning priority
59
+ # @param [String] msg the message
60
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
61
+ # @return [void]
62
+ # @api private
63
+ # @!method notice(msg, prefix = [])
64
+ # Log a message at the notice priority
65
+ # @param [String] msg the message
66
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
67
+ # @return [void]
68
+ # @api private
69
+ # @!method info(msg, prefix = [])
70
+ # Log a message at the info priority
71
+ # @param [String] msg the message
72
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
73
+ # @return [void]
74
+ # @api private
75
+ # @!method debug(msg, prefix = [])
76
+ # Log a message at the debug priority
77
+ # @param [String] msg the message
78
+ # @param [Array<String, nil>] prefix the prefixes of the loggers below
79
+ # @return [void]
80
+ # @api private
13
81
  LOG_METHODS.each do |method|
14
- class_eval <<-END
82
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
15
83
  def #{method}(msg, prefix = [])
16
84
  if @logger.is_a?(self.class)
17
85
  @logger.#{method}(msg, [@prefix] + prefix)
@@ -24,16 +92,31 @@ module Bluepill
24
92
  @logger.#{method}("\#{s_prefix}\#{msg}")
25
93
  end
26
94
  end
27
- END
95
+ RUBY
28
96
  end
29
97
 
98
+ # Return a logger that adds a prefix to this one's messages
99
+ #
100
+ # It's made once for each prefix, and kept.
101
+ #
102
+ # @param [String] prefix the prefix, such as a process's name
103
+ #
104
+ # @return [Logger] the prefixed logger
105
+ #
106
+ # @api private
30
107
  def prefix_with(prefix)
31
108
  @prefixes[prefix] ||= self.class.new(logger: self, prefix: prefix)
32
109
  end
33
110
 
111
+ # Open the log file or syslog again, such as after log rotation
112
+ #
113
+ # @return [void]
114
+ #
115
+ # @api private
34
116
  def reopen
35
- if @logger.is_a?(self.class)
36
- @logger.reopen
117
+ logger = @logger
118
+ if logger.instance_of?(self.class)
119
+ logger.reopen
37
120
  else
38
121
  @logger = create_logger
39
122
  end
@@ -41,21 +124,53 @@ module Bluepill
41
124
 
42
125
  protected
43
126
 
127
+ # Open the log file, or syslog if there isn't one
128
+ #
129
+ # @return [LoggerAdapter, Syslog] where the messages go
130
+ #
131
+ # @api private
44
132
  def create_logger
45
133
  if @options[:log_file]
46
- LoggerAdapter.new(@options[:log_file])
134
+ LoggerAdapter.new(@options.fetch(:log_file))
47
135
  else
136
+ require_syslog
48
137
  Syslog.close if Syslog.opened? # need to explictly close it before reopening it
49
138
  Syslog.open(@options[:identity] || 'bluepilld', Syslog::LOG_PID, Syslog::LOG_LOCAL6)
50
139
  end
51
140
  end
52
141
 
142
+ # Load syslog, which is only loaded when it's used
143
+ #
144
+ # Ruby 3.4 and newer don't include it by default, so apps that log to a
145
+ # file don't need it in their Gemfile.
146
+ #
147
+ # @return [void]
148
+ #
149
+ # @raise [LoadError] with how to fix it, if syslog can't be loaded
150
+ #
151
+ # @api private
152
+ def require_syslog
153
+ require 'syslog'
154
+ rescue LoadError
155
+ message = 'Bluepill logs to syslog unless log_file is set, but the syslog library ' \
156
+ "could not be loaded. Since Ruby 3.4 it's a separate gem, so add gem 'syslog' " \
157
+ 'to your Gemfile or set log_file.'
158
+ raise LoadError, message
159
+ end
160
+
161
+ # A ::Logger that also answers to the syslog priorities' names
162
+ #
163
+ # @api private
53
164
  class LoggerAdapter < ::Logger
165
+ # The ::Logger method for each syslog priority
166
+ #
167
+ # @api private
54
168
  LOGGER_EQUIVALENTS =
55
- {debug: :debug, err: :error, warning: :warn, info: :info, emerg: :fatal, alert: :warn, crit: :fatal, notice: :info}
169
+ {debug: :debug, err: :error, warning: :warn, info: :info, emerg: :fatal, alert: :warn, crit: :fatal, notice: :info}.freeze
56
170
 
57
171
  LOG_METHODS.each do |method|
58
172
  next if method == LOGGER_EQUIVALENTS[method]
173
+
59
174
  alias_method method, LOGGER_EQUIVALENTS[method]
60
175
  end
61
176
  end