@adobe/spacecat-shared-data-access 3.75.1 → 3.75.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [@adobe/spacecat-shared-data-access-v3.75.3](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.75.2...@adobe/spacecat-shared-data-access-v3.75.3) (2026-06-16)
2
+
3
+ ### Bug Fixes
4
+
5
+ * add EBUSY retry support to S3Client via custom retry strategy ([#1677](https://github.com/adobe/spacecat-shared/issues/1677)) ([04e5504](https://github.com/adobe/spacecat-shared/commit/04e550481cdff0e7545036320aaa43ed64e99ce3)), closes [#1659](https://github.com/adobe/spacecat-shared/issues/1659) [#1659](https://github.com/adobe/spacecat-shared/issues/1659)
6
+
7
+ ## [@adobe/spacecat-shared-data-access-v3.75.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.75.1...@adobe/spacecat-shared-data-access-v3.75.2) (2026-06-16)
8
+
9
+ ### Bug Fixes
10
+
11
+ * pageCitability url-upsert + metrics-store $metadata 404 detection ([#1683](https://github.com/adobe/spacecat-shared/issues/1683)) ([693a943](https://github.com/adobe/spacecat-shared/commit/693a943396fb78db3caf31fd2cb7f643f66bb0ef))
12
+
1
13
  ## [@adobe/spacecat-shared-data-access-v3.75.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.75.0...@adobe/spacecat-shared-data-access-v3.75.1) (2026-06-15)
2
14
 
3
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-data-access",
3
- "version": "3.75.1",
3
+ "version": "3.75.3",
4
4
  "description": "Shared modules of the Spacecat Services - Data Access",
5
5
  "type": "module",
6
6
  "engines": {
@@ -44,6 +44,7 @@
44
44
  "@adobe/spacecat-shared-utils": "1.105.0",
45
45
  "@supabase/postgrest-js": "2.106.2",
46
46
  "@aws-sdk/client-s3": "^3.940.0",
47
+ "@smithy/util-retry": "^4.0.0",
47
48
  "@types/joi": "17.2.3",
48
49
  "aws-xray-sdk": "3.12.0",
49
50
  "joi": "18.2.1",
@@ -731,7 +731,7 @@ class BaseCollection {
731
731
  return this.#queryByIndexKeys(keys, { ...options, limit: 1 });
732
732
  }
733
733
 
734
- async create(item, { upsert = false } = {}) {
734
+ async create(item, { upsert = false, onConflict } = {}) {
735
735
  if (!isNonEmptyObject(item)) {
736
736
  const message = `Failed to create [${this.entityName}]: data is required`;
737
737
  this.log.error(message);
@@ -751,7 +751,7 @@ class BaseCollection {
751
751
 
752
752
  const prepared = this.#prepareItem(item);
753
753
  const payload = this.#toDbRecord(prepared);
754
- const conflictKey = this.#toDbField(this.idName);
754
+ const conflictKey = this.#toDbField(onConflict ?? this.idName);
755
755
 
756
756
  let query = this.postgrestService.from(this.tableName);
757
757
  query = upsert ? query.upsert(payload, { onConflict: conflictKey }) : query.insert(payload);
@@ -27,6 +27,10 @@ const S3_CONFIG_KEY = 'config/spacecat/global-config.json';
27
27
  * Unlike other collections, this uses S3 instead of PostgREST.
28
28
  * Configuration is stored as a versioned JSON object in S3.
29
29
  *
30
+ * The S3Client is configured with a custom retry strategy (EbusyRetryStrategy) that extends
31
+ * the AWS SDK's StandardRetryStrategy to also retry EBUSY DNS errors. This handles the chronic
32
+ * DNS resolver exhaustion issue in Lambda without adding application-level retry logic.
33
+ *
30
34
  * @class ConfigurationCollection
31
35
  */
32
36
  class ConfigurationCollection {
@@ -21,7 +21,19 @@ import BaseCollection from '../base/base.collection.js';
21
21
  class PageCitabilityCollection extends BaseCollection {
22
22
  static COLLECTION_NAME = 'PageCitabilityCollection';
23
23
 
24
- // add custom collection-level methods here, if needed
24
+ /**
25
+ * Creates a PageCitability record, upserting on the globally-unique `url` column.
26
+ *
27
+ * The `url` column carries a global unique index (idx_page_citabilities_url_unique),
28
+ * so upserting on `url` makes create idempotent and eliminates the concurrent
29
+ * read-then-insert duplicate-key (Postgres 23505) race observed in production.
30
+ *
31
+ * @param {object} item - the PageCitability data to persist.
32
+ * @returns {Promise<PageCitability>} the created (or updated) PageCitability instance.
33
+ */
34
+ async create(item) {
35
+ return super.create(item, { upsert: true, onConflict: 'url' });
36
+ }
25
37
  }
26
38
 
27
39
  export default PageCitabilityCollection;
@@ -13,6 +13,7 @@
13
13
  import { S3Client } from '@aws-sdk/client-s3';
14
14
  import { PostgrestClient } from '@supabase/postgrest-js';
15
15
  import { h1NoCache, keepAliveNoCache } from '@adobe/fetch';
16
+ import { StandardRetryStrategy } from '@smithy/util-retry';
16
17
 
17
18
  import { instrumentAWSClient } from '@adobe/spacecat-shared-utils';
18
19
  import { EntityRegistry } from '../models/index.js';
@@ -78,6 +79,37 @@ const createPostgrestService = (config, client = undefined) => {
78
79
  });
79
80
  };
80
81
 
82
+ /**
83
+ * Custom retry strategy that extends StandardRetryStrategy to include EBUSY errors.
84
+ * The AWS SDK's default retry strategy handles ECONNRESET, ETIMEDOUT, ENOTFOUND, etc.,
85
+ * but does NOT retry EBUSY DNS errors. This class adds EBUSY to the retryable set.
86
+ *
87
+ * StandardRetryStrategy implements RetryStrategyV2 interface, which uses
88
+ * refreshRetryTokenForRetry() rather than retry(). The SDK's retry middleware
89
+ * calls this method to determine if an error should be retried.
90
+ */
91
+ export class EbusyRetryStrategy extends StandardRetryStrategy {
92
+ constructor(maxAttempts = 4) {
93
+ super(maxAttempts);
94
+ }
95
+
96
+ async refreshRetryTokenForRetry(token, errorInfo) {
97
+ const { error } = errorInfo;
98
+
99
+ // Check if this is an EBUSY error (code or message)
100
+ const isEbusy = error?.code === 'EBUSY'
101
+ || (error?.message?.includes('getaddrinfo') && error?.message?.includes('EBUSY'));
102
+
103
+ if (isEbusy) {
104
+ // Reclassify EBUSY as TRANSIENT so StandardRetryStrategy will retry it
105
+ return super.refreshRetryTokenForRetry(token, { ...errorInfo, errorType: 'TRANSIENT' });
106
+ }
107
+
108
+ // Delegate to StandardRetryStrategy for all other errors
109
+ return super.refreshRetryTokenForRetry(token, errorInfo);
110
+ }
111
+ }
112
+
81
113
  /**
82
114
  * Creates an S3 service configuration if bucket configuration is provided.
83
115
  *
@@ -93,7 +125,11 @@ const createS3Service = (config) => {
93
125
  return null;
94
126
  }
95
127
 
96
- const options = region ? { region } : {};
128
+ const options = {
129
+ ...(region ? { region } : {}),
130
+ maxAttempts: 4,
131
+ retryStrategy: new EbusyRetryStrategy(4),
132
+ };
97
133
  const s3Client = instrumentAWSClient(new S3Client(options));
98
134
 
99
135
  return { s3Client, s3Bucket };