Skip to content

Commit

Permalink
Expose timeout property. (#2358)
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolk authored Dec 19, 2024
1 parent fbf209e commit a66f5f2
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/few-oranges-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@aws-amplify/ai-constructs': minor
'@aws-amplify/backend-ai': minor
---

Expose timeout property
1 change: 1 addition & 0 deletions packages/ai-constructs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type ConversationHandlerFunctionProps = {
region?: string;
}>;
memoryMB?: number;
timeoutSeconds?: number;
logging?: {
level?: ApplicationLogLevel;
retention?: RetentionDays;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,68 @@ void describe('Conversation Handler Function construct', () => {
});
});

void describe('timeout property', () => {
void it('sets valid timeout', () => {
const app = new App();
const stack = new Stack(app);
new ConversationHandlerFunction(stack, 'conversationHandler', {
models: [],
timeoutSeconds: 124,
});
const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::Lambda::Function', {
Timeout: 124,
});
});

void it('sets default timeout', () => {
const app = new App();
const stack = new Stack(app);
new ConversationHandlerFunction(stack, 'conversationHandler', {
models: [],
});
const template = Template.fromStack(stack);

template.hasResourceProperties('AWS::Lambda::Function', {
Timeout: 60,
});
});

void it('throws on timeout below 1', () => {
assert.throws(() => {
const app = new App();
const stack = new Stack(app);
new ConversationHandlerFunction(stack, 'conversationHandler', {
models: [],
timeoutSeconds: 0,
});
}, new Error('timeoutSeconds must be a whole number between 1 and 900 inclusive'));
});

void it('throws on timeout above 15 minutes', () => {
assert.throws(() => {
const app = new App();
const stack = new Stack(app);
new ConversationHandlerFunction(stack, 'conversationHandler', {
models: [],
timeoutSeconds: 60 * 15 + 1,
});
}, new Error('timeoutSeconds must be a whole number between 1 and 900 inclusive'));
});

void it('throws on fractional memory', () => {
assert.throws(() => {
const app = new App();
const stack = new Stack(app);
new ConversationHandlerFunction(stack, 'conversationHandler', {
models: [],
memoryMB: 256.2,
});
}, new Error('memoryMB must be a whole number between 128 and 10240 inclusive'));
});
});

void describe('logging options', () => {
void it('sets log level', () => {
const app = new App();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export type ConversationHandlerFunctionProps = {
*/
memoryMB?: number;

/**
* An amount of time in seconds between 1 second and 15 minutes.
* Must be a whole number.
* Default is 60 seconds.
*/
timeoutSeconds?: number;

logging?: {
level?: ApplicationLogLevel;
retention?: RetentionDays;
Expand Down Expand Up @@ -96,7 +103,7 @@ export class ConversationHandlerFunction
`conversationHandlerFunction`,
{
runtime: LambdaRuntime.NODEJS_18_X,
timeout: Duration.seconds(60),
timeout: Duration.seconds(this.resolveTimeout()),
entry: this.props.entry ?? defaultHandlerFilePath,
handler: 'handler',
memorySize: this.resolveMemory(),
Expand Down Expand Up @@ -185,6 +192,28 @@ export class ConversationHandlerFunction
}
return this.props.memoryMB;
};

private resolveTimeout = () => {
const timeoutMin = 1;
const timeoutMax = 60 * 15; // 15 minutes in seconds
const timeoutDefault = 60;
if (this.props.timeoutSeconds === undefined) {
return timeoutDefault;
}

if (
!isWholeNumberBetweenInclusive(
this.props.timeoutSeconds,
timeoutMin,
timeoutMax
)
) {
throw new Error(
`timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive`
);
}
return this.props.timeoutSeconds;
};
}

const isWholeNumberBetweenInclusive = (
Expand Down
1 change: 1 addition & 0 deletions packages/backend-ai/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type DefineConversationHandlerFunctionProps = {
region?: string;
}>;
memoryMB?: number;
timeoutSeconds?: number;
logging?: ConversationHandlerFunctionLoggingOptions;
};

Expand Down
50 changes: 50 additions & 0 deletions packages/backend-ai/src/conversation/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { customEntryHandler } from './test-assets/with-custom-entry/resource.js'
import { Template } from 'aws-cdk-lib/assertions';
import { defineConversationHandlerFunction } from './factory.js';
import { ConversationHandlerFunction } from '@aws-amplify/ai-constructs/conversation';
import { AmplifyError } from '@aws-amplify/platform-core';

const createStackAndSetContext = (): Stack => {
const app = new App();
Expand Down Expand Up @@ -204,6 +205,55 @@ void describe('ConversationHandlerFactory', () => {
});
});

void it('maps invalid memory error', () => {
const factory = defineConversationHandlerFunction({
entry: './test-assets/with-default-entry/handler.ts',
name: 'testHandlerName',
models: [],
memoryMB: -1,
});
assert.throws(
() => factory.getInstance(getInstanceProps),
(error: Error) => {
assert.ok(AmplifyError.isAmplifyError(error));
assert.strictEqual(error.name, 'InvalidMemoryMBError');
return true;
}
);
});

void it('passes timeout setting to construct', () => {
const factory = defineConversationHandlerFunction({
entry: './test-assets/with-default-entry/handler.ts',
name: 'testHandlerName',
models: [],
timeoutSeconds: 124,
});
const lambda = factory.getInstance(getInstanceProps);
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
template.resourceCountIs('AWS::Lambda::Function', 1);
template.hasResourceProperties('AWS::Lambda::Function', {
Timeout: 124,
});
});

void it('maps invalid timeout error', () => {
const factory = defineConversationHandlerFunction({
entry: './test-assets/with-default-entry/handler.ts',
name: 'testHandlerName',
models: [],
timeoutSeconds: -1,
});
assert.throws(
() => factory.getInstance(getInstanceProps),
(error: Error) => {
assert.ok(AmplifyError.isAmplifyError(error));
assert.strictEqual(error.name, 'InvalidTimeoutError');
return true;
}
);
});

void it('passes log level to construct', () => {
const factory = defineConversationHandlerFunction({
entry: './test-assets/with-default-entry/handler.ts',
Expand Down
46 changes: 39 additions & 7 deletions packages/backend-ai/src/conversation/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import {
ConversationTurnEventVersion,
} from '@aws-amplify/ai-constructs/conversation';
import path from 'path';
import { CallerDirectoryExtractor } from '@aws-amplify/platform-core';
import {
AmplifyUserError,
CallerDirectoryExtractor,
} from '@aws-amplify/platform-core';
import { AiModel } from '@aws-amplify/data-schema-types';
import {
LogLevelConverter,
Expand Down Expand Up @@ -52,6 +55,7 @@ class ConversationHandlerFunctionGenerator
}),
outputStorageStrategy: this.outputStorageStrategy,
memoryMB: this.props.memoryMB,
timeoutSeconds: this.props.timeoutSeconds,
};
const logging: typeof constructProps.logging = {};
if (this.props.logging?.level) {
Expand All @@ -65,12 +69,34 @@ class ConversationHandlerFunctionGenerator
);
}
constructProps.logging = logging;
const conversationHandlerFunction = new ConversationHandlerFunction(
scope,
this.props.name,
constructProps
);
return conversationHandlerFunction;
try {
return new ConversationHandlerFunction(
scope,
this.props.name,
constructProps
);
} catch (e) {
throw this.mapConstructErrors(e);
}
};

private mapConstructErrors = (e: unknown) => {
if (!(e instanceof Error)) {
return e;
}
if (e.message.startsWith('memoryMB must be')) {
return new AmplifyUserError('InvalidMemoryMBError', {
message: `Invalid memoryMB of ${this.props.memoryMB}`,
resolution: e.message,
});
}
if (e.message.startsWith('timeoutSeconds must be')) {
return new AmplifyUserError('InvalidTimeoutError', {
message: `Invalid timeout of ${this.props.timeoutSeconds} seconds`,
resolution: e.message,
});
}
return e;
};
}

Expand Down Expand Up @@ -155,6 +181,12 @@ export type DefineConversationHandlerFunctionProps = {
* Default is 512MB.
*/
memoryMB?: number;
/**
* An amount of time in seconds between 1 second and 15 minutes.
* Must be a whole number.
* Default is 60 seconds.
*/
timeoutSeconds?: number;
logging?: ConversationHandlerFunctionLoggingOptions;
};

Expand Down

0 comments on commit a66f5f2

Please sign in to comment.