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.
- checksums.yaml +7 -0
- data/Changes.md +10 -0
- data/README.md +36 -30
- data/bin/einhorn +17 -2
- data/einhorn.gemspec +23 -21
- data/example/pool_worker.rb +1 -1
- data/example/thin_example +8 -8
- data/example/time_server +5 -5
- data/lib/einhorn/client.rb +8 -9
- data/lib/einhorn/command/interface.rb +100 -95
- data/lib/einhorn/command.rb +167 -88
- data/lib/einhorn/compat.rb +7 -7
- data/lib/einhorn/event/abstract_text_descriptor.rb +31 -35
- data/lib/einhorn/event/ack_timer.rb +2 -2
- data/lib/einhorn/event/command_server.rb +7 -9
- data/lib/einhorn/event/connection.rb +1 -3
- data/lib/einhorn/event/loop_breaker.rb +2 -1
- data/lib/einhorn/event/persistent.rb +2 -2
- data/lib/einhorn/event/timer.rb +4 -4
- data/lib/einhorn/event.rb +29 -20
- data/lib/einhorn/prctl.rb +26 -0
- data/lib/einhorn/prctl_linux.rb +48 -0
- data/lib/einhorn/safe_yaml.rb +17 -0
- data/lib/einhorn/version.rb +1 -1
- data/lib/einhorn/worker.rb +67 -49
- data/lib/einhorn/worker_pool.rb +9 -9
- data/lib/einhorn.rb +155 -126
- metadata +42 -137
- data/.gitignore +0 -17
- data/.travis.yml +0 -10
- data/CONTRIBUTORS +0 -6
- data/Gemfile +0 -11
- data/History.txt +0 -4
- data/README.md.in +0 -76
- data/Rakefile +0 -27
- data/test/_lib.rb +0 -12
- data/test/integration/_lib/fixtures/env_printer/env_printer.rb +0 -26
- data/test/integration/_lib/fixtures/exit_during_upgrade/exiting_server.rb +0 -22
- data/test/integration/_lib/fixtures/exit_during_upgrade/upgrade_reexec.rb +0 -6
- data/test/integration/_lib/fixtures/upgrade_project/upgrading_server.rb +0 -22
- data/test/integration/_lib/helpers/einhorn_helpers.rb +0 -143
- data/test/integration/_lib/helpers.rb +0 -4
- data/test/integration/_lib.rb +0 -6
- data/test/integration/startup.rb +0 -31
- data/test/integration/upgrading.rb +0 -157
- data/test/unit/einhorn/client.rb +0 -88
- data/test/unit/einhorn/command/interface.rb +0 -49
- data/test/unit/einhorn/command.rb +0 -21
- data/test/unit/einhorn/event.rb +0 -89
- data/test/unit/einhorn/worker_pool.rb +0 -39
- data/test/unit/einhorn.rb +0 -58
- /data/{LICENSE → LICENSE.txt} +0 -0
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
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
|
-
|
27
|
-
|
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,
|
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.
|
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
|
-
|
110
|
+
filename = if cmd_name
|
111
|
+
"einhorn-#{cmd_name}.sock"
|
114
112
|
else
|
115
|
-
|
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
|
-
|
124
|
+
filename = if cmd_name
|
125
|
+
"einhorn-#{cmd_name}.lock"
|
128
126
|
else
|
129
|
-
|
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
|
-
|
138
|
+
filename = if cmd_name
|
139
|
+
"einhorn-#{cmd_name}.pid"
|
142
140
|
else
|
143
|
-
|
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
|
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] = {:
|
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.
|
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[
|
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
|
-
|
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 = {
|
234
|
-
response[
|
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 = {
|
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[
|
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
|
-
|
251
|
-
rescue
|
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
|
-
|
248
|
+
msg
|
255
249
|
end
|
256
250
|
else
|
257
251
|
conn.log_debug("Received unrecognized command: #{request.inspect}")
|
258
|
-
|
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
|
-
|
274
|
-
Unrecognized command: #{request[
|
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
|
282
|
-
if pid = request[
|
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
|
294
|
-
|
295
|
-
Welcome, #{request[
|
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
|
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
|
308
|
-
YAML.dump({:
|
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
|
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,
|
324
|
+
send_message(conn, "Reloading, as commanded", request["id"], true)
|
320
325
|
Einhorn::Command.reload
|
321
326
|
end
|
322
327
|
|
323
|
-
command
|
328
|
+
command "inc", "Increment the number of Einhorn child processes" do
|
324
329
|
Einhorn::Command.increment
|
325
330
|
end
|
326
331
|
|
327
|
-
command
|
332
|
+
command "dec", "Decrement the number of Einhorn child processes" do
|
328
333
|
Einhorn::Command.decrement
|
329
334
|
end
|
330
335
|
|
331
|
-
command
|
332
|
-
args = request[
|
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
|
351
|
+
command "quieter", "Decrease verbosity" do
|
347
352
|
Einhorn::Command.quieter
|
348
353
|
end
|
349
354
|
|
350
|
-
command
|
355
|
+
command "louder", "Increase verbosity" do
|
351
356
|
Einhorn::Command.louder
|
352
357
|
end
|
353
358
|
|
354
|
-
command
|
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,
|
359
|
-
conn.subscribe(:upgrade, request[
|
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
|
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,
|
370
|
-
conn.subscribe(:upgrade, request[
|
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
|
377
|
-
args = request[
|
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
|
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[
|
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
|
417
|
-
args = request[
|
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
|
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 =
|
429
|
-
rescue ArgumentError
|
430
|
-
next
|
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.
|
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
|
444
|
-
return
|
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.
|
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
|