mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
* Reduce server memory usage and add container memory best practices Profiling a full-service boot showed ~300MB RSS per service process, much of it client-only dependencies pulled into the server module graph, and no heap limits applied in containers: - Import date-fns locales individually rather than via the locale index, which loaded all ~90 locales into every process - Load @sentry/react dynamically in insertFiles so the browser Sentry SDK stays out of the server-side editor graph - Replace class-validator isHexColor with the existing validateColorHex in the Highlight mark - Extract icon names into a standalone IconNames module so server-side schema validation no longer imports Font Awesome icon packs, with a test to keep it in sync with IconLibrary - Require S3Storage lazily so the AWS SDK and native CRT binding are only loaded when S3 file storage is configured - Default each forked service process to a V8 heap limit derived from the cgroup memory constraint, preventing several processes from together committing more memory than the container allows - Cap glibc malloc arenas in the Docker image to reduce resident memory fragmentation in multi-threaded processes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YMX5ReGHyxrR4ZE5ZbaXa * Require both file storage backends lazily for consistency Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YMX5ReGHyxrR4ZE5ZbaXa * Load AWS SDK via dynamic imports compatible with test environment Requiring the storage TypeScript modules lazily broke under Vitest, which cannot resolve require() of source files and does not apply vi.mock to bare requires. Move the laziness into S3Storage instead: the module is imported statically, and the AWS SDK packages are loaded with dynamic imports on first use, including deferred creation of the S3 client. Also address review feedback: handle rejection of the dynamic Sentry import, and warn when the minimum per-process heap limit exceeds the memory budget of the container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YMX5ReGHyxrR4ZE5ZbaXa * Consolidate S3 SDK loading into a single lazy accessor The commands are loaded together with the client in getS3() rather than as separate dynamic imports per method. The global S3Client test mock is now a class, as a mock function is not constructable when reached through a dynamic import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YMX5ReGHyxrR4ZE5ZbaXa --------- Co-authored-by: Claude <noreply@anthropic.com>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import { vi } from "vitest";
|
|
import { Week, Day } from "@shared/utils/time";
|
|
import Logger from "@server/logging/Logger";
|
|
import BaseStorage from "./BaseStorage";
|
|
import S3Storage from "./S3Storage";
|
|
|
|
describe("S3Storage", () => {
|
|
describe("getFileStream", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns null and does not report an error when the object is missing (masked 403)", async () => {
|
|
// S3 returns AccessDenied for s3:ListBucket instead of 404 when the IAM
|
|
// identity lacks ListBucket permission and the key does not exist.
|
|
const error = Object.assign(
|
|
new Error(
|
|
"User: arn:aws:iam::123:user/attachments is not authorized to perform: s3:ListBucket on resource"
|
|
),
|
|
{
|
|
name: "AccessDenied",
|
|
$metadata: { httpStatusCode: 403 },
|
|
}
|
|
);
|
|
const storage = new S3Storage();
|
|
const { client } = await Reflect.get(storage, "getS3").call(storage);
|
|
vi.spyOn(client, "send").mockRejectedValue(error);
|
|
const errorSpy = vi.spyOn(Logger, "error");
|
|
const infoSpy = vi.spyOn(Logger, "info");
|
|
|
|
const stream = await storage.getFileStream("missing/key");
|
|
|
|
expect(stream).toBeNull();
|
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
expect(infoSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports a real error for unexpected failures", async () => {
|
|
const error = Object.assign(new Error("boom"), {
|
|
name: "InternalError",
|
|
$metadata: { httpStatusCode: 500 },
|
|
});
|
|
const storage = new S3Storage();
|
|
const { client } = await Reflect.get(storage, "getS3").call(storage);
|
|
vi.spyOn(client, "send").mockRejectedValue(error);
|
|
const errorSpy = vi.spyOn(Logger, "error");
|
|
|
|
const stream = await storage.getFileStream("some/key");
|
|
|
|
expect(stream).toBeNull();
|
|
expect(errorSpy).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("getSignedUrl expiration limits", () => {
|
|
it("should define maximum expiration as 7 days for AWS S3 Signature V4", () => {
|
|
// AWS S3 Signature V4 presigned URLs have a maximum expiration of 7 days
|
|
const maxExpiration = Week.seconds;
|
|
|
|
// Verify our constant matches AWS limit
|
|
expect(BaseStorage.maxSignedUrlExpires).toBe(maxExpiration);
|
|
expect(BaseStorage.maxSignedUrlExpires).toBe(604800); // 7 days in seconds
|
|
});
|
|
|
|
it("should have Week.seconds equal to 7 days", () => {
|
|
expect(Week.seconds).toBe(7 * 24 * 60 * 60);
|
|
expect(Week.seconds).toBe(604800);
|
|
});
|
|
|
|
it("should ensure 30 days exceeds the limit", () => {
|
|
const thirtyDays = 30 * Day.seconds;
|
|
expect(thirtyDays).toBeGreaterThan(BaseStorage.maxSignedUrlExpires);
|
|
expect(thirtyDays).toBe(2592000); // 30 days in seconds
|
|
});
|
|
|
|
it("should ensure 4 days is within the limit", () => {
|
|
const fourDays = 4 * Day.seconds;
|
|
expect(fourDays).toBeLessThan(BaseStorage.maxSignedUrlExpires);
|
|
expect(fourDays).toBe(345600); // 4 days in seconds
|
|
});
|
|
|
|
it("should clamp values that exceed the limit", () => {
|
|
const thirtyDays = 30 * Day.seconds;
|
|
const clampedValue = Math.min(
|
|
thirtyDays,
|
|
BaseStorage.maxSignedUrlExpires
|
|
);
|
|
|
|
expect(clampedValue).toBe(BaseStorage.maxSignedUrlExpires);
|
|
expect(clampedValue).toBe(Week.seconds);
|
|
});
|
|
});
|
|
});
|