mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-22 14:38:10 +02:00
fix(qlik): retry engine aborts after selections
This commit is contained in:
1 parent
f005c08ad6
commit
1cb6c2496d
1 file changed
+46
-4
@@ -115,6 +115,36 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
ws.onerror = () => rej(new Error("ws error (403 ?)"));
|
ws.onerror = () => rej(new Error("ws error (403 ?)"));
|
||||||
setTimeout(() => rej(new Error("ws timeout")), 30000);
|
setTimeout(() => rej(new Error("ws timeout")), 30000);
|
||||||
});
|
});
|
||||||
|
// Qlik Engine code 15 = "Request aborted" : déclenché quand une requête
|
||||||
|
// (GetLayout / GetHyperCubeData / CreateSessionObject) entre en collision
|
||||||
|
// avec une évaluation de sélection encore en cours côté Engine. On retente
|
||||||
|
// quelques fois avec un court backoff et un log explicite de l'étape.
|
||||||
|
const isEngineCode15 = (e: unknown): boolean => {
|
||||||
|
const msg = String((e as Error)?.message ?? e);
|
||||||
|
return /"code"\s*:\s*15\b/.test(msg);
|
||||||
|
};
|
||||||
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||||
|
const rpcWithRetry = async (
|
||||||
|
etape: string,
|
||||||
|
method: string,
|
||||||
|
params: unknown,
|
||||||
|
handle: number,
|
||||||
|
maxAttempts = 4,
|
||||||
|
): Promise<{ [k: string]: unknown }> => {
|
||||||
|
let lastErr: unknown;
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await rpc(method, params, handle);
|
||||||
|
} catch (e) {
|
||||||
|
lastErr = e;
|
||||||
|
if (!isEngineCode15(e) || attempt === maxAttempts) throw e;
|
||||||
|
const backoff = 150 * attempt;
|
||||||
|
console.log(`[qlik-pw] retry code15 étape=${etape} méthode=${method} tentative=${attempt}/${maxAttempts} backoff=${backoff}ms`);
|
||||||
|
await sleep(backoff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr;
|
||||||
|
};
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -158,8 +188,11 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
// déjà sélectionnés) puis le détruit. Réutiliser le même objet sur des centaines de
|
// déjà sélectionnés) puis le détruit. Réutiliser le même objet sur des centaines de
|
||||||
// lots force Qlik à recalculer en boucle et finit par "Request aborted" (code 15).
|
// lots force Qlik à recalculer en boucle et finit par "Request aborted" (code 15).
|
||||||
const PAGE = 1400;
|
const PAGE = 1400;
|
||||||
|
// Petite latence après chaque mutation de sélection : laisser l'Engine
|
||||||
|
// finaliser l'évaluation de la sélection avant la prochaine requête layout/cube.
|
||||||
|
const SETTLE_MS = 150;
|
||||||
const createCubeForBatch = async (): Promise<{ handle: number; id: string }> => {
|
const createCubeForBatch = async (): Promise<{ handle: number; id: string }> => {
|
||||||
const obj = await rpc("CreateSessionObject", { qProp: { qInfo: { qType: "cf-net" }, qHyperCubeDef: {
|
const obj = await rpcWithRetry("createCube", "CreateSessionObject", { qProp: { qInfo: { qType: "cf-net" }, qHyperCubeDef: {
|
||||||
qDimensions: [{ qLibraryId: dim }],
|
qDimensions: [{ qLibraryId: dim }],
|
||||||
qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: rCamag }, { qLibraryId: rMarge }],
|
qMeasures: [{ qLibraryId: mca }, { qLibraryId: mqte }, { qLibraryId: mnb }, { qLibraryId: rCamag }, { qLibraryId: rMarge }],
|
||||||
qInitialDataFetch: [],
|
qInitialDataFetch: [],
|
||||||
@@ -170,9 +203,10 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
const destroyCube = async (cube: { handle: number; id: string }) => {
|
const destroyCube = async (cube: { handle: number; id: string }) => {
|
||||||
if (!cube.id) return;
|
if (!cube.id) return;
|
||||||
try {
|
try {
|
||||||
await rpc("DestroySessionObject", { qId: cube.id }, doc);
|
await rpcWithRetry("destroyCube", "DestroySessionObject", { qId: cube.id }, doc);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Non fatal : l'objet est de toute façon scopé à la session websocket.
|
// Non fatal : l'objet est de toute façon scopé à la session websocket.
|
||||||
|
// Un code 15 ici signifie que l'Engine l'a déjà libéré après un abort.
|
||||||
console.log("[qlik-pw] DestroySessionObject ignoré: " + String((e as Error)?.message || e));
|
console.log("[qlik-pw] DestroySessionObject ignoré: " + String((e as Error)?.message || e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -180,11 +214,11 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
const cube = await createCubeForBatch();
|
const cube = await createCubeForBatch();
|
||||||
const oh = cube.handle;
|
const oh = cube.handle;
|
||||||
try {
|
try {
|
||||||
const layout = await rpc("GetLayout", {}, oh);
|
const layout = await rpcWithRetry("getLayout", "GetLayout", {}, oh);
|
||||||
const size = ((layout.qLayout as { qHyperCube: { qSize: { qcy: number } } }).qHyperCube).qSize.qcy;
|
const size = ((layout.qLayout as { qHyperCube: { qSize: { qcy: number } } }).qHyperCube).qSize.qcy;
|
||||||
let got = 0;
|
let got = 0;
|
||||||
for (let top = 0; top < size; top += PAGE) {
|
for (let top = 0; top < size; top += PAGE) {
|
||||||
const d = await rpc("GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 6, qHeight: PAGE }] }, oh);
|
const d = await rpcWithRetry("getHyperCubeData", "GetHyperCubeData", { qPath: "/qHyperCubeDef", qPages: [{ qTop: top, qLeft: 0, qWidth: 6, qHeight: PAGE }] }, oh);
|
||||||
const matrix = (d.qDataPages as Array<{ qMatrix?: Array<Array<{ qText?: string; qNum?: number }>> }>)?.[0]?.qMatrix ?? [];
|
const matrix = (d.qDataPages as Array<{ qMatrix?: Array<Array<{ qText?: string; qNum?: number }>> }>)?.[0]?.qMatrix ?? [];
|
||||||
if (!matrix.length) break;
|
if (!matrix.length) break;
|
||||||
for (const r of matrix) sink.push([
|
for (const r of matrix) sink.push([
|
||||||
@@ -239,6 +273,9 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
qSoftLock: true,
|
qSoftLock: true,
|
||||||
}, dfh);
|
}, dfh);
|
||||||
console.log(prefix + " — SelectValues Date " + m.serials.length + " serials → " + JSON.stringify(dsel.qReturn));
|
console.log(prefix + " — SelectValues Date " + m.serials.length + " serials → " + JSON.stringify(dsel.qReturn));
|
||||||
|
// Laisser l'Engine finaliser l'évaluation de la sélection Date
|
||||||
|
// avant la première SelectValues Article Code du 1er lot.
|
||||||
|
await sleep(SETTLE_MS);
|
||||||
|
|
||||||
// 2) Sélection Article Code par lots contrôlés.
|
// 2) Sélection Article Code par lots contrôlés.
|
||||||
for (let bi = 0; bi < codeBatches.length; bi++) {
|
for (let bi = 0; bi < codeBatches.length; bi++) {
|
||||||
@@ -247,12 +284,17 @@ export async function fetchNetworkMetricsPlaywright(
|
|||||||
try {
|
try {
|
||||||
if (fh !== -1) {
|
if (fh !== -1) {
|
||||||
await rpc("Clear", {}, fh);
|
await rpc("Clear", {}, fh);
|
||||||
|
// Laisser l'Engine appliquer le Clear avant la nouvelle sélection.
|
||||||
|
await sleep(SETTLE_MS);
|
||||||
const csel = await rpc("SelectValues", {
|
const csel = await rpc("SelectValues", {
|
||||||
qFieldValues: batch.map((c) => ({ qText: c })),
|
qFieldValues: batch.map((c) => ({ qText: c })),
|
||||||
qToggleMode: false,
|
qToggleMode: false,
|
||||||
qSoftLock: true,
|
qSoftLock: true,
|
||||||
}, fh);
|
}, fh);
|
||||||
console.log(batchPrefix + " — SelectValues Article Code → " + JSON.stringify(csel.qReturn));
|
console.log(batchPrefix + " — SelectValues Article Code → " + JSON.stringify(csel.qReturn));
|
||||||
|
// Laisser l'Engine finaliser l'évaluation de la sélection
|
||||||
|
// avant de créer / interroger le cube (évite code 15 sur le 1er lot).
|
||||||
|
await sleep(SETTLE_MS);
|
||||||
}
|
}
|
||||||
const got = await fetchCubeForSelection(out);
|
const got = await fetchCubeForSelection(out);
|
||||||
console.log(batchPrefix + " — " + got + " ligne(s), cumul=" + out.length);
|
console.log(batchPrefix + " — " + got + " ligne(s), cumul=" + out.length);
|
||||||
|
|||||||
Reference in new issue
Block a user