Skip to content

[BUGFIX] Keep the site running if the Redis server is unavailable - #35

Merged
bmack merged 2 commits into
mainfrom
fix/graceful-redis-degradation
Sep 9, 2026
Merged

[BUGFIX] Keep the site running if the Redis server is unavailable#35
bmack merged 2 commits into
mainfrom
fix/graceful-redis-degradation

Conversation

@bmack

@bmack bmack commented Sep 3, 2026

Copy link
Copy Markdown
Member

Fixes #34, fixes #27.

Problem

The Redis connection is established in the constructor (connectBackend()), and a RedisException thrown there is not caught. Core does not cushion that either: LockFactory::createLocker() instantiates the highest-priority strategy with new — no try/catch, no fallback to the next strategy — and it is called via ResourceMutex in the PrepareTypoScriptFrontendRendering middleware. A Redis outage therefore takes down the whole frontend with a 503, which is what happened in #34.

The same applies when the connection breaks while a request is running: lock() then returns false, the non-blocking acquire() throws LockAcquireWouldBlockException, and ResourceMutex keeps polling until max_execution_time is hit.

Solution

Connection problems are logged and handled by a fallback locking strategy instead of ending the request:

  • gracefulDegradation (bool, default true) — survive an unavailable Redis server. Set to false for the previous behaviour, which now throws a LockCreateException instead of a raw RedisException.
  • fallbackStrategy (class name, default TYPO3\CMS\Core\Locking\FileLockStrategy) — the strategy used while Redis is unavailable. Set to null to run without any locking in that case: the same page may then be generated by several processes in parallel, which is still better than a broken website. The same happens if the configured fallback strategy itself cannot be created.
  • An unreachable server is not contacted again for 30 seconds within the same PHP process, so a request does not run into the connection timeout for every single lock it creates.
  • Degradation also happens when the connection breaks mid-request (lock(), wait(), unlockAndSignal()), not only on connect.

While at it: the logger is fetched lazily, because LockFactory instantiates locking strategies via new instead of makeInstance(), so the logger is never injected and logging a Redis problem ended in "Call to a member function critical() on null" (#27).

The README documents the new options, the behaviour on an outage and the previously undocumented connectionTimeout / persistentConnection options.

Verification

Manually, against a TYPO3 v13 installation with phpredis 6.3 / PHP 8.5:

  • Redis available: lock is acquired, a second locker for the same subject is blocked, release works (unchanged behaviour).
  • Redis down, default fallback: no exception, the file lock is used and blocks a second locker.
  • Redis down, fallbackStrategy => null: no exception, locking is skipped.
  • Redis down, gracefulDegradation => false: LockCreateException.
  • Redis goes away after a successful connect: the locker degrades to the fallback instead of throwing.
  • Circuit breaker: 20 lockers created in under 100 ms after the first failed connection instead of waiting for the timeout each time.

The connection to Redis is established in the constructor, and a
RedisException thrown there was not caught. As LockFactory neither
catches exceptions nor falls back to another strategy, an unavailable
Redis server took down the whole frontend with a 503 error.

Connection problems are now logged and handled by a fallback locking
strategy (TYPO3's FileLockStrategy by default, configurable via
"fallbackStrategy", null to skip locking entirely), both when the
connection cannot be established and when it breaks during a request.
An unreachable server is not contacted again for 30 seconds within the
same PHP process, so a request does not run into the connection timeout
for every single lock it creates.

Set "gracefulDegradation" to false to get an exception instead, which is
then a LockCreateException.

Additionally, the logger is fetched lazily, as LockFactory instantiates
locking strategies via "new" instead of makeInstance(), so logging a
Redis problem ended in "Call to a member function critical() on null".

Fixes #34
Fixes #27
@mschwemer

Copy link
Copy Markdown

@bmack Tested it in a customer environment using fallbackStrategy = null. Worked perfectly.

Thanks!

@bmack

bmack commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

…gradation

# Conflicts:
#	src/RedisLockingStrategy.php

Co-authored-by: bmack <165630+bmack@users.noreply.github.com>

Copilot AI commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved by merging main into this branch and fixing the conflict in src/RedisLockingStrategy.php in commit 4e93b7d.

@bmack
bmack merged commit 0c03be4 into main Sep 9, 2026
@bmack
bmack deleted the fix/graceful-redis-degradation branch September 9, 2026 10:31
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.

[QUESTION] How to handle exceptions, if redis "fails" Call to a member function critical() on null

3 participants