unicorn_wrangler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []