sidekiq-mem-warden 0.1.2 → 0.1.3

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
2
  SHA256:
3
- metadata.gz: 32004d7f9bc76aa4ed76cd96fe7f23bd1a4159fc51aad23254d894ced12a01d2
4
- data.tar.gz: a2cddf656f6f022041e084e58cbe598b71b169a3bd2c8d3e4a400f8f7efdb2cf
3
+ metadata.gz: e1a5f747821df9e7e8d6f2b15a195b11ef025b2f37210fb1c4a7e43ad88757e9
4
+ data.tar.gz: 42fb14442d1d7b9f9c74950f539c8e3ca2dc844657c2246a9940cb2e18498d6f
5
5
  SHA512:
6
- metadata.gz: f2813c65ffc5a0279e24a8780abb00942c3c932aaff25459141b559b887b272bc747d3fe9abe9b087a2ad2a001702fbb89aa7e8a99c02d40df80d0cb60039516
7
- data.tar.gz: 4d91e86d78fdb0d4b87ebf7f226464443c1c144361526516fb886838e8beb01064e276fa246d3019555ca52f26ccd3146517a00c1c2bc9612099c767caab1620
6
+ metadata.gz: 7fbf5b2e812b1be8a361b74d6417111a3cf05dc60cf549ee1d73c7d67a7c8f535f689804f93204fa05d57d6af4df5c697f891ec782a2d542df648f3677b79f3b
7
+ data.tar.gz: 2b861f4c40a464a6c81a473f02762143d40538117dcc98b56ec31b835b17b715a9df9b0d33305cd4bd40e2d4d9b63ec0ef23eb393e07267a82956cad3dbebff2
data/CHANGELOG.md CHANGED
@@ -8,4 +8,7 @@
8
8
 
9
9
  ## 0.1.2
10
10
  - Default memory limit set to 1 GB (1024 MB).
11
- - Default quiet timeout set to 5 minutes (300 seconds).
11
+ - Default grace time set to 5 minutes (300 seconds).
12
+
13
+ ## 0.1.3
14
+ - Replaced the internal watchdog with Sidekiq server middleware based on sidekiq-worker-killer.
data/Gemfile.lock CHANGED
@@ -1,17 +1,25 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sidekiq-mem-warden (0.1.2)
5
- sidekiq (>= 6.0)
4
+ sidekiq-mem-warden (0.1.3)
5
+ get_process_mem (>= 0.2.0)
6
+ sidekiq (>= 7.0)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ bigdecimal (3.3.1)
12
+ concurrent-ruby (1.3.6)
10
13
  connection_pool (2.5.3)
11
14
  diff-lcs (1.6.2)
15
+ ffi (1.17.3-arm64-darwin)
16
+ get_process_mem (1.0.0)
17
+ bigdecimal (>= 2.0)
18
+ ffi (~> 1.0)
12
19
  rack (2.2.17)
13
20
  rake (13.3.0)
14
- redis (4.8.1)
21
+ redis-client (0.26.2)
22
+ connection_pool
15
23
  rspec (3.13.1)
16
24
  rspec-core (~> 3.13.0)
17
25
  rspec-expectations (~> 3.13.0)
@@ -25,14 +33,14 @@ GEM
25
33
  diff-lcs (>= 1.2.0, < 2.0)
26
34
  rspec-support (~> 3.13.0)
27
35
  rspec-support (3.13.6)
28
- sidekiq (6.5.12)
29
- connection_pool (>= 2.2.5, < 3)
30
- rack (~> 2.0)
31
- redis (>= 4.5.0, < 5)
36
+ sidekiq (7.1.6)
37
+ concurrent-ruby (< 2)
38
+ connection_pool (>= 2.3.0)
39
+ rack (>= 2.2.4)
40
+ redis-client (>= 0.14.0)
32
41
 
33
42
  PLATFORMS
34
43
  arm64-darwin-24
35
- ruby
36
44
 
37
45
  DEPENDENCIES
38
46
  bundler (>= 1.17)
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sidekiq::Mem::Warden
2
2
 
3
- Sidekiq::Mem::Warden is a tiny, in-process watchdog that quiets a Sidekiq process when RSS exceeds a limit, waits for busy jobs to drain, then exits so your process manager can restart it.
3
+ Sidekiq::Mem::Warden is a tiny Sidekiq server middleware that watches RSS and shuts down bloated processes after a grace period, allowing your process manager to restart them.
4
4
 
5
5
  ## Installation
6
6
 
@@ -26,21 +26,23 @@ Configure it in your Sidekiq server process:
26
26
  require "sidekiq/mem/warden"
27
27
 
28
28
  Sidekiq.configure_server do |config|
29
- Sidekiq::Mem::Warden.configure do |c|
30
- c.memory_limit_mb = 1024
31
- c.check_interval = 15
32
- c.quiet_timeout = 30
33
- c.shutdown_timeout = 300
29
+ config.server_middleware do |chain|
30
+ chain.add Sidekiq::Mem::Warden,
31
+ max_rss: 1024,
32
+ grace_time: 300,
33
+ shutdown_wait: 30,
34
+ kill_signal: "SIGKILL",
35
+ gc: true,
36
+ skip_shutdown_if: ->(worker, job, queue) { false },
37
+ on_shutdown: ->(worker, job, queue) { nil }
34
38
  end
35
-
36
- Sidekiq::Mem::Warden.install!(config)
37
39
  end
38
40
  ```
39
41
 
40
42
  Operational notes:
41
43
 
42
44
  - The warden runs inside each Sidekiq process.
43
- - When memory exceeds the limit, it sends `TSTP` to quiet the process, sleeps for `quiet_timeout`, waits for `Sidekiq::Workers` to drain, then sends `TERM` to exit.
45
+ - When RSS exceeds `max_rss`, it quiets the process, waits for jobs to finish up to `grace_time`, then stops and sends `kill_signal`.
44
46
  - Ensure your supervisor (systemd, Kubernetes, etc.) is set to restart the process.
45
47
 
46
48
  ## Development
@@ -53,6 +55,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
53
55
 
54
56
  Bug reports and pull requests are welcome on GitHub at https://github.com/tedaford/sidekiq-mem-warden.
55
57
 
58
+ ## Credits
59
+
60
+ This gem is heavily based on the archived MIT-licensed `sidekiq-worker-killer` project.
61
+
56
62
  ## License
57
63
 
58
64
  The gem is available as open source under the terms of the GNU General Public License v2.0.
@@ -1,7 +1,7 @@
1
1
  module Sidekiq
2
2
  module Mem
3
- module Warden
4
- VERSION = "0.1.2"
3
+ class Warden
4
+ VERSION = "0.1.3"
5
5
  end
6
6
  end
7
7
  end
@@ -1,131 +1,119 @@
1
+ require "get_process_mem"
1
2
  require "sidekiq"
3
+ require "sidekiq/api"
4
+ begin
5
+ require "sidekiq/middleware/modules"
6
+ rescue LoadError
7
+ end
2
8
  require "sidekiq/mem/warden/version"
3
9
 
4
10
  module Sidekiq
5
11
  module Mem
6
- module Warden
7
- class Error < StandardError; end
8
-
9
- class Config
10
- attr_accessor :memory_limit_mb, :check_interval, :quiet_timeout, :shutdown_timeout, :logger
11
-
12
- def initialize
13
- @memory_limit_mb = 1024
14
- @check_interval = 15
15
- @quiet_timeout = 300
16
- @shutdown_timeout = 300
17
- @logger = nil
12
+ class Warden
13
+ include Sidekiq::ServerMiddleware if defined?(Sidekiq::ServerMiddleware)
14
+
15
+ MUTEX = Mutex.new
16
+
17
+ def initialize(options = {})
18
+ @max_rss = options.fetch(:max_rss, 1024)
19
+ @grace_time = options.fetch(:grace_time, 300)
20
+ @shutdown_wait = options.fetch(:shutdown_wait, 30)
21
+ @kill_signal = options.fetch(:kill_signal, "SIGKILL")
22
+ @gc = options.fetch(:gc, true)
23
+ @skip_shutdown = options.fetch(:skip_shutdown_if, proc { false })
24
+ @on_shutdown = options.fetch(:on_shutdown, nil)
25
+ end
26
+
27
+ def call(worker, job, queue)
28
+ yield
29
+
30
+ return unless @max_rss.to_i > 0
31
+ return unless current_rss > @max_rss
32
+
33
+ GC.start(full_mark: true, immediate_sweep: true) if @gc
34
+ return unless current_rss > @max_rss
35
+
36
+ if skip_shutdown?(worker, job, queue)
37
+ warn "current RSS #{current_rss} exceeds maximum RSS #{@max_rss}, " \
38
+ "however shutdown will be ignored"
39
+ return
18
40
  end
41
+
42
+ warn "current RSS #{current_rss} of #{identity} exceeds maximum RSS #{@max_rss}"
43
+ run_shutdown_hook(worker, job, queue)
44
+ request_shutdown
19
45
  end
20
46
 
21
- def self.configure
22
- yield(config)
47
+ private
48
+
49
+ def run_shutdown_hook(worker, job, queue)
50
+ @on_shutdown.respond_to?(:call) && @on_shutdown.call(worker, job, queue)
51
+ end
52
+
53
+ def skip_shutdown?(worker, job, queue)
54
+ @skip_shutdown.respond_to?(:call) && @skip_shutdown.call(worker, job, queue)
23
55
  end
24
56
 
25
- def self.install!(sidekiq_config = nil)
26
- sidekiq_config ||= Sidekiq
27
- sidekiq_config.on(:startup) do
28
- start_monitor(Warden::Monitor.new(config))
57
+ def request_shutdown
58
+ Thread.new do
59
+ shutdown if MUTEX.try_lock
29
60
  end
30
61
  end
31
62
 
32
- def self.config
33
- @config ||= Config.new
63
+ def shutdown
64
+ warn "sending quiet to #{identity}"
65
+ sidekiq_process.quiet!
66
+
67
+ sleep(5)
68
+
69
+ warn "shutting down #{identity} in #{@grace_time} seconds"
70
+ wait_job_finish_in_grace_time
71
+
72
+ warn "stopping #{identity}"
73
+ sidekiq_process.stop!
74
+
75
+ warn "waiting #{@shutdown_wait} seconds before sending #{@kill_signal} to #{identity}"
76
+ sleep(@shutdown_wait)
77
+
78
+ warn "sending #{@kill_signal} to #{identity}"
79
+ ::Process.kill(@kill_signal, ::Process.pid)
34
80
  end
35
81
 
36
- def self.start_monitor(monitor)
37
- @start_mutex ||= Mutex.new
38
- @start_mutex.synchronize do
39
- return if @started
40
- @started = true
41
- end
42
- monitor.start
82
+ def wait_job_finish_in_grace_time
83
+ start = Time.now
84
+ sleep(1) until grace_time_exceeded?(start) || jobs_finished?
43
85
  end
44
86
 
45
- class Monitor
46
- def initialize(config)
47
- @config = config
48
- @logger = config.logger || Sidekiq.logger
49
- @triggered = false
50
- @lock = Mutex.new
51
- @start_lock = Mutex.new
52
- @started = false
53
- end
87
+ def grace_time_exceeded?(start)
88
+ return false if @grace_time == Float::INFINITY
54
89
 
55
- def start
56
- @start_lock.synchronize do
57
- return if @started
58
- @started = true
59
- end
60
-
61
- @thread = Thread.new do
62
- Thread.current.name = "sidekiq-mem-warden" if Thread.current.respond_to?(:name=)
63
- loop do
64
- sleep @config.check_interval
65
- next unless over_limit?
66
- trigger!
67
- break
68
- end
69
- end
70
- end
90
+ start + @grace_time < Time.now
91
+ end
71
92
 
72
- private
73
-
74
- def trigger!
75
- @lock.synchronize do
76
- return if @triggered
77
- @triggered = true
78
- end
79
-
80
- @logger.warn("[sidekiq-mem-warden] RSS over limit (#{rss_mb}MB >= #{@config.memory_limit_mb}MB), quieting")
81
- begin
82
- Process.kill("TSTP", Process.pid)
83
- rescue StandardError => e
84
- @logger.warn("[sidekiq-mem-warden] failed to send TSTP: #{e.class}: #{e.message}")
85
- end
86
-
87
- sleep @config.quiet_timeout
88
- wait_for_idle(@config.shutdown_timeout)
89
-
90
- @logger.warn("[sidekiq-mem-warden] shutting down for restart")
91
- begin
92
- Process.kill("TERM", Process.pid)
93
- rescue StandardError => e
94
- @logger.warn("[sidekiq-mem-warden] failed to send TERM: #{e.class}: #{e.message}")
95
- end
96
- end
93
+ def jobs_finished?
94
+ sidekiq_process.stopping? && sidekiq_process["busy"] == 0
95
+ end
97
96
 
98
- def wait_for_idle(timeout_seconds)
99
- deadline = Time.now + timeout_seconds
100
- while Time.now < deadline
101
- busy = Sidekiq::Workers.new.size
102
- return if busy == 0
103
- sleep 1
104
- end
105
- @logger.warn("[sidekiq-mem-warden] timeout waiting for busy to drain; proceeding")
106
- end
97
+ def current_rss
98
+ ::GetProcessMem.new.mb
99
+ end
107
100
 
108
- def over_limit?
109
- rss_mb >= @config.memory_limit_mb
110
- end
101
+ def sidekiq_process
102
+ Sidekiq::ProcessSet.new.find do |process|
103
+ process["identity"] == identity
104
+ end || raise("No sidekiq worker with identity #{identity} found")
105
+ end
111
106
 
112
- def rss_mb
113
- (rss_kb.to_f / 1024).round(2)
107
+ def identity
108
+ if respond_to?(:config) && config
109
+ config[:identity] || config["identity"]
110
+ else
111
+ Sidekiq.default_configuration[:identity] || Sidekiq.default_configuration["identity"]
114
112
  end
113
+ end
115
114
 
116
- def rss_kb
117
- status_path = "/proc/#{Process.pid}/status"
118
- if File.exist?(status_path)
119
- status = File.read(status_path)
120
- match = status.match(/^VmRSS:\s+(\d+)\s+kB$/)
121
- return match[1].to_i if match
122
- end
123
-
124
- output = `ps -o rss= -p #{Process.pid}`.to_s.strip
125
- output.to_i
126
- rescue StandardError
127
- 0
128
- end
115
+ def warn(msg)
116
+ Sidekiq.logger.warn(msg)
129
117
  end
130
118
  end
131
119
  end
@@ -36,7 +36,8 @@ Gem::Specification.new do |spec|
36
36
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
37
  spec.require_paths = ["lib"]
38
38
 
39
- spec.add_dependency "sidekiq", ">= 6.0"
39
+ spec.add_dependency "get_process_mem", ">= 0.2.0"
40
+ spec.add_dependency "sidekiq", ">= 7.0"
40
41
  spec.add_development_dependency "bundler", ">= 1.17"
41
42
  spec.add_development_dependency "rake", ">= 10.0"
42
43
  spec.add_development_dependency "rspec", "~> 3.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-mem-warden
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - tedaford
@@ -9,20 +9,34 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: get_process_mem
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.2.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.2.0
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: sidekiq
14
28
  requirement: !ruby/object:Gem::Requirement
15
29
  requirements:
16
30
  - - ">="
17
31
  - !ruby/object:Gem::Version
18
- version: '6.0'
32
+ version: '7.0'
19
33
  type: :runtime
20
34
  prerelease: false
21
35
  version_requirements: !ruby/object:Gem::Requirement
22
36
  requirements:
23
37
  - - ">="
24
38
  - !ruby/object:Gem::Version
25
- version: '6.0'
39
+ version: '7.0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: bundler
28
42
  requirement: !ruby/object:Gem::Requirement