feat(postgres): make embedded connection cap configurable
This commit is contained in:
7
.changeset/embedded-postgres-connection-cap.md
Normal file
7
.changeset/embedded-postgres-connection-cap.md
Normal 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.
|
||||
@@ -36,6 +36,12 @@ describe("settings defaults invariants", () => {
|
||||
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", () => {
|
||||
expect(DEFAULT_GLOBAL_SETTINGS.dashboardKeyboardShortcuts).toEqual({
|
||||
quickChat: "Space",
|
||||
|
||||
@@ -43,7 +43,10 @@ const log = createLogger("postgres-connection");
|
||||
* The embedded mode may use an even smaller pool. These can be tuned via
|
||||
* 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_IDLE_TIMEOUT_SECONDS = 20;
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ async function isEmptyNonUtf8Cluster(db: PostgresJsDatabase<Record<string, never
|
||||
}
|
||||
|
||||
async function bootSchemaBackend(
|
||||
options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax">,
|
||||
options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax" | "globalSettingsDir">,
|
||||
bypassProjectIsolation = false,
|
||||
): Promise<SchemaBackendBootResult> {
|
||||
try {
|
||||
@@ -326,7 +326,7 @@ async function bootSchemaBackend(
|
||||
}
|
||||
|
||||
async function bootSchemaBackendOnce(
|
||||
options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax">,
|
||||
options: Pick<CreateTaskStoreForBackendOptions, "env" | "backend" | "embeddedPgRequested" | "embeddedDataDir" | "poolMax" | "globalSettingsDir">,
|
||||
bypassProjectIsolation = false,
|
||||
): Promise<SchemaBackendBootResult> {
|
||||
const env = options.env ?? process.env;
|
||||
@@ -347,12 +347,23 @@ async function bootSchemaBackendOnce(
|
||||
if (backend.mode === "embedded") {
|
||||
const { EmbeddedPostgresLifecycle, defaultEmbeddedDataDir, DEFAULT_EMBEDDED_DATABASE } =
|
||||
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());
|
||||
embeddedDataDir = dataDir;
|
||||
log.log(`startup-factory: starting embedded PostgreSQL (data dir ${dataDir})`);
|
||||
embeddedLifecycle = new EmbeddedPostgresLifecycle({
|
||||
dataDir,
|
||||
database: DEFAULT_EMBEDDED_DATABASE,
|
||||
postgresFlags: ["-c", `max_connections=${maxConnections}`],
|
||||
onLog: (message) => log.log(message),
|
||||
onError: (error) => log.error(String(error)),
|
||||
});
|
||||
|
||||
@@ -72,6 +72,10 @@ type ProjectSettingsSchema = Omit<ProjectSettings, MovedProjectSettingsKey | Non
|
||||
|
||||
/** Default values for global (user-level) 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:
|
||||
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.
|
||||
|
||||
@@ -2440,6 +2440,8 @@ export interface BackupSettingsMigrationConflict {
|
||||
}
|
||||
|
||||
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". */
|
||||
themeMode?: ThemeMode;
|
||||
/** Color theme preference for accent colors and styling. Default: "shadcn-ember"; "default" and "ocean" remain valid explicit legacy selections. */
|
||||
|
||||
@@ -101,7 +101,7 @@ the list for the "Reset this menu" feature.
|
||||
*/
|
||||
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. */
|
||||
"backups-global": new Set(["autoBackupEnabled", "autoBackupSchedule", "autoBackupRetention", "autoBackupDir"]),
|
||||
"backups-global": new Set(["autoBackupEnabled", "autoBackupSchedule", "autoBackupRetention", "autoBackupDir", "embeddedPostgresMaxConnections"]),
|
||||
appearance: new Set([
|
||||
"themeMode",
|
||||
"colorTheme",
|
||||
|
||||
@@ -44,6 +44,24 @@ export function DatabaseBackupsSection({ form, setForm, backupInfo, backupLoadin
|
||||
</div>)}
|
||||
</div>}
|
||||
<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
|
||||
descriptor={{
|
||||
key: "autoBackupEnabled",
|
||||
|
||||
Reference in New Issue
Block a user