monsqlize 2.0.5 → 3.0.0
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 +14 -5
- package/MIGRATION.md +105 -0
- package/README.md +183 -189
- package/SECURITY.md +21 -0
- package/changelogs/README.md +16 -4
- package/changelogs/v2.0.6.md +16 -0
- package/changelogs/v2.0.7.md +63 -0
- package/changelogs/v3.0.0.md +97 -0
- package/dist/cjs/cli/data-task.cjs +17768 -0
- package/dist/cjs/index.cjs +12166 -5505
- package/dist/cjs/transaction/Transaction.cjs +151 -19
- package/dist/cjs/transaction/TransactionManager.cjs +152 -20
- package/dist/esm/index.mjs +12171 -5509
- package/dist/types/base.d.mts +81 -0
- package/dist/types/collection.d.mts +1156 -0
- package/dist/types/collection.d.ts +138 -15
- package/dist/types/data-tasks.d.mts +221 -0
- package/dist/types/data-tasks.d.ts +221 -0
- package/dist/types/expression.d.mts +115 -0
- package/dist/types/index.d.mts +24 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/lock.d.mts +77 -0
- package/dist/types/lock.d.ts +7 -4
- package/dist/types/model.d.mts +666 -0
- package/dist/types/model.d.ts +46 -14
- package/dist/types/mongodb.d.mts +56 -0
- package/dist/types/monsqlize.d.mts +638 -0
- package/dist/types/monsqlize.d.ts +130 -16
- package/dist/types/pool.d.mts +84 -0
- package/dist/types/runtime.d.mts +400 -0
- package/dist/types/runtime.d.ts +18 -5
- package/dist/types/saga.d.mts +148 -0
- package/dist/types/saga.d.ts +11 -6
- package/dist/types/slow-query-log.d.mts +126 -0
- package/dist/types/sync.d.mts +149 -0
- package/dist/types/sync.d.ts +47 -1
- package/dist/types/transaction.d.mts +152 -0
- package/dist/types/transaction.d.ts +8 -0
- package/package.json +32 -14
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
// src/capabilities/transaction/index.ts
|
|
4
4
|
var import_node_crypto = require("node:crypto");
|
|
5
5
|
|
|
6
|
+
// src/core/cache-invalidation-barrier.ts
|
|
7
|
+
var READ_CACHE_PATTERN_PREFIXES = /* @__PURE__ */ new Set([
|
|
8
|
+
"find",
|
|
9
|
+
"findOne",
|
|
10
|
+
"count",
|
|
11
|
+
"findOneById",
|
|
12
|
+
"findByIds",
|
|
13
|
+
"findPage",
|
|
14
|
+
"findPageTotals",
|
|
15
|
+
"aggregate",
|
|
16
|
+
"distinct"
|
|
17
|
+
]);
|
|
18
|
+
function buildCacheInvalidationBarrierKey(namespace) {
|
|
19
|
+
return `cacheDirty:${namespace}`;
|
|
20
|
+
}
|
|
21
|
+
function extractCacheInvalidationBarrierNamespaces(values) {
|
|
22
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
if (!value) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!value.includes("*")) {
|
|
28
|
+
namespaces.add(value);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const match = /^([^:]+):(.+):\*$/.exec(value);
|
|
32
|
+
if (match && READ_CACHE_PATTERN_PREFIXES.has(match[1])) {
|
|
33
|
+
namespaces.add(match[2]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return [...namespaces];
|
|
37
|
+
}
|
|
38
|
+
async function clearCacheInvalidationBarrier(cache, namespacesOrPatterns) {
|
|
39
|
+
if (!cache) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const remove = cache.del ?? cache.delete;
|
|
43
|
+
if (!remove) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const namespaces = extractCacheInvalidationBarrierNamespaces(namespacesOrPatterns);
|
|
47
|
+
for (const namespace of namespaces) {
|
|
48
|
+
await Promise.resolve(remove.call(cache, buildCacheInvalidationBarrierKey(namespace)));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
6
52
|
// src/core/errors/index.ts
|
|
7
53
|
var ErrorCodes = {
|
|
8
54
|
INVALID_ARGUMENT: "INVALID_ARGUMENT",
|
|
@@ -99,6 +145,9 @@ var Transaction = class {
|
|
|
99
145
|
this.startedAt = null;
|
|
100
146
|
this.timeoutTimer = null;
|
|
101
147
|
this.pendingInvalidations = /* @__PURE__ */ new Set();
|
|
148
|
+
this.pendingInvalidationIntents = /* @__PURE__ */ new Map();
|
|
149
|
+
this.recordedWriteOperationCount = 0;
|
|
150
|
+
this.recordedInvalidationCount = 0;
|
|
102
151
|
this.session.__monSQLizeTransaction = this;
|
|
103
152
|
}
|
|
104
153
|
/**
|
|
@@ -117,7 +166,9 @@ var Transaction = class {
|
|
|
117
166
|
this.timeoutTimer = setTimeout(() => {
|
|
118
167
|
if (this.state === "active") {
|
|
119
168
|
this.options.logger?.warn?.(`[Transaction] auto-abort on timeout: ${this.id}`);
|
|
120
|
-
void this.abort()
|
|
169
|
+
void this.abort().catch((error) => {
|
|
170
|
+
this.options.logger?.warn?.("[Transaction] auto-abort failed.", error);
|
|
171
|
+
});
|
|
121
172
|
}
|
|
122
173
|
}, timeout);
|
|
123
174
|
this.timeoutTimer.unref?.();
|
|
@@ -132,12 +183,24 @@ var Transaction = class {
|
|
|
132
183
|
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
|
|
133
184
|
}
|
|
134
185
|
if (typeof this.session.commitTransaction === "function") {
|
|
135
|
-
await this.session.
|
|
186
|
+
await commitTransactionWithRetry(this.session, this.options.logger);
|
|
136
187
|
}
|
|
137
188
|
this.state = "committed";
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
try {
|
|
190
|
+
try {
|
|
191
|
+
await this.flushPendingInvalidations();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.options.logger?.warn?.("[Transaction] post-commit cache invalidation failed.", error);
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
await this.clearPendingInvalidationBarriers().catch((error) => {
|
|
197
|
+
this.options.logger?.warn?.("[Transaction] failed to clear cache invalidation barrier.", error);
|
|
198
|
+
});
|
|
199
|
+
this.options.lockManager?.releaseLocks(this.id);
|
|
200
|
+
this.pendingInvalidations.clear();
|
|
201
|
+
this.pendingInvalidationIntents.clear();
|
|
202
|
+
this.clearTimeout();
|
|
203
|
+
}
|
|
141
204
|
}
|
|
142
205
|
/**
|
|
143
206
|
* Roll back the transaction.
|
|
@@ -147,15 +210,21 @@ var Transaction = class {
|
|
|
147
210
|
if (this.state !== "pending" && this.state !== "active") {
|
|
148
211
|
return;
|
|
149
212
|
}
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
213
|
+
try {
|
|
214
|
+
if (this.state === "active") {
|
|
215
|
+
if (typeof this.session.abortTransaction === "function") {
|
|
216
|
+
await this.session.abortTransaction();
|
|
217
|
+
}
|
|
153
218
|
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.options.logger?.warn?.("[Transaction] abortTransaction failed.", error);
|
|
221
|
+
} finally {
|
|
222
|
+
this.state = "aborted";
|
|
223
|
+
this.options.lockManager?.releaseLocks(this.id);
|
|
224
|
+
this.pendingInvalidations.clear();
|
|
225
|
+
this.pendingInvalidationIntents.clear();
|
|
226
|
+
this.clearTimeout();
|
|
154
227
|
}
|
|
155
|
-
this.state = "aborted";
|
|
156
|
-
this.options.lockManager?.releaseLocks(this.id);
|
|
157
|
-
this.pendingInvalidations.clear();
|
|
158
|
-
this.clearTimeout();
|
|
159
228
|
}
|
|
160
229
|
/**
|
|
161
230
|
* End the transaction session.
|
|
@@ -171,10 +240,45 @@ var Transaction = class {
|
|
|
171
240
|
* @since v1.4.0
|
|
172
241
|
*/
|
|
173
242
|
async recordInvalidation(pattern) {
|
|
174
|
-
this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
await this.recordCacheInvalidation({ type: "pattern", value: pattern });
|
|
244
|
+
}
|
|
245
|
+
async recordCacheInvalidation(intent) {
|
|
246
|
+
if (!intent.value) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const id = `${intent.type}:${intent.value}`;
|
|
250
|
+
if (!this.pendingInvalidationIntents.has(id)) {
|
|
251
|
+
this.recordedInvalidationCount += 1;
|
|
252
|
+
}
|
|
253
|
+
this.pendingInvalidationIntents.set(id, intent);
|
|
254
|
+
this.pendingInvalidations.add(intent.value);
|
|
255
|
+
this.options.lockManager?.addLock(intent.value, this.id);
|
|
256
|
+
}
|
|
257
|
+
recordWriteOperation() {
|
|
258
|
+
this.recordedWriteOperationCount += 1;
|
|
259
|
+
}
|
|
260
|
+
async flushPendingInvalidations() {
|
|
261
|
+
if (!this.options.cache) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const cacheWithDelete = this.options.cache;
|
|
265
|
+
for (const intent of this.pendingInvalidationIntents.values()) {
|
|
266
|
+
if (intent.type === "pattern") {
|
|
267
|
+
await this.options.cache.delPattern?.(intent.value);
|
|
268
|
+
} else if (typeof cacheWithDelete.del === "function") {
|
|
269
|
+
await cacheWithDelete.del(intent.value);
|
|
270
|
+
} else if (typeof cacheWithDelete.delete === "function") {
|
|
271
|
+
await cacheWithDelete.delete(intent.value);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async clearPendingInvalidationBarriers() {
|
|
276
|
+
if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const patterns = [...this.pendingInvalidationIntents.values()].filter((intent) => intent.type === "pattern").map((intent) => intent.value);
|
|
280
|
+
if (patterns.length > 0) {
|
|
281
|
+
await clearCacheInvalidationBarrier(this.options.cache, patterns);
|
|
178
282
|
}
|
|
179
283
|
}
|
|
180
284
|
/**
|
|
@@ -208,11 +312,14 @@ var Transaction = class {
|
|
|
208
312
|
id: this.id,
|
|
209
313
|
state: this.state,
|
|
210
314
|
duration: this.getDuration(),
|
|
211
|
-
hasWriteOperation: this.
|
|
212
|
-
operationCount: this.
|
|
213
|
-
lockedKeysCount: this.
|
|
315
|
+
hasWriteOperation: this.hasWriteOperation(),
|
|
316
|
+
operationCount: this.recordedWriteOperationCount || this.recordedInvalidationCount,
|
|
317
|
+
lockedKeysCount: this.recordedInvalidationCount
|
|
214
318
|
};
|
|
215
319
|
}
|
|
320
|
+
hasWriteOperation() {
|
|
321
|
+
return this.recordedWriteOperationCount > 0 || this.recordedInvalidationCount > 0;
|
|
322
|
+
}
|
|
216
323
|
clearTimeout() {
|
|
217
324
|
if (this.timeoutTimer) {
|
|
218
325
|
clearTimeout(this.timeoutTimer);
|
|
@@ -238,6 +345,31 @@ function stringifySessionId(id) {
|
|
|
238
345
|
}
|
|
239
346
|
return String(id);
|
|
240
347
|
}
|
|
348
|
+
function isUnknownTransactionCommitResult(error) {
|
|
349
|
+
if (!error || typeof error !== "object") {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const candidate = error;
|
|
353
|
+
return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
|
|
354
|
+
}
|
|
355
|
+
async function commitTransactionWithRetry(session, logger) {
|
|
356
|
+
const maxAttempts = 3;
|
|
357
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
358
|
+
try {
|
|
359
|
+
await session.commitTransaction();
|
|
360
|
+
return;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (!isUnknownTransactionCommitResult(error) || attempt === maxAttempts - 1) {
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
logger?.warn?.(`[Transaction] retrying commit after UnknownTransactionCommitResult (attempt ${attempt + 2}/${maxAttempts})`);
|
|
366
|
+
await sleep(100 * (attempt + 1));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function sleep(ms) {
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
372
|
+
}
|
|
241
373
|
|
|
242
374
|
// src/entry/compat/transaction/Transaction.ts
|
|
243
375
|
module.exports = Transaction;
|
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
// src/capabilities/transaction/index.ts
|
|
4
4
|
var import_node_crypto = require("node:crypto");
|
|
5
5
|
|
|
6
|
+
// src/core/cache-invalidation-barrier.ts
|
|
7
|
+
var READ_CACHE_PATTERN_PREFIXES = /* @__PURE__ */ new Set([
|
|
8
|
+
"find",
|
|
9
|
+
"findOne",
|
|
10
|
+
"count",
|
|
11
|
+
"findOneById",
|
|
12
|
+
"findByIds",
|
|
13
|
+
"findPage",
|
|
14
|
+
"findPageTotals",
|
|
15
|
+
"aggregate",
|
|
16
|
+
"distinct"
|
|
17
|
+
]);
|
|
18
|
+
function buildCacheInvalidationBarrierKey(namespace) {
|
|
19
|
+
return `cacheDirty:${namespace}`;
|
|
20
|
+
}
|
|
21
|
+
function extractCacheInvalidationBarrierNamespaces(values) {
|
|
22
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
if (!value) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (!value.includes("*")) {
|
|
28
|
+
namespaces.add(value);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const match = /^([^:]+):(.+):\*$/.exec(value);
|
|
32
|
+
if (match && READ_CACHE_PATTERN_PREFIXES.has(match[1])) {
|
|
33
|
+
namespaces.add(match[2]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return [...namespaces];
|
|
37
|
+
}
|
|
38
|
+
async function clearCacheInvalidationBarrier(cache, namespacesOrPatterns) {
|
|
39
|
+
if (!cache) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const remove = cache.del ?? cache.delete;
|
|
43
|
+
if (!remove) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const namespaces = extractCacheInvalidationBarrierNamespaces(namespacesOrPatterns);
|
|
47
|
+
for (const namespace of namespaces) {
|
|
48
|
+
await Promise.resolve(remove.call(cache, buildCacheInvalidationBarrierKey(namespace)));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
6
52
|
// src/core/errors/index.ts
|
|
7
53
|
var ErrorCodes = {
|
|
8
54
|
INVALID_ARGUMENT: "INVALID_ARGUMENT",
|
|
@@ -99,6 +145,9 @@ var Transaction = class {
|
|
|
99
145
|
this.startedAt = null;
|
|
100
146
|
this.timeoutTimer = null;
|
|
101
147
|
this.pendingInvalidations = /* @__PURE__ */ new Set();
|
|
148
|
+
this.pendingInvalidationIntents = /* @__PURE__ */ new Map();
|
|
149
|
+
this.recordedWriteOperationCount = 0;
|
|
150
|
+
this.recordedInvalidationCount = 0;
|
|
102
151
|
this.session.__monSQLizeTransaction = this;
|
|
103
152
|
}
|
|
104
153
|
/**
|
|
@@ -117,7 +166,9 @@ var Transaction = class {
|
|
|
117
166
|
this.timeoutTimer = setTimeout(() => {
|
|
118
167
|
if (this.state === "active") {
|
|
119
168
|
this.options.logger?.warn?.(`[Transaction] auto-abort on timeout: ${this.id}`);
|
|
120
|
-
void this.abort()
|
|
169
|
+
void this.abort().catch((error) => {
|
|
170
|
+
this.options.logger?.warn?.("[Transaction] auto-abort failed.", error);
|
|
171
|
+
});
|
|
121
172
|
}
|
|
122
173
|
}, timeout);
|
|
123
174
|
this.timeoutTimer.unref?.();
|
|
@@ -132,12 +183,24 @@ var Transaction = class {
|
|
|
132
183
|
throw createError(ErrorCodes.INVALID_OPERATION, `Cannot commit transaction in state: ${this.state}`);
|
|
133
184
|
}
|
|
134
185
|
if (typeof this.session.commitTransaction === "function") {
|
|
135
|
-
await this.session.
|
|
186
|
+
await commitTransactionWithRetry(this.session, this.options.logger);
|
|
136
187
|
}
|
|
137
188
|
this.state = "committed";
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
189
|
+
try {
|
|
190
|
+
try {
|
|
191
|
+
await this.flushPendingInvalidations();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
this.options.logger?.warn?.("[Transaction] post-commit cache invalidation failed.", error);
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
await this.clearPendingInvalidationBarriers().catch((error) => {
|
|
197
|
+
this.options.logger?.warn?.("[Transaction] failed to clear cache invalidation barrier.", error);
|
|
198
|
+
});
|
|
199
|
+
this.options.lockManager?.releaseLocks(this.id);
|
|
200
|
+
this.pendingInvalidations.clear();
|
|
201
|
+
this.pendingInvalidationIntents.clear();
|
|
202
|
+
this.clearTimeout();
|
|
203
|
+
}
|
|
141
204
|
}
|
|
142
205
|
/**
|
|
143
206
|
* Roll back the transaction.
|
|
@@ -147,15 +210,21 @@ var Transaction = class {
|
|
|
147
210
|
if (this.state !== "pending" && this.state !== "active") {
|
|
148
211
|
return;
|
|
149
212
|
}
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
213
|
+
try {
|
|
214
|
+
if (this.state === "active") {
|
|
215
|
+
if (typeof this.session.abortTransaction === "function") {
|
|
216
|
+
await this.session.abortTransaction();
|
|
217
|
+
}
|
|
153
218
|
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.options.logger?.warn?.("[Transaction] abortTransaction failed.", error);
|
|
221
|
+
} finally {
|
|
222
|
+
this.state = "aborted";
|
|
223
|
+
this.options.lockManager?.releaseLocks(this.id);
|
|
224
|
+
this.pendingInvalidations.clear();
|
|
225
|
+
this.pendingInvalidationIntents.clear();
|
|
226
|
+
this.clearTimeout();
|
|
154
227
|
}
|
|
155
|
-
this.state = "aborted";
|
|
156
|
-
this.options.lockManager?.releaseLocks(this.id);
|
|
157
|
-
this.pendingInvalidations.clear();
|
|
158
|
-
this.clearTimeout();
|
|
159
228
|
}
|
|
160
229
|
/**
|
|
161
230
|
* End the transaction session.
|
|
@@ -171,10 +240,45 @@ var Transaction = class {
|
|
|
171
240
|
* @since v1.4.0
|
|
172
241
|
*/
|
|
173
242
|
async recordInvalidation(pattern) {
|
|
174
|
-
this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
await this.recordCacheInvalidation({ type: "pattern", value: pattern });
|
|
244
|
+
}
|
|
245
|
+
async recordCacheInvalidation(intent) {
|
|
246
|
+
if (!intent.value) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const id = `${intent.type}:${intent.value}`;
|
|
250
|
+
if (!this.pendingInvalidationIntents.has(id)) {
|
|
251
|
+
this.recordedInvalidationCount += 1;
|
|
252
|
+
}
|
|
253
|
+
this.pendingInvalidationIntents.set(id, intent);
|
|
254
|
+
this.pendingInvalidations.add(intent.value);
|
|
255
|
+
this.options.lockManager?.addLock(intent.value, this.id);
|
|
256
|
+
}
|
|
257
|
+
recordWriteOperation() {
|
|
258
|
+
this.recordedWriteOperationCount += 1;
|
|
259
|
+
}
|
|
260
|
+
async flushPendingInvalidations() {
|
|
261
|
+
if (!this.options.cache) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const cacheWithDelete = this.options.cache;
|
|
265
|
+
for (const intent of this.pendingInvalidationIntents.values()) {
|
|
266
|
+
if (intent.type === "pattern") {
|
|
267
|
+
await this.options.cache.delPattern?.(intent.value);
|
|
268
|
+
} else if (typeof cacheWithDelete.del === "function") {
|
|
269
|
+
await cacheWithDelete.del(intent.value);
|
|
270
|
+
} else if (typeof cacheWithDelete.delete === "function") {
|
|
271
|
+
await cacheWithDelete.delete(intent.value);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async clearPendingInvalidationBarriers() {
|
|
276
|
+
if (!this.options.cache || this.pendingInvalidationIntents.size === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const patterns = [...this.pendingInvalidationIntents.values()].filter((intent) => intent.type === "pattern").map((intent) => intent.value);
|
|
280
|
+
if (patterns.length > 0) {
|
|
281
|
+
await clearCacheInvalidationBarrier(this.options.cache, patterns);
|
|
178
282
|
}
|
|
179
283
|
}
|
|
180
284
|
/**
|
|
@@ -208,11 +312,14 @@ var Transaction = class {
|
|
|
208
312
|
id: this.id,
|
|
209
313
|
state: this.state,
|
|
210
314
|
duration: this.getDuration(),
|
|
211
|
-
hasWriteOperation: this.
|
|
212
|
-
operationCount: this.
|
|
213
|
-
lockedKeysCount: this.
|
|
315
|
+
hasWriteOperation: this.hasWriteOperation(),
|
|
316
|
+
operationCount: this.recordedWriteOperationCount || this.recordedInvalidationCount,
|
|
317
|
+
lockedKeysCount: this.recordedInvalidationCount
|
|
214
318
|
};
|
|
215
319
|
}
|
|
320
|
+
hasWriteOperation() {
|
|
321
|
+
return this.recordedWriteOperationCount > 0 || this.recordedInvalidationCount > 0;
|
|
322
|
+
}
|
|
216
323
|
clearTimeout() {
|
|
217
324
|
if (this.timeoutTimer) {
|
|
218
325
|
clearTimeout(this.timeoutTimer);
|
|
@@ -303,6 +410,9 @@ var TransactionManager = class {
|
|
|
303
410
|
lastError = error;
|
|
304
411
|
await transaction.abort();
|
|
305
412
|
this.recordStats(transaction, Date.now() - startedAt, false);
|
|
413
|
+
if (transaction.state === "committed") {
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
306
416
|
if (!enableRetry || attempt === maxRetries || !isTransientTransactionError(error)) {
|
|
307
417
|
throw error;
|
|
308
418
|
}
|
|
@@ -364,7 +474,7 @@ var TransactionManager = class {
|
|
|
364
474
|
} else {
|
|
365
475
|
this.stats.failedTransactions += 1;
|
|
366
476
|
}
|
|
367
|
-
if (transaction.
|
|
477
|
+
if (transaction.hasWriteOperation()) {
|
|
368
478
|
this.stats.writeTransactions += 1;
|
|
369
479
|
} else {
|
|
370
480
|
this.stats.readOnlyTransactions += 1;
|
|
@@ -414,6 +524,28 @@ function isTransientTransactionError(error) {
|
|
|
414
524
|
}
|
|
415
525
|
return candidate.code === 112 || candidate.code === 117;
|
|
416
526
|
}
|
|
527
|
+
function isUnknownTransactionCommitResult(error) {
|
|
528
|
+
if (!error || typeof error !== "object") {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const candidate = error;
|
|
532
|
+
return typeof candidate.hasErrorLabel === "function" && candidate.hasErrorLabel("UnknownTransactionCommitResult");
|
|
533
|
+
}
|
|
534
|
+
async function commitTransactionWithRetry(session, logger) {
|
|
535
|
+
const maxAttempts = 3;
|
|
536
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
537
|
+
try {
|
|
538
|
+
await session.commitTransaction();
|
|
539
|
+
return;
|
|
540
|
+
} catch (error) {
|
|
541
|
+
if (!isUnknownTransactionCommitResult(error) || attempt === maxAttempts - 1) {
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
logger?.warn?.(`[Transaction] retrying commit after UnknownTransactionCommitResult (attempt ${attempt + 2}/${maxAttempts})`);
|
|
545
|
+
await sleep(100 * (attempt + 1));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
417
549
|
async function sleep(ms) {
|
|
418
550
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
419
551
|
}
|