fix(db): PostgreSQL pool default fits a stock server

This commit is contained in:
maziggy
2026-10-02 15:39:07 +02:00
parent 44718b2d2f
commit 56e067753a
5 changed files with 31 additions and 7 deletions
+1
View File
@@ -30,6 +30,7 @@ All notable changes to Bambuddy will be documented in this file.
- **Leftover Web Push code and the unused `pywebpush` dependency are gone (#3171)** — Browser push notifications were removed earlier, but the service worker still carried their push handlers and `pywebpush` was still installed, pulling in `py-vapid` and `http_ece`. Both are removed. `aiohttp` stays, now listed as the direct dependency it is for external cameras, with the same version floor.
### Fixed
- **PostgreSQL installs no longer warn at every start that the connection pool may exceed the server** — The built-in pool allowed up to 100 connections (`pool_size` 20 + `max_overflow` 80), but a stock PostgreSQL allows 97 (`max_connections` 100 minus 3 reserved for superusers). So every install that hadn't set `DB_POOL_SIZE` / `DB_MAX_OVERFLOW` logged `DB pool may exceed what PostgreSQL allows` at startup, and under heavy load could hit `TooManyConnectionsError`. The default `max_overflow` is now 60, an 80-connection ceiling that fits. Installs that set their own values are unaffected, and large farms can still raise both, along with the server's `max_connections`.
- **AMS slots that lost their K-profile after a printer restart stayed on the default K, and queued jobs printed with it (#3219)** — A printer that is power-cycled can come back with every slot's K-profile selection cleared (`cali_idx` -1) while the spools, tags and remaining amounts are unchanged. Bambuddy only looked at K-profiles when one of those changed, so it never noticed, and the next queued jobs started on the default K without a warning. Now, while the printer is idle, a loaded slot whose selection is gone gets its spool's stored K-profile back, for built-in and Spoolman inventory and for the external spool. The queue also checks the slots a job uses right before sending it. Only a lost selection is restored: a different profile picked in Bambu Studio is left alone, and so is **Default** picked in Bambuddy's Configure Slot dialog. A slot is retried at most three times, 30 seconds apart; if the profile still doesn't stick (for example because it was deleted on the printer), the log says so and Bambuddy stops until the slot changes. On dual-nozzle printers a slot whose nozzle isn't known yet is skipped rather than guessed.
- **A built-in camera could show the same old picture for hours while everything else updated (#3218, reported by @adamspicedev; also #3189, reported by @Sawtaytoes)** — After a camera connection dropped, the `ffmpeg` process that converts it for the browser could stay running with no connection to the printer, repeating its last frame about 30 times a second. On a P2S this went on for two hours until the process was killed by hand. Bambuddy treated every repeated frame as a sign of life, so it never reconnected, and the camera status said the stream was healthy. Now only a frame that differs from the previous one counts as new. After 20 seconds of identical frames, Bambuddy restarts `ffmpeg` and reconnects, and open viewers, overlays and the Cam Wall stay connected. A working camera practically never sends two identical frames, but a completely dark chamber can. When the picture after a restart is exactly the same as before, Bambuddy treats it as a still picture and only checks again every 5 minutes, until the picture changes.
- **Orca Cloud filaments assigned to an AMS slot showed up in OrcaSlicer as Generic (#3216, reported by @mrnoisytiger)** — OrcaSlicer's **Sync filaments** finds a slot's preset by the filament ID stored in the slot and nothing else. OrcaSlicer's own Device tab writes the profile's filament ID there (`Pfc74047` in the report). For the same profile, Bambuddy wrote the generic ID for the material (`GFL99`), so Sync filaments warned that the filament may not be compatible and used Generic PLA. Bambuddy looked the ID up in the browser, and when that came back empty it used the generic ID without saying so. Nothing in the log showed why. The lookup now happens on the server when the slot is configured, and its result is logged. A profile that only stores its changes against another one now takes the filament ID from that parent: either another of your Orca Cloud profiles, or the Bambu filament it was copied from (for example `GFA00` for Bambu PLA Basic). When no ID can be found, the slot still gets the generic ID, not the filament that was in the slot before, and the dialog shows a warning saying the slicer will see Generic. Also fixed: after a slot was changed from OrcaSlicer's Device tab or the printer's screen, the slot card and the Configure dialog kept showing the preset Bambuddy had last set. Bambuddy now saves the filament ID along with the slot's preset and stops showing that preset once the printer reports a different filament. Slots configured before this update are only checked this way for Bambu's own filaments. Also fixed: re-configuring a slot for a different filament could leave the slot on its old filament. The Configure dialog kept offering the slot's active K-profile, which belongs to the old filament, and selected it automatically. Bambuddy then switched the slot to that K-profile's filament, so OrcaSlicer kept showing the old one until the slot was reset first. The slot's active K-profile is now only offered while the dialog is still on the slot's current filament.
+1 -1
View File
@@ -75,7 +75,7 @@ class Settings(BaseSettings):
database_url: str = _external_db_url or f"sqlite+aiosqlite:///{_db_path}"
# Database connection pool sizing. ``None`` = use the built-in, dialect-aware
# default (PostgreSQL: pool_size 20 + max_overflow 80; SQLite: 20 + 200).
# default (PostgreSQL: pool_size 20 + max_overflow 60; SQLite: 20 + 200).
# Large PostgreSQL printer farms can raise these via the DB_POOL_SIZE /
# DB_MAX_OVERFLOW / DB_POOL_TIMEOUT / DB_POOL_RECYCLE env vars (issue #2572).
# Make sure PostgreSQL ``max_connections`` comfortably exceeds
+5 -3
View File
@@ -37,10 +37,12 @@ def _resolve_pool_kwargs() -> dict:
"""Build the pool kwargs for ``create_async_engine`` (issue #2572).
Dialect-aware defaults, each overridable via env (``DB_POOL_SIZE`` etc.):
- PostgreSQL: pool_size 20 + max_overflow 80, ``pool_pre_ping`` (recover
- PostgreSQL: pool_size 20 + max_overflow 60, ``pool_pre_ping`` (recover
server-dropped connections instead of erroring the request) and
``pool_recycle`` 1800s. The old hard-coded 10 + 20 exhausted on large
farms while printer callbacks held connections.
farms while printer callbacks held connections. The 80-connection
ceiling fits a stock server (max_connections 100, 3 reserved for
superusers); 20 + 80 did not, and tripped the startup pool check.
- SQLite: pool_size 20 + max_overflow 200 (unchanged); no pre-ping /
recycle — the connection is a local file, not a server socket.
"""
@@ -50,7 +52,7 @@ def _resolve_pool_kwargs() -> dict:
kwargs = {"pool_size": pool_size, "max_overflow": max_overflow}
else:
pool_size = settings.db_pool_size if settings.db_pool_size is not None else 20
max_overflow = settings.db_max_overflow if settings.db_max_overflow is not None else 80
max_overflow = settings.db_max_overflow if settings.db_max_overflow is not None else 60
kwargs = {
"pool_size": pool_size,
"max_overflow": max_overflow,
@@ -27,7 +27,7 @@ class TestPoolConfiguration:
assert "pool_recycle" not in kwargs
def test_postgres_defaults_raise_the_old_limits(self, monkeypatch):
"""Postgres default is now 20 + 80 (was 10 + 20) with pre-ping + recycle."""
"""Postgres default is 20 + 60 (was 10 + 20) with pre-ping + recycle."""
from backend.app.core import database
for attr in ("db_pool_size", "db_max_overflow", "db_pool_timeout", "db_pool_recycle"):
@@ -36,7 +36,7 @@ class TestPoolConfiguration:
kwargs = database._resolve_pool_kwargs()
assert kwargs["pool_size"] == 20
assert kwargs["max_overflow"] == 80
assert kwargs["max_overflow"] == 60
assert kwargs["pool_pre_ping"] is True
assert kwargs["pool_recycle"] == 1800
+22 -1
View File
@@ -63,7 +63,7 @@ async def _run_check(*, pool_size, max_overflow, max_conn, reserved, in_use=0, s
@pytest.mark.asyncio
@pytest.mark.unit
async def test_warns_when_the_ceiling_exceeds_what_the_server_allows(caplog):
"""Bambuddy's own PostgreSQL default against a stock server: 100 vs 100-3."""
"""A 100-connection ceiling against a stock server: 100 vs 100-3."""
with caplog.at_level(logging.WARNING, logger="backend.app.core.database"):
await _run_check(pool_size=20, max_overflow=80, max_conn=100, reserved=3)
@@ -74,6 +74,27 @@ async def test_warns_when_the_ceiling_exceeds_what_the_server_allows(caplog):
assert expected in msg, f"warning omits {expected!r}"
@pytest.mark.asyncio
@pytest.mark.unit
async def test_the_postgres_default_fits_a_stock_server(caplog, monkeypatch):
"""Without DB_POOL_* set, a stock server (100, 3 reserved) must not warn.
The default was 20 + 80, one over the 97 a stock server leaves, so every
PostgreSQL install that didn't size its pool logged this at each start.
"""
from backend.app.core import database
for attr in ("db_pool_size", "db_max_overflow", "db_pool_timeout", "db_pool_recycle"):
monkeypatch.setattr(database.settings, attr, None, raising=False)
monkeypatch.setattr(database, "is_sqlite", lambda: False)
kwargs = database._resolve_pool_kwargs()
with caplog.at_level(logging.WARNING, logger="backend.app.core.database"):
await _run_check(pool_size=kwargs["pool_size"], max_overflow=kwargs["max_overflow"], max_conn=100, reserved=3)
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
@pytest.mark.asyncio
@pytest.mark.unit
async def test_silent_when_the_pool_fits(caplog):