Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/genie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ if (sessionIdx !== -1 && sessionIdx + 1 < args.length) {
try {
await program.parseAsync(process.argv);
} finally {
stopOtelReceiver();
await stopOtelReceiver().catch(() => {});
await shutdownDb().catch(() => {});
}
}
Expand All @@ -708,7 +708,7 @@ if (sessionIdx !== -1 && sessionIdx + 1 < args.length) {
if (process.env.GENIE_PROFILE_DB) console.error(`[profile] parseAsync=${Date.now() - _cmdStart}ms`);
} finally {
const _shutStart = Date.now();
stopOtelReceiver();
await stopOtelReceiver().catch(() => {});
await shutdownDb().catch(() => {});
if (process.env.GENIE_PROFILE_DB) console.error(`[profile] shutdown=${Date.now() - _shutStart}ms`);
}
Expand Down
24 changes: 15 additions & 9 deletions src/lib/otel-receiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ describe('otel-receiver', () => {
process.env.GENIE_OTEL_PORT = String(57000 + Math.floor(Math.random() * 7000));
});

afterEach(() => {
stopOtelReceiver();
afterEach(async () => {
await stopOtelReceiver();
if (origPort !== undefined) process.env.GENIE_OTEL_PORT = origPort;
else process.env.GENIE_OTEL_PORT = undefined;
});
Expand Down Expand Up @@ -41,15 +41,17 @@ describe('otel-receiver', () => {
});

test('stopOtelReceiver stops the server', async () => {
await startOtelReceiver();
const started = await startOtelReceiver();
expect(started).toBe(true);
expect(isOtelReceiverRunning()).toBe(true);

stopOtelReceiver();
await stopOtelReceiver();
expect(isOtelReceiverRunning()).toBe(false);
});

test('POST /v1/logs returns 200', async () => {
await startOtelReceiver();
const started = await startOtelReceiver();
expect(started).toBe(true);
const port = getOtelPort();

const res = await fetch(`http://127.0.0.1:${port}/v1/logs`, {
Expand Down Expand Up @@ -83,7 +85,8 @@ describe('otel-receiver', () => {
});

test('POST /v1/metrics returns 200', async () => {
await startOtelReceiver();
const started = await startOtelReceiver();
expect(started).toBe(true);
const port = getOtelPort();

const res = await fetch(`http://127.0.0.1:${port}/v1/metrics`, {
Expand Down Expand Up @@ -116,7 +119,8 @@ describe('otel-receiver', () => {
});

test('POST /v1/logs handles empty payload', async () => {
await startOtelReceiver();
const started = await startOtelReceiver();
expect(started).toBe(true);
const port = getOtelPort();

const res = await fetch(`http://127.0.0.1:${port}/v1/logs`, {
Expand All @@ -129,7 +133,8 @@ describe('otel-receiver', () => {
});

test('GET /health returns ok', async () => {
await startOtelReceiver();
const started = await startOtelReceiver();
expect(started).toBe(true);
const port = getOtelPort();

const res = await fetch(`http://127.0.0.1:${port}/health`);
Expand All @@ -139,7 +144,8 @@ describe('otel-receiver', () => {
});

test('unknown route returns 404', async () => {
await startOtelReceiver();
const started = await startOtelReceiver();
expect(started).toBe(true);
const port = getOtelPort();

const res = await fetch(`http://127.0.0.1:${port}/v1/unknown`, {
Expand Down
11 changes: 9 additions & 2 deletions src/lib/otel-receiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,17 @@ export async function startOtelReceiver(): Promise<boolean> {

/**
* Stop the OTel receiver. Used in tests and graceful shutdown.
*
* Awaits `server.stop(true)` so the listening port is fully released before
* the next start attempt. Without the await, back-to-back start/stop cycles
* in tests (afterEach → next beforeEach) could race on the TCP port and
* produce EADDRINUSE when parallel tests randomly collide within the
* 7000-port window (57000-63999), which caused the intermittent push-event
* CI failure on `POST /v1/logs handles empty payload`.
*/
export function stopOtelReceiver(): void {
export async function stopOtelReceiver(): Promise<void> {
if (server) {
server.stop();
await server.stop(true);
server = null;
}
}
Comment on lines +381 to 386

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a potential race condition in the current implementation of stopOtelReceiver. Because server = null is executed after the await, any calls to isOtelReceiverRunning() or startOtelReceiver() that occur while the stop operation is pending will see the server as still active. This could lead to situations where the system incorrectly assumes the receiver is running when it is actually being shut down.

It is safer to capture the server reference and null the module-level variable immediately before awaiting the stop operation. This also makes the function naturally idempotent if called multiple times concurrently.

Suggested change
export async function stopOtelReceiver(): Promise<void> {
if (server) {
server.stop();
await server.stop(true);
server = null;
}
}
export async function stopOtelReceiver(): Promise<void> {
const s = server;
if (s) {
server = null;
await s.stop(true);
}
}

Expand Down
Loading