Skip to content

Commit 0b6ae12

Browse files
authored
Use Redis for the communication between Server and Engine (#1172)
Part of OPS-2396. Additional Notes - Save the requests to the engine in cache - Save the responses from the engine in cache - Save the run progress requests in cache
1 parent 8bcc250 commit 0b6ae12

8 files changed

Lines changed: 150 additions & 69 deletions

File tree

packages/engine/src/api-handler.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
12
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
23
import {
34
blocksBuilder,
5+
BodyAccessKeyRequest,
6+
bodyConverterModule,
47
logger,
58
MAX_REQUEST_BODY_BYTES,
69
runWithLogContext,
@@ -25,35 +28,38 @@ const engineController: FastifyPluginAsyncTypebox = async (fastify) => {
2528
},
2629
},
2730
async (request, reply) => {
28-
const requestIdHeaders = request.headers;
29-
const requestId = Array.isArray(requestIdHeaders)
30-
? requestIdHeaders[0]
31-
: requestIdHeaders;
32-
31+
const requestBody = request.body;
3332
await runWithLogContext(
3433
{
3534
deadlineTimestamp: request.body.deadlineTimestamp.toString(),
36-
requestId: requestId ?? request.id,
37-
operationType: request.body.operationType,
35+
operationType: requestBody.operationType,
36+
requestId: requestBody.requestId,
3837
},
39-
() => handleRequest(request, reply),
38+
() => handleRequest(reply, requestBody),
4039
);
4140
},
4241
);
4342
};
4443

45-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
46-
async function handleRequest(request: any, reply: any): Promise<void> {
44+
async function handleRequest(
45+
reply: any,
46+
requestBody: EngineRequest,
47+
): Promise<void> {
4748
try {
48-
const { engineInput, operationType } = request.body as EngineRequest;
49-
49+
const { engineInput, operationType } = requestBody;
5050
logger.info(`Received request for operation [${operationType}]`, {
5151
operationType,
5252
});
5353

54-
const result = await executeEngine(engineInput, operationType);
54+
const bodyAccessKey = await executeEngine(
55+
requestBody.requestId,
56+
engineInput,
57+
operationType,
58+
);
5559

56-
await reply.status(StatusCodes.OK).send(result);
60+
await reply.status(StatusCodes.OK).send({
61+
bodyAccessKey,
62+
} as BodyAccessKeyRequest);
5763
} catch (error) {
5864
logger.error('Engine execution failed.', { error });
5965

@@ -73,6 +79,9 @@ export const start = async (): Promise<void> => {
7379
setStopHandlers(app);
7480

7581
await blocksBuilder();
82+
83+
await app.register(bodyConverterModule);
84+
7685
await app.register(engineController);
7786

7887
await app.listen({

packages/engine/src/engine-executor.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import {
22
encryptionKeyInitializer,
33
logger,
44
networkUtls,
5+
saveRequestBody,
56
} from '@openops/server-shared';
6-
import { EngineOperationType, EngineResponse } from '@openops/shared';
7+
import { EngineOperationType } from '@openops/shared';
78
import { execute } from './lib/operations';
89

910
export async function executeEngine(
11+
requestId: string,
1012
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1113
engineInput: any,
1214
operationType: EngineOperationType,
13-
): Promise<EngineResponse<unknown>> {
15+
): Promise<string> {
1416
const startTime = performance.now();
1517

1618
await encryptionKeyInitializer();
@@ -23,6 +25,8 @@ export async function executeEngine(
2325

2426
const duration = Math.floor(performance.now() - startTime);
2527

28+
const key = await saveRequestBody(requestId, result);
29+
2630
logger.info(
2731
`Finished operation [${operationType}] with status [${result.status}] in ${duration}ms`,
2832
{
@@ -31,5 +35,5 @@ export async function executeEngine(
3135
},
3236
);
3337

34-
return result;
38+
return key;
3539
}

packages/engine/src/lambda-handler.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { logger, runWithLogContext, sendLogs } from '@openops/server-shared';
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import {
3+
BodyAccessKeyRequest,
4+
getRequestBody,
5+
logger,
6+
runWithLogContext,
7+
sendLogs,
8+
} from '@openops/server-shared';
29
import { EngineResponseStatus } from '@openops/shared';
310
import { APIGatewayEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
4-
import { nanoid } from 'nanoid';
511
import { executeEngine } from './engine-executor';
612
import { EngineRequest } from './main';
713

@@ -19,41 +25,55 @@ export async function lambdaHandler(
1925
};
2026
}
2127

22-
const data = await parseJson<EngineRequest>(event.body);
28+
const data = await parseJson<BodyAccessKeyRequest>(event.body);
29+
30+
let requestBody: EngineRequest;
31+
try {
32+
requestBody = await getRequestBody<EngineRequest>(data.bodyAccessKey);
33+
} catch (error) {
34+
return {
35+
statusCode: 400,
36+
body: JSON.stringify({
37+
status: EngineResponseStatus.ERROR,
38+
message: (error as Error).message,
39+
}),
40+
};
41+
}
2342

2443
return runWithLogContext<APIGatewayProxyResult | undefined>(
2544
{
26-
requestId: event.headers.requestId ?? nanoid(),
45+
deadlineTimestamp: requestBody.deadlineTimestamp.toString(),
46+
operationType: requestBody.operationType,
2747
awsRequestId: context.awsRequestId,
28-
operationType: data.operationType,
48+
requestId: requestBody.requestId,
2949
},
30-
() => handleEvent(data),
50+
() => handleEvent(requestBody),
3151
);
3252
}
3353

3454
async function handleEvent(
35-
data: EngineRequest,
55+
requestBody: EngineRequest,
3656
): Promise<APIGatewayProxyResult | undefined> {
3757
try {
38-
logger.info('Request received in the engine.');
58+
const { requestId, engineInput, operationType } = requestBody;
3959

40-
if (!data.engineInput) {
41-
return {
42-
statusCode: 400,
43-
body: JSON.stringify({
44-
status: EngineResponseStatus.ERROR,
45-
message: 'The received input is not valid.',
46-
}),
47-
};
48-
}
60+
logger.info(`Received request for operation [${operationType}]`, {
61+
operationType,
62+
});
4963

50-
const result = await executeEngine(data.engineInput, data.operationType);
64+
const bodyAccessKey = await executeEngine(
65+
requestId,
66+
engineInput,
67+
operationType,
68+
);
5169

5270
await sendLogs();
5371

5472
return {
5573
statusCode: 200,
56-
body: JSON.stringify(result),
74+
body: JSON.stringify({
75+
bodyAccessKey,
76+
} as BodyAccessKeyRequest),
5777
};
5878
} catch (error) {
5979
logger.error('Engine execution failed.', { error });

packages/engine/src/lib/services/progress.service.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { makeHttpRequest } from '@openops/common';
2-
import { hashUtils, logger } from '@openops/server-shared';
2+
import {
3+
BodyAccessKeyRequest,
4+
hashUtils,
5+
logger,
6+
saveRequestBody,
7+
} from '@openops/server-shared';
38
import { UpdateRunProgressRequest } from '@openops/shared';
49
import { Mutex } from 'async-mutex';
510
import { AxiosHeaders } from 'axios';
@@ -53,14 +58,18 @@ const sendUpdateRunRequest = async (
5358
lastRequestHash = requestHash;
5459

5560
try {
61+
const resourceKey = await saveRequestBody(request.runId, request);
62+
5663
await makeHttpRequest(
5764
'POST',
5865
url.toString(),
5966
new AxiosHeaders({
6067
'Content-Type': 'application/json',
6168
Authorization: `Bearer ${engineConstants.engineToken}`,
6269
}),
63-
request,
70+
{
71+
bodyAccessKey: resourceKey,
72+
} as BodyAccessKeyRequest,
6473
{
6574
retries: MAX_RETRIES,
6675
retryDelay: (retryCount: number) => {

packages/engine/src/main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { lambdaHandler } from './lambda-handler';
99
import { EngineConstants } from './lib/handler/context/engine-constants';
1010

1111
export const EngineRequest = Type.Object({
12-
operationType: Type.Enum(EngineOperationType),
13-
engineInput: Type.Object({}),
12+
requestId: Type.String(),
13+
engineInput: Type.Unknown(),
1414
deadlineTimestamp: Type.Number(),
15+
operationType: Type.Enum(EngineOperationType),
1516
});
1617

1718
export type EngineRequest = Static<typeof EngineRequest>;

packages/engine/test/services/progress.service.test.ts

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
const saveRequestBodyMock = jest.fn();
12
import { progressService } from '../../src/lib/services/progress.service';
2-
import { logger } from '@openops/server-shared';
33
import { throwIfExecutionTimeExceeded } from '../../src/lib/timeout-validator';
44

55
jest.mock('@openops/server-shared', () => ({
66
...jest.requireActual('@openops/server-shared'),
7+
saveRequestBody: saveRequestBodyMock,
78
logger: {
89
debug: jest.fn(),
910
error: jest.fn(),
@@ -44,12 +45,12 @@ describe('Progress Service', () => {
4445

4546
beforeEach(() => {
4647
jest.clearAllMocks();
47-
48+
4849
// Reset the timeout mock to not throw by default
4950
mockThrowIfExecutionTimeExceeded.mockReset();
50-
51+
5152
mockMakeHttpRequest.mockResolvedValue({});
52-
53+
5354
// Reset the global lastRequestHash by calling with unique params
5455
// This ensures no deduplication issues between tests
5556
jest.clearAllMocks();
@@ -69,18 +70,23 @@ describe('Progress Service', () => {
6970
},
7071
};
7172

73+
saveRequestBodyMock.mockResolvedValue('request:test-run-id');
7274
await progressService.sendUpdate(successParams);
7375

76+
expect(saveRequestBodyMock).toHaveBeenCalledWith('test-run-id', expect.objectContaining({
77+
executionCorrelationId: 'test-correlation-id-success',
78+
runId: 'test-run-id',
79+
workerHandlerId: 'test-handler-id',
80+
progressUpdateType: 'WEBHOOK_RESPONSE',
81+
}));
82+
7483
expect(successParams.flowExecutorContext.toResponse).toHaveBeenCalled();
7584
expect(mockMakeHttpRequest).toHaveBeenCalledWith(
7685
'POST',
7786
'http://localhost:3000/v1/engine/update-run',
7887
expect.any(Object),
7988
expect.objectContaining({
80-
executionCorrelationId: 'test-correlation-id-success',
81-
runId: 'test-run-id',
82-
workerHandlerId: 'test-handler-id',
83-
progressUpdateType: 'WEBHOOK_RESPONSE',
89+
bodyAccessKey: 'request:test-run-id',
8490
}),
8591
expect.objectContaining({
8692
retries: 3,
@@ -98,8 +104,22 @@ describe('Progress Service', () => {
98104
},
99105
};
100106

107+
saveRequestBodyMock.mockResolvedValue('request:test-run-id');
101108
await progressService.sendUpdate(uniqueParams);
102109

110+
expect(saveRequestBodyMock).toHaveBeenCalledWith('test-run-id', expect.objectContaining({
111+
executionCorrelationId: 'test-correlation-id-payload',
112+
runId: 'test-run-id',
113+
workerHandlerId: 'test-handler-id',
114+
progressUpdateType: 'WEBHOOK_RESPONSE',
115+
runDetails: expect.objectContaining({
116+
steps: {},
117+
status: 'RUNNING',
118+
duration: 1000,
119+
tasks: 1,
120+
}),
121+
}));
122+
103123
expect(mockMakeHttpRequest).toHaveBeenCalledTimes(1);
104124
const call = mockMakeHttpRequest.mock.calls[0];
105125
const [method, url, _, requestBody] = call;
@@ -108,16 +128,7 @@ describe('Progress Service', () => {
108128
expect(url).toBe('http://localhost:3000/v1/engine/update-run');
109129
expect(requestBody).toEqual(
110130
expect.objectContaining({
111-
executionCorrelationId: 'test-correlation-id-payload',
112-
runId: 'test-run-id',
113-
workerHandlerId: 'test-handler-id',
114-
progressUpdateType: 'WEBHOOK_RESPONSE',
115-
runDetails: expect.objectContaining({
116-
steps: {},
117-
status: 'RUNNING',
118-
duration: 1000,
119-
tasks: 1,
120-
}),
131+
bodyAccessKey: 'request:test-run-id',
121132
})
122133
);
123134
});
@@ -132,13 +143,30 @@ describe('Progress Service', () => {
132143
},
133144
};
134145

146+
saveRequestBodyMock.mockResolvedValue('request:test-run-id');
135147
await progressService.sendUpdate(paramsWithoutHandlerId);
136148

149+
expect(saveRequestBodyMock).toHaveBeenCalledWith('test-run-id', expect.objectContaining({
150+
executionCorrelationId: 'test-correlation-id-null-handler',
151+
runId: 'test-run-id',
152+
workerHandlerId: null,
153+
progressUpdateType: 'WEBHOOK_RESPONSE',
154+
runDetails: expect.objectContaining({
155+
steps: {},
156+
status: 'RUNNING',
157+
duration: 1000,
158+
tasks: 1,
159+
}),
160+
}));
161+
137162
expect(mockMakeHttpRequest).toHaveBeenCalledTimes(1);
138163
const call = mockMakeHttpRequest.mock.calls[0];
139164
const [_, __, ___, requestBody] = call;
140-
141-
expect(requestBody.workerHandlerId).toBe(null);
165+
166+
expect(requestBody).toEqual({
167+
bodyAccessKey: 'request:test-run-id',
168+
}
169+
);
142170
});
143171

144172
it('should deduplicate identical requests based on hash', async () => {
@@ -327,10 +355,10 @@ describe('Progress Service', () => {
327355
// Test the retry delay function
328356
const call = mockMakeHttpRequest.mock.calls[0];
329357
const [_, __, ___, ____, options] = call;
330-
358+
331359
expect(options.retryDelay(0)).toBe(200); // 1st retry: 200ms
332360
expect(options.retryDelay(1)).toBe(400); // 2nd retry: 400ms
333361
expect(options.retryDelay(2)).toBe(600); // 3rd retry: 600ms
334362
});
335363
});
336-
});
364+
});

0 commit comments

Comments
 (0)