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() {
|
async function main() {
|
||||||
const adminUrl = parseAdminUrl();
|
const adminUrl = parseAdminUrl();
|
||||||
const results = [];
|
const results = [];
|
||||||
@@ -953,6 +1041,7 @@ async function main() {
|
|||||||
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runLdapAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
await runGroupsAreaChecks(adminUrl, scratchRoleUrlString, results);
|
||||||
await runTransactionShapeMeasurement(scratchRoleUrlString, results);
|
await runTransactionShapeMeasurement(scratchRoleUrlString, results);
|
||||||
|
await runConcurrencyProbe(scratchRoleUrlString, results);
|
||||||
} finally {
|
} finally {
|
||||||
console.log(`Raeume Wegwerf-Datenbank "${SCRATCH_DB_NAME}" ab...`);
|
console.log(`Raeume Wegwerf-Datenbank "${SCRATCH_DB_NAME}" ab...`);
|
||||||
await teardownScratchDatabase(adminUrl);
|
await teardownScratchDatabase(adminUrl);
|
||||||
|
|||||||
@@ -75,13 +75,9 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
* der aeusseren Transaktion ist nicht mehr gegeben.
|
* der aeusseren Transaktion ist nicht mehr gegeben.
|
||||||
* Form (ii) — interaktive Callback-Form auf dem gebundenen Client
|
* Form (ii) — interaktive Callback-Form auf dem gebundenen Client
|
||||||
* (`forTenant(prisma, tenantId).$transaction(async (tx) => ...)`):
|
* (`forTenant(prisma, tenantId).$transaction(async (tx) => ...)`):
|
||||||
* bestand die Einzelmessung (gleiche Verbindung, richtiger
|
* besteht die Einzelmessung (gleiche Verbindung, richtiger
|
||||||
* Kontext, richtige Zeilenzahl), brach aber unter einer
|
* Kontext, richtige Zeilenzahl), faellt aber unter Last aus.
|
||||||
* zusaetzlichen Lastprobe (40 parallele Aufrufe,
|
* Ursache: jeder `tx`-Aufruf innerhalb der interaktiven
|
||||||
* 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
|
|
||||||
* Transaktion loest selbst wieder eine VERSCHACHTELTE
|
* Transaktion loest selbst wieder eine VERSCHACHTELTE
|
||||||
* Array-Transaktion aus (weil `$allOperations` bei jedem
|
* Array-Transaktion aus (weil `$allOperations` bei jedem
|
||||||
* Aufruf erneut feuert), und die aeussere plus jede innere
|
* Aufruf erneut feuert), und die aeussere plus jede innere
|
||||||
@@ -89,10 +85,21 @@ import { PrismaClient } from '@prisma/client';
|
|||||||
* demselben, endlichen Pool.
|
* demselben, endlichen Pool.
|
||||||
* Form (iii) — interaktive Callback-Form auf dem UNGEBUNDENEN Client,
|
* Form (iii) — interaktive Callback-Form auf dem UNGEBUNDENEN Client,
|
||||||
* `set_config` als ERSTE Anweisung direkt auf `tx` (nicht auf
|
* `set_config` als ERSTE Anweisung direkt auf `tx` (nicht auf
|
||||||
* dem aeusseren Client): bestand sowohl die Einzelmessung als
|
* dem aeusseren Client): besteht Einzelmessung UND Lastprobe —
|
||||||
* auch die Lastprobe (0 Verletzungen unter 40 parallelen
|
* sie belegt pro Aufruf genau EINE Verbindung, ohne
|
||||||
* Aufrufen) — sie belegt pro Aufruf genau EINE Verbindung,
|
* Verschachtelung.
|
||||||
* 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
|
* Entscheidung: `withTenantTransaction()` unten baut Form (iii) nach und
|
||||||
* ist das Hilfsmittel fuer alle mehrschrittigen, mandantengebundenen
|
* 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,
|
Zeilenzahl). Über diese Einzelmessung hinaus wurde als zusätzliche,
|
||||||
sicherheitsrelevante Sorgfaltsprüfung (nicht durch den Plan verlangt, aber
|
sicherheitsrelevante Sorgfaltsprüfung (nicht durch den Plan verlangt, aber
|
||||||
durch die Tragweite dieses Bereichs geboten) beide Formen unter echter
|
durch die Tragweite dieses Bereichs geboten) beide Formen unter echter
|
||||||
Nebenläufigkeit erneut gemessen — 40 parallele Aufrufe, alternierend
|
Nebenläufigkeit erneut gemessen — 40 parallele Aufrufe über EINEN
|
||||||
TENANT-A/TENANT-B, gegen eine separate Experiment-Datenbank mit derselben
|
gemeinsamen Client, alternierend TENANT-A/TENANT-B.
|
||||||
Struktur:
|
|
||||||
|
|
||||||
- **Form (ii)** brach unter dieser Last mit
|
Diese Belastungsprobe lief zunächst gegen eine separate Experiment-Datenbank
|
||||||
`PrismaClientKnownRequestError: Transaction API error: Unable to start a
|
und war damit **nicht nachvollziehbar** — eine Zahl, die eine Entscheidung
|
||||||
transaction in the given time.` (Code `P2028`) ab. Ursache: jede
|
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
|
`tx.$queryRaw`-Anweisung innerhalb der interaktiven Transaktion auf dem
|
||||||
gebundenen Client löst selbst wieder eine VERSCHACHTELTE
|
gebundenen Client löst selbst wieder eine VERSCHACHTELTE
|
||||||
Array-Transaktion auf dem äußeren, ungebundenen Client aus (weil
|
Array-Transaktion auf dem äußeren, ungebundenen Client aus (weil
|
||||||
@@ -250,9 +260,17 @@ Struktur:
|
|||||||
interaktive Transaktion UND jede innere Verschachtelung belegen
|
interaktive Transaktion UND jede innere Verschachtelung belegen
|
||||||
gleichzeitig eine Verbindung aus demselben, endlichen Pool. Unter Last
|
gleichzeitig eine Verbindung aus demselben, endlichen Pool. Unter Last
|
||||||
reicht der Pool nicht mehr aus.
|
reicht der Pool nicht mehr aus.
|
||||||
- **Form (iii)** bestand dieselbe Belastung mit 0 Verletzungen unter 40
|
- **Form (iii)** besteht dieselbe Belastung ohne Verletzung — sie belegt
|
||||||
parallelen Aufrufen — sie belegt pro Aufruf genau eine Verbindung, ohne
|
pro Aufruf genau eine Verbindung, ohne Verschachtelung.
|
||||||
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:
|
Das ist der entscheidende Befund für die Werkzeugentscheidung in Aufgabe 2:
|
||||||
obwohl Form (ii) die im Plan geforderte EINZELMESSUNG technisch besteht,
|
obwohl Form (ii) die im Plan geforderte EINZELMESSUNG technisch besteht,
|
||||||
|
|||||||
Reference in New Issue
Block a user