mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 06:40:50 +00:00
release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0 * test: stabilize water capture re-entry
This commit is contained in:
1 parent
b01822be48
commit
62eb431d66
372 files changed
+25178
-8561
No files matched your search
@@ -20,6 +20,9 @@ vi.mock("@/lib/db", () => ({
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
DOCUMENT_DEFAULT_MAX_FILE_BYTES,
|
||||
acquireDocumentUploadSlot,
|
||||
DOCUMENT_UPLOAD_GLOBAL_CONCURRENCY,
|
||||
DOCUMENT_UPLOAD_PER_USER_CONCURRENCY,
|
||||
DOCUMENT_DEFAULT_QUOTA_BYTES,
|
||||
DOCUMENT_HARD_MAX_FILE_BYTES,
|
||||
detectDocumentType,
|
||||
@@ -276,6 +279,36 @@ describe("servingClassFor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("document upload concurrency slots", () => {
|
||||
it("bounds concurrent uploads per user and releases exactly once", () => {
|
||||
const releases = Array.from(
|
||||
{ length: DOCUMENT_UPLOAD_PER_USER_CONCURRENCY },
|
||||
() => acquireDocumentUploadSlot("slot-user"),
|
||||
);
|
||||
expect(releases.every(Boolean)).toBe(true);
|
||||
expect(acquireDocumentUploadSlot("slot-user")).toBeNull();
|
||||
|
||||
releases[0]!();
|
||||
releases[0]!();
|
||||
const replacement = acquireDocumentUploadSlot("slot-user");
|
||||
expect(replacement).not.toBeNull();
|
||||
|
||||
replacement!();
|
||||
for (const release of releases.slice(1)) release!();
|
||||
});
|
||||
|
||||
it("bounds total concurrent uploads across different users", () => {
|
||||
const releases = Array.from(
|
||||
{ length: DOCUMENT_UPLOAD_GLOBAL_CONCURRENCY },
|
||||
(_, index) => acquireDocumentUploadSlot(`global-slot-user-${index}`),
|
||||
);
|
||||
expect(releases.every(Boolean)).toBe(true);
|
||||
expect(acquireDocumentUploadSlot("global-slot-overflow")).toBeNull();
|
||||
|
||||
for (const release of releases) release!();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveDocumentLimits", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -47,6 +47,43 @@ export const DOCUMENT_HARD_MAX_FILE_BYTES = 104_857_600;
|
||||
/** Default per-user quota: 1 GiB. */
|
||||
export const DOCUMENT_DEFAULT_QUOTA_BYTES = 1_073_741_824;
|
||||
|
||||
/** In-memory admission bounds for body accumulation in one server process. */
|
||||
export const DOCUMENT_UPLOAD_GLOBAL_CONCURRENCY = 4;
|
||||
export const DOCUMENT_UPLOAD_PER_USER_CONCURRENCY = 2;
|
||||
|
||||
let activeDocumentUploads = 0;
|
||||
const activeDocumentUploadsByUser = new Map<string, number>();
|
||||
|
||||
/**
|
||||
* Reserve one process-local document upload slot. The returned release is
|
||||
* idempotent so a defensive second cleanup cannot corrupt the counters.
|
||||
*/
|
||||
export function acquireDocumentUploadSlot(userId: string): (() => void) | null {
|
||||
const activeForUser = activeDocumentUploadsByUser.get(userId) ?? 0;
|
||||
if (
|
||||
activeDocumentUploads >= DOCUMENT_UPLOAD_GLOBAL_CONCURRENCY ||
|
||||
activeForUser >= DOCUMENT_UPLOAD_PER_USER_CONCURRENCY
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
activeDocumentUploads += 1;
|
||||
activeDocumentUploadsByUser.set(userId, activeForUser + 1);
|
||||
let released = false;
|
||||
|
||||
return () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
activeDocumentUploads -= 1;
|
||||
const remainingForUser = (activeDocumentUploadsByUser.get(userId) ?? 1) - 1;
|
||||
if (remainingForUser === 0) {
|
||||
activeDocumentUploadsByUser.delete(userId);
|
||||
} else {
|
||||
activeDocumentUploadsByUser.set(userId, remainingForUser);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocumentLimits {
|
||||
/** Per-file upload cap in bytes (admin-tunable, clamped to the ceiling). */
|
||||
maxFileBytes: number;
|
||||
|
||||
Reference in new issue
Block a user