Skip to content

Commit e01abd6

Browse files
authored
Tweak lstat bug retry logic (#712)
1 parent e23b5f7 commit e01abd6

File tree

6 files changed

+43
-21
lines changed

6 files changed

+43
-21
lines changed

src/loadScriptFile.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,39 @@ import { PackageJson } from './parsers/parsePackageJson';
1111
import LogCategory = rpc.RpcLog.RpcLogCategory;
1212
import LogLevel = rpc.RpcLog.Level;
1313

14+
let hasLoggedAttempt = 0;
15+
let hasLoggedWarning = false;
16+
1417
export async function loadScriptFile(filePath: string, packageJson: PackageJson): Promise<unknown> {
1518
// See the following issue for more details on why we want to retry
1619
// https://github.com/Azure/azure-functions-nodejs-worker/issues/693
17-
const fileName = path.basename(filePath);
20+
const retries = 9;
1821
return await retry(
1922
async (currentAttempt: number) => {
20-
if (currentAttempt > 1) {
23+
if (currentAttempt > 1 && currentAttempt > hasLoggedAttempt) {
2124
worker.log({
22-
message: `Retrying load of file "${fileName}". Attempt ${currentAttempt}/${10}`,
25+
message: `Retrying file load. Attempt ${currentAttempt}/${retries + 1}`,
2326
level: LogLevel.Debug,
2427
logCategory: LogCategory.System,
2528
});
29+
hasLoggedAttempt = currentAttempt;
2630
}
2731
return loadScriptFileInternal(filePath, packageJson);
2832
},
2933
{
30-
retries: 9,
31-
minTimeout: 50,
34+
retries: retries,
35+
minTimeout: 500,
3236
onFailedAttempt: (error) => {
3337
if (!/lstat.*home/i.test(error?.message || '')) {
3438
// this will abort the retries if it's an error we don't recognize
3539
throw error;
36-
} else if (error.retriesLeft > 0) {
40+
} else if (error.retriesLeft > 0 && !hasLoggedWarning) {
3741
worker.log({
38-
message: `Warning: Failed to load file "${fileName}" with error "${error.message}"`,
42+
message: `Warning: Failed to load file with error "${error.message}"`,
3943
level: LogLevel.Warning,
4044
logCategory: LogCategory.System,
4145
});
46+
hasLoggedWarning = true;
4247
}
4348
},
4449
}

test/eventHandlers/FunctionLoadHandler.test.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Licensed under the MIT License.
33

44
import { expect } from 'chai';
5+
import * as fs from 'fs/promises';
56
import 'mocha';
7+
import * as path from 'path';
68
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
79
import { getLegacyFunction } from '../../src/LegacyFunctionLoader';
810
import { worker } from '../../src/WorkerContext';
@@ -11,6 +13,7 @@ import { nonNullValue } from '../../src/utils/nonNull';
1113
import { RegExpStreamingMessage, TestEventStream } from './TestEventStream';
1214
import { beforeEventHandlerSuite } from './beforeEventHandlerSuite';
1315
import { msg } from './msg';
16+
import { tempFile, testAppSrcPath } from './testAppUtils';
1417

1518
describe('FunctionLoadHandler', () => {
1619
let stream: TestEventStream;
@@ -44,22 +47,31 @@ describe('FunctionLoadHandler', () => {
4447
it('handles transient lstat function load exception', async function (this: Mocha.ITestCallbackContext): Promise<void> {
4548
// https://github.com/Azure/azure-functions-nodejs-worker/issues/693
4649

47-
this.timeout(40 * 1000);
50+
this.timeout(15 * 1000);
4851

49-
stream.addTestMessage(msg.funcLoad.request('throwLstatError.js'));
52+
await fs.writeFile(
53+
path.join(testAppSrcPath, tempFile),
54+
`if (Date.now() < ${Date.now() + 5 * 1000})
55+
{
56+
throw new Error("UNKNOWN: unknown error, lstat 'D:\\\\home'");
57+
} else {
58+
module.exports = async () => { }
59+
}`
60+
);
61+
62+
stream.addTestMessage(msg.funcLoad.request(tempFile));
5063

5164
const errorMessage = "UNKNOWN: unknown error, lstat 'D:\\home'";
52-
const msgs: (rpc.IStreamingMessage | RegExpStreamingMessage)[] = [msg.funcLoad.receivedRequestLog];
53-
for (let i = 2; i <= 10; i++) {
54-
msgs.push(
55-
msg.warningLog(`Warning: Failed to load file "throwLstatError.js" with error "${errorMessage}"`),
56-
msg.debugLog(`Retrying load of file "throwLstatError.js". Attempt ${i}/10`)
57-
);
65+
const msgs: (rpc.IStreamingMessage | RegExpStreamingMessage)[] = [
66+
msg.funcLoad.receivedRequestLog,
67+
msg.warningLog(`Warning: Failed to load file with error "${errorMessage}"`),
68+
];
69+
for (let i = 2; i <= 5; i++) {
70+
msgs.push(msg.debugLog(`Retrying file load. Attempt ${i}/10`));
5871
}
59-
const message = `Worker was unable to load function testFuncName: '${errorMessage}'`;
60-
msgs.push(msg.errorLog(message), msg.funcLoad.failedResponse(message));
72+
msgs.push(msg.funcLoad.response);
6173

62-
await delay(30 * 1000);
74+
await delay(8 * 1000);
6375

6476
await stream.assertCalledWith(...msgs);
6577
});

test/eventHandlers/TestEventStream.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ export class TestEventStream extends EventEmitter implements IEventStream {
4545
const calls = this.written.getCalls();
4646

4747
// First, validate the "shortened" form of the messages. This will result in a more readable error for most test failures
48-
if (!expectedMsgs.find((m) => m instanceof RegExpStreamingMessage)) {
48+
if (
49+
!expectedMsgs.find((m) => m instanceof RegExpStreamingMessage) ||
50+
calls.length !== expectedMsgs.length
51+
) {
4952
// shortened message won't work if it's a regexp
53+
// but if the call count doesn't match, this error will be better than the one below
5054
const shortExpectedMsgs = expectedMsgs.map(getShortenedMsg);
5155
const shortActualMsgs = calls.map((c) => getShortenedMsg(c.args[0]));
5256
expect(shortActualMsgs).to.deep.equal(shortExpectedMsgs);
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
package.json
1+
package.json
2+
src/temp.js

test/eventHandlers/testApp/src/throwLstatError.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/eventHandlers/testAppUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import * as fs from 'fs/promises';
55
import * as path from 'path';
66

7+
export const tempFile = 'temp.js';
78
export const testAppPath = path.join(__dirname, 'testApp');
89
export const testAppSrcPath = path.join(testAppPath, 'src');
910
export const testPackageJsonPath = path.join(testAppPath, 'package.json');

0 commit comments

Comments
 (0)