Laravel Queue Context Deserialization Issues Across Applications
When working with Laravel's asynchronous queues within a single application, context-related issues rarely occur. However, problems emerge when distributing tasks between separate Laravel applications.
Consider a scenario where you have two applications: an "app" application that processes API requests and an "admin" application that handles background tasks. The "app" application dispatches asynchronous jobs to the "admin" application to improve API performance.
The issue arises when using context with object values. For example:
Context::add('laravel_unique_job_key', 4444);
Context::addHidden('eeee', 333333);
$tokenData = new TokenInformation();
Context::add('tokenData', $tokenData);
When dispatching a job using dispatchJob($job), the context's "data" and "hidden" sections are included in the payload:
{
"displayName": "App\\Jobs\\Job",
"job": "Illuminate\\Queue\\CallQueuedHandler@call",
"maxTries": 3,
"maxExceptions": null,
"failOnTimeout": false,
"backoff": null,
"timeout": null,
"retryUntil": null,
"data": {
"commandName": "App\\Jobs\\Job",
"command": "O:27:\"App\\Jobs\\Job\":1:{s:7:\"orderId\";i:221;}"
},
"illuminate:log:context": {
"data": {
"tokenData": "O:40:\"App\\Http\\TokenInformation\":2:{s:4:\"uuid\";s:0:\"\";s:2:\"id\";i:0;}",
"zx": "i:11111;"
},
"hidden": {
"eeee": "i:333333;"
}
}
}
If the "admin" application doesn't have the TokenInformation class defined, it will throw an exception:
RuntimeException: Value is incomplete class: {"__PHP_Incomplete_Class_Name":"App\\Http\\TokenInformation","uuid":"","id":0}
Solutions
- Avoid storing object-type data in context when dispatching asynchronous tasks between applications.
- Ensure all required classes are available in the receiving application.
- Create a custom dispatch method that filters context data before queuing.
Custom Dispatch Implementation
if (!function_exists('dispatchBackgroundJob')) {
function dispatchBackgroundJob(mixed $job)
{
// Keys that should be excluded from context
$excludedKeys = ['laravel_unique_job_cache_store', 'laravel_unique_job_key'];
$allContext = Context::all();
$allHiddenContext = Context::allHidden();
// If no context exists, dispatch directly
if (empty($allContext) && empty($allHiddenContext)) {
dispatch($job);
return;
}
// Filter out excluded keys
$filteredContext = Arr::except($allContext, $excludedKeys);
$filteredHiddenContext = Arr::except($allHiddenContext, $excludedKeys);
$contextKeys = array_keys($filteredContext);
$hiddenKeys = array_keys($filteredHiddenContext);
// Remove unnecessary context keys
if (!empty($contextKeys)) {
foreach ($contextKeys as $key) {
Context::forget($key);
}
}
// Remove unnecessary hidden context keys
if (!empty($hiddenKeys)) {
foreach ($hiddenKeys as $hiddenKey) {
Context::forgetHidden($hiddenKey);
}
}
// Dispatch the job
dispatch($job);
// Restore original context
if (!empty($contextKeys)) {
foreach ($contextKeys as $key) {
Context::add($key, $allContext[$key]);
}
}
if (!empty($hiddenKeys)) {
foreach ($hiddenKeys as $hiddenKey) {
Context::addHidden($hiddenKey, $allHiddenContext[$hiddenKey]);
}
}
}
}