Skip to content

Commit 4ba4fa7

Browse files
authored
Detect blocking sync operations during function executions (Fixes #558)
1 parent d51a7b0 commit 4ba4fa7

File tree

6 files changed

+137
-17
lines changed

6 files changed

+137
-17
lines changed

package-lock.json

Lines changed: 32 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
"dependencies": {
88
"@grpc/grpc-js": "^1.2.7",
99
"@grpc/proto-loader": "^0.6.4",
10+
"blocked-at": "^1.2.0",
1011
"fs-extra": "^10.0.1",
1112
"long": "^4.0.0",
1213
"minimist": "^1.2.5",
1314
"uuid": "^8.3.0"
1415
},
1516
"devDependencies": {
17+
"@types/blocked-at": "^1.0.1",
1618
"@types/chai": "^4.2.22",
1719
"@types/chai-as-promised": "^7.1.5",
1820
"@types/fs-extra": "^9.0.13",
@@ -75,4 +77,4 @@
7577
"dist"
7678
],
7779
"main": "dist/src/nodejsWorker.js"
78-
}
80+
}

src/Worker.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import { FunctionLoader } from './FunctionLoader';
66
import { CreateGrpcEventStream } from './GrpcClient';
77
import { setupCoreModule } from './setupCoreModule';
88
import { setupEventStream } from './setupEventStream';
9+
import { startBlockedMonitor } from './utils/blockedMonitor';
910
import { ensureErrorType } from './utils/ensureErrorType';
1011
import { InternalException } from './utils/InternalException';
1112
import { systemError, systemLog } from './utils/Logger';
13+
import { isEnvironmentVariableSet } from './utils/util';
1214
import { WorkerChannel } from './WorkerChannel';
1315

1416
export function startNodeWorker(args) {
@@ -69,4 +71,8 @@ export function startNodeWorker(args) {
6971
process.on('exit', (code) => {
7072
systemLog(`Worker ${workerId} exited with code ${code}`);
7173
});
74+
75+
if (isEnvironmentVariableSet(process.env.AZURE_FUNCTIONS_NODE_BLOCK_LOG)) {
76+
startBlockedMonitor(channel);
77+
}
7278
}

src/utils/blockedMonitor.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { AzureFunctionsRpcMessages as rpc } from './../../azure-functions-language-worker-protobuf/src/rpc';
5+
import LogCategory = rpc.RpcLog.RpcLogCategory;
6+
import LogLevel = rpc.RpcLog.Level;
7+
import blockedAt = require('blocked-at');
8+
9+
export function startBlockedMonitor(
10+
channel: { log: (log: rpc.IRpcLog) => void },
11+
threshold = 500,
12+
intreval = 10000
13+
): NodeJS.Timer {
14+
function logBlockedWarning(message: string) {
15+
channel.log({
16+
message,
17+
level: LogLevel.Warning,
18+
logCategory: LogCategory.System,
19+
});
20+
}
21+
22+
logBlockedWarning(
23+
`Monitoring for blocking code is turned on, with a threshold of ${threshold} ms. This will have a negative impact on performance. Adjust "AZURE_FUNCTIONS_NODE_BLOCK_LOG" to turn it off. ` +
24+
'IMPORTANT NOTE: The stack traces are only an approximation and you should analyze all synchronous operations'
25+
);
26+
27+
let blockedHistory: { time: string; duration: number; stack: string[] }[] = [];
28+
29+
//threshold - minimum miliseconds of blockage to report.
30+
//other parameters are default, more details on https://github.com/naugtur/blocked-at.
31+
blockedAt(
32+
(ms, stack) => {
33+
const date = new Date();
34+
blockedHistory.push({ time: date.toISOString(), duration: ms, stack: stack });
35+
},
36+
{ threshold: threshold }
37+
);
38+
39+
// Log blockedHistory if it's not empty each 10 seconds
40+
return setInterval(() => {
41+
if (blockedHistory.length > 0) {
42+
logBlockedWarning(`Blocking code monitoring history: ${JSON.stringify(blockedHistory)}`);
43+
blockedHistory = [];
44+
}
45+
}, intreval);
46+
}

src/utils/util.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
export function isEnvironmentVariableSet(val: string | boolean | number | undefined | null): boolean {
5+
return !/^(false|0)?$/i.test(val === undefined || val === null ? '' : String(val));
6+
}

test/blockMonitorTest.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { expect } from 'chai';
5+
import 'mocha';
6+
import { AzureFunctionsRpcMessages as rpc } from './../azure-functions-language-worker-protobuf/src/rpc';
7+
import { startBlockedMonitor } from './../src/utils/blockedMonitor';
8+
import LogLevel = rpc.RpcLog.Level;
9+
10+
describe('Event loop blocking operation monitor', () => {
11+
it('startBlockMonitor logs warning', async () => {
12+
console.log('start ' + new Date().getSeconds() + ':' + new Date().getMilliseconds());
13+
let timer: NodeJS.Timer | null = null;
14+
let isTimerDestroyed = false;
15+
const logFun = function (log: rpc.IRpcLog): void {
16+
expect(log.level).to.equal(LogLevel.Warning);
17+
if (log.message && log.message.startsWith('Blocking code monitoring history')) {
18+
if (timer) {
19+
clearInterval(timer);
20+
isTimerDestroyed = true;
21+
}
22+
}
23+
};
24+
25+
timer = startBlockedMonitor({ log: logFun }, 100, 100);
26+
await new Promise((resolve) => {
27+
//Adding new event to event loop to start monitoring
28+
setTimeout(() => {
29+
resolve(true);
30+
}, 1);
31+
});
32+
const end = Date.now() + 500;
33+
while (Date.now() < end) {} // blocking code
34+
35+
await new Promise((resolve) => {
36+
//assert
37+
setTimeout(() => {
38+
if (isTimerDestroyed) {
39+
resolve(true);
40+
}
41+
}, 500);
42+
});
43+
});
44+
});

0 commit comments

Comments
 (0)