diff --git a/packages/Ecotone/src/Messaging/Scheduling/Clock.php b/packages/Ecotone/src/Messaging/Scheduling/Clock.php index 0137c0b6e..9d6588fb6 100644 --- a/packages/Ecotone/src/Messaging/Scheduling/Clock.php +++ b/packages/Ecotone/src/Messaging/Scheduling/Clock.php @@ -16,6 +16,9 @@ class Clock implements EcotoneClockInterface public function __construct( private readonly ?PsrClockInterface $clock = null, ) { + if (!self::$globalClock) { + self::$globalClock = $this->clock ? $this : self::defaultClock(); + } } public static function set(PsrClockInterface $clock): void @@ -28,7 +31,12 @@ public static function set(PsrClockInterface $clock): void */ public static function get(): EcotoneClockInterface { - return self::$globalClock ??= new NativeClock(); + return self::$globalClock ??= self::defaultClock(); + } + + public static function resetToNativeClock(): void + { + self::$globalClock = null; } public function now(): DatePoint @@ -48,7 +56,12 @@ public function sleep(Duration $duration): void if ($clock instanceof SleepInterface) { $clock->sleep($duration); } else { - (new NativeClock())->sleep($duration); + self::defaultClock()->sleep($duration); } } + + private static function defaultClock(): EcotoneClockInterface + { + return new NativeClock(); + } } diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php new file mode 100644 index 000000000..40f145636 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/CustomNotifier.php @@ -0,0 +1,27 @@ +notifications[$eventName])) { + $this->notifications[$eventName] = []; + } + + $this->notifications[$eventName][] = $value; + } + + public function getNotificationsOf(string $eventName): array + { + return $this->notifications[$eventName] ?? []; + } +} diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php new file mode 100644 index 000000000..d172d5a43 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/NotificationService.php @@ -0,0 +1,23 @@ +notify('placedOrder', $event->orderId); + } +} \ No newline at end of file diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php new file mode 100644 index 000000000..d04b10657 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderService.php @@ -0,0 +1,20 @@ +publish(new OrderWasPlaced($command->orderId)); + } +} diff --git a/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php new file mode 100644 index 000000000..b3677d432 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Fixture/Scheduling/OrderWasPlaced.php @@ -0,0 +1,15 @@ +modify("+{$duration->zeroIfNegative()->inMicroseconds()} microseconds"); + } +} diff --git a/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php new file mode 100644 index 000000000..f7c78c545 --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Integration/Scheduling/DelayedMessageAgainstGlobalClockTest.php @@ -0,0 +1,67 @@ + + * + * @internal + */ +/** + * licence Apache-2.0 + * @internal + */ +class DelayedMessageAgainstGlobalClockTest extends TestCase +{ + protected function setUp(): void + { + parent::tearDown(); + Clock::resetToNativeClock(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Clock::resetToNativeClock(); + } + + public function test_delayed_message_observes_clock_changes() + { + $ecotoneTestSupport = EcotoneLite::bootstrapFlowTesting( + [EcotoneClockInterface::class, OrderService::class, NotificationService::class, CustomNotifier::class], + [$clock = new Clock(new StaticPsrClock('2025-08-11 16:00:00')), new OrderService(), new NotificationService(), $notifier = new CustomNotifier()], + enableAsynchronousProcessing: [ + // 1. Turn on Delayable In Memory Pollable Channel + SimpleMessageChannelBuilder::createQueueChannel('notifications', true) + ] + ); + + $ecotoneTestSupport->sendCommandWithRoutingKey('order.register', new PlaceOrder('123')); + + $clock->sleep(Duration::minutes(1)); + + // 2. Releasing messages awaiting for 60 seconds + $ecotoneTestSupport->run('notifications', releaseAwaitingFor: Clock::get()->now()); + + $this->assertEquals( + 1, + count($notifier->getNotificationsOf('placedOrder')) + ); + } +} diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php index c9ab8c3ac..eabb1878d 100644 --- a/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/SleepableStaticClockTest.php @@ -4,12 +4,10 @@ namespace Test\Ecotone\Messaging\Unit\Scheduling; -use DateTimeImmutable; use Ecotone\Messaging\Scheduling\Clock; use Ecotone\Messaging\Scheduling\Duration; -use Ecotone\Messaging\Scheduling\SleepInterface; use PHPUnit\Framework\TestCase; -use Psr\Clock\ClockInterface; +use Test\Ecotone\Messaging\Fixture\Scheduling\StaticPsrClock; /** * Class SleepableStaticClockTest @@ -26,38 +24,9 @@ class SleepableStaticClockTest extends TestCase { public function test_when_given_psr_clock_instance_with_sleep_support_it_increase_the_clock() { - $staticClock = $this->createStaticClock('2025-08-11 16:00:00'); - $clock = new Clock($staticClock); + $clock = new Clock(new StaticPsrClock('2025-08-11 16:00:00')); $clock->sleep(Duration::seconds(30)); $this->assertEquals('2025-08-11 16:00:30', $clock->now()->format('Y-m-d H:i:s')); } - - /** - * @description Create static clock with given date time to mimic external PsrClockInterface implementation - * as Ecotone's Clock dependency override. - * @param string $currentDateTime - * @return ClockInterface - */ - private function createStaticClock(string $currentDateTime): ClockInterface - { - return new class ($currentDateTime) implements ClockInterface, SleepInterface { - private static DateTimeImmutable $now; - - public function __construct(string $now) - { - self::$now = new DateTimeImmutable($now); - } - - public function now(): DateTimeImmutable - { - return self::$now; - } - - public function sleep(Duration $duration): void - { - self::$now = self::$now->modify("+{$duration->zeroIfNegative()->inMicroseconds()} microseconds"); - } - }; - } } diff --git a/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php new file mode 100644 index 000000000..b070bd65d --- /dev/null +++ b/packages/Ecotone/tests/Messaging/Unit/Scheduling/StaticGlobalClockTest.php @@ -0,0 +1,66 @@ + + * + * @internal + */ +/** + * licence Apache-2.0 + * @internal + */ +class StaticGlobalClockTest extends TestCase +{ + protected function setUp(): void + { + parent::tearDown(); + Clock::resetToNativeClock(); + } + + protected function tearDown(): void + { + parent::tearDown(); + Clock::resetToNativeClock(); + } + + public function test_when_clock_is_not_instantiated_returns_default_native_clock() + { + $now = new DatePoint('now'); + $globalClock = Clock::get(); + + $this->assertInstanceOf(NativeClock::class, $globalClock); + $this->assertEquals($now->format('Y-m-d H:i:s'), $globalClock->now()->format('Y-m-d H:i:s')); + } + + public function test_when_clock_is_not_instantiated_with_null_internal_clock_returns_default_native_clock() + { + $now = new DatePoint('now'); + $clock = new Clock(); + $globalClock = Clock::get(); + + $this->assertInstanceOf(NativeClock::class, $globalClock); + $this->assertEquals($now->format('Y-m-d H:i:s'), $globalClock->now()->format('Y-m-d H:i:s')); + } + + public function test_when_clock_is_not_instantiated_with_not_null_internal_clock_returns_ecotone_clock() + { + $clock = new Clock(new StaticPsrClock('2025-08-11 16:00:00')); + + $globalClock = Clock::get(); + + $this->assertInstanceOf(Clock::class, $globalClock); + $this->assertEquals('2025-08-11 16:00:00', $globalClock->now()->format('Y-m-d H:i:s')); + } +}