Skip to content

Commit cbb1270

Browse files
authored
Camel case timer trigger (#261)
* updates to make 2.0 worker 1.0 compatible * split validation logic so it's both v1 and v2 compatible * hard-coded solution to camelCase timer trigger while proper fix is out of scope for timeline * added test * adding unit test * testing both v1 behavior and v2 behavior * dont code without intellisense * remove e2e test until have better test on functions host v2 and v3 * re-add node 8 unit tests
1 parent ca042e6 commit cbb1270

File tree

10 files changed

+137
-31
lines changed

10 files changed

+137
-31
lines changed

azure-pipelines.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
variables: {
22
WORKER_VERSION: '2.0.0',
3+
NODE_8: '8.x',
34
NODE_10: '10.x',
45
NODE_12: '12.x'
56
}
@@ -14,18 +15,27 @@ jobs:
1415
- job: UnitTests
1516
strategy:
1617
matrix:
18+
UBUNTU_NODE8:
19+
IMAGE_TYPE: 'ubuntu-latest'
20+
NODE_VERSION: $(NODE_8)
1721
UBUNTU_NODE10:
1822
IMAGE_TYPE: 'ubuntu-latest'
1923
NODE_VERSION: $(NODE_10)
2024
UBUNTU_NODE12:
2125
IMAGE_TYPE: 'ubuntu-latest'
2226
NODE_VERSION: $(NODE_12)
27+
WINDOWS_NODE8:
28+
IMAGE_TYPE: 'vs2017-win2016'
29+
NODE_VERSION: $(NODE_8)
2330
WINDOWS_NODE10:
2431
IMAGE_TYPE: 'vs2017-win2016'
2532
NODE_VERSION: $(NODE_10)
2633
WINDOWS_NODE12:
2734
IMAGE_TYPE: 'vs2017-win2016'
2835
NODE_VERSION: $(NODE_12)
36+
MAC_NODE8:
37+
IMAGE_TYPE: 'macos-10.13'
38+
NODE_VERSION: $(NODE_8)
2939
MAC_NODE10:
3040
IMAGE_TYPE: 'macos-10.13'
3141
NODE_VERSION: $(NODE_10)

package.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ copy-item ./dist/src/nodejsWorker.js ./pkg/dist/src/
2424
copy-item ./worker.config.json pkg
2525
./node_modules/.bin/webpack
2626
StopOnFailedExecution # fail if error
27+
# Node 8 support
28+
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=ia32 --target=8.4.0 --target_platform=win32
29+
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=ia32 --target=8.4.0 --target_platform=darwin
30+
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=ia32 --target=8.4.0 --target_platform=linux --target_libc=glibc
31+
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=x64 --target=8.4.0 --target_platform=win32
32+
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=x64 --target=8.4.0 --target_platform=darwin
33+
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=x64 --target=8.4.0 --target_platform=linux --target_libc=glibc
2734
# Node 10 support
2835
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=ia32 --target=10.1.0 --target_platform=win32
2936
./node_modules/.bin/node-pre-gyp install -C pkg/grpc --target_arch=ia32 --target=10.1.0 --target_platform=darwin

src/Context.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
import { FunctionInfo } from './FunctionInfo';
2-
import { fromRpcHttp, fromTypedData, getNormalizedBindingData, getBindingDefinitions, fromRpcTraceContext } from './converters';
2+
import { fromRpcHttp, fromTypedData, getNormalizedBindingData, getBindingDefinitions, fromRpcTraceContext, convertKeysToCamelCase } from './converters';
33
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
44
import { Request, RequestProperties } from './http/Request';
55
import { Response } from './http/Response';
66
import LogLevel = rpc.RpcLog.Level;
77
import LogCategory = rpc.RpcLog.RpcLogCategory;
88
import { Context, ExecutionContext, Logger, BindingDefinition, HttpRequest, TraceContext } from './public/Interfaces'
99

10-
export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback) {
10+
export function CreateContextAndInputs(info: FunctionInfo, request: rpc.IInvocationRequest, logCallback: LogCallback, callback: ResultCallback, v1WorkerBehavior: boolean) {
1111
let context = new InvocationContext(info, request, logCallback, callback);
1212

1313
let bindings: Dict<any> = {};
14-
let inputs: InputTypes[] = [];
14+
let inputs: any[] = [];
1515
let httpInput: RequestProperties | undefined;
1616
for (let binding of <rpc.IParameterBinding[]>request.inputData) {
1717
if (binding.data && binding.name) {
18-
let input: InputTypes;
18+
let input;
1919
if (binding.data && binding.data.http) {
2020
input = httpInput = fromRpcHttp(binding.data.http);
2121
} else {
22-
input = fromTypedData(binding.data);
22+
// TODO: Don't hard code fix for camelCase https://github.com/Azure/azure-functions-nodejs-worker/issues/188
23+
if (v1WorkerBehavior && info.getTimerTriggerName() === binding.name) {
24+
input = convertKeysToCamelCase(binding)["data"];
25+
} else {
26+
input = fromTypedData(binding.data);
27+
}
2328
}
2429
bindings[binding.name] = input;
2530
inputs.push(input);
@@ -127,6 +132,3 @@ export type ResultCallback = (err?: any, result?: InvocationResult) => void;
127132
export interface Dict<T> {
128133
[key: string]: T
129134
}
130-
131-
// Allowed input types
132-
export type InputTypes = HttpRequest | string | Buffer | null | undefined;

src/FunctionInfo.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,14 @@ export class FunctionInfo {
4444
public getReturnBinding() {
4545
return this.outputBindings[returnBindingKey];
4646
}
47+
48+
public getTimerTriggerName(): string | undefined {
49+
for (let name in this.bindings) {
50+
let type = this.bindings[name].type;
51+
if (type && type.toLowerCase() === "timertrigger") {
52+
return name;
53+
}
54+
}
55+
return;
56+
}
4757
}

src/WorkerChannel.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CreateContextAndInputs, LogCallback, ResultCallback } from './Context';
55
import { IEventStream } from './GrpcService';
66
import { toTypedData } from './converters';
77
import { augmentTriggerMetadata } from './augmenters';
8-
import { systemError } from './utils/Logger';
8+
import { systemError, systemWarn } from './utils/Logger';
99
import { InternalException } from './utils/InternalException';
1010
import LogCategory = rpc.RpcLog.RpcLogCategory;
1111
import LogLevel = rpc.RpcLog.Level;
@@ -34,14 +34,14 @@ export class WorkerChannel implements IWorkerChannel {
3434
private _eventStream: IEventStream;
3535
private _functionLoader: IFunctionLoader;
3636
private _workerId: string;
37-
private _v2Compatible: boolean;
37+
private _v1WorkerBehavior: boolean;
3838

3939
constructor(workerId: string, eventStream: IEventStream, functionLoader: IFunctionLoader) {
4040
this._workerId = workerId;
4141
this._eventStream = eventStream;
4242
this._functionLoader = functionLoader;
4343
// default value
44-
this._v2Compatible = false;
44+
this._v1WorkerBehavior = false;
4545

4646
// call the method with the matching 'event' name on this class, passing the requestId and event message
4747
eventStream.on('data', (msg) => {
@@ -92,13 +92,31 @@ export class WorkerChannel implements IWorkerChannel {
9292
*/
9393
public workerInitRequest(requestId: string, msg: rpc.WorkerInitRequest) {
9494
// TODO: add capability from host to go to "non-breaking" mode
95-
if (msg.hostVersion) {
96-
this._v2Compatible = true;
95+
if (msg.capabilities && msg.capabilities.V2Compatable) {
96+
this._v1WorkerBehavior = true;
9797
}
98+
99+
// Validate version
100+
let version = process.version;
101+
if (this._v1WorkerBehavior) {
102+
if (version.startsWith("v12."))
103+
{
104+
systemWarn("The Node.js version you are using (" + version + ") is not fully supported with Azure Functions V2. We recommend using one the following major versions: 8, 10.");
105+
}
106+
} else {
107+
if (version.startsWith("v8."))
108+
{
109+
let msg = "Incompatible Node.js version. The version you are using (" + version + ") is not supported with Azure Functions V3. Please use one of the following major versions: 10, 12.";
110+
systemError(msg);
111+
throw msg;
112+
}
113+
}
114+
98115
const workerCapabilities = {
99116
RpcHttpTriggerMetadataRemoved: "true",
100117
RpcHttpBodyOnly: "true"
101118
};
119+
102120
this._eventStream.write({
103121
requestId: requestId,
104122
workerInitResponse: {
@@ -146,7 +164,7 @@ export class WorkerChannel implements IWorkerChannel {
146164
*/
147165
public invocationRequest(requestId: string, msg: rpc.InvocationRequest) {
148166
// Repopulate triggerMetaData if http.
149-
if (this._v2Compatible) {
167+
if (this._v1WorkerBehavior) {
150168
augmentTriggerMetadata(msg);
151169
}
152170

@@ -170,8 +188,7 @@ export class WorkerChannel implements IWorkerChannel {
170188
try {
171189
if (result) {
172190
if (result.return) {
173-
// TODO: add capability from host to go to "non-breaking" mode
174-
if (this._v2Compatible) {
191+
if (this._v1WorkerBehavior) {
175192
response.returnValue = toTypedData(result.return);
176193
} else {
177194
let returnBinding = info.getReturnBinding();
@@ -197,7 +214,7 @@ export class WorkerChannel implements IWorkerChannel {
197214
});
198215
}
199216

200-
let { context, inputs } = CreateContextAndInputs(info, msg, logCallback, resultCallback);
217+
let { context, inputs } = CreateContextAndInputs(info, msg, logCallback, resultCallback, this._v1WorkerBehavior);
201218
let userFunction = this._functionLoader.getFunc(<string>msg.functionId);
202219

203220
// catch user errors from the same async context in the event loop and correlate with invocation

src/converters/BindingConverters.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function isBindingDirection(input: string | undefined): boolean {
4343
}
4444

4545
// Recursively convert keys of objects to camel case
46-
function convertKeysToCamelCase(obj: any) {
46+
export function convertKeysToCamelCase(obj: any) {
4747
var output = {};
4848
for (var key in obj) {
4949
let value = fromTypedData(obj[key]) || obj[key];

src/nodejsWorker.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var logPrefix = "LanguageWorkerConsoleLog";
22
var errorPrefix = logPrefix + "[error] ";
33
var warnPrefix = logPrefix + "[warn] ";
4-
var supportedVersions:string[] = ["v10", "v12"];
4+
var supportedVersions:string[] = ["v8", "v10", "v12"];
55
var worker;
66

77
// Try validating node version
@@ -17,12 +17,12 @@ function validateNodeVersion(version) {
1717
message = "Could not parse Node.js version: '" + version + "'";
1818
// Unsupported version note: Documentation about Node's stable versions here: https://github.com/nodejs/Release#release-plan and an explanation here: https://medium.com/swlh/understanding-how-node-releases-work-in-2018-6fd356816db4
1919
} else if (supportedVersions.indexOf(major) < 0) {
20-
message = "Incompatible Node.js version. The version you are using is "
21-
+ version +
22-
", but the runtime requires an LTS-covered major version. LTS-covered versions have an even major version number (10.x, 12.x, etc.) as per https://github.com/nodejs/Release#release-plan. "
23-
+ "For deployed code, change WEBSITE_NODE_DEFAULT_VERSION to '~12' in App Settings. Locally, install or switch to a supported node version (make sure to quit and restart your code editor to pick up the changes).";
20+
message = "Incompatible Node.js version. The version you are using ("
21+
+ version
22+
+ ") is not supported with Azure Functions. Please use one of the following major versions: 10, 12."
23+
+ "For deployed code on Windows, change WEBSITE_NODE_DEFAULT_VERSION to '~12' in App Settings."
24+
+ "Locally, install or switch to a supported node version (make sure to quit and restart your code editor to pick up the changes).";
2425
}
25-
2626
// Unknown error
2727
} catch(err) {
2828
var unknownError = "Error in validating Node.js version. ";

test/ContextTests.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Context } from "../src/public/Interfaces";
33
import { FunctionInfo } from '../src/FunctionInfo';
44
import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc';
55
import * as sinon from 'sinon';
6+
import { expect } from 'chai';
67
import 'mocha';
78
import { isFunction } from 'util';
89

@@ -21,10 +22,60 @@ describe('Context', () => {
2122
_logger = sinon.spy();
2223
_resultCallback = sinon.spy();
2324

24-
let { context, inputs } = CreateContextAndInputs(info, msg, _logger, _resultCallback);
25+
let { context, inputs } = CreateContextAndInputs(info, msg, _logger, _resultCallback, true);
2526
_context = context;
2627
});
2728

29+
it ('camelCases timer trigger input when appropriate', async () => {
30+
var inputDataValue: rpc.IParameterBinding = {
31+
name: "myTimer",
32+
data: {
33+
json: JSON.stringify({
34+
"Schedule":{
35+
},
36+
"ScheduleStatus": {
37+
"Last":"2016-10-04T10:15:00+00:00",
38+
"LastUpdated":"2016-10-04T10:16:00+00:00",
39+
"Next":"2016-10-04T10:20:00+00:00"
40+
},
41+
"IsPastDue":false
42+
})
43+
}
44+
};
45+
var msg: rpc.IInvocationRequest = <rpc.IInvocationRequest> {
46+
functionId: 'id',
47+
invocationId: '1',
48+
inputData: [inputDataValue]
49+
};
50+
51+
let info: FunctionInfo = new FunctionInfo({
52+
name: 'test',
53+
bindings: {
54+
myTimer: {
55+
type: "timerTrigger",
56+
direction: 0,
57+
dataType: 0
58+
}
59+
}
60+
});
61+
// Node.js Worker V2 behavior
62+
let workerV2Outputs = CreateContextAndInputs(info, msg, _logger, _resultCallback, true);
63+
let myTimerWorkerV2 = workerV2Outputs.inputs[0];
64+
expect(myTimerWorkerV2.schedule).to.be.empty;
65+
expect(myTimerWorkerV2.scheduleStatus.last).to.equal("2016-10-04T10:15:00+00:00");
66+
expect(myTimerWorkerV2.scheduleStatus.lastUpdated).to.equal("2016-10-04T10:16:00+00:00");
67+
expect(myTimerWorkerV2.scheduleStatus.next).to.equal("2016-10-04T10:20:00+00:00");
68+
expect(myTimerWorkerV2.isPastDue).to.equal(false);
69+
70+
let workerV1Outputs = CreateContextAndInputs(info, msg, _logger, _resultCallback, false);
71+
let myTimerWorkerV1 = workerV1Outputs.inputs[0];
72+
expect(myTimerWorkerV1.Schedule).to.be.empty;
73+
expect(myTimerWorkerV1.ScheduleStatus.Last).to.equal("2016-10-04T10:15:00+00:00");
74+
expect(myTimerWorkerV1.ScheduleStatus.LastUpdated).to.equal("2016-10-04T10:16:00+00:00");
75+
expect(myTimerWorkerV1.ScheduleStatus.Next).to.equal("2016-10-04T10:20:00+00:00");
76+
expect(myTimerWorkerV1.IsPastDue).to.equal(false);
77+
});
78+
2879
it ('async function logs error on calling context.done', async () => {
2980
await callUserFunc(BasicAsync.asyncAndCallback, _context);
3081
sinon.assert.calledOnce(_logger);

test/FunctionInfoTests.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ describe('FunctionInfo', () => {
2424
};
2525

2626
let funcInfo = new FunctionInfo(metadata);
27-
console.log(funcInfo);
2827
expect(funcInfo.getReturnBinding().converter.name).to.equal("toRpcHttp");
2928
});
3029

@@ -45,7 +44,6 @@ describe('FunctionInfo', () => {
4544
};
4645

4746
let funcInfo = new FunctionInfo(metadata);
48-
console.log(funcInfo);
4947
expect(funcInfo.getReturnBinding().converter.name).to.equal("toTypedData");
5048
});
5149
})

test/WorkerChannelTests.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,18 @@ describe('WorkerChannel', () => {
2020
});
2121

2222
it('responds to init', () => {
23-
stream.addTestMessage({
23+
let initMessage = {
2424
requestId: 'id',
25-
workerInitRequest: { }
26-
});
25+
workerInitRequest: {
26+
capabilities: {}
27+
}
28+
};
29+
30+
if (process.version.startsWith("v8")) {
31+
initMessage.workerInitRequest.capabilities["V2Compatable"] = "true";
32+
}
33+
34+
stream.addTestMessage(initMessage);
2735
sinon.assert.calledWith(stream.written, <rpc.IStreamingMessage>{
2836
requestId: 'id',
2937
workerInitResponse: {
@@ -258,7 +266,10 @@ describe('WorkerChannel', () => {
258266

259267
stream.addTestMessage({
260268
workerInitRequest: {
261-
hostVersion: "2.0"
269+
hostVersion: "3.0.0000",
270+
capabilities: {
271+
V2Compatable: "true"
272+
}
262273
}
263274
})
264275

0 commit comments

Comments
 (0)