|
1 | | -// Isolation probe (NOT part of the suite): does the VGI `worker:` `boom` table function |
2 | | -// (which throws inside its scan) settle the async query promise under threads=1 vs |
3 | | -// threads=4? A generic DuckDB parallel throw propagates fine (see probe-throw.mjs), so |
4 | | -// this pins down whether the earlier "boom hangs under threads>1" is VGI-specific. |
5 | | -// Serve with: VGI_ENTRY=probe-vgi-boom.mjs node serve.mjs 8799 |
| 1 | +// Root-cause probe (NOT part of the suite): reproduce the flaky "worker error under |
| 2 | +// threads>1 + heavy load" hang, then answer the key question — is the WHOLE engine |
| 3 | +// frozen (worker thread stuck in the boom C++ call) or just the boom query's result? |
| 4 | +// Mirrors the full suite's prefix, then runs boom under threads=4 with a timeout; on a |
| 5 | +// HANG it (a) probes a trivial query on a fresh connection to test engine-frozen, and |
| 6 | +// (b) reads the raw channel (slot STATE + ring positions — transport-written, no |
| 7 | +// instrumentation). Serve: VGI_ENTRY=probe-vgi-boom.mjs node serve.mjs 8799 |
6 | 8 | import * as duckdb from './duckdb-browser.mjs'; |
7 | 9 | import { installVgiWebWorkerBridge } from './vgi-webworker-bridge.ts'; |
8 | 10 |
|
9 | 11 | const out = document.getElementById('out'); |
10 | 12 | const log = (m) => { out.textContent += m + '\n'; }; |
11 | 13 | window.__done = false; |
12 | 14 | window.__result = {}; |
| 15 | +const W = "'worker:vgi-worker-boot.js'"; |
| 16 | +const timeout = (ms, tag) => new Promise((_, rej) => setTimeout(() => rej(new Error('__TIMEOUT__' + (tag || ''))), ms)); |
| 17 | + |
| 18 | +function readChannel() { |
| 19 | + const d = globalThis.__vgiDiag; if (!(d && d.buffer)) return null; |
| 20 | + const i = new Int32Array(d.buffer), b = d.offset >> 2; |
| 21 | + const nSlots = i[b + 2], stride = i[b + 4], slotsOff = i[b + 5], slots = []; |
| 22 | + for (let s = 0; s < nSlots; s++) { |
| 23 | + const sb = (d.offset + slotsOff + s * stride) >> 2; |
| 24 | + slots.push({ slot: s, STATE: i[sb], C2W_CL: i[sb + 3], W2C_W: i[sb + 4], W2C_R: i[sb + 5], W2C_CL: i[sb + 6] }); |
| 25 | + } |
| 26 | + return slots; |
| 27 | +} |
13 | 28 |
|
14 | 29 | (async () => { |
15 | 30 | const R = window.__result; |
16 | 31 | try { |
17 | 32 | R.coi = self.crossOriginIsolated; |
18 | | - log('coi=' + R.coi); |
19 | | - const bundle = await duckdb.selectBundle({ |
20 | | - coi: { |
21 | | - mainModule: './duckdb-coi.wasm', |
22 | | - mainWorker: './duckdb-browser-coi.worker.js', |
23 | | - pthreadWorker: './duckdb-browser-coi.pthread.worker.js', |
24 | | - }, |
25 | | - }); |
| 33 | + const bundle = await duckdb.selectBundle({ coi: { |
| 34 | + mainModule: './duckdb-coi.wasm', mainWorker: './duckdb-browser-coi.worker.js', |
| 35 | + pthreadWorker: './duckdb-browser-coi.pthread.worker.js' } }); |
26 | 36 | const worker = new Worker(bundle.mainWorker); |
27 | 37 | installVgiWebWorkerBridge()(worker); |
28 | 38 | const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker); |
29 | 39 | await db.instantiate(bundle.mainModule, bundle.pthreadWorker); |
30 | 40 | await db.open({ allowUnsignedExtensions: true, query: { castBigIntToDouble: true } }); |
31 | 41 | const conn = await db.connect(); |
32 | 42 | await conn.query("SET custom_extension_repository='" + location.origin + "/extensions'"); |
33 | | - await conn.query('INSTALL vgi'); |
34 | | - await conn.query('LOAD vgi'); |
35 | | - // Warm the worker with a successful scan so the boom probe isn't the first RPC. |
36 | | - await conn.query("SELECT * FROM vgi_table_function('worker:vgi-worker-boot.js', 'count_to', [3])"); |
37 | | - log('warmup ok'); |
38 | | - // Bisect: does an ATTACH'd VGI catalog present at error time trigger the hang? |
39 | | - if (new URLSearchParams(location.search).get('attach') !== '0') { |
40 | | - await conn.query("ATTACH 'worker:vgi-worker-boot.js' AS wcat (TYPE vgi)"); |
41 | | - await conn.query('SELECT * FROM wcat.main.count_to(2)'); |
42 | | - log('attach ok'); |
43 | | - } |
44 | | - // Bisect: replicate the suite's concurrent multi-connection case before boom. |
45 | | - if (new URLSearchParams(location.search).get('concurrent') !== '0') { |
46 | | - await conn.query('SET threads=4'); |
47 | | - const ns = [6, 9]; |
48 | | - const conns = await Promise.all(ns.map(() => db.connect())); |
49 | | - await Promise.all(conns.map((c, k) => |
50 | | - c.query("SELECT * FROM vgi_table_function('worker:vgi-worker-boot.js', 'count_to', [" + ns[k] + '])'))); |
51 | | - for (const c of conns) await c.close(); |
52 | | - log('concurrent ok'); |
| 43 | + await conn.query('INSTALL vgi'); await conn.query('LOAD vgi'); |
| 44 | + |
| 45 | + // ---- mirror the full suite's prefix (this is what triggers the flaky hang) ---- |
| 46 | + await conn.query(`SELECT * FROM vgi_table_function(${W}, 'count_to', [5])`); |
| 47 | + await conn.query(`SELECT count(*)::INT c, sum(value)::INT s FROM vgi_table_function(${W}, 'emit_batches', [3, 4])`); |
| 48 | + await conn.query(`ATTACH ${W} AS wcat (TYPE vgi)`); |
| 49 | + await conn.query("SELECT DISTINCT function_name FROM vgi_function_arguments() WHERE catalog_name='wcat'"); |
| 50 | + await conn.query('SELECT * FROM wcat.main.count_to(3)'); |
| 51 | + await conn.query('SET threads=4'); |
| 52 | + const cs = await Promise.all([6, 9].map(() => db.connect())); |
| 53 | + await Promise.all(cs.map((c, k) => c.query(`SELECT * FROM vgi_table_function(${W}, 'count_to', [${[6, 9][k]}])`))); |
| 54 | + for (const c of cs) await c.close(); |
| 55 | + log('prefix ok'); |
| 56 | + |
| 57 | + // ---- boom under threads=4, on the MAIN connection (matches the suite: same conn |
| 58 | + // that did LOAD + all the prior scans + ATTACH + discovery; threads=4 already set) ---- |
| 59 | + const bc = conn; |
| 60 | + let boom; |
| 61 | + const t0 = Date.now(); |
| 62 | + try { |
| 63 | + await Promise.race([bc.query(`SELECT * FROM vgi_table_function(${W}, 'boom', [])`), timeout(15000, 'boom')]); |
| 64 | + boom = 'no_throw'; |
| 65 | + } catch (e) { |
| 66 | + const m = String((e && e.message) || e); |
| 67 | + boom = m.includes('__TIMEOUT__') ? 'HANG' : 'threw'; |
53 | 68 | } |
| 69 | + R.boom = boom; R.boomMs = Date.now() - t0; |
| 70 | + log('boom(threads=4) => ' + boom + ' (' + R.boomMs + 'ms)'); |
54 | 71 |
|
55 | | - async function probeBoom(threads) { |
56 | | - const c = await db.connect(); |
57 | | - await c.query('SET threads=' + threads); |
58 | | - const started = Date.now(); |
59 | | - let r; |
| 72 | + if (boom === 'HANG') { |
| 73 | + // Is the whole engine frozen, or just boom's result? Trivial query, fresh conn. |
| 74 | + const fc = await db.connect(); |
60 | 75 | try { |
61 | | - await Promise.race([ |
62 | | - c.query("SELECT * FROM vgi_table_function('worker:vgi-worker-boot.js', 'boom', [])"), |
63 | | - new Promise((_, rej) => setTimeout(() => rej(new Error('__TIMEOUT__')), 15000)), |
64 | | - ]); |
65 | | - r = { threads, outcome: 'no_throw' }; |
| 76 | + await Promise.race([fc.query('SELECT 42 AS x'), timeout(8000, 'sel42')]); |
| 77 | + R.engineFrozen = false; // engine still services other queries → boom-query-specific |
66 | 78 | } catch (e) { |
67 | | - const msg = String((e && e.message) || e); |
68 | | - r = msg.includes('__TIMEOUT__') |
69 | | - ? { threads, outcome: 'HANG' } |
70 | | - : { threads, outcome: 'threw', msg: msg.slice(0, 90) }; |
| 79 | + R.engineFrozen = String((e && e.message) || e).includes('__TIMEOUT__') ? true : ('err:' + e); |
71 | 80 | } |
72 | | - r.ms = Date.now() - started; |
73 | | - log('boom threads=' + threads + ' => ' + JSON.stringify(r)); |
74 | | - return r; |
| 81 | + log('engineFrozen=' + R.engineFrozen); |
| 82 | + R.channel = readChannel(); |
| 83 | + log('channel=' + JSON.stringify(R.channel)); |
75 | 84 | } |
76 | | - |
77 | | - R.t1 = await probeBoom(1); // control |
78 | | - R.t4 = await probeBoom(4); // the question |
79 | 85 | R.pass = true; |
80 | 86 | } catch (e) { |
81 | 87 | R.error = String(e && e.stack ? e.stack : e); |
|
0 commit comments