@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.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/bulk.ts
1092
- var EXPORT_BATCH_SIZE = 100;
1093
- var IMPORT_CONCURRENCY = 20;
1094
- async function runWithConcurrency(items, concurrency, fn) {
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
- const workers = [];
1098
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1099
- workers.push(
1100
- (async () => {
1101
- while (nextIndex < items.length) {
1102
- const idx = nextIndex++;
1103
- results[idx] = await fn(items[idx]);
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 runWithConcurrency(
1377
+ const results = await runAdaptive(
1174
1378
  chunk.entities,
1175
- IMPORT_CONCURRENCY,
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 runWithConcurrency(
1406
+ const results = await runAdaptive(
1203
1407
  chunk.relationships,
1204
- IMPORT_CONCURRENCY,
1408
+ controller,
1205
1409
  async (rel) => {
1206
1410
  try {
1207
1411
  if (skipCheck) {