Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restructuring #71

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pkg/
Gemfile.lock
.ruby-version
2 changes: 2 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--color
--require ./spec/spec_helper
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rvm:
- 1.9.3
- 2.2.9
- 2.3.6
- 2.4.3
- 2.5.0

before_install:
- bash -c "yes | gem uninstall -i /home/travis/.rvm/gems/ruby-2.5.0@global rake | true"
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Kazuki Ohta <kazuki.ohta _at_ gmail.com>
Sadayuki FURUHASHI <frsyuki _at_ gmail.com>
Jonathan Clem <jonathan _at_ jclem.net>
Vilius Luneckas <vilius.luneckas _at_ gmail.com>
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Release 0.4.5 - 2018/01/09
* Add rspec as development dependency
* Restructure code

Release 0.4.4 - 2015/11/13
* update for Unicorn 5

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# unicorn-worker-killer

[![Build Status](https://travis-ci.org/kzk/unicorn-worker-killer.svg?branch=master)](https://travis-ci.org/kzk/unicorn-worker-killer)

[Unicorn](http://unicorn.bogomips.org/) is widely used HTTP-server for Rack applications. One thing we thought Unicorn missed, is killing the Unicorn workers based on the number of requests and consumed memories.

`unicorn-worker-killer` gem provides automatic restart of Unicorn workers based on 1) max number of requests, and 2) process memory size (RSS), without affecting any requests. This will greatly improves site's stability by avoiding unexpected memory exhaustion at the application nodes.
Expand Down
5 changes: 4 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
require 'bundler'
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'

Bundler::GemHelper.install_tasks
RSpec::Core::RakeTask.new(:spec)

require 'rake/testtask'
task :default => [:build]
1 change: 0 additions & 1 deletion VERSION

This file was deleted.

88 changes: 8 additions & 80 deletions lib/unicorn/worker_killer.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
require 'unicorn'
require 'unicorn/worker_killer/configuration'
require 'get_process_mem'

require 'unicorn/worker_killer/configuration'
require 'unicorn/worker_killer/randomize'
require 'unicorn/worker_killer/middleware/max_requests'
require 'unicorn/worker_killer/middleware/oom'

module Unicorn::WorkerKiller
class << self
attr_accessor :configuration
Expand All @@ -13,7 +17,7 @@ class << self
# signal. A single signal is sent per request.
# @see http://unicorn.bogomips.org/SIGNALS.html
def self.kill_self(logger, start_time)
alive_sec = (Time.now - start_time).round
alive_sec = (Time.now - start_time).round
worker_pid = Process.pid

@@kill_attempts ||= 0
Expand All @@ -24,89 +28,13 @@ def self.kill_self(logger, start_time)
sig = :KILL if @@kill_attempts > configuration.max_term

logger.warn "#{self} send SIG#{sig} (pid: #{worker_pid}) alive: #{alive_sec} sec (trial #{@@kill_attempts})"
Process.kill sig, worker_pid
end

module Oom
# Killing the process must be occurred at the outside of the request. We're
# using similar techniques used by OobGC, to ensure actual killing doesn't
# affect the request.
#
# @see https://github.com/defunkt/unicorn/blob/master/lib/unicorn/oob_gc.rb#L40
def self.new(app, memory_limit_min = (1024**3), memory_limit_max = (2*(1024**3)), check_cycle = 16, verbose = false)
ObjectSpace.each_object(Unicorn::HttpServer) do |s|
s.extend(self)
s.instance_variable_set(:@_worker_memory_limit_min, memory_limit_min)
s.instance_variable_set(:@_worker_memory_limit_max, memory_limit_max)
s.instance_variable_set(:@_worker_check_cycle, check_cycle)
s.instance_variable_set(:@_worker_check_count, 0)
s.instance_variable_set(:@_verbose, verbose)
end
app # pretend to be Rack middleware since it was in the past
end

def randomize(integer)
RUBY_VERSION > "1.9" ? Random.rand(integer.abs) : rand(integer)
end

def process_client(client)
super(client) # Unicorn::HttpServer#process_client
return if @_worker_memory_limit_min == 0 && @_worker_memory_limit_max == 0

@_worker_process_start ||= Time.now
@_worker_memory_limit ||= @_worker_memory_limit_min + randomize(@_worker_memory_limit_max - @_worker_memory_limit_min + 1)
@_worker_check_count += 1
if @_worker_check_count % @_worker_check_cycle == 0
rss = GetProcessMem.new.bytes
logger.info "#{self}: worker (pid: #{Process.pid}) using #{rss} bytes." if @_verbose
if rss > @_worker_memory_limit
logger.warn "#{self}: worker (pid: #{Process.pid}) exceeds memory limit (#{rss} bytes > #{@_worker_memory_limit} bytes)"
Unicorn::WorkerKiller.kill_self(logger, @_worker_process_start)
end
@_worker_check_count = 0
end
end
end

module MaxRequests
# Killing the process must be occurred at the outside of the request. We're
# using similar techniques used by OobGC, to ensure actual killing doesn't
# affect the request.
#
# @see https://github.com/defunkt/unicorn/blob/master/lib/unicorn/oob_gc.rb#L40
def self.new(app, max_requests_min = 3072, max_requests_max = 4096, verbose = false)
ObjectSpace.each_object(Unicorn::HttpServer) do |s|
s.extend(self)
s.instance_variable_set(:@_worker_max_requests_min, max_requests_min)
s.instance_variable_set(:@_worker_max_requests_max, max_requests_max)
s.instance_variable_set(:@_verbose, verbose)
end

app # pretend to be Rack middleware since it was in the past
end

def randomize(integer)
RUBY_VERSION > "1.9" ? Random.rand(integer.abs) : rand(integer)
end

def process_client(client)
super(client) # Unicorn::HttpServer#process_client
return if @_worker_max_requests_min == 0 && @_worker_max_requests_max == 0

@_worker_process_start ||= Time.now
@_worker_cur_requests ||= @_worker_max_requests_min + randomize(@_worker_max_requests_max - @_worker_max_requests_min + 1)
@_worker_max_requests ||= @_worker_cur_requests
logger.info "#{self}: worker (pid: #{Process.pid}) has #{@_worker_cur_requests} left before being killed" if @_verbose

if (@_worker_cur_requests -= 1) <= 0
logger.warn "#{self}: worker (pid: #{Process.pid}) exceeds max number of requests (limit: #{@_worker_max_requests})"
Unicorn::WorkerKiller.kill_self(logger, @_worker_process_start)
end
end
Process.kill(sig, worker_pid)
end

def self.configure
self.configuration ||= Configuration.new

yield(configuration) if block_given?
end

Expand Down
16 changes: 9 additions & 7 deletions lib/unicorn/worker_killer/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
module Unicorn::WorkerKiller
class Configuration
attr_accessor :max_quit, :max_term, :sleep_interval
module Unicorn
module WorkerKiller
class Configuration
attr_accessor :max_quit, :max_term, :sleep_interval

def initialize
self.max_quit = 10
self.max_term = 15
self.sleep_interval = 1
def initialize
self.max_quit = 10
self.max_term = 15
self.sleep_interval = 1
end
end
end
end
43 changes: 43 additions & 0 deletions lib/unicorn/worker_killer/middleware/max_requests.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module Unicorn
module WorkerKiller
module MaxRequests
include Randomize

# Killing the process must be occurred at the outside of the request. We're
# using similar techniques used by OobGC, to ensure actual killing doesn't
# affect the request.
#
# @see https://github.com/defunkt/unicorn/blob/master/lib/unicorn/oob_gc.rb#L40
def self.new(app, max_requests_min = 3072, max_requests_max = 4096, verbose = false)
ObjectSpace.each_object(Unicorn::HttpServer) do |s|
s.extend(self)
s.instance_variable_set(:@_worker_max_requests_min, max_requests_min)
s.instance_variable_set(:@_worker_max_requests_max, max_requests_max)
s.instance_variable_set(:@_verbose, verbose)
end

app # pretend to be Rack middleware since it was in the past
end

def process_client(client)
super(client) # Unicorn::HttpServer#process_client

return if @_worker_max_requests_min.zero? && @_worker_max_requests_max.zero?

@_worker_process_start ||= Time.now
@_worker_cur_requests ||= @_worker_max_requests_min + randomize(@_worker_max_requests_max - @_worker_max_requests_min + 1)
@_worker_max_requests ||= @_worker_cur_requests

logger.info "#{self}: worker (pid: #{Process.pid}) has #{@_worker_cur_requests} left before being killed" if @_verbose

@_worker_cur_requests -= 1

return if @_worker_cur_requests > 0

logger.warn "#{self}: worker (pid: #{Process.pid}) exceeds max number of requests (limit: #{@_worker_max_requests})"

Unicorn::WorkerKiller.kill_self(logger, @_worker_process_start)
end
end
end
end
49 changes: 49 additions & 0 deletions lib/unicorn/worker_killer/middleware/oom.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module Unicorn
module WorkerKiller
module Oom
include Randomize

# Killing the process must be occurred at the outside of the request. We're
# using similar techniques used by OobGC, to ensure actual killing doesn't
# affect the request.
#
# @see https://github.com/defunkt/unicorn/blob/master/lib/unicorn/oob_gc.rb#L40
def self.new(app, memory_limit_min = (1024 ** 3), memory_limit_max = (2 * (1024 ** 3)), check_cycle = 16, verbose = false)
ObjectSpace.each_object(Unicorn::HttpServer) do |s|
s.extend(self)
s.instance_variable_set(:@_worker_memory_limit_min, memory_limit_min)
s.instance_variable_set(:@_worker_memory_limit_max, memory_limit_max)
s.instance_variable_set(:@_worker_check_cycle, check_cycle)
s.instance_variable_set(:@_worker_check_count, 0)
s.instance_variable_set(:@_verbose, verbose)
end

app # pretend to be Rack middleware since it was in the past
end

def process_client(client)
super(client) # Unicorn::HttpServer#process_client

return if @_worker_memory_limit_min == 0 && @_worker_memory_limit_max == 0

@_worker_process_start ||= Time.now
@_worker_memory_limit ||= @_worker_memory_limit_min + randomize(@_worker_memory_limit_max - @_worker_memory_limit_min + 1)
@_worker_check_count += 1

return unless @_worker_check_count % @_worker_check_cycle == 0

bytes_used = GetProcessMem.new.bytes

logger.info "#{self}: worker (pid: #{Process.pid}) using #{bytes_used} bytes." if @_verbose

if bytes_used > @_worker_memory_limit
logger.warn "#{self}: worker (pid: #{Process.pid}) exceeds memory limit (#{bytes_used} bytes > #{@_worker_memory_limit} bytes)"

Unicorn::WorkerKiller.kill_self(logger, @_worker_process_start)
end

@_worker_check_count = 0
end
end
end
end
9 changes: 9 additions & 0 deletions lib/unicorn/worker_killer/randomize.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Unicorn
module WorkerKiller
module Randomize
def randomize(integer)
RUBY_VERSION > '1.9' ? Random.rand(integer.abs) : rand(integer)
end
end
end
end
5 changes: 5 additions & 0 deletions lib/unicorn/worker_killer/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Unicorn
module WorkerKiller
VERSION = '0.4.5'
end
end
12 changes: 12 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require 'bundler/setup'
require 'rspec'
require 'timecop'

require 'unicorn/worker_killer'
require 'support/fake_http_server'

RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
6 changes: 6 additions & 0 deletions spec/support/fake_http_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
require 'unicorn'

class FakeHttpServer < Unicorn::HttpServer
def process_client(_client)
end
end
55 changes: 55 additions & 0 deletions spec/unicorn/worker_killer/middleware/max_requests_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
describe Unicorn::WorkerKiller::MaxRequests do
let(:app) { double }
let!(:http_server) { FakeHttpServer.new(app) }
let(:mount_middleware) { described_class.new(app, 5, 10, true) }

before { mount_middleware }

describe 'initialization' do
it { expect(mount_middleware).to eq(app) }
it { expect(http_server.instance_variable_get(:@_worker_max_requests_min)).to eq(5) }
it { expect(http_server.instance_variable_get(:@_worker_max_requests_max)).to eq(10) }
it { expect(http_server.instance_variable_get(:@_verbose)).to be_truthy }
end

describe '#process_client' do
subject { http_server.process_client(double) }

it 'decreases requests counter' do
http_server.instance_variable_set(:@_worker_cur_requests, 10)

expect {
subject
}.to change { http_server.instance_variable_get(:@_worker_cur_requests) }.by(-1)
end

it 'sets process start timestamp' do
Timecop.freeze

expect {
subject
}.to change { http_server.instance_variable_get(:@_worker_process_start) }.to(Time.now)
end

context 'when requests counter reaches zero' do
before { http_server.instance_variable_set(:@_worker_cur_requests, 1) }

it 'performs suicide' do
expect(Unicorn::WorkerKiller).to receive(:kill_self)

subject
end

context 'when max requests min and max are zeros' do
it 'does nothing' do
http_server.instance_variable_set(:@_worker_max_requests_min, 0)
http_server.instance_variable_set(:@_worker_max_requests_max, 0)

expect(Unicorn::WorkerKiller).not_to receive(:kill_self)

subject
end
end
end
end
end
Loading