Skip to content

Commit a79f962

Browse files
authored
feat(property-provider): memoize() supports force refresh (#3413)
* feat(property-provider): memoize() supports force refresh * chore(property-provider): update unit test
1 parent 4fd26e4 commit a79f962

File tree

8 files changed

+91
-24
lines changed

8 files changed

+91
-24
lines changed

packages/credential-provider-node/src/defaultProvider.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { fromSSO, FromSSOInit } from "@aws-sdk/credential-provider-sso";
66
import { fromTokenFile, FromTokenFileInit } from "@aws-sdk/credential-provider-web-identity";
77
import { chain, CredentialsProviderError, memoize } from "@aws-sdk/property-provider";
88
import { ENV_PROFILE, loadSharedConfigFiles } from "@aws-sdk/shared-ini-file-loader";
9-
import { CredentialProvider } from "@aws-sdk/types";
9+
import { Credentials, MemoizedProvider } from "@aws-sdk/types";
1010

1111
import { remoteProvider } from "./remoteProvider";
1212

@@ -46,7 +46,7 @@ import { remoteProvider } from "./remoteProvider";
4646
*/
4747
export const defaultProvider = (
4848
init: FromIniInit & RemoteProviderInit & FromProcessInit & FromSSOInit & FromTokenFileInit = {}
49-
): CredentialProvider => {
49+
): MemoizedProvider<Credentials> => {
5050
const options = {
5151
profile: process.env[ENV_PROFILE],
5252
...init,

packages/middleware-endpoint-discovery/src/resolveEndpointDiscoveryConfig.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { EndpointCache } from "@aws-sdk/endpoint-cache";
2-
import { Credentials, Provider } from "@aws-sdk/types";
2+
import { Credentials, MemoizedProvider, Provider } from "@aws-sdk/types";
33

44
export interface EndpointDiscoveryInputConfig {}
55

66
export interface PreviouslyResolved {
77
isCustomEndpoint: boolean;
8-
credentials: Provider<Credentials>;
8+
credentials: MemoizedProvider<Credentials>;
99
endpointDiscoveryEnabledProvider: Provider<boolean | undefined>;
1010
}
1111

packages/middleware-sdk-ec2/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import {
99
InitializeHandlerOptions,
1010
InitializeHandlerOutput,
1111
InitializeMiddleware,
12+
MemoizedProvider,
1213
MetadataBearer,
1314
Pluggable,
1415
Provider,
1516
} from "@aws-sdk/types";
1617
import { formatUrl } from "@aws-sdk/util-format-url";
1718

1819
interface PreviouslyResolved {
19-
credentials: Provider<Credentials>;
20+
credentials: MemoizedProvider<Credentials>;
2021
endpoint: Provider<Endpoint>;
2122
region: Provider<string>;
2223
sha256: HashConstructor;

packages/middleware-sdk-rds/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
InitializeHandlerOptions,
1010
InitializeHandlerOutput,
1111
InitializeMiddleware,
12+
MemoizedProvider,
1213
MetadataBearer,
1314
Pluggable,
1415
Provider,
@@ -28,7 +29,7 @@ const sourceIdToCommandKeyMap: { [key: string]: string } = {
2829
const version = "2014-10-31";
2930

3031
interface PreviouslyResolved {
31-
credentials: Provider<Credentials>;
32+
credentials: MemoizedProvider<Credentials>;
3233
endpoint: Provider<Endpoint>;
3334
region: Provider<string>;
3435
sha256: HashConstructor;

packages/middleware-signing/src/configurations.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Credentials,
55
HashConstructor,
66
Logger,
7+
MemoizedProvider,
78
Provider,
89
RegionInfo,
910
RegionInfoProvider,
@@ -75,7 +76,7 @@ export interface SigV4AuthInputConfig {
7576
}
7677

7778
interface PreviouslyResolved {
78-
credentialDefaultProvider: (input: any) => Provider<Credentials>;
79+
credentialDefaultProvider: (input: any) => MemoizedProvider<Credentials>;
7980
region: string | Provider<string>;
8081
regionInfoProvider: RegionInfoProvider;
8182
signingName?: string;
@@ -86,7 +87,7 @@ interface PreviouslyResolved {
8687
}
8788

8889
interface SigV4PreviouslyResolved {
89-
credentialDefaultProvider: (input: any) => Provider<Credentials>;
90+
credentialDefaultProvider: (input: any) => MemoizedProvider<Credentials>;
9091
region: string | Provider<string>;
9192
signingName: string;
9293
sha256: HashConstructor;
@@ -96,8 +97,10 @@ interface SigV4PreviouslyResolved {
9697
export interface AwsAuthResolvedConfig {
9798
/**
9899
* Resolved value for input config {@link AwsAuthInputConfig.credentials}
100+
* This provider MAY memoize the loaded credentials for certain period.
101+
* See {@link MemoizedProvider} for more information.
99102
*/
100-
credentials: Provider<Credentials>;
103+
credentials: MemoizedProvider<Credentials>;
101104
/**
102105
* Resolved value for input config {@link AwsAuthInputConfig.signer}
103106
*/
@@ -211,7 +214,9 @@ const normalizeProvider = <T>(input: T | Provider<T>): Provider<T> => {
211214
return input as Provider<T>;
212215
};
213216

214-
const normalizeCredentialProvider = (credentials: Credentials | Provider<Credentials>): Provider<Credentials> => {
217+
const normalizeCredentialProvider = (
218+
credentials: Credentials | Provider<Credentials>
219+
): MemoizedProvider<Credentials> => {
215220
if (typeof credentials === "function") {
216221
return memoize(
217222
credentials,

packages/property-provider/src/memoize.spec.ts

+45-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ describe("memoize", () => {
5252
expect(await memoized()).toBe("Retry");
5353
expect(provider).toBeCalledTimes(2);
5454
});
55+
56+
it("should retry provider if forceRefresh parameter is used", async () => {
57+
provider
58+
.mockReset()
59+
.mockResolvedValueOnce("1st")
60+
.mockResolvedValueOnce("2nd")
61+
.mockRejectedValueOnce("Should not call 3rd time");
62+
const memoized = memoize(provider);
63+
expect(await memoized()).toBe("1st");
64+
expect(await memoized()).toBe("1st");
65+
expect(await memoized({ forceRefresh: true })).toBe("2nd");
66+
expect(await memoized()).toBe("2nd");
67+
expect(provider).toBeCalledTimes(2);
68+
});
5569
});
5670

5771
describe("refreshing memoization", () => {
@@ -115,7 +129,27 @@ describe("memoize", () => {
115129
});
116130
});
117131

118-
describe("should return the same promise for invocations 2-infinity if `requiresRefresh` returns `false`", () => {
132+
describe("when called with forceRefresh set to `true`", () => {
133+
it("should reinvoke the underlying provider even if isExpired returns false", async () => {
134+
const memoized = memoize(provider, isExpired, requiresRefresh);
135+
isExpired.mockReturnValue(false);
136+
for (const _ in [...Array(repeatTimes).keys()]) {
137+
expect(await memoized({ forceRefresh: true })).toEqual(mockReturn);
138+
}
139+
expect(provider).toHaveBeenCalledTimes(repeatTimes);
140+
});
141+
142+
it("should reinvoke the underlying provider even if requiresRefresh returns false", async () => {
143+
const memoized = memoize(provider, isExpired, requiresRefresh);
144+
requiresRefresh.mockReturnValue(false);
145+
for (const _ in [...Array(repeatTimes).keys()]) {
146+
expect(await memoized({ forceRefresh: true })).toEqual(mockReturn);
147+
}
148+
expect(provider).toHaveBeenCalledTimes(repeatTimes);
149+
});
150+
});
151+
152+
describe("when `requiresRefresh` returns `false`", () => {
119153
const requiresRefreshFalseTest = async () => {
120154
const memoized = memoize(provider, isExpired, requiresRefresh);
121155
const result = memoized();
@@ -130,14 +164,22 @@ describe("memoize", () => {
130164
expect(isExpired).not.toHaveBeenCalled();
131165
};
132166

133-
it("when isExpired returns true", () => {
167+
it("should return the same promise for invocations 2-infinity if isExpired returns true", () => {
134168
return requiresRefreshFalseTest();
135169
});
136170

137-
it("when isExpired returns false", () => {
171+
it("should return the same promise for invocations 2-infinity if isExpired returns false", () => {
138172
isExpired.mockReturnValue(false);
139173
return requiresRefreshFalseTest();
140174
});
175+
176+
it("should re-evaluate `requiresRefresh` after force refresh", async () => {
177+
const memoized = memoize(provider, isExpired, requiresRefresh);
178+
for (const _ in [...Array(repeatTimes).keys()]) {
179+
expect(await memoized({ forceRefresh: true })).toStrictEqual(mockReturn);
180+
}
181+
expect(requiresRefresh).toBeCalledTimes(repeatTimes);
182+
});
141183
});
142184

143185
describe("should not make extra request for concurrent calls", () => {

packages/property-provider/src/memoize.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Provider } from "@aws-sdk/types";
1+
import { MemoizedProvider, Provider } from "@aws-sdk/types";
22

33
interface MemoizeOverload {
44
/**
@@ -12,7 +12,7 @@ interface MemoizeOverload {
1212
*
1313
* @param provider The provider whose result should be cached indefinitely.
1414
*/
15-
<T>(provider: Provider<T>): Provider<T>;
15+
<T>(provider: Provider<T>): MemoizedProvider<T>;
1616

1717
/**
1818
* Decorates a provider function with refreshing memoization.
@@ -37,17 +37,18 @@ interface MemoizeOverload {
3737
provider: Provider<T>,
3838
isExpired: (resolved: T) => boolean,
3939
requiresRefresh?: (resolved: T) => boolean
40-
): Provider<T>;
40+
): MemoizedProvider<T>;
4141
}
4242

4343
export const memoize: MemoizeOverload = <T>(
4444
provider: Provider<T>,
4545
isExpired?: (resolved: T) => boolean,
4646
requiresRefresh?: (resolved: T) => boolean
47-
): Provider<T> => {
47+
): MemoizedProvider<T> => {
4848
let resolved: T;
4949
let pending: Promise<T> | undefined;
5050
let hasResult: boolean;
51+
let isConstant = false;
5152
// Wrapper over supplied provider with side effect to handle concurrent invocation.
5253
const coalesceProvider: Provider<T> = async () => {
5354
if (!pending) {
@@ -56,26 +57,25 @@ export const memoize: MemoizeOverload = <T>(
5657
try {
5758
resolved = await pending;
5859
hasResult = true;
60+
isConstant = false;
5961
} finally {
6062
pending = undefined;
6163
}
6264
return resolved;
6365
};
6466

6567
if (isExpired === undefined) {
66-
// This is a static memoization; no need to incorporate refreshing
67-
return async () => {
68-
if (!hasResult) {
68+
// This is a static memoization; no need to incorporate refreshing unless using forceRefresh;
69+
return async (options) => {
70+
if (!hasResult || options?.forceRefresh) {
6971
resolved = await coalesceProvider();
7072
}
7173
return resolved;
7274
};
7375
}
7476

75-
let isConstant = false;
76-
77-
return async () => {
78-
if (!hasResult) {
77+
return async (options) => {
78+
if (!hasResult || options?.forceRefresh) {
7979
resolved = await coalesceProvider();
8080
}
8181
if (isConstant) {

packages/types/src/util.ts

+18
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ export interface Provider<T> {
4242
(): Promise<T>;
4343
}
4444

45+
/**
46+
* A function that, when invoked, returns a promise that will be fulfilled with
47+
* a value of type T. It memoizes the result from the previous invocation
48+
* instead of calling the underlying resources every time.
49+
*
50+
* You can force the provider to refresh the memoized value by invoke the
51+
* function with optional parameter hash with `forceRefresh` boolean key and
52+
* value `true`.
53+
*
54+
* @example A function that reads credentials from IMDS service that could
55+
* return expired credentials. The SDK will keep using the expired credentials
56+
* until an unretryable service error requiring a force refresh of the
57+
* credentials.
58+
*/
59+
export interface MemoizedProvider<T> {
60+
(options?: { forceRefresh?: boolean }): Promise<T>;
61+
}
62+
4563
/**
4664
* A function that, given a request body, determines the
4765
* length of the body. This is used to determine the Content-Length

0 commit comments

Comments
 (0)