einhorn 0.7.4 → 1.0.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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/Changes.md +10 -0
  3. data/README.md +36 -30
  4. data/bin/einhorn +17 -2
  5. data/einhorn.gemspec +23 -21
  6. data/example/pool_worker.rb +1 -1
  7. data/example/thin_example +8 -8
  8. data/example/time_server +5 -5
  9. data/lib/einhorn/client.rb +8 -9
  10. data/lib/einhorn/command/interface.rb +100 -95
  11. data/lib/einhorn/command.rb +167 -88
  12. data/lib/einhorn/compat.rb +7 -7
  13. data/lib/einhorn/event/abstract_text_descriptor.rb +31 -35
  14. data/lib/einhorn/event/ack_timer.rb +2 -2
  15. data/lib/einhorn/event/command_server.rb +7 -9
  16. data/lib/einhorn/event/connection.rb +1 -3
  17. data/lib/einhorn/event/loop_breaker.rb +2 -1
  18. data/lib/einhorn/event/persistent.rb +2 -2
  19. data/lib/einhorn/event/timer.rb +4 -4
  20. data/lib/einhorn/event.rb +29 -20
  21. data/lib/einhorn/prctl.rb +26 -0
  22. data/lib/einhorn/prctl_linux.rb +48 -0
  23. data/lib/einhorn/safe_yaml.rb +17 -0
  24. data/lib/einhorn/version.rb +1 -1
  25. data/lib/einhorn/worker.rb +67 -49
  26. data/lib/einhorn/worker_pool.rb +9 -9
  27. data/lib/einhorn.rb +155 -126
  28. metadata +42 -137
  29. data/.gitignore +0 -17
  30. data/.travis.yml +0 -10
  31. data/CONTRIBUTORS +0 -6
  32. data/Gemfile +0 -11
  33. data/History.txt +0 -4
  34. data/README.md.in +0 -76
  35. data/Rakefile +0 -27
  36. data/test/_lib.rb +0 -12
  37. data/test/integration/_lib/fixtures/env_printer/env_printer.rb +0 -26
  38. data/test/integration/_lib/fixtures/exit_during_upgrade/exiting_server.rb +0 -22
  39. data/test/integration/_lib/fixtures/exit_during_upgrade/upgrade_reexec.rb +0 -6
  40. data/test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb +0 -22
  41. data/test/integration/_lib/helpers/einhorn_helpers.rb +0 -143
  42. data/test/integration/_lib/helpers.rb +0 -4
  43. data/test/integration/_lib.rb +0 -6
  44. data/test/integration/startup.rb +0 -31
  45. data/test/integration/upgrading.rb +0 -157
  46. data/test/unit/einhorn/client.rb +0 -88
  47. data/test/unit/einhorn/command/interface.rb +0 -49
  48. data/test/unit/einhorn/command.rb +0 -21
  49. data/test/unit/einhorn/event.rb +0 -89
  50. data/test/unit/einhorn/worker_pool.rb +0 -39
  51. data/test/unit/einhorn.rb +0 -58
  52. /data/{LICENSE → LICENSE.txt} +0 -0
data/lib/einhorn.rb CHANGED
@@ -1,18 +1,36 @@
1
- require 'fcntl'
2
- require 'optparse'
3
- require 'pp'
4
- require 'set'
5
- require 'socket'
6
- require 'tmpdir'
7
- require 'yaml'
8
- require 'shellwords'
1
+ require "fcntl"
2
+ require "optparse"
3
+ require "pp"
4
+ require "set"
5
+ require "socket"
6
+ require "tmpdir"
7
+ require "yaml"
8
+ require "shellwords"
9
+ require "einhorn/safe_yaml"
9
10
 
10
11
  module Einhorn
11
12
  module AbstractState
12
- def default_state; raise NotImplementedError.new('Override in extended modules'); end
13
- def state; @state ||= default_state; end
14
- def state=(v); @state = v; end
15
- def dumpable_state; state; end
13
+ def default_state
14
+ raise NotImplementedError.new("Override in extended modules")
15
+ end
16
+
17
+ def state
18
+ @state ||= default_state
19
+ end
20
+
21
+ def state=(v)
22
+ @state = v
23
+ end
24
+
25
+ def dumpable_state
26
+ state
27
+ end
28
+
29
+ def respond_to_missing?(name)
30
+ ((name.to_s =~ /(.*)=$/) && state.has_key?($1.to_sym)) ||
31
+ state.has_key?(name) ||
32
+ default_state.has_key?(name)
33
+ end
16
34
 
17
35
  def method_missing(name, *args)
18
36
  if (name.to_s =~ /(.*)=$/) && state.has_key?($1.to_sym)
@@ -37,37 +55,39 @@ module Einhorn
37
55
  # about backwards/forwards compatibility for upgrades/downgrades
38
56
  def self.default_state
39
57
  {
40
- :children => {},
41
- :config => {:number => 1, :backlog => 100, :seconds => 1},
42
- :versions => {},
43
- :version => 0,
44
- :sockets => {},
45
- :orig_cmd => nil,
46
- :bind => [],
47
- :bind_fds => [],
48
- :cmd => nil,
49
- :script_name => nil,
50
- :respawn => true,
51
- :upgrading => false,
52
- :smooth_upgrade => false,
53
- :reloading_for_upgrade => false,
54
- :path => nil,
55
- :cmd_name => nil,
56
- :verbosity => 1,
57
- :generation => 0,
58
- :last_spinup => nil,
59
- :ack_mode => {:type => :timer, :timeout => 1},
60
- :kill_children_on_exit => false,
61
- :command_socket_as_fd => false,
62
- :socket_path => nil,
63
- :pidfile => nil,
64
- :lockfile => nil,
65
- :consecutive_deaths_before_ack => 0,
66
- :last_upgraded => nil,
67
- :nice => {:master => nil, :worker => nil, :renice_cmd => '/usr/bin/renice'},
68
- :reexec_commandline => nil,
69
- :drop_environment_variables => [],
70
- :signal_timeout => nil,
58
+ children: {},
59
+ config: {number: 1, backlog: 100, seconds: 1},
60
+ versions: {},
61
+ version: 0,
62
+ sockets: {},
63
+ orig_cmd: nil,
64
+ bind: [],
65
+ bind_fds: [],
66
+ bound_ports: [],
67
+ cmd: nil,
68
+ script_name: nil,
69
+ respawn: true,
70
+ upgrading: false,
71
+ smooth_upgrade: false,
72
+ reloading_for_upgrade: false,
73
+ path: nil,
74
+ cmd_name: nil,
75
+ verbosity: 1,
76
+ generation: 0,
77
+ last_spinup: nil,
78
+ ack_mode: {type: :timer, timeout: 1},
79
+ kill_children_on_exit: false,
80
+ command_socket_as_fd: false,
81
+ socket_path: nil,
82
+ pidfile: nil,
83
+ lockfile: nil,
84
+ consecutive_deaths_before_ack: 0,
85
+ last_upgraded: nil,
86
+ nice: {master: nil, worker: nil, renice_cmd: "/usr/bin/renice"},
87
+ reexec_commandline: nil,
88
+ drop_environment_variables: [],
89
+ signal_timeout: nil,
90
+ preloaded: false
71
91
  }
72
92
  end
73
93
  end
@@ -76,21 +96,20 @@ module Einhorn
76
96
  extend AbstractState
77
97
  def self.default_state
78
98
  {
79
- :whatami => :master,
80
- :preloaded => false,
81
- :script_name => nil,
82
- :argv => [],
83
- :environ => {},
84
- :has_outstanding_spinup_timer => false,
85
- :stateful => nil,
99
+ whatami: :master,
100
+ script_name: nil,
101
+ argv: [],
102
+ environ: {},
103
+ has_outstanding_spinup_timer: false,
104
+ stateful: nil,
86
105
  # Holds references so that the GC doesn't go and close your sockets.
87
- :socket_handles => Set.new
106
+ socket_handles: Set.new
88
107
  }
89
108
  end
90
109
  end
91
110
 
92
111
  def self.restore_state(state)
93
- parsed = YAML.load(state)
112
+ parsed = SafeYAML.load(state)
94
113
  updated_state, message = update_state(Einhorn::State, "einhorn", parsed[:state])
95
114
  Einhorn::State.state = updated_state
96
115
  Einhorn::Event.restore_persistent_descriptors(parsed[:persistent_descriptors])
@@ -112,15 +131,13 @@ module Einhorn
112
131
  # them all
113
132
  dead = []
114
133
  updated_state[:children].each do |pid, v|
115
- begin
116
- pid = Process.wait(pid, Process::WNOHANG)
117
- dead << pid if pid
118
- rescue Errno::ECHILD
119
- dead << pid
120
- end
134
+ pid = Process.wait(pid, Process::WNOHANG)
135
+ dead << pid if pid
136
+ rescue Errno::ECHILD
137
+ dead << pid
121
138
  end
122
139
  Einhorn::Event::Timer.open(0) do
123
- dead.each {|pid| Einhorn::Command.mourn(pid)}
140
+ dead.each { |pid| Einhorn::Command.cleanup(pid) }
124
141
  end
125
142
  end
126
143
 
@@ -129,12 +146,12 @@ module Einhorn
129
146
  deleted_keys = updated_state.keys - default.keys
130
147
  return [updated_state, message.first] if added_keys.length == 0 && deleted_keys.length == 0
131
148
 
132
- added_keys.each {|key| updated_state[key] = default[key]}
133
- deleted_keys.each {|key| updated_state.delete(key)}
149
+ added_keys.each { |key| updated_state[key] = default[key] }
150
+ deleted_keys.each { |key| updated_state.delete(key) }
134
151
 
135
152
  message << "adding default values for #{added_keys.inspect}"
136
153
  message << "deleting values for #{deleted_keys.inspect}"
137
- message = "State format for #{store_name} has changed: #{message.join(', ')}"
154
+ message = "State format for #{store_name} has changed: #{message.join(", ")}"
138
155
 
139
156
  # Can't print yet, since state hasn't been set, so we pass along the message.
140
157
  [updated_state, message]
@@ -149,42 +166,45 @@ module Einhorn
149
166
  sd = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
150
167
  Einhorn::Compat.cloexec!(sd, false)
151
168
 
152
- if flags.include?('r') || flags.include?('so_reuseaddr')
169
+ if flags.include?("r") || flags.include?("so_reuseaddr")
153
170
  sd.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
154
171
  end
155
172
 
156
173
  sd.bind(Socket.pack_sockaddr_in(port, addr))
157
174
  sd.listen(Einhorn::State.config[:backlog])
158
175
 
159
- if flags.include?('n') || flags.include?('o_nonblock')
176
+ if flags.include?("n") || flags.include?("o_nonblock")
160
177
  fl = sd.fcntl(Fcntl::F_GETFL)
161
178
  sd.fcntl(Fcntl::F_SETFL, fl | Fcntl::O_NONBLOCK)
162
179
  end
163
180
 
164
181
  Einhorn::TransientState.socket_handles << sd
165
- sd.fileno
182
+ [sd.fileno, sd.local_address.ip_port]
166
183
  end
167
184
 
168
185
  # Implement these ourselves so it plays nicely with state persistence
169
- def self.log_debug(msg, tag=nil)
170
- $stderr.puts("#{log_tag} DEBUG: #{msg}\n") if Einhorn::State.verbosity <= 0
171
- self.send_tagged_message(tag, msg) if tag
186
+ def self.log_debug(msg, tag = nil)
187
+ warn("#{log_tag} DEBUG: #{msg}\n") if Einhorn::State.verbosity <= 0
188
+ $stderr.flush
189
+ send_tagged_message(tag, msg) if tag
172
190
  end
173
- def self.log_info(msg, tag=nil)
174
- $stderr.puts("#{log_tag} INFO: #{msg}\n") if Einhorn::State.verbosity <= 1
175
- self.send_tagged_message(tag, msg) if tag
191
+
192
+ def self.log_info(msg, tag = nil)
193
+ warn("#{log_tag} INFO: #{msg}\n") if Einhorn::State.verbosity <= 1
194
+ $stderr.flush
195
+ send_tagged_message(tag, msg) if tag
176
196
  end
177
- def self.log_error(msg, tag=nil)
178
- $stderr.puts("#{log_tag} ERROR: #{msg}\n") if Einhorn::State.verbosity <= 2
179
- self.send_tagged_message(tag, "ERROR: #{msg}") if tag
197
+
198
+ def self.log_error(msg, tag = nil)
199
+ warn("#{log_tag} ERROR: #{msg}\n") if Einhorn::State.verbosity <= 2
200
+ $stderr.flush
201
+ send_tagged_message(tag, "ERROR: #{msg}") if tag
180
202
  end
181
203
 
182
- def self.send_tagged_message(tag, message, last=false)
204
+ def self.send_tagged_message(tag, message, last = false)
183
205
  Einhorn::Command::Interface.send_tagged_message(tag, message, last)
184
206
  end
185
207
 
186
- private
187
-
188
208
  def self.log_tag
189
209
  case whatami = Einhorn::TransientState.whatami
190
210
  when :master
@@ -197,17 +217,16 @@ module Einhorn
197
217
  "[UNKNOWN (#{whatami.inspect}) #{$$}]"
198
218
  end
199
219
  end
200
-
201
- public
220
+ private_class_method :log_tag
202
221
 
203
222
  def self.which(cmd)
204
- if cmd.include?('/')
205
- return cmd if File.exists?(cmd)
223
+ if cmd.include?("/")
224
+ return cmd if File.exist?(cmd)
206
225
  raise "Could not find #{cmd}"
207
226
  else
208
- ENV['PATH'].split(':').each do |f|
227
+ ENV["PATH"].split(":").each do |f|
209
228
  abs = File.join(f, cmd)
210
- return abs if File.exists?(abs)
229
+ return abs if File.exist?(abs)
211
230
  end
212
231
  raise "Could not find #{cmd} in PATH"
213
232
  end
@@ -217,29 +236,33 @@ module Einhorn
217
236
  def self.is_script(file)
218
237
  File.open(file) do |f|
219
238
  bytes = f.read(2)
220
- bytes == '#!'
239
+ bytes == "#!"
221
240
  end
222
241
  end
223
242
 
224
243
  def self.preload
225
- if path = Einhorn::State.path
244
+ if (path = Einhorn::State.path)
226
245
  set_argv(Einhorn::State.cmd, false)
227
246
 
228
247
  begin
248
+ # Reset preloaded state to false - this allows us to monitor for failed preloads during reloads.
249
+ Einhorn::State.preloaded = false
229
250
  # If it's not going to be requireable, then load it.
230
- if !path.end_with?('.rb') && File.exists?(path)
251
+ if !path.end_with?(".rb") && File.exist?(path)
231
252
  log_info("Loading #{path} (if this hangs, make sure your code can be properly loaded as a library)", :upgrade)
232
253
  load path
233
254
  else
234
255
  log_info("Requiring #{path} (if this hangs, make sure your code can be properly loaded as a library)", :upgrade)
235
256
  require path
257
+
258
+ force_move_to_oldgen if Einhorn::State.config[:gc_before_fork]
236
259
  end
237
- rescue Exception => e
260
+ rescue StandardError, LoadError => e
238
261
  log_info("Proceeding with postload -- could not load #{path}: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}", :upgrade)
239
262
  else
240
263
  if defined?(einhorn_main)
241
264
  log_info("Successfully loaded #{path}", :upgrade)
242
- Einhorn::TransientState.preloaded = true
265
+ Einhorn::State.preloaded = true
243
266
  else
244
267
  log_info("Proceeding with postload -- loaded #{path}, but no einhorn_main method was defined", :upgrade)
245
268
  end
@@ -247,10 +270,26 @@ module Einhorn
247
270
  end
248
271
  end
249
272
 
273
+ # Make the GC more copy-on-write friendly by forcibly incrementing the generation
274
+ # counter on all objects to its maximum value. Learn more at: https://github.com/ko1/nakayoshi_fork
275
+ def self.force_move_to_oldgen
276
+ log_info("Starting GC to improve copy-on-write memory sharing", :upgrade)
277
+
278
+ GC.start
279
+ 3.times do
280
+ GC.start(full_mark: false)
281
+ end
282
+
283
+ GC.compact if GC.respond_to?(:compact)
284
+
285
+ log_info("Finished GC after preloading", :upgrade)
286
+ end
287
+ private_class_method :force_move_to_oldgen
288
+
250
289
  def self.set_argv(cmd, set_ps_name)
251
290
  # TODO: clean up this hack
252
291
  idx = 0
253
- if cmd[0] =~ /(^|\/)ruby$/
292
+ if /(^|\/)ruby$/.match?(cmd[0])
254
293
  idx = 1
255
294
  elsif !is_script(cmd[0])
256
295
  log_info("WARNING: Going to set $0 to #{cmd[idx]}, but it doesn't look like a script")
@@ -267,7 +306,7 @@ module Einhorn
267
306
  $0 = worker_ps_name
268
307
  end
269
308
 
270
- ARGV[0..-1] = cmd[idx+1..-1]
309
+ ARGV[0..-1] = cmd[idx + 1..-1]
271
310
  log_info("Set#{set_ps_name ? " $0 = #{$0.inspect}, " : nil} ARGV = #{ARGV.inspect}")
272
311
  end
273
312
 
@@ -280,15 +319,15 @@ module Einhorn
280
319
  end
281
320
 
282
321
  def self.worker_ps_name
283
- Einhorn::State.cmd_name ? "ruby #{Einhorn::State.cmd_name}" : Einhorn::State.orig_cmd.join(' ')
322
+ Einhorn::State.cmd_name ? "ruby #{Einhorn::State.cmd_name}" : Einhorn::State.orig_cmd.join(" ")
284
323
  end
285
324
 
286
325
  def self.renice_self
287
326
  whatami = Einhorn::TransientState.whatami
288
- return unless nice = Einhorn::State.nice[whatami]
327
+ return unless (nice = Einhorn::State.nice[whatami])
289
328
  pid = $$
290
329
 
291
- unless nice.kind_of?(Fixnum)
330
+ unless nice.is_a?(Integer)
292
331
  raise "Nice must be a fixnum: #{nice.inspect}"
293
332
  end
294
333
 
@@ -304,32 +343,15 @@ module Einhorn
304
343
 
305
344
  def self.socketify_env!
306
345
  Einhorn::State.bind.each do |host, port, flags|
307
- fd = bind(host, port, flags)
346
+ fd, actual_port = bind(host, port, flags)
308
347
  Einhorn::State.bind_fds << fd
309
- end
310
- end
311
-
312
- # This duplicates some code from the environment path, but is
313
- # deprecated so that's ok.
314
- def self.socketify!(cmd)
315
- cmd.map! do |arg|
316
- if arg =~ /^(.*=|)srv:([^:]+):(\d+)((?:,\w+)*)$/
317
- log_error("Using deprecated command-line configuration for Einhorn; should upgrade to the environment variable interface.")
318
- opt = $1
319
- host = $2
320
- port = $3
321
- flags = $4.split(',').select {|flag| flag.length > 0}.map {|flag| flag.downcase}
322
- fd = (Einhorn::State.sockets[[host, port]] ||= bind(host, port, flags))
323
- "#{opt}#{fd}"
324
- else
325
- arg
326
- end
348
+ Einhorn::State.bound_ports << actual_port
327
349
  end
328
350
  end
329
351
 
330
352
  # Construct and a command and args that can be used to re-exec
331
353
  # Einhorn for upgrades.
332
- def self.upgrade_commandline(einhorn_flags=[])
354
+ def self.upgrade_commandline(einhorn_flags = [])
333
355
  cmdline = []
334
356
  if Einhorn::State.reexec_commandline
335
357
  cmdline += Einhorn::State.reexec_commandline
@@ -337,7 +359,7 @@ module Einhorn
337
359
  cmdline << Einhorn::TransientState.script_name
338
360
  end
339
361
  cmdline += einhorn_flags
340
- cmdline << '--'
362
+ cmdline << "--"
341
363
  cmdline += Einhorn::State.cmd
342
364
  [cmdline[0], cmdline[1..-1]]
343
365
  end
@@ -349,7 +371,7 @@ module Einhorn
349
371
  upgrade_sentinel = fork do
350
372
  Einhorn::TransientState.whatami = :upgrade_sentinel
351
373
  Einhorn.initialize_reload_environment
352
- Einhorn::Compat.exec(*Einhorn.upgrade_commandline(['--upgrade-check']))
374
+ Einhorn::Compat.exec(*Einhorn.upgrade_commandline(["--upgrade-check"]))
353
375
  end
354
376
  Process.wait(upgrade_sentinel)
355
377
  $?.exitstatus.zero?
@@ -374,10 +396,10 @@ module Einhorn
374
396
  # startup. Currently, this means the bundler and rbenv versions.
375
397
  def self.dump_environment_info
376
398
  log_info("Running under Ruby #{RUBY_VERSION}", :environment)
377
- log_info("Rbenv ruby version: #{ENV['RBENV_VERSION']}", :environment) if ENV['RBENV_VERSION']
399
+ log_info("Rbenv ruby version: #{ENV["RBENV_VERSION"]}", :environment) if ENV["RBENV_VERSION"]
378
400
  begin
379
- bundler_gem = Gem::Specification.find_by_name('bundler')
380
- log_info("Using Bundler #{bundler_gem.version.to_s}", :environment)
401
+ bundler_gem = Gem::Specification.find_by_name("bundler")
402
+ log_info("Using Bundler #{bundler_gem.version}", :environment)
381
403
  rescue Gem::LoadError
382
404
  end
383
405
  end
@@ -398,7 +420,6 @@ module Einhorn
398
420
  # TODO: don't actually alter ARGV[0]?
399
421
  Einhorn::State.cmd[0] = which(Einhorn::State.cmd[0])
400
422
  socketify_env!
401
- socketify!(Einhorn::State.cmd)
402
423
  end
403
424
 
404
425
  set_master_ps_name
@@ -411,6 +432,14 @@ module Einhorn
411
432
  Einhorn::State.reloading_for_upgrade = false
412
433
  end
413
434
 
435
+ # If setting a signal-timeout, timeout the event loop
436
+ # in the same timeframe, ensuring processes are culled
437
+ # on a regular basis.
438
+ if Einhorn::State.signal_timeout
439
+ Einhorn::Event.default_timeout = Einhorn::Event.default_timeout.nil? ?
440
+ Einhorn::State.signal_timeout : [Einhorn::State.signal_timeout, Einhorn::Event.default_timeout].min
441
+ end
442
+
414
443
  while Einhorn::State.respawn || Einhorn::State.children.size > 0
415
444
  log_debug("Entering event loop")
416
445
 
@@ -425,10 +454,10 @@ module Einhorn
425
454
  end
426
455
  end
427
456
 
428
- require 'einhorn/command'
429
- require 'einhorn/compat'
430
- require 'einhorn/client'
431
- require 'einhorn/event'
432
- require 'einhorn/worker'
433
- require 'einhorn/worker_pool'
434
- require 'einhorn/version'
457
+ require "einhorn/command"
458
+ require "einhorn/compat"
459
+ require "einhorn/client"
460
+ require "einhorn/event"
461
+ require "einhorn/worker"
462
+ require "einhorn/worker_pool"
463
+ require "einhorn/version"