Skip to content

Commit e3b0183

Browse files
committed
fix(sdk-api): replace naive keychain batching with FFD bin packing
- Replace count-based equal-split batching with FFD bin packing - Bound each batch by maxBatchSizeKB from server response - Add packKeychainsFFD private method with per-keychain size guard Ticket: WP-8343
1 parent ade84e8 commit e3b0183

1 file changed

Lines changed: 64 additions & 14 deletions

File tree

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,7 +2028,7 @@ export class BitGoAPI implements BitGoBase {
20282028
await this.processKeychainPasswordUpdatesInBatches(
20292029
updatePasswordParams.keychains,
20302030
updatePasswordParams.v2_keychains,
2031-
batchingFlowCheck.noOfBatches,
2031+
batchingFlowCheck.maxBatchSizeKB,
20322032
3
20332033
);
20342034
// Call changepassword API without keychains for batching flow
@@ -2287,30 +2287,80 @@ export class BitGoAPI implements BitGoBase {
22872287
}
22882288

22892289
/**
2290-
* Process keychain password updates in batches with retry logic
2290+
* Pack keychains into batches using First Fit Decreasing (FFD) algorithm.
2291+
*
2292+
* @param keychains - V1 keychains
2293+
* @param v2Keychains - V2 keychains
2294+
* @param maxBatchSizeBytes - Maximum byte size per batch
2295+
* @private
2296+
*/
2297+
private packKeychainsFFD(
2298+
keychains: Record<string, string>,
2299+
v2Keychains: Record<string, string>,
2300+
maxBatchSizeBytes: number
2301+
): Array<{ v1Batch: Record<string, string>; v2Batch: Record<string, string>; sizeBytes: number }> {
2302+
const entrySize = (id: string, value: string) => Buffer.byteLength(id, 'utf8') + Buffer.byteLength(value, 'utf8');
2303+
2304+
const items = [
2305+
...Object.entries(keychains).map(([id, value]) => ({ id, value, sizeBytes: entrySize(id, value), isV2: false })),
2306+
...Object.entries(v2Keychains).map(([id, value]) => ({ id, value, sizeBytes: entrySize(id, value), isV2: true })),
2307+
].sort((a, b) => b.sizeBytes - a.sizeBytes);
2308+
2309+
const bins: Array<{ v1Batch: Record<string, string>; v2Batch: Record<string, string>; sizeBytes: number }> = [];
2310+
2311+
for (const item of items) {
2312+
if (item.sizeBytes > maxBatchSizeBytes) {
2313+
throw new Error(`Keychain with id ${item.id} exceeds the maximum batch size and cannot be processed`);
2314+
}
2315+
2316+
const target = bins.find((bin) => bin.sizeBytes + item.sizeBytes <= maxBatchSizeBytes);
2317+
if (target) {
2318+
if (item.isV2) {
2319+
target.v2Batch[item.id] = item.value;
2320+
} else {
2321+
target.v1Batch[item.id] = item.value;
2322+
}
2323+
target.sizeBytes += item.sizeBytes;
2324+
} else {
2325+
const newBin = {
2326+
v1Batch: {} as Record<string, string>,
2327+
v2Batch: {} as Record<string, string>,
2328+
sizeBytes: item.sizeBytes,
2329+
};
2330+
if (item.isV2) {
2331+
newBin.v2Batch[item.id] = item.value;
2332+
} else {
2333+
newBin.v1Batch[item.id] = item.value;
2334+
}
2335+
bins.push(newBin);
2336+
}
2337+
}
2338+
2339+
return bins;
2340+
}
2341+
2342+
/**
2343+
* Process keychain password updates in batches with retry logic.
2344+
* Uses First Fit Decreasing (FFD) bin packing to ensure no batch exceeds
2345+
* maxBatchSizeKB
2346+
*
22912347
* @param keychains - The v1 keychains to update
22922348
* @param v2Keychains - The v2 keychains to update
2293-
* @param noOfBatches - Number of batches to split the keychains into
2349+
* @param maxBatchSizeKB - Maximum payload size per batch in kilobytes
22942350
* @param maxRetries - Maximum number of retries per batch
22952351
* @private
22962352
*/
22972353
private async processKeychainPasswordUpdatesInBatches(
22982354
keychains: Record<string, string>,
22992355
v2Keychains: Record<string, string>,
2300-
noOfBatches: number,
2356+
maxBatchSizeKB: number,
23012357
maxRetries: number
23022358
): Promise<void> {
2303-
// Split keychains into batches
2304-
const v1KeychainEntries = Object.entries(keychains);
2305-
const v2KeychainEntries = Object.entries(v2Keychains);
2306-
2307-
const v1BatchSize = Math.ceil(v1KeychainEntries.length / noOfBatches);
2308-
const v2BatchSize = Math.ceil(v2KeychainEntries.length / noOfBatches);
2359+
const maxBatchSizeBytes = maxBatchSizeKB * 1024;
2360+
const bins = this.packKeychainsFFD(keychains, v2Keychains, maxBatchSizeBytes);
23092361

2310-
// Call batching API for each batch with retry logic
2311-
for (let i = 0; i < noOfBatches; i++) {
2312-
const v1Batch = Object.fromEntries(v1KeychainEntries.slice(i * v1BatchSize, (i + 1) * v1BatchSize));
2313-
const v2Batch = Object.fromEntries(v2KeychainEntries.slice(i * v2BatchSize, (i + 1) * v2BatchSize));
2362+
for (let i = 0; i < bins.length; i++) {
2363+
const { v1Batch, v2Batch } = bins[i];
23142364

23152365
let retryCount = 0;
23162366
let success = false;

0 commit comments

Comments
 (0)