einhorn 0.7.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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"