Skip to content

Commit

Permalink
feat(middleware-user-agent): cache user agent string
Browse files Browse the repository at this point in the history
Cache the user agent string after the first successful computation.
Do this in a way that ensures `await` isn't called in the middleware if
the user agent is already computed (run middleware in single tick).

Middleware such as the user agent is called on every single call to aws,
so for dbs (e.g. fetching single keys), the middleware should in
principle be memory/cpu efficient.

Context: I saw that client-dynamodb calls took twice as long to resolve,
and cpu usage was slightly higher after switching from aws-sdk,
even after the fix for 2223. The impact on dynamodb promise resolution
times was higher at peak load when Node.js was using more cpu per process.
(The Node.js process had around 40% cpu usage)
- I'm not completely certain this is the cause, but it seems related.
  There's extra function calls, map, array creation/join, and
  string escaping.
  Additionally, the call to await() would spread out the work of
  creating the user agent across multiple CPU ticks.
- (Related to 2027 which was fixed by memoizing in the implementation of
  defaultUserAgentProvider)

Avoid extra cpu from calling map, defaultUserAgentProvider+await, etc.

As a result of 388b180 switching from
defaultUserAgent to defaultUserAgentProvider, the middleware would have
to recompute the user agent on every single api call. This fixes that by
computing the user agent only once, when the middleware is instantiated.
(So creating a client but not using it will now call this, but creating
a client has overhead anyway)
  • Loading branch information
TysonAndre committed May 15, 2024
1 parent 9466c82 commit 84e13eb
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,22 @@ describe("userAgentMiddleware", () => {
describe(runtime, () => {
for (const { ua, expected } of cases) {
it(`should sanitize user agent ${ua} to ${expected}`, async () => {
const defaultUserAgentProvider = jest.fn().mockReturnValue([ua]);
const middleware = userAgentMiddleware({
defaultUserAgentProvider: async () => [ua],
defaultUserAgentProvider,
runtime,
});
const handler = middleware(mockNextHandler, {});
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
expect(mockNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]).toEqual(
expect.stringContaining(expected)
);
// should work when middleware is reused
await handler({ input: {}, request: new HttpRequest({ headers: {} }) });
expect(mockNextHandler.mock.calls[1][0].request.headers[sdkUserAgentKey]).toEqual(
mockNextHandler.mock.calls[0][0].request.headers[sdkUserAgentKey]
);
expect(defaultUserAgentProvider.mock.calls.length).toEqual(1);
});

it(`should include internal metadata, user agent ${ua} customization: ${expected}`, async () => {
Expand Down
46 changes: 26 additions & 20 deletions packages/middleware-user-agent/src/user-agent-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,36 @@ import {
* config or middleware setting the `userAgent` context to generate desired user
* agent.
*/
export const userAgentMiddleware =
(options: UserAgentResolvedConfig) =>
<Output extends MetadataBearer>(
next: BuildHandler<any, any>,
context: HandlerExecutionContext
): BuildHandler<any, any> =>
async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
export const userAgentMiddleware = (options: UserAgentResolvedConfig) => <Output extends MetadataBearer>(
next: BuildHandler<any, any>,
context: HandlerExecutionContext
): BuildHandler<any, any> => {
let sdkUserAgentValue = "";
let normalUAValue = "";
let initialized = false;
return async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
const { request } = args;
if (!HttpRequest.isInstance(request)) return next(args);
const { headers } = request;
const userAgent = context?.userAgent?.map(escapeUserAgent) || [];
const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
const prefix = getUserAgentPrefix();

// Set value to AWS-specific user agent header
const sdkUserAgentValue = (prefix ? [prefix] : [])
.concat([...defaultUserAgent, ...userAgent, ...customUserAgent])
.join(SPACE);
if (!initialized) {
const userAgent = context?.userAgent?.map(escapeUserAgent) || [];
const defaultUserAgent = (await options.defaultUserAgentProvider()).map(escapeUserAgent);
const customUserAgent = options?.customUserAgent?.map(escapeUserAgent) || [];
const prefix = getUserAgentPrefix();

// Get value to be sent with non-AWS-specific user agent header.
const normalUAValue = [
...defaultUserAgent.filter((section) => section.startsWith("aws-sdk-")),
...customUserAgent,
].join(SPACE);
// Set value to AWS-specific user agent header
sdkUserAgentValue = (prefix ? [prefix] : [])
.concat([...defaultUserAgent, ...userAgent, ...customUserAgent])
.join(SPACE);

// Get value to be sent with non-AWS-specific user agent header.
normalUAValue = [
...defaultUserAgent.filter((section) => section.startsWith("aws-sdk-")),
...customUserAgent,
].join(SPACE);
initialized = true;
}

if (options.runtime !== "browser") {
if (normalUAValue) {
Expand All @@ -77,6 +82,7 @@ export const userAgentMiddleware =
request,
});
};
};

/**
* Escape the each pair according to https://tools.ietf.org/html/rfc5234 and join the pair with pattern `name/version`.
Expand Down

0 comments on commit 84e13eb

Please sign in to comment.