Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Retry Bundle Laravel Package

keboola/doctrine-retry-bundle

A Symfony bundle that adds configurable retry logic to Doctrine DBAL connections, helping applications recover from transient database failures and improve resilience with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Check the stack before you install anything. 1.2.0 requires PHP ^8.4, doctrine/dbal ^4.2, doctrine/doctrine-bundle ^3.0, and Symfony components ^7.4|^8.0. If you are on Symfony 6.4 / doctrine-bundle 2.x you cannot take 1.2.0 at all — stay on 1.1.1 or plan the framework upgrade first. Run composer why-not keboola/doctrine-retry-bundle ^1.2 rather than guessing.
  2. Install the bundle with Composer. It is a Symfony bundle; with Flex it is usually registered automatically, otherwise add it to config/bundles.php.
  3. Decide which Doctrine connections should retry. The bundle is opt-in per connection — nothing happens unless you set the option.
  4. Add x_connect_retries to the connection's driverOptions:
# config/packages/doctrine.yaml
doctrine:
    dbal:
        connections:
            default:
                url: '%env(resolve:DATABASE_URL)%'
                driverOptions:
                    x_connect_retries: 3
  1. Clear the cache and confirm the middleware landed (see the debugging tip below). A connection with no option, or x_connect_retries: 0, is skipped entirely — that is the correct way to disable the bundle for one connection without removing it.

Where to look first: the bundle README (x_connect_retries is the only public knob) and, in the source, DependencyInjection\Compiler\ConfigureDbalRetryProxyPass — the compiler pass is the real substance of the package, and it tells you exactly which connection keys are read and how the middleware is wired. Database\Retry\Driver shows what is and is not retried.

First use case: an app that fails to boot or serve its first request during a deploy, a MySQL failover, or a transient network blip. Instead of adding retry loops in bootstrap code or in a custom DBAL connection factory, you set one integer and the connection establishment gets retried automatically. This is particularly relevant for ephemeral CI/container environments and for workers that open a single connection at startup and die immediately if the database isn't reachable yet.

Implementation Patterns

Per-connection opt-in, driven by environment. Because the value is read from the connection definition at compile time, you can source it from an env var and keep staging/production different:

driverOptions:
    x_connect_retries: '%env(int:DB_CONNECT_RETRIES)%'

Keep it low (2–5). The retry budget multiplies your connection timeout; it is not a substitute for a working healthcheck.

Upgrade the bundle and doctrine-bundle together, then re-verify. 1.2.0 exists specifically to sit on doctrine-bundle 3.x, so the framework bump and the bundle bump happen in the same change. The compiler pass works by rewriting DoctrineBundle's setMiddlewares() method call in the compiled container; a major DoctrineBundle release is exactly the kind of thing that can move that call. Land the upgrade, then immediately run your fault-injection test before merging anything else on top — do not treat "it installed" as "it retries".

Retry connect, retry transactions yourself. The driver overrides only connect(). That is deliberate: replaying a query after a mid-transaction failure corrupts state. For deadlocks and serialization failures, keep an application-level retry around the whole transaction closure (Doctrine's transactional() plus your own retry helper), with idempotency guarantees. The two layers are complementary, not overlapping — do not expect this bundle to save a flaky query.

Tune it differently for web vs. CLI. The short-lived web request wants a small number of quick attempts; a migration or a messenger:consume worker boot can afford more. Use separate connections (or separate env values per process type) rather than a single global compromise.

Multiple connections and replicas. The pass walks every entry in doctrine.connections, so you can enable retries on the primary write connection only, or on read replicas independently. Remember that Configuration middleware is shared where the connections share a configuration service — verify behaviour per connection rather than assuming isolation.

Middleware ordering matters, and the bundle handles it. The retry middleware is prepended, so it wraps the driver outermost and sees failures raised by every other middleware. This means middleware you add (logging, profiling, custom instrumentation) sits inside the retry loop: with DBAL logging enabled you will typically see one logged failure per attempt, which is exactly what you want for alerting on "how often do we actually retry".

Verify the path with a real fault, not a mock. The upstream test suite uses Toxiproxy to cut the connection and force the failure path, plus a FailingRetryProxy alias to count attempts deterministically. In your own app, the cheap equivalent is pointing the connection at a closed port and asserting the request takes roughly attempts × connect timeout and that your logs show N attempts. For a permanent regression guard, alias/replace the retry proxy service in config/services_test.yaml with a counting stub and assert it was invoked. This test is worth more after 1.2.0 than it was before it.

Health checks and readiness probes. Since a broken database now costs you N connect attempts, make sure your readiness endpoint uses a very short timeout or x_connect_retries: 0; otherwise probes stack up during an outage and slow down orchestration decisions.

Gotchas and Tips

  • It silently stops working after a DoctrineBundle upgrade — and 1.2.0 is a DoctrineBundle upgrade. The compiler pass rewrites DoctrineBundle's existing setMiddlewares() method call (removes it, re-adds it with the retry middleware prepended). If DoctrineBundle changed how middlewares are registered, the pass finds nothing to rewrite and your retries disappear with no error, no warning, no deprecation. 1.2.0 is the first release targeting doctrine-bundle 3.x, so if you are moving from 1.1.1 + doctrine-bundle 2.x, treat re-verification of the wiring as a required step of the upgrade, not an optional one. Keep doing this for every subsequent doctrine/doctrine-bundle or Symfony major.
  • 1.2.0 is breaking for anyone on the LTS stack. Symfony 6.4 and doctrine-bundle 2.x are no longer supported. If your other dependencies pin you to Symfony 6.4, you are stuck on 1.1.1 and its older constraints — pin explicitly (^1.1.1) so a future resolution doesn't try to drag the bundle forward and fail the install, and be aware that 1.1.1 will not receive the Symfony 8 fixes.
  • Only connection establishment is retried. Authentication errors, permission errors, and an unreachable host all look like "connection failure" to the driver. With retries enabled, a genuinely misconfigured password turns into N failed attempts on every request — watch p99 latency on boot and log the underlying exception.
  • driverOptions is forwarded to the real driver. x_connect_retries is a synthetic key sitting in the options array that DBAL passes down to PDO. Confirm with your driver/PHP version that the key is stripped before delegation or tolerated; a smoke test that just opens a connection on each of your configured drivers is worth the five minutes — re-run it after the DBAL 4.2 / doctrine-bundle 3 bump, since driver option handling is one of the things that moves between majors.
  • YAML quoting. A bare string "3" versus 3 can matter; use %env(int:...)% or an unquoted integer so the pass doesn't see something it treats as absent.
  • Debugging the wiring. Dump the compiled container (var/cache/dev/App_KernelDevDebugContainer.xml) and look for the Configuration service's setMiddlewares call — the middleware list should contain a per-connection retry middleware. bin/console debug:container plus a cache clear is the fastest way to confirm a config change actually reached the pass, since compile-time passes do not react to runtime config edits.
  • Version constraints stay aggressive. 1.2.0 moved the floor up rather than widening it: PHP ^8.4, doctrine/dbal ^4.2, doctrine-bundle ^3.0, Symfony ^7.4|^8.0. DBAL 3 and doctrine-bundle 2.x are not supported, and the package will not install on the LTS stack most apps still run. Check composer why-not before assuming a conflict is your app's fault.
  • Extension points. If you need jitter, custom backoff, or attempt logging, the seam is the retry proxy service that the middleware is built from — override or decorate it in your container (that is precisely what the upstream tests do with their FailingRetryProxy). Replacing the compiler pass entirely is the other option if you have non-standard middleware registration.
  • The GitHub repo is a read-only mirror. Do not file PRs or branch against keboola/doctrine-retry-bundle; changes go to libs/doctrine-retry-bundle/ in keboola/platform-libraries and are force-pushed over the mirror. If you need a patch, vendor a fork or pin a commit from the monorepo path.
  • Adoption is still near zero. The 1.2.0 notes mention one first-party downstream consumer (the editor service) that was previously blocked by doctrine-bundle 2.x, but there are no external dependents and no stars — you are still an early tester. Expect thin docs beyond x_connect_retries, and budget time to read the four classes rather than searching for blog posts. The upside is that the entire surface area is small enough to audit in one sitting, which is the right thing to do before it sits in your critical boot path.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect