From 4becc7bd83125880ec001a84a5d115e2b3d551e2 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 03:33:16 +0100 Subject: [PATCH] Forge database pool caps are config values and a change recreates the forge (issue 1966, recovery) The caps were literals in server.env and the container named no restart-on, so a changed env-file never reached the running forge. They are now three gitea config values placed with ${config:...}, and the server container restarts on server-env. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- modules/gitea/files/server.env | 6 +++--- modules/gitea/module.json | 23 +++++++++++++++++++++++ modules/gitea/test/pool-cap.test.ts | 19 ++++++++++++++++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/modules/gitea/files/server.env b/modules/gitea/files/server.env index 59c2a9686..164b0c40b 100644 --- a/modules/gitea/files/server.env +++ b/modules/gitea/files/server.env @@ -7,6 +7,6 @@ GITEA__database__PASSWD=${secret:postgres-database} GITEA__webhook__ALLOWED_HOST_LIST=172.17.0.1 # Issue 1966: cap the forge's own pool; the store's 100 connections are shared by every module on this machine. # SSH git operations go through the forge's private API, so this pool carries them. -GITEA__database__MAX_OPEN_CONNS=40 -GITEA__database__MAX_IDLE_CONNS=10 -GITEA__database__CONN_MAX_LIFETIME=5m +GITEA__database__MAX_OPEN_CONNS=${config:database-max-open-connections} +GITEA__database__MAX_IDLE_CONNS=${config:database-max-idle-connections} +GITEA__database__CONN_MAX_LIFETIME=${config:database-connection-lifetime} diff --git a/modules/gitea/module.json b/modules/gitea/module.json index ba21ac17c..7d803c79c 100644 --- a/modules/gitea/module.json +++ b/modules/gitea/module.json @@ -187,6 +187,26 @@ ] }, "config": { + "database-max-open-connections": { + "type": "number", + "description": "The most connections the forge's own database pool holds open to the store. The store's 100 connections are shared by every module on its machine, and the forge's git operations over ssh go through the forge's private API, which this pool carries: 40 leaves room for the others.", + "trusted": false, + "minimum": 1, + "default": 40 + }, + "database-max-idle-connections": { + "type": "number", + "description": "The connections the forge's database pool keeps idle, ready for the next request. They count against the store's 100 connections, shared on its machine, so keep this well under the open-connection cap.", + "trusted": false, + "minimum": 0, + "default": 10 + }, + "database-connection-lifetime": { + "type": "string", + "description": "How long the forge's database pool keeps one connection before it closes and reopens it, as a duration such as 5m. A shorter lifetime gives back connections of the store's 100 connections, shared on its machine, sooner.", + "trusted": false, + "default": "5m" + }, "url": { "type": "string", "description": "The address the module's tools reach the forge at, as a URL; unset, the code falls back to its environment, else this machine's gitea on its port.", @@ -286,6 +306,9 @@ { "id": "server", "type": "container", + "restart-on": [ + "server-env" + ], "health": { "kind": "http", "endpoint": "web", diff --git a/modules/gitea/test/pool-cap.test.ts b/modules/gitea/test/pool-cap.test.ts index fc19701c5..0634dabed 100644 --- a/modules/gitea/test/pool-cap.test.ts +++ b/modules/gitea/test/pool-cap.test.ts @@ -3,11 +3,24 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; // The forge's own database pool is capped (issue 1966): the store's connections are shared by every module on the machine. +// The caps are config values placed into the env-file, and a changed env-file recreates the container. const env = readFileSync(new URL("../files/server.env", import.meta.url), "utf8"); +const manifest = JSON.parse(readFileSync(new URL("../module.json", import.meta.url), "utf8")); -test("the forge caps its database pool", () => { +test("the forge's pool caps are config values, placed in server.env, and a change recreates the container", () => { const lines = env.split("\n"); - for (const line of ["GITEA__database__MAX_OPEN_CONNS=40", "GITEA__database__MAX_IDLE_CONNS=10", "GITEA__database__CONN_MAX_LIFETIME=5m"]) { - assert.ok(lines.includes(line), `server.env lacks ${line}`); + const placed: Record = { + GITEA__database__MAX_OPEN_CONNS: ["database-max-open-connections", 40], + GITEA__database__MAX_IDLE_CONNS: ["database-max-idle-connections", 10], + GITEA__database__CONN_MAX_LIFETIME: ["database-connection-lifetime", "5m"], + }; + for (const [name, [key, def]] of Object.entries(placed)) { + assert.ok(lines.includes(`${name}=\${config:${key}}`), `server.env does not place ${key} at ${name}`); + assert.equal(manifest.config[key].default, def); + assert.equal(manifest.config[key].trusted, false); + assert.ok(manifest.config[key].description.includes("100 connections")); } + assert.equal(manifest.config["database-max-open-connections"].minimum, 1); + const server = manifest.resources.find((r: { id: string }) => r.id === "server"); + assert.deepEqual(server["restart-on"], ["server-env"]); });