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
5 changes: 5 additions & 0 deletions .changeset/cache-step-port-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/core': patch
---

Cache self-hosted step port discovery per process.
6 changes: 3 additions & 3 deletions packages/core/src/runtime/get-port-lazy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getPort } from '@workflow/utils/get-port';

// Per-process cache of the resolved port. The workflow dev server listens on a
// Per-process cache of the resolved port. The workflow server listens on a
// stable port for the lifetime of the process, but `getPort()` rediscovers it
// on every call by querying the OS for the process's listening sockets — on
// macOS that shells out to `lsof` (~60ms), which the runtime pays on EVERY
// workflow replay (the non-Vercel branch of `runWorkflow`). Since the port does
// not change within a process, resolve it once and reuse it. `_inFlight`
// workflow replay or step invocation. Since the port does not change within a
// process, resolve it once and reuse it. `_inFlight`
// dedupes concurrent first calls so discovery never runs more than once.
//
// The first concrete port is pinned for the lifetime of the process — there is
Expand Down
29 changes: 23 additions & 6 deletions packages/core/src/runtime/step-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,23 +134,33 @@ vi.mock('../private.js', () => ({
getStepFunction: vi.fn().mockReturnValue(mockStepFn),
}));

// Mock get-port
vi.mock('@workflow/utils/get-port', () => ({
getPort: vi.fn().mockResolvedValue(3000),
}));

// Import the module AFTER all mocks are set up - this triggers createQueueHandler
// which populates capturedHandlerRef
import './step-handler.js';
import { MAX_QUEUE_DELIVERIES } from './constants.js';
import { getStepFunction } from '../private.js';
import {
getErrorName,
getErrorStack,
normalizeUnknownError,
} from '../types.js';
import { MAX_QUEUE_DELIVERIES } from './constants.js';
import {
resetPortCacheForTesting,
setPortResolverForTesting,
} from './get-port-lazy.js';
import { getWorld } from './world.js';

const mockPortResolver = vi.fn(async () => 3000);

beforeEach(() => {
mockPortResolver.mockReset().mockResolvedValue(3000);
setPortResolverForTesting(mockPortResolver);
});

afterEach(() => {
resetPortCacheForTesting();
});

function capturedHandler(
message: unknown,
metadata: { queueName: string; messageId: string; attempt: number }
Expand Down Expand Up @@ -228,6 +238,13 @@ describe('step-handler 409 handling', () => {
vi.restoreAllMocks();
});

it('reuses cached port discovery across step invocations', async () => {
await capturedHandler(createMessage(), createMetadata('myStep'));
await capturedHandler(createMessage(), createMetadata('myStep'));

expect(mockPortResolver).toHaveBeenCalledOnce();
});

describe('step_completed 409', () => {
it('should warn and return when step_completed gets a 409', async () => {
// step_started succeeds, step function succeeds, step_completed returns 409
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/runtime/step-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
WorkflowWorldError,
} from '@workflow/errors';
import { createWorkflowBaseUrl, pluralize } from '@workflow/utils';
import { getPort } from '@workflow/utils/get-port';
import {
getQueueTopicPrefix,
resolveQueueNamespace,
Expand Down Expand Up @@ -41,6 +40,7 @@ import {
} from '../types.js';

import { MAX_QUEUE_DELIVERIES } from './constants.js';
import { getPortLazy } from './get-port-lazy.js';
import {
getQueueOverhead,
getWorkflowQueueName,
Expand Down Expand Up @@ -158,7 +158,7 @@ const stepHandler = createQueueHandler(

// Resolve local async values concurrently before entering the trace span
const [port, spanKind] = await Promise.all([
isVercel ? undefined : getPort(),
isVercel ? undefined : getPortLazy(),
getSpanKind('CONSUMER'),
]);

Expand Down
3 changes: 3 additions & 0 deletions packages/world-postgres/src/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ vi.mock('graphile-worker', () => ({

vi.mock('@workflow/utils/get-port', () => ({
getWorkflowPort: vi.fn(),
// stepEntrypoint (imported from the core dist bundle below) calls
// getPortLazy(), which imports getPort from this same module.
getPort: vi.fn().mockResolvedValue(undefined),
}));

vi.mock('@workflow/world-local', async (importOriginal) => {
Expand Down
Loading