Skip to content

Commit

Permalink
Tweak lstat bug retry logic (#712)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejizba committed Aug 17, 2023
1 parent e23b5f7 commit e01abd6
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 21 deletions.
19 changes: 12 additions & 7 deletions src/loadScriptFile.ts
Expand Up @@ -11,34 +11,39 @@ import { PackageJson } from './parsers/parsePackageJson';
import LogCategory = rpc.RpcLog.RpcLogCategory;
import LogLevel = rpc.RpcLog.Level;

let hasLoggedAttempt = 0;
let hasLoggedWarning = false;

export async function loadScriptFile(filePath: string, packageJson: PackageJson): Promise<unknown> {
// See the following issue for more details on why we want to retry
// https://github.com/Azure/azure-functions-nodejs-worker/issues/693
const fileName = path.basename(filePath);
const retries = 9;
return await retry(
async (currentAttempt: number) => {
if (currentAttempt > 1) {
if (currentAttempt > 1 && currentAttempt > hasLoggedAttempt) {
worker.log({
message: `Retrying load of file "${fileName}". Attempt ${currentAttempt}/${10}`,
message: `Retrying file load. Attempt ${currentAttempt}/${retries + 1}`,
level: LogLevel.Debug,
logCategory: LogCategory.System,
});
hasLoggedAttempt = currentAttempt;
}
return loadScriptFileInternal(filePath, packageJson);
},
{
retries: 9,
minTimeout: 50,
retries: retries,
minTimeout: 500,
onFailedAttempt: (error) => {
if (!/lstat.*home/i.test(error?.message || '')) {
// this will abort the retries if it's an error we don't recognize
throw error;
} else if (error.retriesLeft > 0) {
} else if (error.retriesLeft > 0 && !hasLoggedWarning) {
worker.log({
message: `Warning: Failed to load file "${fileName}" with error "${error.message}"`,
message: `Warning: Failed to load file with error "${error.message}"`,
level: LogLevel.Warning,
logCategory: LogCategory.System,
});
hasLoggedWarning = true;
}
},
}
Expand Down
34 changes: 23 additions & 11 deletions test/eventHandlers/FunctionLoadHandler.test.ts
Expand Up @@ -2,7 +2,9 @@
// Licensed under the MIT License.

import { expect } from 'chai';
import * as fs from 'fs/promises';
import 'mocha';
import * as path from 'path';
import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc';
import { getLegacyFunction } from '../../src/LegacyFunctionLoader';
import { worker } from '../../src/WorkerContext';
Expand All @@ -11,6 +13,7 @@ import { nonNullValue } from '../../src/utils/nonNull';
import { RegExpStreamingMessage, TestEventStream } from './TestEventStream';
import { beforeEventHandlerSuite } from './beforeEventHandlerSuite';
import { msg } from './msg';
import { tempFile, testAppSrcPath } from './testAppUtils';

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

this.timeout(40 * 1000);
this.timeout(15 * 1000);

stream.addTestMessage(msg.funcLoad.request('throwLstatError.js'));
await fs.writeFile(
path.join(testAppSrcPath, tempFile),
`if (Date.now() < ${Date.now() + 5 * 1000})
{
throw new Error("UNKNOWN: unknown error, lstat 'D:\\\\home'");
} else {
module.exports = async () => { }
}`
);

stream.addTestMessage(msg.funcLoad.request(tempFile));

const errorMessage = "UNKNOWN: unknown error, lstat 'D:\\home'";
const msgs: (rpc.IStreamingMessage | RegExpStreamingMessage)[] = [msg.funcLoad.receivedRequestLog];
for (let i = 2; i <= 10; i++) {
msgs.push(
msg.warningLog(`Warning: Failed to load file "throwLstatError.js" with error "${errorMessage}"`),
msg.debugLog(`Retrying load of file "throwLstatError.js". Attempt ${i}/10`)
);
const msgs: (rpc.IStreamingMessage | RegExpStreamingMessage)[] = [
msg.funcLoad.receivedRequestLog,
msg.warningLog(`Warning: Failed to load file with error "${errorMessage}"`),
];
for (let i = 2; i <= 5; i++) {
msgs.push(msg.debugLog(`Retrying file load. Attempt ${i}/10`));
}
const message = `Worker was unable to load function testFuncName: '${errorMessage}'`;
msgs.push(msg.errorLog(message), msg.funcLoad.failedResponse(message));
msgs.push(msg.funcLoad.response);

await delay(30 * 1000);
await delay(8 * 1000);

await stream.assertCalledWith(...msgs);
});
Expand Down
6 changes: 5 additions & 1 deletion test/eventHandlers/TestEventStream.ts
Expand Up @@ -45,8 +45,12 @@ export class TestEventStream extends EventEmitter implements IEventStream {
const calls = this.written.getCalls();

// First, validate the "shortened" form of the messages. This will result in a more readable error for most test failures
if (!expectedMsgs.find((m) => m instanceof RegExpStreamingMessage)) {
if (
!expectedMsgs.find((m) => m instanceof RegExpStreamingMessage) ||
calls.length !== expectedMsgs.length
) {
// shortened message won't work if it's a regexp
// but if the call count doesn't match, this error will be better than the one below
const shortExpectedMsgs = expectedMsgs.map(getShortenedMsg);
const shortActualMsgs = calls.map((c) => getShortenedMsg(c.args[0]));
expect(shortActualMsgs).to.deep.equal(shortExpectedMsgs);
Expand Down
3 changes: 2 additions & 1 deletion test/eventHandlers/testApp/.gitignore
@@ -1 +1,2 @@
package.json
package.json
src/temp.js
1 change: 0 additions & 1 deletion test/eventHandlers/testApp/src/throwLstatError.js

This file was deleted.

1 change: 1 addition & 0 deletions test/eventHandlers/testAppUtils.ts
Expand Up @@ -4,6 +4,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';

export const tempFile = 'temp.js';
export const testAppPath = path.join(__dirname, 'testApp');
export const testAppSrcPath = path.join(testAppPath, 'src');
export const testPackageJsonPath = path.join(testAppPath, 'package.json');
Expand Down

0 comments on commit e01abd6

Please sign in to comment.