Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

/**
* Milestone 7 (Kickoff doc): two targeted resilience checks for the
* documented, accepted v1 limitations of running backend functions
* in-process rather than in an isolated child process/thread — see the
* RFC's "Decisions and Trade-Offs" section. These don't fix anything; they
* empirically confirm the actual failure modes, which is what the milestone
* asks for before deciding whether closing them (real process/thread
* isolation) is worth pursuing.
*/

import { mockLogger, moduleResolverFor } from '@dd/tests/_jest/helpers/mocks';
import { spawnSync } from 'child_process';

import type { BackendFunction } from '../backend/types';

import type { ExecuteAction } from './local-execution';
import { executeScriptLocally } from './local-execution';

const func: BackendFunction = {
relativePath: 'src/example',
name: 'example',
absolutePath: '/src/example.backend.ts',
allowedConnectionIds: [],
};

const stubExecuteAction: ExecuteAction = async (fqn) => ({ data: null, stub: true, fqn });

describe('local-execution resilience (Milestone 7)', () => {
// A real `while (true) {}` would hang this test (and the whole Jest
// worker) forever, since nothing — including the timeout's own
// setTimeout callback — can run while the event loop is synchronously
// blocked. A bounded, time-boxed busy-wait demonstrates the exact same
// mechanism without actually hanging: if the 20ms timeout could
// interrupt a synchronous loop, this would settle around 20ms with a
// timeout rejection; instead it can only settle once the loop itself
// finishes on its own, ~200ms later, with the loop's real result.
test('Should NOT interrupt a synchronous CPU-bound loop with the current timeout — known, accepted v1 limitation', async () => {
const start = Date.now();

const result = await executeScriptLocally(
func,
[],
stubExecuteAction,
moduleResolverFor(func, {
example: () => {
const deadline = Date.now() + 200;
// eslint-disable-next-line no-empty
while (Date.now() < deadline) {}
return 'loop finished on its own';
},
}),
mockLogger,
20,
);

const elapsedMs = Date.now() - start;

expect(result).toEqual({ data: 'loop finished on its own' });
expect(elapsedMs).toBeGreaterThanOrEqual(150);
});

// process.exit() can't be run inside this same Jest process — it would
// actually terminate the test runner. Spawning a real child process is
// the only safe way to observe what it does, and it directly tests the
// relevant claim: does try/finally around the customer's function call
// (the same shape runScriptLocally uses to run cleanup unconditionally)
// offer any protection against it? It doesn't — process.exit() is
// immediate and unconditional at the OS level, so no JS-level exception
// handling in this in-process design can intercept it. A customer
// function calling process.exit() takes the whole dev server down with
// it, not just its own execution.
test('Should confirm process.exit() inside the customer function crashes the whole process, bypassing try/finally cleanup — known, real risk, not a safely-contained failure', () => {
const script = `
async function customerFunction() {
process.exit(7);
}
(async () => {
try {
await customerFunction();
} finally {
console.log('CLEANUP_RAN');
}
})();
`;

const result = spawnSync(process.execPath, ['-e', script]);

expect(result.status).toBe(7);
expect(result.stdout.toString()).not.toContain('CLEANUP_RAN');
});
});
Loading