@wictorwilen/cocogen 1.1.0-preview.10 → 1.1.0-preview.11
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 +6 -0
- package/data/graph-external-connectors-principal.json +1 -1
- package/dist/init/templates/dotnet/Core/ConnectorCore.cs.ejs +79 -18
- package/dist/init/templates/dotnet/Program.commandline.cs.ejs +15 -3
- package/dist/init/templates/dotnet/README.md.ejs +1 -0
- package/dist/init/templates/ts/README.md.ejs +1 -0
- package/dist/init/templates/ts/src/cli.ts.ejs +13 -1
- package/dist/init/templates/ts/src/core/connectorCore.ts.ejs +62 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.0-preview.11] - 2026-04-09
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Generated TypeScript and .NET connector CLIs now support `--retry-attempts` (default `2`, max `10`) for ingestion; failed items are queued and retried after the initial batch pass completes, with retry-round backoff and final failed-item summaries.
|
|
14
|
+
|
|
10
15
|
## [1.1.0-preview.10] - 2026-04-08
|
|
11
16
|
|
|
12
17
|
## [1.1.0-preview.9] - 2026-04-08
|
|
@@ -378,6 +383,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
378
383
|
- Collection values no longer split on commas; use semicolons instead.
|
|
379
384
|
|
|
380
385
|
[Unreleased]: https://github.com/wictorwilen/cocogen/compare/v1.0.16...HEAD
|
|
386
|
+
[1.1.0-preview.11]: https://github.com/wictorwilen/cocogen/compare/main...v1.1.0-preview.11
|
|
381
387
|
[1.1.0-preview.10]: https://github.com/wictorwilen/cocogen/compare/main...v1.1.0-preview.10
|
|
382
388
|
[1.1.0-preview.9]: https://github.com/wictorwilen/cocogen/compare/main...v1.1.0-preview.9
|
|
383
389
|
[1.1.0-preview.8]: https://github.com/wictorwilen/cocogen/compare/main...v1.1.0-preview.8
|
|
@@ -269,43 +269,54 @@ public sealed class ConnectorCore<TItem>
|
|
|
269
269
|
/// <summary>
|
|
270
270
|
/// Ingest items from a datasource.
|
|
271
271
|
/// </summary>
|
|
272
|
-
public async Task IngestAsync(IItemSource<TItem> source, bool dryRun, int? limit, int batchSize, bool verbose, bool failFast)
|
|
272
|
+
public async Task IngestAsync(IItemSource<TItem> source, bool dryRun, int? limit, int batchSize, int retryAttempts, bool verbose, bool failFast)
|
|
273
273
|
{
|
|
274
274
|
if (batchSize < 1 || batchSize > 20)
|
|
275
275
|
throw new InvalidOperationException("Invalid batch size: expected integer between 1 and 20.");
|
|
276
|
+
if (retryAttempts < 0 || retryAttempts > MaxItemRetryAttempts)
|
|
277
|
+
throw new InvalidOperationException($"Invalid retry attempts: expected integer between 0 and {MaxItemRetryAttempts}.");
|
|
278
|
+
|
|
279
|
+
var retryQueueDelayAttempts = Math.Max(0, retryAttempts);
|
|
276
280
|
|
|
277
281
|
var count = 0;
|
|
278
282
|
var successCount = 0;
|
|
279
|
-
var failures = new List<(int Index, string Id, string Message)>();
|
|
280
|
-
var pending = new List<(int Index, string Id, TItem Item)>();
|
|
283
|
+
var failures = new List<(int Index, string Id, int Attempts, string Message)>();
|
|
284
|
+
var pending = new List<(int Index, string Id, TItem Item, int Attempt)>();
|
|
285
|
+
var retryQueue = new List<(int Index, string Id, TItem Item, int Attempt)>();
|
|
281
286
|
|
|
282
|
-
async Task<(int Index, string Id, bool Success, string Message, Exception? Error)> ProcessPendingItemAsync(
|
|
283
|
-
(int Index, string Id, TItem Item) pendingItem)
|
|
287
|
+
async Task<(int Index, string Id, int Attempt, bool Success, string Message, Exception? Error, TItem Item)> ProcessPendingItemAsync(
|
|
288
|
+
(int Index, string Id, TItem Item, int Attempt) pendingItem)
|
|
284
289
|
{
|
|
285
290
|
try
|
|
286
291
|
{
|
|
287
292
|
await PutItemAsync(pendingItem.Item, verbose);
|
|
288
|
-
return (pendingItem.Index, pendingItem.Id, true, string.Empty, null);
|
|
293
|
+
return (pendingItem.Index, pendingItem.Id, pendingItem.Attempt, true, string.Empty, null, pendingItem.Item);
|
|
289
294
|
}
|
|
290
295
|
catch (Exception ex)
|
|
291
296
|
{
|
|
292
|
-
return (pendingItem.Index, pendingItem.Id, false, ex.Message, ex);
|
|
297
|
+
return (pendingItem.Index, pendingItem.Id, pendingItem.Attempt, false, ex.Message, ex, pendingItem.Item);
|
|
293
298
|
}
|
|
294
299
|
}
|
|
295
300
|
|
|
296
|
-
async Task FlushBatchAsync()
|
|
301
|
+
async Task FlushBatchAsync(IReadOnlyList<(int Index, string Id, TItem Item, int Attempt)> batch)
|
|
297
302
|
{
|
|
298
|
-
if (
|
|
303
|
+
if (batch.Count == 0)
|
|
299
304
|
return;
|
|
300
305
|
|
|
301
|
-
var tasks = new List<Task<(int Index, string Id, bool Success, string Message, Exception? Error)>>();
|
|
302
|
-
foreach (var pendingItem in
|
|
306
|
+
var tasks = new List<Task<(int Index, string Id, int Attempt, bool Success, string Message, Exception? Error, TItem Item)>>();
|
|
307
|
+
foreach (var pendingItem in batch)
|
|
303
308
|
{
|
|
304
|
-
|
|
309
|
+
if (pendingItem.Attempt == 0)
|
|
310
|
+
{
|
|
311
|
+
Console.WriteLine($"info: ingesting item {pendingItem.Index} (id={pendingItem.Id})");
|
|
312
|
+
}
|
|
313
|
+
else
|
|
314
|
+
{
|
|
315
|
+
Console.WriteLine($"info: retrying item {pendingItem.Index} (id={pendingItem.Id}) attempt {pendingItem.Attempt}/{retryAttempts}");
|
|
316
|
+
}
|
|
305
317
|
tasks.Add(ProcessPendingItemAsync(pendingItem));
|
|
306
318
|
}
|
|
307
319
|
|
|
308
|
-
pending.Clear();
|
|
309
320
|
var results = await Task.WhenAll(tasks);
|
|
310
321
|
Exception? firstError = null;
|
|
311
322
|
|
|
@@ -319,7 +330,13 @@ public sealed class ConnectorCore<TItem>
|
|
|
319
330
|
}
|
|
320
331
|
|
|
321
332
|
Console.Error.WriteLine($"error: failed item {result.Index} (id={result.Id})");
|
|
322
|
-
|
|
333
|
+
if (!failFast && result.Attempt < retryAttempts)
|
|
334
|
+
{
|
|
335
|
+
retryQueue.Add((result.Index, result.Id, result.Item, result.Attempt + 1));
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
failures.Add((result.Index, result.Id, result.Attempt + 1, result.Message));
|
|
323
340
|
if (failFast && firstError is null)
|
|
324
341
|
{
|
|
325
342
|
firstError = result.Error;
|
|
@@ -340,9 +357,13 @@ public sealed class ConnectorCore<TItem>
|
|
|
340
357
|
|
|
341
358
|
if (!dryRun)
|
|
342
359
|
{
|
|
343
|
-
pending.Add((index, itemId, item));
|
|
360
|
+
pending.Add((index, itemId, item, 0));
|
|
344
361
|
if (pending.Count >= batchSize)
|
|
345
|
-
|
|
362
|
+
{
|
|
363
|
+
var batch = pending.ToArray();
|
|
364
|
+
pending.Clear();
|
|
365
|
+
await FlushBatchAsync(batch);
|
|
366
|
+
}
|
|
346
367
|
}
|
|
347
368
|
else if (verbose)
|
|
348
369
|
{
|
|
@@ -354,7 +375,46 @@ public sealed class ConnectorCore<TItem>
|
|
|
354
375
|
}
|
|
355
376
|
|
|
356
377
|
if (!dryRun)
|
|
357
|
-
|
|
378
|
+
{
|
|
379
|
+
var batch = pending.ToArray();
|
|
380
|
+
pending.Clear();
|
|
381
|
+
await FlushBatchAsync(batch);
|
|
382
|
+
|
|
383
|
+
for (var retryAttempt = 1; retryAttempt <= retryAttempts && retryQueue.Count > 0; retryAttempt++)
|
|
384
|
+
{
|
|
385
|
+
var queue = retryQueue.ToArray();
|
|
386
|
+
retryQueue.Clear();
|
|
387
|
+
Console.WriteLine($"warn: retry pass {retryAttempt}/{retryAttempts} for {queue.Length} item(s)");
|
|
388
|
+
|
|
389
|
+
for (var offset = 0; offset < queue.Length; offset += batchSize)
|
|
390
|
+
{
|
|
391
|
+
var length = Math.Min(batchSize, queue.Length - offset);
|
|
392
|
+
var chunk = new (int Index, string Id, TItem Item, int Attempt)[length];
|
|
393
|
+
Array.Copy(queue, offset, chunk, 0, length);
|
|
394
|
+
await FlushBatchAsync(chunk);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (retryQueue.Count > 0 && retryAttempt < retryQueueDelayAttempts)
|
|
398
|
+
{
|
|
399
|
+
var delay = ComputeDelayMs(retryAttempt - 1, (int?)null);
|
|
400
|
+
Console.WriteLine($"warn: waiting {delay}ms before next retry pass");
|
|
401
|
+
await Task.Delay(delay);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (retryQueue.Count > 0)
|
|
406
|
+
{
|
|
407
|
+
foreach (var item in retryQueue)
|
|
408
|
+
{
|
|
409
|
+
failures.Add((
|
|
410
|
+
item.Index,
|
|
411
|
+
item.Id,
|
|
412
|
+
item.Attempt,
|
|
413
|
+
$"exhausted retry queue after {item.Attempt} attempt(s)"
|
|
414
|
+
));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
358
418
|
|
|
359
419
|
if (!dryRun)
|
|
360
420
|
{
|
|
@@ -370,7 +430,7 @@ public sealed class ConnectorCore<TItem>
|
|
|
370
430
|
Console.WriteLine($"warn: {failures.Count} item(s) failed");
|
|
371
431
|
foreach (var failure in failures)
|
|
372
432
|
{
|
|
373
|
-
Console.WriteLine($"warn: failed item {failure.Index} (id={failure.Id}) - {failure.Message}");
|
|
433
|
+
Console.WriteLine($"warn: failed item {failure.Index} (id={failure.Id}) after {failure.Attempts} attempt(s) - {failure.Message}");
|
|
374
434
|
}
|
|
375
435
|
}
|
|
376
436
|
}
|
|
@@ -643,6 +703,7 @@ public sealed class ConnectorCore<TItem>
|
|
|
643
703
|
private const int MaxRetries = 6;
|
|
644
704
|
private const int BaseDelayMs = 1000;
|
|
645
705
|
private const int MaxDelayMs = 30000;
|
|
706
|
+
private const int MaxItemRetryAttempts = 10;
|
|
646
707
|
|
|
647
708
|
<% if (isPeopleConnector) { -%>
|
|
648
709
|
/// <summary>
|
|
@@ -92,6 +92,14 @@ int ValidateBatchSize(int? batchSize)
|
|
|
92
92
|
throw new InvalidOperationException("Invalid --batch-size: expected an integer between 1 and 20.");
|
|
93
93
|
return batchSize.Value;
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
int ValidateRetryAttempts(int? retryAttempts)
|
|
97
|
+
{
|
|
98
|
+
retryAttempts ??= 2;
|
|
99
|
+
if (retryAttempts < 0 || retryAttempts > 10)
|
|
100
|
+
throw new InvalidOperationException("Invalid --retry-attempts: expected an integer between 0 and 10.");
|
|
101
|
+
return retryAttempts.Value;
|
|
102
|
+
}
|
|
95
103
|
<% if (inputFormat === "rest") { -%>
|
|
96
104
|
string RestBaseUrl(string? inputPath)
|
|
97
105
|
{
|
|
@@ -182,9 +190,10 @@ async Task ProvisionAsync()
|
|
|
182
190
|
/// <summary>
|
|
183
191
|
/// Ingest items from the configured input.
|
|
184
192
|
/// </summary>
|
|
185
|
-
async Task IngestAsync(string? inputPath, bool dryRun, int? limit, int? batchSize, bool verbose, bool failFast)
|
|
193
|
+
async Task IngestAsync(string? inputPath, bool dryRun, int? limit, int? batchSize, int? retryAttempts, bool verbose, bool failFast)
|
|
186
194
|
{
|
|
187
195
|
var validatedBatchSize = ValidateBatchSize(batchSize);
|
|
196
|
+
var validatedRetryAttempts = ValidateRetryAttempts(retryAttempts);
|
|
188
197
|
|
|
189
198
|
GraphServiceClient? graph = null;
|
|
190
199
|
TokenCredential? credential = null;
|
|
@@ -209,7 +218,7 @@ async Task IngestAsync(string? inputPath, bool dryRun, int? limit, int? batchSiz
|
|
|
209
218
|
<% } -%>
|
|
210
219
|
var core = BuildConnectorCore(graph, credential, connectionId);
|
|
211
220
|
|
|
212
|
-
await core.IngestAsync(source, dryRun, limit, validatedBatchSize, verbose, failFast);
|
|
221
|
+
await core.IngestAsync(source, dryRun, limit, validatedBatchSize, validatedRetryAttempts, verbose, failFast);
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
/// <summary>
|
|
@@ -231,6 +240,7 @@ var dryRunOption = new Option<bool>("--dry-run") { Description = "Build payloads
|
|
|
231
240
|
var failFastOption = new Option<bool>("--fail-fast") { Description = "Abort on the first item failure" };
|
|
232
241
|
var limitOption = new Option<int?>("--limit") { Description = "Limit number of items" };
|
|
233
242
|
var batchSizeOption = new Option<int?>("--batch-size") { Description = "Number of concurrent PUT requests to send per batch (1-20)" };
|
|
243
|
+
var retryAttemptsOption = new Option<int?>("--retry-attempts") { Description = "Retry failed items after initial pass (0-10, default 2)" };
|
|
234
244
|
var verboseOption = new Option<bool>("--verbose") { Description = "Print payloads sent to Graph" };
|
|
235
245
|
|
|
236
246
|
var root = new RootCommand("Connector CLI generated by cocogen");
|
|
@@ -245,6 +255,7 @@ ingestCommand.Options.Add(dryRunOption);
|
|
|
245
255
|
ingestCommand.Options.Add(failFastOption);
|
|
246
256
|
ingestCommand.Options.Add(limitOption);
|
|
247
257
|
ingestCommand.Options.Add(batchSizeOption);
|
|
258
|
+
ingestCommand.Options.Add(retryAttemptsOption);
|
|
248
259
|
ingestCommand.Options.Add(verboseOption);
|
|
249
260
|
ingestCommand.SetAction(async (parseResult, cancellationToken) =>
|
|
250
261
|
{
|
|
@@ -253,8 +264,9 @@ ingestCommand.SetAction(async (parseResult, cancellationToken) =>
|
|
|
253
264
|
var failFast = parseResult.GetValue(failFastOption);
|
|
254
265
|
var limit = parseResult.GetValue(limitOption);
|
|
255
266
|
var batchSize = parseResult.GetValue(batchSizeOption);
|
|
267
|
+
var retryAttempts = parseResult.GetValue(retryAttemptsOption);
|
|
256
268
|
var verbose = parseResult.GetValue(verboseOption);
|
|
257
|
-
await IngestAsync(input, dryRun, limit, batchSize, verbose, failFast);
|
|
269
|
+
await IngestAsync(input, dryRun, limit, batchSize, retryAttempts, verbose, failFast);
|
|
258
270
|
});
|
|
259
271
|
|
|
260
272
|
var deleteCommand = new Command("delete", "Delete the connection");
|
|
@@ -73,6 +73,7 @@ Use `dotnet run -- ingest` with:
|
|
|
73
73
|
- `--fail-fast` (abort on the first item failure)
|
|
74
74
|
- `--limit <n>` (ingest only N items)
|
|
75
75
|
- `--batch-size <n>` (send up to N concurrent PUT requests per batch, default `1`, max `20`)
|
|
76
|
+
- `--retry-attempts <n>` (queue failed items and retry after the initial pass, default `2`, max `10`)
|
|
76
77
|
- `--verbose` (print the exact payload sent to Graph)
|
|
77
78
|
|
|
78
79
|
Note: `--dry-run` does not require Azure AD or connection settings.
|
|
@@ -62,6 +62,7 @@ Use `npm run ingest --` with:
|
|
|
62
62
|
- `--fail-fast` (abort on the first item failure)
|
|
63
63
|
- `--limit <n>` (ingest only N items)
|
|
64
64
|
- `--batch-size <n>` (send up to N concurrent PUT requests per batch, default `1`, max `20`)
|
|
65
|
+
- `--retry-attempts <n>` (queue failed items and retry after the initial pass, default `2`, max `10`)
|
|
65
66
|
- `--verbose` (print the exact payload sent to Graph)
|
|
66
67
|
|
|
67
68
|
Note: `--dry-run` does not require CONNECTION_ID, but you still need it for real ingestion.
|
|
@@ -118,6 +118,14 @@ function parseBatchSize(value: string): number {
|
|
|
118
118
|
return parsed;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function parseRetryAttempts(value: string): number {
|
|
122
|
+
const parsed = Number.parseInt(value, 10);
|
|
123
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 10) {
|
|
124
|
+
throw new InvalidArgumentError("expected an integer between 0 and 10");
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
function buildItemSource(path: string): ItemSource<<%= itemTypeName %>> {
|
|
122
130
|
<% if (inputFormat === "rest") { -%>
|
|
123
131
|
return new RestItemSource(resolveRestOptions(path));
|
|
@@ -224,6 +232,7 @@ async function ingest(options: {
|
|
|
224
232
|
dryRun?: boolean;
|
|
225
233
|
limit?: number;
|
|
226
234
|
batchSize?: number;
|
|
235
|
+
retryAttempts?: number;
|
|
227
236
|
verbose?: boolean;
|
|
228
237
|
failFast?: boolean;
|
|
229
238
|
}): Promise<void> {
|
|
@@ -237,6 +246,7 @@ async function ingest(options: {
|
|
|
237
246
|
dryRun: options.dryRun,
|
|
238
247
|
limit: options.limit,
|
|
239
248
|
batchSize: options.batchSize,
|
|
249
|
+
retryAttempts: options.retryAttempts,
|
|
240
250
|
verbose: options.verbose,
|
|
241
251
|
failFast: options.failFast,
|
|
242
252
|
toExternalItem
|
|
@@ -259,14 +269,16 @@ program
|
|
|
259
269
|
.option("--fail-fast", "Abort on the first item failure")
|
|
260
270
|
.option("--limit <n>", "Limit number of items", (value) => Number(value))
|
|
261
271
|
.option("--batch-size <n>", "Number of concurrent PUT requests to send per batch (1-20)", parseBatchSize, 1)
|
|
272
|
+
.option("--retry-attempts <n>", "Retry failed items after initial pass (0-10, default 2)", parseRetryAttempts, 2)
|
|
262
273
|
.option("--verbose", "Print payloads sent to Graph")
|
|
263
|
-
.action((options: { input?: string; dryRun?: boolean; limit?: number; batchSize?: number; verbose?: boolean; failFast?: boolean }) =>
|
|
274
|
+
.action((options: { input?: string; dryRun?: boolean; limit?: number; batchSize?: number; retryAttempts?: number; verbose?: boolean; failFast?: boolean }) =>
|
|
264
275
|
ingest({
|
|
265
276
|
inputPath: options.input,
|
|
266
277
|
dryRun: options.dryRun,
|
|
267
278
|
failFast: options.failFast,
|
|
268
279
|
limit: options.limit,
|
|
269
280
|
batchSize: options.batchSize,
|
|
281
|
+
retryAttempts: options.retryAttempts,
|
|
270
282
|
verbose: options.verbose,
|
|
271
283
|
})
|
|
272
284
|
);
|
|
@@ -41,6 +41,7 @@ export type IngestOptions<Item> = {
|
|
|
41
41
|
dryRun?: boolean;
|
|
42
42
|
limit?: number;
|
|
43
43
|
batchSize?: number;
|
|
44
|
+
retryAttempts?: number;
|
|
44
45
|
verbose?: boolean;
|
|
45
46
|
failFast?: boolean;
|
|
46
47
|
toExternalItem: (item: Item) => unknown;
|
|
@@ -229,24 +230,34 @@ export class ConnectorCore<Item> {
|
|
|
229
230
|
if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 20) {
|
|
230
231
|
throw new Error("Invalid batch size: expected integer between 1 and 20.");
|
|
231
232
|
}
|
|
233
|
+
const retryAttempts = options.retryAttempts ?? DEFAULT_ITEM_RETRY_ATTEMPTS;
|
|
234
|
+
if (!Number.isInteger(retryAttempts) || retryAttempts < 0 || retryAttempts > MAX_ITEM_RETRY_ATTEMPTS) {
|
|
235
|
+
throw new Error(`Invalid retry attempts: expected integer between 0 and ${MAX_ITEM_RETRY_ATTEMPTS}.`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
type PendingItem = { index: number; id: string; item: Item; attempt: number };
|
|
232
239
|
|
|
233
240
|
let count = 0;
|
|
234
241
|
let successCount = 0;
|
|
235
|
-
const failures: Array<{ index: number; id: string; message: string }> = [];
|
|
236
|
-
const pending:
|
|
242
|
+
const failures: Array<{ index: number; id: string; attempts: number; message: string }> = [];
|
|
243
|
+
const pending: PendingItem[] = [];
|
|
244
|
+
let retryQueue: PendingItem[] = [];
|
|
237
245
|
|
|
238
|
-
const flushBatch = async (): Promise<void> => {
|
|
239
|
-
if (
|
|
246
|
+
const flushBatch = async (batch: PendingItem[]): Promise<void> => {
|
|
247
|
+
if (batch.length === 0) {
|
|
240
248
|
return;
|
|
241
249
|
}
|
|
242
250
|
|
|
243
|
-
for (const entry of
|
|
244
|
-
|
|
251
|
+
for (const entry of batch) {
|
|
252
|
+
if (entry.attempt === 0) {
|
|
253
|
+
console.log(`info: ingesting item ${entry.index} (id=${entry.id})`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
console.log(`info: retrying item ${entry.index} (id=${entry.id}) attempt ${entry.attempt}/${retryAttempts}`);
|
|
245
257
|
}
|
|
246
258
|
|
|
247
|
-
const currentBatch = pending.splice(0, pending.length);
|
|
248
259
|
const results = await Promise.all(
|
|
249
|
-
|
|
260
|
+
batch.map(async (entry) => {
|
|
250
261
|
try {
|
|
251
262
|
await this.putItem(options.connectionId, entry.item, Boolean(options.verbose));
|
|
252
263
|
return { ...entry, success: true as const };
|
|
@@ -266,7 +277,13 @@ export class ConnectorCore<Item> {
|
|
|
266
277
|
|
|
267
278
|
const message = result.error instanceof Error ? result.error.message : String(result.error);
|
|
268
279
|
console.error(`error: failed item ${result.index} (id=${result.id})`);
|
|
269
|
-
|
|
280
|
+
|
|
281
|
+
if (!options.failFast && result.attempt < retryAttempts) {
|
|
282
|
+
retryQueue.push({ index: result.index, id: result.id, item: result.item, attempt: result.attempt + 1 });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
failures.push({ index: result.index, id: result.id, attempts: result.attempt + 1, message });
|
|
270
287
|
if (options.failFast && firstError === null) {
|
|
271
288
|
firstError = result.error;
|
|
272
289
|
}
|
|
@@ -282,9 +299,10 @@ export class ConnectorCore<Item> {
|
|
|
282
299
|
const index = count + 1;
|
|
283
300
|
const itemId = this.getItemId(item as Item);
|
|
284
301
|
if (!options.dryRun) {
|
|
285
|
-
pending.push({ index, id: itemId, item: item as Item });
|
|
302
|
+
pending.push({ index, id: itemId, item: item as Item, attempt: 0 });
|
|
286
303
|
if (pending.length >= batchSize) {
|
|
287
|
-
|
|
304
|
+
const batch = pending.splice(0, pending.length);
|
|
305
|
+
await flushBatch(batch);
|
|
288
306
|
}
|
|
289
307
|
} else if (options.verbose) {
|
|
290
308
|
const payload = options.toExternalItem(item as Item) as any;
|
|
@@ -299,7 +317,36 @@ export class ConnectorCore<Item> {
|
|
|
299
317
|
}
|
|
300
318
|
|
|
301
319
|
if (!options.dryRun) {
|
|
302
|
-
|
|
320
|
+
const batch = pending.splice(0, pending.length);
|
|
321
|
+
await flushBatch(batch);
|
|
322
|
+
|
|
323
|
+
for (let retryAttempt = 1; retryAttempt <= retryAttempts && retryQueue.length > 0; retryAttempt++) {
|
|
324
|
+
const queue = retryQueue;
|
|
325
|
+
retryQueue = [];
|
|
326
|
+
console.warn(`warn: retry pass ${retryAttempt}/${retryAttempts} for ${queue.length} item(s)`);
|
|
327
|
+
|
|
328
|
+
for (let index = 0; index < queue.length; index += batchSize) {
|
|
329
|
+
const batchSlice = queue.slice(index, index + batchSize);
|
|
330
|
+
await flushBatch(batchSlice);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (retryQueue.length > 0 && retryAttempt < retryAttempts) {
|
|
334
|
+
const delayMs = computeRetryDelayMs(retryAttempt - 1);
|
|
335
|
+
console.warn(`warn: waiting ${delayMs}ms before next retry pass`);
|
|
336
|
+
await sleep(delayMs);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (retryQueue.length > 0) {
|
|
341
|
+
for (const entry of retryQueue) {
|
|
342
|
+
failures.push({
|
|
343
|
+
index: entry.index,
|
|
344
|
+
id: entry.id,
|
|
345
|
+
attempts: entry.attempt,
|
|
346
|
+
message: `exhausted retry queue after ${entry.attempt} attempt(s)`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
303
350
|
}
|
|
304
351
|
|
|
305
352
|
if (!options.dryRun) {
|
|
@@ -311,7 +358,7 @@ export class ConnectorCore<Item> {
|
|
|
311
358
|
if (failures.length > 0) {
|
|
312
359
|
console.warn(`warn: ${failures.length} item(s) failed`);
|
|
313
360
|
for (const failure of failures) {
|
|
314
|
-
console.warn(`warn: failed item ${failure.index} (id=${failure.id}) - ${failure.message}`);
|
|
361
|
+
console.warn(`warn: failed item ${failure.index} (id=${failure.id}) after ${failure.attempts} attempt(s) - ${failure.message}`);
|
|
315
362
|
}
|
|
316
363
|
}
|
|
317
364
|
}
|
|
@@ -518,6 +565,8 @@ const SCHEMA_POLL_DELAY_MS = 30000;
|
|
|
518
565
|
const MAX_RETRIES = 6;
|
|
519
566
|
const BASE_RETRY_DELAY_MS = 1000;
|
|
520
567
|
const MAX_RETRY_DELAY_MS = 30000;
|
|
568
|
+
const DEFAULT_ITEM_RETRY_ATTEMPTS = 2;
|
|
569
|
+
const MAX_ITEM_RETRY_ATTEMPTS = 10;
|
|
521
570
|
const SCHEMA_READY_STATES = new Set(["completed", "ready", "succeeded", "success"]);
|
|
522
571
|
const SCHEMA_PENDING_STATES = new Set(["inprogress", "pending", "running", "updating"]);
|
|
523
572
|
|
package/package.json
CHANGED