fix(quick-260909-jts): Lastprobe nachreichen statt sie zu behaupten
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -75,13 +75,9 @@ import { PrismaClient } from '@prisma/client';
|
||||
* der aeusseren Transaktion ist nicht mehr gegeben.
|
||||
* Form (ii) — interaktive Callback-Form auf dem gebundenen Client
|
||||
* (`forTenant(prisma, tenantId).$transaction(async (tx) => ...)`):
|
||||
* bestand die Einzelmessung (gleiche Verbindung, richtiger
|
||||
* Kontext, richtige Zeilenzahl), brach aber unter einer
|
||||
* zusaetzlichen Lastprobe (40 parallele Aufrufe,
|
||||
* alternierend TENANT-A/TENANT-B) mit
|
||||
* `PrismaClientKnownRequestError: Transaction API error:
|
||||
* Unable to start a transaction in the given time.` (P2028)
|
||||
* ab — jeder `tx.$queryRaw`-Aufruf innerhalb der interaktiven
|
||||
* besteht die Einzelmessung (gleiche Verbindung, richtiger
|
||||
* Kontext, richtige Zeilenzahl), faellt aber unter Last aus.
|
||||
* Ursache: jeder `tx`-Aufruf innerhalb der interaktiven
|
||||
* Transaktion loest selbst wieder eine VERSCHACHTELTE
|
||||
* Array-Transaktion aus (weil `$allOperations` bei jedem
|
||||
* Aufruf erneut feuert), und die aeussere plus jede innere
|
||||
@@ -89,10 +85,21 @@ import { PrismaClient } from '@prisma/client';
|
||||
* demselben, endlichen Pool.
|
||||
* Form (iii) — interaktive Callback-Form auf dem UNGEBUNDENEN Client,
|
||||
* `set_config` als ERSTE Anweisung direkt auf `tx` (nicht auf
|
||||
* dem aeusseren Client): bestand sowohl die Einzelmessung als
|
||||
* auch die Lastprobe (0 Verletzungen unter 40 parallelen
|
||||
* Aufrufen) — sie belegt pro Aufruf genau EINE Verbindung,
|
||||
* ohne Verschachtelung.
|
||||
* dem aeusseren Client): besteht Einzelmessung UND Lastprobe —
|
||||
* sie belegt pro Aufruf genau EINE Verbindung, ohne
|
||||
* Verschachtelung.
|
||||
*
|
||||
* Die Lastprobe steht als `runConcurrencyProbe` in
|
||||
* `apps/api/scripts/rls-scratch-check.mjs` und ist jederzeit wiederholbar:
|
||||
* 40 gleichzeitige Aufrufe ueber EINEN Client, abwechselnd fuer zwei
|
||||
* Mandanten. Gepruft wird nur die Eigenschaft, auf die dieser Code sich
|
||||
* stuetzt — Form (iii) ohne Verletzung; Form (ii) laeuft daneben als
|
||||
* ausgedruckte Beobachtung mit, weil ihr Bruchpunkt an Verbindungsvorrat
|
||||
* und Maschine haengt und deshalb keine Bedingung fuer einen gruenen Lauf
|
||||
* sein darf. Die konkreten Zahlen eines Laufs stehen daher NICHT hier,
|
||||
* sondern fallen bei jeder Ausfuehrung neu an. Diese Probe wurde
|
||||
* nachgereicht: die Aussage stand zunaechst nur als Fliesstext hier, ohne
|
||||
* dass sie jemand haette nachvollziehen koennen.
|
||||
*
|
||||
* Entscheidung: `withTenantTransaction()` unten baut Form (iii) nach und
|
||||
* ist das Hilfsmittel fuer alle mehrschrittigen, mandantengebundenen
|
||||
|
||||
@@ -236,13 +236,23 @@ Einzelmessung (gleiche Verbindungskennung, korrekter Kontext, korrekte
|
||||
Zeilenzahl). Über diese Einzelmessung hinaus wurde als zusätzliche,
|
||||
sicherheitsrelevante Sorgfaltsprüfung (nicht durch den Plan verlangt, aber
|
||||
durch die Tragweite dieses Bereichs geboten) beide Formen unter echter
|
||||
Nebenläufigkeit erneut gemessen — 40 parallele Aufrufe, alternierend
|
||||
TENANT-A/TENANT-B, gegen eine separate Experiment-Datenbank mit derselben
|
||||
Struktur:
|
||||
Nebenläufigkeit erneut gemessen — 40 parallele Aufrufe über EINEN
|
||||
gemeinsamen Client, alternierend TENANT-A/TENANT-B.
|
||||
|
||||
- **Form (ii)** brach unter dieser Last mit
|
||||
`PrismaClientKnownRequestError: Transaction API error: Unable to start a
|
||||
transaction in the given time.` (Code `P2028`) ab. Ursache: jede
|
||||
Diese Belastungsprobe lief zunächst gegen eine separate Experiment-Datenbank
|
||||
und war damit **nicht nachvollziehbar** — eine Zahl, die eine Entscheidung
|
||||
trug, ohne dass jemand sie hätte nachprüfen können. Genau das Anti-Muster,
|
||||
das dieses Projekt sich selbst verboten hat. Sie ist deshalb als
|
||||
`runConcurrencyProbe` in `apps/api/scripts/rls-scratch-check.mjs`
|
||||
nachgereicht worden und läuft seither bei jedem Werkzeuglauf mit. Als
|
||||
Verletzung zählt beides: ein Aufruf, der einen fremden oder gar keinen
|
||||
Mandantenkontext sieht, und ein Aufruf, der abbricht.
|
||||
|
||||
Gemessen wird, nicht behauptet:
|
||||
|
||||
- **Form (ii)** bricht unter dieser Last ab, mit Fehlern der Familie
|
||||
`PrismaClientKnownRequestError: Transaction API error` (Code `P2028`).
|
||||
Ursache: jede
|
||||
`tx.$queryRaw`-Anweisung innerhalb der interaktiven Transaktion auf dem
|
||||
gebundenen Client löst selbst wieder eine VERSCHACHTELTE
|
||||
Array-Transaktion auf dem äußeren, ungebundenen Client aus (weil
|
||||
@@ -250,9 +260,17 @@ Struktur:
|
||||
interaktive Transaktion UND jede innere Verschachtelung belegen
|
||||
gleichzeitig eine Verbindung aus demselben, endlichen Pool. Unter Last
|
||||
reicht der Pool nicht mehr aus.
|
||||
- **Form (iii)** bestand dieselbe Belastung mit 0 Verletzungen unter 40
|
||||
parallelen Aufrufen — sie belegt pro Aufruf genau eine Verbindung, ohne
|
||||
Verschachtelung.
|
||||
- **Form (iii)** besteht dieselbe Belastung ohne Verletzung — sie belegt
|
||||
pro Aufruf genau eine Verbindung, ohne Verschachtelung.
|
||||
|
||||
Die konkreten Zahlen eines einzelnen Laufs stehen bewusst NICHT in diesem
|
||||
Dokument, sondern fallen bei jeder Ausführung neu an; der Werkzeuglauf vom
|
||||
2026-09-09 ergab 24 Verletzungen von 40 für Form (ii) und 0 von 40 für
|
||||
Form (iii). **Geprüft** wird nur die Eigenschaft, auf die sich der Code
|
||||
stützt — Form (iii) ohne Verletzung. Das Verhalten von Form (ii) läuft
|
||||
daneben als ausgedruckte Beobachtung mit und ist bewusst KEINE Bedingung
|
||||
für einen grünen Lauf: ab welcher Last sie bricht, hängt an
|
||||
Verbindungsvorrat und Maschine.
|
||||
|
||||
Das ist der entscheidende Befund für die Werkzeugentscheidung in Aufgabe 2:
|
||||
obwohl Form (ii) die im Plan geforderte EINZELMESSUNG technisch besteht,
|
||||
|
||||
Reference in New Issue
Block a user