Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add v4 support for specifying retry policy #106

Merged
merged 2 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
CosmosDBTrigger,
EventGridFunctionOptions,
EventHubFunctionOptions,
FunctionOptions,
ExponentialBackoffRetryOptions,
FixedDelayRetryOptions,
GenericFunctionOptions,
HttpFunctionOptions,
HttpHandler,
HttpMethod,
Expand All @@ -21,6 +23,7 @@ import * as coreTypes from '@azure/functions-core';
import { CoreInvocationContext, FunctionCallback } from '@azure/functions-core';
import { InvocationModel } from './InvocationModel';
import { returnBindingKey, version } from './constants';
import { toRpcDuration } from './converters/toRpcDuration';
import * as output from './output';
import * as trigger from './trigger';
import { isTrigger } from './utils/isTrigger';
Expand Down Expand Up @@ -231,7 +234,7 @@ export function cosmosDB(name: string, options: CosmosDBFunctionOptions): void {
});
}

export function generic(name: string, options: FunctionOptions): void {
export function generic(name: string, options: GenericFunctionOptions): void {
if (!hasSetup) {
setup();
}
Expand Down Expand Up @@ -271,12 +274,29 @@ export function generic(name: string, options: FunctionOptions): void {
}
}

let retryOptions: coreTypes.RpcRetryOptions | undefined;
if (options.retry) {
retryOptions = {
...options.retry,
hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
retryStrategy: options.retry.strategy,
delayInterval: toRpcDuration((<FixedDelayRetryOptions>options.retry).delayInterval, 'retry.delayInterval'),
maximumInterval: toRpcDuration(
(<ExponentialBackoffRetryOptions>options.retry).maximumInterval,
'retry.maximumInterval'
),
minimumInterval: toRpcDuration(
(<ExponentialBackoffRetryOptions>options.retry).minimumInterval,
'retry.minimumInterval'
),
};
}

const coreApi = tryGetCoreApiLazy();
if (!coreApi) {
console.warn(
`WARNING: Skipping call to register function "${name}" because the "@azure/functions" package is in test mode.`
);
} else {
coreApi.registerFunction({ name, bindings }, <FunctionCallback>options.handler);
coreApi.registerFunction({ name, bindings, retryOptions }, <FunctionCallback>options.handler);
}
}
36 changes: 36 additions & 0 deletions src/converters/toRpcDuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { RpcDuration } from '@azure/functions-core';
import { Duration } from '../../types';
import { AzFuncSystemError } from '../errors';
import { isDefined } from '../utils/nonNull';

export function toRpcDuration(dateTime: Duration | number | undefined, propertyName: string): RpcDuration | undefined {
if (isDefined(dateTime)) {
try {
let timeInMilliseconds: number | undefined;
if (typeof dateTime === 'object') {
const minutes = (dateTime.minutes || 0) + (dateTime.hours || 0) * 60;
const seconds = (dateTime.seconds || 0) + minutes * 60;
timeInMilliseconds = (dateTime.milliseconds || 0) + seconds * 1000;
} else if (typeof dateTime === 'number') {
timeInMilliseconds = dateTime;
}

if (isDefined(timeInMilliseconds) && timeInMilliseconds >= 0) {
return {
seconds: Math.round(timeInMilliseconds / 1000),
hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
};
}
} catch {
// fall through
}

throw new AzFuncSystemError(
`A 'number' or 'Duration' object was expected instead of a '${typeof dateTime}'. Cannot parse value of '${propertyName}'.`
);
}

return undefined;
}
56 changes: 56 additions & 0 deletions test/converters/toRpcDuration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { expect } from 'chai';
import 'mocha';
import { toRpcDuration } from '../../src/converters/toRpcDuration';

describe('toRpcDuration', () => {
it('number', () => {
const value = toRpcDuration(5000, 'test');
expect(value).to.deep.equal({ seconds: 5 });
});

it('zero', () => {
const value = toRpcDuration(0, 'test');
expect(value).to.deep.equal({ seconds: 0 });
});

it('milliseconds', () => {
const value = toRpcDuration({ milliseconds: 6000 }, 'test');
expect(value).to.deep.equal({ seconds: 6 });
});

it('seconds', () => {
const value = toRpcDuration({ seconds: 7 }, 'test');
expect(value).to.deep.equal({ seconds: 7 });
});

it('minutes', () => {
const value = toRpcDuration({ minutes: 3 }, 'test');
expect(value).to.deep.equal({ seconds: 180 });
});

it('hours', () => {
const value = toRpcDuration({ hours: 2 }, 'test');
expect(value).to.deep.equal({ seconds: 7200 });
});

it('combined', () => {
const value = toRpcDuration({ hours: 1, minutes: 1, seconds: 1, milliseconds: 1000 }, 'test');
expect(value).to.deep.equal({ seconds: 3662 });
});

it('throws and does not convert string', () => {
expect(() => {
toRpcDuration(<any>'invalid', 'test');
}).to.throw(
"A 'number' or 'Duration' object was expected instead of a 'string'. Cannot parse value of 'test'."
);
});

it('does not convert null', () => {
const value = toRpcDuration(<any>null, 'test');
expect(value).to.be.undefined;
});
});
27 changes: 27 additions & 0 deletions types-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ declare module '@azure/functions-core' {
* A dictionary of binding name to binding info
*/
bindings: { [name: string]: RpcBindingInfo };

/**
* The retry policy options
*/
retryOptions?: RpcRetryOptions;
}

/**
Expand Down Expand Up @@ -298,6 +303,8 @@ declare module '@azure/functions-core' {
functionId?: string | null;

managedDependencyEnabled?: boolean | null;

retryOptions?: RpcRetryOptions | null;
}

interface RpcStatusResult {
Expand Down Expand Up @@ -352,6 +359,20 @@ declare module '@azure/functions-core' {

type RpcBindingDataType = 'undefined' | 'string' | 'binary' | 'stream';

interface RpcRetryOptions {
maxRetryCount?: number | null;

delayInterval?: RpcDuration | null;

minimumInterval?: RpcDuration | null;

maximumInterval?: RpcDuration | null;

retryStrategy?: RpcRetryStrategy | null;
}

type RpcRetryStrategy = 'exponentialBackoff' | 'fixedDelay';

interface RpcTypedData {
string?: string | null;

Expand Down Expand Up @@ -508,6 +529,12 @@ declare module '@azure/functions-core' {
nanos?: number | null;
}

interface RpcDuration {
seconds?: number | Long | null;

nanos?: number | null;
}

type RpcHttpCookieSameSite = 'none' | 'lax' | 'strict' | 'explicitNone';
// #endregion rpc types
}
4 changes: 2 additions & 2 deletions types/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import { CosmosDBFunctionOptions } from './cosmosDB';
import { EventGridFunctionOptions } from './eventGrid';
import { EventHubFunctionOptions } from './eventHub';
import { GenericFunctionOptions } from './generic';
import { HttpFunctionOptions, HttpHandler, HttpMethodFunctionOptions } from './http';
import { FunctionOptions } from './index';
import { ServiceBusQueueFunctionOptions, ServiceBusTopicFunctionOptions } from './serviceBus';
import { StorageBlobFunctionOptions, StorageQueueFunctionOptions } from './storage';
import { TimerFunctionOptions } from './timer';
Expand Down Expand Up @@ -149,4 +149,4 @@ export function cosmosDB(name: string, options: CosmosDBFunctionOptions): void;
* @param name The name of the function. The name must be unique within your app and will mostly be used for your own tracking purposes
* @param options Configuration options describing the inputs, outputs, and handler for this function
*/
export function generic(name: string, options: FunctionOptions): void;
export function generic(name: string, options: GenericFunctionOptions): void;
8 changes: 7 additions & 1 deletion types/cosmosDB.v3.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { FunctionInput, FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger } from './index';
import { FunctionInput, FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger, RetryOptions } from './index';
import { InvocationContext } from './InvocationContext';

export type CosmosDBv3Handler = (documents: unknown[], context: InvocationContext) => FunctionResult;
Expand All @@ -10,6 +10,12 @@ export interface CosmosDBv3FunctionOptions extends CosmosDBv3TriggerOptions, Par
handler: CosmosDBv3Handler;

trigger?: CosmosDBv3Trigger;

/**
* An optional retry policy to rerun a failed execution until either successful completion occurs or the maximum number of retries is reached.
* Learn more [here](https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages)
*/
retry?: RetryOptions;
}

export interface CosmosDBv3InputOptions {
Expand Down
8 changes: 7 additions & 1 deletion types/cosmosDB.v4.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { FunctionInput, FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger } from './index';
import { FunctionInput, FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger, RetryOptions } from './index';
import { InvocationContext } from './InvocationContext';

export type CosmosDBv4Handler = (documents: unknown[], context: InvocationContext) => FunctionResult;
Expand All @@ -10,6 +10,12 @@ export interface CosmosDBv4FunctionOptions extends CosmosDBv4TriggerOptions, Par
handler: CosmosDBv4Handler;

trigger?: CosmosDBv4Trigger;

/**
* An optional retry policy to rerun a failed execution until either successful completion occurs or the maximum number of retries is reached.
* Learn more [here](https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages)
*/
retry?: RetryOptions;
}

export interface CosmosDBv4InputOptions {
Expand Down
8 changes: 7 additions & 1 deletion types/eventHub.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger } from './index';
import { FunctionOptions, FunctionOutput, FunctionResult, FunctionTrigger, RetryOptions } from './index';
import { InvocationContext } from './InvocationContext';

export type EventHubHandler = (messages: unknown, context: InvocationContext) => FunctionResult;
Expand All @@ -10,6 +10,12 @@ export interface EventHubFunctionOptions extends EventHubTriggerOptions, Partial
handler: EventHubHandler;

trigger?: EventHubTrigger;

/**
* An optional retry policy to rerun a failed execution until either successful completion occurs or the maximum number of retries is reached.
* Learn more [here](https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages)
*/
retry?: RetryOptions;
}

export interface EventHubTriggerOptions {
Expand Down
10 changes: 10 additions & 0 deletions types/generic.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { FunctionOptions, RetryOptions } from './index';

export interface GenericFunctionOptions extends FunctionOptions {
/**
* An optional retry policy to rerun a failed execution until either successful completion occurs or the maximum number of retries is reached.
* Learn more [here](https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages)
*/
retry?: RetryOptions;
}

hossam-nasr marked this conversation as resolved.
Show resolved Hide resolved
export interface GenericTriggerOptions extends Record<string, unknown> {
type: string;
}
Expand Down
53 changes: 53 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,56 @@ export interface FunctionOutput extends Record<string, unknown> {
*/
name: string;
}

export type RetryOptions = FixedDelayRetryOptions | ExponentialBackoffRetryOptions;

export interface FixedDelayRetryOptions {
/**
* A specified amount of time is allowed to elapse between each retry.
*/
strategy: 'fixedDelay';

/**
* The maximum number of retries allowed per function execution. -1 means to retry indefinitely.
*/
maxRetryCount: number;

/**
* The delay that's used between retries.
* This can be a number in milliseconds or a Duration object
*/
delayInterval: Duration | number;
}

export interface ExponentialBackoffRetryOptions {
/**
* The first retry waits for the minimum delay. On subsequent retries, time is added exponentially to
* the initial duration for each retry, until the maximum delay is reached. Exponential back-off adds
* some small randomization to delays to stagger retries in high-throughput scenarios.
*/
strategy: 'exponentialBackoff';

/**
* The maximum number of retries allowed per function execution. -1 means to retry indefinitely.
*/
maxRetryCount: number;

/**
* The minimum retry delay.
* This can be a number in milliseconds, or a Duration object
*/
minimumInterval: Duration | number;

/**
* The maximum retry delay.
* This can be a number in milliseconds, or a Duration object
*/
maximumInterval: Duration | number;
}

export interface Duration {
hours?: number;
minutes?: number;
seconds?: number;
milliseconds?: number;
}
8 changes: 7 additions & 1 deletion types/timer.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import { FunctionOptions, FunctionResult, FunctionTrigger } from './index';
import { FunctionOptions, FunctionResult, FunctionTrigger, RetryOptions } from './index';
import { InvocationContext } from './InvocationContext';

export type TimerHandler = (myTimer: Timer, context: InvocationContext) => FunctionResult;
Expand All @@ -10,6 +10,12 @@ export interface TimerFunctionOptions extends TimerTriggerOptions, Partial<Funct
handler: TimerHandler;

trigger?: TimerTrigger;

/**
* An optional retry policy to rerun a failed execution until either successful completion occurs or the maximum number of retries is reached.
* Learn more [here](https://learn.microsoft.com/azure/azure-functions/functions-bindings-error-pages)
*/
retry?: RetryOptions;
}

export interface TimerTriggerOptions {
Expand Down