Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Fix JobWatcher when processing deleted instance of SerializesModels #1539

Merged
merged 17 commits into from
Oct 29, 2024
38 changes: 30 additions & 8 deletions src/Watchers/JobWatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Bus\BatchRepository;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Queue;
Expand Down Expand Up @@ -210,25 +211,21 @@ protected function updateBatch($payload)

Telescope::$shouldRecord = false;

$command = $this->getCommand($payload['data']);
$batchId = $this->getBatchId($payload['data']);

if ($wasRecordingEnabled) {
Telescope::$shouldRecord = true;
}

$properties = ExtractProperties::from(
$command
);

if (isset($properties['batchId'])) {
$batch = app(BatchRepository::class)->find($properties['batchId']);
if (! is_null($batchId)) {
$batch = app(BatchRepository::class)->find($batchId);

if (is_null($batch)) {
return;
}

Telescope::recordUpdate(EntryUpdate::make(
$properties['batchId'], EntryType::BATCH, $batch->toArray()
$batchId, EntryType::BATCH, $batch->toArray()
));
}
}
Expand All @@ -253,4 +250,29 @@ protected function getCommand(array $data)

throw new RuntimeException('Unable to extract job payload.');
}

/**
* Get the Batch ID from the given payload.
*
* @param array $data
* @return int|null
*
* @throws \RuntimeException
*/
protected function getBatchId(array $data)
{
try {
$command = $this->getCommand($data);

$properties = ExtractProperties::from($command);

return $properties['batchId'] ?? null;
} catch (ModelNotFoundException $e) {
if (preg_match('/"batchId";s:\d+:"([^"]+)"/', $data['command'], $matches)) {
return $matches[1];
}
}

return null;
}
}
78 changes: 47 additions & 31 deletions tests/Watchers/JobWatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Foundation\Auth\User;
use Illuminate\Queue\Jobs\Job;
use Illuminate\Queue\QueueManager;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Laravel\Telescope\EntryType;
use Laravel\Telescope\Tests\FeatureTestCase;
use Laravel\Telescope\Watchers\JobWatcher;
use Orchestra\Testbench\Attributes\WithMigration;
use Orchestra\Testbench\Factories\UserFactory;
use Throwable;

#[WithMigration('queue')]
class JobWatcherTest extends FeatureTestCase
{
protected function getEnvironmentSetUp($app)
Expand All @@ -31,13 +36,6 @@ protected function getEnvironmentSetUp($app)
$app->get('config')->set('logging.default', 'syslog');
}

protected function setUp(): void
{
parent::setUp();

$this->createJobsTable();
}

public function test_job_registers_entry()
{
$this->app->get(Dispatcher::class)->dispatch(new MyDatabaseJob('Awesome Laravel'));
Expand Down Expand Up @@ -130,31 +128,28 @@ public function test_it_handles_pushed_jobs()
$this->assertSame(['framework' => 'Laravel'], $entry->content['data']);
}

private function createJobsTable(): void
public function test_job_can_handle_deleted_serialized_model()
{
if (! Schema::hasTable('jobs')) {
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}

if (! Schema::hasTable('failed_jobs')) {
Schema::create('failed_jobs', function (Blueprint $table) {
$table->uuid('uuid');
$table->bigIncrements('id');
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
$user = UserFactory::new()->create();

$this->app->get(Dispatcher::class)->dispatch(
new MockedDeleteUserJob($user)
);

$this->artisan('queue:work', [
'connection' => 'database',
'--once' => true,
])->run();

$entry = $this->loadTelescopeEntries()->first();

$this->assertSame(EntryType::JOB, $entry->type);
$this->assertSame('processed', $entry->content['status']);
$this->assertSame('database', $entry->content['connection']);
$this->assertSame(MockedDeleteUserJob::class, $entry->content['name']);
$this->assertSame('default', $entry->content['queue']);

$this->assertSame(sprintf('%s:%s', get_class($user), $user->getKey()), $entry->content['data']['user']);
}
}

Expand All @@ -177,6 +172,27 @@ public function handle()
}
}

class MockedDeleteUserJob implements ShouldQueue
{
use SerializesModels;

public $connection = 'database';

public $deleteWhenMissingModels = true;

public $user;

public function __construct(User $user)
{
$this->user = $user;
}

public function handle()
{
$this->user->delete();
}
}

class MyDatabaseJob implements ShouldQueue
{
public $connection = 'database';
Expand Down