unicorn_wrangler 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b9bc650286ed195d169cc671badd483392764a89
4
+ data.tar.gz: 93edb5fc2480c7234f787e3bc69341eb952ad9c0
5
+ SHA512:
6
+ metadata.gz: 947a5a76a418ad1ee71b18246b04d8a3264f1485426f5499424f346dbe596ffdd1b987e50486a376221fe01cfd00e31df27d5e6f3a18290936ad6a846d838c9c
7
+ data.tar.gz: 86bede236627be26a1ea6f85d1a3c577cac936dbabaeb97ab5e2dec3a9c62a015ec2c68333c998ea12620bf839e7de6470804e689d9f542620e5288152cf860a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2013 Michael Grosser <michael@grosser.it>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ module UnicornWrangler
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,143 @@
1
+ # - kills worker when they use too much memory
2
+ # - kills worker when they did too many requests (resets leaked memory)
3
+ # - runs GC out of band (does not block requests)
4
+
5
+ require 'benchmark'
6
+
7
+ module UnicornWrangler
8
+ STATS_NAMESPACE = 'unicorn'
9
+
10
+ class << self
11
+ attr_reader :handlers
12
+
13
+ # called from unicorn config (usually config/unicorn.rb)
14
+ # high level interface to keep setup consistent / simple
15
+ # set values to false to disable
16
+ def setup(
17
+ kill_after_requests: 10000,
18
+ gc_after_request_time: 10,
19
+ kill_on_too_much_memory: {},
20
+ logger:,
21
+ stats: nil # provide a statsd client with your apps namespace to collect stats
22
+ )
23
+ logger.info "Sending stats to under #{stats.namespace}.#{STATS_NAMESPACE}" if stats
24
+ @handlers = []
25
+ @handlers << RequestKiller.new(logger, stats, kill_after_requests) if kill_after_requests
26
+ @handlers << OutOfMemoryKiller.new(logger, stats, kill_on_too_much_memory) if kill_on_too_much_memory
27
+ @handlers << OutOfBandGC.new(logger, stats, gc_after_request_time) if gc_after_request_time
28
+ Unicorn::HttpServer.prepend UnicornExtension
29
+ end
30
+
31
+ # called from the unicorn server after each request
32
+ def perform_request
33
+ returned = nil
34
+ @requests ||= 0
35
+ @requests += 1
36
+ @request_time ||= 0
37
+ @request_time += Benchmark.realtime { returned = yield }
38
+ returned
39
+ ensure
40
+ @handlers.each { |handler| handler.call(@requests, @request_time) }
41
+ end
42
+ end
43
+
44
+ module UnicornExtension
45
+ def process_client(*)
46
+ UnicornWrangler.perform_request { super }
47
+ end
48
+ end
49
+
50
+ class Killer
51
+ def initialize(logger, stats)
52
+ @logger = logger
53
+ @stats = stats
54
+ end
55
+
56
+ private
57
+
58
+ # Kills the server, thereby resetting @requests / @request_time in the UnicornWrangler
59
+ #
60
+ # Possible issue: kill_worker is not meant to kill the server pid ... might have strange side effects
61
+ def kill(reason, memory, requests, request_time)
62
+ if @stats
63
+ @stats.increment("unicorn.kill.#{reason}")
64
+
65
+ @stats.histogram('unicorn.kill.memory', request_time)
66
+ @stats.histogram('unicorn.kill.total_requests', requests)
67
+ @stats.histogram('unicorn.kill.total_request_time', request_time)
68
+ end
69
+
70
+ @logger.info "Killing unicorn worker ##{Process.pid} for #{reason}. Requests: #{requests}, Time: #{request_time}, Memory: #{memory}MB"
71
+
72
+ Process.kill(:QUIT, Process.pid)
73
+ end
74
+
75
+ # expensive, do not run on every request
76
+ def used_memory
77
+ `ps -o rss= -p #{Process.pid}`.to_i / 1024
78
+ end
79
+ end
80
+
81
+ class OutOfMemoryKiller < Killer
82
+ def initialize(logger, stats, max: 20, check_every: 250)
83
+ super(logger, stats)
84
+ @max = max
85
+ @check_every = check_every
86
+ @logger.info "Killing workers when using more than #{@max}MB"
87
+ end
88
+
89
+ def call(requests, request_time)
90
+ return unless (requests % @check_every).zero? # avoid overhead of checking memory too often
91
+ return unless (memory = used_memory) > @max
92
+ kill :memory, memory, requests, request_time
93
+ end
94
+ end
95
+
96
+ class RequestKiller < Killer
97
+ def initialize(logger, stats, max_requests)
98
+ super(logger, stats)
99
+ @max_requests = max_requests
100
+ @logger.info "Killing workers after #{@max_requests} requests"
101
+ end
102
+
103
+ def call(requests, request_time)
104
+ kill(:requests, used_memory, requests, request_time) if requests >= @max_requests
105
+ end
106
+ end
107
+
108
+ # Do not run GC inside of requests, but only after a certain time spent in requests
109
+ #
110
+ # Alternative:
111
+ # https://github.com/tmm1/gctools
112
+ # which is more sophisticated and will result in less time spent GCing and less overall memory needed
113
+ class OutOfBandGC
114
+ def initialize(logger, stats, max_request_time)
115
+ @logger = logger
116
+ @stats = stats
117
+ @max_request_time = max_request_time
118
+ GC.disable
119
+ @logger.info "Garbage collecting after #{@max_request_time}s of request processing time"
120
+ @gc_ran_at = 0
121
+ end
122
+
123
+ def call(_requests, request_time)
124
+ time_since_last_gc = request_time - @gc_ran_at
125
+ return unless time_since_last_gc >= @max_request_time
126
+ @gc_ran_at = request_time
127
+
128
+ time = Benchmark.realtime do
129
+ GC.enable
130
+ GC.start
131
+ GC.disable
132
+ end
133
+
134
+ time = (time * 1000).round # s -> ms
135
+ if @stats
136
+ @stats.increment("#{STATS_NAMESPACE}.oobgc.runs")
137
+ @stats.timing("#{STATS_NAMESPACE}.oobgc.time", time)
138
+ end
139
+ @logger.info "Garbage collecting: took #{time}ms"
140
+ true
141
+ end
142
+ end
143
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unicorn_wrangler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Grosser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-09-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: michael@grosser.it
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - MIT-LICENSE
20
+ - lib/unicorn_wrangler.rb
21
+ - lib/unicorn_wrangler/version.rb
22
+ homepage: https://github.com/grosser/unicorn_wrangler
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 2.0.0
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 2.5.1
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: 'Unicorn: out of band GC / restart on max memory bloat / restart after X
46
+ requests'
47
+ test_files: []