Merge pull request #710 from buster-so/staging

hotfix on multiple trigger tasks for download
This commit is contained in:
dal 2025-08-14 12:39:05 -06:00 committed by GitHub
commit e2a6454b33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 38 additions and 19 deletions

View File

@ -121,12 +121,19 @@ describe('downloadMetricFileHandler', () => {
organizationId: mockOrganizationId, organizationId: mockOrganizationId,
}); });
// Verify task was triggered with correct parameters // Verify task was triggered with correct parameters and idempotency
expect(tasks.trigger).toHaveBeenCalledWith('export-metric-data', { expect(tasks.trigger).toHaveBeenCalledWith(
metricId: mockMetricId, 'export-metric-data',
userId: mockUser.id, {
organizationId: mockOrganizationId, metricId: mockMetricId,
}); userId: mockUser.id,
organizationId: mockOrganizationId,
},
{
idempotencyKey: `export-${mockUser.id}-${mockMetricId}`,
idempotencyKeyTTL: '5m',
}
);
// Verify successful response // Verify successful response
expect(result).toMatchObject({ expect(result).toMatchObject({

View File

@ -50,12 +50,21 @@ export async function downloadMetricFileHandler(
} }
try { try {
// Trigger the export task // Trigger the export task with idempotency to prevent duplicates
const handle = await tasks.trigger('export-metric-data', { // If the same user tries to download the same metric within 5 minutes,
metricId, // it will return the existing task instead of creating a new one
userId: user.id, const handle = await tasks.trigger(
organizationId, 'export-metric-data',
}); {
metricId,
userId: user.id,
organizationId,
},
{
idempotencyKey: `export-${user.id}-${metricId}`,
idempotencyKeyTTL: '5m', // 5 minutes TTL
}
);
// Poll for task completion with timeout // Poll for task completion with timeout
const startTime = Date.now(); const startTime = Date.now();

View File

@ -22,9 +22,9 @@ export const MetricDataTruncatedWarning: React.FC<MetricDataTruncatedWarningProp
setIsDownloading(true); setIsDownloading(true);
setHasError(false); setHasError(false);
// Create a timeout promise that rejects after 3 minutes // Create a timeout promise that rejects after 2 minutes (matching backend timeout)
const timeoutPromise = new Promise((_, reject) => { const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Download timeout')), 3 * 60 * 1000); // 3 minutes setTimeout(() => reject(new Error('Download timeout')), 2 * 60 * 1000); // 2 minutes
}); });
// Race between the API call and the timeout // Race between the API call and the timeout
@ -36,14 +36,17 @@ export const MetricDataTruncatedWarning: React.FC<MetricDataTruncatedWarningProp
// Simply navigate to the download URL // Simply navigate to the download URL
// The response-content-disposition header will force a download // The response-content-disposition header will force a download
window.location.href = response.downloadUrl; window.location.href = response.downloadUrl;
// Keep button disabled for longer since download is async
// User can click again after 5 seconds if needed
setTimeout(() => {
setIsDownloading(false);
}, 5000);
} catch (error) { } catch (error) {
console.error('Failed to download metric file:', error); console.error('Failed to download metric file:', error);
setHasError(true); setHasError(true);
} finally { // Re-enable button immediately on error so user can retry
// Add a small delay before removing loading state since download happens async setIsDownloading(false);
setTimeout(() => {
setIsDownloading(false);
}, 1000);
} }
}; };