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
@@ -1,5 +1,5 @@
1
- require 'tmpdir'
2
- require 'socket'
1
+ require "tmpdir"
2
+ require "socket"
3
3
 
4
4
  module Einhorn::Command
5
5
  module Interface
@@ -23,10 +23,8 @@ module Einhorn::Command
23
23
  # Don't nuke socket_path if we never successfully acquired it
24
24
  to_remove << socket_path if @@command_server
25
25
  to_remove.each do |file|
26
- begin
27
- File.unlink(file)
28
- rescue Errno::ENOENT
29
- end
26
+ File.unlink(file)
27
+ rescue Errno::ENOENT
30
28
  end
31
29
  end
32
30
  end
@@ -57,8 +55,8 @@ module Einhorn::Command
57
55
  # this lockfile lying around forever.
58
56
  def self.with_file_lock(&blk)
59
57
  path = lockfile
60
- File.open(path, 'w', 0600) do |f|
61
- unless f.flock(File::LOCK_EX|File::LOCK_NB)
58
+ File.open(path, "w", 0o600) do |f|
59
+ unless f.flock(File::LOCK_EX | File::LOCK_NB)
62
60
  raise "File lock already acquired by another Einhorn process. This likely indicates you tried to run Einhorn masters with the same cmd_name at the same time. This is a pretty rare race condition."
63
61
  end
64
62
 
@@ -96,7 +94,7 @@ module Einhorn::Command
96
94
  def self.write_pidfile
97
95
  file = pidfile
98
96
  Einhorn.log_info("Writing PID to #{file}")
99
- File.open(file, 'w') {|f| f.write($$)}
97
+ File.write(file, $$)
100
98
  end
101
99
 
102
100
  def self.uninit
@@ -107,12 +105,12 @@ module Einhorn::Command
107
105
  Einhorn::State.socket_path || default_socket_path
108
106
  end
109
107
 
110
- def self.default_socket_path(cmd_name=nil)
108
+ def self.default_socket_path(cmd_name = nil)
111
109
  cmd_name ||= Einhorn::State.cmd_name
112
- if cmd_name
113
- filename = "einhorn-#{cmd_name}.sock"
110
+ filename = if cmd_name
111
+ "einhorn-#{cmd_name}.sock"
114
112
  else
115
- filename = "einhorn.sock"
113
+ "einhorn.sock"
116
114
  end
117
115
  File.join(Dir.tmpdir, filename)
118
116
  end
@@ -121,12 +119,12 @@ module Einhorn::Command
121
119
  Einhorn::State.lockfile || default_lockfile_path
122
120
  end
123
121
 
124
- def self.default_lockfile_path(cmd_name=nil)
122
+ def self.default_lockfile_path(cmd_name = nil)
125
123
  cmd_name ||= Einhorn::State.cmd_name
126
- if cmd_name
127
- filename = "einhorn-#{cmd_name}.lock"
124
+ filename = if cmd_name
125
+ "einhorn-#{cmd_name}.lock"
128
126
  else
129
- filename = "einhorn.lock"
127
+ "einhorn.lock"
130
128
  end
131
129
  File.join(Dir.tmpdir, filename)
132
130
  end
@@ -135,12 +133,12 @@ module Einhorn::Command
135
133
  Einhorn::State.pidfile || default_pidfile
136
134
  end
137
135
 
138
- def self.default_pidfile(cmd_name=nil)
136
+ def self.default_pidfile(cmd_name = nil)
139
137
  cmd_name ||= Einhorn::State.cmd_name
140
- if cmd_name
141
- filename = "einhorn-#{cmd_name}.pid"
138
+ filename = if cmd_name
139
+ "einhorn-#{cmd_name}.pid"
142
140
  else
143
- filename = "einhorn.pid"
141
+ "einhorn.pid"
144
142
  end
145
143
  File.join(Dir.tmpdir, filename)
146
144
  end
@@ -162,11 +160,7 @@ module Einhorn::Command
162
160
  Einhorn::Command.stop_respawning
163
161
  exit(1)
164
162
  end
165
- trap_async("HUP") {Einhorn::Command.full_upgrade_smooth}
166
- trap_async("ALRM") do
167
- Einhorn.log_error("Upgrading using SIGALRM is deprecated. Please switch to SIGHUP")
168
- Einhorn::Command.full_upgrade_smooth
169
- end
163
+ trap_async("HUP") { Einhorn::Command.full_upgrade_smooth }
170
164
  trap_async("CHLD") {}
171
165
  trap_async("USR2") do
172
166
  Einhorn::Command.signal_all("USR2")
@@ -191,14 +185,14 @@ module Einhorn::Command
191
185
  end
192
186
 
193
187
  def self.remove_handlers
194
- %w{INT TERM QUIT HUP ALRM CHLD USR2}.each do |signal|
188
+ %w[INT TERM QUIT HUP ALRM CHLD USR2].each do |signal|
195
189
  Signal.trap(signal, "DEFAULT")
196
190
  end
197
191
  end
198
192
 
199
193
  ## Commands
200
- def self.command(name, description=nil, &code)
201
- @@commands[name] = {:description => description, :code => code}
194
+ def self.command(name, description = nil, &code)
195
+ @@commands[name] = {description: description, code: code}
202
196
  end
203
197
 
204
198
  def self.process_command(conn, command)
@@ -206,63 +200,63 @@ module Einhorn::Command
206
200
  request = Einhorn::Client::Transport.deserialize_message(command)
207
201
  rescue Einhorn::Client::Transport::ParseError
208
202
  end
209
- unless request.kind_of?(Hash)
203
+ unless request.is_a?(Hash)
210
204
  send_message(conn, "Could not parse command")
211
205
  return
212
206
  end
213
207
 
214
208
  message = generate_message(conn, request)
215
209
  if !message.nil?
216
- send_message(conn, message, request['id'], true)
210
+ send_message(conn, message, request["id"], true)
217
211
  else
218
212
  conn.log_debug("Got back nil response, so not responding to command.")
219
213
  end
220
214
  end
221
215
 
222
- def self.send_tagged_message(tag, message, last=false)
216
+ def self.send_tagged_message(tag, message, last = false)
223
217
  Einhorn::Event.connections.each do |conn|
224
- if id = conn.subscription(tag)
225
- self.send_message(conn, message, id, last)
218
+ if (id = conn.subscription(tag))
219
+ send_message(conn, message, id, last)
226
220
  conn.unsubscribe(tag) if last
227
221
  end
228
222
  end
229
223
  end
230
224
 
231
- def self.send_message(conn, message, request_id=nil, last=false)
225
+ def self.send_message(conn, message, request_id = nil, last = false)
232
226
  if request_id
233
- response = {'message' => message, 'request_id' => request_id }
234
- response['wait'] = true unless last
227
+ response = {"message" => message, "request_id" => request_id}
228
+ response["wait"] = true unless last
235
229
  else
236
230
  # support old-style protocol
237
- response = {'message' => message}
231
+ response = {"message" => message}
238
232
  end
239
233
  Einhorn::Client::Transport.send_message(conn, response)
240
234
  end
241
235
 
242
236
  def self.generate_message(conn, request)
243
- unless command_name = request['command']
237
+ unless (command_name = request["command"])
244
238
  return 'No "command" parameter provided; not sure what you want me to do.'
245
239
  end
246
240
 
247
- if command_spec = @@commands[command_name]
241
+ if (command_spec = @@commands[command_name])
248
242
  conn.log_debug("Received command: #{request.inspect}")
249
243
  begin
250
- return command_spec[:code].call(conn, request)
251
- rescue StandardError => e
244
+ command_spec[:code].call(conn, request)
245
+ rescue => e
252
246
  msg = "Error while processing command #{command_name.inspect}: #{e} (#{e.class})\n #{e.backtrace.join("\n ")}"
253
247
  conn.log_error(msg)
254
- return msg
248
+ msg
255
249
  end
256
250
  else
257
251
  conn.log_debug("Received unrecognized command: #{request.inspect}")
258
- return unrecognized_command(conn, request)
252
+ unrecognized_command(conn, request)
259
253
  end
260
254
  end
261
255
 
262
256
  def self.command_descriptions
263
257
  command_specs = @@commands.select do |_, spec|
264
258
  spec[:description]
265
- end.sort_by {|name, _| name}
259
+ end.sort_by { |name, _| name }
266
260
 
267
261
  command_specs.map do |name, spec|
268
262
  "#{name}: #{spec[:description]}"
@@ -270,16 +264,16 @@ module Einhorn::Command
270
264
  end
271
265
 
272
266
  def self.unrecognized_command(conn, request)
273
- <<EOF
274
- Unrecognized command: #{request['command'].inspect}
275
-
276
- #{command_descriptions}
277
- EOF
267
+ <<~EOF
268
+ Unrecognized command: #{request["command"].inspect}
269
+
270
+ #{command_descriptions}
271
+ EOF
278
272
  end
279
273
 
280
274
  # Used by workers
281
- command 'worker:ack' do |conn, request|
282
- if pid = request['pid']
275
+ command "worker:ack" do |conn, request|
276
+ if (pid = request["pid"])
283
277
  Einhorn::Command.register_manual_ack(pid)
284
278
  else
285
279
  conn.log_error("Invalid request (no pid): #{request.inspect}")
@@ -289,26 +283,37 @@ EOF
289
283
  nil
290
284
  end
291
285
 
286
+ command "worker:ping" do |conn, request|
287
+ if (pid = request["pid"])
288
+ Einhorn::Command.register_ping(pid, request["request_id"])
289
+ else
290
+ conn.log_error("Invalid request (no pid): #{request.inspect}")
291
+ end
292
+ # Throw away this connection in case the application forgets to
293
+ conn.close
294
+ nil
295
+ end
296
+
292
297
  # Used by einhornsh
293
- command 'ehlo' do |conn, request|
294
- <<EOF
295
- Welcome, #{request['user']}! You are speaking to Einhorn Master Process #{$$}#{Einhorn::State.cmd_name ? " (#{Einhorn::State.cmd_name})" : ''}.
296
- This is Einhorn #{Einhorn::VERSION}.
297
- EOF
298
+ command "ehlo" do |conn, request|
299
+ <<~EOF
300
+ Welcome, #{request["user"]}! You are speaking to Einhorn Master Process #{$$}#{Einhorn::State.cmd_name ? " (#{Einhorn::State.cmd_name})" : ""}.
301
+ This is Einhorn #{Einhorn::VERSION}.
302
+ EOF
298
303
  end
299
304
 
300
- command 'help', 'Print out available commands' do
301
- "You are speaking to the Einhorn command socket. You can run the following commands:
305
+ command "help", "Print out available commands" do
306
+ "You are speaking to the Einhorn command socket. You can run the following commands:
302
307
 
303
308
  #{command_descriptions}
304
309
  "
305
310
  end
306
311
 
307
- command 'state', "Get a dump of Einhorn's current state" do
308
- YAML.dump({:state => Einhorn::State.dumpable_state})
312
+ command "state", "Get a dump of Einhorn's current state" do
313
+ YAML.dump({state: Einhorn::State.dumpable_state})
309
314
  end
310
315
 
311
- command 'reload', 'Reload Einhorn' do |conn, request|
316
+ command "reload", "Reload Einhorn" do |conn, request|
312
317
  # TODO: make reload actually work (command socket reopening is
313
318
  # an issue). Would also be nice if user got a confirmation that
314
319
  # the reload completed, though that's not strictly necessary.
@@ -316,21 +321,21 @@ EOF
316
321
  # In the normal case, this will do a write
317
322
  # synchronously. Otherwise, the bytes will be stuck into the
318
323
  # buffer and lost upon reload.
319
- send_message(conn, 'Reloading, as commanded', request['id'], true)
324
+ send_message(conn, "Reloading, as commanded", request["id"], true)
320
325
  Einhorn::Command.reload
321
326
  end
322
327
 
323
- command 'inc', 'Increment the number of Einhorn child processes' do
328
+ command "inc", "Increment the number of Einhorn child processes" do
324
329
  Einhorn::Command.increment
325
330
  end
326
331
 
327
- command 'dec', 'Decrement the number of Einhorn child processes' do
332
+ command "dec", "Decrement the number of Einhorn child processes" do
328
333
  Einhorn::Command.decrement
329
334
  end
330
335
 
331
- command 'set_workers', 'Set the number of Einhorn child processes' do |conn, request|
332
- args = request['args']
333
- if message = validate_args(args)
336
+ command "set_workers", "Set the number of Einhorn child processes" do |conn, request|
337
+ args = request["args"]
338
+ if (message = validate_args(args))
334
339
  next message
335
340
  end
336
341
 
@@ -343,45 +348,45 @@ EOF
343
348
  Einhorn::Command.set_workers(count)
344
349
  end
345
350
 
346
- command 'quieter', 'Decrease verbosity' do
351
+ command "quieter", "Decrease verbosity" do
347
352
  Einhorn::Command.quieter
348
353
  end
349
354
 
350
- command 'louder', 'Increase verbosity' do
355
+ command "louder", "Increase verbosity" do
351
356
  Einhorn::Command.louder
352
357
  end
353
358
 
354
- command 'upgrade', 'Upgrade all Einhorn workers smoothly. This causes Einhorn to reload its own code as well.' do |conn, request|
359
+ command "upgrade", "Upgrade all Einhorn workers smoothly. This causes Einhorn to reload its own code as well." do |conn, request|
355
360
  # send first message directly for old clients that don't support request
356
361
  # ids or subscriptions. Everything else is sent tagged with request id
357
362
  # for new clients.
358
- send_message(conn, 'Upgrading smoothly, as commanded', request['id'])
359
- conn.subscribe(:upgrade, request['id'])
363
+ send_message(conn, "Upgrading smoothly, as commanded", request["id"])
364
+ conn.subscribe(:upgrade, request["id"])
360
365
  # If the app is preloaded this doesn't return.
361
366
  Einhorn::Command.full_upgrade_smooth
362
367
  nil
363
368
  end
364
369
 
365
- command 'upgrade_fleet', 'Upgrade all Einhorn workers a fleet at a time. This causes Einhorn to reload its own code as well.' do |conn, request|
370
+ command "upgrade_fleet", "Upgrade all Einhorn workers a fleet at a time. This causes Einhorn to reload its own code as well." do |conn, request|
366
371
  # send first message directly for old clients that don't support request
367
372
  # ids or subscriptions. Everything else is sent tagged with request id
368
373
  # for new clients.
369
- send_message(conn, 'Upgrading fleet, as commanded', request['id'])
370
- conn.subscribe(:upgrade, request['id'])
374
+ send_message(conn, "Upgrading fleet, as commanded", request["id"])
375
+ conn.subscribe(:upgrade, request["id"])
371
376
  # If the app is preloaded this doesn't return.
372
377
  Einhorn::Command.full_upgrade_fleet
373
378
  nil
374
379
  end
375
380
 
376
- command 'signal', 'Send one or more signals to all workers (args: SIG1 [SIG2 ...])' do |conn, request|
377
- args = request['args']
378
- if message = validate_args(args)
381
+ command "signal", "Send one or more signals to all workers (args: SIG1 [SIG2 ...])" do |conn, request|
382
+ args = request["args"]
383
+ if (message = validate_args(args))
379
384
  next message
380
385
  end
381
386
 
382
387
  args = normalize_signals(args)
383
388
 
384
- if message = validate_signals(args)
389
+ if (message = validate_signals(args))
385
390
  next message
386
391
  end
387
392
 
@@ -392,16 +397,16 @@ EOF
392
397
  results.join("\n")
393
398
  end
394
399
 
395
- command 'die', 'Send SIGNAL (default: SIGUSR2) to all workers, stop spawning new ones, and exit once all workers die (args: [SIGNAL])' do |conn, request|
400
+ command "die", "Send SIGNAL (default: SIGUSR2) to all workers, stop spawning new ones, and exit once all workers die (args: [SIGNAL])" do |conn, request|
396
401
  # TODO: dedup this code with signal
397
- args = request['args']
398
- if message = validate_args(args)
402
+ args = request["args"]
403
+ if (message = validate_args(args))
399
404
  next message
400
405
  end
401
406
 
402
407
  args = normalize_signals(args)
403
408
 
404
- if message = validate_signals(args)
409
+ if (message = validate_signals(args))
405
410
  next message
406
411
  end
407
412
 
@@ -413,24 +418,24 @@ EOF
413
418
  "Einhorn is going down! #{response}"
414
419
  end
415
420
 
416
- command 'config', 'Merge in a new set of config options. (Note: this will likely be subsumed by config file reloading at some point.)' do |conn, request|
417
- args = request['args']
418
- if message = validate_args(args)
421
+ command "config", "Merge in a new set of config options. (Note: this will likely be subsumed by config file reloading at some point.)" do |conn, request|
422
+ args = request["args"]
423
+ if (message = validate_args(args))
419
424
  next message
420
425
  end
421
426
 
422
427
  unless args.length > 0
423
- next 'Must pass in a YAML-encoded hash'
428
+ next "Must pass in a YAML-encoded hash"
424
429
  end
425
430
 
426
431
  begin
427
432
  # We do the joining so people don't need to worry about quoting
428
- parsed = YAML.load(args.join(' '))
429
- rescue ArgumentError => e
430
- next 'Could not parse argument. Must be a YAML-encoded hash'
433
+ parsed = SafeYAML.load(args.join(" "))
434
+ rescue ArgumentError
435
+ next "Could not parse argument. Must be a YAML-encoded hash"
431
436
  end
432
437
 
433
- unless parsed.kind_of?(Hash)
438
+ unless parsed.is_a?(Hash)
434
439
  next "Parsed argument is a #{parsed.class}, not a hash"
435
440
  end
436
441
 
@@ -440,11 +445,11 @@ EOF
440
445
  end
441
446
 
442
447
  def self.validate_args(args)
443
- return 'No args provided' unless args
444
- return 'Args must be an array' unless args.kind_of?(Array)
448
+ return "No args provided" unless args
449
+ return "Args must be an array" unless args.is_a?(Array)
445
450
 
446
451
  args.each do |arg|
447
- return "Argument is a #{arg.class}, not a string: #{arg.inspect}" unless arg.kind_of?(String)
452
+ return "Argument is a #{arg.class}, not a string: #{arg.inspect}" unless arg.is_a?(String)
448
453
  end
449
454
 
450
455
  nil