85 lines
3.4 KiB
JavaScript
85 lines
3.4 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
test('gRPC streaming releases processed buffers while the stream stays open', (t) => {
|
|
const result = spawnSync(process.execPath, ['--expose-gc', '--input-type=module', '--eval', `
|
|
import assert from 'node:assert/strict';
|
|
import { create } from '@bufbuild/protobuf';
|
|
import { createClient } from '@connectrpc/connect';
|
|
import { connectNodeAdapter, createGrpcTransport } from '@connectrpc/connect-node';
|
|
import http2 from 'node:http2';
|
|
import { setImmediate as tick } from 'node:timers/promises';
|
|
import { ConnectionEventsSchema, StartedService } from './dist/server/generated/daemon/started_service_pb.js';
|
|
|
|
const count = 512;
|
|
const payload = 'x'.repeat(16 * 1024);
|
|
const sessions = new Set();
|
|
const server = http2.createServer(connectNodeAdapter({
|
|
routes(router) {
|
|
router.service(StartedService, {
|
|
async *subscribeConnections(_request, context) {
|
|
for (let i = 0; i < count; i++) {
|
|
yield create(ConnectionEventsSchema, {
|
|
events: [{ id: String(i), connection: { id: String(i), rule: payload } }],
|
|
});
|
|
}
|
|
await new Promise((resolve) => {
|
|
if (context.signal.aborted) resolve();
|
|
else context.signal.addEventListener('abort', resolve, { once: true });
|
|
});
|
|
},
|
|
});
|
|
},
|
|
}));
|
|
server.on('session', (session) => {
|
|
sessions.add(session);
|
|
session.once('close', () => sessions.delete(session));
|
|
});
|
|
const controller = new AbortController();
|
|
async function collectedBuffers() {
|
|
for (let i = 0; i < 3; i++) {
|
|
await tick();
|
|
global.gc();
|
|
}
|
|
return process.memoryUsage().arrayBuffers;
|
|
}
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', resolve);
|
|
});
|
|
const client = createClient(StartedService, createGrpcTransport({
|
|
baseUrl: 'http://127.0.0.1:' + server.address().port,
|
|
}));
|
|
const baseline = await collectedBuffers();
|
|
const iterator = client.subscribeConnections({ interval: 1n }, {
|
|
signal: controller.signal,
|
|
})[Symbol.asyncIterator]();
|
|
for (let i = 0; i < count; i++) {
|
|
const next = await iterator.next();
|
|
assert.equal(next.done, false);
|
|
assert.equal(next.value.events[0].id, String(i));
|
|
assert.equal(next.value.events[0].connection.rule, payload);
|
|
}
|
|
const retainedBytes = await collectedBuffers() - baseline;
|
|
console.log(JSON.stringify({ processedBytes: count * payload.length, retainedBytes }));
|
|
assert.equal(controller.signal.aborted, false);
|
|
assert.ok(retainedBytes < 2 * 1024 * 1024,
|
|
'processed gRPC buffers remain retained while the stream is open: ' + retainedBytes);
|
|
} finally {
|
|
controller.abort();
|
|
for (const session of sessions) session.destroy();
|
|
if (server.listening) await new Promise((resolve) => server.close(resolve));
|
|
}
|
|
`], {
|
|
cwd: path.resolve(import.meta.dirname, '../..'),
|
|
encoding: 'utf8',
|
|
timeout: 20_000,
|
|
});
|
|
assert.ifError(result.error);
|
|
assert.equal(result.status, 0, result.stderr + result.stdout);
|
|
t.diagnostic(result.stdout.trim());
|
|
});
|