Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/Illuminate/Foundation/Cloud/FailedJobProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,16 @@ public function log($connection, $queue, $payload, $exception)
'started_at' => $processingJobDetails['started_at']->toDateTimeString('microsecond'),
'attempts' => $processingJobDetails['attempts'],
'payload' => $payload,
'exception' => (string) mb_convert_encoding($exception, 'UTF-8'),
'exception_preview' => mb_substr(
string: $exception->getMessage()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? $exception::class.': '.$exception->getMessage().' in '.$exception->getFile().':'.$exception->getLine()
: $exception::class.' in '.$exception->getFile().':'.$exception->getLine(),
start: 0,
length: 1001,
encoding: 'UTF-8',
),
'job_name' => (json_decode($payload, associative: true) ?? [])['displayName'] ?? '',
'exception' => (string) $exception,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mb_convert_encoding is redundant here. Our event class handles converting everything to UTF-8.

]);

$this->queue->finishProcessingJob(timestamp: $timestamp);
Expand Down
190 changes: 181 additions & 9 deletions tests/Foundation/Cloud/QueueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@ public function testItEmitsFailedJobEvents()
$job = $queue->pop();
$job->fail();
Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]);
$failedJobProvider->log('cloud', 'default', ['payload' => 'here'], new RuntimeException('Whoops!'));
$line = __LINE__ + 1;
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here', 'displayName' => 'App\\Jobs\\ProcessPodcast']), new RuntimeException('Whoops!'));
Str::createUuidsNormally();
$queue->pop();

Expand All @@ -495,9 +496,9 @@ public function testItEmitsFailedJobEvents()
'queue' => 'default',
'started_at' => '2000-01-02 03:04:05.060708',
'attempts' => 1,
'payload' => [
'payload' => 'here',
],
'payload' => json_encode(['payload' => 'here', 'displayName' => 'App\\Jobs\\ProcessPodcast']),
'exception_preview' => 'RuntimeException: Whoops! in '.__FILE__.':'.$line,
'job_name' => 'App\\Jobs\\ProcessPodcast',
],
[
'_cloud_event' => 'queue',
Expand All @@ -509,6 +510,170 @@ public function testItEmitsFailedJobEvents()
], $eventsFake->emitted);
}

public function testItEmitsFailedJobEventsWithExceptionPreviewWithMessage()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$agent->pushJob();
$job = $queue->pop();
$job->fail();
Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]);
$line = __LINE__ + 1;
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here']), new RuntimeException('Whoops!'));
Str::createUuidsNormally();
$queue->pop();

$this->assertSame(
'RuntimeException: Whoops! in '.__FILE__.':'.$line,
$eventsFake->emitted[1]['exception_preview'],
);
}

public function testItEmitsFailedJobEventsWithExceptionPreviewWithoutMessage()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$agent->pushJob();
$job = $queue->pop();
$job->fail();
Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]);
$line = __LINE__ + 1;
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here']), new RuntimeException);
Str::createUuidsNormally();
$queue->pop();

$this->assertSame(
'RuntimeException in '.__FILE__.':'.$line,
$eventsFake->emitted[1]['exception_preview'],
);
}

public function testItTruncatesLongExceptionPreviews()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$agent->pushJob();
$job = $queue->pop();
$job->fail();
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here']), new RuntimeException(str_repeat('a', 2000)));
$queue->pop();

$this->assertSame(1001, mb_strlen($eventsFake->emitted[1]['exception_preview']));
$this->assertSame('RuntimeException: '.str_repeat('a', 1001 - strlen('RuntimeException: ')), $eventsFake->emitted[1]['exception_preview']);
}

public function testItTruncatesExceptionPreviewsByWidthNotByteCountForMultibyteMessages()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$message = str_repeat('😎', 4).str_repeat('a', 2000);

$agent->pushJob();
$job = $queue->pop();
$job->fail();
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here']), new RuntimeException($message));
$queue->pop();

$this->assertSame(1001, mb_strlen($eventsFake->emitted[1]['exception_preview']));
$this->assertSame('RuntimeException: '.str_repeat('😎', 4).str_repeat('a', 1001 - 4 - strlen('RuntimeException: ')), $eventsFake->emitted[1]['exception_preview']);
}

public function testItSanitizesInvalidUtf8InTheExceptionField()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$agent->pushJob();
$job = $queue->pop();
$job->fail();
Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]);
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here']), new RuntimeException("Bad byte: \xFF"));
Str::createUuidsNormally();
$queue->pop();

$this->assertTrue(mb_check_encoding($eventsFake->stream, 'UTF-8'));
$this->assertTrue(mb_check_encoding($eventsFake->emitted[1]['exception'], 'UTF-8'));
$this->assertStringContainsString('Bad byte: �', $eventsFake->emitted[1]['exception']);
$this->assertStringContainsString('Bad byte: ?', $eventsFake->emitted[1]['exception_preview']);
}

public function testItEmitsFailedJobEventsWithJobDisplayName()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$agent->pushJob();
$job = $queue->pop();
$job->fail();
Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]);
$failedJobProvider->log('cloud', 'default', json_encode(['displayName' => 'App\\Jobs\\ProcessPodcast']), new RuntimeException('Whoops!'));
Str::createUuidsNormally();
$queue->pop();

$this->assertSame(
'App\\Jobs\\ProcessPodcast',
$eventsFake->emitted[1]['job_name'],
);
}

public function testItEmitsFailedJobEventsWithoutJobDisplayName()
{
$this->travelTo('2000-01-02 03:04:05.060708');
$eventsFake = $this->fakeEvents();
[$queue, $agent] = $this->fakeQueue();
$failerFake = $this->fakeFailer();
$failedJobProvider = new FailedJobProvider($failerFake, $eventsFake, $this->app['encrypter']);
$failedJobProvider->setQueue($queue);
$this->app[FailedJobProvider::class] = $failedJobProvider;

$agent->pushJob();
$job = $queue->pop();
$job->fail();
Str::createUuidsUsingSequence([Uuid::fromString('00dc709e-90c4-70c2-87c8-9b7127d20e8f')]);
$failedJobProvider->log('cloud', 'default', json_encode(['payload' => 'here']), new RuntimeException('Whoops!'));
Str::createUuidsNormally();
$queue->pop();

$this->assertSame(
'',
$eventsFake->emitted[1]['job_name'],
);
}

public function testItEmitsReleasedJobEvents()
{
$this->travelTo('2000-01-02 03:04:05.060708');
Expand Down Expand Up @@ -1638,13 +1803,20 @@ private function fakeEvents()
return $this->app->instance(Events::class, new class('test-socket') extends Events
{
public array $emitted = [];
public string $stream = '';

protected function connected(): bool
{
return true;
}

public function emitMany(array $payloads): void
protected function write(string $payload): void
{
$this->emitted = [
...$this->emitted,
...$payloads,
];
$this->stream .= $payload;

foreach (explode("\n", rtrim($payload, "\n")) as $write) {
$this->emitted[] = json_decode($write, associative: true);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refactor allows us to see what was really sent to the socket after being encoded. This helps with our UTF-8 assertions.

}
});
}
Expand Down