rails_lens 0.2.4 → 0.2.5

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: 7b17142784a27f1ffd45512665eea05a135e1c97b0e1dcc38505f8a07b067ad0
4
- data.tar.gz: 9ae11ad7619a74b5662a8b6bb2e767c33ad1523f5c6a2cf590cb809389d0ab9e
3
+ metadata.gz: b150328b6771f2de4015820d40e3942c10b41903687f800fd4b068f79605deeb
4
+ data.tar.gz: 2d67fc7fd815b1af80c3cb9e1ad8b94e1166129fc3a1eb24e28958ad3e625b47
5
5
  SHA512:
6
- metadata.gz: 7ebe5bfc79a4415138fc65f03f393796780e514a32f5226c0fbf66acb2d4d6dd2ac5e1b8878429f3383023f7489b5b40a35d9930876d8ffecf563a14f8bdbfb5
7
- data.tar.gz: 2b40b476f42536db544130081063e3a0e992bbeda51d4ff993b4b3be3d0b8073a5ea00c3df98d57321de995d3dca6120f335fc9c2fca652abe3794cc4c704cdd
6
+ metadata.gz: 0b9f944f8e7be0f11789817a49f168f7be310e6742207897029c20540b701c525da861a1b49cdcbeee0690a9bd6424fbace720e6bb42fd5d9d81b46cd244ed88
7
+ data.tar.gz: 22f3bc6e6223ad726c2fc3cf617c37af1e587e466119fd1138965e38ff51cb9128b441b5081214e79f9e2d334b8ec7990e437bdfb20257783a40a194f572ebfc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.5](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.4...rails_lens/v0.2.5) (2025-07-31)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * change strategy to cover database with hundrends of connections pool ([#12](https://github.com/seuros/rails_lens/issues/12)) ([7054d35](https://github.com/seuros/rails_lens/commit/7054d3582bfee41f0050725c2bd23e80c5898486))
9
+
3
10
  ## [0.2.4](https://github.com/seuros/rails_lens/compare/rails_lens/v0.2.3...rails_lens/v0.2.4) (2025-07-31)
4
11
 
5
12
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent'
4
-
5
3
  module RailsLens
6
4
  class ModelDetector
7
5
  class << self
@@ -198,7 +196,10 @@ module RailsLens
198
196
 
199
197
  # Exclude abstract models and models without valid tables
200
198
  before_count = models.size
201
- models = filter_models_concurrently(models, trace_filtering, options)
199
+
200
+ # Use connection management during model filtering to prevent connection exhaustion
201
+ models = filter_models_with_connection_management(models, trace_filtering, options)
202
+
202
203
  log_filter_step('Abstract/invalid table removal', before_count, models.size, trace_filtering)
203
204
 
204
205
  # Exclude tables from configuration
@@ -257,59 +258,178 @@ module RailsLens
257
258
  end
258
259
 
259
260
  def filter_models_concurrently(models, trace_filtering, options = {})
260
- # Use concurrent futures to check table existence in parallel
261
- futures = models.map do |model|
262
- Concurrent::Future.execute do
263
- should_exclude = false
264
- reason = nil
265
-
266
- begin
267
- # Skip abstract models unless explicitly included
268
- if model.abstract_class? && !options[:include_abstract]
269
- should_exclude = true
270
- reason = 'abstract class'
271
- # For abstract models that are included, skip table checks
272
- elsif model.abstract_class? && options[:include_abstract]
273
- reason = 'abstract class (included)'
274
- # Skip models without configured tables
275
- elsif !model.table_name
276
- should_exclude = true
277
- reason = 'no table name'
278
- # Skip models whose tables don't exist
279
- elsif !model.table_exists?
280
- should_exclude = true
281
- reason = "table '#{model.table_name}' does not exist"
282
- # Additional check: Skip models that don't have any columns
283
- elsif model.columns.empty?
284
- should_exclude = true
285
- reason = "table '#{model.table_name}' has no columns"
286
- else
287
- reason = "table '#{model.table_name}' exists with #{model.columns.size} columns"
288
- end
289
- rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
261
+ puts "ModelDetector: Sequential filtering #{models.size} models..." if options[:verbose]
262
+ # Process models sequentially to prevent concurrent database connections
263
+ results = models.map do |model|
264
+ should_exclude = false
265
+ reason = nil
266
+
267
+ begin
268
+ # Skip abstract models unless explicitly included
269
+ if model.abstract_class? && !options[:include_abstract]
270
+ should_exclude = true
271
+ reason = 'abstract class'
272
+ # For abstract models that are included, skip table checks
273
+ elsif model.abstract_class? && options[:include_abstract]
274
+ reason = 'abstract class (included)'
275
+ # Skip models without configured tables
276
+ elsif !model.table_name
290
277
  should_exclude = true
291
- reason = "database error checking model - #{e.message}"
292
- rescue NameError, NoMethodError => e
278
+ reason = 'no table name'
279
+ # Skip models whose tables don't exist
280
+ elsif !model.table_exists?
293
281
  should_exclude = true
294
- reason = "method error checking model - #{e.message}"
295
- rescue StandardError => e
296
- # Catch any other errors and exclude the model to prevent ERD corruption
282
+ reason = "table '#{model.table_name}' does not exist"
283
+ # Additional check: Skip models that don't have any columns
284
+ elsif model.columns.empty?
297
285
  should_exclude = true
298
- reason = "unexpected error checking model - #{e.message}"
286
+ reason = "table '#{model.table_name}' has no columns"
287
+ else
288
+ reason = "table '#{model.table_name}' exists with #{model.columns.size} columns"
299
289
  end
290
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
291
+ should_exclude = true
292
+ reason = "database error checking model - #{e.message}"
293
+ rescue NameError, NoMethodError => e
294
+ should_exclude = true
295
+ reason = "method error checking model - #{e.message}"
296
+ rescue StandardError => e
297
+ # Catch any other errors and exclude the model to prevent ERD corruption
298
+ should_exclude = true
299
+ reason = "unexpected error checking model - #{e.message}"
300
+ end
300
301
 
301
- if trace_filtering
302
- action = should_exclude ? 'Excluding' : 'Keeping'
303
- Rails.logger.debug { "[ModelDetector] #{action} #{model.name}: #{reason}" }
302
+ if trace_filtering
303
+ action = should_exclude ? 'Excluding' : 'Keeping'
304
+ Rails.logger.debug { "[ModelDetector] #{action} #{model.name}: #{reason}" }
305
+ end
306
+
307
+ { model: model, exclude: should_exclude }
308
+ end
309
+
310
+ # Filter out excluded models
311
+ results.reject { |result| result[:exclude] }.pluck(:model)
312
+ end
313
+
314
+ def filter_models_with_connection_management(models, trace_filtering, options = {})
315
+ # Group models by connection pool first
316
+ models_by_pool = models.group_by do |model|
317
+ model.connection_pool
318
+ rescue StandardError
319
+ nil
320
+ end
321
+
322
+ # Assign orphaned models to primary pool
323
+ if models_by_pool[nil]&.any?
324
+ begin
325
+ primary_pool = ApplicationRecord.connection_pool
326
+ models_by_pool[primary_pool] ||= []
327
+ models_by_pool[primary_pool].concat(models_by_pool[nil])
328
+ models_by_pool.delete(nil)
329
+ rescue StandardError
330
+ # Keep orphaned models for individual processing
331
+ end
332
+ end
333
+
334
+ all_pools = models_by_pool.keys.compact
335
+ valid_models = []
336
+
337
+ models_by_pool.each do |pool, pool_models|
338
+ if pool
339
+ # Disconnect other pools before processing this one
340
+ all_pools.each do |p|
341
+ next if p == pool
342
+
343
+ begin
344
+ p.disconnect! if p.connected?
345
+ rescue StandardError
346
+ # Ignore disconnect errors
347
+ end
304
348
  end
305
349
 
306
- { model: model, exclude: should_exclude }
350
+ # Process models with managed connection
351
+ pool.with_connection do |connection|
352
+ pool_models.each do |model|
353
+ result = filter_single_model(model, connection, trace_filtering, options)
354
+ valid_models << model unless result[:exclude]
355
+ end
356
+ end
357
+ else
358
+ # Fallback for models without pools
359
+ pool_models.each do |model|
360
+ result = filter_single_model(model, nil, trace_filtering, options)
361
+ valid_models << model unless result[:exclude]
362
+ end
307
363
  end
308
364
  end
309
365
 
310
- # Wait for all futures to complete and filter results
311
- results = futures.map(&:value!)
312
- results.reject { |result| result[:exclude] }.pluck(:model)
366
+ valid_models
367
+ end
368
+
369
+ def filter_single_model(model, connection, trace_filtering, options)
370
+ should_exclude = false
371
+ reason = nil
372
+
373
+ begin
374
+ # Skip abstract models unless explicitly included
375
+ if model.abstract_class? && !options[:include_abstract]
376
+ should_exclude = true
377
+ reason = 'abstract class'
378
+ # For abstract models that are included, skip table checks
379
+ elsif model.abstract_class? && options[:include_abstract]
380
+ reason = 'abstract class (included)'
381
+ # Skip models without configured tables
382
+ elsif !model.table_name
383
+ should_exclude = true
384
+ reason = 'no table name'
385
+ # Skip models whose tables don't exist (use connection if available)
386
+ elsif connection ? !table_exists_with_connection?(model, connection) : !model.table_exists?
387
+ should_exclude = true
388
+ reason = "table '#{model.table_name}' does not exist"
389
+ # Additional check: Skip models that don't have any columns
390
+ elsif connection ? columns_empty_with_connection?(model, connection) : model.columns.empty?
391
+ should_exclude = true
392
+ reason = "table '#{model.table_name}' has no columns"
393
+ else
394
+ column_count = connection ? get_column_count_with_connection(model, connection) : model.columns.size
395
+ reason = "table '#{model.table_name}' exists with #{column_count} columns"
396
+ end
397
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotDefined => e
398
+ should_exclude = true
399
+ reason = "database error checking model - #{e.message}"
400
+ rescue NameError, NoMethodError => e
401
+ should_exclude = true
402
+ reason = "method error checking model - #{e.message}"
403
+ rescue StandardError => e
404
+ # Catch any other errors and exclude the model to prevent corruption
405
+ should_exclude = true
406
+ reason = "unexpected error checking model - #{e.message}"
407
+ end
408
+
409
+ if trace_filtering
410
+ action = should_exclude ? 'Excluding' : 'Keeping'
411
+ Rails.logger.debug { "[ModelDetector] #{action} #{model.name}: #{reason}" }
412
+ end
413
+
414
+ { model: model, exclude: should_exclude }
415
+ end
416
+
417
+ def table_exists_with_connection?(model, connection)
418
+ connection.table_exists?(model.table_name)
419
+ rescue StandardError
420
+ false
421
+ end
422
+
423
+ def columns_empty_with_connection?(model, connection)
424
+ connection.columns(model.table_name).empty?
425
+ rescue StandardError
426
+ true
427
+ end
428
+
429
+ def get_column_count_with_connection(model, connection)
430
+ connection.columns(model.table_name).size
431
+ rescue StandardError
432
+ 0
313
433
  end
314
434
 
315
435
  def has_sti_column?(model)
@@ -12,8 +12,12 @@ module RailsLens
12
12
  end
13
13
 
14
14
  def process(model_class, connection = nil)
15
- # Use passed connection or fall back to model's connection
16
- conn = connection || model_class.connection
15
+ # Always require a connection to be passed to prevent connection pool exhaustion
16
+ if connection.nil?
17
+ raise ArgumentError, 'SchemaProvider requires a connection to be passed to prevent connection pool exhaustion'
18
+ end
19
+
20
+ conn = connection
17
21
 
18
22
  if model_class.abstract_class?
19
23
  # For abstract classes, show database connection information in TOML format
@@ -85,8 +85,33 @@ module RailsLens
85
85
  end
86
86
  end
87
87
  else
88
- # Fall back to normal processing without connection management
89
- results = pipeline.process(model_class)
88
+ # Fallback: Use the model's connection pool with proper management
89
+ # This path is used when annotating individual models
90
+ warn "Using fallback connection management for #{model_class.name}" if RailsLens.config.verbose
91
+
92
+ # Force connection management even in fallback mode
93
+ results = { schema: nil, sections: [], notes: [] }
94
+
95
+ model_class.connection_pool.with_connection do |connection|
96
+ pipeline.instance_variable_get(:@providers).each do |provider|
97
+ next unless provider.applicable?(model_class)
98
+
99
+ begin
100
+ result = provider.process(model_class, connection)
101
+
102
+ case provider.type
103
+ when :schema
104
+ results[:schema] = result
105
+ when :section
106
+ results[:sections] << result if result
107
+ when :notes
108
+ results[:notes].concat(Array(result))
109
+ end
110
+ rescue StandardError => e
111
+ warn "Provider #{provider.class} error for #{model_class}: #{e.message}"
112
+ end
113
+ end
114
+ end
90
115
  end
91
116
 
92
117
  annotation = Annotation.new
@@ -117,7 +142,13 @@ module RailsLens
117
142
  end
118
143
 
119
144
  def self.annotate_all(options = {})
145
+ # Convert models option to include option for ModelDetector
146
+ if options[:models]
147
+ options[:include] = options[:models]
148
+ end
149
+
120
150
  models = ModelDetector.detect_models(options)
151
+ puts "Detected #{models.size} models for annotation" if options[:verbose]
121
152
 
122
153
  # Filter abstract classes based on options
123
154
  if options[:include_abstract]
@@ -133,13 +164,35 @@ module RailsLens
133
164
 
134
165
  # Group models by their connection pool to process each database separately
135
166
  models_by_connection_pool = models.group_by do |model|
136
- model.connection_pool
137
- rescue StandardError
138
- nil # Models without connection pools will be processed separately
167
+ pool = model.connection_pool
168
+ pool
169
+ rescue StandardError => e
170
+ puts "Model #{model.name} -> NO POOL (#{e.message})" if options[:verbose]
171
+ nil # Models without connection pools will use primary pool
139
172
  end
140
173
 
174
+ # Force models without connection pools to use the primary connection pool
175
+ if models_by_connection_pool[nil]&.any?
176
+ begin
177
+ primary_pool = ApplicationRecord.connection_pool
178
+ models_by_connection_pool[primary_pool] ||= []
179
+ models_by_connection_pool[primary_pool].concat(models_by_connection_pool[nil])
180
+ models_by_connection_pool.delete(nil)
181
+ rescue StandardError => e
182
+ puts "Failed to assign to primary pool: #{e.message}" if options[:verbose]
183
+ end
184
+ end
185
+
186
+ # Get all connection pools first
187
+ all_pools = get_all_connection_pools(models_by_connection_pool)
188
+
189
+ # Log initial connection status (removed verbose output)
190
+
141
191
  models_by_connection_pool.each do |connection_pool, pool_models|
142
192
  if connection_pool
193
+ # Disconnect all OTHER connection pools before processing this one
194
+ disconnect_other_pools(connection_pool, all_pools, options)
195
+
143
196
  # Process all models for this database using a single connection
144
197
  connection_pool.with_connection do |connection|
145
198
  pool_models.each do |model|
@@ -147,9 +200,20 @@ module RailsLens
147
200
  end
148
201
  end
149
202
  else
150
- # Process models without connection pools individually
151
- pool_models.each do |model|
152
- process_model_with_connection(model, nil, results, options)
203
+ # This should not happen anymore since we assign orphaned models to primary pool
204
+ # Use primary connection pool as fallback to avoid creating new connections
205
+ begin
206
+ primary_pool = ApplicationRecord.connection_pool
207
+ primary_pool.with_connection do |connection|
208
+ pool_models.each do |model|
209
+ process_model_with_connection(model, connection, results, options)
210
+ end
211
+ end
212
+ rescue StandardError => e
213
+ # Last resort: process without connection management (will create multiple connections)
214
+ pool_models.each do |model|
215
+ process_model_with_connection(model, nil, results, options)
216
+ end
153
217
  end
154
218
  end
155
219
  end
@@ -167,7 +231,6 @@ module RailsLens
167
231
  # Skip models without tables or with missing tables (but not abstract classes)
168
232
  unless model.abstract_class? || model.table_exists?
169
233
  results[:skipped] << model.name
170
- warn "Skipping #{model.name} - table does not exist" if options[:verbose]
171
234
  return
172
235
  end
173
236
 
@@ -191,10 +254,9 @@ module RailsLens
191
254
  else
192
255
  results[:skipped] << model.name
193
256
  end
194
- rescue ActiveRecord::StatementInvalid => e
257
+ rescue ActiveRecord::StatementInvalid
195
258
  # Handle database-related errors (missing tables, schemas, etc.)
196
259
  results[:skipped] << model.name
197
- warn "Skipping #{model.name} - database error: #{e.message}" if options[:verbose]
198
260
  rescue StandardError => e
199
261
  model_name = if model.is_a?(Class) && model.respond_to?(:name)
200
262
  model.name
@@ -222,6 +284,45 @@ module RailsLens
222
284
  results
223
285
  end
224
286
 
287
+ def self.disconnect_other_pools(current_pool, all_pools, options = {})
288
+ all_pools.each do |pool|
289
+ next if pool == current_pool || pool.nil?
290
+
291
+ begin
292
+ if pool.connected?
293
+ pool.disconnect!
294
+ end
295
+ rescue StandardError => e
296
+ warn "Failed to disconnect pool: #{e.message}"
297
+ end
298
+ end
299
+ end
300
+
301
+ def self.get_all_connection_pools(models_by_pool)
302
+ models_by_pool.keys.compact
303
+ end
304
+
305
+ def self.log_connection_status(all_pools, options = {})
306
+ return unless options[:verbose]
307
+
308
+ puts "\n=== Connection Pool Status ==="
309
+ all_pools.each do |pool|
310
+ next unless pool
311
+
312
+ begin
313
+ name = pool.db_config&.name || 'unknown'
314
+ connected = pool.connected?
315
+ size = pool.size
316
+ checked_out = pool.stat[:busy]
317
+
318
+ puts "Pool #{name}: connected=#{connected}, size=#{size}, busy=#{checked_out}"
319
+ rescue StandardError => e
320
+ puts "Pool status error: #{e.message}"
321
+ end
322
+ end
323
+ puts "================================\n"
324
+ end
325
+
225
326
  private
226
327
 
227
328
  def add_annotation(content, _file_path = nil)
@@ -9,7 +9,14 @@ namespace :rails_lens do
9
9
  options = {}
10
10
  options[:include_abstract] = true if ENV['INCLUDE_ABSTRACT'] == 'true'
11
11
 
12
- puts 'Annotating models with schema information...'
12
+ # Support model filtering via environment variable
13
+ if ENV['MODELS']
14
+ model_list = ENV['MODELS'].split(',').map(&:strip)
15
+ options[:models] = model_list
16
+ puts "Filtering to specific models: #{model_list.join(', ')}"
17
+ end
18
+
19
+ options[:verbose] = true # Force verbose mode to see connection management
13
20
  results = RailsLens.annotate_models(options)
14
21
 
15
22
  if results[:annotated].any?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsLens
4
- VERSION = '0.2.4'
4
+ VERSION = '0.2.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih