php 45 lines · 6 steps

Releasing stale document locks in Laravel

An Artisan command finds documents locked past a timeout and clears them inside a single transaction.

Explained by highlit
1<?php
2 
3namespace App\Console\Commands;
4 
5use App\Models\Document;
6use Illuminate\Console\Command;
7use Illuminate\Support\Facades\DB;
8 
9class ReleaseStaleDocumentLocks extends Command
10{
11 protected $signature = 'documents:release-locks {--timeout=15 : Minutes before a lock is considered stale}';
12 
13 protected $description = 'Release soft locks held longer than the configured timeout';
14 
15 public function handle(): int
16 {
17 $threshold = now()->subMinutes((int) $this->option('timeout'));
18 
19 $released = DB::transaction(function () use ($threshold) {
20 $documents = Document::query()
21 ->whereNotNull('locked_by')
22 ->where('locked_at', '<=', $threshold)
23 ->lockForUpdate()
24 ->get();
25 
26 foreach ($documents as $document) {
27 $document->forceFill([
28 'locked_by' => null,
29 'locked_at' => null,
30 ])->save();
31 
32 activity()
33 ->performedOn($document)
34 ->withProperties(['reason' => 'lock_timeout'])
35 ->log('lock_released');
36 }
37 
38 return $documents;
39 });
40 
41 $this->info("Released {$released->count()} stale document lock(s).");
42 
43 return self::SUCCESS;
44 }
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Wrapping a find-then-update sweep in a transaction with row locks prevents two workers from clobbering the same records.
  2. 2Console commands expose configurable behavior cleanly through signature options with sensible defaults.
  3. 3Emitting an activity log alongside each mutation gives you an auditable trail of why records changed.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Releasing stale document locks in Laravel — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code