Skip to content

fix(ci): retry transient platform errors, run firms in parallel, warn on firm drift#28

Merged
Benjvandam merged 2 commits into
mainfrom
parallel_tests_stability
Jul 6, 2026
Merged

fix(ci): retry transient platform errors, run firms in parallel, warn on firm drift#28
Benjvandam merged 2 commits into
mainfrom
parallel_tests_stability

Conversation

@Benjvandam

Copy link
Copy Markdown
Contributor

Why

The first real PRs after #27 merged showed three problems (analysed from runs 28656835005, 28647828601, 28658481661 on be_market):

  1. Transient platform errors counted as failures, with no retry. vol_2_1 "failed" with Internal error. Try to run the test again or contact support if the issue persists. - the platform itself says to retry, and nothing did.
  2. Firm assignment silently drifts. With no test_firm_id and no SF_TEST_FIRM_ID, a template runs on the first firm id in its config.json. Regenerating config.json reorders those keys: one PR's templates all silently moved to firm 400583 (no matching test data there) and failed en masse - templates that pass on firms 6056/542. This logic predates feat(ci): run liquid tests in parallel, grouped per firm and template type #27, but parallel runs made the blast radius visible.
  3. Firms waited on each other, and long batches gave zero feedback. Buckets ran one after another, so a slow firm delayed everyone. One platform test can take ~10 minutes (run 28656835005: a single test, 9m51s server-side) while the job printed nothing.

Worth noting from the same analysis: the parallelism itself works (a 10-template batch finished in 16s), and two of the "failures" investigated were genuine test regressions that CI should have caught. The real defects were the missing retry, the firm drift, and the serialization.

What this does

  • Retries transient failures. Each template gets up to MAX_TEST_ATTEMPTS (default 3) when its failure reason is transient: internal error, no response, run never completed, timeout - the CLI prints the reason as an indented line since silverfin-cli#262, and that is what the classifier reads. Genuine test failures (named failing unit tests) are never retried - rerunning cannot fix an assertion.
  • Retries lost verdicts. If the CLI crashes mid-batch, the templates without a verdict are retried instead of failing the whole batch; only after retries are exhausted do they fail (with the exit code in the reason).
  • Runs firms in parallel. Each <firm>|<type> bucket is a background job, capped at MAX_PARALLEL_FIRMS (default 3). Global in-flight ceiling = MAX_PARALLEL_TESTS x MAX_PARALLEL_FIRMS. A slow firm now only delays its own bucket.
  • Shows progress. A start line per bucket, a heartbeat every 60s while a bucket runs, the full CLI output folded in one group per bucket, and the final summary now includes the attempt count (PASS (attempt 2)) so retried flakes are visible.
  • Warns on firm drift. One ::warning:: when templates fall back to "first firm id in config.json", with the fix (pin test_firm_id or set SF_TEST_FIRM_ID). The real fix for drift is pinning - the workflow can only make it visible.
  • timeout-minutes: 120 on the job, since retries can extend it (a single platform test can poll up to ~33 min).

What this deliberately does not do

  • It cannot make a single platform test faster - the 10-minute test runs are server-side. Cross-firm parallelism and retries fix wall-time waste and false reds, not platform speed.
  • Polling backoff (interval grows 5%/poll, ~30-60s between polls after 10-20 min) lives in silverfin-cli - follow-up there: cap the poll interval.

Validation

Offline, with a stateful mock CLI (every scenario asserted on call counts):

scenario result
genuine assertion failure 1 call, FAIL, never retried
internal error once, then ok 2 calls, PASS (attempt 2)
persistent internal error 3 calls, FAIL - transient platform error persisted
CLI crashes, drops one verdict victim retried alone, PASS (attempt 2); batch-mates unaffected
names with & ( ) and spaces parsed exactly, no regex on names
all pass / any fail exit 0 / exit 1
two 4s-buckets, cap 3 vs cap 1 10s vs 20s wall - firms genuinely parallel

Live: validated on be_market (test branch pointed at this ref) - run linked in comments once green.

…n on firm drift

Production revealed three problems with the parallel liquid-test run:

1. Transient platform errors counted as test failures. The platform
   sometimes returns internal_error ('Internal error. Try to run the test
   again...') or no response at all; the workflow reported those as FAILED
   with no second chance. Each template now gets up to MAX_TEST_ATTEMPTS
   (default 3) when its failure reason is transient (internal error, no
   response, run never completed, timeout). Genuine test failures - the CLI
   names the failing unit tests - are never retried.

2. A CLI crash failed every template in the batch. A template whose
   verdict is missing (the CLI died or dropped it) is now retried instead
   of immediately failing, and only reported as failed when retries are
   exhausted. A crashed bucket runner is also detected per template.

3. Firms waited on each other. Each <firm>|<type> bucket now runs as its
   own background job (capped at MAX_PARALLEL_FIRMS, default 3), so a slow
   firm only delays its own bucket. Bucket output is flushed as one log
   group per bucket when it finishes, with a heartbeat line while waiting.

Also: warn once when templates fall back to 'first firm id in config.json'
- that choice silently changes when config.json is regenerated (this moved
a whole PR's tests to a firm without test data and failed them all), and
add timeout-minutes to the job now that retries can extend it.
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44b91c13-afca-49eb-bd6d-a60769c01210

📥 Commits

Reviewing files that changed from the base of the PR and between 25eb887 and 7623925.

📒 Files selected for processing (1)
  • .github/workflows/run_tests.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/run_tests.yml

Walkthrough

The run_tests.yml workflow adds job timeout and new tuning variables, switches template execution to bucketized parallel runs with retries, tracks fallback firm-id selection, and updates summary handling for missing or transient test verdicts.

Changes

Test Runner Concurrency and Retry Logic

Layer / File(s) Summary
Job timeout and tuning env vars
.github/workflows/run_tests.yml
Adds job-level timeout and new env vars MAX_PARALLEL_FIRMS and MAX_TEST_ATTEMPTS alongside MAX_PARALLEL_TESTS, and initializes derived run parameters plus bucket and fallback state.
Fallback firm-id selection and warning
.github/workflows/run_tests.yml
Updates default firm-id selection to increment FALLBACK_COUNT when no matching test_firm_id or SF_TEST_FIRM_ID is found, and emits a CI warning when fallback selection was used.
Retry-aware bucket runner
.github/workflows/run_tests.yml
Replaces single-pass bucket execution with run_bucket(), which writes normalized CLI output to temp files, parses verdicts and transient signals with awk, retries transient failures up to MAX_TEST_ATTEMPTS, and records templates that never produce a verdict as failures.
Concurrent bucket orchestration and results
.github/workflows/run_tests.yml
Launches firm

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • silverfin/bso_github_actions#27: Refactors the same workflow area to parallelize template test execution and normalize CLI output parsing around firm/type bucketing.

Suggested labels: ci, workflow

Suggested reviewers: None identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits required template sections: Description, Fixes #, Type of change, and Checklist. Add the missing template sections with a brief summary, issue link, change type, and checklist items, even if some are not applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main CI workflow changes: retrying transient errors, parallel firm runs, and drift warnings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch parallel_tests_stability

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Comment @coderabbitai help to get the list of available commands.

@Benjvandam

Copy link
Copy Markdown
Contributor Author

Live validation on be_market: run 28661065102 (test branch pointed at this ref) — 27 templates, 2 firms, 3 buckets, 5m20s total.

Everything this PR adds fired for real in that run:

  • The retry saved a false red. 2018_276_K_capitalgains hit Internal error. Try to run the test again or contact support if the issue persists. on attempt 1, was classified transient, retried, and finished PASS (attempt 2). On main's workflow this exact run would have failed the PR for nothing.
  • Genuine failures were not retried. Leningen & Leasings (42) and Rekening Courant failed with named unit tests (expectation mismatches on firm 400583) — classified genuine, failed once, no retry time wasted. That is why the run is red, and correctly so: it's a data/expectation issue on that firm, not workflow flakiness.
  • Firms ran in parallel: three buckets started concurrently (▶ firm 400583 · accountTemplate, ▶ firm 1355 · reconciliationText, ▶ firm 400583 · reconciliationText).
  • Heartbeats every 60s while the slow bucket ran, and the summary shows the attempt count (PASS (attempt 2)).
  • The firm-drift warning fired (1 template still falls back to first-firm-id).

Final tally: 2 failed, 25 passed — with 1 of the 25 rescued by the retry.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/workflows/run_tests.yml (1)

379-384: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor duplication in SUMMARY append.

Both branches push the identical tuple to SUMMARY; only the ERRORS push differs. This can be collapsed to reduce drift risk.

♻️ Optional simplification
-              echo "${NAME}: ${STATUS}${NOTE}"
-              if [[ "${STATUS}" == "PASS" ]]; then
-                SUMMARY+=("${STATUS}${NOTE}"$'\t'"${FIRM_ID}"$'\t'"${NAME}")
-              else
-                ERRORS+=("${NAME}")
-                SUMMARY+=("${STATUS}${NOTE}"$'\t'"${FIRM_ID}"$'\t'"${NAME}")
-              fi
+              echo "${NAME}: ${STATUS}${NOTE}"
+              SUMMARY+=("${STATUS}${NOTE}"$'\t'"${FIRM_ID}"$'\t'"${NAME}")
+              [[ "${STATUS}" != "PASS" ]] && ERRORS+=("${NAME}")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/run_tests.yml around lines 379 - 384, The STATUS branch in
the test summary block duplicates the same SUMMARY append in both outcomes, so
simplify the logic in the workflow step around the STATUS/PASS check by moving
the SUMMARY+= call outside the if/else and keeping only the ERRORS+=("${NAME}")
addition in the non-PASS branch. Use the existing STATUS, NOTE, SUMMARY, and
ERRORS handling in this block to preserve behavior while removing the repeated
tuple append.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/run_tests.yml:
- Around line 379-384: The STATUS branch in the test summary block duplicates
the same SUMMARY append in both outcomes, so simplify the logic in the workflow
step around the STATUS/PASS check by moving the SUMMARY+= call outside the
if/else and keeping only the ERRORS+=("${NAME}") addition in the non-PASS
branch. Use the existing STATUS, NOTE, SUMMARY, and ERRORS handling in this
block to preserve behavior while removing the repeated tuple append.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d77143d2-83fb-443c-9d31-5074f2ea4057

📥 Commits

Reviewing files that changed from the base of the PR and between 4764ae6 and 25eb887.

📒 Files selected for processing (1)
  • .github/workflows/run_tests.yml

@Benjvandam

Copy link
Copy Markdown
Contributor Author

CLI-side follow-up is up as well: silverfin/silverfin-cli#263 caps the poll interval at 5s (was growing to 30-100s on long runs). Together: this PR removes false reds and firm serialization; that one removes the detection lag on long tests.

Comment thread .github/workflows/run_tests.yml Outdated
KEY="${KEYS[${NEXT_LAUNCH}]}"
FIRM="${KEY%%|*}"
TYPE="${KEY##*|}"
printf '%s' "${TEMPLATE_BUCKETS[${KEY}]}" | sort -u | grep -v '^$' > "${WORKDIR}/list.${NEXT_LAUNCH}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Minor — step aborts if a bucket's identifiers are all empty. Under the Actions default bash -eo pipefail, this bare pipeline aborts the entire step (killing every bucket, not just this one) whenever grep -v '^$' selects nothing — i.e. a bucket whose only identifier is an empty string. That is reachable: a reconciliation config.json with "handle": "" yields IDENTIFIER="", because .handle // .name only falls through on null/false, not on an empty string.

The pre-refactor code ran the identical pipeline inside a <( … ) process substitution feeding mapfile, where the non-zero exit was ignored; moving it to a plain redirect dropped that implicit guard. Fails red (not silently green), but one malformed config would fail CI for all templates in the run.

Suggested change
printf '%s' "${TEMPLATE_BUCKETS[${KEY}]}" | sort -u | grep -v '^$' > "${WORKDIR}/list.${NEXT_LAUNCH}"
printf '%s' "${TEMPLATE_BUCKETS[${KEY}]}" | sort -u | grep -v '^$' > "${WORKDIR}/list.${NEXT_LAUNCH}" || true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should assume that the config file is always correctly formatted and contains at least the handle or account name.

…ntifiers

Address review on PR #28:

- launch_ready_buckets: the `sort -u | grep -v '^$'` pipeline exits 1 when a
  bucket's identifiers are all empty (e.g. a config with "handle": "", since
  `.handle // .name` only falls through on null/false). Under the Actions
  default `bash -eo pipefail` that aborted the whole step, killing every
  bucket. Add `|| true`; an empty list is already handled downstream.
- Collapse the duplicated SUMMARY append into a single push with a guarded
  ERRORS append (CodeRabbit nitpick), matching the existing `&&` idiom.
@Benjvandam Benjvandam merged commit 1b05cbf into main Jul 6, 2026
7 checks passed
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.

2 participants