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.

Instance Attribute Summary collapse

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, overflow_policy: :evict_idle) ⇒ PoolReaper

Returns a new instance of PoolReaper.

Raises:

  • (ArgumentError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/apartment/pool_reaper.rb', line 32

def initialize(pool_manager:, interval:, idle_timeout:, max_total: nil,
               default_tenant: nil, shard_key_prefix: nil, on_evict: nil,
               overflow_policy: :evict_idle)
  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
  @overflow_policy = overflow_policy
  @mutex = Mutex.new
  @timer = nil
end

Instance Attribute Details

#idle_timeoutObject (readonly)

Reap cadence (seconds) and the idle window (seconds) a pool must exceed before it is eligible for idle eviction. Decoupled so a deployment can reap frequently without shrinking the idle window. Exposed for introspection and wiring assertions.



15
16
17
# File 'lib/apartment/pool_reaper.rb', line 15

def idle_timeout
  @idle_timeout
end

#intervalObject (readonly)

Reap cadence (seconds) and the idle window (seconds) a pool must exceed before it is eligible for idle eviction. Decoupled so a deployment can reap frequently without shrinking the idle window. Exposed for introspection and wiring assertions.



15
16
17
# File 'lib/apartment/pool_reaper.rb', line 15

def interval
  @interval
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)


26
27
28
29
30
# File 'lib/apartment/pool_reaper.rb', line 26

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

#admit!(incoming_tenant_key) ⇒ Object

Synchronously enforce max_total before a new tenant pool is admitted. Called by Apartment::PoolManager#fetch_or_create under its creation lock (so the capacity check, eviction, and insert are atomic w.r.t. other creators). Evicts LRU idle (non-protected, non-default) pools until there is room for one more; if none can be freed, applies the overflow policy. A no-op when no cap is configured. See docs/designs/pool-admission-control.md.



61
62
63
64
65
66
67
68
69
70
71
# File 'lib/apartment/pool_reaper.rb', line 61

def admit!(incoming_tenant_key)
  return unless @max_total

  loop do
    break if @pool_manager.stats[:total_pools] < @max_total
    break unless evict_one_for_admission(incoming_tenant_key)
  end
  return if @pool_manager.stats[:total_pools] < @max_total

  apply_overflow_policy
end

#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`.



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/apartment/pool_reaper.rb', line 93

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)


86
87
88
# File 'lib/apartment/pool_reaper.rb', line 86

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

#startObject



73
74
75
76
77
78
79
80
# File 'lib/apartment/pool_reaper.rb', line 73

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

#stopObject



82
83
84
# File 'lib/apartment/pool_reaper.rb', line 82

def stop
  @mutex.synchronize { stop_internal }
end