diff --git a/package-lock.json b/package-lock.json index 9b02f16f..6bea261c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "blocked-at": "^1.2.0", "fs-extra": "^10.0.1", "globby": "^11.0.0", - "minimist": "^1.2.5" + "minimist": "^1.2.5", + "uuid": "^8.3.2" }, "devDependencies": { "@types/blocked-at": "^1.0.1", @@ -29,6 +30,7 @@ "@types/node": "^16.9.6", "@types/semver": "^7.3.9", "@types/sinon": "^7.0.0", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.12.1", "@typescript-eslint/parser": "^5.12.1", "chai": "^4.2.0", @@ -622,6 +624,12 @@ "integrity": "sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==", "dev": true }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.1.tgz", @@ -5138,6 +5146,12 @@ "integrity": "sha512-T+m89VdXj/eidZyejvmoP9jivXgBDdkOSBVQjU9kF349NEx10QdPNGxHeZUaj1IlJ32/ewdyXJjnJxyxJroYwg==", "dev": true }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.1.tgz", diff --git a/package.json b/package.json index a6147ed3..22d21562 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "blocked-at": "^1.2.0", "fs-extra": "^10.0.1", "globby": "^11.0.0", - "minimist": "^1.2.5" + "minimist": "^1.2.5", + "uuid": "^8.3.2" }, "devDependencies": { "@types/blocked-at": "^1.0.1", @@ -25,6 +26,7 @@ "@types/node": "^16.9.6", "@types/semver": "^7.3.9", "@types/sinon": "^7.0.0", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.12.1", "@typescript-eslint/parser": "^5.12.1", "chai": "^4.2.0", diff --git a/src/FunctionLoader.ts b/src/LegacyFunctionLoader.ts similarity index 71% rename from src/FunctionLoader.ts rename to src/LegacyFunctionLoader.ts index 28844cf5..2fe06628 100644 --- a/src/FunctionLoader.ts +++ b/src/LegacyFunctionLoader.ts @@ -7,21 +7,19 @@ import { loadScriptFile } from './loadScriptFile'; import { PackageJson } from './parsers/parsePackageJson'; import { InternalException } from './utils/InternalException'; import { nonNullProp } from './utils/nonNull'; +import { RegisteredFunction } from './WorkerChannel'; -export interface IFunctionLoader { +export interface ILegacyFunctionLoader { load(functionId: string, metadata: rpc.IRpcFunctionMetadata, packageJson: PackageJson): Promise; - getRpcMetadata(functionId: string): rpc.IRpcFunctionMetadata; - getCallback(functionId: string): FunctionCallback; + getFunction(functionId: string): RegisteredFunction; } -interface LoadedFunction { - metadata: rpc.IRpcFunctionMetadata; - callback: FunctionCallback; +interface LegacyRegisteredFunction extends RegisteredFunction { thisArg: unknown; } -export class FunctionLoader implements IFunctionLoader { - #loadedFunctions: { [k: string]: LoadedFunction | undefined } = {}; +export class LegacyFunctionLoader implements ILegacyFunctionLoader { + #loadedFunctions: { [k: string]: LegacyRegisteredFunction | undefined } = {}; async load(functionId: string, metadata: rpc.IRpcFunctionMetadata, packageJson: PackageJson): Promise { if (metadata.isProxy === true) { @@ -33,21 +31,14 @@ export class FunctionLoader implements IFunctionLoader { this.#loadedFunctions[functionId] = { metadata, callback, thisArg }; } - getRpcMetadata(functionId: string): rpc.IRpcFunctionMetadata { - const loadedFunction = this.#getLoadedFunction(functionId); - return loadedFunction.metadata; - } - - getCallback(functionId: string): FunctionCallback { - const loadedFunction = this.#getLoadedFunction(functionId); - // `bind` is necessary to set the `this` arg, but it's also nice because it makes a clone of the function, preventing this invocation from affecting future invocations - return loadedFunction.callback.bind(loadedFunction.thisArg); - } - - #getLoadedFunction(functionId: string): LoadedFunction { + getFunction(functionId: string): RegisteredFunction { const loadedFunction = this.#loadedFunctions[functionId]; if (loadedFunction) { - return loadedFunction; + return { + metadata: loadedFunction.metadata, + // `bind` is necessary to set the `this` arg, but it's also nice because it makes a clone of the function, preventing this invocation from affecting future invocations + callback: loadedFunction.callback.bind(loadedFunction.thisArg), + }; } else { throw new InternalException(`Function code for '${functionId}' is not loaded and cannot be invoked.`); } diff --git a/src/Worker.ts b/src/Worker.ts index 8c90c0c8..be85aaa2 100644 --- a/src/Worker.ts +++ b/src/Worker.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import * as parseArgs from 'minimist'; -import { FunctionLoader } from './FunctionLoader'; import { CreateGrpcEventStream } from './GrpcClient'; +import { LegacyFunctionLoader } from './LegacyFunctionLoader'; import { setupCoreModule } from './setupCoreModule'; import { setupEventStream } from './setupEventStream'; import { startBlockedMonitor } from './utils/blockedMonitor'; @@ -43,7 +43,7 @@ export function startNodeWorker(args) { throw error; } - const channel = new WorkerChannel(eventStream, new FunctionLoader()); + const channel = new WorkerChannel(eventStream, new LegacyFunctionLoader()); setupEventStream(workerId, channel); setupCoreModule(channel); diff --git a/src/WorkerChannel.ts b/src/WorkerChannel.ts index e32b6052..c1a810b6 100644 --- a/src/WorkerChannel.ts +++ b/src/WorkerChannel.ts @@ -1,19 +1,24 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { HookCallback, HookContext, HookData, ProgrammingModel } from '@azure/functions-core'; +import { FunctionCallback, HookCallback, HookContext, HookData, ProgrammingModel } from '@azure/functions-core'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import { Disposable } from './Disposable'; -import { IFunctionLoader } from './FunctionLoader'; import { IEventStream } from './GrpcClient'; +import { ILegacyFunctionLoader } from './LegacyFunctionLoader'; import { PackageJson, parsePackageJson } from './parsers/parsePackageJson'; import { ensureErrorType } from './utils/ensureErrorType'; import LogLevel = rpc.RpcLog.Level; import LogCategory = rpc.RpcLog.RpcLogCategory; +export interface RegisteredFunction { + metadata: rpc.IRpcFunctionMetadata; + callback: FunctionCallback; +} + export class WorkerChannel { eventStream: IEventStream; - functionLoader: IFunctionLoader; + legacyFunctionLoader: ILegacyFunctionLoader; packageJson: PackageJson; /** * This will only be set after worker init request is received @@ -40,10 +45,12 @@ export class WorkerChannel { #preInvocationHooks: HookCallback[] = []; #postInvocationHooks: HookCallback[] = []; #appStartHooks: HookCallback[] = []; + functions: { [id: string]: RegisteredFunction } = {}; + hasIndexedFunctions = false; - constructor(eventStream: IEventStream, functionLoader: IFunctionLoader) { + constructor(eventStream: IEventStream, legacyFunctionLoader: ILegacyFunctionLoader) { this.eventStream = eventStream; - this.functionLoader = functionLoader; + this.legacyFunctionLoader = legacyFunctionLoader; this.packageJson = {}; } diff --git a/src/coreApi/registerFunction.ts b/src/coreApi/registerFunction.ts new file mode 100644 index 00000000..02bbf130 --- /dev/null +++ b/src/coreApi/registerFunction.ts @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { FunctionCallback, FunctionMetadata, RpcBindingInfo } from '@azure/functions-core'; +import { v4 as uuid } from 'uuid'; +import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; +import { Disposable } from '../Disposable'; +import { InternalException } from '../utils/InternalException'; +import { WorkerChannel } from '../WorkerChannel'; + +export function registerFunction( + channel: WorkerChannel, + metadata: FunctionMetadata, + callback: FunctionCallback +): Disposable { + if (channel.hasIndexedFunctions) { + throw new InternalException('A function can only be registered during app startup.'); + } + const functionId = uuid(); + + const rpcMetadata: rpc.IRpcFunctionMetadata = metadata; + rpcMetadata.functionId = functionId; + // `rawBindings` is what's actually used by the host + // `bindings` is used by the js library in both the old host indexing and the new worker indexing + rpcMetadata.rawBindings = Object.entries(metadata.bindings).map(([name, binding]) => { + return convertToRawBinding(name, binding); + }); + // The host validates that the `scriptFile` property is defined even though neither the host nor the worker needs it + // Long term we should adjust the host to remove that unnecessary validation, but for now we'll just set it to 'n/a' + rpcMetadata.scriptFile = 'n/a'; + channel.functions[functionId] = { metadata: rpcMetadata, callback }; + + return new Disposable(() => { + if (channel.hasIndexedFunctions) { + throw new InternalException('A function can only be disposed during app startup.'); + } else { + delete channel.functions[functionId]; + } + }); +} + +function convertToRawBinding(name: string, binding: RpcBindingInfo): string { + const rawBinding: any = { ...binding, name }; + switch (binding.direction) { + case rpc.BindingInfo.Direction.in: + rawBinding.direction = 'in'; + break; + case rpc.BindingInfo.Direction.out: + rawBinding.direction = 'out'; + break; + case rpc.BindingInfo.Direction.inout: + rawBinding.direction = 'inout'; + break; + } + return JSON.stringify(rawBinding); +} diff --git a/src/eventHandlers/EventHandler.ts b/src/eventHandlers/EventHandler.ts index d1ecab01..43d9f5ac 100644 --- a/src/eventHandlers/EventHandler.ts +++ b/src/eventHandlers/EventHandler.ts @@ -8,14 +8,16 @@ export type SupportedRequestName = | 'functionEnvironmentReloadRequest' | 'functionLoadRequest' | 'invocationRequest' - | 'workerInitRequest'; + | 'workerInitRequest' + | 'functionsMetadataRequest'; export type SupportedRequest = rpc.StreamingMessage[SupportedRequestName]; export type SupportedResponseName = | 'functionEnvironmentReloadResponse' | 'functionLoadResponse' | 'invocationResponse' - | 'workerInitResponse'; + | 'workerInitResponse' + | 'functionMetadataResponse'; export type SupportedResponse = rpc.StreamingMessage[SupportedResponseName]; export abstract class EventHandler< diff --git a/src/eventHandlers/FunctionLoadHandler.ts b/src/eventHandlers/FunctionLoadHandler.ts index 1f66a4c9..5b26d640 100644 --- a/src/eventHandlers/FunctionLoadHandler.ts +++ b/src/eventHandlers/FunctionLoadHandler.ts @@ -22,13 +22,15 @@ export class FunctionLoadHandler extends EventHandler<'functionLoadRequest', 'fu const functionId = nonNullProp(msg, 'functionId'); const metadata = nonNullProp(msg, 'metadata'); - try { - await channel.functionLoader.load(functionId, metadata, channel.packageJson); - } catch (err) { - const error = ensureErrorType(err); - error.isAzureFunctionsInternalException = true; - error.message = `Worker was unable to load function ${metadata.name}: '${error.message}'`; - throw error; + if (!channel.functions[functionId]) { + try { + await channel.legacyFunctionLoader.load(functionId, metadata, channel.packageJson); + } catch (err) { + const error = ensureErrorType(err); + error.isAzureFunctionsInternalException = true; + error.message = `Worker was unable to load function ${metadata.name}: '${error.message}'`; + throw error; + } } return response; diff --git a/src/eventHandlers/FunctionsMetadataHandler.ts b/src/eventHandlers/FunctionsMetadataHandler.ts new file mode 100644 index 00000000..75bed62f --- /dev/null +++ b/src/eventHandlers/FunctionsMetadataHandler.ts @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. + +import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; +import { WorkerChannel } from '../WorkerChannel'; +import { EventHandler } from './EventHandler'; +import LogCategory = rpc.RpcLog.RpcLogCategory; +import LogLevel = rpc.RpcLog.Level; + +export class FunctionsMetadataHandler extends EventHandler<'functionsMetadataRequest', 'functionMetadataResponse'> { + readonly responseName = 'functionMetadataResponse'; + + getDefaultResponse(_msg: rpc.IFunctionsMetadataRequest): rpc.IFunctionMetadataResponse { + return { + useDefaultMetadataIndexing: true, + }; + } + + async handleEvent( + channel: WorkerChannel, + msg: rpc.IFunctionsMetadataRequest + ): Promise { + const response = this.getDefaultResponse(msg); + + channel.log({ + message: 'Received FunctionsMetadataRequest', + level: LogLevel.Debug, + logCategory: LogCategory.System, + }); + + const functions = Object.values(channel.functions); + if (functions.length > 0) { + response.useDefaultMetadataIndexing = false; + response.functionMetadataResults = functions.map((f) => f.metadata); + } + + channel.hasIndexedFunctions = true; + + return response; + } +} diff --git a/src/eventHandlers/InvocationHandler.ts b/src/eventHandlers/InvocationHandler.ts index f9b22e3e..70f3c172 100644 --- a/src/eventHandlers/InvocationHandler.ts +++ b/src/eventHandlers/InvocationHandler.ts @@ -31,7 +31,8 @@ export class InvocationHandler extends EventHandler<'invocationRequest', 'invoca async handleEvent(channel: WorkerChannel, msg: rpc.IInvocationRequest): Promise { const functionId = nonNullProp(msg, 'functionId'); - const metadata = channel.functionLoader.getRpcMetadata(functionId); + let { metadata, callback } = + channel.functions[functionId] || channel.legacyFunctionLoader.getFunction(functionId); const msgCategory = `${nonNullProp(metadata, 'name')}.Invocation`; const coreCtx = new CoreInvocationContext(channel, msg, metadata, msgCategory); @@ -43,7 +44,6 @@ export class InvocationHandler extends EventHandler<'invocationRequest', 'invoca const hookData: HookData = {}; let { context, inputs } = await invocModel.getArguments(); - let callback = channel.functionLoader.getCallback(functionId); const preInvocContext: PreInvocationContext = { hookData, diff --git a/src/setupCoreModule.ts b/src/setupCoreModule.ts index 3defce6c..95e72bdd 100644 --- a/src/setupCoreModule.ts +++ b/src/setupCoreModule.ts @@ -1,9 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. -import { HookCallback, ProgrammingModel } from '@azure/functions-core'; +import { FunctionCallback, FunctionMetadata, HookCallback, ProgrammingModel } from '@azure/functions-core'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; import { version } from './constants'; +import { registerFunction } from './coreApi/registerFunction'; import { Disposable } from './Disposable'; import { WorkerChannel } from './WorkerChannel'; import Module = require('module'); @@ -36,6 +37,9 @@ export function setupCoreModule(channel: WorkerChannel): void { getProgrammingModel: () => { return channel.programmingModel; }, + registerFunction: (metadata: FunctionMetadata, callback: FunctionCallback) => { + return registerFunction(channel, metadata, callback); + }, Disposable, // NOTE: We have to pass along any and all enums used in the RPC api to the core api RpcLog: rpc.RpcLog, diff --git a/src/setupEventStream.ts b/src/setupEventStream.ts index 735daa5d..73a284ad 100644 --- a/src/setupEventStream.ts +++ b/src/setupEventStream.ts @@ -5,6 +5,7 @@ import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-wo import { EventHandler, SupportedRequest } from './eventHandlers/EventHandler'; import { FunctionEnvironmentReloadHandler } from './eventHandlers/FunctionEnvironmentReloadHandler'; import { FunctionLoadHandler } from './eventHandlers/FunctionLoadHandler'; +import { FunctionsMetadataHandler } from './eventHandlers/FunctionsMetadataHandler'; import { InvocationHandler } from './eventHandlers/InvocationHandler'; import { WorkerInitHandler } from './eventHandlers/WorkerInitHandler'; import { ensureErrorType } from './utils/ensureErrorType'; @@ -71,10 +72,12 @@ async function handleMessage(workerId: string, channel: WorkerChannel, inMsg: rp outMsg.workerStatusResponse = {}; channel.eventStream.write(outMsg); return; + case 'functionsMetadataRequest': + eventHandler = new FunctionsMetadataHandler(); + break; case 'closeSharedMemoryResourcesRequest': case 'fileChangeEventRequest': case 'functionLoadRequestCollection': - case 'functionsMetadataRequest': case 'invocationCancel': case 'startStream': case 'workerHeartbeat': diff --git a/test/FunctionLoader.test.ts b/test/LegacyFunctionLoader.test.ts similarity index 89% rename from test/FunctionLoader.test.ts rename to test/LegacyFunctionLoader.test.ts index f0556f86..8b60158f 100644 --- a/test/FunctionLoader.test.ts +++ b/test/LegacyFunctionLoader.test.ts @@ -6,16 +6,16 @@ import * as chaiAsPromised from 'chai-as-promised'; import 'mocha'; import * as mock from 'mock-require'; import { AzureFunctionsRpcMessages as rpc } from '../azure-functions-language-worker-protobuf/src/rpc'; -import { FunctionLoader } from '../src/FunctionLoader'; +import { LegacyFunctionLoader } from '../src/LegacyFunctionLoader'; const expect = chai.expect; chai.use(chaiAsPromised); -describe('FunctionLoader', () => { - let loader: FunctionLoader; +describe('LegacyFunctionLoader', () => { + let loader: LegacyFunctionLoader; let context, logs; beforeEach(() => { - loader = new FunctionLoader(); + loader = new LegacyFunctionLoader(); logs = []; context = { _inputs: [], @@ -53,7 +53,7 @@ describe('FunctionLoader', () => { ); expect(() => { - loader.getCallback('functionId'); + loader.getFunction('functionId'); }).to.throw("Function code for 'functionId' is not loaded and cannot be invoked."); }); @@ -118,9 +118,9 @@ describe('FunctionLoader', () => { {} ); - const userFunction = loader.getCallback('functionId'); + const userFunction = loader.getFunction('functionId'); - userFunction(context, (results) => { + userFunction.callback(context, (results) => { expect(results).to.eql({ prop: true }); }); }); @@ -137,8 +137,8 @@ describe('FunctionLoader', () => { {} ); - const userFunction = loader.getCallback('functionId'); - const result = userFunction({}); + const userFunction = loader.getFunction('functionId'); + const result = userFunction.callback({}); expect(result).to.be.not.an('undefined'); expect((result).then).to.be.a('function'); @@ -156,10 +156,10 @@ describe('FunctionLoader', () => { {} ); - const userFunction = loader.getCallback('functionId'); + const userFunction = loader.getFunction('functionId').callback; Object.assign(userFunction, { hello: 'world' }); - const userFunction2 = loader.getCallback('functionId'); + const userFunction2 = loader.getFunction('functionId').callback; expect(userFunction).to.not.equal(userFunction2); expect(userFunction['hello']).to.equal('world'); diff --git a/test/eventHandlers/FunctionLoadHandler.test.ts b/test/eventHandlers/FunctionLoadHandler.test.ts index 5869b80c..bf4e980b 100644 --- a/test/eventHandlers/FunctionLoadHandler.test.ts +++ b/test/eventHandlers/FunctionLoadHandler.test.ts @@ -4,7 +4,7 @@ import 'mocha'; import * as sinon from 'sinon'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; -import { FunctionLoader } from '../../src/FunctionLoader'; +import { LegacyFunctionLoader } from '../../src/LegacyFunctionLoader'; import { PackageJson } from '../../src/parsers/parsePackageJson'; import { beforeEventHandlerSuite } from './beforeEventHandlerSuite'; import { TestEventStream } from './TestEventStream'; @@ -13,7 +13,7 @@ import LogLevel = rpc.RpcLog.Level; describe('FunctionLoadHandler', () => { let stream: TestEventStream; - let loader: sinon.SinonStubbedInstance; + let loader: sinon.SinonStubbedInstance; before(() => { ({ stream, loader } = beforeEventHandlerSuite()); diff --git a/test/eventHandlers/InvocationHandler.test.ts b/test/eventHandlers/InvocationHandler.test.ts index f5addbc4..fbbc9462 100644 --- a/test/eventHandlers/InvocationHandler.test.ts +++ b/test/eventHandlers/InvocationHandler.test.ts @@ -9,7 +9,7 @@ import { expect } from 'chai'; import 'mocha'; import * as sinon from 'sinon'; import { AzureFunctionsRpcMessages as rpc } from '../../azure-functions-language-worker-protobuf/src/rpc'; -import { FunctionLoader } from '../../src/FunctionLoader'; +import { LegacyFunctionLoader } from '../../src/LegacyFunctionLoader'; import { WorkerChannel } from '../../src/WorkerChannel'; import { Msg as AppStartMsg } from '../startApp.test'; import { beforeEventHandlerSuite } from './beforeEventHandlerSuite'; @@ -322,7 +322,9 @@ namespace InputData { } type TestFunctionLoader = sinon.SinonStubbedInstance< - FunctionLoader & { getCallback(functionId: string): AzureFunction } + LegacyFunctionLoader & { + getFunction(functionId: string): { metadata: coreTypes.RpcFunctionMetadata; callback: AzureFunction }; + } >; describe('InvocationHandler', () => { @@ -388,8 +390,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.basic) { it('invokes function' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.httpRes); + loader.getFunction.returns({ + metadata: Binding.httpRes, + callback: func, + }); sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), @@ -401,8 +405,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.returnHttp) { it('returns correct data with $return binding' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.httpReturn); + loader.getFunction.returns({ + metadata: Binding.httpReturn, + callback: func, + }); sendInvokeMessage([InputData.http]); const expectedOutput = getHttpResponse(undefined, '$return'); const expectedReturnValue = { @@ -422,8 +428,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.returnArray) { it('returns returned output if not http' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); sendInvokeMessage([]); const expectedReturnValue = { json: '["hello, seattle!","hello, tokyo!"]', @@ -434,8 +442,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.returnArray) { it('returned output is ignored if http' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.httpRes); + loader.getFunction.returns({ + metadata: Binding.httpRes, + callback: func, + }); sendInvokeMessage([]); await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([], undefined)); }); @@ -443,8 +453,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.resHttp) { it('serializes output binding data through context.done' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.httpRes); + loader.getFunction.returns({ + metadata: Binding.httpRes, + callback: func, + }); sendInvokeMessage([InputData.http]); const expectedOutput = [getHttpResponse({ hello: 'world' })]; await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse(expectedOutput)); @@ -453,15 +465,17 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.multipleBindings) { it('serializes multiple output bindings through context.done and context.bindings' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns({ - bindings: { - req: Binding.httpInput, - res: Binding.httpOutput, - queueOutput: Binding.queueOutput, - overriddenQueueOutput: Binding.queueOutput, + loader.getFunction.returns({ + metadata: { + bindings: { + req: Binding.httpInput, + res: Binding.httpOutput, + queueOutput: Binding.queueOutput, + overriddenQueueOutput: Binding.queueOutput, + }, + name: 'testFuncName', }, - name: 'testFuncName', + callback: func, }); sendInvokeMessage([InputData.http]); const expectedOutput = [ @@ -485,8 +499,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.error) { it('returns failed status for user error' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); sendInvokeMessage([InputData.http]); await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResFailed); }); @@ -501,17 +517,21 @@ describe('InvocationHandler', () => { }); it('empty function does not return invocation response', async () => { - loader.getCallback.returns(() => {}); - loader.getRpcMetadata.returns(Binding.httpRes); + loader.getFunction.returns({ + callback: () => {}, + metadata: Binding.httpRes, + }); sendInvokeMessage([InputData.http]); await stream.assertCalledWith(Msg.receivedInvocLog()); }); it('logs error on calling context.done in async function', async () => { - loader.getCallback.returns(async (context: Context) => { - context.done(); + loader.getFunction.returns({ + callback: async (context: Context) => { + context.done(); + }, + metadata: Binding.httpRes, }); - loader.getRpcMetadata.returns(Binding.httpRes); sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), @@ -521,11 +541,13 @@ describe('InvocationHandler', () => { }); it('logs error on calling context.done more than once', async () => { - loader.getCallback.returns((context: Context) => { - context.done(); - context.done(); + loader.getFunction.returns({ + callback: (context: Context) => { + context.done(); + context.done(); + }, + metadata: Binding.httpRes, }); - loader.getRpcMetadata.returns(Binding.httpRes); sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), @@ -535,11 +557,13 @@ describe('InvocationHandler', () => { }); it('logs error on calling context.log after context.done', async () => { - loader.getCallback.returns((context: Context) => { - context.done(); - context.log('testUserLog'); + loader.getFunction.returns({ + callback: (context: Context) => { + context.done(); + context.log('testUserLog'); + }, + metadata: Binding.httpRes, }); - loader.getRpcMetadata.returns(Binding.httpRes); sendInvokeMessage([InputData.http]); await stream.assertCalledWith( Msg.receivedInvocLog(), @@ -551,11 +575,13 @@ describe('InvocationHandler', () => { it('logs error on calling context.log after async function', async () => { let _context: Context; - loader.getCallback.returns(async (context: Context) => { - _context = context; - return 'hello'; + loader.getFunction.returns({ + callback: async (context: Context) => { + _context = context; + return 'hello'; + }, + metadata: Binding.httpRes, }); - loader.getRpcMetadata.returns(Binding.httpRes); sendInvokeMessage([InputData.http]); // wait for first two messages to ensure invocation happens await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([getHttpResponse()])); @@ -566,8 +592,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.logHookData) { it('preInvocationHook' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); testDisposables.push( coreApi.registerHook('preInvocation', () => { @@ -589,8 +617,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.logInput) { it('preInvocationHook respects change to inputs' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); testDisposables.push( coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { @@ -612,10 +642,12 @@ describe('InvocationHandler', () => { } it('preInvocationHook respects change to functionCallback', async () => { - loader.getCallback.returns(async (invocContext: Context) => { - invocContext.log('old function'); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async (invocContext: Context) => { + invocContext.log('old function'); + }, }); - loader.getRpcMetadata.returns(Binding.queue); testDisposables.push( coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { @@ -638,8 +670,11 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.logHookData) { it('postInvocationHook' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + channel.functions; + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); testDisposables.push( coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { @@ -665,8 +700,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.logHookData) { it('postInvocationHook respects change to context.result' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); testDisposables.push( coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { @@ -691,8 +728,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.error) { it('postInvocationHook executes if function throws error' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); testDisposables.push( coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { @@ -715,8 +754,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.error) { it('postInvocationHook respects change to context.error' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: func, + }); testDisposables.push( coreApi.registerHook('postInvocation', (context: coreTypes.PostInvocationContext) => { @@ -740,8 +781,10 @@ describe('InvocationHandler', () => { } it('pre and post invocation hooks share data', async () => { - loader.getCallback.returns(async () => {}); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async () => {}, + }); testDisposables.push( coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { @@ -794,8 +837,10 @@ describe('InvocationHandler', () => { ); expect(startFunc.callCount).to.be.equal(1); - loader.getCallback.returns(async () => {}); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async () => {}, + }); testDisposables.push( coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { @@ -847,8 +892,10 @@ describe('InvocationHandler', () => { ); expect(startFunc.callCount).to.be.equal(1); - loader.getCallback.returns(async () => {}); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async () => {}, + }); testDisposables.push( coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { @@ -887,8 +934,10 @@ describe('InvocationHandler', () => { }, }; - loader.getCallback.returns(async () => {}); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async () => {}, + }); testDisposables.push( coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { @@ -931,8 +980,10 @@ describe('InvocationHandler', () => { }, }; - loader.getCallback.returns(async () => {}); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async () => {}, + }); const pre1 = coreApi.registerHook('preInvocation', (context: coreTypes.PreInvocationContext) => { Object.assign(context.appHookData, expectedAppHookData); @@ -990,8 +1041,10 @@ describe('InvocationHandler', () => { }); it('dispose hooks', async () => { - loader.getCallback.returns(async () => {}); - loader.getRpcMetadata.returns(Binding.queue); + loader.getFunction.returns({ + metadata: Binding.queue, + callback: async () => {}, + }); const disposableA: coreTypes.Disposable = coreApi.registerHook('preInvocation', () => { hookData += 'a'; @@ -1029,8 +1082,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.returnEmptyString) { it('returns and serializes falsy value in Durable: ""' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.activity); + loader.getFunction.returns({ + metadata: Binding.activity, + callback: func, + }); sendInvokeMessage([]); const expectedReturnValue = { string: '' }; await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([], expectedReturnValue)); @@ -1039,8 +1094,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.returnZero) { it('returns and serializes falsy value in Durable: 0' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.activity); + loader.getFunction.returns({ + metadata: Binding.activity, + callback: func, + }); sendInvokeMessage([]); const expectedReturnValue = { int: 0 }; await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([], expectedReturnValue)); @@ -1049,8 +1106,10 @@ describe('InvocationHandler', () => { for (const [func, suffix] of TestFunc.returnFalse) { it('returns and serializes falsy value in Durable: false' + suffix, async () => { - loader.getCallback.returns(func); - loader.getRpcMetadata.returns(Binding.activity); + loader.getFunction.returns({ + metadata: Binding.activity, + callback: func, + }); sendInvokeMessage([]); const expectedReturnValue = { json: 'false' }; await stream.assertCalledWith(Msg.receivedInvocLog(), Msg.invocResponse([], expectedReturnValue)); diff --git a/test/eventHandlers/beforeEventHandlerSuite.ts b/test/eventHandlers/beforeEventHandlerSuite.ts index 8460dadf..7c05c12f 100644 --- a/test/eventHandlers/beforeEventHandlerSuite.ts +++ b/test/eventHandlers/beforeEventHandlerSuite.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import * as sinon from 'sinon'; -import { FunctionLoader } from '../../src/FunctionLoader'; +import { LegacyFunctionLoader } from '../../src/LegacyFunctionLoader'; import { setupCoreModule } from '../../src/setupCoreModule'; import { setupEventStream } from '../../src/setupEventStream'; import { WorkerChannel } from '../../src/WorkerChannel'; @@ -11,7 +11,7 @@ import { TestEventStream } from './TestEventStream'; let testWorkerData: | { stream: TestEventStream; - loader: sinon.SinonStubbedInstance; + loader: sinon.SinonStubbedInstance; channel: WorkerChannel; } | undefined = undefined; @@ -19,7 +19,7 @@ let testWorkerData: export function beforeEventHandlerSuite() { if (!testWorkerData) { const stream = new TestEventStream(); - const loader = sinon.createStubInstance(FunctionLoader); + const loader = sinon.createStubInstance(LegacyFunctionLoader); const channel = new WorkerChannel(stream, loader); setupEventStream('workerId', channel); setupCoreModule(channel); diff --git a/types-core/index.d.ts b/types-core/index.d.ts index dd1a2808..407bcb66 100644 --- a/types-core/index.d.ts +++ b/types-core/index.d.ts @@ -15,6 +15,27 @@ declare module '@azure/functions-core' { */ const hostVersion: string; + /** + * Register a function + * This is a preview feature and requires the feature flag `EnableWorkerIndexing` to be set in the app setting `AzureWebJobsFeatureFlags` + */ + function registerFunction(metadata: FunctionMetadata, callback: FunctionCallback): Disposable; + + /** + * A slimmed down version of `RpcFunctionMetadata` that includes the minimum amount of information needed to register a function + */ + interface FunctionMetadata { + /** + * The function name + */ + name: string; + + /** + * A dictionary of binding name to binding info + */ + bindings: { [name: string]: RpcBindingInfo }; + } + /** * Register a hook to interact with the lifecycle of Azure Functions. * Hooks are executed in the order they were registered and will block execution if they throw an error diff --git a/worker.config.json b/worker.config.json index c2fcd2fb..4681958f 100644 --- a/worker.config.json +++ b/worker.config.json @@ -3,6 +3,7 @@ "language":"node", "extensions":[".js", ".mjs", ".cjs"], "defaultExecutablePath":"node", - "defaultWorkerPath":"dist/src/nodejsWorker.js" + "defaultWorkerPath":"dist/src/nodejsWorker.js", + "workerIndexing": "true" } } \ No newline at end of file