Skip to content

feat: configurable pool size (poolMaxSize setting) - #63

Open
aesslinger wants to merge 1 commit into
mainfrom
61-configurable-pool-size
Open

feat: configurable pool size (poolMaxSize setting)#63
aesslinger wants to merge 1 commit into
mainfrom
61-configurable-pool-size

Conversation

@aesslinger

Copy link
Copy Markdown
Collaborator

Summary

Ports the built-in postgres driver's configurable connection-pool size (tabularis#681) to the plugin, and closes a latent parity gap in the same code path. Closes #61.

The parity gap this fixes

build_pool previously never set max_size, so deadpool fell back to PoolConfig::default() = logical_cores × 2 (up to ~32 on a 16-thread machine) — up to ~3× more backend connections per target than the built-in's pinned 10. The 82-test parity suite compares query results, not connection counts, so it never caught this. For the pgBouncer use case this setting exists for (a small client pool), the plugin was worse out of the box.

What changed

  • .tabularium — declare a poolMaxSize number setting (default 10), matching the host's PluginSettingDefinition serde contract (key/label/type/default/description).
  • src/settings.rs (new) — port postgres_pool_max_size_from_value verbatim from the built-in's pool_manager.rs (u64/i64/string parse, zero/invalid → default 10, cap 64) into a process-global Mutex<usize>, default-initialized to 10 so pools are correctly sized even if initialize never arrives.
  • src/handlers/connection.rsinitialize no longer discards params: reads params["settings"]["poolMaxSize"] and stores it. Never panics (the host silently ignores initialize errors).
  • src/client.rsbuild_pool applies .max_size(settings::pool_max_size()) in both TLS and non-TLS branches, restoring parity with the built-in's default of 10.

The one intentional adaptation (not a gap)

The built-in reads the setting from the host config cache on every pool build (get_cached_config().plugins.get("postgres")...). An external plugin has no access to that cache — it receives settings only via the initialize RPC — so the value is captured at initialize into a process-global. This is the exact adaptation the issue author prescribed (see the tabularis#681 comment and #61's plan). Mutex (most-recent-initialize-wins) mirrors the built-in's read-current-config intent as closely as an external plugin can; a corrected re-init replaces the value, matching a config-cache re-read.

Feature-parity verification against tabularis#681

Compared the current full PR #681 diff (1 commit, CLEAN), review, and all 15 changed files against this work:

Parity point Builtin Plugin Match
Parser function postgres_pool_max_size_from_value pool_max_size_from_value ✅ byte-for-byte identical
Default 10 10
Cap 64 64
Applied as .max_size(postgres_pool_max_size()) .max_size(settings::pool_max_size())
Manifest fields key/label/type/default/description same 5 fields ✅ identical values
Test cases 4 4 verbatim ports + 12 more ✅ superset

Files with no plugin equivalent (correctly N/A): useDrivers.ts (external plugins have no TS fallback — manifest is source of truth) and the 11 i18n locale files (the host resolves labels under builtin.postgres.poolMaxSize, but this plugin's id is postgresql, so those keys never match and labels fall back to the manifest's English — the issue itself flags this as non-blocking).

Defense in depth

  • 64 hard cap — the single bound on pool size; no matter what arrives via initialize (10_000, a poisoned payload, a re-init), pool_max_size() can never exceed 64. Clamp happens before storage.
  • Zero → 10, never zero — a poolMaxSize: 0 falls back to 10, so a pgBouncer user who fat-fingers 0 gets a working 10-connection pool, not a dead 0-connection pool.
  • Never panics — the host silently ignores initialize errors, so a panic would kill the handshake. Every parse path is infallible: null/bool/object/array/negative/garbage-string all → default.
  • Graceful degradation — if initialize never arrives or is dropped, the global stays at default 10.

Verification

  • Build: clean
  • Clippy: -D warnings clean (resolved an await_holding_lock lint via tokio::sync::Mutex for the test lock)
  • fmt: clean
  • Unit tests: 133/133 (114 original + 16 settings + 3 build_pool), deterministic across 8 consecutive runs
  • Live DB (podman pg-tabularis-test): 8/8 — pool connects, startup scripts run, SSL mode resolves, queries round-trip with the new max_size wiring in place
  • Manifest: valid JSON

Versioning

feature: type → minor version impact per the README's type→bump table. Labeled prerelease:beta (current channel).

Closes #61.

Port the built-in postgres driver's configurable connection-pool size
(tabularis#681) to the plugin, and close a latent parity gap in the same
code path the 82-test suite doesn't cover (it compares query results,
not connection counts).

Previously build_pool never set max_size, so deadpool fell back to
PoolConfig::default() = logical_cores × 2 (up to ~32 on a 16-thread
machine) — up to ~3× more backend connections per target than the
built-in's pinned 10, the wrong default for the pgBouncer use case
this setting exists for.

Changes:
- .tabularium: declare a poolMaxSize number setting (default 10), matching
  the host's PluginSettingDefinition serde contract
- src/settings.rs (new): port postgres_pool_max_size_from_value verbatim
  from pool_manager.rs (u64/i64/string parse, zero/invalid -> default 10,
  cap 64) into a process-global Mutex<usize>; default initialized to 10 so
  pools are correctly sized even if initialize never arrives
- src/handlers/connection.rs: initialize no longer discards params — reads
  params['settings']['poolMaxSize'] and stores it; never panics (host
  silently ignores initialize errors)
- src/client.rs: build_pool applies .max_size(settings::pool_max_size())
  in both TLS and non-TLS branches, restoring parity with the built-in's
  default of 10

The one intentional adaptation: the built-in reads the setting from the
host config cache on each pool build; an external plugin has no access
to that cache, so the value is captured at the initialize RPC. This is
the adaptation the issue author prescribed (tabularis#681 comment).
Mutex (most-recent-initialize-wins) mirrors the built-in's read-current-
config intent as closely as an external plugin can.

Defense in depth: the 64 cap is the single bound on pool size regardless
of what arrives via initialize; zero falls back to 10 (never a dead
0-connection pool); every parse path is infallible (null/bool/object/
array/negative/garbage-string all -> default).

Tests: 16 settings tests (4 builtin parity cases ported verbatim + edge
cases + initialize-handshake path) and 3 build_pool tests asserting
pool.status().max_size directly (default 10 not cpu×2, custom honored,
oversized clamped) — the connection-count coverage the parity suite lacks.
Shared tokio::sync::Mutex serializes all tests touching the global.

Verified: 133/133 unit tests (deterministic across 8 runs), clippy -D
warnings clean, fmt clean, 8/8 live_db against podman pg-tabularis-test.

Closes #61
@aesslinger aesslinger self-assigned this Sep 3, 2026
@aesslinger aesslinger added the prerelease:beta Version suggestion targets a beta prerelease label Sep 3, 2026
@aesslinger aesslinger changed the title feature: configurable pool size (poolMaxSize setting) feat: configurable pool size (poolMaxSize setting) Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Version suggestion

Based on this PR's title (feat) and the prerelease:beta label:

Current 1.0.0-beta.9
Suggested next tag v1.0.0-beta.10

This is informational only — no tag or release is created automatically yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease:beta Version suggestion targets a beta prerelease

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configurable pool size (poolMaxSize setting) — port tabularis#681 to the plugin

1 participant