The Problem
Hi folks -
Running into some strange issues with multi-threaded behavior where I have multiple nested, queued Jobs that each modify a JSON object ($info) on my model.
Each job simply adds a new key-value pair to the array, however when they do so they seem to be overwriting the entire JSON object at once, despite me using the atomic JSON update syntax found in the Laravel docs.
This problem only emerges when I use Horizon to spin up more than one queue worker, as I have some jobs that take much longer than other jobs, presumably overwriting older data added to the model with the newer data.
Code Examples
My Model configuration:
protected $fillable = [
'url',
'email',
'status',
'progress',
'html_content',
'title',
'screenshot_path',
'meta_description',
'issues',
'info',
'info->messaging_evaluation',
'info->performance_metrics',
'info->html_size_kb',
'info->image_count',
'score',
'completed_at'
];
My parent Job:
Bus::batch([
new EvaluateLoadTime($this->quickScan),
new EvaluateCopy($this->quickScan),
new EvaluateImages($this->quickScan),
])->then(function (Batch $batch) use ($quickScan) {
// Now that everything is truly done, inform the user
dispatch(new Inform($quickScan)); // Email the visitor
Log::info('✅ All eval jobs completed.');
})->finally(function (Batch $batch) use ($quickScan) {
// Log::info('⚠️ Batch processing completed (success or failure)');
})->dispatch();
And this is how I am mutating the Job JSON object in the child jobs:
/**
* Set a key in the info array
*
* @param string $key The key to set
* @param mixed $value The value to set
* @param array $additional Additional fields to update
* @return bool Whether the update was successful
*/
public function setInfo($key, $value, array $additional = [])
{
// Problem: This should update ONLY $info->key
$updateData = ["info->{$key}" => $value];
// Add any additional fields
if ( ! empty($additional)) {
$updateData = array_merge($updateData, $additional);
}
// Perform the atomic update
return $this->update($updateData);
}
More Info
What I have tried so far:
- Passing IDs instead of serialized
QuickScan Eloquent models.
- Moving all the fields out of the
$info array, and onto the model, but this is not scalable.
- Refreshing the object before updating in
setInfo(): $this->refresh();
- (This actually works, but it forces a new database query every time I want to update a key-value pair on my
$info JSON object, not ideal)
Note: Nowhere in any of the child jobs is the $info object updated any other way. The setInfo() function is always used.
My Question
Am I doing something wrong here? Or is this simply not possible in Laravel?