Skip to content

Commit 252ea80

Browse files
Fix v2 client reuse regression, and make it even better than before
1 parent 132431e commit 252ea80

File tree

3 files changed

+236
-22
lines changed

3 files changed

+236
-22
lines changed

lib/aws/v2-client-factory.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use strict';
2+
3+
const memoize = require('memoizee');
4+
const sdk = require('./sdk-v2');
5+
const deepSortObjectByKey = require('../utils/deep-sort-object-by-key');
6+
7+
// Map service names to their SDK v2 classes
8+
const SERVICE_MAP = {
9+
APIGateway: sdk.APIGateway,
10+
ApiGatewayV2: sdk.ApiGatewayV2,
11+
CloudFormation: sdk.CloudFormation,
12+
CloudWatch: sdk.CloudWatch,
13+
CloudWatchLogs: sdk.CloudWatchLogs,
14+
CognitoIdentityServiceProvider: sdk.CognitoIdentityServiceProvider,
15+
DynamoDB: sdk.DynamoDB,
16+
ECR: sdk.ECR,
17+
EventBridge: sdk.EventBridge,
18+
IAM: sdk.IAM,
19+
Iot: sdk.Iot,
20+
IotData: sdk.IotData,
21+
Kafka: sdk.Kafka,
22+
Kinesis: sdk.Kinesis,
23+
Lambda: sdk.Lambda,
24+
MQ: sdk.MQ,
25+
S3: sdk.S3,
26+
SecretsManager: sdk.SecretsManager,
27+
SNS: sdk.SNS,
28+
SQS: sdk.SQS,
29+
STS: sdk.STS,
30+
};
31+
32+
class AWSV2ClientFactory {
33+
constructor(baseConfig = {}) {
34+
this.baseConfig = baseConfig;
35+
this.clients = new Map();
36+
37+
// Create memoized request function for response caching
38+
this.memoizedRequest = memoize(this._executeRequest.bind(this), {
39+
promise: true,
40+
normalizer: ([serviceName, method, params, clientConfig]) => {
41+
return [
42+
serviceName,
43+
method,
44+
JSON.stringify(deepSortObjectByKey(params)),
45+
JSON.stringify(deepSortObjectByKey(clientConfig)),
46+
].join('|');
47+
},
48+
});
49+
}
50+
51+
/**
52+
* Get a configured AWS service client
53+
* @param {string} serviceName - Name of the AWS service (e.g., 'S3', 'CloudFormation')
54+
* @param {Object} overrideConfig - Configuration to override base config
55+
* @returns {Object} AWS SDK v2 service instance
56+
*/
57+
getClient(serviceName, overrideConfig = {}) {
58+
const ServiceClass = SERVICE_MAP[serviceName];
59+
if (!ServiceClass) {
60+
throw new Error(`Unknown AWS service: ${serviceName}`);
61+
}
62+
63+
// Create cache key - include service + sorted config for consistent keys
64+
const configKey = JSON.stringify({
65+
serviceName,
66+
...deepSortObjectByKey({ ...this.baseConfig, ...overrideConfig }),
67+
});
68+
69+
if (!this.clients.has(configKey)) {
70+
const clientConfig = { ...this.baseConfig, ...overrideConfig };
71+
this.clients.set(configKey, new ServiceClass(clientConfig));
72+
}
73+
74+
return this.clients.get(configKey);
75+
}
76+
77+
/**
78+
* Execute an AWS request with optional response caching
79+
* @param {string} serviceName - Name of the AWS service
80+
* @param {string} method - Method to call on the service
81+
* @param {Object} params - Parameters for the AWS API call
82+
* @param {Object} clientConfig - Client configuration
83+
* @param {Object} options - Request options
84+
* @param {boolean} options.useCache - Whether to cache the response
85+
* @returns {Promise} Result of the AWS API call
86+
*/
87+
async request(serviceName, method, params, clientConfig = {}, options = {}) {
88+
const { useCache = false } = options;
89+
90+
if (useCache) {
91+
return this.memoizedRequest(serviceName, method, params, clientConfig);
92+
}
93+
94+
return this._executeRequest(serviceName, method, params, clientConfig);
95+
}
96+
97+
/**
98+
* Execute the actual AWS request
99+
* @private
100+
*/
101+
async _executeRequest(serviceName, method, params, clientConfig) {
102+
const client = this.getClient(serviceName, clientConfig);
103+
return client[method](params).promise();
104+
}
105+
106+
/**
107+
* Clear the client cache
108+
*/
109+
clearCache() {
110+
this.clients.clear();
111+
// Clear response cache if it exists
112+
if (this.memoizedRequest.clear) {
113+
this.memoizedRequest.clear();
114+
}
115+
}
116+
117+
/**
118+
* Update the base configuration for all future clients
119+
* @param {Object} newConfig - New base configuration
120+
*/
121+
updateBaseConfig(newConfig) {
122+
this.baseConfig = { ...this.baseConfig, ...newConfig };
123+
// Clear cache to force recreation with new config
124+
this.clearCache();
125+
}
126+
127+
/**
128+
* Get list of supported AWS services
129+
* @returns {Array} Array of supported service names
130+
*/
131+
getSupportedServices() {
132+
return Object.keys(SERVICE_MAP);
133+
}
134+
}
135+
136+
module.exports = AWSV2ClientFactory;

lib/aws/client-factory.js renamed to lib/aws/v3-client-factory.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const CLIENT_MAP = {
3131
STS: STSClient,
3232
};
3333

34-
class AWSClientFactory {
34+
class AWSV3ClientFactory {
3535
constructor(baseConfig = {}) {
3636
this.baseConfig = baseConfig;
3737
this.clients = new Map();
@@ -98,4 +98,4 @@ class AWSClientFactory {
9898
}
9999
}
100100

101-
module.exports = AWSClientFactory;
101+
module.exports = AWSV3ClientFactory;

lib/plugins/aws/provider.js

Lines changed: 98 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,13 @@ const d = require('d');
1313
const path = require('path');
1414
const spawnExt = require('child-process-ext/spawn');
1515
const ServerlessError = require('../../serverless-error');
16-
const awsRequest = require('../../aws/request');
1716
const { cfValue } = require('../../utils/aws-schema-get-cf-value');
1817
const reportDeprecatedProperties = require('../../utils/report-deprecated-properties');
1918
const deepSortObjectByKey = require('../../utils/deep-sort-object-by-key');
2019
const { progress, log } = require('@serverless/utils/log');
2120

22-
// AWS SDK v3 infrastructure
23-
const AWSClientFactory = require('../../aws/client-factory');
21+
const AWSV2ClientFactory = require('../../aws/v2-client-factory');
22+
const AWSV3ClientFactory = require('../../aws/v3-client-factory');
2423
const { createCommand } = require('../../aws/commands');
2524
const { buildClientConfig, shouldUseS3Acceleration } = require('../../aws/config');
2625
const { transformV3Error } = require('../../aws/error-utils');
@@ -1736,20 +1735,33 @@ class AwsProvider {
17361735
* @private
17371736
*/
17381737
async _requestV2(service, method, params, options) {
1739-
// TODO: Determine calling module and log that
17401738
const requestOptions = _.isObject(options) ? options : {};
17411739
const shouldCache = _.get(requestOptions, 'useCache', false);
1742-
// Copy is required as the credentials may be modified during the request
1743-
const credentials = Object.assign({}, this.getCredentials());
1744-
const serviceOptions = {
1745-
name: service,
1746-
params: {
1747-
...credentials,
1748-
region: _.get(requestOptions, 'region', this.getRegion()),
1749-
isS3TransferAccelerationEnabled: this.isS3TransferAccelerationEnabled(),
1750-
},
1740+
const region = _.get(requestOptions, 'region', this.getRegion());
1741+
1742+
// Initialize factory if needed
1743+
if (!this.v2ClientFactory) {
1744+
this.v2ClientFactory = new AWSV2ClientFactory(this._getV2BaseConfig());
1745+
}
1746+
1747+
// Build client config (only configuration that affects client creation)
1748+
const clientConfig = {
1749+
...this.getCredentials(),
1750+
region,
17511751
};
1752-
return (shouldCache ? awsRequest.memoized : awsRequest)(serviceOptions, method, params);
1752+
1753+
// Handle S3 acceleration
1754+
if (service === 'S3') {
1755+
const accelerationCompatibleMethods = new Set(['upload', 'putObject']);
1756+
if (accelerationCompatibleMethods.has(method) && this.isS3TransferAccelerationEnabled()) {
1757+
clientConfig.useAccelerateEndpoint = true;
1758+
}
1759+
}
1760+
1761+
// Use factory's request method which handles both client caching and response caching
1762+
return this.v2ClientFactory.request(service, method, params, clientConfig, {
1763+
useCache: shouldCache,
1764+
});
17531765
}
17541766

17551767
/**
@@ -1764,8 +1776,8 @@ class AwsProvider {
17641776
async _requestV3(service, method, params = {}, options = {}) {
17651777
try {
17661778
// Initialize client factory if not already done
1767-
if (!this.clientFactory) {
1768-
this.clientFactory = new AWSClientFactory(this._getV3BaseConfig());
1779+
if (!this.v3ClientFactory) {
1780+
this.v3ClientFactory = new AWSV3ClientFactory(this._getV3BaseConfig());
17691781
}
17701782

17711783
// Build client configuration
@@ -1775,7 +1787,7 @@ class AwsProvider {
17751787
const command = createCommand(service, method, params);
17761788

17771789
// Send request
1778-
const result = await this.clientFactory.send(service, command, clientConfig);
1790+
const result = await this.v3ClientFactory.send(service, command, clientConfig);
17791791

17801792
return result;
17811793
} catch (error) {
@@ -1791,12 +1803,12 @@ class AwsProvider {
17911803
* @returns {Object} AWS SDK v3 client instance
17921804
*/
17931805
getV3Client(service, options = {}) {
1794-
if (!this.clientFactory) {
1795-
this.clientFactory = new AWSClientFactory(this._getV3BaseConfig());
1806+
if (!this.v3ClientFactory) {
1807+
this.v3ClientFactory = new AWSV3ClientFactory(this._getV3BaseConfig());
17961808
}
17971809

17981810
const clientConfig = this._buildV3ClientConfig(service, null, options);
1799-
return this.clientFactory.getClient(service, clientConfig);
1811+
return this.v3ClientFactory.getClient(service, clientConfig);
18001812
}
18011813

18021814
/**
@@ -1837,6 +1849,72 @@ class AwsProvider {
18371849
return baseConfig;
18381850
}
18391851

1852+
/**
1853+
* Build base configuration for AWS SDK v2 clients
1854+
* @private
1855+
*/
1856+
_getV2BaseConfig() {
1857+
const config = {};
1858+
1859+
// Handle proxy configuration
1860+
const proxy =
1861+
process.env.proxy ||
1862+
process.env.HTTP_PROXY ||
1863+
process.env.http_proxy ||
1864+
process.env.HTTPS_PROXY ||
1865+
process.env.https_proxy;
1866+
1867+
const proxyOptions = {};
1868+
if (proxy) {
1869+
Object.assign(proxyOptions, require('url').parse(proxy));
1870+
}
1871+
1872+
// Handle CA certificates
1873+
const ca = process.env.ca || process.env.HTTPS_CA || process.env.https_ca;
1874+
let caCerts = [];
1875+
1876+
if (ca) {
1877+
const caArr = ca.split(',');
1878+
caCerts = caCerts.concat(caArr.map((cert) => cert.replace(/\\n/g, '\n')));
1879+
}
1880+
1881+
const cafile = process.env.cafile || process.env.HTTPS_CAFILE || process.env.https_cafile;
1882+
if (cafile) {
1883+
const caPathArr = cafile.split(',');
1884+
caCerts = caCerts.concat(
1885+
caPathArr.map((cafilePath) => require('fs').readFileSync(cafilePath.trim()))
1886+
);
1887+
}
1888+
1889+
if (caCerts.length > 0) {
1890+
Object.assign(proxyOptions, {
1891+
rejectUnauthorized: true,
1892+
ca: caCerts,
1893+
});
1894+
}
1895+
1896+
// Set up HTTP options
1897+
const httpOptions = {};
1898+
1899+
if (proxy) {
1900+
httpOptions.agent = new (require('https-proxy-agent'))(proxyOptions);
1901+
} else if (proxyOptions.ca) {
1902+
httpOptions.agent = new (require('https').Agent)(proxyOptions);
1903+
}
1904+
1905+
// Handle timeout
1906+
const timeout = process.env.AWS_CLIENT_TIMEOUT || process.env.aws_client_timeout;
1907+
if (timeout) {
1908+
httpOptions.timeout = parseInt(timeout, 10);
1909+
}
1910+
1911+
if (Object.keys(httpOptions).length > 0) {
1912+
config.httpOptions = httpOptions;
1913+
}
1914+
1915+
return config;
1916+
}
1917+
18401918
/**
18411919
* Fetch credentials directly or using a profile from serverless yml configuration or from the
18421920
* well known environment variables

0 commit comments

Comments
 (0)