Class: Pgbus::Streams::PoolAutoscaler

Inherits:
Object
  • Object
show all
Defined in:
lib/pgbus/streams/pool_autoscaler.rb

Overview

Self-tuning decision logic for the dedicated streams DB pool (issue #323).

A PURE DECISION OBJECT — it owns no thread and no connection. Two callers drive it, both feeding it a live-headroom reading:

* the streamer's Listener, as a periodic maintenance check on its idle
LISTEN connection (see Maintenance below); and
* a publisher (Client#send_stream_message), throttled, on the job pool.

#evaluate reads the streams pool's busy ratio, decides, and resizes via the #325 hot-swap primitive.

Cadence is slow (streams_pool_autoscale_interval, default 5 min), so the interval itself is the debounce: it acts on each check with no multi-sample hysteresis and no cooldown. A sustained burst converges over a few checks (one grow step each); a swap's transient (a freshly-swapped connection_pool is lazy → reads busy ≈ 0) simply means "no shrink this check" and is gone by the next check minutes later.

Decision priority per check:

1. EMERGENCY SHRINK — DB free connections critically low → resize to
 baseline now (protect the DB; overrides the busy signal).
2. GROW — pool saturated AND a fair share of real headroom exists.
3. SHRINK — pool idle → step toward baseline.
4. HOLD.

SAFETY (proven in the #323 design; independent of cadence):

* No multi-process exhaustion: four stacked grow guards (GROW_RESERVE
gate, SAFETY peer inflation, STEP_MAX, per-process floor(free/2)).
* No grow↔emergency limit cycle: GROW_RESERVE (0.20·maxc) ≥ 4×
EMERGENCY_MARGIN (0.05·maxc) → a ~15% dead-zone.

Defined Under Namespace

Classes: Maintenance

Constant Summary collapse

GROW_THRESHOLD =

busy_ratio to grow

0.85
SHRINK_THRESHOLD =

busy_ratio to shrink

0.30
FAIR_FRACTION =

claim only ¼ of the computed fair share per grow

0.25
SAFETY =

inflate peer count → deflate fair share (undercount guard)

1.5
STEP_MAX =

hard cap on connections added per single grow

4
HEADROOM_SQL =

SQL a caller runs on a connection to gather headroom. Public so the Listener/publisher can issue it; $1 is the application_name LIKE pattern.

<<~SQL
  SELECT current_setting('max_connections')::int AS maxc,
         count(*) AS used,
         count(DISTINCT application_name)
           FILTER (WHERE application_name LIKE $1) AS peers
  FROM pg_stat_activity
SQL

Instance Method Summary collapse

Constructor Details

#initialize(client:, config:, logger: Pgbus.logger) ⇒ PoolAutoscaler

Returns a new instance of PoolAutoscaler.



51
52
53
54
55
56
# File 'lib/pgbus/streams/pool_autoscaler.rb', line 51

def initialize(client:, config:, logger: Pgbus.logger)
  @client = client
  @config = config
  @logger = logger
  @baseline = config.streams_pool_size
end

Instance Method Details

#evaluate(headroom, allow_shrink: true) ⇒ Object

One maintenance decision. headroom is used:, peers: from a caller's query, or nil if it failed (→ HOLD). Returns the action taken (:emergency_shrink / :grow / :shrink / :hold) — handy for tests + logging.

allow_shrink: gates the NORMAL idle shrink (priority 3). The streamer passes true (it sees the sustained idle picture across replay reads). A publisher passes false: it only ever reacts to its OWN publish pressure (grow), and leaves idle-shrink to the streamer/consumer. Emergency shrink is NEVER gated — a DB running out of connections must always be relieved, whoever notices first.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/pgbus/streams/pool_autoscaler.rb', line 68

def evaluate(headroom, allow_shrink: true)
  return :hold if headroom.nil?

  free = headroom[:maxc] - headroom[:used]

  # PRIORITY 1 — EMERGENCY SHRINK (keys off DB `free`, an external fact).
  return emergency_shrink(free, headroom[:maxc]) if free < emergency_margin(headroom[:maxc])

  size, busy_ratio = pool_busy
  return :hold if size.nil?

  if busy_ratio >= GROW_THRESHOLD
    maybe_grow(size, free, headroom)
  elsif allow_shrink && busy_ratio < SHRINK_THRESHOLD
    maybe_shrink(size)
  else
    :hold
  end
end