feat(postgres): make embedded connection cap configurable

This commit is contained in:
gsxdsm
2026-07-18 10:00:49 -07:00
parent 2b455672c6
commit 2c17fa70ab
8 changed files with 55 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Let operators set the embedded PostgreSQL connection cap in Advanced Settings.
category: feature
dev: External PostgreSQL runtime pools now default to 3 connections; embedded PostgreSQL reads the global cap on restart.

View File

@@ -36,6 +36,12 @@ describe("settings defaults invariants", () => {
expect(isGlobalOnlySettingsKey("localNetworkDiscoveryEnabled")).toBe(true); expect(isGlobalOnlySettingsKey("localNetworkDiscoveryEnabled")).toBe(true);
}); });
it("keeps the embedded PostgreSQL connection cap global and high by default", () => {
expect(DEFAULT_GLOBAL_SETTINGS.embeddedPostgresMaxConnections).toBe(500);
expect(GLOBAL_SETTINGS_KEYS).toContain("embeddedPostgresMaxConnections");
expect(PROJECT_SETTINGS_KEYS).not.toContain("embeddedPostgresMaxConnections");
});
it("defaults dashboard keyboard shortcuts globally", () => { it("defaults dashboard keyboard shortcuts globally", () => {
expect(DEFAULT_GLOBAL_SETTINGS.dashboardKeyboardShortcuts).toEqual({ expect(DEFAULT_GLOBAL_SETTINGS.dashboardKeyboardShortcuts).toEqual({
quickChat: "Space", quickChat: "Space",

View File

@@ -43,7 +43,10 @@ const log = createLogger("postgres-connection");
* The embedded mode may use an even smaller pool. These can be tuned via * The embedded mode may use an even smaller pool. These can be tuned via
* environment variables if needed. * environment variables if needed.
*/ */
const DEFAULT_POOL_MAX = 10; // External databases frequently impose a low connection quota. Each Fusion
// store owns a runtime pool plus a migration session, so keep the default
// deliberately small; callers with a known capacity can still override it.
const DEFAULT_POOL_MAX = 3;
const DEFAULT_CONNECT_TIMEOUT_SECONDS = 10; const DEFAULT_CONNECT_TIMEOUT_SECONDS = 10;
const DEFAULT_IDLE_TIMEOUT_SECONDS = 20; const DEFAULT_IDLE_TIMEOUT_SECONDS = 20;

View File

@@ -308,7 +308,7 @@ async function isEmptyNonUtf8Cluster(db: PostgresJsDatabase<Record<string, never
} }
async function bootSchemaBackend( async function bootSchemaBackend(
options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax">, options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax" | "globalSettingsDir">,
bypassProjectIsolation = false, bypassProjectIsolation = false,
): Promise<SchemaBackendBootResult> { ): Promise<SchemaBackendBootResult> {
try { try {
@@ -326,7 +326,7 @@ async function bootSchemaBackend(
} }
async function bootSchemaBackendOnce( async function bootSchemaBackendOnce(
options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax">, options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax" | "globalSettingsDir">,
bypassProjectIsolation = false, bypassProjectIsolation = false,
): Promise<SchemaBackendBootResult> { ): Promise<SchemaBackendBootResult> {
const env = options.env ?? process.env; const env = options.env ?? process.env;
@@ -347,12 +347,23 @@ async function bootSchemaBackendOnce(
if (backend.mode === "embedded") { if (backend.mode === "embedded") {
const { EmbeddedPostgresLifecycle, defaultEmbeddedDataDir, DEFAULT_EMBEDDED_DATABASE } = const { EmbeddedPostgresLifecycle, defaultEmbeddedDataDir, DEFAULT_EMBEDDED_DATABASE } =
await import("./embedded-lifecycle.js"); await import("./embedded-lifecycle.js");
const { GlobalSettingsStore } = await import("../global-settings.js");
// Tests that boot an embedded backend intentionally do not have an
// operator global-settings directory. Production always reads the shared
// global preference before it starts the server.
const configuredMaxConnections = process.env.VITEST === "true" && !options.globalSettingsDir
? undefined
: (await new GlobalSettingsStore(options.globalSettingsDir).getSettings()).embeddedPostgresMaxConnections;
const maxConnections = Number.isInteger(configuredMaxConnections)
? Math.min(2_000, Math.max(32, configuredMaxConnections!))
: 500;
const dataDir = resolve(options.embeddedDataDir ?? defaultEmbeddedDataDir()); const dataDir = resolve(options.embeddedDataDir ?? defaultEmbeddedDataDir());
embeddedDataDir = dataDir; embeddedDataDir = dataDir;
log.log(`startup-factory: starting embedded PostgreSQL (data dir ${dataDir})`); log.log(`startup-factory: starting embedded PostgreSQL (data dir ${dataDir})`);
embeddedLifecycle = new EmbeddedPostgresLifecycle({ embeddedLifecycle = new EmbeddedPostgresLifecycle({
dataDir, dataDir,
database: DEFAULT_EMBEDDED_DATABASE, database: DEFAULT_EMBEDDED_DATABASE,
postgresFlags: ["-c", `max_connections=${maxConnections}`],
onLog: (message) => log.log(message), onLog: (message) => log.log(message),
onError: (error) => log.error(String(error)), onError: (error) => log.error(String(error)),
}); });

View File

@@ -72,6 +72,10 @@ type ProjectSettingsSchema = Omit<ProjectSettings, MovedProjectSettingsKey | Non
/** Default values for global (user-level) settings. */ /** Default values for global (user-level) settings. */
export const DEFAULT_GLOBAL_SETTINGS = { export const DEFAULT_GLOBAL_SETTINGS = {
// Embedded PostgreSQL is shared by all local Fusion projects and processes.
// Keep this well above the conservative external-pool budget while still
// bounded for a local machine.
embeddedPostgresMaxConnections: 500,
/* /*
FNXC:DashboardTheming 2026-07-03-00:00: FNXC:DashboardTheming 2026-07-03-00:00:
Fresh installs must follow the operating system theme until the user explicitly chooses Light, Dark, or System. Keep this global default aligned with dashboard and desktop pre-hydration fallbacks. Fresh installs must follow the operating system theme until the user explicitly chooses Light, Dark, or System. Keep this global default aligned with dashboard and desktop pre-hydration fallbacks.

View File

@@ -2440,6 +2440,8 @@ export interface BackupSettingsMigrationConflict {
} }
export interface GlobalSettings { export interface GlobalSettings {
/** Maximum PostgreSQL server connections for Fusion's embedded database. Applied on the next Fusion restart. */
embeddedPostgresMaxConnections?: number;
/** Theme mode preference: dark, light, or system (follows OS). Default: "dark". */ /** Theme mode preference: dark, light, or system (follows OS). Default: "dark". */
themeMode?: ThemeMode; themeMode?: ThemeMode;
/** Color theme preference for accent colors and styling. Default: "shadcn-ember"; "default" and "ocean" remain valid explicit legacy selections. */ /** Color theme preference for accent colors and styling. Default: "shadcn-ember"; "default" and "ocean" remain valid explicit legacy selections. */

View File

@@ -101,7 +101,7 @@ the list for the "Reset this menu" feature.
*/ */
export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = { export const GLOBAL_SECTION_KEYS: Record<string, ReadonlySet<string>> = {
/* FNXC:SettingsBackups 2026-07-16-14:35: shared PostgreSQL backup policy may only write through its global Database Backups section. */ /* FNXC:SettingsBackups 2026-07-16-14:35: shared PostgreSQL backup policy may only write through its global Database Backups section. */
"backups-global": new Set(["autoBackupEnabled", "autoBackupSchedule", "autoBackupRetention", "autoBackupDir"]), "backups-global": new Set(["autoBackupEnabled", "autoBackupSchedule", "autoBackupRetention", "autoBackupDir", "embeddedPostgresMaxConnections"]),
appearance: new Set([ appearance: new Set([
"themeMode", "themeMode",
"colorTheme", "colorTheme",

View File

@@ -44,6 +44,24 @@ export function DatabaseBackupsSection({ form, setForm, backupInfo, backupLoadin
</div>)} </div>)}
</div>} </div>}
<h4 className="settings-section-heading">{t("settings.backups.databaseBackups", "Database Backups")}</h4> <h4 className="settings-section-heading">{t("settings.backups.databaseBackups", "Database Backups")}</h4>
<details className="settings-advanced-disclosure">
<summary>{t("settings.database.advanced", "Advanced database settings")}</summary>
<SettingsNumberRow
descriptor={{
key: "embeddedPostgresMaxConnections",
label: t("settings.database.embeddedConnectionCap", "Embedded PostgreSQL connection cap"),
help: t("settings.database.embeddedConnectionCapHelp", "Maximum server connections for Fusion's embedded PostgreSQL. Applies after restarting Fusion. Range: 32–2,000. Default: 500. External PostgreSQL uses its provider's connection limit."),
scope: "global",
min: 32,
max: 2000,
}}
value={form.embeddedPostgresMaxConnections ?? 500}
onChange={(v) => setForm((f) => ({ ...f, embeddedPostgresMaxConnections: v ?? 500 }))}
error={form.embeddedPostgresMaxConnections !== undefined && (form.embeddedPostgresMaxConnections < 32 || form.embeddedPostgresMaxConnections > 2000)
? t("settings.database.embeddedConnectionCapError", "Enter a value between 32 and 2,000.")
: undefined}
/>
</details>
<SettingsToggleRow <SettingsToggleRow
descriptor={{ descriptor={{
key: "autoBackupEnabled", key: "autoBackupEnabled",