fix(quick-260909-jts): Lastprobe nachreichen statt sie zu behaupten

This commit is contained in:
2026-09-09 15:18:55 +02:00
parent abb6c8bea3
commit 604428a91d
3 changed files with 134 additions and 20 deletions
+89
View File
@@ -935,6 +935,94 @@ async function runTransactionShapeMeasurement(scratchRoleUrl, results) {
);
}
/**
* Ein einzelner Aufruf in Form (ii): interaktive Callback-Transaktion auf dem
* GEBUNDENEN Client. Liefert den im Inneren gelesenen Mandantenkontext.
*/
async function runOneBoundInteractive(prisma, tenantId) {
const bound = buildInlineExtendedClient(prisma, tenantId);
return bound.$transaction(async (tx) => {
const rows = await tx.$queryRaw`SELECT current_tenant_id() AS t`;
return rows[0]?.t;
});
}
/**
* Ein einzelner Aufruf in Form (iii): interaktive Callback-Transaktion auf dem
* UNGEBUNDENEN Client, set_config als erste Anweisung direkt auf tx.
*/
async function runOneUnboundInteractive(prisma, tenantId) {
return prisma.$transaction(async (tx) => {
await tx.$queryRaw`SELECT set_config('app.current_tenant', ${tenantId}, true)`;
const rows = await tx.$queryRaw`SELECT current_tenant_id() AS t`;
return rows[0]?.t;
});
}
/**
* Belastet eine Transaktionsform mit `parallelCount` gleichzeitigen Aufrufen,
* abwechselnd fuer TENANT-A und TENANT-B, ueber EINEN gemeinsamen Client —
* also aus demselben endlichen Verbindungsvorrat, so wie im Betrieb.
*
* Als Verletzung zaehlt beides: ein Aufruf, der einen fremden oder gar keinen
* Mandantenkontext sieht, UND ein Aufruf, der abbricht. Der Abbruch ist hier
* kein Werkzeugfehler, sondern das Messergebnis — genau deshalb wird er
* gefangen und gezaehlt statt nach oben durchgereicht.
*/
async function measureUnderLoad(scratchRoleUrl, runOne, parallelCount) {
const prisma = new PrismaClient({ datasourceUrl: scratchRoleUrl });
try {
const calls = Array.from({ length: parallelCount }, (_, i) => {
const tenantId = i % 2 === 0 ? 'TENANT-A' : 'TENANT-B';
return runOne(prisma, tenantId).then(
(seen) => (seen === tenantId ? null : `erwartet ${tenantId}, gesehen ${seen ?? 'NULL'}`),
(err) => `abbruch ${err.code ?? ''}: ${String(err.message).split('\n')[0]}`.trim(),
);
});
const verletzungen = (await Promise.all(calls)).filter(Boolean);
return {
aufrufe: parallelCount,
verletzungen: verletzungen.length,
beispiele: verletzungen.slice(0, 2),
};
} finally {
await prisma.$disconnect();
}
}
/**
* Die Lastprobe hinter der Entscheidung fuer Form (iii).
*
* Sie existierte zunaechst nur als Fliesstext im Kopf von
* `prisma-tenant.extension.ts` — eine Zahl, die eine Entscheidung trug, aber
* nicht nachvollziehbar war. Genau das Anti-Muster, das dieses Projekt sich
* selbst verboten hat. Deshalb steht sie jetzt hier.
*
* GEPRUEFT wird nur die Eigenschaft, auf die sich der Code stuetzt: die
* GEWAEHLTE Form ueberlebt Nebenlaeufigkeit ohne Verletzung. Das Verhalten von
* Form (ii) wird daneben BEOBACHTET und ausgedruckt, aber nicht gepruft —
* ob und ab welcher Last sie abbricht, haengt an Verbindungsvorrat und
* Maschine und taugt nicht als Bedingung fuer einen gruenen Lauf.
*/
async function runConcurrencyProbe(scratchRoleUrl, results) {
const PARALLEL = 40;
const formZwei = await measureUnderLoad(scratchRoleUrl, runOneBoundInteractive, PARALLEL);
beobachte(`Form (ii) unter ${PARALLEL} parallelen Aufrufen`, formZwei);
const formDrei = await measureUnderLoad(scratchRoleUrl, runOneUnboundInteractive, PARALLEL);
beobachte(`Form (iii) unter ${PARALLEL} parallelen Aufrufen`, formDrei);
report(
results,
'gewaehlte-transaktionsform-uebersteht-nebenlaeufigkeit',
formDrei.verletzungen === 0,
`Form (iii): ${formDrei.verletzungen} Verletzung(en) bei ${PARALLEL} parallelen Aufrufen` +
` — Form (ii) zum Vergleich, nicht gepruft: ${formZwei.verletzungen}` +
(formZwei.beispiele.length ? ` (z.B. ${formZwei.beispiele[0]})` : ''),
);
}
async function main() {
const adminUrl = parseAdminUrl();
const results = [];
@@ -953,6 +1041,7 @@ async function main() {
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
await runTransactionShapeMeasurement(scratchRoleUrlString, results);
await runConcurrencyProbe(scratchRoleUrlString, results);
} finally {
console.log(`Raeume Wegwerf-Datenbank "${SCRATCH_DB_NAME}" ab...`);
await teardownScratchDatabase(adminUrl);