diff --git a/UPGRADE.md b/UPGRADE.md index 61a78a7..62c8f70 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,41 +2,50 @@ ## 0.5.0 -Two new `bigint` columns were added to the `processed_messages` table: -`wait_time` and `handle_time`. These are milliseconds. You will need to -create a migration to add these columns to your database. They are not -nullable so your migration will need to account for existing data. You -can either truncate (purge) the `processed_messages` table or have your -migration calculate these values based on the existing data. - -Here's a calculation example for MySQL: - -```php -use Doctrine\DBAL\Schema\Schema; -use Doctrine\Migrations\AbstractMigration; - -final class VersionXXX extends AbstractMigration -{ - public function getDescription(): string - { - return 'Add processed_messages.wait_time and handle_time columns'; - } - - public function up(Schema $schema): void - { - // Add the columns as nullable - $this->addSql('ALTER TABLE processed_messages ADD wait_time BIGINT DEFAULT NULL, ADD handle_time BIGINT DEFAULT NULL'); - - // set the times from existing data - $this->addSql('UPDATE processed_messages SET wait_time = TIMESTAMPDIFF(SECOND, dispatched_at, received_at) * 1000, handle_time = TIMESTAMPDIFF(SECOND, received_at, finished_at) * 1000'); - - // Make the columns not nullable - $this->addSql('ALTER TABLE processed_messages CHANGE wait_time wait_time BIGINT NOT NULL, CHANGE handle_time handle_time BIGINT NOT NULL'); - } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE processed_messages DROP wait_time, DROP handle_time'); - } -} -``` +* Two new `bigint` columns were added to the `processed_messages` table: + `wait_time` and `handle_time`. These are milliseconds. You will need to + create a migration to add these columns to your database. They are not + nullable so your migration will need to account for existing data. You + can either truncate (purge) the `processed_messages` table or have your + migration calculate these values based on the existing data. + + Here's a calculation example for MySQL: + + ```php + use Doctrine\DBAL\Schema\Schema; + use Doctrine\Migrations\AbstractMigration; + + final class VersionXXX extends AbstractMigration + { + public function getDescription(): string + { + return 'Add processed_messages.wait_time and handle_time columns'; + } + + public function up(Schema $schema): void + { + // Add the columns as nullable + $this->addSql('ALTER TABLE processed_messages ADD wait_time BIGINT DEFAULT NULL, ADD handle_time BIGINT DEFAULT NULL'); + + // set the times from existing data + $this->addSql('UPDATE processed_messages SET wait_time = TIMESTAMPDIFF(SECOND, dispatched_at, received_at) * 1000, handle_time = TIMESTAMPDIFF(SECOND, received_at, finished_at) * 1000'); + + // Make the columns not nullable + $this->addSql('ALTER TABLE processed_messages CHANGE wait_time wait_time BIGINT NOT NULL, CHANGE handle_time handle_time BIGINT NOT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE processed_messages DROP wait_time, DROP handle_time'); + } + } + ``` +* `ProcessedMessage::timeInQueue()` now returns milliseconds instead of seconds. +* `ProcessedMessage::timeToHandle()` now returns milliseconds instead of seconds. +* `ProcessedMessage::timeToProcess()` now returns milliseconds instead of seconds. +* `Snapshot::averageWaitTime()` now returns milliseconds instead of seconds. +* `Snapshot::averageHandlingTime()` now returns milliseconds instead of seconds. +* `Snapshot::averageProcessingTime()` now returns milliseconds instead of seconds. +* The `results` column in the `processed_messages` table is now nullable. Create + a migration to capture this change. This is to allow for a future feature where + result processing/storage can be disabled. diff --git a/config/doctrine/mapping/ProcessedMessage.orm.xml b/config/doctrine/mapping/ProcessedMessage.orm.xml index fac2a6f..c116a22 100644 --- a/config/doctrine/mapping/ProcessedMessage.orm.xml +++ b/config/doctrine/mapping/ProcessedMessage.orm.xml @@ -18,6 +18,6 @@ - + diff --git a/src/Command/MonitorCommand.php b/src/Command/MonitorCommand.php index 2d20e7a..e6537f2 100644 --- a/src/Command/MonitorCommand.php +++ b/src/Command/MonitorCommand.php @@ -123,8 +123,8 @@ private function renderSnapshot(SymfonyStyle $io, InputInterface $input): void $failRate < 10 => \sprintf('%s%%', $failRate), default => \sprintf('%s%%', $failRate), }, - $waitTime ? Helper::formatTime($snapshot->averageWaitTime()) : 'n/a', - $handlingTime ? Helper::formatTime($snapshot->averageHandlingTime()) : 'n/a', + $waitTime ? Helper::formatTime($snapshot->averageWaitTime() / 1000) : 'n/a', + $handlingTime ? Helper::formatTime($snapshot->averageHandlingTime() / 1000) : 'n/a', \round($snapshot->handledPerMinute(), 2), \round($snapshot->handledPerHour(), 2), \round($snapshot->handledPerDay(), 2), diff --git a/src/History/Metric.php b/src/History/Metric.php new file mode 100644 index 0000000..6282421 --- /dev/null +++ b/src/History/Metric.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Messenger\Monitor\History; + +/** + * @author Kevin Bond + */ +abstract class Metric +{ + final public function failRate(): float + { + try { + return $this->failureCount() / $this->totalCount(); + } catch (\DivisionByZeroError) { + return 0; + } + } + + /** + * @param positive-int $divisor Seconds + */ + final public function handledPer(int $divisor): float + { + $interval = $this->totalSeconds() / $divisor; + + return $this->totalCount() / $interval; + } + + final public function handledPerMinute(): float + { + return $this->handledPer(60); + } + + final public function handledPerHour(): float + { + return $this->handledPer(60 * 60); + } + + final public function handledPerDay(): float + { + return $this->handledPer(60 * 60 * 24); + } + + /** + * @return int milliseconds + */ + final public function averageProcessingTime(): int + { + return $this->averageWaitTime() + $this->averageHandlingTime(); + } + + /** + * @return int milliseconds + */ + abstract public function averageWaitTime(): int; + + /** + * @return int milliseconds + */ + abstract public function averageHandlingTime(): int; + + abstract public function failureCount(): int; + + abstract public function totalCount(): int; + + abstract protected function totalSeconds(): int; +} diff --git a/src/History/Model/MessageTypeMetric.php b/src/History/Model/MessageTypeMetric.php index f0fc67a..bd58399 100644 --- a/src/History/Model/MessageTypeMetric.php +++ b/src/History/Model/MessageTypeMetric.php @@ -11,62 +11,57 @@ namespace Zenstruck\Messenger\Monitor\History\Model; +use Zenstruck\Messenger\Monitor\History\Metric; use Zenstruck\Messenger\Monitor\Type; /** * @author Kevin Bond */ -final class MessageTypeMetric +final class MessageTypeMetric extends Metric { - public readonly Type $type; + private readonly Type $type; /** * @param class-string $class - * @param float $averageWaitTime In seconds - * @param float $averageHandlingTime In seconds */ public function __construct( string $class, - public readonly int $totalCount, - public readonly int $failureCount, - public readonly float $averageWaitTime, - public readonly float $averageHandlingTime, + private readonly int $totalCount, + private readonly int $failureCount, + private readonly int $averageWaitTime, + private readonly int $averageHandlingTime, private readonly int $totalSeconds, ) { $this->type = new Type($class); } - public function failRate(): float + public function type(): Type { - try { - return $this->failureCount / $this->totalCount; - } catch (\DivisionByZeroError) { - return 0; - } + return $this->type; } - /** - * @param positive-int $divisor Seconds - */ - public function handledPer(int $divisor): float + public function averageWaitTime(): int { - $interval = $this->totalSeconds / $divisor; + return $this->averageWaitTime; + } - return $this->totalCount / $interval; + public function averageHandlingTime(): int + { + return $this->averageHandlingTime; } - public function handledPerMinute(): float + public function failureCount(): int { - return $this->handledPer(60); + return $this->failureCount; } - public function handledPerHour(): float + public function totalCount(): int { - return $this->handledPer(60 * 60); + return $this->totalCount; } - public function handledPerDay(): float + protected function totalSeconds(): int { - return $this->handledPer(60 * 60 * 24); + return $this->totalSeconds; } } diff --git a/src/History/Model/ProcessedMessage.php b/src/History/Model/ProcessedMessage.php index 9612a70..5db7c2d 100644 --- a/src/History/Model/ProcessedMessage.php +++ b/src/History/Model/ProcessedMessage.php @@ -146,25 +146,25 @@ final public function isFailure(): bool } /** - * @return float In seconds + * @return int milliseconds */ - final public function timeInQueue(): float + final public function timeInQueue(): int { - return $this->waitTime / 1000; + return $this->waitTime; } /** - * @return float In seconds + * @return int milliseconds */ - final public function timeToHandle(): float + final public function timeToHandle(): int { - return $this->handleTime / 1000; + return $this->handleTime; } /** - * @return float In seconds + * @return int milliseconds */ - final public function timeToProcess(): float + final public function timeToProcess(): int { return $this->timeInQueue() + $this->timeToHandle(); } diff --git a/src/History/Snapshot.php b/src/History/Snapshot.php index 6ca9835..8eabbc8 100644 --- a/src/History/Snapshot.php +++ b/src/History/Snapshot.php @@ -20,12 +20,12 @@ /** * @author Kevin Bond */ -final class Snapshot +final class Snapshot extends Metric { private int $successCount; private int $failureCount; - private float $averageWaitTime; - private float $averageHandlingTime; + private int $averageWaitTime; + private int $averageHandlingTime; private int $totalSeconds; public function __construct(private Storage $storage, private Specification $specification) @@ -68,62 +68,14 @@ public function failureCount(): int return $this->failureCount ??= $this->storage->count($this->specification->failures()); } - public function failRate(): float + public function averageWaitTime(): int { - try { - return $this->failureCount() / $this->totalCount(); - } catch (\DivisionByZeroError) { - return 0; - } - } - - /** - * @return float In seconds - */ - public function averageWaitTime(): float - { - return $this->averageWaitTime ??= $this->storage->averageWaitTime($this->specification) ?? 0.0; - } - - /** - * @return float In seconds - */ - public function averageHandlingTime(): float - { - return $this->averageHandlingTime ??= $this->storage->averageHandlingTime($this->specification) ?? 0.0; - } - - /** - * @return float In seconds - */ - public function averageProcessingTime(): float - { - return $this->averageWaitTime() + $this->averageHandlingTime(); - } - - /** - * @param positive-int $divisor Seconds - */ - public function handledPer(int $divisor): float - { - $interval = $this->totalSeconds() / $divisor; - - return $this->totalCount() / $interval; - } - - public function handledPerMinute(): float - { - return $this->handledPer(60); - } - - public function handledPerHour(): float - { - return $this->handledPer(60 * 60); + return $this->averageWaitTime ??= $this->storage->averageWaitTime($this->specification) ?? 0; } - public function handledPerDay(): float + public function averageHandlingTime(): int { - return $this->handledPer(60 * 60 * 24); + return $this->averageHandlingTime ??= $this->storage->averageHandlingTime($this->specification) ?? 0; } public function totalSeconds(): int diff --git a/src/History/Storage.php b/src/History/Storage.php index 099289b..8797145 100644 --- a/src/History/Storage.php +++ b/src/History/Storage.php @@ -36,14 +36,14 @@ public function save(Envelope $envelope, Results $results, ?\Throwable $exceptio public function delete(mixed $id): void; /** - * @return float|null In seconds + * @return int|null milliseconds */ - public function averageWaitTime(Specification $specification): ?float; + public function averageWaitTime(Specification $specification): ?int; /** - * @return float|null In seconds + * @return int|null milliseconds */ - public function averageHandlingTime(Specification $specification): ?float; + public function averageHandlingTime(Specification $specification): ?int; public function count(Specification $specification): int; diff --git a/src/History/Storage/ORMStorage.php b/src/History/Storage/ORMStorage.php index fe7c8f8..8ea9401 100644 --- a/src/History/Storage/ORMStorage.php +++ b/src/History/Storage/ORMStorage.php @@ -64,8 +64,8 @@ public function perMessageTypeMetrics(Specification $specification): Collection ->select('m.type') ->addSelect('COUNT(m.type) as total_count') ->addSelect('COUNT(m.failureType) as failure_count') - ->addSelect('AVG(m.waitTime) / 1000 AS avg_wait_time') - ->addSelect('AVG(m.handleTime) / 1000 AS avg_handling_time') + ->addSelect('AVG(m.waitTime) AS avg_wait_time') + ->addSelect('AVG(m.handleTime) AS avg_handling_time') ->groupBy('m.type') ; @@ -77,8 +77,8 @@ public function perMessageTypeMetrics(Specification $specification): Collection $data['type'], $data['total_count'], $data['failure_count'], - (float) $data['avg_wait_time'], - (float) $data['avg_handling_time'], + (int) $data['avg_wait_time'], + (int) $data['avg_handling_time'], $totalSeconds, ); }) @@ -117,24 +117,24 @@ public function availableMessageTypes(Specification $specification): Collection return (new EntityResult($qb))->asString(); // @phpstan-ignore-line } - public function averageWaitTime(Specification $specification): ?float + public function averageWaitTime(Specification $specification): ?int { $qb = $this ->queryBuilderFor($specification, false) - ->select('AVG(m.waitTime) / 1000') + ->select('AVG(m.waitTime)') ; - return (new EntityResult($qb))->asFloat()->first(); + return (new EntityResult($qb))->asInt()->first(); } - public function averageHandlingTime(Specification $specification): ?float + public function averageHandlingTime(Specification $specification): ?int { $qb = $this ->queryBuilderFor($specification, false) - ->select('AVG(m.handleTime) / 1000') + ->select('AVG(m.handleTime)') ; - return (new EntityResult($qb))->asFloat()->first(); + return (new EntityResult($qb))->asInt()->first(); } public function count(Specification $specification): int diff --git a/src/Twig/ViewHelper.php b/src/Twig/ViewHelper.php index 40e668d..cdb2c07 100644 --- a/src/Twig/ViewHelper.php +++ b/src/Twig/ViewHelper.php @@ -12,6 +12,7 @@ namespace Zenstruck\Messenger\Monitor\Twig; use Knp\Bundle\TimeBundle\DateTimeFormatter; +use Symfony\Component\Console\Helper\Helper; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; @@ -48,17 +49,23 @@ public function formatTime(\DateTimeInterface $from): string return $this->timeFormatter?->formatDiff($from) ?? $from->format('c'); } - public function formatDuration(float $seconds): string + public function formatDuration(int $milliseconds): string { - if ($seconds < 1) { - return \sprintf('%d ms', $seconds * 1000); + if ($milliseconds < 1000) { + return \sprintf('%d ms', $milliseconds); } - if (!$this->timeFormatter || !\method_exists($this->timeFormatter, 'formatDuration')) { - return \sprintf('%.3f s', $seconds); + $seconds = $milliseconds / 1000; + + if ($this->timeFormatter && \method_exists($this->timeFormatter, 'formatDuration')) { + return $this->timeFormatter->formatDuration($seconds); + } + + if (\class_exists(Helper::class)) { + return Helper::formatTime($seconds); } - return $this->timeFormatter->formatDuration($seconds); + return \sprintf('%.3f s', $seconds); } public function generateCsrfToken(string ...$parts): string diff --git a/templates/layout.html.twig b/templates/layout.html.twig index da397d6..caf18a1 100644 --- a/templates/layout.html.twig +++ b/templates/layout.html.twig @@ -87,6 +87,10 @@ this.pauseTarget.classList.remove('d-none'); } + if (this.hasRefreshTarget) { + this.refreshTarget.classList.add('d-none'); + } + if (this.hasIndicatorTarget) { this.indicatorTarget.classList.remove('d-none'); } @@ -103,6 +107,10 @@ this.pauseTarget.classList.add('d-none'); } + if (this.hasRefreshTarget) { + this.refreshTarget.classList.remove('d-none'); + } + if (this.hasIndicatorTarget) { this.indicatorTarget.classList.add('d-none'); } diff --git a/tests/Integration/History/Storage/ORMStorageTest.php b/tests/Integration/History/Storage/ORMStorageTest.php index ae52c48..1a870c1 100644 --- a/tests/Integration/History/Storage/ORMStorageTest.php +++ b/tests/Integration/History/Storage/ORMStorageTest.php @@ -46,7 +46,7 @@ public function average_wait_time(): void 'receivedAt' => $start->modify('+10 seconds'), ]); - $this->assertSame(15.0, $this->storage()->averageWaitTime(Specification::new())); + $this->assertSame(15000, $this->storage()->averageWaitTime(Specification::new())); } /** @@ -65,7 +65,7 @@ public function average_handling_time(): void 'finishedAt' => $start->modify('+20 seconds'), ]); - $this->assertSame(30, (int) $this->storage()->averageHandlingTime(Specification::new())); + $this->assertSame(30000, (int) $this->storage()->averageHandlingTime(Specification::new())); } /** @@ -112,29 +112,29 @@ public function per_message_type_metrics(): void $messageTypeMetrics = $this->storage() ->perMessageTypeMetrics(Specification::create(Period::IN_LAST_HOUR)) ->eager() - ->sortBy(fn(MessageTypeMetric $metric) => $metric->type->class()) + ->sortBy(fn(MessageTypeMetric $metric) => $metric->type()->class()) ->values() ->all() ; $this->assertCount(2, $messageTypeMetrics); - $this->assertSame(MessageA::class, $messageTypeMetrics[0]->type->class()); - $this->assertSame(2, $messageTypeMetrics[0]->totalCount); - $this->assertSame(0, $messageTypeMetrics[0]->failureCount); + $this->assertSame(MessageA::class, $messageTypeMetrics[0]->type()->class()); + $this->assertSame(2, $messageTypeMetrics[0]->totalCount()); + $this->assertSame(0, $messageTypeMetrics[0]->failureCount()); $this->assertSame(0.0, $messageTypeMetrics[0]->failRate()); - $this->assertSame(15.0, $messageTypeMetrics[0]->averageWaitTime); - $this->assertSame(20.0, $messageTypeMetrics[0]->averageHandlingTime); + $this->assertSame(15000, $messageTypeMetrics[0]->averageWaitTime()); + $this->assertSame(20000, $messageTypeMetrics[0]->averageHandlingTime()); $this->assertSame(48.0, $messageTypeMetrics[0]->handledPerDay()); $this->assertSame(2.0, \round($messageTypeMetrics[0]->handledPerHour(), 2)); $this->assertSame(0.03, \round($messageTypeMetrics[0]->handledPerMinute(), 2)); - $this->assertSame(MessageB::class, $messageTypeMetrics[1]->type->class()); - $this->assertSame(3, $messageTypeMetrics[1]->totalCount); - $this->assertSame(1, $messageTypeMetrics[1]->failureCount); + $this->assertSame(MessageB::class, $messageTypeMetrics[1]->type()->class()); + $this->assertSame(3, $messageTypeMetrics[1]->totalCount()); + $this->assertSame(1, $messageTypeMetrics[1]->failureCount()); $this->assertSame(0.33, \round($messageTypeMetrics[1]->failRate(), 2)); - $this->assertSame(11.67, \round($messageTypeMetrics[1]->averageWaitTime, 2)); - $this->assertSame(13.33, \round($messageTypeMetrics[1]->averageHandlingTime, 2)); + $this->assertSame(11666, $messageTypeMetrics[1]->averageWaitTime()); + $this->assertSame(13333, $messageTypeMetrics[1]->averageHandlingTime()); $this->assertSame(72.0, $messageTypeMetrics[1]->handledPerDay()); $this->assertSame(0.05, \round($messageTypeMetrics[1]->handledPerMinute(), 2)); } diff --git a/tests/Unit/History/Model/ProcessedMessageTest.php b/tests/Unit/History/Model/ProcessedMessageTest.php index 9ef028c..d53e266 100644 --- a/tests/Unit/History/Model/ProcessedMessageTest.php +++ b/tests/Unit/History/Model/ProcessedMessageTest.php @@ -65,9 +65,9 @@ public function id(): string|int|\Stringable|null $this->assertSame([], $message->tags()->all()); $this->assertSame([], $message->results()->all()); $this->assertSame('foo', $message->transport()); - $this->assertSame(1.0, $message->timeInQueue()); - $this->assertSame(2.0, $message->timeToHandle()); - $this->assertSame(3.0, $message->timeToProcess()); + $this->assertSame(1000, $message->timeInQueue()); + $this->assertSame(2000, $message->timeToHandle()); + $this->assertSame(3000, $message->timeToProcess()); $this->assertFalse($message->isFailure()); $this->assertNull($message->failure()); $this->assertTrue($message->memoryUsage()->isGreaterThan(0)); @@ -169,8 +169,8 @@ public function id(): string|int|\Stringable|null $this->assertEquals($start, $message->dispatchedAt()); $this->assertEquals($start->modify('+1100 milliseconds'), $message->receivedAt()); $this->assertEquals($start->modify('+3300 milliseconds'), $message->finishedAt()); - $this->assertSame(1.1, $message->timeInQueue()); - $this->assertSame(2.2, $message->timeToHandle()); - $this->assertSame(3.3, \round($message->timeToProcess(), 1)); + $this->assertSame(1100, $message->timeInQueue()); + $this->assertSame(2200, $message->timeToHandle()); + $this->assertSame(3300, $message->timeToProcess()); } } diff --git a/tests/Unit/History/SnapshotTest.php b/tests/Unit/History/SnapshotTest.php index 926f122..291d7e6 100644 --- a/tests/Unit/History/SnapshotTest.php +++ b/tests/Unit/History/SnapshotTest.php @@ -32,8 +32,8 @@ public function access_values(): void $storage = $this->createMock(Storage::class); $storage->expects($this->once())->method('filter')->with($spec)->willReturn(collect(['foo', 'bar'])); $storage->expects($this->exactly(2))->method('count')->with($this->isInstanceOf(Specification::class))->willReturn(60, 40); - $storage->expects($this->once())->method('averageWaitTime')->with($spec)->willReturn(2.0); - $storage->expects($this->once())->method('averageHandlingTime')->with($spec)->willReturn(1.0); + $storage->expects($this->once())->method('averageWaitTime')->with($spec)->willReturn(2000); + $storage->expects($this->once())->method('averageHandlingTime')->with($spec)->willReturn(1000); $snapshot = new Snapshot($storage, $spec); @@ -41,9 +41,9 @@ public function access_values(): void $this->assertSame(60, $snapshot->successCount()); $this->assertSame(40, $snapshot->failureCount()); $this->assertSame(100, $snapshot->totalCount()); - $this->assertSame(2.0, $snapshot->averageWaitTime()); - $this->assertSame(1.0, $snapshot->averageHandlingTime()); - $this->assertSame(3.0, $snapshot->averageProcessingTime()); + $this->assertSame(2000, $snapshot->averageWaitTime()); + $this->assertSame(1000, $snapshot->averageHandlingTime()); + $this->assertSame(3000, $snapshot->averageProcessingTime()); $this->assertSame(0.4, $snapshot->failRate()); $this->assertSame(0.069, \round($snapshot->handledPerMinute(), 3)); $this->assertSame(4.167, \round($snapshot->handledPerHour(), 3)); @@ -88,8 +88,8 @@ public function invalid_wait_times(): void $snapshot = new Snapshot($storage, $spec); - $this->assertSame(0.0, $snapshot->averageWaitTime()); - $this->assertSame(0.0, $snapshot->averageHandlingTime()); - $this->assertSame(0.0, $snapshot->averageProcessingTime()); + $this->assertSame(0, $snapshot->averageWaitTime()); + $this->assertSame(0, $snapshot->averageHandlingTime()); + $this->assertSame(0, $snapshot->averageProcessingTime()); } }