Skip to content

fix(model): retry a failed SQL Server probe after a back-off - #4332

Merged
bpamiri merged 1 commit into
developfrom
peter/sqlserver-probe-retry
Oct 5, 2026
Merged

bpamiri merged 1 commit into
developfrom
peter/sqlserver-probe-retry

Conversation

@bpamiri

@bpamiri bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

On SQL Server, Wheels reads two facts once per datasource and keeps them in the application's Wheels settings:

A failed read was kept as well: compatibility level 0, or no bind types. So one transient error, such as a dropped connection or a permission hiccup during the first long query, turned the feature off for that datasource until the next application reload. This came up as an INFO in the review of #4329, for both reads.

Change (MicrosoftSQLServerModel.cfc)

  • A good read is kept, as before. That includes a compatibility level below 130, which is a real answer, not a failure.

  • A failed read isn't kept. Its time is recorded in application.wheels.sqlServerProbeFailures (keyed probe|dataSource), and until $probeRetrySeconds() (60) have passed:

    • $supportsStringSplit() returns false;
    • $stringSplitDateCasts() returns {}.

    Long statements behave exactly as they do with an unsupported database in the meantime. The probe then runs again, and a success clears the recorded failure.

  • Why a back-off. It stops a broken probe from running before every long statement.

  • New helpers: $probeFailedRecently(), $noteProbeResult(), $probeFailures() and $probeRetrySeconds(). Only $supportsStringSplit() and $stringSplitDateCasts() call them.

No changelog fragment: both reads are new in the unreleased 4.2.

The PR for #4326 / #4327 edits other functions of the same adapter. dev1-core confirmed there's no overlap; whichever merges second merges develop in.

Tests

New vendor/wheels/tests/specs/model/sqlServerProbeRetrySpec.cfc. It stubs the two reads with MockBox (prepareMock() on a SQL Server adapter for a datasource no other spec uses), so it runs on every database:

  • Compatibility level. A failed read (0) isn't kept and isn't retried within the back-off: a second call doesn't read again. After the back-off it's read again (150), kept, and the failure is cleared.
  • Date bind types. Same, with {} then datetime2/7 / date/0.
  • A level below 130 (120) is kept after one read, not treated as a failure.

RED (this spec on develop, Lucee 7 + SQLite): both retry specs fail with "a failed read was kept". The below-130 spec passes on develop too, as it should.

GREEN, model directory (pass / fail / error):

Engine SQLite SQL Server
Lucee 7 1384 / 0 / 0 1431 / 0 / 0 (STRING_SPLIT specs 21/21)
Lucee 6 1387 / 0 / 0
Adobe ColdFusion 2023 1384 / 0 / 0
Adobe ColdFusion 2025 1384 / 0 / 0
BoxLang 1387 / 0 / 0
RustCFML v0.693.0 1380 / 0 / 0

sqlServerProbeRetrySpec passes 3/3 on every engine.

  • Full core suite, Lucee 7 + SQLite: 6791 passed.
  • internal specs: 113 passed.
  • Complexity gate: passes.

🤖 Generated with Claude Code

The SQL Server adapter reads two facts once per datasource and keeps them
in application.wheels: the database's compatibility level (STRING_SPLIT,
#4321) and the types the driver binds dates as (#4329). A failed read was
kept too, so one transient error disabled STRING_SPLIT, or date-list
splitting, until the next reload.

A failed read is no longer kept. Its time is recorded, the answer is "not
supported" until $probeRetrySeconds() (60) have passed, and then the probe
runs again; a good read is kept as before. The back-off stops a broken
probe from running before every long statement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri bpamiri added this to the v4.2.0 milestone Oct 5, 2026
@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Review @ a057e68: source looks good. Final verdict after matrix 37290287029 and PR CI

The back-off is per datasource and per probe

  • Failures are recorded in application[appKey].sqlServerProbeFailures, keyed "<probe>|<effective datasource>". The compatibility-level read and the date-bind-type read each have their own key, and tenant datasources are separate.
  • A reload replaces the struct, so a reload starts afresh.
  • A good read deletes the failure entry and is cached as before.
  • A level below 130 (for example 120) is a good read and stays cached. Only 0 (an error, or no sys.databases row) counts as a failure. The spec pins that distinction.
  • A non-empty bind-type read that $dateCastsFor() doesn't accept (for example datetime/3) is likewise cached as a good read and yields no casts. That's deterministic, not a failure, so there is no retry loop.

Concurrency is harmless

  • Two requests that both find no cached value, or both find an expired failure, each run the probe and each write the same outcome: the same level or bind types, or GetTickCount() for a failure.
  • Struct writes of the same key are last-writer-wins, and every interleaving leaves a consistent state: a cached value, or a fresh failure time. Nothing reads a half-built value.

Cost of a permanently failing probe

  • At most one probe per 60 s per request that arrives after the back-off expires and before the first of them records the new failure time.
  • Under steady traffic, that is one probe per 60 s plus a small burst bounded by how many requests overlap the probe's own latency. It never runs before every long statement, as the old uncached path would have.
  • Until the retry, the answer is the safe one: false / no casts, so Wheels.TooManyParameters, as before.

Spec (sqlServerProbeRetrySpec, MockBox-stubbed, so it runs on every database)

  • $results(0, 150): two calls inside the back-off probe once and cache nothing. After ageFailure(...) the next call reads 150, caches it, and clears the failure entry. A further call doesn't probe again.
  • The same sequence runs for the date bind types.
  • 120 is a good read.

Findings

On the matrix I'll read sqlServerProbeRetrySpec on every leg, plus both STRING_SPLIT specs on the five SQL Server legs, then the PR CI tally. CI was 7 pass and 10 pending when I looked.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Wheels Test Results

221 959 tests   217 049 ✅  1h 23m 44s ⏱️
 17 184 suites    4 910 💤
     32 files          0 ❌

Results for commit a057e68.

@bpamiri

bpamiri commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator Author

Final: APPROVE @ a057e68 (matrix 37290287029, all 32 legs read; PR CI read)

Matrix

  • 32/32 legs have 0 fail, 0 error.
  • sqlServerProbeRetrySpec passes 3/3 with 0 skipped on every leg. It's MockBox-stubbed, so the back-off logic is exercised on all five engines whatever the database.
  • All five SQL Server legs: sqlServerStringSplitSpec 12/12 and sqlServerDateStringSplitSpec 9/9, unchanged with the retry path in place.
  • Every other leg: the SQL Server-only cases skip as designed (5/12 and 4/9).
  • Skips. The skipped specs on adobe2023-sqlserver and lucee7-mysql are identical to a develop-based run (fix(model): take MySQL and PostgreSQL advisory locks with the shared connection options #4331's final matrix 37284943699).

PR CI: 18 pass, 3 skipping (Deploy CLI tests, Reviewer, Update docs), 0 fail, 0 pending.

No open findings. The INFO about a burst of probes at expiry stands as a note only.

@bpamiri
bpamiri merged commit b4b5ed6 into develop Oct 5, 2026
59 checks passed
@bpamiri
bpamiri deleted the peter/sqlserver-probe-retry branch October 5, 2026 09:51
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.

1 participant