Class: Apartment::PoolReaper

Inherits:
Object
  • Object
show all
Defined in:
lib/apartment/pool_reaper.rb

Overview

Evicts idle and excess tenant pools on a background timer. Complementary to ActiveRecord’s ConnectionPool::Reaper which handles intra-pool connection reaping — this handles inter-pool (tenant) eviction.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pool_manager:, interval:, idle_timeout:, max_total: nil, default_tenant: nil, shard_key_prefix: nil, on_evict: nil) ⇒ PoolReaper

Returns a new instance of PoolReaper.

Raises:

  • (ArgumentError)


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/apartment/pool_reaper.rb', line 26

def initialize(pool_manager:, interval:, idle_timeout:, max_total: nil,
               default_tenant: nil, shard_key_prefix: nil, on_evict: nil)
  raise(ArgumentError, 'interval must be a positive number') unless interval.is_a?(Numeric) && interval.positive?
  unless idle_timeout.is_a?(Numeric) && idle_timeout.positive?
    raise(ArgumentError, 'idle_timeout must be a positive number')
  end
  if max_total && (!max_total.is_a?(Integer) || max_total < 1)
    raise(ArgumentError, 'max_total must be a positive integer or nil')
  end

  @pool_manager = pool_manager
  @interval = interval
  @idle_timeout = idle_timeout
  @max_total = max_total
  @default_tenant = default_tenant
  @shard_key_prefix = shard_key_prefix
  @on_evict = on_evict
  @mutex = Mutex.new
  @timer = nil
end

Class Method Details

.pool_pinned?(pool) ⇒ Boolean

True when Rails’ transactional-fixture machinery has pinned the pool (ConnectionPool#pin_connection!, Rails 7.1+). Evicting or discarding a pinned pool strands the fixture transaction; teardown then errors or marks the DB dirty. ActiveRecord exposes no public predicate, so we read the ivar it sets. TOCTOU caveat applies — see docs/testing.md “Pool lifecycle in tests”.

Exposed as a class method so Apartment.reset_tenant_pools! can reuse the same primitive without instantiating a reaper.

Returns:

  • (Boolean)


20
21
22
23
24
# File 'lib/apartment/pool_reaper.rb', line 20

def self.pool_pinned?(pool)
  return false unless pool&.instance_variable_defined?(:@pinned_connection)

  !pool.instance_variable_get(:@pinned_connection).nil?
end

Instance Method Details

#run_cycleObject

Perform one synchronous eviction pass (idle + LRU). Returns the total number of pools evicted. Called by the background timer and by CLI ‘pool evict`.



67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/apartment/pool_reaper.rb', line 67

def run_cycle
  count = 0
  count += evict_idle
  count += evict_lru if @max_total
  count
rescue Apartment::ApartmentError => e
  warn "[Apartment::PoolReaper] #{e.class}: #{e.message}"
  0
rescue StandardError => e
  warn "[Apartment::PoolReaper] Unexpected error: #{e.class}: #{e.message}"
  warn e.backtrace&.first(5)&.join("\n") if e.backtrace
  0
end

#running?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/apartment/pool_reaper.rb', line 60

def running?
  @mutex.synchronize { @timer&.running? || false }
end

#startObject



47
48
49
50
51
52
53
54
# File 'lib/apartment/pool_reaper.rb', line 47

def start
  @mutex.synchronize do
    stop_internal
    @timer = Concurrent::TimerTask.new(execution_interval: @interval) { reap }
    @timer.execute
  end
  self
end

#stopObject



56
57
58
# File 'lib/apartment/pool_reaper.rb', line 56

def stop
  @mutex.synchronize { stop_internal }
end