Skip to content

Name the store when it is the thing that failed - #198

Merged
koriym merged 6 commits into
bearsunday:1.xfrom
koriym:pool-error-visibility
Aug 19, 2026
Merged

Name the store when it is the thing that failed#198
koriym merged 6 commits into
bearsunday:1.xfrom
koriym:pool-error-visibility

Conversation

@koriym

@koriym koriym commented Aug 17, 2026

Copy link
Copy Markdown
Member

symfony/cache adapters never throw at the application: an unreachable store answers a read as a miss and a write as false. So ResourceStorage::guard() catches nothing, no cache_error is recorded, and a Redis restart reads as a run of ordinary cold reads.

Measured against a real adapter pointed at a closed port, before this change:

cold read    get cache_miss put_donut cdn_headers pre_write_cleanup invalidate save_etag save_donut_view save_donut cache_miss
             save_etag saved=false  save_donut_view saved=false  save_donut saved=false
failing sides []

The write side says saved: false. The read side says nothing - and a read is what a request hits first.

What this adds

The adapters report backend failures to a PSR-3 logger. The pools are now given the cache log through one:

pool_error key=ro-_value_ operation=read  error=Connection refused [tcp://127.0.0.1:1] exceptionClass=ConnectionException
pool_error key=ro-_value_ operation=write error=Connection refused [tcp://127.0.0.1:1] exceptionClass=ConnectionException

Distinct from cache_error, which is an exception this package caught with the resource URI in hand; here only the pool key is known. Both guides say which is which.

Wired where the pools are built - StorageRedisDsnModule and StorageMemcachedModule, via InjectionPoints on setLogger - so an application gets it by installing the module it already installs.

Notes

operation is derived from the adapter's wording (fetch → read, save/write/delete/… → write) and is unknown when the wording is not one this package recognises: a mislabelled side is worse than an unlabelled one. The schema does not enum it, for the same reason as cache_policy.expiry - the demo-coverage gate would demand a fabricated demo for a value that exists for future wordings.

Demonstrated in demo/run-degraded.php session J against a real RedisTagAwareAdapter on redis://127.0.0.1:1: connection refused, no service to install, no timeout.

Stacked on #196 (which is stacked on #191). Merge those first.

`symfony/cache` adapters never throw at the application: an unreachable store answers a read as
a miss and a write as `false`. So `guard()` catches nothing, no `cache_error` is recorded, and a
Redis restart reads as a run of ordinary cold reads - the one shape an operator cannot act on.
The write side at least said `saved: false`; the read side said nothing at all.

The adapters report those failures to a PSR-3 logger, so the pools are handed the cache log:
`pool_error{key, operation, error, exceptionClass}` carries the backend's own message beside the
miss it caused. Wired where the pools are built (Redis and Memcached), demonstrated against a
real adapter pointed at a closed port, and distinguished from `cache_error` in both guides -
that one is an exception this package caught, with the resource URI in hand.

Found by an application flow that pointed the DSN at a dead port and asked what the log said.
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b0843a12-5e61-4c6c-8043-6c32989821a4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

koriym added 4 commits August 18, 2026 13:01
`#[Refresh]` and `#[Purge]` carry a URI. A resource whose entries share one invalidation handle -
a corpus tag across every query string that produced an entry - cannot be expressed that way, and
a write that does not happen inside a resource method has no interceptor at all. Both cases end at
`invalidateTags()`, which is why the log roots a direct call in `manual_invalidate` instead of
dropping its events.

Written where a reader looks when the cache misbehaves, not in the attribute that already types
its argument as a URI.
The closed-port probes connected eagerly under ext-redis (Predis is lazy by
default), so the InvalidArgumentException escaped at configure time instead
of surfacing as pool_error events at run time - every ubuntu CI job runs
with ext-redis loaded and failed at PoolErrorLogTest:46. Pass lazy=1 in the
DSN so both backends defer the connection.

testTheBackendsOwnMessageIsCarried pinned the Predis exception class
(ConnectionException); ext-redis surfaces the adapter's
InvalidArgumentException. The contract is that the caught throwable's class
is carried, so assert exceptionClass is not 'unknown'.

PoolErrorLogger: guard the mixed $level/$key casts PHPStan flagged.
A standalone StorageMemcachedEtagModule install bound its MemcachedAdapter
without the PSR-3 logger, so a dead ETag store failed silently on the
validator side - the silent-failure class this PR exists to kill.
# Conflicts:
#	docs/reading-the-log.ja.md
#	docs/reading-the-log.md
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a08e747) to head (22a2c0b).

Additional details and impacted files
@@             Coverage Diff             @@
##                 1.x      #198   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity       483       496   +13     
===========================================
  Files             94        96    +2     
  Lines           1281      1311   +30     
===========================================
+ Hits            1281      1311   +30     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…lters

Windows words a refused connection differently ('actively refused it'), so
pinning the POSIX wording failed the windows jobs. The contract is that the
backend's own message is carried: assert it is non-empty and the exception
class is not 'unknown'.

The same intent covers the two lines codecov flagged: a new test pins that
non-failure levels are ignored and a failure with no operation word is
recorded as operation 'unknown'.
@koriym
koriym merged commit 283badd into bearsunday:1.x Aug 19, 2026
20 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.

1 participant