feat(ops): name 1536 MB as the supported memory floor

The getting-started requirements now say a host needs 3 GB of RAM, 4 GB
recommended, and drop the advice to lower APP_MEMORY_LIMIT to 1g on a
2 GB host. The compose and .env example comments say the same.

At boot the app reads its cgroup memory limit (v2, then v1) and logs one
line when it is below MEMORY_FLOOR_MB. The support report adds the
container limit and a note below the floor, and Admin > System status
shows "below the supported floor" next to the existing low-heap flag.
The app keeps running either way.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 19:36:06 +02:00
1 parent 9941db1549
commit 07f9615dba
20 files changed
+313 -26

No files matched your search

@@ -44,12 +44,19 @@ vi.mock("@/lib/jobs/worker-status", () => ({
})),
}));
const heapMock = vi.hoisted(() => ({ mb: 792 }));
const heapMock = vi.hoisted(() => ({
mb: 792,
container: 1536 as number | null,
}));
vi.mock("@/lib/support/heap-limit", async () => {
const actual = await vi.importActual<
typeof import("@/lib/support/heap-limit")
>("@/lib/support/heap-limit");
return { ...actual, heapLimitMb: () => heapMock.mb };
return {
...actual,
heapLimitMb: () => heapMock.mb,
containerMemoryLimitMb: () => heapMock.container,
};
});
const geoMock = vi.hoisted(() => ({
@@ -200,12 +207,40 @@ describe("GET /api/admin/status (integration health summary)", () => {
const low = (await (await GET()).json()) as {
data: { memory: { heapLimitMb: number; low: boolean } };
};
expect(low.data.memory).toEqual({ heapLimitMb: 524, low: true });
expect(low.data.memory).toMatchObject({ heapLimitMb: 524, low: true });
heapMock.mb = 792;
const enough = (await (await GET()).json()) as {
data: { memory: { heapLimitMb: number; low: boolean } };
};
expect(enough.data.memory).toEqual({ heapLimitMb: 792, low: false });
expect(enough.data.memory).toMatchObject({ heapLimitMb: 792, low: false });
});
it("flags a container memory limit below the supported floor", async () => {
type Memory = {
containerLimitMb: number | null;
floorMb: number;
belowFloor: boolean;
};
heapMock.container = 1024;
const below = (await (await GET()).json()) as { data: { memory: Memory } };
expect(below.data.memory).toMatchObject({
containerLimitMb: 1024,
floorMb: 1536,
belowFloor: true,
});
heapMock.container = 1536;
const atFloor = (await (await GET()).json()) as {
data: { memory: Memory };
};
expect(atFloor.data.memory.belowFloor).toBe(false);
// No limit, or none readable: nothing to flag.
heapMock.container = null;
const none = (await (await GET()).json()) as { data: { memory: Memory } };
expect(none.data.memory).toMatchObject({
containerLimitMb: null,
belowFloor: false,
});
heapMock.container = 1536;
});
it("reports named failing queues through failingJobs only", async () => {
+16 -2
View File
@@ -9,7 +9,13 @@ import {
readFailingQueues,
} from "@/lib/jobs/job-failures";
import { readFileSync } from "node:fs";
import { heapLimitMb, isHeapLimitLow } from "@/lib/support/heap-limit";
import {
containerMemoryLimitMb,
heapLimitMb,
isBelowMemoryFloor,
isHeapLimitLow,
MEMORY_FLOOR_MB,
} from "@/lib/support/heap-limit";
import {
geoLookupMode,
geoProviderChosen,
@@ -77,6 +83,8 @@ export const GET = apiHandler(async () => {
// The process's heap ceiling, and whether it is the 1 GB-container size
// that a long-used account can fill (`heap-limit.ts`).
const heapLimit = heapLimitMb();
// And the container's own limit against the supported floor.
const containerLimit = containerMemoryLimitMb();
return apiSuccess({
version: process.env.npm_package_version || "0.1.0",
@@ -85,7 +93,13 @@ export const GET = apiHandler(async () => {
buildTime: BUILD_TIME,
startTime: START_TIME,
database: "connected",
memory: { heapLimitMb: heapLimit, low: isHeapLimitLow(heapLimit) },
memory: {
heapLimitMb: heapLimit,
low: isHeapLimitLow(heapLimit),
containerLimitMb: containerLimit,
floorMb: MEMORY_FLOOR_MB,
belowFloor: isBelowMemoryFloor(containerLimit),
},
worker: {
running: workerStatus.running,
startedAt: workerStatus.startedAt,