seigen_watchdog 0.3.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +47 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE.txt +21 -0
- data/README.md +238 -0
- data/Rakefile +12 -0
- data/SECURITY.md +12 -0
- data/lib/seigen_watchdog/killers/base.rb +13 -0
- data/lib/seigen_watchdog/killers/signal.rb +25 -0
- data/lib/seigen_watchdog/limiters/base.rb +21 -0
- data/lib/seigen_watchdog/limiters/counter.rb +50 -0
- data/lib/seigen_watchdog/limiters/custom.rb +21 -0
- data/lib/seigen_watchdog/limiters/rss.rb +28 -0
- data/lib/seigen_watchdog/limiters/time.rb +42 -0
- data/lib/seigen_watchdog/monitor.rb +216 -0
- data/lib/seigen_watchdog/version.rb +5 -0
- data/lib/seigen_watchdog.rb +60 -0
- data/sig/seigen_watchdog/killers/base.rbs +11 -0
- data/sig/seigen_watchdog/killers/signal.rbs +20 -0
- data/sig/seigen_watchdog/limiters/base.rbs +19 -0
- data/sig/seigen_watchdog/limiters/counter.rbs +39 -0
- data/sig/seigen_watchdog/limiters/custom.rbs +16 -0
- data/sig/seigen_watchdog/limiters/rss.rbs +21 -0
- data/sig/seigen_watchdog/limiters/time.rbs +32 -0
- data/sig/seigen_watchdog/monitor.rbs +125 -0
- data/sig/seigen_watchdog/version.rbs +5 -0
- data/sig/seigen_watchdog.rbs +30 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a2a0a72ac34884493eebe4d7e9fe04e845136f0498408d0fc70dba907d9d54d9
|
|
4
|
+
data.tar.gz: ebcd461562d6755b976697096b2aff15c884278ecf075e63f8f505099f1412c9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3d4680614a457b4f74ce4958be27fe78b200a82e8f2cdb9b9207895e87fd49508ec982de39824c7702dba23a98448900b90f7dc569efde236b53a344334ccfe2
|
|
7
|
+
data.tar.gz: c64475ed1f9dc7b19bb9e581518798ed4b0b78bcb647f64b4defa44ddf6b3a7636a25f4d736f92ca49d4af538ccaf293327886c207df5f7cbe9cd162a02a3448
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rake
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
TargetRubyVersion: 3.3
|
|
7
|
+
NewCops: enable
|
|
8
|
+
|
|
9
|
+
Style/StringLiterals:
|
|
10
|
+
EnforcedStyle: single_quotes
|
|
11
|
+
|
|
12
|
+
Style/StringLiteralsInInterpolation:
|
|
13
|
+
EnforcedStyle: single_quotes
|
|
14
|
+
|
|
15
|
+
Metrics/MethodLength:
|
|
16
|
+
Max: 20
|
|
17
|
+
Exclude:
|
|
18
|
+
- 'spec/**/*.rb'
|
|
19
|
+
- 'lib/seigen_watchdog/monitor.rb'
|
|
20
|
+
|
|
21
|
+
Metrics/BlockLength:
|
|
22
|
+
Exclude:
|
|
23
|
+
- 'spec/**/*.rb'
|
|
24
|
+
|
|
25
|
+
Metrics/ClassLength:
|
|
26
|
+
Max: 200
|
|
27
|
+
|
|
28
|
+
Metrics/ParameterLists:
|
|
29
|
+
CountKeywordArgs: false
|
|
30
|
+
|
|
31
|
+
Layout/LeadingCommentSpace:
|
|
32
|
+
AllowRBSInlineAnnotation: true
|
|
33
|
+
|
|
34
|
+
RSpec/NamedSubject:
|
|
35
|
+
Enabled: false
|
|
36
|
+
|
|
37
|
+
RSpec/MultipleExpectations:
|
|
38
|
+
Enabled: false
|
|
39
|
+
|
|
40
|
+
RSpec/MultipleMemoizedHelpers:
|
|
41
|
+
Enabled: false
|
|
42
|
+
|
|
43
|
+
RSpec/NestedGroups:
|
|
44
|
+
Max: 4
|
|
45
|
+
|
|
46
|
+
RSpec/ExampleLength:
|
|
47
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# 0.1.0
|
|
2
|
+
- initial release with basic functionality:
|
|
3
|
+
- RSS memory limiter
|
|
4
|
+
- Execution time limiter
|
|
5
|
+
- Iteration count limiter
|
|
6
|
+
- Custom condition limiter
|
|
7
|
+
- Signal-based killer strategy
|
|
8
|
+
- Threadsafe background watchdog
|
|
9
|
+
- Configurable check interval
|
|
10
|
+
- Optional logging and exception handling
|
|
11
|
+
# 0.2.0
|
|
12
|
+
- store limiters as Hash, access specific limiter by it's name, for ex. `monitor.limiter(:rss)`
|
|
13
|
+
- counter limiter all init, increment/decrement, and reset with specified value
|
|
14
|
+
# 0.3.0
|
|
15
|
+
- monitor prevents multiple kill calls (only calls `killer.kill!` once even if limiters continue to be exceeded)
|
|
16
|
+
- added `killed?` method to check if killer was invoked
|
|
17
|
+
- added `seconds_since_killed` method to track time elapsed since kill
|
|
18
|
+
- `before_kill` callback now receives limiter name and limiter instance: `->(name, limiter)`
|
|
19
|
+
- limiter keys are now standardized to Symbol type
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Denis Talakevich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# SeigenWatchdog
|
|
2
|
+
|
|
3
|
+
Seigen (制限 — “limit, restriction”).
|
|
4
|
+
|
|
5
|
+
Monitors and gracefully terminates a Ruby application based on configurable memory usage, execution time, iteration count, or custom conditions. Threadsafe and easy to integrate.
|
|
6
|
+
|
|
7
|
+
How it works:
|
|
8
|
+
After setting up SeigenWatchdog with desired limiters and a killer strategy, it spawns a background thread that periodically checks the defined conditions. If any limiter exceeds its threshold, the specified killer strategy is invoked to terminate the application gracefully.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bundle add seigen_watchdog
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
gem install seigen_watchdog
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Ruby >= 3.3.0
|
|
27
|
+
- Dependencies:
|
|
28
|
+
- `get_process_mem` - for RSS memory monitoring
|
|
29
|
+
- `logger` - for optional debug logging
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'seigen_watchdog'
|
|
35
|
+
|
|
36
|
+
SeigenWatchdog.start(
|
|
37
|
+
check_interval: 5, # seconds or nil for no periodic checks
|
|
38
|
+
killer: SeigenWatchdog::Killers::Signal.new(signal: 'INT'),
|
|
39
|
+
limiters: {
|
|
40
|
+
rss: SeigenWatchdog::Limiters::RSS.new(max_rss: 200 * 1024 * 1024), # 200 MB
|
|
41
|
+
time: SeigenWatchdog::Limiters::Time.new(max_duration: 24 * 60 * 60), # 24 hours
|
|
42
|
+
jobs: SeigenWatchdog::Limiters::Counter.new(max_count: 1_000_000), # 1 million iterations
|
|
43
|
+
custom: SeigenWatchdog::Limiters::Custom.new(checker: -> { SomeCondition.met? }) # custom condition
|
|
44
|
+
},
|
|
45
|
+
logger: Logger.new($stdout), # optional logger, logs DEBUG for each check, INFO when killer is invoked
|
|
46
|
+
on_exception: ->(e) { Sentry.capture_exception(e) }, # optional exception handler
|
|
47
|
+
before_kill: ->(name, limiter) { Prometheus::KillInstrument.send_metrics(name, limiter.class.name) } # optional callback before kill
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# to increment particular count limiter
|
|
51
|
+
SeigenWatchdog.monitor.limiter(:jobs).increment # increment by 1
|
|
52
|
+
SeigenWatchdog.monitor.limiter(:jobs).increment(5) # increment by 5
|
|
53
|
+
|
|
54
|
+
# to perform check manually (if check_interval is nil)
|
|
55
|
+
SeigenWatchdog.monitor.check_once
|
|
56
|
+
|
|
57
|
+
# to stop the watchdog
|
|
58
|
+
SeigenWatchdog.stop
|
|
59
|
+
|
|
60
|
+
# to check if watchdog is running
|
|
61
|
+
SeigenWatchdog.started? # => true or false
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API Reference
|
|
65
|
+
|
|
66
|
+
### Module Methods
|
|
67
|
+
|
|
68
|
+
#### `SeigenWatchdog.start(check_interval:, killer:, limiters:, logger: nil, on_exception: nil, before_kill: nil)`
|
|
69
|
+
Starts the watchdog monitor with the specified configuration.
|
|
70
|
+
|
|
71
|
+
**Parameters:**
|
|
72
|
+
- `check_interval` - Interval in seconds between checks, or `nil` for manual checks only
|
|
73
|
+
- `killer` - Killer strategy instance (e.g., `SeigenWatchdog::Killers::Signal.new(signal: 'INT')`)
|
|
74
|
+
- `limiters` - Hash of limiter instances (keys are limiter names, values are limiter instances)
|
|
75
|
+
- `logger` - Optional logger instance for debug/info logging
|
|
76
|
+
- `on_exception` - Optional callback proc for exception handling (receives exception as argument)
|
|
77
|
+
- `before_kill` - Optional callback proc invoked before killing (receives limiter name and limiter instance as arguments)
|
|
78
|
+
|
|
79
|
+
**Returns:** Monitor instance
|
|
80
|
+
|
|
81
|
+
#### `SeigenWatchdog.stop`
|
|
82
|
+
Stops the watchdog monitor and terminates the background thread.
|
|
83
|
+
|
|
84
|
+
#### `SeigenWatchdog.started?`
|
|
85
|
+
Returns `true` if the watchdog is currently running, `false` otherwise.
|
|
86
|
+
|
|
87
|
+
#### `SeigenWatchdog.monitor`
|
|
88
|
+
Returns the current monitor instance, or `nil` if not started.
|
|
89
|
+
|
|
90
|
+
### Monitor Instance Methods
|
|
91
|
+
|
|
92
|
+
#### `monitor.check_once`
|
|
93
|
+
Performs a single manual check of all limiters. Useful when `check_interval` is `nil`.
|
|
94
|
+
|
|
95
|
+
**Returns:** `true` if a limit was exceeded and killer was invoked, `false` otherwise.
|
|
96
|
+
|
|
97
|
+
#### `monitor.limiter(name)`
|
|
98
|
+
Returns a limiter instance by its name.
|
|
99
|
+
|
|
100
|
+
**Parameters:**
|
|
101
|
+
- `name` - Symbol or String name of the limiter
|
|
102
|
+
|
|
103
|
+
**Returns:** Limiter instance
|
|
104
|
+
|
|
105
|
+
**Raises:** `KeyError` if limiter with the given name doesn't exist
|
|
106
|
+
|
|
107
|
+
#### `monitor.limiters`
|
|
108
|
+
Returns a hash of all limiters. Modifications to the returned hash won't affect internal state, but limiter instances are shared.
|
|
109
|
+
|
|
110
|
+
**Returns:** Hash of limiters (keys are names, values are limiter instances)
|
|
111
|
+
|
|
112
|
+
#### `monitor.killed?`
|
|
113
|
+
Returns whether the killer has been invoked.
|
|
114
|
+
|
|
115
|
+
**Returns:** `true` if killer was invoked, `false` otherwise.
|
|
116
|
+
|
|
117
|
+
#### `monitor.seconds_since_killed`
|
|
118
|
+
Returns the number of seconds elapsed since the killer was invoked.
|
|
119
|
+
|
|
120
|
+
**Returns:** Float representing seconds since kill, or `nil` if killer has not been invoked.
|
|
121
|
+
|
|
122
|
+
#### `monitor.seconds_after_last_check`
|
|
123
|
+
Returns the number of seconds elapsed since the last check was performed.
|
|
124
|
+
|
|
125
|
+
**Returns:** Float representing seconds since last check, or `nil` if no check has been performed.
|
|
126
|
+
|
|
127
|
+
### Limiters
|
|
128
|
+
|
|
129
|
+
#### `SeigenWatchdog::Limiters::RSS.new(max_rss:)`
|
|
130
|
+
Monitors RSS (Resident Set Size) memory usage.
|
|
131
|
+
- `max_rss` - Maximum RSS in bytes
|
|
132
|
+
|
|
133
|
+
#### `SeigenWatchdog::Limiters::Time.new(max_duration:)`
|
|
134
|
+
Monitors execution time since limiter creation.
|
|
135
|
+
- `max_duration` - Maximum duration in seconds
|
|
136
|
+
|
|
137
|
+
#### `SeigenWatchdog::Limiters::Counter.new(max_count:, initial: 0)`
|
|
138
|
+
Monitors iteration count with manual incrementing.
|
|
139
|
+
- `max_count` - Maximum count before exceeding
|
|
140
|
+
- `initial` - Initial counter value (default: 0)
|
|
141
|
+
|
|
142
|
+
**Instance Methods:**
|
|
143
|
+
- `increment(count = 1)` - Increments the counter by the specified amount (default: 1)
|
|
144
|
+
- `decrement(count = 1)` - Decrements the counter by the specified amount (default: 1)
|
|
145
|
+
- `reset(initial = 0)` - Resets the counter to the specified value (default: 0)
|
|
146
|
+
|
|
147
|
+
**Usage:**
|
|
148
|
+
```ruby
|
|
149
|
+
# Access counter limiter via monitor
|
|
150
|
+
counter = SeigenWatchdog.monitor.limiter(:jobs)
|
|
151
|
+
counter.increment # increment by 1
|
|
152
|
+
counter.increment(5) # increment by 5
|
|
153
|
+
counter.decrement # decrement by 1
|
|
154
|
+
counter.decrement(3) # decrement by 3
|
|
155
|
+
counter.reset # reset to 0
|
|
156
|
+
counter.reset(10) # reset to 10
|
|
157
|
+
|
|
158
|
+
# Create counter with custom initial value
|
|
159
|
+
SeigenWatchdog::Limiters::Counter.new(max_count: 100, initial: 50)
|
|
160
|
+
# Counter starts at 50, will exceed at 100
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### `SeigenWatchdog::Limiters::Custom.new(checker:)`
|
|
164
|
+
Custom condition limiter using a proc.
|
|
165
|
+
- `checker` - Proc that returns `true` when limit is exceeded
|
|
166
|
+
|
|
167
|
+
### Killers
|
|
168
|
+
|
|
169
|
+
#### `SeigenWatchdog::Killers::Signal.new(signal:)`
|
|
170
|
+
Terminates the process by sending a signal.
|
|
171
|
+
- `signal` - Signal name as string or symbol (e.g., `'INT'`, `:TERM`)
|
|
172
|
+
|
|
173
|
+
## Callbacks
|
|
174
|
+
|
|
175
|
+
### `before_kill` Callback
|
|
176
|
+
|
|
177
|
+
The `before_kill` callback is invoked immediately before the killer strategy is executed when a limiter exceeds its threshold. This allows you to perform cleanup operations, send metrics, or log information about which limit was exceeded.
|
|
178
|
+
|
|
179
|
+
**Callback signature:**
|
|
180
|
+
```ruby
|
|
181
|
+
->(name, limiter) { ... }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Arguments:**
|
|
185
|
+
- `name` - Symbol representing the limiter name (e.g., `:rss`, `:time`, `:jobs`)
|
|
186
|
+
- `limiter` - The limiter instance that exceeded its threshold
|
|
187
|
+
|
|
188
|
+
**Example use cases:**
|
|
189
|
+
```ruby
|
|
190
|
+
# Send metrics to monitoring system
|
|
191
|
+
before_kill: ->(name, limiter) {
|
|
192
|
+
Prometheus::KillInstrument.send_metrics(name, limiter.class.name)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Log detailed information with limiter name
|
|
196
|
+
before_kill: ->(name, limiter) {
|
|
197
|
+
Rails.logger.warn("Process killed due to #{name} (#{limiter.class.name}) limit exceeded")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Send alert with limiter name
|
|
201
|
+
before_kill: ->(name, limiter) {
|
|
202
|
+
Sentry.capture_message("Watchdog killing process: #{name}")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Perform cleanup based on limiter type
|
|
206
|
+
before_kill: ->(name, limiter) {
|
|
207
|
+
case limiter
|
|
208
|
+
when SeigenWatchdog::Limiters::RSS
|
|
209
|
+
Rails.logger.warn("Memory limit exceeded (#{name}): #{limiter.max_rss / 1024 / 1024} MB")
|
|
210
|
+
when SeigenWatchdog::Limiters::Time
|
|
211
|
+
Rails.logger.warn("Time limit exceeded (#{name}): #{limiter.max_duration} seconds")
|
|
212
|
+
end
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Exception handling:**
|
|
217
|
+
If the `before_kill` callback raises an exception, it will be handled by the `on_exception` callback (if provided) and the killer will still be invoked to ensure the process terminates as expected.
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
222
|
+
|
|
223
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
Generate sig using the following command:
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
bundle exec rbs-inline --opt-out --output=sig lib
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/senid231/seigen_watchdog.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/SECURITY.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Killers
|
|
5
|
+
# Killer that sends a signal to the current process
|
|
6
|
+
class Signal < Base
|
|
7
|
+
# @rbs @signal: String
|
|
8
|
+
|
|
9
|
+
attr_reader :signal #: String
|
|
10
|
+
|
|
11
|
+
# @rbs signal: String | Symbol
|
|
12
|
+
# @rbs return: void
|
|
13
|
+
def initialize(signal:)
|
|
14
|
+
super()
|
|
15
|
+
@signal = signal.to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Sends the signal to the current process
|
|
19
|
+
# @rbs return: void
|
|
20
|
+
def kill!
|
|
21
|
+
Process.kill(@signal, Process.pid)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Base class for all limiters
|
|
6
|
+
class Base
|
|
7
|
+
# @rbs return: bool
|
|
8
|
+
def exceeded?
|
|
9
|
+
raise NotImplementedError, "#{self.class} must implement #exceeded?"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Called when the limiter is started (when monitor initializes)
|
|
13
|
+
# @rbs return: void
|
|
14
|
+
def started; end
|
|
15
|
+
|
|
16
|
+
# Called when the limiter is stopped (when monitor stops)
|
|
17
|
+
# @rbs return: void
|
|
18
|
+
def stopped; end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on iteration count
|
|
6
|
+
class Counter < Base
|
|
7
|
+
# @rbs @max_count: Integer
|
|
8
|
+
# @rbs @count: Integer
|
|
9
|
+
# @rbs @mutex: Thread::Mutex
|
|
10
|
+
|
|
11
|
+
attr_reader :max_count #: Integer
|
|
12
|
+
|
|
13
|
+
# @rbs max_count: Integer
|
|
14
|
+
# @rbs initial: Integer
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def initialize(max_count:, initial: 0)
|
|
17
|
+
super()
|
|
18
|
+
@max_count = max_count
|
|
19
|
+
@count = initial
|
|
20
|
+
@mutex = Mutex.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Increments the counter by the specified amount
|
|
24
|
+
# @rbs count: Integer
|
|
25
|
+
# @rbs return: void
|
|
26
|
+
def increment(count = 1)
|
|
27
|
+
@mutex.synchronize { @count += count }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Decrements the counter by the specified amount
|
|
31
|
+
# @rbs count: Integer
|
|
32
|
+
# @rbs return: void
|
|
33
|
+
def decrement(count = 1)
|
|
34
|
+
@mutex.synchronize { @count -= count }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Resets the counter to the specified initial value
|
|
38
|
+
# @rbs initial: Integer
|
|
39
|
+
# @rbs return: void
|
|
40
|
+
def reset(initial = 0)
|
|
41
|
+
@mutex.synchronize { @count = initial }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @rbs return: bool
|
|
45
|
+
def exceeded?
|
|
46
|
+
@mutex.synchronize { @count >= @max_count }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on a custom checker lambda
|
|
6
|
+
class Custom < Base
|
|
7
|
+
# @rbs @checker: Proc | #call
|
|
8
|
+
# @rbs checker: Proc | #call
|
|
9
|
+
# @rbs return: void
|
|
10
|
+
def initialize(checker:)
|
|
11
|
+
super()
|
|
12
|
+
@checker = checker
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs return: bool
|
|
16
|
+
def exceeded?
|
|
17
|
+
@checker.call
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'get_process_mem'
|
|
4
|
+
|
|
5
|
+
module SeigenWatchdog
|
|
6
|
+
module Limiters
|
|
7
|
+
# Limiter based on RSS memory usage
|
|
8
|
+
class RSS < Base
|
|
9
|
+
# @rbs @max_rss: Integer
|
|
10
|
+
# @rbs @mem: GetProcessMem
|
|
11
|
+
|
|
12
|
+
attr_reader :max_rss #: Integer
|
|
13
|
+
|
|
14
|
+
# @rbs max_rss: Integer
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def initialize(max_rss:)
|
|
17
|
+
super()
|
|
18
|
+
@max_rss = max_rss
|
|
19
|
+
@mem = GetProcessMem.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs return: bool
|
|
23
|
+
def exceeded?
|
|
24
|
+
@mem.bytes >= @max_rss
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on execution time
|
|
6
|
+
class Time < Base
|
|
7
|
+
# @rbs @max_duration: Numeric
|
|
8
|
+
# @rbs @start_time: Float?
|
|
9
|
+
|
|
10
|
+
attr_reader :start_time #: Float?
|
|
11
|
+
attr_reader :max_duration #: Numeric
|
|
12
|
+
|
|
13
|
+
# @rbs max_duration: Numeric
|
|
14
|
+
# @rbs return: void
|
|
15
|
+
def initialize(max_duration:)
|
|
16
|
+
super()
|
|
17
|
+
@max_duration = max_duration
|
|
18
|
+
@start_time = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Called when monitor starts - records the start time
|
|
22
|
+
# @rbs return: void
|
|
23
|
+
def started
|
|
24
|
+
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs return: bool
|
|
28
|
+
def exceeded?
|
|
29
|
+
elapsed_time >= @max_duration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# @rbs return: Float
|
|
35
|
+
def elapsed_time
|
|
36
|
+
return 0.0 if @start_time.nil?
|
|
37
|
+
|
|
38
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
# Monitor class that checks limiters and invokes the killer when needed
|
|
5
|
+
class Monitor
|
|
6
|
+
# @rbs @check_interval: Numeric?
|
|
7
|
+
# @rbs @killer: Killers::Base
|
|
8
|
+
# @rbs @limiters: Hash[Symbol, Limiters::Base]
|
|
9
|
+
# @rbs @logger: Logger?
|
|
10
|
+
# @rbs @on_exception: Proc?
|
|
11
|
+
# @rbs @before_kill: Proc?
|
|
12
|
+
# @rbs @checks: Integer
|
|
13
|
+
# @rbs @last_check_time: Float? | nil
|
|
14
|
+
# @rbs @time_killed: Float? | nil
|
|
15
|
+
# @rbs @thread: Thread? | nil
|
|
16
|
+
# @rbs @running: bool
|
|
17
|
+
# @rbs @mutex: Thread::Mutex
|
|
18
|
+
|
|
19
|
+
# @rbs!
|
|
20
|
+
# attr_reader checks: Integer
|
|
21
|
+
attr_reader :checks
|
|
22
|
+
|
|
23
|
+
# Interval in seconds between checks, nil to disable background thread
|
|
24
|
+
# @rbs check_interval: Numeric?
|
|
25
|
+
# The killer to invoke when a limit is exceeded
|
|
26
|
+
# @rbs killer: Killers::Base
|
|
27
|
+
# Hash of limiters to check
|
|
28
|
+
# @rbs limiters: Hash[Symbol, Limiters::Base]
|
|
29
|
+
# Optional logger for debugging
|
|
30
|
+
# @rbs logger: Logger?
|
|
31
|
+
# Optional callback when an exception occurs
|
|
32
|
+
# @rbs on_exception: Proc?
|
|
33
|
+
# Optional callback invoked before killing, receives exceeded limiter
|
|
34
|
+
# @rbs before_kill: Proc?
|
|
35
|
+
# @rbs return: void
|
|
36
|
+
def initialize(check_interval:, killer:, limiters:, logger: nil, on_exception: nil, before_kill: nil)
|
|
37
|
+
@check_interval = check_interval
|
|
38
|
+
@killer = killer
|
|
39
|
+
@limiters = limiters.transform_keys(&:to_sym)
|
|
40
|
+
@logger = logger
|
|
41
|
+
@on_exception = on_exception
|
|
42
|
+
@before_kill = before_kill
|
|
43
|
+
@checks = 0
|
|
44
|
+
@last_check_time = nil
|
|
45
|
+
@time_killed = nil
|
|
46
|
+
@thread = nil
|
|
47
|
+
@running = false
|
|
48
|
+
@mutex = Mutex.new
|
|
49
|
+
|
|
50
|
+
# Call started on all limiters to initialize their state
|
|
51
|
+
@limiters.each_value(&:started)
|
|
52
|
+
|
|
53
|
+
if @check_interval
|
|
54
|
+
log_info('Monitor started with background thread')
|
|
55
|
+
start_background_thread
|
|
56
|
+
else
|
|
57
|
+
log_info('Monitor initialized without background thread; manual checks required')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Performs a single check of all limiters
|
|
62
|
+
# Returns true if any limiter exceeded and killer was invoked
|
|
63
|
+
# @rbs return: bool
|
|
64
|
+
def check_once
|
|
65
|
+
increment_checks
|
|
66
|
+
log_debug("Performing check ##{@checks}")
|
|
67
|
+
|
|
68
|
+
name, limiter = @limiters.find { |_k, v| v.exceeded? }
|
|
69
|
+
if limiter
|
|
70
|
+
if killed?
|
|
71
|
+
log_debug("Limit exceeded but killer already invoked #{seconds_since_killed} seconds ago")
|
|
72
|
+
else
|
|
73
|
+
log_info("Limit exceeded: #{name}, invoking killer")
|
|
74
|
+
perform_kill(name, limiter)
|
|
75
|
+
end
|
|
76
|
+
true
|
|
77
|
+
else
|
|
78
|
+
log_debug('All limiters within bounds')
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
handle_exception(e)
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the number of seconds since the last check
|
|
87
|
+
# Seconds since last check, or nil if no check has been performed
|
|
88
|
+
# @rbs return: Float?
|
|
89
|
+
def seconds_after_last_check
|
|
90
|
+
return nil if @last_check_time.nil?
|
|
91
|
+
|
|
92
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @last_check_time
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns true if the killer has been invoked
|
|
96
|
+
# @rbs return: bool
|
|
97
|
+
def killed?
|
|
98
|
+
!@time_killed.nil?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns the number of seconds since the killer was invoked
|
|
102
|
+
# Seconds since killer was invoked, or nil if killer has not been invoked
|
|
103
|
+
# @rbs return: Float?
|
|
104
|
+
def seconds_since_killed
|
|
105
|
+
return nil if @time_killed.nil?
|
|
106
|
+
|
|
107
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @time_killed
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Stops the background thread if running
|
|
111
|
+
# @rbs return: void
|
|
112
|
+
def stop
|
|
113
|
+
if @thread
|
|
114
|
+
@mutex.synchronize { @running = false }
|
|
115
|
+
@thread.join(5) # Wait up to 5 seconds for thread to finish
|
|
116
|
+
@thread = nil
|
|
117
|
+
log_debug('Monitor stopped')
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Call stopped on all limiters to clean up their state
|
|
121
|
+
@limiters.each_value(&:stopped)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Checks if the background thread is running
|
|
125
|
+
# Returns true if the background thread is running
|
|
126
|
+
# @rbs return: bool
|
|
127
|
+
def running?
|
|
128
|
+
@running && @thread&.alive?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns a limiter by name
|
|
132
|
+
# @rbs name: Symbol | String
|
|
133
|
+
# @rbs return: Limiters::Base
|
|
134
|
+
def limiter(name)
|
|
135
|
+
@limiters.fetch(name.to_sym)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns a hash of all limiters
|
|
139
|
+
# Returns a new hash with the same keys and values
|
|
140
|
+
# Modifications to the hash won't affect internal state, but limiter instances are shared
|
|
141
|
+
# @rbs return: Hash[Symbol | String, Limiters::Base]
|
|
142
|
+
def limiters
|
|
143
|
+
@limiters.to_h { |k, v| [k, v] }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# @rbs name: Symbol
|
|
149
|
+
# @rbs limiter: Limiters::Base
|
|
150
|
+
# @rbs return: void
|
|
151
|
+
def perform_kill(name, limiter)
|
|
152
|
+
run_before_kill(name, limiter)
|
|
153
|
+
@killer.kill!
|
|
154
|
+
@time_killed = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @rbs name: Symbol
|
|
158
|
+
# @rbs limiter: Limiters::Base
|
|
159
|
+
# @rbs return: void
|
|
160
|
+
def run_before_kill(name, limiter)
|
|
161
|
+
@before_kill&.call(name, limiter)
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
handle_exception(e)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @rbs return: void
|
|
167
|
+
def increment_checks
|
|
168
|
+
@mutex.synchronize do
|
|
169
|
+
@checks += 1
|
|
170
|
+
@last_check_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @rbs return: void
|
|
175
|
+
def start_background_thread
|
|
176
|
+
@running = true
|
|
177
|
+
@thread = Thread.new { background_loop }
|
|
178
|
+
@thread.abort_on_exception = false
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @rbs return: void
|
|
182
|
+
def background_loop
|
|
183
|
+
while @running
|
|
184
|
+
check_once
|
|
185
|
+
sleep(@check_interval) if @running
|
|
186
|
+
end
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
handle_exception(e)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @rbs exception: StandardError
|
|
192
|
+
# @rbs return: void
|
|
193
|
+
def handle_exception(exception)
|
|
194
|
+
log_error("Exception in monitor: #{exception.class}: #{exception.message}")
|
|
195
|
+
@on_exception&.call(exception)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @rbs message: String
|
|
199
|
+
# @rbs return: void
|
|
200
|
+
def log_debug(message)
|
|
201
|
+
@logger&.debug { "SeigenWatchdog: #{message}" }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @rbs message: String
|
|
205
|
+
# @rbs return: void
|
|
206
|
+
def log_info(message)
|
|
207
|
+
@logger&.info("SeigenWatchdog: #{message}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# @rbs message: String
|
|
211
|
+
# @rbs return: void
|
|
212
|
+
def log_error(message)
|
|
213
|
+
@logger&.error("SeigenWatchdog: #{message}")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'seigen_watchdog/version'
|
|
4
|
+
|
|
5
|
+
# Monitoring and watchdog module for Ruby applications
|
|
6
|
+
module SeigenWatchdog
|
|
7
|
+
# @rbs self.@monitor: Monitor?
|
|
8
|
+
|
|
9
|
+
class Error < StandardError; end
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_reader :monitor #: Monitor?
|
|
13
|
+
|
|
14
|
+
# Starts the SeigenWatchdog monitor
|
|
15
|
+
# @rbs check_interval: Numeric?
|
|
16
|
+
# @rbs killer: Killers::Base
|
|
17
|
+
# @rbs limiters: Hash[Symbol | String, Limiters::Base]
|
|
18
|
+
# @rbs logger: Logger?
|
|
19
|
+
# @rbs on_exception: Proc?
|
|
20
|
+
# @rbs before_kill: Proc?
|
|
21
|
+
# @rbs return: Monitor
|
|
22
|
+
def start(check_interval:, killer:, limiters:, logger: nil, on_exception: nil, before_kill: nil)
|
|
23
|
+
stop if started?
|
|
24
|
+
|
|
25
|
+
@monitor = Monitor.new(
|
|
26
|
+
check_interval: check_interval,
|
|
27
|
+
killer: killer,
|
|
28
|
+
limiters: limiters,
|
|
29
|
+
logger: logger,
|
|
30
|
+
on_exception: on_exception,
|
|
31
|
+
before_kill: before_kill
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Stops the SeigenWatchdog monitor
|
|
36
|
+
# @rbs return: void
|
|
37
|
+
def stop
|
|
38
|
+
return unless @monitor
|
|
39
|
+
|
|
40
|
+
@monitor.stop
|
|
41
|
+
@monitor = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Checks if the monitor has been started
|
|
45
|
+
# Returns true if the monitor is started
|
|
46
|
+
# @rbs return: bool
|
|
47
|
+
def started?
|
|
48
|
+
!@monitor.nil?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
require_relative 'seigen_watchdog/limiters/base'
|
|
54
|
+
require_relative 'seigen_watchdog/limiters/rss'
|
|
55
|
+
require_relative 'seigen_watchdog/limiters/time'
|
|
56
|
+
require_relative 'seigen_watchdog/limiters/counter'
|
|
57
|
+
require_relative 'seigen_watchdog/limiters/custom'
|
|
58
|
+
require_relative 'seigen_watchdog/killers/base'
|
|
59
|
+
require_relative 'seigen_watchdog/killers/signal'
|
|
60
|
+
require_relative 'seigen_watchdog/monitor'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/killers/signal.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Killers
|
|
5
|
+
# Killer that sends a signal to the current process
|
|
6
|
+
class Signal < Base
|
|
7
|
+
@signal: String
|
|
8
|
+
|
|
9
|
+
attr_reader signal: String
|
|
10
|
+
|
|
11
|
+
# @rbs signal: String | Symbol
|
|
12
|
+
# @rbs return: void
|
|
13
|
+
def initialize: (signal: String | Symbol) -> void
|
|
14
|
+
|
|
15
|
+
# Sends the signal to the current process
|
|
16
|
+
# @rbs return: void
|
|
17
|
+
def kill!: () -> void
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/limiters/base.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Base class for all limiters
|
|
6
|
+
class Base
|
|
7
|
+
# @rbs return: bool
|
|
8
|
+
def exceeded?: () -> bool
|
|
9
|
+
|
|
10
|
+
# Called when the limiter is started (when monitor initializes)
|
|
11
|
+
# @rbs return: void
|
|
12
|
+
def started: () -> void
|
|
13
|
+
|
|
14
|
+
# Called when the limiter is stopped (when monitor stops)
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def stopped: () -> void
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/limiters/counter.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on iteration count
|
|
6
|
+
class Counter < Base
|
|
7
|
+
@max_count: Integer
|
|
8
|
+
|
|
9
|
+
@count: Integer
|
|
10
|
+
|
|
11
|
+
@mutex: Thread::Mutex
|
|
12
|
+
|
|
13
|
+
attr_reader max_count: Integer
|
|
14
|
+
|
|
15
|
+
# @rbs max_count: Integer
|
|
16
|
+
# @rbs initial: Integer
|
|
17
|
+
# @rbs return: void
|
|
18
|
+
def initialize: (max_count: Integer, ?initial: Integer) -> void
|
|
19
|
+
|
|
20
|
+
# Increments the counter by the specified amount
|
|
21
|
+
# @rbs count: Integer
|
|
22
|
+
# @rbs return: void
|
|
23
|
+
def increment: (?Integer count) -> void
|
|
24
|
+
|
|
25
|
+
# Decrements the counter by the specified amount
|
|
26
|
+
# @rbs count: Integer
|
|
27
|
+
# @rbs return: void
|
|
28
|
+
def decrement: (?Integer count) -> void
|
|
29
|
+
|
|
30
|
+
# Resets the counter to the specified initial value
|
|
31
|
+
# @rbs initial: Integer
|
|
32
|
+
# @rbs return: void
|
|
33
|
+
def reset: (?Integer initial) -> void
|
|
34
|
+
|
|
35
|
+
# @rbs return: bool
|
|
36
|
+
def exceeded?: () -> bool
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/limiters/custom.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on a custom checker lambda
|
|
6
|
+
class Custom < Base
|
|
7
|
+
# @rbs @checker: Proc | #call
|
|
8
|
+
# @rbs checker: Proc | #call
|
|
9
|
+
# @rbs return: void
|
|
10
|
+
def initialize: (checker: untyped) -> void
|
|
11
|
+
|
|
12
|
+
# @rbs return: bool
|
|
13
|
+
def exceeded?: () -> bool
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/limiters/rss.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on RSS memory usage
|
|
6
|
+
class RSS < Base
|
|
7
|
+
@max_rss: Integer
|
|
8
|
+
|
|
9
|
+
@mem: GetProcessMem
|
|
10
|
+
|
|
11
|
+
attr_reader max_rss: Integer
|
|
12
|
+
|
|
13
|
+
# @rbs max_rss: Integer
|
|
14
|
+
# @rbs return: void
|
|
15
|
+
def initialize: (max_rss: Integer) -> void
|
|
16
|
+
|
|
17
|
+
# @rbs return: bool
|
|
18
|
+
def exceeded?: () -> bool
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/limiters/time.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
module Limiters
|
|
5
|
+
# Limiter based on execution time
|
|
6
|
+
class Time < Base
|
|
7
|
+
@max_duration: Numeric
|
|
8
|
+
|
|
9
|
+
@start_time: Float?
|
|
10
|
+
|
|
11
|
+
attr_reader start_time: Float?
|
|
12
|
+
|
|
13
|
+
attr_reader max_duration: Numeric
|
|
14
|
+
|
|
15
|
+
# @rbs max_duration: Numeric
|
|
16
|
+
# @rbs return: void
|
|
17
|
+
def initialize: (max_duration: Numeric) -> void
|
|
18
|
+
|
|
19
|
+
# Called when monitor starts - records the start time
|
|
20
|
+
# @rbs return: void
|
|
21
|
+
def started: () -> void
|
|
22
|
+
|
|
23
|
+
# @rbs return: bool
|
|
24
|
+
def exceeded?: () -> bool
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# @rbs return: Float
|
|
29
|
+
def elapsed_time: () -> Float
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog/monitor.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SeigenWatchdog
|
|
4
|
+
# Monitor class that checks limiters and invokes the killer when needed
|
|
5
|
+
class Monitor
|
|
6
|
+
@mutex: Thread::Mutex
|
|
7
|
+
|
|
8
|
+
@running: bool
|
|
9
|
+
|
|
10
|
+
@thread: Thread? | nil
|
|
11
|
+
|
|
12
|
+
@time_killed: Float? | nil
|
|
13
|
+
|
|
14
|
+
@last_check_time: Float? | nil
|
|
15
|
+
|
|
16
|
+
@checks: Integer
|
|
17
|
+
|
|
18
|
+
@before_kill: Proc?
|
|
19
|
+
|
|
20
|
+
@on_exception: Proc?
|
|
21
|
+
|
|
22
|
+
@logger: Logger?
|
|
23
|
+
|
|
24
|
+
@limiters: Hash[Symbol, Limiters::Base]
|
|
25
|
+
|
|
26
|
+
@killer: Killers::Base
|
|
27
|
+
|
|
28
|
+
@check_interval: Numeric?
|
|
29
|
+
|
|
30
|
+
# @rbs!
|
|
31
|
+
# attr_reader checks: Integer
|
|
32
|
+
attr_reader checks: untyped
|
|
33
|
+
|
|
34
|
+
# Interval in seconds between checks, nil to disable background thread
|
|
35
|
+
# @rbs check_interval: Numeric?
|
|
36
|
+
# The killer to invoke when a limit is exceeded
|
|
37
|
+
# @rbs killer: Killers::Base
|
|
38
|
+
# Hash of limiters to check
|
|
39
|
+
# @rbs limiters: Hash[Symbol, Limiters::Base]
|
|
40
|
+
# Optional logger for debugging
|
|
41
|
+
# @rbs logger: Logger?
|
|
42
|
+
# Optional callback when an exception occurs
|
|
43
|
+
# @rbs on_exception: Proc?
|
|
44
|
+
# Optional callback invoked before killing, receives exceeded limiter
|
|
45
|
+
# @rbs before_kill: Proc?
|
|
46
|
+
# @rbs return: void
|
|
47
|
+
def initialize: (check_interval: Numeric?, killer: Killers::Base, limiters: Hash[Symbol, Limiters::Base], ?logger: Logger?, ?on_exception: Proc?, ?before_kill: Proc?) -> void
|
|
48
|
+
|
|
49
|
+
# Performs a single check of all limiters
|
|
50
|
+
# Returns true if any limiter exceeded and killer was invoked
|
|
51
|
+
# @rbs return: bool
|
|
52
|
+
def check_once: () -> bool
|
|
53
|
+
|
|
54
|
+
# Returns the number of seconds since the last check
|
|
55
|
+
# Seconds since last check, or nil if no check has been performed
|
|
56
|
+
# @rbs return: Float?
|
|
57
|
+
def seconds_after_last_check: () -> Float?
|
|
58
|
+
|
|
59
|
+
# Returns true if the killer has been invoked
|
|
60
|
+
# @rbs return: bool
|
|
61
|
+
def killed?: () -> bool
|
|
62
|
+
|
|
63
|
+
# Returns the number of seconds since the killer was invoked
|
|
64
|
+
# Seconds since killer was invoked, or nil if killer has not been invoked
|
|
65
|
+
# @rbs return: Float?
|
|
66
|
+
def seconds_since_killed: () -> Float?
|
|
67
|
+
|
|
68
|
+
# Stops the background thread if running
|
|
69
|
+
# @rbs return: void
|
|
70
|
+
def stop: () -> void
|
|
71
|
+
|
|
72
|
+
# Checks if the background thread is running
|
|
73
|
+
# Returns true if the background thread is running
|
|
74
|
+
# @rbs return: bool
|
|
75
|
+
def running?: () -> bool
|
|
76
|
+
|
|
77
|
+
# Returns a limiter by name
|
|
78
|
+
# @rbs name: Symbol | String
|
|
79
|
+
# @rbs return: Limiters::Base
|
|
80
|
+
def limiter: (Symbol | String name) -> Limiters::Base
|
|
81
|
+
|
|
82
|
+
# Returns a hash of all limiters
|
|
83
|
+
# Returns a new hash with the same keys and values
|
|
84
|
+
# Modifications to the hash won't affect internal state, but limiter instances are shared
|
|
85
|
+
# @rbs return: Hash[Symbol | String, Limiters::Base]
|
|
86
|
+
def limiters: () -> Hash[Symbol | String, Limiters::Base]
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# @rbs name: Symbol
|
|
91
|
+
# @rbs limiter: Limiters::Base
|
|
92
|
+
# @rbs return: void
|
|
93
|
+
def perform_kill: (Symbol name, Limiters::Base limiter) -> void
|
|
94
|
+
|
|
95
|
+
# @rbs name: Symbol
|
|
96
|
+
# @rbs limiter: Limiters::Base
|
|
97
|
+
# @rbs return: void
|
|
98
|
+
def run_before_kill: (Symbol name, Limiters::Base limiter) -> void
|
|
99
|
+
|
|
100
|
+
# @rbs return: void
|
|
101
|
+
def increment_checks: () -> void
|
|
102
|
+
|
|
103
|
+
# @rbs return: void
|
|
104
|
+
def start_background_thread: () -> void
|
|
105
|
+
|
|
106
|
+
# @rbs return: void
|
|
107
|
+
def background_loop: () -> void
|
|
108
|
+
|
|
109
|
+
# @rbs exception: StandardError
|
|
110
|
+
# @rbs return: void
|
|
111
|
+
def handle_exception: (StandardError exception) -> void
|
|
112
|
+
|
|
113
|
+
# @rbs message: String
|
|
114
|
+
# @rbs return: void
|
|
115
|
+
def log_debug: (String message) -> void
|
|
116
|
+
|
|
117
|
+
# @rbs message: String
|
|
118
|
+
# @rbs return: void
|
|
119
|
+
def log_info: (String message) -> void
|
|
120
|
+
|
|
121
|
+
# @rbs message: String
|
|
122
|
+
# @rbs return: void
|
|
123
|
+
def log_error: (String message) -> void
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated from lib/seigen_watchdog.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
# Monitoring and watchdog module for Ruby applications
|
|
4
|
+
module SeigenWatchdog
|
|
5
|
+
self.@monitor: Monitor?
|
|
6
|
+
|
|
7
|
+
class Error < StandardError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader monitor: Monitor?
|
|
11
|
+
|
|
12
|
+
# Starts the SeigenWatchdog monitor
|
|
13
|
+
# @rbs check_interval: Numeric?
|
|
14
|
+
# @rbs killer: Killers::Base
|
|
15
|
+
# @rbs limiters: Hash[Symbol | String, Limiters::Base]
|
|
16
|
+
# @rbs logger: Logger?
|
|
17
|
+
# @rbs on_exception: Proc?
|
|
18
|
+
# @rbs before_kill: Proc?
|
|
19
|
+
# @rbs return: Monitor
|
|
20
|
+
def self.start: (check_interval: Numeric?, killer: Killers::Base, limiters: Hash[Symbol | String, Limiters::Base], ?logger: Logger?, ?on_exception: Proc?, ?before_kill: Proc?) -> Monitor
|
|
21
|
+
|
|
22
|
+
# Stops the SeigenWatchdog monitor
|
|
23
|
+
# @rbs return: void
|
|
24
|
+
def self.stop: () -> void
|
|
25
|
+
|
|
26
|
+
# Checks if the monitor has been started
|
|
27
|
+
# Returns true if the monitor is started
|
|
28
|
+
# @rbs return: bool
|
|
29
|
+
def self.started?: () -> bool
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: seigen_watchdog
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Denis Talakevich
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: get_process_mem
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: logger
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.7'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.7'
|
|
41
|
+
description: Monitors and gracefully terminates a Ruby application based on configurable
|
|
42
|
+
memory usage,execution time, iteration count, or custom conditions
|
|
43
|
+
email:
|
|
44
|
+
- senid231@gmail.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- ".rspec"
|
|
50
|
+
- ".rubocop.yml"
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE.txt
|
|
53
|
+
- README.md
|
|
54
|
+
- Rakefile
|
|
55
|
+
- SECURITY.md
|
|
56
|
+
- lib/seigen_watchdog.rb
|
|
57
|
+
- lib/seigen_watchdog/killers/base.rb
|
|
58
|
+
- lib/seigen_watchdog/killers/signal.rb
|
|
59
|
+
- lib/seigen_watchdog/limiters/base.rb
|
|
60
|
+
- lib/seigen_watchdog/limiters/counter.rb
|
|
61
|
+
- lib/seigen_watchdog/limiters/custom.rb
|
|
62
|
+
- lib/seigen_watchdog/limiters/rss.rb
|
|
63
|
+
- lib/seigen_watchdog/limiters/time.rb
|
|
64
|
+
- lib/seigen_watchdog/monitor.rb
|
|
65
|
+
- lib/seigen_watchdog/version.rb
|
|
66
|
+
- sig/seigen_watchdog.rbs
|
|
67
|
+
- sig/seigen_watchdog/killers/base.rbs
|
|
68
|
+
- sig/seigen_watchdog/killers/signal.rbs
|
|
69
|
+
- sig/seigen_watchdog/limiters/base.rbs
|
|
70
|
+
- sig/seigen_watchdog/limiters/counter.rbs
|
|
71
|
+
- sig/seigen_watchdog/limiters/custom.rbs
|
|
72
|
+
- sig/seigen_watchdog/limiters/rss.rbs
|
|
73
|
+
- sig/seigen_watchdog/limiters/time.rbs
|
|
74
|
+
- sig/seigen_watchdog/monitor.rbs
|
|
75
|
+
- sig/seigen_watchdog/version.rbs
|
|
76
|
+
homepage: https://github.com/senid231/seigen_watchdog
|
|
77
|
+
licenses:
|
|
78
|
+
- MIT
|
|
79
|
+
metadata:
|
|
80
|
+
homepage_uri: https://github.com/senid231/seigen_watchdog
|
|
81
|
+
source_code_uri: https://github.com/senid231/seigen_watchdog
|
|
82
|
+
changelog_uri: https://github.com/senid231/seigen_watchdog/blob/master/CHANGELOG.md
|
|
83
|
+
rubygems_mfa_required: 'true'
|
|
84
|
+
post_install_message:
|
|
85
|
+
rdoc_options: []
|
|
86
|
+
require_paths:
|
|
87
|
+
- lib
|
|
88
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: 3.3.0
|
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '0'
|
|
98
|
+
requirements: []
|
|
99
|
+
rubygems_version: 3.5.22
|
|
100
|
+
signing_key:
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Monitors and gracefully terminates a Ruby application based on configurable
|
|
103
|
+
memory usage,execution time, iteration count, or custom conditions
|
|
104
|
+
test_files: []
|