unicorn_wrangler 0.4.0 → 0.6.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 +5 -5
- data/lib/unicorn_wrangler.rb +60 -8
- data/lib/unicorn_wrangler/rss_reader.rb +50 -0
- data/lib/unicorn_wrangler/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cfcaf29cd13aac1fef51db90f04d6e531f8d7c8c3b944e02449c35dae44d8211
|
4
|
+
data.tar.gz: edc4413e67dad70cd6f18589499b06ac1bff9a6830a6494e3139a2ff6cb2cb18
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a5b367df477b3ce85ab39d08cb07de5e21c207bc17b0a459e3e0e2f1124b8e83b03baac0ca9316680c1330ec989a05d2e727380bbe6a3bfb3ac9d2ae5fde633
|
7
|
+
data.tar.gz: 5f6675d7e9e086bb25bd1604949ad24326fc1e8a2f88c01e2f50cd570a3314e23edb73322cabd6cb696cf88e50e389a0ff9c82e44dbed2d8a04abdef0a012713
|
data/lib/unicorn_wrangler.rb
CHANGED
@@ -3,12 +3,14 @@
|
|
3
3
|
# - runs GC out of band (does not block requests)
|
4
4
|
|
5
5
|
require 'benchmark'
|
6
|
+
require 'unicorn_wrangler/rss_reader'
|
6
7
|
|
7
8
|
module UnicornWrangler
|
8
9
|
STATS_NAMESPACE = 'unicorn'
|
9
10
|
|
10
11
|
class << self
|
11
|
-
attr_reader :handlers
|
12
|
+
attr_reader :handlers, :requests, :request_time
|
13
|
+
attr_accessor :sending_myself_term
|
12
14
|
|
13
15
|
# called from unicorn config (usually config/unicorn.rb)
|
14
16
|
# high level interface to keep setup consistent / simple
|
@@ -17,6 +19,7 @@ module UnicornWrangler
|
|
17
19
|
kill_after_requests: 10000,
|
18
20
|
gc_after_request_time: 10,
|
19
21
|
kill_on_too_much_memory: {},
|
22
|
+
map_term_to_quit: false,
|
20
23
|
logger:,
|
21
24
|
stats: nil # provide a statsd client with your apps namespace to collect stats
|
22
25
|
)
|
@@ -25,9 +28,40 @@ module UnicornWrangler
|
|
25
28
|
@handlers << RequestKiller.new(logger, stats, kill_after_requests) if kill_after_requests
|
26
29
|
@handlers << OutOfMemoryKiller.new(logger, stats, kill_on_too_much_memory) if kill_on_too_much_memory
|
27
30
|
@handlers << OutOfBandGC.new(logger, stats, gc_after_request_time) if gc_after_request_time
|
31
|
+
|
32
|
+
@hooks = {}
|
33
|
+
if map_term_to_quit
|
34
|
+
# - on heroku & kubernetes all processes get TERM, so we need to trap in master and worker
|
35
|
+
# - trapping has to be done in the before_fork since unicorn sets up it's own traps on start
|
36
|
+
# - we cannot write to logger inside of a trap, so need to spawn a new Thread
|
37
|
+
# - manual test: add a slow route + rails s + curl + pkill -TERM -f 'unicorn master' - request finished?
|
38
|
+
@hooks[:before_fork] = -> do
|
39
|
+
Signal.trap :TERM do
|
40
|
+
Thread.new { logger.info 'master intercepting TERM and sending myself QUIT instead' }
|
41
|
+
Process.kill :QUIT, Process.pid
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@hooks[:after_fork] = ->(*) do
|
46
|
+
# Signal.trap returns the trap that unicorn set, which is an exit!(0) and calls that when sending myself term
|
47
|
+
previous_trap = Signal.trap :TERM do
|
48
|
+
if sending_myself_term
|
49
|
+
previous_trap.call
|
50
|
+
else
|
51
|
+
Thread.new { logger.info 'worker intercepting TERM and doing nothing. Wait for master to send QUIT' }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
28
57
|
Unicorn::HttpServer.prepend UnicornExtension
|
29
58
|
end
|
30
59
|
|
60
|
+
def kill_worker
|
61
|
+
self.sending_myself_term = true # no need to clean up since we are dead after
|
62
|
+
Process.kill(:TERM, Process.pid)
|
63
|
+
end
|
64
|
+
|
31
65
|
# called from the unicorn server after each request
|
32
66
|
def perform_request
|
33
67
|
returned = nil
|
@@ -39,9 +73,26 @@ module UnicornWrangler
|
|
39
73
|
ensure
|
40
74
|
@handlers.each { |handler| handler.call(@requests, @request_time) }
|
41
75
|
end
|
76
|
+
|
77
|
+
def perform_hook(name)
|
78
|
+
if hook = @hooks[name]
|
79
|
+
hook.call
|
80
|
+
end
|
81
|
+
end
|
42
82
|
end
|
43
83
|
|
44
84
|
module UnicornExtension
|
85
|
+
# call our hook and the users hook since only a single hook can be configured at a time
|
86
|
+
# we need to call super so the @<hook> variables get set and unset properly in after_fork to not leak memory
|
87
|
+
[:after_fork, :before_fork].each do |hook|
|
88
|
+
define_method("#{hook}=") do |value|
|
89
|
+
super(->(*args) do
|
90
|
+
UnicornWrangler.perform_hook(hook)
|
91
|
+
value.call(*args)
|
92
|
+
end)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
45
96
|
def process_client(*)
|
46
97
|
UnicornWrangler.perform_request { super }
|
47
98
|
end
|
@@ -59,6 +110,7 @@ module UnicornWrangler
|
|
59
110
|
def initialize(logger, stats)
|
60
111
|
@logger = logger
|
61
112
|
@stats = stats
|
113
|
+
@rss_reader = RssReader.new(logger: logger)
|
62
114
|
end
|
63
115
|
|
64
116
|
private
|
@@ -75,18 +127,18 @@ module UnicornWrangler
|
|
75
127
|
@stats.histogram("#{STATS_NAMESPACE}.kill.total_request_time", request_time)
|
76
128
|
end
|
77
129
|
|
78
|
-
report_status "Killing", reason, memory, requests, request_time
|
130
|
+
report_status "Killing", reason, memory, requests, request_time, :warn
|
79
131
|
|
80
|
-
|
132
|
+
UnicornWrangler.kill_worker
|
81
133
|
end
|
82
134
|
|
83
|
-
# expensive, do not run on every request
|
135
|
+
# RSS memory in MB. Can be expensive, do not run on every request
|
84
136
|
def used_memory
|
85
|
-
|
137
|
+
@rss_reader.rss
|
86
138
|
end
|
87
139
|
|
88
|
-
def report_status(status, reason, memory, requests, request_time)
|
89
|
-
@logger.
|
140
|
+
def report_status(status, reason, memory, requests, request_time, log_level = :debug)
|
141
|
+
@logger.send log_level, "#{status} unicorn worker ##{Process.pid} for #{reason}. Requests: #{requests}, Time: #{request_time}, Memory: #{memory}MB"
|
90
142
|
end
|
91
143
|
end
|
92
144
|
|
@@ -152,7 +204,7 @@ module UnicornWrangler
|
|
152
204
|
@stats.increment("#{STATS_NAMESPACE}.oobgc.runs")
|
153
205
|
@stats.timing("#{STATS_NAMESPACE}.oobgc.time", time)
|
154
206
|
end
|
155
|
-
@logger.
|
207
|
+
@logger.debug "Garbage collecting: took #{time}ms"
|
156
208
|
true
|
157
209
|
end
|
158
210
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Read RSS based on the OS we are in. When in linux, we can read the proc status file and parse out
|
2
|
+
# the RSS which is much faster than forking+execing ps.
|
3
|
+
module UnicornWrangler
|
4
|
+
class RssReader
|
5
|
+
LINUX = RbConfig::CONFIG['host_os'].start_with?('linux')
|
6
|
+
PS_CMD = 'ps -o rss= -p %d'.freeze
|
7
|
+
VM_RSS = /^VmRSS:\s+(\d+)\s+(\w+)/
|
8
|
+
UNITS = {
|
9
|
+
b: 1024**0,
|
10
|
+
kb: 1024**1,
|
11
|
+
mb: 1024**2,
|
12
|
+
gb: 1024**3,
|
13
|
+
tb: 1024**4,
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(logger:)
|
17
|
+
@logger = logger
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns RSS in megabytes; should work on Linux and Mac OS X
|
21
|
+
def rss(pid: Process.pid)
|
22
|
+
LINUX ? rss_linux(pid) : rss_posix(pid)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Fork/exec ps and parse result.
|
28
|
+
# Should work on any system with POSIX ps.
|
29
|
+
# ~4ms
|
30
|
+
# returns kb but we want mb
|
31
|
+
def rss_posix(pid)
|
32
|
+
`#{PS_CMD % [pid]}`.to_i / 1024
|
33
|
+
end
|
34
|
+
|
35
|
+
# Read from /proc/$pid/status. Linux only.
|
36
|
+
# ~100x faster and doesn't incur significant memory cost.
|
37
|
+
# file returns variable units, we want mb
|
38
|
+
def rss_linux(pid)
|
39
|
+
File.read("/proc/#{pid}/status").match(VM_RSS) do |match|
|
40
|
+
value, magnitude = match[1].to_i, UNITS.fetch(match[2].downcase.to_sym)
|
41
|
+
|
42
|
+
value * magnitude / UNITS.fetch(:mb)
|
43
|
+
end
|
44
|
+
rescue
|
45
|
+
# If the given pid is dead, file will not be found
|
46
|
+
@logger.warn 'Failed to read RSS from /proc, falling back to exec+ps' if @logger
|
47
|
+
rss_posix(pid)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unicorn_wrangler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-10-13 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: michael@grosser.it
|
@@ -18,6 +18,7 @@ extra_rdoc_files: []
|
|
18
18
|
files:
|
19
19
|
- MIT-LICENSE
|
20
20
|
- lib/unicorn_wrangler.rb
|
21
|
+
- lib/unicorn_wrangler/rss_reader.rb
|
21
22
|
- lib/unicorn_wrangler/version.rb
|
22
23
|
homepage: https://github.com/grosser/unicorn_wrangler
|
23
24
|
licenses:
|
@@ -31,15 +32,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
31
32
|
requirements:
|
32
33
|
- - ">="
|
33
34
|
- !ruby/object:Gem::Version
|
34
|
-
version: 2.
|
35
|
+
version: 2.5.0
|
35
36
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
37
|
requirements:
|
37
38
|
- - ">="
|
38
39
|
- !ruby/object:Gem::Version
|
39
40
|
version: '0'
|
40
41
|
requirements: []
|
41
|
-
|
42
|
-
rubygems_version: 2.5.1
|
42
|
+
rubygems_version: 3.0.3
|
43
43
|
signing_key:
|
44
44
|
specification_version: 4
|
45
45
|
summary: 'Unicorn: out of band GC / restart on max memory bloat / restart after X
|