Skip to content

Retry and rate-pace TestOps uploads - #818

Closed
todti wants to merge 4 commits into
mainfrom
testops-retry-error-pacer
Closed

Retry and rate-pace TestOps uploads#818
todti wants to merge 4 commits into
mainfrom
testops-retry-error-pacer

Conversation

@todti

@todti todti commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • TestOps upload error classificationerrors.ts classifies upload failures into transient (5xx/429/timeout), recoverable ("launch is closed"), or terminal (auth/validation/payload/conflict) kinds, recognizing both raw HTTP errors and the KnownError/UnknownError wrappers that @allurereport/service's HTTP client actually throws. withUploadRetry() retries only the retryable kinds with exponential backoff, and is wired into all four TestOps upload call sites (test results, global attachments, global errors, quality gate).
  • Closed-launch reopening — a new reopenClosedLaunch option reopens a launch TestOps reports as closed instead of failing the upload outright.
  • Upload rate pacinguploadPacer.ts adds a leaky-bucket pacer with independent per-window budgets for requests, files, and bytes sent to TestOps, applied at every upload call site (global attachments, per-result attachments/fixtures, test results, quality gate). Enabled by default with sane defaults (20 req/s, 1000 files/s, 1 GiB/s). Can be disabled via uploadRateLimit: false, or tuned via an explicit uploadRateLimit config.
  • Also fixes an open.ts command that only handled SIGINT for temp-report cleanup — it now handles SIGTERM too, via a small shared utils/signals.ts helper (notifySignals/waitForAbort) with conventional exit codes (130/143) and a graceful-shutdown deadline.

Test plan

  • yarn vitest run in packages/plugin-testops — all tests pass
  • tsc --noEmit — clean
  • oxlint — no new warnings introduced

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Allure Report Summary

Name Duration Stats New Flaky Retry Report
Allure 3 Report 39m 54s Passed tests 9903   Failed tests 2   Skipped tests 15   Unknown tests 24 150 0 33 View
My Dashboard 39m 54s Passed tests 9903   Failed tests 2   Skipped tests 15   Unknown tests 24 150 0 33 View

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

Allure perf metrics

Generated at: 2026-08-06T14:14:44.769Z

Phase Count Total Avg Min Max
restoreState.total 1 2396.4 ms 2396.4 ms 2396.4 ms 2396.4 ms
restoreState.dump 3 2395.9 ms 798.6 ms 715.6 ms 847.4 ms
restoreState.attachments 3 1789.2 ms 596.4 ms 543.3 ms 637.6 ms
restoreState.storeRestore 3 183.0 ms 61.0 ms 54.0 ms 64.9 ms
generate.total 1 7836.9 ms 7836.9 ms 7836.9 ms 7836.9 ms
generate.plugins.done 1 7306.7 ms 7306.7 ms 7306.7 ms 7306.7 ms
publish.upload.total 1 152975.3 ms 152975.3 ms 152975.3 ms 152975.3 ms
summary.generate 2 23.4 ms 11.7 ms 6.5 ms 16.9 ms
generate.plugin.done.agent 1 0.1 ms 0.1 ms 0.1 ms 0.1 ms
generate.plugin.done.awesome 1 6202.8 ms 6202.8 ms 6202.8 ms 6202.8 ms
generate.plugin.done.dashboard 1 1098.3 ms 1098.3 ms 1098.3 ms 1098.3 ms
generate.plugin.done.log 1 4.6 ms 4.6 ms 4.6 ms 4.6 ms
generate.plugin.done.testops 1 0.3 ms 0.3 ms 0.3 ms 0.3 ms
publish.upload.plugin.awesome 1 125933.9 ms 125933.9 ms 125933.9 ms 125933.9 ms
publish.upload.plugin.dashboard 1 17664.3 ms 17664.3 ms 17664.3 ms 17664.3 ms

Artifacts: allure-perf-metrics

todti added 2 commits August 6, 2026 14:28
Classify TestOps upload failures into transient (5xx/429/timeout),
recoverable ("launch is closed"), or terminal (auth/validation/payload/
conflict) kinds, and retry only the retryable ones with exponential
backoff. Wired into all three upload call sites (test results, global
attachments, global errors) — previously a failed request just logged
and gave up with no retry at all.

Add a leaky-bucket pacer with independent per-window budgets for
requests, files, and bytes sent to TestOps, enabled by default with
sane defaults (20 req/s, 1000 files/s, 1 GiB/s) since the server-side
limits it approximates are real regardless of whether anyone configures
for them. Disable via a new uploadRateLimit: false plugin option, or
tune it via an explicit uploadRateLimit config.

Also makes the `open` command's temp-report cleanup handle SIGTERM, not
just SIGINT, via a small shared signals helper (notifySignals/
waitForAbort) with conventional exit codes and a graceful-shutdown
deadline.
- classifyError only recognized raw AxiosError, but TestOpsClient never
  sees one: @allurereport/service wraps every HTTP failure into a
  KnownError/UnknownError first, so retry classification silently fell
  through to Unknown and withUploadRetry never retried anything for real.
- uploadTestResults swallowed its own errors internally, making the
  retry wrapper around it a no-op; it now lets failures propagate, with
  logging moved to the plugin-level catch (matching the other upload
  call sites).
- Wire the pacer into every upload call site (global attachments,
  per-result attachments/fixtures, quality gate), including byte cost
  for attachment content, not just requests/files.
- Add reopenClosedLaunch option: reopens a launch TestOps reports as
  closed instead of failing the upload outright.
- Wrap quality gate upload in withUploadRetry, the one upload path that
  was missing it.
@todti
todti force-pushed the testops-retry-error-pacer branch from 5b47e9e to 3a989cd Compare August 6, 2026 13:30
Comment thread packages/plugin-testops/src/plugin.ts Outdated
onProgress: () => incrementProgress(),
}),
{
onRetry: async (error, attempt) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

try + retry here are wrapping full upload however if 2-3d chunk will fail for example here - retry will duplicate them, is it okay? I mean we should be sure that testops know how to dedup results in this case

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — retry is now scoped per chunk instead of the whole upload call. Each chunk's POST to /api/upload/test-result retries independently, so a later chunk failing no longer causes earlier, already-acknowledged chunks to be resent.

@todti todti Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Checked this — the server does not dedup by our uuid on re-upload. Test result creation resolves conflicts by internal TEST_RESULT.ID, not by the uuid we send; a repeated chunk gets a brand-new row unless it happens to match a preallocated result via job-run reconciliation (unrelated mechanism). So yes, if a chunk's request actually lands server-side but we never see the response and retry it, that would create a duplicate today. Worth tracking as a follow-up — likely either an idempotency key on the upload endpoint, or having the client only retry on errors that are safe to assume as "not applied" (e.g. connection refused before the request went out) rather than any transient failure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

absolutely!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed — retry is now scoped per chunk instead of the whole upload call. Each chunk's POST to /api/upload/test-result retries independently, so a later chunk failing no longer causes earlier, already-acknowledged chunks to be resent.

Do we actually need outer retry now, when we cover everything inside operations? As I see we still has another case here:

  1. chunk 1 POST -> success
  2. chunk 2 POST -> 503
  3. inner retry chunk 2 -> 503
  4. inner retry chunk 2 -> 503
  5. inner retry chunk 2 -> 503
  6. client.uploadTestResults throws outside
  7. outer retry в plugin.ts run full client.uploadTestResults again
  8. chunk 1 POST -> success second time

todti added 2 commits August 6, 2026 22:48
… upload

A transient failure on chunk N used to make the outer retry replay
uploadTestResults() from scratch, resending every chunk that had already
been acknowledged by TestOps. Retrying is now scoped to the failing chunk's
own post call, so already-succeeded chunks are never resent.
@todti

todti commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #906 — this branch's content is fully merged there (byte-identical for #818/#888; #889's fixes cherry-picked in 52798bf).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants