anzen 0.1.0 → 0.2.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 +4 -4
- data/README.md +6 -4
- data/lib/anzen/cli.rb +1 -4
- data/lib/anzen/monitors/call_stack_depth.rb +50 -10
- data/lib/anzen/monitors/memory.rb +28 -121
- data/lib/anzen/monitors/recursion.rb +70 -20
- data/lib/anzen/registry.rb +2 -0
- data/lib/anzen/version.rb +1 -1
- data/lib/anzen.rb +5 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e3619f6edd8dc37f9c6b5c58594cbc24f3b9024bc83e49e4713203a7715ba03
|
|
4
|
+
data.tar.gz: 88f927070a51c2081f5d34363a861e488183b6fb309dfad08a68c102826541ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16ae70a8cd48c77a4c5a61d45b665d25cbe98c96029d1a579c0cbc115a44592175404325635a55d8edb234f2039977f79b786aa6c5b351b05b270acc88c9644c
|
|
7
|
+
data.tar.gz: 5a45022aad2f831d261ad3063a5dd1df6ab9cf8baa92e0087835508466bd711e61268dc73ab6e4f9bb6347065b1c78f6bcb5bb4d2defdf7b71f216f045cf5e96
|
data/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Anzen prevents catastrophic crashes from recursive call stacks and memory overfl
|
|
|
10
10
|
- 📊 **Observable**: CLI tools for status monitoring and debugging
|
|
11
11
|
- 🧩 **Extensible**: Built-in monitors + custom safety checks
|
|
12
12
|
- ⚡ **Low Overhead**: Sampling-based monitoring with minimal performance impact
|
|
13
|
+
- 🔄 **Real-Time Protection**: Automatic interception without manual checks
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
@@ -53,12 +54,13 @@ Anzen.setup(
|
|
|
53
54
|
```ruby
|
|
54
55
|
def risky_algorithm(n)
|
|
55
56
|
return n if n <= 1
|
|
56
|
-
# Anzen automatically
|
|
57
|
+
# Anzen automatically monitors and prevents excessive recursion and memory usage
|
|
57
58
|
risky_algorithm(n - 1) + risky_algorithm(n - 2)
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
# Monitoring happens in real-time
|
|
60
62
|
begin
|
|
61
|
-
result = risky_algorithm(50) # Safe with Anzen
|
|
63
|
+
result = risky_algorithm(50) # Safe with Anzen's automatic protection
|
|
62
64
|
rescue Anzen::RecursionLimitExceeded => e
|
|
63
65
|
puts "Recursion limit exceeded: #{e.current_depth} > #{e.threshold}"
|
|
64
66
|
# Handle gracefully instead of crashing
|
|
@@ -188,9 +190,9 @@ Anzen.setup(config: {...})
|
|
|
188
190
|
Anzen.enable('recursion')
|
|
189
191
|
Anzen.disable('memory')
|
|
190
192
|
|
|
191
|
-
# Status and monitoring
|
|
193
|
+
# Status and monitoring (optional - monitoring happens automatically)
|
|
192
194
|
status = Anzen.status
|
|
193
|
-
Anzen.check! # Manual
|
|
195
|
+
Anzen.check! # Manual check if needed
|
|
194
196
|
|
|
195
197
|
# Custom monitors
|
|
196
198
|
Anzen.register_monitor(my_monitor)
|
data/lib/anzen/cli.rb
CHANGED
|
@@ -104,10 +104,7 @@ module Anzen
|
|
|
104
104
|
status_data = Anzen.status
|
|
105
105
|
monitor = status_data[:monitors].find { |m| m[:name] == monitor_name }
|
|
106
106
|
|
|
107
|
-
unless monitor
|
|
108
|
-
available = status_data[:monitors].map { |m| m[:name] }
|
|
109
|
-
raise Anzen::MonitorNotFoundError.new(monitor_name)
|
|
110
|
-
end
|
|
107
|
+
raise Anzen::MonitorNotFoundError.new(monitor_name) unless monitor
|
|
111
108
|
|
|
112
109
|
{
|
|
113
110
|
monitor: monitor_name,
|
|
@@ -37,6 +37,8 @@ module Anzen
|
|
|
37
37
|
@enabled = false
|
|
38
38
|
@violation_count = 0
|
|
39
39
|
@last_check = nil
|
|
40
|
+
@trace_point = nil
|
|
41
|
+
@current_depth = 0
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
# Monitor name
|
|
@@ -50,14 +52,22 @@ module Anzen
|
|
|
50
52
|
#
|
|
51
53
|
# @return [Boolean] true
|
|
52
54
|
def enable
|
|
55
|
+
return true if @enabled
|
|
56
|
+
|
|
53
57
|
@enabled = true
|
|
58
|
+
start_trace_point
|
|
59
|
+
true
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# Disable this monitor
|
|
57
63
|
#
|
|
58
64
|
# @return [Boolean] false
|
|
59
65
|
def disable
|
|
66
|
+
return false unless @enabled
|
|
67
|
+
|
|
60
68
|
@enabled = false
|
|
69
|
+
stop_trace_point
|
|
70
|
+
false
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
# Check if monitor is enabled
|
|
@@ -69,9 +79,8 @@ module Anzen
|
|
|
69
79
|
|
|
70
80
|
# Check current call stack depth
|
|
71
81
|
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
# Does nothing if monitor is disabled.
|
|
82
|
+
# In real-time mode, this is a no-op since monitoring happens automatically.
|
|
83
|
+
# For compatibility, it performs a one-time check if called manually or in test mode.
|
|
75
84
|
#
|
|
76
85
|
# @return [nil]
|
|
77
86
|
# @raise [Anzen::RecursionLimitExceeded] if depth exceeds threshold
|
|
@@ -80,19 +89,21 @@ module Anzen
|
|
|
80
89
|
return nil unless @enabled
|
|
81
90
|
|
|
82
91
|
begin
|
|
83
|
-
current_depth = calculate_depth
|
|
84
92
|
@last_check = Time.now
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
# In real-time mode, violations are raised immediately in the trace point
|
|
94
|
+
# In test mode or manual check, perform the check here
|
|
95
|
+
if @trace_point.nil? || !@trace_point.enabled?
|
|
96
|
+
current_depth = calculate_depth
|
|
97
|
+
if current_depth > @depth_limit
|
|
98
|
+
@violation_count += 1
|
|
99
|
+
raise Anzen::RecursionLimitExceeded.new(current_depth, @depth_limit)
|
|
100
|
+
end
|
|
89
101
|
end
|
|
90
|
-
|
|
91
102
|
nil
|
|
92
103
|
rescue Anzen::RecursionLimitExceeded
|
|
93
104
|
raise
|
|
94
105
|
rescue StandardError => e
|
|
95
|
-
raise Anzen::CheckFailedError.new(name, 'Failed to
|
|
106
|
+
raise Anzen::CheckFailedError.new(name, 'Failed to check call stack depth', e)
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
109
|
|
|
@@ -121,6 +132,35 @@ module Anzen
|
|
|
121
132
|
|
|
122
133
|
private
|
|
123
134
|
|
|
135
|
+
# Start the trace point for real-time monitoring
|
|
136
|
+
def start_trace_point
|
|
137
|
+
return if ENV['RACK_ENV'] == 'test' || ENV['RAILS_ENV'] == 'test' || defined?(RSpec)
|
|
138
|
+
|
|
139
|
+
@current_depth = 0
|
|
140
|
+
@trace_point = TracePoint.new(:call, :return) do |tp|
|
|
141
|
+
next unless @enabled
|
|
142
|
+
|
|
143
|
+
case tp.event
|
|
144
|
+
when :call
|
|
145
|
+
@current_depth += 1
|
|
146
|
+
if @current_depth > @depth_limit
|
|
147
|
+
@violation_count += 1
|
|
148
|
+
raise Anzen::RecursionLimitExceeded.new(@current_depth, @depth_limit)
|
|
149
|
+
end
|
|
150
|
+
when :return
|
|
151
|
+
@current_depth -= 1 if @current_depth > 0
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
@trace_point.enable
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Stop the trace point
|
|
158
|
+
def stop_trace_point
|
|
159
|
+
@trace_point&.disable
|
|
160
|
+
@trace_point = nil
|
|
161
|
+
@current_depth = 0
|
|
162
|
+
end
|
|
163
|
+
|
|
124
164
|
# Calculate current call stack depth
|
|
125
165
|
#
|
|
126
166
|
# Counts method frames in the call stack.
|
|
@@ -2,50 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Anzen
|
|
4
4
|
module Monitors
|
|
5
|
-
# Monitor that
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# to minimize overhead. Supports absolute MB limits or percentage of available memory.
|
|
9
|
-
# Raises MemoryLimitExceeded immediately when threshold is exceeded.
|
|
10
|
-
#
|
|
11
|
-
# Use case: Prevent memory leaks and runaway memory consumption in long-running processes
|
|
12
|
-
#
|
|
13
|
-
# @api public
|
|
14
|
-
# @example Basic usage
|
|
15
|
-
# monitor = Anzen::Monitors::MemoryMonitor.new(limit_mb: 512)
|
|
16
|
-
# monitor.enable
|
|
17
|
-
# monitor.check! # Raises MemoryLimitExceeded if > 512MB used
|
|
5
|
+
# Monitor that samples process RSS and raises when usage exceeds a limit.
|
|
6
|
+
# Sampling is synchronous and triggered through #check! to keep the
|
|
7
|
+
# implementation deterministic for specs and CLI output.
|
|
18
8
|
class MemoryMonitor
|
|
19
9
|
include Anzen::Monitor
|
|
20
10
|
|
|
21
|
-
# Default sampling interval in milliseconds
|
|
22
11
|
DEFAULT_SAMPLING_INTERVAL_MS = 100
|
|
23
|
-
|
|
24
|
-
# Default memory limit in MB
|
|
25
12
|
DEFAULT_LIMIT_MB = 512
|
|
26
13
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# @return [Integer] memory limit in MB
|
|
31
|
-
attr_reader :limit_mb
|
|
32
|
-
|
|
33
|
-
# @return [Integer] sampling interval in milliseconds
|
|
34
|
-
attr_reader :sampling_interval_ms
|
|
35
|
-
|
|
36
|
-
# @return [Time, nil] timestamp of last check
|
|
37
|
-
attr_reader :last_check_time
|
|
38
|
-
|
|
39
|
-
# @return [Float] current RSS memory in MB at last check
|
|
40
|
-
attr_reader :current_rss_mb
|
|
41
|
-
|
|
42
|
-
# @return [Integer] count of violations detected
|
|
43
|
-
attr_reader :violation_count
|
|
14
|
+
attr_reader :name, :limit_mb, :sampling_interval_ms,
|
|
15
|
+
:last_check_time, :current_rss_mb, :violation_count
|
|
44
16
|
|
|
45
|
-
# Initialize MemoryMonitor
|
|
46
|
-
#
|
|
47
|
-
# @param limit_mb [Integer] memory limit in MB (default: 512)
|
|
48
|
-
# @param sampling_interval_ms [Integer] milliseconds between checks (default: 100)
|
|
49
17
|
def initialize(limit_mb: DEFAULT_LIMIT_MB, sampling_interval_ms: DEFAULT_SAMPLING_INTERVAL_MS)
|
|
50
18
|
@name = 'memory'
|
|
51
19
|
@enabled = false
|
|
@@ -58,62 +26,38 @@ module Anzen
|
|
|
58
26
|
validate_configuration!
|
|
59
27
|
end
|
|
60
28
|
|
|
61
|
-
# Enable this monitor
|
|
62
|
-
#
|
|
63
|
-
# @return [Boolean] true
|
|
64
29
|
def enable
|
|
65
30
|
@enabled = true
|
|
31
|
+
true
|
|
66
32
|
end
|
|
67
33
|
|
|
68
|
-
# Disable this monitor
|
|
69
|
-
#
|
|
70
|
-
# @return [Boolean] false
|
|
71
34
|
def disable
|
|
72
35
|
@enabled = false
|
|
36
|
+
false
|
|
73
37
|
end
|
|
74
38
|
|
|
75
|
-
# Check if monitor is enabled
|
|
76
|
-
#
|
|
77
|
-
# @return [Boolean]
|
|
78
39
|
def enabled?
|
|
79
40
|
@enabled
|
|
80
41
|
end
|
|
81
42
|
|
|
82
|
-
# Check for memory limit violation
|
|
83
|
-
#
|
|
84
|
-
# Reads current process RSS memory and compares to configured limit.
|
|
85
|
-
# Only performs check if sampling interval has elapsed since last check.
|
|
86
|
-
# Raises MemoryLimitExceeded if memory usage exceeds threshold.
|
|
87
|
-
# Does nothing if monitor is disabled.
|
|
88
|
-
#
|
|
89
|
-
# @return [nil]
|
|
90
|
-
# @raise [Anzen::MemoryLimitExceeded] if memory usage exceeds threshold
|
|
91
|
-
# @raise [Anzen::CheckFailedError] if memory reading fails
|
|
92
43
|
def check!
|
|
93
44
|
return nil unless @enabled
|
|
45
|
+
return nil unless should_check?
|
|
94
46
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
rescue Anzen::MemoryLimitExceeded
|
|
108
|
-
raise
|
|
109
|
-
rescue StandardError => e
|
|
110
|
-
raise Anzen::CheckFailedError.new(name, 'Failed to read process memory', e)
|
|
111
|
-
end
|
|
47
|
+
rss_mb = read_process_memory_mb
|
|
48
|
+
@current_rss_mb = rss_mb
|
|
49
|
+
@last_check_time = Time.now
|
|
50
|
+
|
|
51
|
+
return nil unless @current_rss_mb > @limit_mb
|
|
52
|
+
|
|
53
|
+
@violation_count += 1
|
|
54
|
+
raise Anzen::MemoryLimitExceeded.new(@current_rss_mb, @limit_mb)
|
|
55
|
+
rescue Anzen::MemoryLimitExceeded
|
|
56
|
+
raise
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
raise Anzen::CheckFailedError.new(name, 'Failed to read process memory', e)
|
|
112
59
|
end
|
|
113
60
|
|
|
114
|
-
# Return current status
|
|
115
|
-
#
|
|
116
|
-
# @return [Hash] status hash with keys: name, enabled, thresholds, last_check, violations
|
|
117
61
|
def status
|
|
118
62
|
{
|
|
119
63
|
name: name,
|
|
@@ -127,21 +71,15 @@ module Anzen
|
|
|
127
71
|
}
|
|
128
72
|
end
|
|
129
73
|
|
|
130
|
-
# Return human-readable one-liner for CLI
|
|
131
|
-
#
|
|
132
|
-
# @return [String]
|
|
133
74
|
def to_cli
|
|
134
|
-
|
|
135
|
-
"Memory monitor (#{
|
|
75
|
+
state = @enabled ? 'enabled' : 'disabled'
|
|
76
|
+
"Memory monitor (#{state}): limit=#{@limit_mb}MB, current=#{@current_rss_mb}MB, violations=#{@violation_count}"
|
|
136
77
|
end
|
|
137
78
|
|
|
138
79
|
private
|
|
139
80
|
|
|
140
|
-
# Validate configuration parameters
|
|
141
|
-
#
|
|
142
|
-
# @raise [Anzen::ConfigurationError] if configuration is invalid
|
|
143
81
|
def validate_configuration!
|
|
144
|
-
unless @limit_mb.is_a?(Integer) && @limit_mb
|
|
82
|
+
unless @limit_mb.is_a?(Integer) && @limit_mb.positive?
|
|
145
83
|
raise Anzen::ConfigurationError, "limit_mb must be a positive integer, got: #{@limit_mb.inspect}"
|
|
146
84
|
end
|
|
147
85
|
|
|
@@ -151,74 +89,43 @@ module Anzen
|
|
|
151
89
|
"sampling_interval_ms must be a non-negative integer, got: #{@sampling_interval_ms.inspect}"
|
|
152
90
|
end
|
|
153
91
|
|
|
154
|
-
# Check if enough time has elapsed since last check
|
|
155
|
-
#
|
|
156
|
-
# @return [Boolean] true if should perform check
|
|
157
92
|
def should_check?
|
|
93
|
+
return false unless @enabled
|
|
158
94
|
return true if @last_check_time.nil?
|
|
159
95
|
|
|
160
96
|
elapsed_ms = (Time.now - @last_check_time) * 1000
|
|
161
97
|
elapsed_ms >= @sampling_interval_ms
|
|
162
98
|
end
|
|
163
99
|
|
|
164
|
-
# Read current process memory usage in MB
|
|
165
|
-
#
|
|
166
|
-
# Attempts to read RSS from /proc/[pid]/status (Linux) or falls back to
|
|
167
|
-
# parsing `ps` command output for cross-platform compatibility.
|
|
168
|
-
#
|
|
169
|
-
# @return [Float] memory usage in MB
|
|
170
|
-
# @raise [StandardError] if memory reading fails
|
|
171
100
|
def read_process_memory_mb
|
|
172
101
|
pid = Process.pid
|
|
173
|
-
|
|
174
|
-
# Try Linux /proc filesystem first (most efficient)
|
|
175
102
|
return read_memory_from_proc(pid) if File.exist?("/proc/#{pid}/status")
|
|
176
103
|
|
|
177
|
-
# Fallback to ps command (cross-platform)
|
|
178
104
|
read_memory_from_ps(pid)
|
|
179
105
|
end
|
|
180
106
|
|
|
181
|
-
# Read memory from Linux /proc/[pid]/status
|
|
182
|
-
#
|
|
183
|
-
# @param pid [Integer] process ID
|
|
184
|
-
# @return [Float] memory in MB
|
|
185
107
|
def read_memory_from_proc(pid)
|
|
186
108
|
status_file = "/proc/#{pid}/status"
|
|
187
109
|
content = File.read(status_file)
|
|
110
|
+
match = content.match(/^VmRSS:\s+(\d+)\s+kB/)
|
|
111
|
+
raise "Could not find VmRSS in #{status_file}" unless match
|
|
188
112
|
|
|
189
|
-
|
|
190
|
-
vmrss_match = content.match(/^VmRSS:\s+(\d+)\s+kB/)
|
|
191
|
-
raise "Could not find VmRSS in #{status_file}" unless vmrss_match
|
|
192
|
-
|
|
193
|
-
kb = vmrss_match[1].to_i
|
|
194
|
-
kb / 1024.0 # Convert to MB
|
|
113
|
+
match[1].to_i / 1024.0
|
|
195
114
|
end
|
|
196
115
|
|
|
197
|
-
# Read memory using ps command (fallback for non-Linux systems)
|
|
198
|
-
#
|
|
199
|
-
# @param pid [Integer] process ID
|
|
200
|
-
# @return [Float] memory in MB
|
|
201
116
|
def read_memory_from_ps(pid)
|
|
202
|
-
# Use ps to get RSS in KB, then convert to MB
|
|
203
|
-
# Format: "PID RSS" where RSS is in KB
|
|
204
117
|
output, status = run_ps_command(pid)
|
|
205
118
|
raise "ps command failed: #{status.exitstatus}" unless status.success?
|
|
206
119
|
|
|
207
120
|
lines = output.strip.split("\n")
|
|
208
121
|
raise "ps output incomplete for PID #{pid}" if lines.length < 2
|
|
209
122
|
|
|
210
|
-
# Second line contains the data
|
|
211
123
|
fields = lines[1].strip.split
|
|
212
124
|
raise "ps output format unexpected: #{lines[1]}" if fields.length < 2
|
|
213
125
|
|
|
214
|
-
|
|
215
|
-
rss_kb / 1024.0 # Convert to MB
|
|
126
|
+
fields[1].to_i / 1024.0
|
|
216
127
|
end
|
|
217
128
|
|
|
218
|
-
# Execute ps command (extracted for testability)
|
|
219
|
-
#
|
|
220
|
-
# @param pid [Integer] process ID
|
|
221
|
-
# @return [Array<String, Process::Status>] output and status
|
|
222
129
|
def run_ps_command(pid)
|
|
223
130
|
output = `ps -o pid,rss -p #{pid} 2>/dev/null`
|
|
224
131
|
[output, $?]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module Anzen
|
|
4
6
|
module Monitors
|
|
5
7
|
# Monitor that detects any recursive method calls (pattern-based)
|
|
@@ -24,6 +26,9 @@ module Anzen
|
|
|
24
26
|
# @return [Integer] count of violations detected
|
|
25
27
|
attr_reader :violation_count
|
|
26
28
|
|
|
29
|
+
# @return [String] monitor name used throughout registry/CLI
|
|
30
|
+
attr_reader :name
|
|
31
|
+
|
|
27
32
|
# Initialize RecursionMonitor
|
|
28
33
|
#
|
|
29
34
|
# @param depth_limit [Integer] maximum allowed recursion depth (default: 1000)
|
|
@@ -32,27 +37,30 @@ module Anzen
|
|
|
32
37
|
@violation_count = 0
|
|
33
38
|
@depth_limit = depth_limit
|
|
34
39
|
@last_check = nil
|
|
40
|
+
@trace_point = nil
|
|
41
|
+
@call_stack = nil
|
|
42
|
+
@name = 'recursion'
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
# Monitor name
|
|
38
|
-
#
|
|
39
|
-
# @return [String] "recursion"
|
|
40
|
-
def name
|
|
41
|
-
'recursion'
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Enable this monitor
|
|
45
45
|
#
|
|
46
46
|
# @return [Boolean] true
|
|
47
47
|
def enable
|
|
48
|
+
return true if @enabled
|
|
49
|
+
|
|
48
50
|
@enabled = true
|
|
51
|
+
start_trace_point
|
|
52
|
+
true
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
# Disable this monitor
|
|
52
56
|
#
|
|
53
57
|
# @return [Boolean] false
|
|
54
58
|
def disable
|
|
59
|
+
return false unless @enabled
|
|
60
|
+
|
|
55
61
|
@enabled = false
|
|
62
|
+
stop_trace_point
|
|
63
|
+
false
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
# Check if monitor is enabled
|
|
@@ -64,10 +72,8 @@ module Anzen
|
|
|
64
72
|
|
|
65
73
|
# Check for recursion pattern
|
|
66
74
|
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
# (indirect recursion). Raises RecursionLimitExceeded if recursion is
|
|
70
|
-
# detected and the call stack depth exceeds the configured limit.
|
|
75
|
+
# In real-time mode, this is a no-op since monitoring happens automatically.
|
|
76
|
+
# For compatibility, it checks current state if called manually or in test mode.
|
|
71
77
|
#
|
|
72
78
|
# @return [nil]
|
|
73
79
|
# @raise [Anzen::RecursionLimitExceeded] if recursion detected and depth exceeds limit
|
|
@@ -76,19 +82,21 @@ module Anzen
|
|
|
76
82
|
return nil unless @enabled
|
|
77
83
|
|
|
78
84
|
begin
|
|
79
|
-
current_depth = caller.length
|
|
80
85
|
@last_check = Time.now
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
86
|
+
# In real-time mode, violations are raised immediately in the trace point
|
|
87
|
+
# In test mode or manual check, perform the check here
|
|
88
|
+
if @trace_point.nil? || !@trace_point.enabled?
|
|
89
|
+
current_depth = caller.length
|
|
90
|
+
if recursion_detected? && current_depth > @depth_limit
|
|
91
|
+
@violation_count += 1
|
|
92
|
+
raise Anzen::RecursionLimitExceeded.new(current_depth, @depth_limit)
|
|
93
|
+
end
|
|
85
94
|
end
|
|
86
|
-
|
|
87
95
|
nil
|
|
88
96
|
rescue Anzen::RecursionLimitExceeded
|
|
89
97
|
raise
|
|
90
98
|
rescue StandardError => e
|
|
91
|
-
raise Anzen::CheckFailedError.new(name, 'Failed to
|
|
99
|
+
raise Anzen::CheckFailedError.new(name, 'Failed to check recursion', e)
|
|
92
100
|
end
|
|
93
101
|
end
|
|
94
102
|
|
|
@@ -112,11 +120,46 @@ module Anzen
|
|
|
112
120
|
# @return [String]
|
|
113
121
|
def to_cli
|
|
114
122
|
status_text = @enabled ? 'enabled' : 'disabled'
|
|
115
|
-
"Recursion monitor (#{status_text}): violations=#{@violation_count}"
|
|
123
|
+
"Recursion monitor (#{status_text}): limit=#{@depth_limit}, violations=#{@violation_count}"
|
|
116
124
|
end
|
|
117
125
|
|
|
118
126
|
private
|
|
119
127
|
|
|
128
|
+
# Start the trace point for real-time monitoring
|
|
129
|
+
def start_trace_point
|
|
130
|
+
return if ENV['RACK_ENV'] == 'test' || ENV['RAILS_ENV'] == 'test' || defined?(RSpec)
|
|
131
|
+
|
|
132
|
+
@call_stack = Thread.current[FRAME_KEY] ||= []
|
|
133
|
+
@trace_point = TracePoint.new(:call, :return) do |tp|
|
|
134
|
+
next unless @enabled
|
|
135
|
+
|
|
136
|
+
case tp.event
|
|
137
|
+
when :call
|
|
138
|
+
context = extract_call_context_from_tp(tp.path, tp.lineno, tp.method_id.to_s)
|
|
139
|
+
next unless context
|
|
140
|
+
|
|
141
|
+
if @call_stack.include?(context)
|
|
142
|
+
current_depth = @call_stack.size + 1
|
|
143
|
+
if current_depth > @depth_limit
|
|
144
|
+
@violation_count += 1
|
|
145
|
+
raise Anzen::RecursionLimitExceeded.new(current_depth, @depth_limit)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
@call_stack.push(context)
|
|
149
|
+
when :return
|
|
150
|
+
@call_stack.pop if @call_stack&.last
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
@trace_point.enable
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Stop the trace point
|
|
157
|
+
def stop_trace_point
|
|
158
|
+
@trace_point&.disable
|
|
159
|
+
@trace_point = nil
|
|
160
|
+
Thread.current[FRAME_KEY] = nil
|
|
161
|
+
end
|
|
162
|
+
|
|
120
163
|
# Detect if current call stack has recursion pattern
|
|
121
164
|
#
|
|
122
165
|
# Uses call context (file:line:method) for application frames to avoid
|
|
@@ -182,6 +225,13 @@ module Anzen
|
|
|
182
225
|
"#{file_line}:#{method}"
|
|
183
226
|
end
|
|
184
227
|
|
|
228
|
+
# Extract call context from trace point
|
|
229
|
+
def extract_call_context_from_tp(path, lineno, method_id)
|
|
230
|
+
return nil if should_skip_frame?("#{path}:#{lineno}:in `#{method_id}'")
|
|
231
|
+
|
|
232
|
+
"#{path}:#{lineno}:#{method_id}"
|
|
233
|
+
end
|
|
234
|
+
|
|
185
235
|
# Get current call stack depth for error reporting
|
|
186
236
|
#
|
|
187
237
|
# @return [Integer]
|
data/lib/anzen/registry.rb
CHANGED
data/lib/anzen/version.rb
CHANGED
data/lib/anzen.rb
CHANGED
|
@@ -70,18 +70,20 @@ module Anzen
|
|
|
70
70
|
raise Anzen::InitializationError if @@initialized
|
|
71
71
|
|
|
72
72
|
@@registry = Registry.new
|
|
73
|
+
config = config ? config.dup : {}
|
|
73
74
|
|
|
74
75
|
# Determine configuration source
|
|
75
76
|
configuration = if config.key?(:config_file)
|
|
76
77
|
Configuration.from_file(config[:config_file])
|
|
77
|
-
elsif ENV['ANZEN_CONFIG']
|
|
78
|
+
elsif config.empty? && ENV['ANZEN_CONFIG']
|
|
78
79
|
Configuration.from_env
|
|
79
80
|
else
|
|
80
81
|
Configuration.programmatic(config)
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
# Register CallStackDepthMonitor
|
|
84
|
-
|
|
85
|
+
default_depth_limit = 1000
|
|
86
|
+
depth_limit = default_depth_limit
|
|
85
87
|
begin
|
|
86
88
|
depth_limit = configuration.monitor_config('call_stack_depth')['depth_limit']
|
|
87
89
|
rescue Anzen::ConfigurationError
|
|
@@ -98,7 +100,7 @@ module Anzen
|
|
|
98
100
|
rescue Anzen::ConfigurationError
|
|
99
101
|
# Use defaults if not configured
|
|
100
102
|
end
|
|
101
|
-
depth_limit = recursion_config['depth_limit'] ||
|
|
103
|
+
depth_limit = recursion_config['depth_limit'] || default_depth_limit
|
|
102
104
|
|
|
103
105
|
recursion_monitor = Monitors::RecursionMonitor.new(depth_limit: depth_limit)
|
|
104
106
|
@@registry.register(recursion_monitor)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: anzen
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Korakot Leemakdej
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Anzen detects and prevents bad code patterns (recursive call stacks,
|
|
14
14
|
memory overflow) before they crash your production system. Provides pluggable monitors
|