Symfony 8.2 New Published on

One import at a time, with #[Lock]

#[Lock] puts a lock on a controller: a concurrent request gets a 409, and reads can share a lock between them.

Topics covered: HttpKernelLock

New in Symfony 8.2 (in development). To keep two requests from doing the same thing at the same time, you had to inject a LockFactory, create the lock, acquire it, return an error if it was taken, and remember to release it. The #[Lock] attribute does all of this for a controller.

#[Lock]

use Symfony\Component\HttpKernel\Attribute\Lock;

#[Route('/catalog/import', methods: ['POST'])]
#[Lock('catalog')]
public function import(): Response
  • As long as a request holds the lock, the others get 409 Conflict. With blocking: true, they wait for their turn.
  • The lock is released at the end of the controller, or once a StreamedResponse has been fully sent.
  • {name} in the key is replaced by the name route parameter: #[Lock('sales-{conference}')] gives one lock per conference.
  • read: true takes a shared lock: reads run together, but not with the write that holds the same key.
  • The lock lasts 30 seconds at most (ttl); Symfony does not extend it. It is taken as soon as the controller is known, even before #[IsGranted] is checked.

The Lock component must be installed; it then configures itself, with semaphores when the sysvsem extension is available, file locks otherwise. Read locks need a store that supports them (flock, Redis, PostgreSQL…): here, config/packages/lock.yaml enforces flock.

Your task

CatalogueController imports and exports the catalog of each shop. An import rewrites the catalog file: two simultaneous imports mix their products, and an export read during an import delivers half of it.

  1. Only one import at a time per shop: a concurrent import gets a 409.
  2. The exports of a shop run together, but not at the same time as an import: whichever of the two arrives second gets a 409.
  3. The product count, for its part, stays available at all times.

The nightly import and the accounting export, started outside the application, already take the catalog-{shop} lock (catalog-lyon for Lyon): use the same key.

Where the feature comes from: the original pull request.