Class: Parse::NPlusOneDetector
- Inherits:
-
Object
- Object
- Parse::NPlusOneDetector
- Defined in:
- lib/parse/query/n_plus_one_detector.rb
Overview
Detects N+1 query patterns when accessing associations.
N+1 queries occur when you load a collection of objects and then access an association on each object individually, triggering a separate query for each. This is inefficient and can be avoided by using includes() to eager-load the associations.
Constant Summary collapse
- DEFAULT_DETECTION_WINDOW =
Default time window in seconds to track related fetches
2.0- DEFAULT_FETCH_THRESHOLD =
Default minimum number of fetches to trigger a warning
3- DEFAULT_CLEANUP_INTERVAL =
Default cleanup interval in seconds
60.0- TRACKING_KEY =
Thread-local storage key for tracking
:parse_n_plus_one_tracking- CLEANUP_KEY =
Thread-local key for last cleanup time
:parse_n_plus_one_last_cleanup- SOURCE_REGISTRY_KEY =
Thread-local key for source registry (maps object_id to source info)
:parse_n_plus_one_source_registry- VALID_MODES =
Valid modes for N+1 detection
[:warn, :raise, :ignore].freeze
- MODE_KEY =
Thread-local key for mode
:parse_n_plus_one_mode
Class Attribute Summary collapse
- .cleanup_interval ⇒ Object
- .detection_window ⇒ Object
- .fetch_threshold ⇒ Object
-
.logger ⇒ Logger?
Get the configured logger.
Class Method Summary collapse
-
.callbacks ⇒ Array<Proc>
Get registered callbacks.
-
.clear_callbacks! ⇒ Object
Clear all registered callbacks.
-
.clear_source_registry! ⇒ Object
Clear the source registry (called during reset).
-
.emit_warning(source_class, association, target_class, count) ⇒ Object
Emit an N+1 warning or raise an error based on the current mode.
-
.enabled=(value) ⇒ Object
Enable or disable N+1 detection for the current thread.
-
.enabled? ⇒ Boolean
Whether N+1 detection is enabled (not in ignore mode).
-
.lookup_source(pointer) ⇒ Hash?
Look up the source info for a pointer object.
-
.mode ⇒ Symbol
Get the current N+1 detection mode.
-
.mode=(value) ⇒ Object
Set the N+1 detection mode.
-
.on_n_plus_one {|source_class, association, target_class, count, location| ... } ⇒ Object
Register a callback to be called when N+1 is detected.
-
.register_source(pointer, source_class:, association:) ⇒ Object
Register a source (class and association) for a pointer object.
-
.reset! ⇒ Object
Reset all tracking data.
-
.summary ⇒ Hash
Get summary statistics of detected N+1 patterns.
-
.track_autofetch(source_class:, association:, target_class:, object_id:) ⇒ Object
Track an autofetch event for N+1 detection.
Class Attribute Details
.cleanup_interval ⇒ Object
99 100 101 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 99 def cleanup_interval @cleanup_interval || DEFAULT_CLEANUP_INTERVAL end |
.detection_window ⇒ Object
91 92 93 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 91 def detection_window @detection_window || DEFAULT_DETECTION_WINDOW end |
.fetch_threshold ⇒ Object
95 96 97 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 95 def fetch_threshold @fetch_threshold || DEFAULT_FETCH_THRESHOLD end |
.logger ⇒ Logger?
Get the configured logger
265 266 267 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 265 def logger @logger end |
Class Method Details
.callbacks ⇒ Array<Proc>
Get registered callbacks
255 256 257 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 255 def callbacks @callbacks ||= [] end |
.clear_callbacks! ⇒ Object
Clear all registered callbacks
249 250 251 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 249 def clear_callbacks! @callbacks = [] end |
.clear_source_registry! ⇒ Object
Clear the source registry (called during reset)
131 132 133 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 131 def clear_source_registry! Thread.current[SOURCE_REGISTRY_KEY] = nil end |
.emit_warning(source_class, association, target_class, count) ⇒ Object
Emit an N+1 warning or raise an error based on the current mode.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 211 def emit_warning(source_class, association, target_class, count) location = find_user_code_location # Call registered callbacks regardless of mode callbacks.each { |cb| cb.call(source_class, association, target_class, count, location) } case mode when :raise raise NPlusOneQueryError.new(source_class, association, target_class, count, location) when :warn = "[Parse::N+1] Warning: N+1 query detected on #{source_class}.#{association} " \ "(#{count} separate fetches for #{target_class})" if location += "\n Location: #{location}" end += "\n Suggestion: Use `.includes(:#{association})` to eager-load this association" # Output warning if logger logger.warn() else warn() end # :ignore mode does nothing (but callbacks still run) end end |
.enabled=(value) ⇒ Object
Enable or disable N+1 detection for the current thread
161 162 163 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 161 def enabled=(value) self.mode = value ? :warn : :ignore end |
.enabled? ⇒ Boolean
Whether N+1 detection is enabled (not in ignore mode)
155 156 157 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 155 def enabled? mode != :ignore end |
.lookup_source(pointer) ⇒ Hash?
Look up the source info for a pointer object.
124 125 126 127 128 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 124 def lookup_source(pointer) return nil unless pointer registry = get_source_registry registry[pointer.object_id] end |
.mode ⇒ Symbol
Get the current N+1 detection mode
137 138 139 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 137 def mode Thread.current[MODE_KEY] || :ignore end |
.mode=(value) ⇒ Object
Set the N+1 detection mode
144 145 146 147 148 149 150 151 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 144 def mode=(value) value = value.to_sym if value.respond_to?(:to_sym) unless VALID_MODES.include?(value) raise ArgumentError, "Invalid N+1 mode: #{value.inspect}. Valid modes: #{VALID_MODES.join(", ")}" end Thread.current[MODE_KEY] = value reset! if value == :ignore end |
.on_n_plus_one {|source_class, association, target_class, count, location| ... } ⇒ Object
Register a callback to be called when N+1 is detected. Useful for custom logging or metrics.
244 245 246 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 244 def on_n_plus_one(&block) callbacks << block if block_given? end |
.register_source(pointer, source_class:, association:) ⇒ Object
Register a source (class and association) for a pointer object. This uses the object’s Ruby object_id as a key in a thread-local registry, avoiding the need to set instance variables on foreign objects.
110 111 112 113 114 115 116 117 118 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 110 def register_source(pointer, source_class:, association:) return unless pointer && enabled? registry = get_source_registry registry[pointer.object_id] = { source_class: source_class, association: association, registered_at: Time.now.to_f, } end |
.reset! ⇒ Object
Reset all tracking data
166 167 168 169 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 166 def reset! Thread.current[TRACKING_KEY] = nil clear_source_registry! end |
.summary ⇒ Hash
Get summary statistics of detected N+1 patterns
271 272 273 274 275 276 277 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 271 def summary tracking = get_tracking { patterns_detected: tracking.count { |_, v| v[:warned] }, associations: tracking.map { |k, v| { pattern: k, fetches: v[:fetches].size, warned: v[:warned] } }, } end |
.track_autofetch(source_class:, association:, target_class:, object_id:) ⇒ Object
Track an autofetch event for N+1 detection.
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/parse/query/n_plus_one_detector.rb', line 177 def track_autofetch(source_class:, association:, target_class:, object_id:) return unless enabled? tracking = get_tracking key = "#{source_class}.#{association}" now = Time.now.to_f # Periodically clean up stale tracking entries to prevent memory leaks # in long-running threads (e.g., Puma, Sidekiq thread pools) cleanup_stale_entries(tracking, now) # Initialize or update tracking for this association tracking[key] ||= { fetches: [], warned: false, target_class: target_class } data = tracking[key] # Remove stale entries outside the detection window data[:fetches] = data[:fetches].select { |t| now - t < detection_window } # Add this fetch data[:fetches] << now # Check if we've exceeded the threshold and haven't warned yet if data[:fetches].size >= fetch_threshold && !data[:warned] data[:warned] = true emit_warning(source_class, association, target_class, data[:fetches].size) end end |