Skip to content

Commit 180a793

Browse files
committed
chore: surface rollback errors
1 parent cc6cf99 commit 180a793

File tree

2 files changed

+28
-7
lines changed

2 files changed

+28
-7
lines changed

controlplane/src/core/blobstorage/dual.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,21 @@ export class DualBlobStorage implements BlobStorage {
2626
return;
2727
}
2828

29-
// Roll back successful writes before throwing
29+
// Roll back successful writes before throwing, independent of the caller's signal
30+
const rollbacks: Promise<void>[] = [];
3031
if (primaryResult.status === 'fulfilled') {
31-
await this.primary.deleteObject({ key: data.key, abortSignal: data.abortSignal });
32+
rollbacks.push(this.primary.deleteObject({ key: data.key }));
3233
}
3334
if (secondaryResult.status === 'fulfilled') {
34-
await this.secondary.deleteObject({ key: data.key, abortSignal: data.abortSignal });
35+
rollbacks.push(this.secondary.deleteObject({ key: data.key }));
3536
}
37+
const rollbackResults = await Promise.allSettled(rollbacks);
3638

37-
const errors = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map((r) => r.reason);
38-
throw new AggregateError(errors, 'Failed to put object into storage');
39+
const putErrors = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected').map((r) => r.reason);
40+
const rollbackErrors = rollbackResults
41+
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
42+
.map((r) => r.reason);
43+
throw new AggregateError([...putErrors, ...rollbackErrors], 'Failed to put object into storage');
3944
}
4045

4146
async getObject(data: { key: string; abortSignal?: AbortSignal }): Promise<BlobObject> {

controlplane/test/dual-blob-storage.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('DualBlobStorage', () => {
4040
});
4141

4242
expect(primary.deleteObject).not.toHaveBeenCalled();
43-
expect(secondary.deleteObject).toHaveBeenCalledWith({ key: 'k', abortSignal: undefined });
43+
expect(secondary.deleteObject).toHaveBeenCalledWith({ key: 'k' });
4444
});
4545

4646
test('rejects when secondary fails and rolls back primary', async () => {
@@ -56,9 +56,25 @@ describe('DualBlobStorage', () => {
5656
errors: [secondaryError],
5757
});
5858

59-
expect(primary.deleteObject).toHaveBeenCalledWith({ key: 'k', abortSignal: undefined });
59+
expect(primary.deleteObject).toHaveBeenCalledWith({ key: 'k' });
6060
expect(secondary.deleteObject).not.toHaveBeenCalled();
6161
});
62+
test('includes rollback errors in aggregate when rollback also fails', async () => {
63+
const secondaryError = new Error('secondary write failed');
64+
const rollbackError = new Error('primary rollback failed');
65+
const primary = createMockBlobStorage({
66+
deleteObject: vi.fn().mockRejectedValue(rollbackError),
67+
});
68+
const secondary = createMockBlobStorage({
69+
putObject: vi.fn().mockRejectedValue(secondaryError),
70+
});
71+
const dual = new DualBlobStorage(primary, secondary);
72+
73+
await expect(dual.putObject({ key: 'k', body: Buffer.from('d'), contentType: 'text/plain' })).rejects.toMatchObject({
74+
message: 'Failed to put object into storage',
75+
errors: [secondaryError, rollbackError],
76+
});
77+
});
6278
});
6379

6480
describe('getObject', () => {

0 commit comments

Comments
 (0)