From 7c9f8c5b88e60f58a5b7c7ee2448528f5f89d97b Mon Sep 17 00:00:00 2001 From: merge-check <140916359+agentHits@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:58:31 -0400 Subject: [PATCH] perf(db): close panel database connections after a minute idle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что: - Пул postgres.js панели закрывает соединение, которое простояло без запросов 60 секунд (`idle_timeout: 60`). Раньше соединение жило 30–60 минут (`max_lifetime` по умолчанию), даже если запросов не было. - Тест `__test__/db/pool-options.test.ts` проверяет, что пул создаётся с этим параметром. Зачем: - Каждое соединение — отдельный процесс Postgres со своей памятью, 2–10 МБ. Панель почти всё время простаивает, а после любой пачки запросов пул держал до 20 таких процессов. - На изолированном стенде память процессов Postgres после двух минут простоя упала со 107 до 14 МБ, соединений осталось одно вместо двадцати. Риски: - Первый запрос после минуты простоя открывает соединение заново (TCP и вход по SCRAM, единицы миллисекунд) и заново готовит свои запросы. Healthcheck образа раз в 30 секунд держит одно соединение открытым. Проверки: - Команды и результаты: `vitest run __test__/db/pool-options.test.ts` проходит, на коде без правки падает; полный `vitest run`: 4294 прошли, 5 упали в `vector-validate.test.ts` (на Mac Colima не монтирует временную папку `docker run`, без правки так же); `tsc --noEmit` в `apps/dokploy` и `packages/server` без ошибок; `biome check` без замечаний. Стенд: нативные arm64-образы `AgentHits-Dev` в Docker-in-Docker, сценарий из входа, страниц, нагрузки на API, деплоев, логов и терминалов, по 2–3 прогона на вариант вперемешку. - Ограничения: стенд на Mac, а не на сервере; по скорости разница между прогонами одного варианта (±15%) больше, чем между вариантами. What: - The panel's postgres.js pool closes a connection after 60 seconds without queries (`idle_timeout: 60`). Before, a connection lived 30–60 minutes (the default `max_lifetime`) even with no queries. - The test `__test__/db/pool-options.test.ts` checks that the pool is created with this option. Why: - Every connection is a separate Postgres process with its own memory, 2–10 MB. The panel is idle most of the time, yet after any burst of queries the pool kept up to 20 such processes. - On the isolated bench, Postgres process memory after two idle minutes fell from 107 to 14 MB, with one connection left instead of twenty. Risks: - The first query after a minute idle opens a connection again (TCP and SCRAM login, a few milliseconds) and prepares its statements again. The image healthcheck every 30 seconds keeps one connection open. Checks: - Commands and results: `vitest run __test__/db/pool-options.test.ts` passes and fails without the change; full `vitest run`: 4294 passed, 5 failed in `vector-validate.test.ts` (Colima on the Mac does not mount the `docker run` temp folder, same without the change); `tsc --noEmit` in `apps/dokploy` and `packages/server` clean; `biome check` clean. Bench: native arm64 images of `AgentHits-Dev` in Docker-in-Docker, a scenario of sign-in, pages, API load, deploys, logs and terminals, 2–3 interleaved runs per variant. - Limitations: the bench runs on a Mac, not the server; for speed, runs of one variant differ more (±15%) than variants do. Co-Authored-By: Claude Opus 5.5 --- apps/dokploy/__test__/db/pool-options.test.ts | 20 +++++++++++++++++++ packages/server/src/db/index.ts | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 apps/dokploy/__test__/db/pool-options.test.ts diff --git a/apps/dokploy/__test__/db/pool-options.test.ts b/apps/dokploy/__test__/db/pool-options.test.ts new file mode 100644 index 00000000000..6271e64968a --- /dev/null +++ b/apps/dokploy/__test__/db/pool-options.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it, vi } from "vitest"; + +const postgres = vi.hoisted(() => vi.fn(() => ({}))); + +vi.mock("postgres", () => ({ default: postgres })); +vi.mock("drizzle-orm/postgres-js", () => ({ drizzle: vi.fn(() => ({})) })); +vi.unmock("@dokploy/server/db"); + +describe("@dokploy/server/db", () => { + it("closes connections that sit idle for a minute", async () => { + delete (globalThis as { db?: unknown }).db; + await import("@dokploy/server/db"); + + expect(postgres).toHaveBeenCalledTimes(1); + expect(postgres).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ idle_timeout: 60 }), + ); + }); +}); diff --git a/packages/server/src/db/index.ts b/packages/server/src/db/index.ts index 724cace60ea..e666f66a540 100644 --- a/packages/server/src/db/index.ts +++ b/packages/server/src/db/index.ts @@ -17,7 +17,9 @@ const globalForDb = globalThis as unknown as { }; if (!globalForDb.db) { - globalForDb.db = drizzle(postgres(dbUrl), { + // postgres.js keeps every connection it opens for 30-60 minutes. The panel + // is idle most of the time, and each idle backend holds memory in Postgres. + globalForDb.db = drizzle(postgres(dbUrl, { idle_timeout: 60 }), { schema, }); }