Skip to content

Commit a66f5f2

Browse files
authored
Expose timeout property. (#2358)
1 parent fbf209e commit a66f5f2

File tree

7 files changed

+189
-8
lines changed

7 files changed

+189
-8
lines changed

.changeset/few-oranges-tell.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@aws-amplify/ai-constructs': minor
3+
'@aws-amplify/backend-ai': minor
4+
---
5+
6+
Expose timeout property

packages/ai-constructs/API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type ConversationHandlerFunctionProps = {
5757
region?: string;
5858
}>;
5959
memoryMB?: number;
60+
timeoutSeconds?: number;
6061
logging?: {
6162
level?: ApplicationLogLevel;
6263
retention?: RetentionDays;

packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,68 @@ void describe('Conversation Handler Function construct', () => {
287287
});
288288
});
289289

290+
void describe('timeout property', () => {
291+
void it('sets valid timeout', () => {
292+
const app = new App();
293+
const stack = new Stack(app);
294+
new ConversationHandlerFunction(stack, 'conversationHandler', {
295+
models: [],
296+
timeoutSeconds: 124,
297+
});
298+
const template = Template.fromStack(stack);
299+
300+
template.hasResourceProperties('AWS::Lambda::Function', {
301+
Timeout: 124,
302+
});
303+
});
304+
305+
void it('sets default timeout', () => {
306+
const app = new App();
307+
const stack = new Stack(app);
308+
new ConversationHandlerFunction(stack, 'conversationHandler', {
309+
models: [],
310+
});
311+
const template = Template.fromStack(stack);
312+
313+
template.hasResourceProperties('AWS::Lambda::Function', {
314+
Timeout: 60,
315+
});
316+
});
317+
318+
void it('throws on timeout below 1', () => {
319+
assert.throws(() => {
320+
const app = new App();
321+
const stack = new Stack(app);
322+
new ConversationHandlerFunction(stack, 'conversationHandler', {
323+
models: [],
324+
timeoutSeconds: 0,
325+
});
326+
}, new Error('timeoutSeconds must be a whole number between 1 and 900 inclusive'));
327+
});
328+
329+
void it('throws on timeout above 15 minutes', () => {
330+
assert.throws(() => {
331+
const app = new App();
332+
const stack = new Stack(app);
333+
new ConversationHandlerFunction(stack, 'conversationHandler', {
334+
models: [],
335+
timeoutSeconds: 60 * 15 + 1,
336+
});
337+
}, new Error('timeoutSeconds must be a whole number between 1 and 900 inclusive'));
338+
});
339+
340+
void it('throws on fractional memory', () => {
341+
assert.throws(() => {
342+
const app = new App();
343+
const stack = new Stack(app);
344+
new ConversationHandlerFunction(stack, 'conversationHandler', {
345+
models: [],
346+
memoryMB: 256.2,
347+
});
348+
}, new Error('memoryMB must be a whole number between 128 and 10240 inclusive'));
349+
});
350+
});
351+
290352
void describe('logging options', () => {
291353
void it('sets log level', () => {
292354
const app = new App();

packages/ai-constructs/src/conversation/conversation_handler_construct.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ export type ConversationHandlerFunctionProps = {
4242
*/
4343
memoryMB?: number;
4444

45+
/**
46+
* An amount of time in seconds between 1 second and 15 minutes.
47+
* Must be a whole number.
48+
* Default is 60 seconds.
49+
*/
50+
timeoutSeconds?: number;
51+
4552
logging?: {
4653
level?: ApplicationLogLevel;
4754
retention?: RetentionDays;
@@ -96,7 +103,7 @@ export class ConversationHandlerFunction
96103
`conversationHandlerFunction`,
97104
{
98105
runtime: LambdaRuntime.NODEJS_18_X,
99-
timeout: Duration.seconds(60),
106+
timeout: Duration.seconds(this.resolveTimeout()),
100107
entry: this.props.entry ?? defaultHandlerFilePath,
101108
handler: 'handler',
102109
memorySize: this.resolveMemory(),
@@ -185,6 +192,28 @@ export class ConversationHandlerFunction
185192
}
186193
return this.props.memoryMB;
187194
};
195+
196+
private resolveTimeout = () => {
197+
const timeoutMin = 1;
198+
const timeoutMax = 60 * 15; // 15 minutes in seconds
199+
const timeoutDefault = 60;
200+
if (this.props.timeoutSeconds === undefined) {
201+
return timeoutDefault;
202+
}
203+
204+
if (
205+
!isWholeNumberBetweenInclusive(
206+
this.props.timeoutSeconds,
207+
timeoutMin,
208+
timeoutMax
209+
)
210+
) {
211+
throw new Error(
212+
`timeoutSeconds must be a whole number between ${timeoutMin} and ${timeoutMax} inclusive`
213+
);
214+
}
215+
return this.props.timeoutSeconds;
216+
};
188217
}
189218

190219
const isWholeNumberBetweenInclusive = (

packages/backend-ai/API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type DefineConversationHandlerFunctionProps = {
7171
region?: string;
7272
}>;
7373
memoryMB?: number;
74+
timeoutSeconds?: number;
7475
logging?: ConversationHandlerFunctionLoggingOptions;
7576
};
7677

packages/backend-ai/src/conversation/factory.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { customEntryHandler } from './test-assets/with-custom-entry/resource.js'
1616
import { Template } from 'aws-cdk-lib/assertions';
1717
import { defineConversationHandlerFunction } from './factory.js';
1818
import { ConversationHandlerFunction } from '@aws-amplify/ai-constructs/conversation';
19+
import { AmplifyError } from '@aws-amplify/platform-core';
1920

2021
const createStackAndSetContext = (): Stack => {
2122
const app = new App();
@@ -204,6 +205,55 @@ void describe('ConversationHandlerFactory', () => {
204205
});
205206
});
206207

208+
void it('maps invalid memory error', () => {
209+
const factory = defineConversationHandlerFunction({
210+
entry: './test-assets/with-default-entry/handler.ts',
211+
name: 'testHandlerName',
212+
models: [],
213+
memoryMB: -1,
214+
});
215+
assert.throws(
216+
() => factory.getInstance(getInstanceProps),
217+
(error: Error) => {
218+
assert.ok(AmplifyError.isAmplifyError(error));
219+
assert.strictEqual(error.name, 'InvalidMemoryMBError');
220+
return true;
221+
}
222+
);
223+
});
224+
225+
void it('passes timeout setting to construct', () => {
226+
const factory = defineConversationHandlerFunction({
227+
entry: './test-assets/with-default-entry/handler.ts',
228+
name: 'testHandlerName',
229+
models: [],
230+
timeoutSeconds: 124,
231+
});
232+
const lambda = factory.getInstance(getInstanceProps);
233+
const template = Template.fromStack(Stack.of(lambda.resources.lambda));
234+
template.resourceCountIs('AWS::Lambda::Function', 1);
235+
template.hasResourceProperties('AWS::Lambda::Function', {
236+
Timeout: 124,
237+
});
238+
});
239+
240+
void it('maps invalid timeout error', () => {
241+
const factory = defineConversationHandlerFunction({
242+
entry: './test-assets/with-default-entry/handler.ts',
243+
name: 'testHandlerName',
244+
models: [],
245+
timeoutSeconds: -1,
246+
});
247+
assert.throws(
248+
() => factory.getInstance(getInstanceProps),
249+
(error: Error) => {
250+
assert.ok(AmplifyError.isAmplifyError(error));
251+
assert.strictEqual(error.name, 'InvalidTimeoutError');
252+
return true;
253+
}
254+
);
255+
});
256+
207257
void it('passes log level to construct', () => {
208258
const factory = defineConversationHandlerFunction({
209259
entry: './test-assets/with-default-entry/handler.ts',

packages/backend-ai/src/conversation/factory.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
ConversationTurnEventVersion,
1818
} from '@aws-amplify/ai-constructs/conversation';
1919
import path from 'path';
20-
import { CallerDirectoryExtractor } from '@aws-amplify/platform-core';
20+
import {
21+
AmplifyUserError,
22+
CallerDirectoryExtractor,
23+
} from '@aws-amplify/platform-core';
2124
import { AiModel } from '@aws-amplify/data-schema-types';
2225
import {
2326
LogLevelConverter,
@@ -52,6 +55,7 @@ class ConversationHandlerFunctionGenerator
5255
}),
5356
outputStorageStrategy: this.outputStorageStrategy,
5457
memoryMB: this.props.memoryMB,
58+
timeoutSeconds: this.props.timeoutSeconds,
5559
};
5660
const logging: typeof constructProps.logging = {};
5761
if (this.props.logging?.level) {
@@ -65,12 +69,34 @@ class ConversationHandlerFunctionGenerator
6569
);
6670
}
6771
constructProps.logging = logging;
68-
const conversationHandlerFunction = new ConversationHandlerFunction(
69-
scope,
70-
this.props.name,
71-
constructProps
72-
);
73-
return conversationHandlerFunction;
72+
try {
73+
return new ConversationHandlerFunction(
74+
scope,
75+
this.props.name,
76+
constructProps
77+
);
78+
} catch (e) {
79+
throw this.mapConstructErrors(e);
80+
}
81+
};
82+
83+
private mapConstructErrors = (e: unknown) => {
84+
if (!(e instanceof Error)) {
85+
return e;
86+
}
87+
if (e.message.startsWith('memoryMB must be')) {
88+
return new AmplifyUserError('InvalidMemoryMBError', {
89+
message: `Invalid memoryMB of ${this.props.memoryMB}`,
90+
resolution: e.message,
91+
});
92+
}
93+
if (e.message.startsWith('timeoutSeconds must be')) {
94+
return new AmplifyUserError('InvalidTimeoutError', {
95+
message: `Invalid timeout of ${this.props.timeoutSeconds} seconds`,
96+
resolution: e.message,
97+
});
98+
}
99+
return e;
74100
};
75101
}
76102

@@ -155,6 +181,12 @@ export type DefineConversationHandlerFunctionProps = {
155181
* Default is 512MB.
156182
*/
157183
memoryMB?: number;
184+
/**
185+
* An amount of time in seconds between 1 second and 15 minutes.
186+
* Must be a whole number.
187+
* Default is 60 seconds.
188+
*/
189+
timeoutSeconds?: number;
158190
logging?: ConversationHandlerFunctionLoggingOptions;
159191
};
160192

0 commit comments

Comments
 (0)