Skip to content

Commit b04acab

Browse files
authored
feat(engine): Let the control plane mint the engine 2.0 execution id (#37224)
1 parent 1f19b11 commit b04acab

26 files changed

Lines changed: 225 additions & 93 deletions

packages/@n8n/constants/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './execution';
77
export * from './logstreaming';
88
export * from './nodes';
99
export * from './scheduler';
10+
export * from './uuid';
1011

1112
export const LICENSE_FEATURES = {
1213
SHARING: 'feat:sharing',
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Matches a uuidv7: version `7` in the third group, RFC 4122 variant in the
3+
* fourth. Time-ordered ids are validated against this on the wire.
4+
*/
5+
export const UUID_V7_PATTERN =
6+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

packages/@n8n/engine/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
],
2626
"dependencies": {
2727
"@n8n/config": "workspace:*",
28+
"@n8n/constants": "workspace:*",
2829
"@n8n/di": "workspace:*",
2930
"@n8n/typeorm": "workspace:*",
3031
"express": "catalog:",

packages/@n8n/engine/src/database/__tests__/workflow-execution.integration.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ExecutionNotFoundError } from '../../execution/execution-store';
77
import { createDataSource } from '../data-source';
88
import { WorkflowExecution } from '../entities/workflow-execution.entity';
99
import { WorkflowStepExecution } from '../entities/workflow-step-execution.entity';
10+
import { generateId } from '../generate-id';
1011
import { TypeOrmExecutionViewStore } from '../typeorm-execution-view-store';
1112

1213
describe('workflow_execution table (integration)', () => {
@@ -29,6 +30,7 @@ describe('workflow_execution table (integration)', () => {
2930
const repo = dataSource.getRepository(WorkflowExecution);
3031

3132
const created = repo.create({
33+
id: generateId(),
3234
workflowId: 'wf-1',
3335
status: 'running',
3436
mode: 'production',
@@ -57,6 +59,7 @@ describe('workflow_execution table (integration)', () => {
5759
const repo = dataSource.getRepository(WorkflowExecution);
5860
const finishedAt = new Date();
5961
const created = repo.create({
62+
id: generateId(),
6063
workflowId: 'wf-3',
6164
status: 'completed',
6265
mode: 'manual',
@@ -100,6 +103,7 @@ describe('workflow_execution table (integration)', () => {
100103

101104
await repo.save(
102105
repo.create({
106+
id: generateId(),
103107
workflowId: 'wf-2',
104108
status: 'running',
105109
mode: 'production',
@@ -110,6 +114,7 @@ describe('workflow_execution table (integration)', () => {
110114
);
111115
await repo.save(
112116
repo.create({
117+
id: generateId(),
113118
workflowId: 'wf-2',
114119
status: 'completed',
115120
mode: 'production',

packages/@n8n/engine/src/database/__tests__/workflow-step-execution.integration.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { StepNotFoundError, type NewStepRecord } from '../../execution/step-stor
88
import { createDataSource } from '../data-source';
99
import { WorkflowExecution } from '../entities/workflow-execution.entity';
1010
import { WorkflowStepExecution } from '../entities/workflow-step-execution.entity';
11+
import { generateId } from '../generate-id';
1112
import { TypeOrmExecutionViewStore } from '../typeorm-execution-view-store';
1213
import { TypeOrmStepStore } from '../typeorm-step-store';
1314

@@ -39,6 +40,7 @@ describe('workflow_step_execution table (integration)', () => {
3940
async function createExecution(): Promise<string> {
4041
const repo = dataSource.getRepository(WorkflowExecution);
4142
const execution = repo.create({
43+
id: generateId(),
4244
workflowId: 'wf-1',
4345
status: 'running',
4446
mode: 'production',

packages/@n8n/engine/src/database/entities/workflow-execution.entity.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
BeforeInsert,
32
Column,
43
CreateDateColumn,
54
Entity,
@@ -14,7 +13,6 @@ import type {
1413
TriggerOutputs,
1514
} from '../../execution/execution.types';
1615
import type { WorkflowGraph } from '../../graph';
17-
import { generateId } from '../generate-id';
1816

1917
@Entity('workflow_execution')
2018
@Index('idx_workflow_execution_workflow_id', ['workflowId'])
@@ -46,9 +44,4 @@ export class WorkflowExecution {
4644

4745
@Column({ name: 'finished_at', type: 'timestamptz', nullable: true, precision: 3 })
4846
finishedAt!: Date | null;
49-
50-
@BeforeInsert()
51-
setId(): void {
52-
if (!this.id) this.id = generateId();
53-
}
5447
}

packages/@n8n/engine/src/database/typeorm-execution-store.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ type InsertValues = Parameters<Repository<WorkflowExecution>['insert']>[0];
1919
export class TypeOrmExecutionStore implements ExecutionStore {
2020
constructor(private readonly repo: Repository<WorkflowExecution>) {}
2121

22-
async createExecution(record: NewExecutionRecord): Promise<{ id: string }> {
22+
async createExecution(record: NewExecutionRecord): Promise<void> {
2323
const execution = this.repo.create({ ...record, finishedAt: null });
2424
// The cast is needed because the insert payload type recurses into the
2525
// opaque `graph` jsonb and rejects `StepConfig`'s deliberate `unknown`.
2626
// NOTE: prefer insert to save for performance reasons.
2727
await this.repo.insert(execution as InsertValues);
28-
return { id: execution.id };
2928
}
3029

3130
async loadExecution(id: string): Promise<ExecutionRecord> {

packages/@n8n/engine/src/execution/__tests__/execution-start.integration.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
WorkflowExecution,
1212
WorkflowStepExecution,
1313
} from '../../database';
14+
import { generateId } from '../../database/generate-id';
1415
import type { WorkflowGraph } from '../../graph';
1516
import { noopLifecycleEventPublisher } from '../../lifecycle-events';
1617
import {
@@ -97,6 +98,7 @@ describe('execution start (integration)', () => {
9798
workflowId: 'wf-1',
9899
graph,
99100
triggerOutputs: [[{ json: { hello: 'world' } }]],
101+
executionId: generateId(),
100102
});
101103
await ready;
102104

@@ -134,7 +136,9 @@ describe('execution start (integration)', () => {
134136
noopLifecycleEventPublisher,
135137
);
136138

137-
const { id: executionId } = await executionStore.createExecution({
139+
const executionId = generateId();
140+
await executionStore.createExecution({
141+
id: executionId,
138142
workflowId: 'wf-2',
139143
status: 'queued',
140144
mode: 'production',

packages/@n8n/engine/src/execution/__tests__/start-execution.service.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function makeQueue(): WorkQueue<OrchestrationMessage> {
1717

1818
function makeStore(overrides: Partial<ExecutionStore> = {}): ExecutionStore {
1919
return {
20-
createExecution: vi.fn().mockResolvedValue({ id: 'exec-id-1' }),
20+
createExecution: vi.fn(),
2121
loadExecution: vi.fn(),
2222
transitionStatus: vi.fn().mockResolvedValue(true),
2323
finishExecution: vi.fn().mockResolvedValue(true),
@@ -26,7 +26,7 @@ function makeStore(overrides: Partial<ExecutionStore> = {}): ExecutionStore {
2626
}
2727

2828
describe('StartExecutionService', () => {
29-
it('admits, persists a queued execution, publishes execution:enqueued, returns id', async () => {
29+
it('admits, persists a queued execution under the caller-minted id, publishes execution:enqueued', async () => {
3030
const admittance: AdmittanceService = {
3131
evaluate: vi.fn().mockResolvedValue({ accept: true }),
3232
};
@@ -38,11 +38,13 @@ describe('StartExecutionService', () => {
3838
workflowId: 'wf-1',
3939
graph: sampleGraph,
4040
triggerOutputs: [[{ json: { hello: 'world' } }]],
41+
executionId: 'exec-id-1',
4142
});
4243

4344
expect(result.executionId).toBe('exec-id-1');
4445
expect(admittance.evaluate).toHaveBeenCalledWith({ workflowId: 'wf-1' });
4546
expect(store.createExecution).toHaveBeenCalledWith({
47+
id: 'exec-id-1',
4648
workflowId: 'wf-1',
4749
status: 'queued',
4850
mode: 'production',
@@ -63,7 +65,7 @@ describe('StartExecutionService', () => {
6365
const queue = makeQueue();
6466
const service = new StartExecutionService(admittance, store, queue);
6567

66-
await service.start({ workflowId: 'wf-1', graph: sampleGraph });
68+
await service.start({ workflowId: 'wf-1', graph: sampleGraph, executionId: 'exec-id-1' });
6769

6870
expect(store.createExecution).toHaveBeenCalledWith(
6971
expect.objectContaining({ mode: 'production', triggerOutputs: null }),
@@ -77,7 +79,7 @@ describe('StartExecutionService', () => {
7779
const validateGraph = vi.fn();
7880
const service = new StartExecutionService(admittance, makeStore(), makeQueue(), validateGraph);
7981

80-
await service.start({ workflowId: 'wf-1', graph: sampleGraph });
82+
await service.start({ workflowId: 'wf-1', graph: sampleGraph, executionId: 'exec-id-1' });
8183

8284
expect(validateGraph).toHaveBeenCalledExactlyOnceWith(sampleGraph);
8385
});
@@ -94,7 +96,9 @@ describe('StartExecutionService', () => {
9496
});
9597
const service = new StartExecutionService(admittance, store, queue, validateGraph);
9698

97-
await expect(service.start({ workflowId: 'wf-1', graph: sampleGraph })).rejects.toBe(rejection);
99+
await expect(
100+
service.start({ workflowId: 'wf-1', graph: sampleGraph, executionId: 'exec-id-1' }),
101+
).rejects.toBe(rejection);
98102

99103
expect(store.createExecution).not.toHaveBeenCalled();
100104
expect(queue.publish).not.toHaveBeenCalled();
@@ -108,9 +112,9 @@ describe('StartExecutionService', () => {
108112
const queue = makeQueue();
109113
const service = new StartExecutionService(admittance, store, queue);
110114

111-
await expect(service.start({ workflowId: 'wf-1', graph: sampleGraph })).rejects.toBeInstanceOf(
112-
AdmittanceRejectedError,
113-
);
115+
await expect(
116+
service.start({ workflowId: 'wf-1', graph: sampleGraph, executionId: 'exec-id-1' }),
117+
).rejects.toBeInstanceOf(AdmittanceRejectedError);
114118

115119
expect(store.createExecution).not.toHaveBeenCalled();
116120
expect(queue.publish).not.toHaveBeenCalled();

packages/@n8n/engine/src/execution/__tests__/step-execution.integration.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
WorkflowExecution,
1313
WorkflowStepExecution,
1414
} from '../../database';
15+
import { generateId } from '../../database/generate-id';
1516
import type { IStepExecutor, StepExecutionRequest } from '../../dependencies';
1617
import type { WorkflowGraph } from '../../graph';
1718
import { noopLifecycleEventPublisher } from '../../lifecycle-events';
@@ -101,7 +102,7 @@ describe('step execution (integration)', () => {
101102
const response = await request(runtime.app)
102103
.post('/api/workflow-executions')
103104
.set(authHeader())
104-
.send({ workflowId, graph: workflowGraph, triggerOutputs })
105+
.send({ workflowId, graph: workflowGraph, triggerOutputs, executionId: generateId() })
105106
.expect(201);
106107
const { executionId } = response.body as StartExecutionResult;
107108
await finished;
@@ -381,7 +382,9 @@ describe('step execution (integration)', () => {
381382
noopLifecycleEventPublisher,
382383
);
383384

384-
const { id: executionId } = await executionStore.createExecution({
385+
const executionId = generateId();
386+
await executionStore.createExecution({
387+
id: executionId,
385388
workflowId: 'wf-2',
386389
status: 'running',
387390
mode: 'production',

0 commit comments

Comments
 (0)