Skip to content

Conversation

@pawelangelow
Copy link
Contributor

@pawelangelow pawelangelow commented Dec 1, 2025

What

Bulk delete can include a huge number of keys, and our current implementation keeps them in memory. So the robust solution in this case is to stream all the data to a report (successes and failures).

So these changes on a high level look like this:

high-level-overview

  • User initiates bulk delete with "Download report" checkbox enabled => sends generateReport: true
  • Backend creates bulk action and returns downloadUrl in the overview, but waits before processing (via waitForStreamIfNeeded())
  • Frontend immediately triggers HTTP GET to /bulk-actions/:id/report (automatic, no button click)
  • Backend attaches HTTP response stream to BulkAction via setStreamingResponse(res):
    • Writes report header directly to HTTP stream
    • Resolves the promise => unblocks processing
  • Processing runs with direct HTTP streaming:
    • Runner deletes keys via Redis
    • Calls bulkAction.writeToReport() => writes directly to HTTP response
    • Only tracks counts in memory (addFailed(1), addSuccess(1))
  • When complete, finalizeReport() writes summary and calls res.end()
  • Browser receives the complete file and saves it automatically

Update

After the copilot suggestion - if the HTTP request that starts the actual delete process, for some reason, hangs, the user receives an error that the bulk delete didn't work. If they disable the report, the delete process starts immediately.

Screen.Recording.2025-12-01.at.14.21.11.mov

Testing

  1. Set up a database with a large enough set (that depends on the RAM, but let's say > 1M keys)
  2. Bulk delete everything (*)

Expected result: All good
Actual result: it fails at some point, like in the following recording:

Screen.Recording.2025-11-27.at.15.33.02.mov

After

Screen.Recording.2025-12-01.at.13.31.40.mov

The result log format

Bulk Delete Report
Command Executed for each key: UNLINK key_name
A summary is provided at the end of this file.
==================
string:926189__53.... - OK.  <--- when delete was successful
string:941__807a7..... - Error: NOPERM No permissions to access a key <--- when there is an error
...
=============
Summary:
=============
Status: completed
Processed: 1000 keys
Succeeded: 2 keys
Failed: 998 keys

Note

Adds streaming bulk delete reports with a new download endpoint, updates processing to write directly to the stream, and introduces a UI checkbox that auto-starts the download.

  • Backend:
    • Streaming reports: Add GET /bulk-actions/:id/report/download in bulk-actions.controller.ts and streamReport(id, res) in BulkActionsService to set headers and attach the response stream.
    • BulkAction streaming: Implement report flow in models/bulk-action.ts (waitForStreamIfNeeded, setStreamingResponse, writeToReport, finalizeReport, timeout handling) and include downloadUrl and error in getOverview.
    • DTO/Provider: Add generateReport?: boolean to CreateBulkActionDto; pass through in BulkActionsProvider.create.
    • Runners: Update simple runner to stop accumulating errors/keys in memory and call bulkAction.writeToReport(...) while maintaining counters.
    • Interfaces: Extend IBulkActionOverview with downloadUrl and error; extend IBulkAction with writeToReport.
  • Frontend:
    • UI toggle: Add "Download report" checkbox (Redux bulkDelete.generateReport) to control report generation.
    • Auto-download: On create, include generateReport; when downloadUrl arrives, trigger download via triggerDownloadFromUrl using getBaseUrl().
    • Status/UX: Show failure banner with server error; remove legacy summary download button.
    • Utils: Add triggerDownloadFromUrl helper.
  • Tests:
    • Add/expand unit tests for streaming flow (service, BulkAction, runner) and UI checkbox behavior; remove obsolete summary button tests.

Written by Cursor Bugbot for commit 9ce17ac. This will update automatically on new commits. Configure here.

@pawelangelow pawelangelow self-assigned this Dec 1, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 1, 2025

Code Coverage - Integration Tests

Status Category Percentage Covered / Total
🟢 Statements 81.49% 16309/20012
🟡 Branches 64.53% 7362/11407
🟡 Functions 70.38% 2286/3248
🟢 Lines 81.13% 15343/18911

@github-actions
Copy link
Contributor

github-actions bot commented Dec 1, 2025

Code Coverage - Backend unit tests

St.
Category Percentage Covered / Total
🟢 Statements 92.33% 13936/15093
🟡 Branches 74.07% 4201/5672
🟢 Functions 85.87% 2145/2498
🟢 Lines 92.14% 13323/14459

Test suite run success

2988 tests passing in 287 suites.

Report generated by 🧪jest coverage report action from 9ce17ac

valkirilov
valkirilov previously approved these changes Dec 1, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 1, 2025

Code Coverage - Frontend unit tests

St.
Category Percentage Covered / Total
🟢 Statements 82.85% 20952/25289
🟡 Branches 68.06% 8845/12996
🟡 Functions 77.89% 5720/7344
🟢 Lines 83.26% 20518/24644

Test suite run success

5440 tests passing in 701 suites.

Report generated by 🧪jest coverage report action from 9ce17ac

getFilter(): BulkActionFilter;
changeState(): void;
getSocket(): Socket;
writeToReport(keyName: string, success: boolean, error?: string): void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key name shouldn't be a string. We must use Buffers

const errors = [];

res.forEach(([err], i) => {
const keyName = keys[i].toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must use Buffers

Comment on lines +30 to +36
@Get(':id/report/download')
async downloadReport(
@Param() { id }: BulkActionIdDto,
@Res() res: Response,
): Promise<void> {
await this.service.streamReport(id, res);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution does not support multi-tenancy we need to rework it in the future

ArtemHoruzhenko
ArtemHoruzhenko previously approved these changes Dec 2, 2025
Copy link
Contributor

@ArtemHoruzhenko ArtemHoruzhenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can go as is with assumption that we will back to it soon and enhance

}

private getDownloadUrl(): string {
return `databases/${this.databaseId}/bulk-actions/${this.id}/report/download`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: URL path mismatch prevents report downloads

The getDownloadUrl() method generates a URL with a databases/${databaseId}/ prefix, but the BulkActionsController is registered at just /bulk-actions without that prefix. The generated URL databases/{databaseId}/bulk-actions/{id}/report/download won't match the actual controller endpoint /bulk-actions/{id}/report/download, causing 404 errors when the frontend attempts to download reports.

Additional Locations (1)

Fix in Cursor Fix in Web

@pawelangelow pawelangelow merged commit 9ef8847 into main Dec 2, 2025
51 of 52 checks passed
@pawelangelow pawelangelow deleted the feature/RI-7799/http-streaming-download branch December 2, 2025 11:09
valkirilov pushed a commit that referenced this pull request Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants