@utaba/deep-memory-storage-cosmosdb 0.9.2 → 0.11.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/dist/index.cjs +232 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +221 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1088,26 +1088,229 @@ function isInTimeRange(timestamp, timeRange) {
|
|
|
1088
1088
|
return timestamp >= timeRange.from && timestamp <= timeRange.to;
|
|
1089
1089
|
}
|
|
1090
1090
|
|
|
1091
|
-
// src/queries/
|
|
1092
|
-
|
|
1093
|
-
var
|
|
1094
|
-
|
|
1091
|
+
// src/queries/adaptive-import.ts
|
|
1092
|
+
import { ImportThrottleAbortError } from "@utaba/deep-memory";
|
|
1093
|
+
var DEFAULT_MIN = 2;
|
|
1094
|
+
var DEFAULT_START = 5;
|
|
1095
|
+
var DEFAULT_MAX = 32;
|
|
1096
|
+
var DEFAULT_INCREASE_AFTER = 50;
|
|
1097
|
+
var DEFAULT_COOLDOWN_MS = 1e3;
|
|
1098
|
+
var DEFAULT_MAX_CONSECUTIVE_THROTTLES_AT_MIN = 10;
|
|
1099
|
+
function resolveOptions(opts) {
|
|
1100
|
+
const min = Math.max(1, opts?.min ?? DEFAULT_MIN);
|
|
1101
|
+
const max = Math.max(min, opts?.max ?? DEFAULT_MAX);
|
|
1102
|
+
const start = Math.min(max, Math.max(min, opts?.start ?? DEFAULT_START));
|
|
1103
|
+
const increaseAfter = Math.max(1, opts?.increaseAfter ?? DEFAULT_INCREASE_AFTER);
|
|
1104
|
+
const cooldownMs = Math.max(0, opts?.cooldownMs ?? DEFAULT_COOLDOWN_MS);
|
|
1105
|
+
const maxConsecutiveThrottlesAtMin = Math.max(
|
|
1106
|
+
1,
|
|
1107
|
+
opts?.maxConsecutiveThrottlesAtMin ?? DEFAULT_MAX_CONSECUTIVE_THROTTLES_AT_MIN
|
|
1108
|
+
);
|
|
1109
|
+
return {
|
|
1110
|
+
min,
|
|
1111
|
+
start,
|
|
1112
|
+
max,
|
|
1113
|
+
increaseAfter,
|
|
1114
|
+
cooldownMs,
|
|
1115
|
+
maxConsecutiveThrottlesAtMin,
|
|
1116
|
+
onAdjust: opts?.onAdjust
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
var AdaptiveConcurrencyController = class {
|
|
1120
|
+
opts;
|
|
1121
|
+
current;
|
|
1122
|
+
streak = 0;
|
|
1123
|
+
cooldownUntil = 0;
|
|
1124
|
+
completed = 0;
|
|
1125
|
+
throttled = 0;
|
|
1126
|
+
startEmitted = false;
|
|
1127
|
+
consecutiveThrottlesAtMin = 0;
|
|
1128
|
+
constructor(opts) {
|
|
1129
|
+
this.opts = resolveOptions(opts);
|
|
1130
|
+
this.current = this.opts.start;
|
|
1131
|
+
}
|
|
1132
|
+
/** Current target concurrency. */
|
|
1133
|
+
getConcurrency() {
|
|
1134
|
+
return this.current;
|
|
1135
|
+
}
|
|
1136
|
+
/** Configured ceiling — used by the runner to size its worker pool. */
|
|
1137
|
+
getMaxConcurrency() {
|
|
1138
|
+
return this.opts.max;
|
|
1139
|
+
}
|
|
1140
|
+
/** Total tasks that have been noted as completed (success or throttle). */
|
|
1141
|
+
getCompleted() {
|
|
1142
|
+
return this.completed;
|
|
1143
|
+
}
|
|
1144
|
+
/** Total tasks that observed at least one throttle. */
|
|
1145
|
+
getThrottledCount() {
|
|
1146
|
+
return this.throttled;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Earliest time (ms since epoch) at which a new task may be dispatched. Zero
|
|
1150
|
+
* if there is no active cooldown.
|
|
1151
|
+
*/
|
|
1152
|
+
getCooldownUntil() {
|
|
1153
|
+
return this.cooldownUntil;
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Emit the initial `start` event the first time the controller is queried
|
|
1157
|
+
* for adjustment events. Kept separate so construction has no side effects.
|
|
1158
|
+
*/
|
|
1159
|
+
emitStartIfNeeded() {
|
|
1160
|
+
if (this.startEmitted) return;
|
|
1161
|
+
this.startEmitted = true;
|
|
1162
|
+
this.notify("start", this.current);
|
|
1163
|
+
}
|
|
1164
|
+
/** Record a throttle-free task completion. May trigger ramp-up. */
|
|
1165
|
+
noteSuccess() {
|
|
1166
|
+
this.completed++;
|
|
1167
|
+
this.streak++;
|
|
1168
|
+
this.consecutiveThrottlesAtMin = 0;
|
|
1169
|
+
if (this.streak >= this.opts.increaseAfter && this.current < this.opts.max) {
|
|
1170
|
+
const previous = this.current;
|
|
1171
|
+
this.current = Math.min(this.opts.max, this.current + 1);
|
|
1172
|
+
this.streak = 0;
|
|
1173
|
+
this.notify("ramp-up", previous);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
/** Record a task that observed at least one throttle. Halves concurrency. */
|
|
1177
|
+
noteThrottle(now = Date.now()) {
|
|
1178
|
+
this.completed++;
|
|
1179
|
+
this.throttled++;
|
|
1180
|
+
this.streak = 0;
|
|
1181
|
+
const previous = this.current;
|
|
1182
|
+
const next = Math.max(this.opts.min, Math.floor(this.current / 2));
|
|
1183
|
+
this.cooldownUntil = now + this.opts.cooldownMs;
|
|
1184
|
+
if (next !== previous) {
|
|
1185
|
+
this.current = next;
|
|
1186
|
+
this.notify("throttle", previous);
|
|
1187
|
+
}
|
|
1188
|
+
if (this.current === this.opts.min) {
|
|
1189
|
+
this.consecutiveThrottlesAtMin++;
|
|
1190
|
+
} else {
|
|
1191
|
+
this.consecutiveThrottlesAtMin = 0;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/** Configured circuit-breaker threshold. */
|
|
1195
|
+
getMaxConsecutiveThrottlesAtMin() {
|
|
1196
|
+
return this.opts.maxConsecutiveThrottlesAtMin;
|
|
1197
|
+
}
|
|
1198
|
+
/** How many consecutive throttles have occurred at min. */
|
|
1199
|
+
getConsecutiveThrottlesAtMin() {
|
|
1200
|
+
return this.consecutiveThrottlesAtMin;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Whether the circuit breaker has tripped — runner should stop dispatching
|
|
1204
|
+
* new tasks and surface an `ImportThrottleAbortError` to the caller.
|
|
1205
|
+
*/
|
|
1206
|
+
shouldAbort() {
|
|
1207
|
+
return this.consecutiveThrottlesAtMin >= this.opts.maxConsecutiveThrottlesAtMin;
|
|
1208
|
+
}
|
|
1209
|
+
notify(reason, previous) {
|
|
1210
|
+
const cb = this.opts.onAdjust;
|
|
1211
|
+
if (!cb) return;
|
|
1212
|
+
try {
|
|
1213
|
+
cb({
|
|
1214
|
+
concurrency: this.current,
|
|
1215
|
+
previousConcurrency: previous,
|
|
1216
|
+
reason,
|
|
1217
|
+
tasksCompleted: this.completed,
|
|
1218
|
+
throttledCount: this.throttled
|
|
1219
|
+
});
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
async function runTaskWithUsage(fn) {
|
|
1225
|
+
const parent = usageScope.getStore();
|
|
1226
|
+
const taskAcc = { ru: 0, calls: 0, retries: 0 };
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await usageScope.run(taskAcc, fn);
|
|
1229
|
+
return { result, retries: taskAcc.retries };
|
|
1230
|
+
} finally {
|
|
1231
|
+
if (parent) {
|
|
1232
|
+
parent.ru += taskAcc.ru;
|
|
1233
|
+
parent.calls += taskAcc.calls;
|
|
1234
|
+
parent.retries += taskAcc.retries;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function sleep2(ms) {
|
|
1239
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1240
|
+
}
|
|
1241
|
+
async function runAdaptive(items, controller, fn) {
|
|
1242
|
+
if (items.length === 0) return [];
|
|
1243
|
+
controller.emitStartIfNeeded();
|
|
1095
1244
|
const results = new Array(items.length);
|
|
1096
1245
|
let nextIndex = 0;
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1246
|
+
let inFlight = 0;
|
|
1247
|
+
let aborted = false;
|
|
1248
|
+
let gate = createGate();
|
|
1249
|
+
function pokeGate() {
|
|
1250
|
+
const old = gate;
|
|
1251
|
+
gate = createGate();
|
|
1252
|
+
old.resolve();
|
|
1253
|
+
}
|
|
1254
|
+
async function worker() {
|
|
1255
|
+
while (nextIndex < items.length) {
|
|
1256
|
+
if (aborted) return;
|
|
1257
|
+
while (true) {
|
|
1258
|
+
if (aborted || nextIndex >= items.length) return;
|
|
1259
|
+
const cooldownRemaining = controller.getCooldownUntil() - Date.now();
|
|
1260
|
+
const slotsAvailable = inFlight < controller.getConcurrency();
|
|
1261
|
+
if (cooldownRemaining <= 0 && slotsAvailable) break;
|
|
1262
|
+
if (cooldownRemaining > 0) {
|
|
1263
|
+
await Promise.race([sleep2(cooldownRemaining), gate.promise]);
|
|
1264
|
+
} else {
|
|
1265
|
+
await gate.promise;
|
|
1104
1266
|
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1267
|
+
}
|
|
1268
|
+
if (aborted || nextIndex >= items.length) return;
|
|
1269
|
+
const idx = nextIndex++;
|
|
1270
|
+
inFlight++;
|
|
1271
|
+
try {
|
|
1272
|
+
const { result, retries } = await runTaskWithUsage(() => fn(items[idx]));
|
|
1273
|
+
results[idx] = result;
|
|
1274
|
+
if (retries > 0) {
|
|
1275
|
+
controller.noteThrottle();
|
|
1276
|
+
} else {
|
|
1277
|
+
controller.noteSuccess();
|
|
1278
|
+
}
|
|
1279
|
+
if (controller.shouldAbort()) {
|
|
1280
|
+
aborted = true;
|
|
1281
|
+
}
|
|
1282
|
+
} finally {
|
|
1283
|
+
inFlight--;
|
|
1284
|
+
pokeGate();
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
const workerCount = Math.min(items.length, controller.getMaxConcurrency());
|
|
1289
|
+
const workers = [];
|
|
1290
|
+
for (let w = 0; w < workerCount; w++) {
|
|
1291
|
+
workers.push(worker());
|
|
1107
1292
|
}
|
|
1108
1293
|
await Promise.all(workers);
|
|
1294
|
+
if (aborted) {
|
|
1295
|
+
throw new ImportThrottleAbortError(
|
|
1296
|
+
controller.getConcurrency(),
|
|
1297
|
+
controller.getConsecutiveThrottlesAtMin(),
|
|
1298
|
+
controller.getCompleted(),
|
|
1299
|
+
controller.getThrottledCount()
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1109
1302
|
return results;
|
|
1110
1303
|
}
|
|
1304
|
+
function createGate() {
|
|
1305
|
+
let resolve;
|
|
1306
|
+
const promise = new Promise((r) => {
|
|
1307
|
+
resolve = r;
|
|
1308
|
+
});
|
|
1309
|
+
return { promise, resolve };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/queries/bulk.ts
|
|
1313
|
+
var EXPORT_BATCH_SIZE = 100;
|
|
1111
1314
|
async function* exportAll(conn, repositoryId) {
|
|
1112
1315
|
let sequence = 0;
|
|
1113
1316
|
let cursor = "";
|
|
@@ -1168,11 +1371,12 @@ async function importBulk(conn, repositoryId, data, options) {
|
|
|
1168
1371
|
let relationshipsImported = 0;
|
|
1169
1372
|
const errors = [];
|
|
1170
1373
|
const skipCheck = options?.skipExistenceCheck ?? false;
|
|
1374
|
+
const controller = new AdaptiveConcurrencyController(options?.adaptiveConcurrency);
|
|
1171
1375
|
for (const chunk of data) {
|
|
1172
1376
|
if (chunk.entities && chunk.entities.length > 0) {
|
|
1173
|
-
const results = await
|
|
1377
|
+
const results = await runAdaptive(
|
|
1174
1378
|
chunk.entities,
|
|
1175
|
-
|
|
1379
|
+
controller,
|
|
1176
1380
|
async (entity) => {
|
|
1177
1381
|
try {
|
|
1178
1382
|
if (skipCheck) {
|
|
@@ -1199,9 +1403,9 @@ async function importBulk(conn, repositoryId, data, options) {
|
|
|
1199
1403
|
}
|
|
1200
1404
|
}
|
|
1201
1405
|
if (chunk.relationships && chunk.relationships.length > 0) {
|
|
1202
|
-
const results = await
|
|
1406
|
+
const results = await runAdaptive(
|
|
1203
1407
|
chunk.relationships,
|
|
1204
|
-
|
|
1408
|
+
controller,
|
|
1205
1409
|
async (rel) => {
|
|
1206
1410
|
try {
|
|
1207
1411
|
if (skipCheck) {
|