ros-apartment 4.0.0.alpha7 → 4.0.0.alpha8
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 +4 -4
- data/README.md +7 -3
- data/lib/apartment/CLAUDE.md +4 -3
- data/lib/apartment/config.rb +54 -1
- data/lib/apartment/errors.rb +18 -9
- data/lib/apartment/patches/postgresql_sequence_name.rb +62 -0
- data/lib/apartment/pool_observer.rb +40 -2
- data/lib/apartment/version.rb +1 -1
- data/lib/apartment.rb +17 -5
- data/lib/generators/apartment/install/templates/apartment.rb +14 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58a2c1795ee20c09d5ffa99ece8bb18d8697f981f6a6292750941775ee81738b
|
|
4
|
+
data.tar.gz: fb37448602b18043aee5f2d49d4f037c82b9b744917eeafb33de7e1099bdec6d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9913b7e11919a0a2fb39398a67cab8a5dad0dda6d637df954bec0222bcc83c93b1e71dc36469316d2a50fbdf07996d665903b24f17c87cf99191fea009661ffa
|
|
7
|
+
data.tar.gz: f1128508d59269eec5ed3c5f2a684b0a8205049242c53b0729b461502ae2d7e2b8613519e4e0cb949896f10468011c6684d0ceca266961ced545fba5e3242885
|
data/README.md
CHANGED
|
@@ -118,15 +118,19 @@ All options are set in `config/initializers/apartment.rb` inside an `Apartment.c
|
|
|
118
118
|
|
|
119
119
|
### Pool Settings
|
|
120
120
|
|
|
121
|
-
`tenant_pool_size`: max connections per tenant pool. Default `nil` — each tenant pool inherits the app's base pool size (`DB_POOL_SIZE` / `pool:` in `database.yml`). Set it to
|
|
121
|
+
`tenant_pool_size`: max connections per tenant pool. Default `nil` — each tenant pool inherits the app's base pool size (`DB_POOL_SIZE` / `pool:` in `database.yml`). **Set it to at least the peak number of threads/fibers in one process that can touch the _same_ tenant at once** (e.g. a Sidekiq role's concurrency for a same-tenant job fan-out); a smaller pool makes those threads block on connection checkout. It is a lazy ceiling — connections are created on demand and idle pools are reaped — so sizing for peak does not hold that many connections open at steady state.
|
|
122
122
|
|
|
123
123
|
`pool_idle_timeout`: seconds an idle tenant pool must exceed before it is eligible for reaping (default: 300).
|
|
124
124
|
|
|
125
125
|
`reaper_interval`: seconds between background reap passes. Default `nil` — derives from `pool_idle_timeout`. Set it lower to reap more often without shrinking the idle window.
|
|
126
126
|
|
|
127
|
-
`
|
|
127
|
+
`max_tenant_pools`: ceiling on the number of live tenant pools per process; `nil` for unlimited (default: `nil`). Enforced synchronously at pool-creation time (see `pool_overflow_policy`) and trimmed continuously by the background reaper.
|
|
128
128
|
|
|
129
|
-
`
|
|
129
|
+
`max_tenant_connections`: ceiling on total *tenant-pool* connections per process (default: `nil`); requires `tenant_pool_size`. The admission controller derives the pool budget as `floor(max_tenant_connections / tenant_pool_size)`. Per-process tenant-pool connections ≈ `effective_pool_budget × tenant_pool_size`, where `effective_pool_budget = min(max_tenant_pools, floor(max_tenant_connections / tenant_pool_size))`; the default pool and any separate pinned pool are additional. A tenant using multiple roles (e.g. `writing` + `reading`) holds one pool per role (`tenant:role` keys), counting once per role against the budget. For a hard external-pooler budget, pair this with `pool_overflow_policy: :raise` (the ceiling is soft under the default `:evict_idle`).
|
|
130
|
+
|
|
131
|
+
`max_total_connections`: **deprecated** (removed in v5) — it capped tenant-pool *count*, not connections. It now aliases `max_tenant_pools`; rename it. For a true connection ceiling, set `max_tenant_connections`.
|
|
132
|
+
|
|
133
|
+
`pool_overflow_policy`: behavior when a new pool would breach the pool budget (`max_tenant_pools` / `max_tenant_connections`) and every existing pool is pinned or in use (no idle pool to evict). `:evict_idle` (default) — allow the new pool, emit a `cap_unmet` notification (soft cap, prioritizes availability). `:raise` — raise `Apartment::PoolCapacityReached` (hard cap, sheds load). When an idle pool *is* available it is always evicted inline regardless of policy. See `docs/designs/pool-admission-control.md`.
|
|
130
134
|
|
|
131
135
|
`reap_in_test`: keep the background reaper running under `Rails.env.test?` (default `false` — the Railtie stops it in test, where fixture transactions make mid-example eviction a liability). Set `true` if a deployed process can run under test-env semantics and must keep reaping — that's cleaner than guarding `RAILS_ENV` at boot to avoid silently leaking connections. It applies to *every* `Rails.env.test?` process, including CI, so enable it only when a real deployment needs it.
|
|
132
136
|
|
data/lib/apartment/CLAUDE.md
CHANGED
|
@@ -20,7 +20,8 @@ lib/apartment/
|
|
|
20
20
|
│ └── mysql_config.rb # MysqlConfig: placeholder
|
|
21
21
|
├── elevators/ # Rack middleware for tenant detection (see CLAUDE.md); v4 uses constructor keyword args, no class-level state; Generic, Subdomain, FirstSubdomain, Domain, Host, HostHash, Header
|
|
22
22
|
├── patches/ # ActiveRecord patches for tenant-aware connections
|
|
23
|
-
│
|
|
23
|
+
│ ├── connection_handling.rb # Prepends on AR::Base — tenant-aware connection_pool
|
|
24
|
+
│ └── postgresql_sequence_name.rb # Prepends on the PG adapter — schema-agnostic Model.sequence_name memoization
|
|
24
25
|
├── tasks/ # Rake task utilities; v4.rake for apartment:create/drop/migrate/seed/rollback
|
|
25
26
|
├── config.rb # Configuration with validate!/freeze!
|
|
26
27
|
├── current.rb # Fiber-safe tenant context (CurrentAttributes)
|
|
@@ -59,7 +60,7 @@ lib/apartment/
|
|
|
59
60
|
|
|
60
61
|
### pool_manager.rb — Pool Cache
|
|
61
62
|
|
|
62
|
-
`Concurrent::Map` storing connection pools by tenant key. Monotonic clock timestamps for idle/LRU tracking. `stats_for` returns `{ seconds_idle: N }`. `clear` disconnects all pools before clearing. When `max_total_connections` is
|
|
63
|
+
`Concurrent::Map` storing connection pools by tenant key. Monotonic clock timestamps for idle/LRU tracking. `stats_for` returns `{ seconds_idle: N }`. `clear` disconnects all pools before clearing. When a pool budget is configured (`max_tenant_pools` and/or `max_tenant_connections`, resolved by `Config#effective_pool_budget`; `max_total_connections` is the deprecated alias of `max_tenant_pools`), `Apartment.configure` wires an `admission_controller` (the reaper) so cold creates route through a serialized, capacity-bounded path; otherwise the lock-free `compute_if_absent` fast path is used. See `docs/designs/pool-connection-budget.md` and `docs/designs/pool-admission-control.md`.
|
|
63
64
|
|
|
64
65
|
### pool_reaper.rb — Pool Eviction + Admission
|
|
65
66
|
|
|
@@ -97,7 +98,7 @@ All inherit from `AbstractAdapter`. Override `resolve_connection_config`, `creat
|
|
|
97
98
|
|
|
98
99
|
### pool_observer.rb — Observability (opt-in)
|
|
99
100
|
|
|
100
|
-
Sink-agnostic subscriber for the pool events (`create`/`evict`/`cap_unmet`/`skip_evict`/`reaper_stopped`) + an optional `Concurrent::TimerTask` gauge sampler
|
|
101
|
+
Sink-agnostic subscriber for the pool events (`create`/`evict`/`cap_unmet`/`skip_evict`/`reaper_stopped`) + an optional `Concurrent::TimerTask` gauge sampler. Gauges: `tenant_pools_live`, optional adopter `backend_count`, and per-pass checkout-pressure gauges from each tenant pool's AR `ConnectionPool#stat` — `pools_waiting` (blocked on checkout), `pools_saturated` (`busy >= size`), `max_checkout_waiting` (worst pool's `waiting`, tenant in payload not a dimension). Per-pool `#stat` is rescued so one tearing-down pool can't abort the pass. Normalizes each to a `Sample` via the shared `emit_gauge` helper and forwards to a caller `sink`; ships no transport. Error-isolated — never raises into instrumentation. See `docs/observability.md`.
|
|
101
102
|
|
|
102
103
|
### railtie.rb — v4 Rails Integration
|
|
103
104
|
|
data/lib/apartment/config.rb
CHANGED
|
@@ -17,7 +17,8 @@ module Apartment
|
|
|
17
17
|
attr_accessor :tenants_provider, :default_tenant,
|
|
18
18
|
:default_tenant_switch_allowed,
|
|
19
19
|
:tenant_pool_size, :pool_idle_timeout, :reaper_interval,
|
|
20
|
-
:max_total_connections, :
|
|
20
|
+
:max_total_connections, :max_tenant_pools, :max_tenant_connections,
|
|
21
|
+
:pool_overflow_policy,
|
|
21
22
|
:seed_after_create, :seed_data_file,
|
|
22
23
|
:schema_load_strategy, :schema_file,
|
|
23
24
|
:parallel_migration_threads,
|
|
@@ -38,6 +39,8 @@ module Apartment
|
|
|
38
39
|
@pool_idle_timeout = 300
|
|
39
40
|
@reaper_interval = nil
|
|
40
41
|
@max_total_connections = nil
|
|
42
|
+
@max_tenant_pools = nil
|
|
43
|
+
@max_tenant_connections = nil
|
|
41
44
|
@pool_overflow_policy = :evict_idle
|
|
42
45
|
@seed_after_create = false
|
|
43
46
|
@seed_data_file = nil
|
|
@@ -124,6 +127,17 @@ module Apartment
|
|
|
124
127
|
# Reap on the idle-timeout cadence unless an explicit interval decouples
|
|
125
128
|
# the two (reap more often without shrinking the idle window).
|
|
126
129
|
@reaper_interval = @pool_idle_timeout if @reaper_interval.nil?
|
|
130
|
+
|
|
131
|
+
# max_total_connections is deprecated: the name said "connections" but it
|
|
132
|
+
# always capped tenant-pool COUNT. Alias it to its true meaning without
|
|
133
|
+
# changing behavior. Only fill when max_tenant_pools was not set explicitly,
|
|
134
|
+
# so validate!'s both-set guard can still catch a genuine double-spec.
|
|
135
|
+
return unless @max_total_connections
|
|
136
|
+
|
|
137
|
+
warn '[Apartment] DEPRECATION: config.max_total_connections is deprecated and will be ' \
|
|
138
|
+
'removed in v5. It caps tenant-pool COUNT, not connections; rename it to ' \
|
|
139
|
+
'max_tenant_pools. For a true connection ceiling, set max_tenant_connections.'
|
|
140
|
+
@max_tenant_pools = @max_total_connections if @max_tenant_pools.nil?
|
|
127
141
|
end
|
|
128
142
|
|
|
129
143
|
# Validate configuration completeness and consistency.
|
|
@@ -171,6 +185,35 @@ module Apartment
|
|
|
171
185
|
"max_total_connections must be a positive integer or nil, got: #{@max_total_connections.inspect}")
|
|
172
186
|
end
|
|
173
187
|
|
|
188
|
+
if @max_tenant_pools && (!@max_tenant_pools.is_a?(Integer) || @max_tenant_pools < 1)
|
|
189
|
+
raise(ConfigurationError,
|
|
190
|
+
"max_tenant_pools must be a positive integer or nil, got: #{@max_tenant_pools.inspect}")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if @max_total_connections && @max_tenant_pools && @max_total_connections != @max_tenant_pools
|
|
194
|
+
raise(ConfigurationError,
|
|
195
|
+
'max_total_connections and max_tenant_pools are the same setting under two names; ' \
|
|
196
|
+
"set only one. Got max_total_connections=#{@max_total_connections}, " \
|
|
197
|
+
"max_tenant_pools=#{@max_tenant_pools}")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
if @max_tenant_connections && (!@max_tenant_connections.is_a?(Integer) || @max_tenant_connections < 1)
|
|
201
|
+
raise(ConfigurationError,
|
|
202
|
+
"max_tenant_connections must be a positive integer or nil, got: #{@max_tenant_connections.inspect}")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if @max_tenant_connections && @tenant_pool_size.nil?
|
|
206
|
+
raise(ConfigurationError,
|
|
207
|
+
'max_tenant_connections requires tenant_pool_size to be set (the pool budget is ' \
|
|
208
|
+
'derived as max_tenant_connections / tenant_pool_size)')
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if @max_tenant_connections && @tenant_pool_size && @max_tenant_connections < @tenant_pool_size
|
|
212
|
+
raise(ConfigurationError,
|
|
213
|
+
"max_tenant_connections (#{@max_tenant_connections}) must be >= tenant_pool_size " \
|
|
214
|
+
"(#{@tenant_pool_size}); it cannot fit a single tenant pool")
|
|
215
|
+
end
|
|
216
|
+
|
|
174
217
|
unless %i[evict_idle raise].include?(@pool_overflow_policy)
|
|
175
218
|
raise(ConfigurationError,
|
|
176
219
|
'pool_overflow_policy must be :evict_idle or :raise, ' \
|
|
@@ -238,5 +281,15 @@ module Apartment
|
|
|
238
281
|
def rails_env_name
|
|
239
282
|
(Rails.env if defined?(Rails.env)) || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'default_env'
|
|
240
283
|
end
|
|
284
|
+
|
|
285
|
+
# The pool-count bound the admission controller enforces: the stricter of the
|
|
286
|
+
# explicit pool cap (max_tenant_pools) and the pool budget derived from the
|
|
287
|
+
# connection ceiling (max_tenant_connections / tenant_pool_size, floored).
|
|
288
|
+
# Returns nil (uncapped) when neither knob is set. Relies on a global
|
|
289
|
+
# tenant_pool_size, so tenant-pool connections <= budget * tenant_pool_size.
|
|
290
|
+
def effective_pool_budget
|
|
291
|
+
derived = @max_tenant_connections && @tenant_pool_size ? @max_tenant_connections / @tenant_pool_size : nil
|
|
292
|
+
[@max_tenant_pools, derived].compact.min
|
|
293
|
+
end
|
|
241
294
|
end
|
|
242
295
|
end
|
data/lib/apartment/errors.rb
CHANGED
|
@@ -33,22 +33,31 @@ module Apartment
|
|
|
33
33
|
# Raised when the tenant connection pool is exhausted.
|
|
34
34
|
class PoolExhausted < ApartmentError; end
|
|
35
35
|
|
|
36
|
-
# Raised when admitting a new tenant pool would exceed
|
|
37
|
-
# and no idle pool can be evicted to make
|
|
38
|
-
# policy. Distinct from PoolExhausted (a single
|
|
39
|
-
# the process-wide pool-
|
|
36
|
+
# Raised when admitting a new tenant pool would exceed the effective pool
|
|
37
|
+
# budget (Config#effective_pool_budget) and no idle pool can be evicted to make
|
|
38
|
+
# room, under the :raise overflow policy. Distinct from PoolExhausted (a single
|
|
39
|
+
# pool's connections) — this is the process-wide pool-COUNT ceiling. The message
|
|
40
|
+
# names the knobs that set it, never the deprecated max_total_connections: that
|
|
41
|
+
# name is what made the budget look like a connection count in the first place.
|
|
42
|
+
# See docs/designs/pool-connection-budget.md and pool-admission-control.md.
|
|
40
43
|
class PoolCapacityReached < ApartmentError
|
|
44
|
+
# +max_total+ is the effective POOL budget, not a connection count. The
|
|
45
|
+
# reader keeps its name for compatibility with anything already rescuing this.
|
|
41
46
|
attr_reader :max_total, :current
|
|
42
47
|
|
|
48
|
+
alias pool_budget max_total
|
|
49
|
+
|
|
43
50
|
def initialize(max_total: nil, current: nil)
|
|
44
51
|
@max_total = max_total
|
|
45
52
|
@current = current
|
|
46
53
|
super(
|
|
47
|
-
"Tenant pool capacity reached: #{current.inspect} pools open, " \
|
|
48
|
-
"
|
|
49
|
-
'be evicted to admit another (all pinned or in use). Raise ' \
|
|
50
|
-
'
|
|
51
|
-
'
|
|
54
|
+
"Tenant pool capacity reached: #{current.inspect} tenant pools open, " \
|
|
55
|
+
"the effective pool budget is #{max_total.inspect}, and no idle pool " \
|
|
56
|
+
'could be evicted to admit another (all pinned or in use). Raise ' \
|
|
57
|
+
'max_tenant_pools, or raise max_tenant_connections (which derives the ' \
|
|
58
|
+
'pool budget as max_tenant_connections / tenant_pool_size), reduce ' \
|
|
59
|
+
'concurrent tenants, or set pool_overflow_policy to :evict_idle to ' \
|
|
60
|
+
'allow soft overflow.'
|
|
52
61
|
)
|
|
53
62
|
end
|
|
54
63
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apartment
|
|
4
|
+
module Patches
|
|
5
|
+
# Keeps ActiveRecord's class-level Model.sequence_name memoization
|
|
6
|
+
# schema-agnostic under pool-per-tenant.
|
|
7
|
+
#
|
|
8
|
+
# Rails resolves default_sequence_name via pg_get_serial_sequence with an
|
|
9
|
+
# unqualified table name, so PostgreSQL answers through the current
|
|
10
|
+
# connection's search_path and returns a schema-QUALIFIED name — the schema
|
|
11
|
+
# of whichever tenant's pool happened to resolve it first. ActiveRecord then
|
|
12
|
+
# memoizes that value once per model class, process-wide. Consumers that
|
|
13
|
+
# render it into SQL (activerecord-import prefetches primary keys as a
|
|
14
|
+
# literal nextval(Model.sequence_name)) would draw ids from the
|
|
15
|
+
# first-resolver tenant's sequence in every tenant: wrong-tenant ids, silent
|
|
16
|
+
# sequence drift, and eventual PG::UniqueViolation.
|
|
17
|
+
#
|
|
18
|
+
# Stripping the connection's own current_schema prefix makes the memoized
|
|
19
|
+
# value schema-agnostic: nextval('widgets_id_seq') re-resolves through each
|
|
20
|
+
# pool's search_path, per tenant — the same invariant v4 relies on for table
|
|
21
|
+
# names (see docs/designs/v4-connection-model-rationale.md). A prefix naming
|
|
22
|
+
# any OTHER schema is preserved on purpose: persistent-schema tables and
|
|
23
|
+
# pinned models (whose table names are default_tenant-qualified and may
|
|
24
|
+
# execute on a shared tenant connection) are only correct BECAUSE they stay
|
|
25
|
+
# qualified.
|
|
26
|
+
#
|
|
27
|
+
# v3 shipped this guarantee as PostgreSqlAdapterPatch; it was lost in the
|
|
28
|
+
# Phase 2.5 v3 deletion (PR #356). Reset-on-switch is not an alternative:
|
|
29
|
+
# v4 serves tenants concurrently in one process, so only a schema-agnostic
|
|
30
|
+
# memoized value can be correct.
|
|
31
|
+
module PostgresqlSequenceName
|
|
32
|
+
def default_sequence_name(table_name, primary_key = 'id')
|
|
33
|
+
res = super
|
|
34
|
+
return res if res.nil?
|
|
35
|
+
# A schema-qualified table name is a PINNED model: Apartment qualifies
|
|
36
|
+
# those to the default tenant, and they resolve on whatever connection is
|
|
37
|
+
# current (the schema strategy shares the tenant's connection). Its
|
|
38
|
+
# sequence must stay qualified — stripping here is the mirror-image bug,
|
|
39
|
+
# because an unqualified name would later re-resolve against a *tenant's*
|
|
40
|
+
# search_path and draw ids from that tenant's stale copy of the pinned
|
|
41
|
+
# table's sequence. That order is the common one, not a corner case:
|
|
42
|
+
# pinned models are typically first touched at boot, on the default pool.
|
|
43
|
+
# v3 guarded the same case by force-adding the default_tenant prefix.
|
|
44
|
+
return res if table_name.to_s.include?('.')
|
|
45
|
+
|
|
46
|
+
# Routed table: drop whatever schema PostgreSQL qualified the sequence
|
|
47
|
+
# with — not just the connection's own. The two differ when the tenant
|
|
48
|
+
# schema is missing the table and search_path falls through to a schema
|
|
49
|
+
# that has it: the resolved sequence then names the FALLBACK schema, and
|
|
50
|
+
# preserving that would memoize it process-wide, so every tenant —
|
|
51
|
+
# including correctly-migrated ones — would draw from the fallback's
|
|
52
|
+
# sequence forever. Unqualified is the only value that lets the sequence
|
|
53
|
+
# re-resolve per pool exactly like the table name does.
|
|
54
|
+
#
|
|
55
|
+
# Parsed with AR's own splitter (rather than by hand) because it is what
|
|
56
|
+
# produced `res`, so it round-trips quoting and dotted identifiers.
|
|
57
|
+
ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
|
|
58
|
+
.extract_schema_qualified_name(res).identifier
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -66,14 +66,15 @@ module Apartment
|
|
|
66
66
|
# when supplied. Safe to call from start_sampler! or an external scheduler.
|
|
67
67
|
def sample!
|
|
68
68
|
total = Apartment.pool_manager&.stats&.fetch(:total_pools, 0) || 0
|
|
69
|
-
|
|
69
|
+
emit_gauge(:tenant_pools_live, total)
|
|
70
|
+
emit_checkout_pressure!
|
|
70
71
|
|
|
71
72
|
return unless @backend_count
|
|
72
73
|
|
|
73
74
|
backends = @backend_count.call
|
|
74
75
|
return if backends.nil?
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
emit_gauge(:backend_connections, backends)
|
|
77
78
|
rescue StandardError => e
|
|
78
79
|
warn_failure('sample!', e)
|
|
79
80
|
end
|
|
@@ -104,6 +105,43 @@ module Apartment
|
|
|
104
105
|
@sampler = nil
|
|
105
106
|
end
|
|
106
107
|
|
|
108
|
+
# Per-tenant-pool checkout pressure, aggregated to low cardinality. waiting>0
|
|
109
|
+
# means threads are blocked acquiring a connection (the same-tenant fan-out
|
|
110
|
+
# starvation signal). The worst tenant is carried in the Sample payload, NOT
|
|
111
|
+
# as a metric dimension, to avoid unbounded time-series churn. Per-pool #stat
|
|
112
|
+
# is rescued so a pool tearing down mid-sample can't abort the whole pass.
|
|
113
|
+
def emit_checkout_pressure!
|
|
114
|
+
manager = Apartment.pool_manager
|
|
115
|
+
return unless manager
|
|
116
|
+
|
|
117
|
+
stats = collect_pool_stats(manager)
|
|
118
|
+
worst = stats.max_by { |s| s[:pending] } || { tenant: nil, pending: 0 }
|
|
119
|
+
payload = worst[:pending].positive? ? { tenant: worst[:tenant] } : {}
|
|
120
|
+
|
|
121
|
+
emit_gauge(:pools_waiting, stats.count { |s| s[:pending].positive? })
|
|
122
|
+
emit_gauge(:pools_saturated, stats.count { |s| s[:saturated] })
|
|
123
|
+
emit_gauge(:max_checkout_waiting, worst[:pending], payload: payload)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def emit_gauge(name, value, payload: {})
|
|
127
|
+
emit(Sample.new(name: name, kind: :gauge, value: value, dimensions: {}, payload: payload))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Read each tenant pool's checkout stats into [{ tenant:, pending:, saturated: }],
|
|
131
|
+
# skipping any pool whose #stat raises (mid-teardown) so one bad pool can't
|
|
132
|
+
# abort the whole pass.
|
|
133
|
+
def collect_pool_stats(manager)
|
|
134
|
+
stats = []
|
|
135
|
+
manager.each_pair do |tenant_key, pool|
|
|
136
|
+
stat = pool.stat
|
|
137
|
+
stats << { tenant: tenant_key, pending: stat[:waiting].to_i,
|
|
138
|
+
saturated: stat[:busy].to_i >= stat[:size].to_i }
|
|
139
|
+
rescue StandardError
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
stats
|
|
143
|
+
end
|
|
144
|
+
|
|
107
145
|
def record_event(event, payload)
|
|
108
146
|
# Copy the notification payload so a sink that mutates Sample#payload
|
|
109
147
|
# can't corrupt it for other subscribers of the same event.
|
data/lib/apartment/version.rb
CHANGED
data/lib/apartment.rb
CHANGED
|
@@ -265,21 +265,23 @@ module Apartment # rubocop:disable Metrics/ModuleLength
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
# Build the pool manager + reaper for a freshly-validated config and start
|
|
268
|
-
# the background reaper. When
|
|
269
|
-
#
|
|
270
|
-
#
|
|
268
|
+
# the background reaper. When a pool budget is configured (max_tenant_pools
|
|
269
|
+
# and/or max_tenant_connections, via Config#effective_pool_budget), wire the
|
|
270
|
+
# reaper as the pool manager's admission controller so cold creates are
|
|
271
|
+
# bounded synchronously; otherwise the manager keeps its lock-free create path.
|
|
271
272
|
def setup_pools!(new_config)
|
|
273
|
+
budget = new_config.effective_pool_budget
|
|
272
274
|
@pool_manager = PoolManager.new
|
|
273
275
|
@pool_reaper = PoolReaper.new(
|
|
274
276
|
pool_manager: @pool_manager,
|
|
275
277
|
interval: new_config.reaper_interval,
|
|
276
278
|
idle_timeout: new_config.pool_idle_timeout,
|
|
277
|
-
max_total:
|
|
279
|
+
max_total: budget,
|
|
278
280
|
default_tenant: new_config.default_tenant,
|
|
279
281
|
shard_key_prefix: new_config.shard_key_prefix,
|
|
280
282
|
overflow_policy: new_config.pool_overflow_policy
|
|
281
283
|
)
|
|
282
|
-
@pool_manager.admission_controller = @pool_reaper if
|
|
284
|
+
@pool_manager.admission_controller = @pool_reaper if budget
|
|
283
285
|
@pool_reaper.start
|
|
284
286
|
end
|
|
285
287
|
|
|
@@ -370,6 +372,16 @@ module Apartment # rubocop:disable Metrics/ModuleLength
|
|
|
370
372
|
end
|
|
371
373
|
end
|
|
372
374
|
|
|
375
|
+
# Prepend the sequence-name patch whenever the PostgreSQL adapter loads
|
|
376
|
+
# (immediately, if it already has). Registered at gem load rather than in
|
|
377
|
+
# activate! because ActiveRecord memoizes Model.sequence_name at first touch,
|
|
378
|
+
# which can happen during boot before Apartment.activate! runs. No-op for apps
|
|
379
|
+
# that never load the PostgreSQL adapter, so MySQL/SQLite consumers never pull
|
|
380
|
+
# in pg. See the patch file for the full rationale.
|
|
381
|
+
ActiveSupport.on_load(:active_record_postgresqladapter) do
|
|
382
|
+
prepend(Apartment::Patches::PostgresqlSequenceName)
|
|
383
|
+
end
|
|
384
|
+
|
|
373
385
|
# Load Railtie when Rails is present (standard gem convention).
|
|
374
386
|
# Railtie is Zeitwerk-ignored — this explicit require is the only load path.
|
|
375
387
|
require_relative 'apartment/railtie' if defined?(Rails::Railtie)
|
|
@@ -37,9 +37,20 @@ Apartment.configure do |config|
|
|
|
37
37
|
|
|
38
38
|
# == Connection Pool =====================================================
|
|
39
39
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
40
|
+
# tenant_pool_size MUST be >= the peak number of threads/fibers in one process
|
|
41
|
+
# that can touch the SAME tenant at once (e.g. a Sidekiq role's concurrency for a
|
|
42
|
+
# same-tenant job fan-out); otherwise those threads block on connection checkout.
|
|
43
|
+
# It is a lazy ceiling — connections are created on demand and idle pools reaped.
|
|
44
|
+
# config.tenant_pool_size = 5
|
|
45
|
+
# config.pool_idle_timeout = 300
|
|
46
|
+
#
|
|
47
|
+
# Bound how many tenant pools exist per process:
|
|
48
|
+
# config.max_tenant_pools = nil
|
|
49
|
+
# ...or bound total tenant-pool CONNECTIONS (requires tenant_pool_size); the
|
|
50
|
+
# admission controller derives the pool budget as floor(value / tenant_pool_size):
|
|
51
|
+
# config.max_tenant_connections = nil
|
|
52
|
+
#
|
|
53
|
+
# config.max_total_connections = nil # DEPRECATED: alias of max_tenant_pools; removed in v5
|
|
43
54
|
|
|
44
55
|
# == Elevator (Request Tenant Detection) =================================
|
|
45
56
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ros-apartment
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.0.
|
|
4
|
+
version: 4.0.0.alpha8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Brunner
|
|
@@ -194,6 +194,7 @@ files:
|
|
|
194
194
|
- lib/apartment/migrator.rb
|
|
195
195
|
- lib/apartment/patches/connection_handling.rb
|
|
196
196
|
- lib/apartment/patches/live_tenant_propagation.rb
|
|
197
|
+
- lib/apartment/patches/postgresql_sequence_name.rb
|
|
197
198
|
- lib/apartment/pool_manager.rb
|
|
198
199
|
- lib/apartment/pool_observer.rb
|
|
199
200
|
- lib/apartment/pool_reaper.rb
|