Use a cache lock with an explicit failure path
Two requests try to rebuild the same expensive report. A cache lock can avoid duplicate work, but the caller must decide what to do when it cannot acquire the lock promptly.
Bound the wait and lock lifetime
$result = Cache::lock('report:tenant:42:monthly', 60)
->block(3, fn () => $this->rebuildReport());
This is an application fragment with a sample key. Replace the tenant component with trusted context. The lock has a finite lifetime, and the caller waits only a bounded period to acquire it. Handle lock-timeout failure by returning an appropriate pending or busy state rather than reporting a successful rebuild.
A lock is not a database constraint
If report construction runs longer than the lock's lifetime, another process can acquire the key while the first is still working. Choose a realistic duration and design the write path to tolerate failure or duplication. For data invariants, use database constraints and transactions as appropriate.
All participating processes must use the same supported lock store. A local file cache on separate servers does not create a shared lock. Verify the selected driver against your deployment architecture.
Test simultaneous requests, timeout behavior, and an exception inside the callback. Confirm that errors remain visible and locks are released according to the API's behavior. Do not catch every exception and return an old success value; the user needs to know whether the requested rebuild actually happened.