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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 59947ed43ab082da1be724e6a29d42b65127f90d
4
- data.tar.gz: 27c530b49b897af2ee2b922a0df9e03fa929f190
2
+ SHA256:
3
+ metadata.gz: cfcaf29cd13aac1fef51db90f04d6e531f8d7c8c3b944e02449c35dae44d8211
4
+ data.tar.gz: edc4413e67dad70cd6f18589499b06ac1bff9a6830a6494e3139a2ff6cb2cb18
5
5
  SHA512:
6
- metadata.gz: 5a041d009ad555382389ec005e2b288d4874562dcd4c63b02410c4305f79872d124ee68fa30ea99bd957ff4fa58a7aadc2da43391be5c66a4d414d4827e20bea
7
- data.tar.gz: 363370e818b00151b5a99412deff1990538d1fcc9f43b8d2428b15a58e66c2fbabe22a9a3d3327fea18c3f22dfc4136769b0d2268bfeea8ef3eb8472fb8f9abe
6
+ metadata.gz: 9a5b367df477b3ce85ab39d08cb07de5e21c207bc17b0a459e3e0e2f1124b8e83b03baac0ca9316680c1330ec989a05d2e727380bbe6a3bfb3ac9d2ae5fde633
7
+ data.tar.gz: 5f6675d7e9e086bb25bd1604949ad24326fc1e8a2f88c01e2f50cd570a3314e23edb73322cabd6cb696cf88e50e389a0ff9c82e44dbed2d8a04abdef0a012713
@@ -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
- Process.kill(:TERM, Process.pid)
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
- `ps -o rss= -p #{Process.pid}`.to_i / 1024
137
+ @rss_reader.rss
86
138
  end
87
139
 
88
- def report_status(status, reason, memory, requests, request_time)
89
- @logger.info "#{status} unicorn worker ##{Process.pid} for #{reason}. Requests: #{requests}, Time: #{request_time}, Memory: #{memory}MB"
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.info "Garbage collecting: took #{time}ms"
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
@@ -1,3 +1,3 @@
1
1
  module UnicornWrangler
2
- VERSION = "0.4.0"
2
+ VERSION = "0.6.0"
3
3
  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.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: 2017-11-15 00:00:00.000000000 Z
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.2.0
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
- rubyforge_project:
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