Weave Code
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

dualmedia/doctrine-retry-bundle

Symfony bundle that wraps Doctrine transactions with automatic retries for deadlocks and transient DB errors. Configure optional nesting tracking, then call Retrier->execute() with a closure receiving the EntityManager to safely run retryable work.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require dualmedia/doctrine-retry-bundle
    

    Register the bundle in config/bundles.php:

    DualMedia\DoctrineRetryBundle\DoctrineRetryBundle::class => ['all' => true],
    
  2. First Use Case: Inject Retrier into a service and wrap database operations that may fail due to deadlocks or contention:

    use DualMedia\DoctrineRetryBundle\Retrier;
    use Doctrine\ORM\EntityManagerInterface;
    
    class OrderProcessor
    {
        public function __construct(private Retrier $retrier) {}
    
        public function processOrder(int $orderId): void
        {
            $this->retrier->execute(function (EntityManagerInterface $em) use ($orderId) {
                $order = $em->getRepository(Order::class)->find($orderId, LockMode::PESSIMISTIC_WRITE);
                // Business logic here
                $em->flush();
            });
        }
    }
    

Key Configuration

  • Default config works out-of-the-box. Customize in config/packages/dm_doctrine_retry.yaml:
    dm_doctrine_retry:
        track_nesting: '%kernel.debug%' # Warns about nested transactions in debug mode
    

Implementation Patterns

Core Workflow

  1. Retryable Operations: Use Retrier::execute() for any operation that might fail due to:

    • Database deadlocks
    • Lock contention
    • Temporary network issues
  2. Locking Strategies: Combine with Doctrine lock modes (e.g., PESSIMISTIC_WRITE) for atomicity:

    $entity = $em->find(Entity::class, $id, LockMode::PESSIMISTIC_WRITE);
    
  3. Event-Driven Extensions: Listen to TransactionStartEvent for pre-transaction hooks:

    $eventDispatcher->addListener(TransactionStartEvent::class, function (TransactionStartEvent $event) {
        // Log or pre-process before retry
    });
    

Integration Tips

  • Dependency Injection: Prefer constructor injection of Retrier over manual instantiation.

    // services.yaml
    services:
        App\Services\OrderProcessor:
            arguments:
                $retrier: '@dm_doctrine_retry.retrier'
    
  • Command Bus Integration: Wrap retryable logic in commands:

    $bus->handle(new ProcessOrderCommand($orderId));
    
  • Testing: Mock Retrier to simulate retries:

    $this->mockRetrier->shouldReceive('execute')->once()->andReturnUsing(function ($callback) {
        $callback($this->mockEm);
    });
    

Gotchas and Tips

Common Pitfalls

  1. Nested Transactions:

    • Avoid nesting Retrier::execute() calls. Enable track_nesting in debug mode to catch this:
      dm_doctrine_retry:
          track_nesting: true
      
    • Fix: Use a single Retrier wrapper for the entire operation.
  2. Stateful Operations:

    • Retry logic does not preserve state between retries (e.g., external API calls, cached data).
    • Fix: Re-fetch or recompute state inside the retry block.
  3. Event Listeners:

    • Events like TransactionStartEvent fire per retry, not just once.
    • Fix: Use a flag or context object to avoid duplicate side effects.
  4. Performance Overhead:

    • Retries add latency. Use sparingly for idempotent operations.
    • Tip: Log retry attempts for monitoring:
      $this->retrier->execute(function (EntityManagerInterface $em) {
          $this->logger->info('Retry attempt', ['attempt' => $this->retrier->getAttemptCount()]);
          // ...
      });
      

Debugging Tips

  • Enable Debug Mode: Set track_nesting: true to log nested transaction warnings.
  • Check Retry Count: Access $retrier->getAttemptCount() inside the callback to debug retry loops.
  • Doctrine Events: Listen to TransactionEvent for low-level debugging:
    $eventDispatcher->addListener(TransactionEvent::class, function (TransactionEvent $event) {
        if ($event->isRolledBack()) {
            $this->logger->error('Transaction rolled back', ['exception' => $event->getException()]);
        }
    });
    

Extension Points

  1. Custom Retry Logic: Extend Retrier by implementing RetryStrategyInterface:

    class CustomRetryStrategy implements RetryStrategyInterface
    {
        public function shouldRetry(Throwable $exception, int $attempt): bool
        {
            return $exception instanceof DeadlockException && $attempt < 3;
        }
    }
    

    Register it in services.yaml:

    services:
        App\Retry\CustomRetryStrategy:
            tags: { name: 'dm_doctrine_retry.strategy' }
    
  2. Global Configuration: Override default retry limits via DI:

    services:
        dm_doctrine_retry.retrier:
            arguments:
                $maxAttempts: 5
                $retryStrategy: '@App\Retry\CustomRetryStrategy'
    
  3. Transaction Isolation: Combine with Doctrine’s Connection::beginTransaction() for fine-grained control:

    $this->retrier->execute(function (EntityManagerInterface $em) {
        $conn = $em->getConnection();
        $conn->beginTransaction();
        // Custom isolation level
        $conn->setTransactionIsolation(Connection::TRANSACTION_READ_COMMITTED);
        // ...
    });
    
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.
emuniq/filament-browser-notifications
syriable/filament-translator
hungnm28/livewire-form
wenprise/eloquent
crudly/encrypted
fadion/bouncy
cuci/prototurk-sdk
gos/pubsub-router-bundle
cuci/prototurk-sdk-symfony
clementtalleu/easyadmin-markdown-bundle
codeflextech/permission-manager
karnoweb/livewire-datepicker
sayedenam/sayed-dashboard
milito/query-filter
apiboxsym/user-bundle
apiboxsym/health-check-bundle
jayeshmepani/jpl-moshier-ephemeris-php
elnasnato/laraliveui
labrodev/rest-sdk
sampaui/sampaui