Skip to content

Commit b1a217f

Browse files
authored
Merge pull request #127 from devsapp/fix-code
fix: wait currentInstances is 0
2 parents f10c110 + 12782dd commit b1a217f

File tree

7 files changed

+181
-35
lines changed

7 files changed

+181
-35
lines changed

__tests__/e2e/ci-mac-linux.sh

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ cd model
3232
pip install -r requirements.txt
3333
export fc_component_function_name=model-$(uname)-$(uname -m)-$RANDSTR-$RANDOM
3434
python -u deploy_and_test_model.py --model-id iic/cv_LightweightEdge_ocr-recognitoin-general_damo --region cn-shanghai --auto-cleanup
35-
sleep 5
35+
sleep 10
3636
python -u deploy_and_test_model.py --model-id Qwen/Qwen2.5-0.5B-Instruct --region cn-shanghai --auto-cleanup
37-
sleep 5
37+
sleep 10
3838
python -u deploy_and_test_model.py --model-id iic/cv_LightweightEdge_ocr-recognitoin-general_damo --region cn-shanghai --storage oss --auto-cleanup
39-
sleep 5
39+
sleep 10
4040
python -u deploy_and_test_model.py --model-id Qwen/Qwen2.5-0.5B-Instruct --region cn-shanghai --storage oss --auto-cleanup
4141

42-
sleep 5
42+
sleep 10
4343
echo "test model s_file.yaml"
4444
# python -u test.py
4545
s model download -t s_file.yaml

__tests__/ut/commands/deploy/impl/provision_config_test.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,27 @@ describe('ProvisionConfig', () => {
100100
});
101101
expect(provisionConfig.ProvisionMode).toBe('async');
102102
});
103+
104+
it('should initialize correctly with drain mode config', () => {
105+
const inputsWithMode = {
106+
...mockInputs,
107+
props: {
108+
...mockInputs.props,
109+
provisionConfig: {
110+
defaultTarget: 10,
111+
alwaysAllocateCPU: false,
112+
alwaysAllocateGPU: false,
113+
scheduledActions: [],
114+
targetTrackingPolicies: [],
115+
mode: 'drain',
116+
},
117+
},
118+
};
119+
120+
provisionConfig = new ProvisionConfig(inputsWithMode, mockOpts);
121+
122+
expect(provisionConfig.ProvisionMode).toBe('drain');
123+
});
103124
});
104125

105126
describe('before', () => {
@@ -350,12 +371,15 @@ describe('ProvisionConfig', () => {
350371
});
351372

352373
describe('waitForProvisionReady', () => {
353-
it('should return immediately when target is 0', async () => {
374+
it('should still call getFunctionProvisionConfig when target is 0 but loop exits early', async () => {
354375
provisionConfig = new ProvisionConfig(mockInputs, mockOpts);
355376

356377
// Mock fcSdk
357378
const mockFcSdk = {
358-
getFunctionProvisionConfig: jest.fn(),
379+
getFunctionProvisionConfig: jest.fn().mockImplementation(() => {
380+
// 返回模拟数据,但我们会验证它被调用了
381+
return Promise.resolve({ current: 0, target: 0 });
382+
}),
359383
disableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
360384
enableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
361385
};
@@ -366,7 +390,8 @@ describe('ProvisionConfig', () => {
366390

367391
await (provisionConfig as any).waitForProvisionReady('LATEST', { target: 0 });
368392

369-
expect(provisionConfig.fcSdk.getFunctionProvisionConfig).not.toHaveBeenCalled();
393+
// 验证getFunctionProvisionConfig至少被调用了一次
394+
expect(provisionConfig.fcSdk.getFunctionProvisionConfig).toHaveBeenCalled();
370395
});
371396

372397
it('should wait until current equals target', async () => {
@@ -474,6 +499,7 @@ describe('ProvisionConfig', () => {
474499

475500
// Mock fcSdk
476501
const mockFcSdk = {
502+
getFunctionProvisionConfig: jest.fn(),
477503
disableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
478504
enableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
479505
};
@@ -499,6 +525,7 @@ describe('ProvisionConfig', () => {
499525

500526
// Mock fcSdk
501527
const mockFcSdk = {
528+
getFunctionProvisionConfig: jest.fn(),
502529
disableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
503530
enableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
504531
};
@@ -518,6 +545,32 @@ describe('ProvisionConfig', () => {
518545
expect(provisionConfig.fcSdk.enableFunctionInvocation).toHaveBeenCalledWith('test-function');
519546
});
520547

548+
it('should handle drain mode with target 0', async () => {
549+
provisionConfig = new ProvisionConfig(mockInputs, mockOpts);
550+
provisionConfig.ProvisionMode = 'drain';
551+
552+
// Mock fcSdk
553+
const mockFcSdk = {
554+
getFunctionProvisionConfig: jest.fn(),
555+
disableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
556+
enableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
557+
};
558+
Object.defineProperty(provisionConfig, 'fcSdk', {
559+
value: mockFcSdk,
560+
writable: true,
561+
});
562+
563+
await (provisionConfig as any).waitForProvisionReady('LATEST', { target: 0 });
564+
565+
expect(provisionConfig.fcSdk.disableFunctionInvocation).toHaveBeenCalledWith(
566+
'test-function',
567+
true,
568+
'Fast scale-to-zero',
569+
);
570+
expect(sleepMock).toHaveBeenCalledWith(5);
571+
expect(provisionConfig.fcSdk.enableFunctionInvocation).toHaveBeenCalledWith('test-function');
572+
});
573+
521574
it('should handle error retries in waitForProvisionReady', async () => {
522575
provisionConfig = new ProvisionConfig(mockInputs, mockOpts);
523576

@@ -753,6 +806,22 @@ describe('ProvisionConfig', () => {
753806
});
754807
});
755808

809+
it('should keep target when defaultTarget does not exist', () => {
810+
provisionConfig = new ProvisionConfig(mockInputs, mockOpts);
811+
812+
const config = {
813+
target: 5,
814+
current: 5,
815+
functionArn: 'arn:xxx',
816+
};
817+
818+
const result = (provisionConfig as any).sanitizeProvisionConfig(config);
819+
820+
expect(result).toEqual({
821+
target: 5,
822+
});
823+
});
824+
756825
it('should remove empty targetTrackingPolicies', () => {
757826
provisionConfig = new ProvisionConfig(mockInputs, mockOpts);
758827

__tests__/ut/commands/deploy/impl/scaling_config_test.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,17 @@ describe('ScalingConfig', () => {
322322
});
323323
expect(scalingConfig.ScalingMode).toBe('async');
324324
});
325+
326+
it('should initialize correctly with drain mode config', () => {
327+
mockInputs.props.scalingConfig = {
328+
minInstances: 1,
329+
mode: 'drain',
330+
} as any;
331+
332+
scalingConfig = new ScalingConfig(mockInputs, mockOpts);
333+
334+
expect(scalingConfig.ScalingMode).toBe('drain');
335+
});
325336
});
326337

327338
describe('before', () => {
@@ -611,12 +622,14 @@ describe('ScalingConfig', () => {
611622
});
612623

613624
describe('waitForScalingReady', () => {
614-
it('should return immediately when minInstances is 0', async () => {
625+
it('should still call getFunctionScalingConfig when minInstances is 0 but loop exits early', async () => {
615626
scalingConfig = new ScalingConfig(mockInputs, mockOpts);
616627

617628
// Mock fcSdk
618629
const mockFcSdk = {
619-
getFunctionScalingConfig: jest.fn(),
630+
getFunctionScalingConfig: jest
631+
.fn()
632+
.mockResolvedValue({ currentInstances: 0, minInstances: 0 }),
620633
disableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
621634
enableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
622635
};
@@ -627,7 +640,8 @@ describe('ScalingConfig', () => {
627640

628641
await (scalingConfig as any).waitForScalingReady('LATEST', { minInstances: 0 });
629642

630-
expect(mockFcSdk.getFunctionScalingConfig).not.toHaveBeenCalled();
643+
// 验证getFunctionScalingConfig至少被调用了一次
644+
expect(mockFcSdk.getFunctionScalingConfig).toHaveBeenCalled();
631645
});
632646

633647
it('should wait until currentInstances reaches minInstances', async () => {
@@ -681,6 +695,32 @@ describe('ScalingConfig', () => {
681695
expect(mockFcSdk.enableFunctionInvocation).toHaveBeenCalledWith('test-function');
682696
});
683697

698+
it('should handle drain mode without minInstances', async () => {
699+
scalingConfig = new ScalingConfig(mockInputs, mockOpts);
700+
scalingConfig.ScalingMode = 'drain';
701+
702+
// Mock fcSdk
703+
const mockFcSdk = {
704+
disableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
705+
enableFunctionInvocation: jest.fn().mockResolvedValue(undefined),
706+
getFunctionScalingConfig: jest.fn().mockResolvedValue({}),
707+
};
708+
Object.defineProperty(scalingConfig, 'fcSdk', {
709+
value: mockFcSdk,
710+
writable: true,
711+
});
712+
713+
await (scalingConfig as any).waitForScalingReady('LATEST', {});
714+
715+
expect(mockFcSdk.disableFunctionInvocation).toHaveBeenCalledWith(
716+
'test-function',
717+
true,
718+
'Fast scale-to-zero',
719+
);
720+
expect(sleepMock).toHaveBeenCalledWith(5);
721+
expect(mockFcSdk.enableFunctionInvocation).toHaveBeenCalledWith('test-function');
722+
});
723+
684724
it('should timeout when max retries reached', async () => {
685725
scalingConfig = new ScalingConfig(mockInputs, mockOpts);
686726

publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Type: Component
33
Name: fc3
44
Provider:
55
- 阿里云
6-
Version: 0.1.8
6+
Version: 0.1.9
77
Description: 阿里云函数计算全生命周期管理
88
HomePage: https://github.com/devsapp/fc3
99
Organization: 阿里云函数计算(FC)

src/resources/fc/error-code.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,47 @@ export const isFunctionStateWaitTimedOut = (ex) => {
5353
return false;
5454
};
5555

56+
const PROVISION_ERROR_CODES = [
57+
FC_API_ERROR_CODE.ProvisionConfigExist,
58+
FC_API_ERROR_CODE.ResidentScalingConfigExists,
59+
];
60+
61+
const ERROR_MESSAGES = {
62+
GPU_TYPE_CHANGE: 'GPU type should not be changed with resident scaling config',
63+
GPU_TYPE_MISMATCH: (localGPUType, remoteGPUType) =>
64+
`function gpu type '${localGPUType}' doesn't match resident pool gpu type '${remoteGPUType}'`,
65+
IDLE_PROVISION_CONFIG: (functionName) =>
66+
`idle provision config exists for function '${functionName}'`,
67+
FUNCTION_BOUND_TO_POOL: (functionName) =>
68+
`Function '${functionName}' has been bound to resident source pool`,
69+
};
70+
71+
const checkProvisionErrorCode = (ex) => {
72+
const code = ex?.code;
73+
return code && PROVISION_ERROR_CODES.includes(code);
74+
};
75+
76+
const checkInvalidArgumentConditions = (ex, localGPUType, remoteGPUType, functionName) => {
77+
if (!isInvalidArgument(ex)) return false;
78+
79+
const message = ex?.message;
80+
const code = ex?.code;
81+
82+
return (
83+
message?.includes(ERROR_MESSAGES.GPU_TYPE_CHANGE) ||
84+
message?.includes(ERROR_MESSAGES.GPU_TYPE_MISMATCH(localGPUType, remoteGPUType)) ||
85+
message?.includes(ERROR_MESSAGES.IDLE_PROVISION_CONFIG(functionName)) ||
86+
code === FC_API_ERROR_CODE.ResourcePoolInsufficientCapacity ||
87+
message?.includes(ERROR_MESSAGES.FUNCTION_BOUND_TO_POOL(functionName))
88+
);
89+
};
90+
5691
export const isFunctionScalingConfigError = (
5792
ex,
5893
{ localGPUType = '', remoteGPUType = '', functionName = 'F' },
5994
) => {
60-
if (
61-
isInvalidArgument(ex) &&
62-
(ex.message.includes('GPU type should not be changed with resident scaling config') ||
63-
ex.message.includes(
64-
`function gpu type '${localGPUType}' doesn't match resident pool gpu type '${remoteGPUType}'`,
65-
) ||
66-
ex.message.includes(`idle provision config exists for function '${functionName}'`) ||
67-
ex.code === FC_API_ERROR_CODE.ResourcePoolInsufficientCapacity)
68-
) {
69-
return true;
70-
}
71-
return false;
95+
return (
96+
checkProvisionErrorCode(ex) ||
97+
checkInvalidArgumentConditions(ex, localGPUType, remoteGPUType, functionName)
98+
);
7299
};

src/subCommands/deploy/impl/provision_config.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,12 @@ export default class ProvisionConfig extends Base {
9393

9494
// 如果没有目标值或目标值为0,则无需等待
9595
if (!realTarget || realTarget <= 0) {
96-
if (this.ProvisionMode !== 'drain') {
97-
return;
98-
} else {
96+
if (this.ProvisionMode === 'drain') {
9997
logger.info(`disableFunctionInvocation ${this.functionName} ...`);
10098
await this.fcSdk.disableFunctionInvocation(this.functionName, true, 'Fast scale-to-zero');
10199
await sleep(5);
102100
logger.info(`enableFunctionInvocation ${this.functionName} ...`);
103101
await this.fcSdk.enableFunctionInvocation(this.functionName);
104-
return;
105102
}
106103
}
107104

@@ -114,7 +111,7 @@ export default class ProvisionConfig extends Base {
114111
const { current, currentError } = result || {};
115112

116113
// 检查是否已达到目标值
117-
if (current && current === realTarget) {
114+
if (current === undefined || (current && current === realTarget)) {
118115
logger.info(
119116
`ProvisionConfig of ${this.functionName}/${qualifier} is ready. Current: ${current}, Target: ${realTarget}`,
120117
);
@@ -138,7 +135,12 @@ export default class ProvisionConfig extends Base {
138135
logger.warn('=========== LAST 10 MINUTES END =========== ');
139136
*/
140137
// 如果是系统内部错误,则继续尝试
141-
if (!currentError.includes('an internal error has occurred')) {
138+
if (
139+
!(
140+
currentError.includes('an internal error has occurred') ||
141+
currentError.includes('Resources are being replenished')
142+
)
143+
) {
142144
// 不是系统内部错误,满足一定的重试次数则退出
143145
getCurrentErrorCount++;
144146
if (getCurrentErrorCount > 3 || (index > 6 && getCurrentErrorCount > 0)) {

src/subCommands/deploy/impl/scaling_config.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,12 @@ export default class ScalingConfig extends Base {
9090

9191
// 如果没有最小实例数或最小实例数为0,则无需等待
9292
if (!minInstances || minInstances <= 0) {
93-
if (this.ScalingMode !== 'drain') {
94-
return;
95-
} else {
93+
if (this.ScalingMode === 'drain') {
9694
logger.info(`disableFunctionInvocation ${this.functionName} ...`);
9795
await this.fcSdk.disableFunctionInvocation(this.functionName, true, 'Fast scale-to-zero');
9896
await sleep(5);
9997
logger.info(`enableFunctionInvocation ${this.functionName} ...`);
10098
await this.fcSdk.enableFunctionInvocation(this.functionName);
101-
return;
10299
}
103100
}
104101

@@ -110,16 +107,27 @@ export default class ScalingConfig extends Base {
110107
const result = await this.fcSdk.getFunctionScalingConfig(this.functionName, qualifier);
111108
const { currentInstances, currentError } = result || {};
112109

110+
logger.debug(
111+
`get ${this.functionName}/${qualifier} scaling config result: ${JSON.stringify(result)}`,
112+
);
113113
// 检查是否已达到最小实例数
114-
if (currentInstances && currentInstances >= result.minInstances) {
114+
if (
115+
currentInstances === undefined ||
116+
(currentInstances && currentInstances === result.minInstances)
117+
) {
115118
logger.info(
116119
`ScalingConfig of ${this.functionName}/${qualifier} is ready. CurrentInstances: ${currentInstances}, MinInstances: ${minInstances}`,
117120
);
118121
return;
119122
}
120123
if (currentError && currentError.length > 0) {
121124
// 如果是系统内部错误,则继续尝试
122-
if (!currentError.includes('an internal error has occurred')) {
125+
if (
126+
!(
127+
currentError.includes('an internal error has occurred') ||
128+
currentError.includes('Resources are being replenished')
129+
)
130+
) {
123131
// 不是系统内部错误,满足一定的重试次数则退出
124132
getCurrentErrorCount++;
125133
if (getCurrentErrorCount > 3 || (index > 6 && getCurrentErrorCount > 0)) {

0 commit comments

Comments
 (0)