Skip to content

Commit 6bb6a46

Browse files
refactor(vefaas): wait for Active in createFunction + update/delete waits
veFaaS GetFunction response carries Status (CLI already reads it); ready value is 'Active'. createFunction now polls until Active, update waits for Active first (update-before-active race), delete waits for the function to be gone (delete-then-recreate race). Mirrors SCF/FC3 pattern. Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 99a31e1 commit 6bb6a46

2 files changed

Lines changed: 86 additions & 6 deletions

File tree

src/common/volcengineClient/vefaasOperations.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Service } from '@volcengine/openapi';
22
import type { VefaasFunctionConfig, VefaasFunctionInfo } from './types';
33
import { logger } from '../../common/logger';
44
import { lang } from '../../lang';
5+
import { pollUntil, PollingTimeoutError } from '../polling';
6+
import { SCF_STATUS_POLL_INTERVAL_MS, SCF_STATUS_POLL_MAX_ATTEMPTS } from '../constants';
57
import * as fs from 'node:fs';
68

79
type VefaasSdkClient = Service;
@@ -132,7 +134,48 @@ export const createVefaasOperations = (client: VefaasSdkClient) => {
132134
region: (client as unknown as { region: string }).region,
133135
});
134136

135-
return {
137+
const operations = {
138+
waitForFunctionActive: async (functionName: string): Promise<VefaasFunctionInfo | null> => {
139+
try {
140+
return await pollUntil({
141+
description: `veFaaS function ${functionName} to become Active`,
142+
fetch: () => operations.getFunction(functionName),
143+
isDone: (info) => info?.status === 'Active',
144+
intervalMs: SCF_STATUS_POLL_INTERVAL_MS,
145+
maxAttempts: SCF_STATUS_POLL_MAX_ATTEMPTS,
146+
});
147+
} catch (e) {
148+
if (e instanceof PollingTimeoutError) {
149+
throw new Error(
150+
`Timed out waiting for veFaaS function ${functionName} to become Active`,
151+
{
152+
cause: e,
153+
},
154+
);
155+
}
156+
throw e;
157+
}
158+
},
159+
160+
waitForFunctionDeleted: async (functionName: string): Promise<void> => {
161+
try {
162+
await pollUntil({
163+
description: `veFaaS function ${functionName} to be deleted`,
164+
fetch: () => operations.getFunction(functionName),
165+
isDone: (info) => info === null,
166+
intervalMs: SCF_STATUS_POLL_INTERVAL_MS,
167+
maxAttempts: SCF_STATUS_POLL_MAX_ATTEMPTS,
168+
});
169+
} catch (e) {
170+
if (e instanceof PollingTimeoutError) {
171+
throw new Error(`Timed out waiting for veFaaS function ${functionName} to be deleted`, {
172+
cause: e,
173+
});
174+
}
175+
throw e;
176+
}
177+
},
178+
136179
createFunction: async (config: VefaasFunctionConfig, codePath: string): Promise<void> => {
137180
const { size, sizeMB } = await validateCodePackage(codePath);
138181

@@ -228,6 +271,11 @@ export const createVefaasOperations = (client: VefaasSdkClient) => {
228271
data: params,
229272
});
230273

274+
// CreateFunction is async on veFaaS — the function stays non-Active until
275+
// the platform finishes provisioning. Follow-up calls (e.g. update or
276+
// trigger setup) fail if issued too early, so poll until Active.
277+
await operations.waitForFunctionActive(config.functionName);
278+
231279
logger.info(lang.__('FUNCTION_CREATED', { functionName: config.functionName }));
232280
},
233281

@@ -325,6 +373,7 @@ export const createVefaasOperations = (client: VefaasSdkClient) => {
325373
}),
326374
};
327375

376+
await operations.waitForFunctionActive(config.functionName);
328377
await client.fetchOpenAPI({
329378
Action: 'UpdateFunction',
330379
Version: '2024-06-06',
@@ -378,6 +427,7 @@ export const createVefaasOperations = (client: VefaasSdkClient) => {
378427
};
379428
}
380429

430+
await operations.waitForFunctionActive(functionName);
381431
await client.fetchOpenAPI({
382432
Action: 'UpdateFunction',
383433
Version: '2024-06-06',
@@ -401,6 +451,10 @@ export const createVefaasOperations = (client: VefaasSdkClient) => {
401451
data: { FunctionName: functionName },
402452
});
403453

454+
// DeleteFunction returns immediately — wait for the function to be gone so
455+
// a subsequent create (retry) does not collide with a still-deleting function.
456+
await operations.waitForFunctionDeleted(functionName);
457+
404458
logger.info(lang.__('FUNCTION_DELETED', { functionName }));
405459
},
406460

@@ -430,4 +484,6 @@ export const createVefaasOperations = (client: VefaasSdkClient) => {
430484
}));
431485
},
432486
};
487+
488+
return operations;
433489
};

tests/unit/common/volcengineClient/vefaasOperations.test.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import type { VefaasFunctionConfig } from '../../../../src/common/volcengineClie
77
jest.mock('@volcengine/openapi', () => {
88
return {
99
Service: jest.fn().mockImplementation(() => ({
10-
fetchOpenAPI: jest.fn().mockResolvedValue({ Result: {} }),
10+
fetchOpenAPI: jest
11+
.fn()
12+
.mockImplementation(({ Action }: { Action: string }) =>
13+
Promise.resolve(
14+
Action === 'GetFunction' ? { Result: { Status: 'Active' } } : { Result: {} },
15+
),
16+
),
1117
})),
1218
};
1319
});
@@ -180,10 +186,19 @@ describe('vefaasOperations code size validation', () => {
180186
secretKey: 'test-sk',
181187
region: 'cn-beijing',
182188
}) as jest.Mocked<Service>;
183-
mockService.fetchOpenAPI = jest.fn().mockResolvedValue({
184-
Result: {},
185-
ResponseMetadata: { RequestId: 'test-request-id', Service: 'vefaas' },
186-
});
189+
mockService.fetchOpenAPI = jest.fn().mockImplementation(({ Action }: { Action: string }) =>
190+
Promise.resolve(
191+
Action === 'GetFunction'
192+
? {
193+
Result: { Status: 'Active' },
194+
ResponseMetadata: { RequestId: 'test-request-id', Service: 'vefaas' },
195+
}
196+
: {
197+
Result: {},
198+
ResponseMetadata: { RequestId: 'test-request-id', Service: 'vefaas' },
199+
},
200+
),
201+
);
187202
operations = createVefaasOperations(mockService);
188203
});
189204

@@ -309,6 +324,15 @@ describe('vefaasOperations code size validation', () => {
309324

310325
describe('deleteFunction', () => {
311326
it('should delete function', async () => {
327+
// DeleteFunction succeeds, then waitForFunctionDeleted polls GetFunction
328+
// until it returns null (FunctionNotFound) to confirm deletion.
329+
mockService.fetchOpenAPI
330+
.mockResolvedValueOnce({
331+
Result: {},
332+
ResponseMetadata: { RequestId: 'test-request-id', Service: 'vefaas' },
333+
})
334+
.mockRejectedValueOnce({ code: 'FunctionNotFound' });
335+
312336
await operations.deleteFunction('test-function');
313337

314338
expect(mockService.fetchOpenAPI).toHaveBeenCalledWith(

0 commit comments

Comments
 (0)