Class: Apartment::PoolReaper
- Inherits:
-
Object
- Object
- Apartment::PoolReaper
- 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
-
#idle_timeout ⇒ Object
readonly
Reap cadence (seconds) and the idle window (seconds) a pool must exceed before it is eligible for idle eviction.
-
#interval ⇒ Object
readonly
Reap cadence (seconds) and the idle window (seconds) a pool must exceed before it is eligible for idle eviction.
Class Method Summary collapse
-
.pool_pinned?(pool) ⇒ Boolean
True when Rails’ transactional-fixture machinery has pinned the pool (ConnectionPool#pin_connection!, Rails 7.1+).
Instance Method Summary collapse
-
#admit!(incoming_tenant_key) ⇒ Object
Synchronously enforce max_total before a new tenant pool is admitted.
-
#initialize(pool_manager:, interval:, idle_timeout:, max_total: nil, default_tenant: nil, shard_key_prefix: nil, on_evict: nil, overflow_policy: :evict_idle) ⇒ PoolReaper
constructor
A new instance of PoolReaper.
-
#run_cycle ⇒ Object
Perform one synchronous eviction pass (idle + LRU).
- #running? ⇒ Boolean
- #start ⇒ Object
- #stop ⇒ Object
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.
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_timeout ⇒ Object (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 |
#interval ⇒ Object (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.
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_cycle ⇒ Object
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.}" 0 rescue StandardError => e warn "[Apartment::PoolReaper] Unexpected error: #{e.class}: #{e.}" warn e.backtrace&.first(5)&.join("\n") if e.backtrace 0 end |
#running? ⇒ Boolean
86 87 88 |
# File 'lib/apartment/pool_reaper.rb', line 86 def running? @mutex.synchronize { @timer&.running? || false } end |
#start ⇒ Object
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 |
#stop ⇒ Object
82 83 84 |
# File 'lib/apartment/pool_reaper.rb', line 82 def stop @mutex.synchronize { stop_internal } end |