Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Clock] A new component to decouple applications from the system clock #46715

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

Merged
merged 1 commit into from
Jul 28, 2022

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Jun 19, 2022

Q A
Branch? 6.2
Bug fix? no
New feature? yes
Deprecations? no
Tickets -
License MIT
Doc PR -

After watching @afilina's talk at SymfonyWorld Online last week + listening to her using a "clock" service to improve testability of time-sensitive logics, I decided to propose this new "Clock" component. This also relates to the ongoing efforts to standardize a ClockInterface in PSR-20.

This PR provides a ClockInterface, with 3 methods:

  • now(): \DateTimeImmutable is designed to be compatible with the envisioned PSR-20;
  • sleep(float|int $seconds): void advances the clock by the provided number of seconds;
  • withTimeZone(): static changes the time zone returned by now().

The sleep() methods takes inspiration from ClockMock in the PhpUnitBridge, where this proved useful to improve testability. Ideally, we could use this component everywhere measuring the current time is needed and stop relying on ClockMock.

This PR provides 3 clock implementations:

  • NativeClock which relies on the system clock;
  • MockClock which allows mocking the time;
  • MonotonicClock which relies on hrtime() to provide a monotonic clock.

If this gets accepted, I'll follow up with a PR to add clock services to FrameworkBundle and we'll then be able to see where we could use such clock services in other components. I hope PSR-20 will be stabilized by Symfony 6.2, but this is not required for this PR to be useful.

Copy link
Contributor

@drupol drupol left a comment

Choose a reason for hiding this comment

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

Cool initiative!

Copy link
Contributor

@heiglandreas heiglandreas left a comment

Choose a reason for hiding this comment

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

I doubt that PSR-20 is going to be ready by the release of Symfony 6.2. Though in the meantime there is the forward-compatible stella-maris/clock interface that can be used and already is implemented by some of the most used clock-implementations.

With that in mind I'd think about - for the sake of interface segregation - removing the now function completely from the TimeInterface (and perhaps also moving the sleep function into a separate interface - we've discussed that during the PSR-20 discussions IIRC). The different implementations can then still implement the different interfaces in one class. And should more than one interface be required in a method we can by now require an implementation via a UnionType.

/**
* @author Nicolas Grekas <[email protected]>
*/
class MockClock implements TimeInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of calling the class by what it should be used for it might make more sense to call the class by what it actually does. In this case that would be a FrozenClock which freezes the point in time to a specific one.

Or to multiple specific ones when the constructor also accepts an array of DateTimeImmutables.

A possible further Implementation might then be a FreezableClock that can be frozen multiple times using @theofidry's idea via a freeze method.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Jun 20, 2022

Choose a reason for hiding this comment

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

Instead of calling the class by what it should be used for it might make more sense to call the class by what it actually does. In this case that would be a FrozenClock which freezes the point in time to a specific one.

I prefer keeping MockClock. I think the name kinda says both what it might be use for and what it does, thus providing better discoverability (and is consistent with "MockHttpClient".)

Or to multiple specific ones when the constructor also accepts an array of DateTimeImmutables.

now() would then return dates from that sequence? Interesting idea :)

A possible further Implementation might then be a FreezableClock that can be frozen multiple times using @theofidry's idea via a freeze method.

Agreed. I might wait for someone that needs that to submit a PR instead of providing it in this initial PR though.

Copy link

@eerison eerison Dec 2, 2023

Choose a reason for hiding this comment

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

Hey @nicolas-grekas

I saw that your first MockClock version wasn't final, But I can see that all class are final, what made you add classes as final?

I ask this because I see others symfony's classes implementing an interface But those aren't final.
for example: https://github.com/symfony/serializer/blob/7.0/Encoder/JsonEncode.php#L21

is there any reason to make those clock classes as final and in others repositories not?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's subtle and certainly never a black and white decision. Why are you asking?

Copy link

Choose a reason for hiding this comment

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

Because I see some open source projects closing their classes for extensions and force the dev implement the interface, or maybe decarorate the classes in case they want to reuse some existing code. And as far as I know it is to help maintainers handle BC easily, because the dev won't extend classes.

But why symfony doesn't close the api and make the dev always implement the interface?

And as this clock classes are different of others, I thought it has a different reason for this.

BTW, thank you for your time in replying me, I really appreciate that.

Copy link
Member Author

Choose a reason for hiding this comment

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

In Symfony we're not dogmatic on the topic. I think we learned that from a maintenance perspective, making classes final makes it easier to evolve them without too much BC concerns. But our core desire is to empower end users, and "final" comes into the way sometimes so we're open to reconsidering. As for why some classes are not final: history for some, and openness to inheritance for others.
The open/closed principle is a balanced one.

Copy link

Choose a reason for hiding this comment

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

So nice ♥️

But our core desire is to empower end users.

Do you mean give them more flexibility?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I mean more flexibility to achieve what they need to achieve, and some framework to guide them in the process :)

Copy link

Choose a reason for hiding this comment

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

This vision is so nice, because even you know that closes classes are easier for maintaining, but symfony prefer give flexibility for users, even if it costs more work from symfony side 🤯♥️

Well thank you again for your time

@drupol
Copy link
Contributor

drupol commented Jun 20, 2022

With that in mind I'd think about - for the sake of interface segregation - removing the now function completely from the TimeInterface. The different implementations can then still implement the different interfaces in one class. And should more than one interface be required in a method we can by now require an implementation via a UnionType.

Hi Andreas,

Why would we want to remove the now method? The whole point of this component is actually to have such method.
Could you please develop?

@nicolas-grekas nicolas-grekas force-pushed the clock branch 2 times, most recently from 9d304cf to b8694fe Compare June 20, 2022 07:42
@nicolas-grekas
Copy link
Member Author

I doubt that PSR-20 is going to be ready by the release of Symfony 6.2.

🤷 hurry up PHP-FIG :)

for the sake of interface segregation - removing the now function completely from the TimeInterface (and perhaps also moving the sleep function into a separate interface - we've discussed that during the PSR-20 discussions IIRC).

I'd better not as I explained in #46715 (comment)
If we end up being incompatible with PSR-20, we'll deal with it with a deprecation layer.
But looking at the state of PSR-20, it looks unlikely that we'll have to do that.

@nicolas-grekas
Copy link
Member Author

Thanks for the review btw @heiglandreas !

@heiglandreas
Copy link
Contributor

heiglandreas commented Jun 20, 2022

With that in mind I'd think about - for the sake of interface segregation - removing the now function completely from the TimeInterface. The different implementations can then still implement the different interfaces in one class. And should more than one interface be required in a method we can by now require an implementation via a UnionType.

Hi Andreas,

Why would we want to remove the now method? The whole point of this component is actually to have such method. Could you please develop?

The now-method would be already defined in a ClockInterface - like psr/clock or stella-maris/clock, so defining it here again in a TimeInterface is not really necessary. The actual Implementation then would implement TimeInterface, ClockInterface, SleepInterface.

So the component would either require an external ClockInterface or declare a spearate ClockInterface in parallel to the current TimeInterface and a currently not existing SleepInterface.

@nicolas-grekas
Copy link
Member Author

The now-method would be already defined in a ClockInterface - like psr/clock or stella-maris/clock, so defining it here again in a TimeInterface is not really necessary. The actual Implementation then would implement TimeInterface, ClockInterface, SleepInterface.

I want the component to be standalone and complete on its own. When PSR-20 will be ready, we might extend it, but I wouldn't make a stable PSR-20 a requirement to this component.

So the component would either require an external ClockInterface or declare a spearate ClockInterface in parallel to the current TimeInterface and a currently not existing SleepInterface.

Replying in the main thread to help readers follow the discussion: see #46715 (comment) about this topic.

Copy link
Member

@chalasr chalasr left a comment

Choose a reason for hiding this comment

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

Cool

@nicolas-grekas nicolas-grekas force-pushed the clock branch 2 times, most recently from 0d67d50 to 8a65085 Compare June 20, 2022 13:11
@javiereguiluz
Copy link
Member

Thanks for this proposal!

In the description we see this:

timeInt(): float returns the current timestamp;

I guess it's a typo that timeInt() returns a float instead of int.


If the naming of the class/methods is up for debate, here's a proposal for your consideration:

// Before
interface TimeInterface
{
    public function now(): \DateTimeImmutable;
    public function sleep(float|int $seconds): void;
    public function timeInt(): int;
    public function timeFloat(): float;
    public function timeArray(): array;
}

// After
interface ClockInterface
{
    public function now(): \DateTimeImmutable;
    public function sleep(float|int $seconds): void;
    public function timestamp(): float;
}

I'd remove timeInt(), timeFloat() and timeArray() ... but you said that they are important for performance reasons. So, why not merging all of them into a single method called timestamp() which returns a timestamp with microseconds precision. It'd be equivalent to timeFloat() ... and the timeInt() value can be obtained as (int) $clock->timestamp(). About the timeArray() method, I don't understand well its need.

Thanks.

@nicolas-grekas nicolas-grekas force-pushed the clock branch 2 times, most recently from 605f167 to ff18540 Compare June 24, 2022 10:00
@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Jun 24, 2022

Brain activity at night made me realize that we could remove the timeInt() and timeArray() variants and keep only time(), provided time() returns a string. As a reminder, the concern this addresses is loss-less time measurements on 32-bit archs. This is the same concern that leads to having microtime(false) also return a string.

Then I did my homework and tried to verify my own claim about performance, which is the justification I've provided for these time*() methods.

Comparing new \DateTimeImmutable('now', $tz) to microtime(true), the latter is almost 5x faster.
But when comparing new \DateTimeImmutable('now', $tz) to (string) microtime(true), the latter is only 1.1x faster!

This difference is not significant enough to warrant my claim. I'm thus removing all time*() methods from my proposal. Performance-critical time measurements might continue to use ClockMock since we can't abstract time measurements without adding overhead to it. Note that using the component for not-so-critical performance measurements is still valid, because we're talking about quite fast measurements.

Since this means the interface now contains only now() and sleep(), I've renamed it to SleepableClockInterface.

I've also implemented the offset logic @derrabus suggested for HrClock, which is now renamed MonotonicClock (because it's not "high-resolution" anymore, see notes about the overhead above.)

I updated the PR and the description above with all those changes.

I think this addresses most if not all concerns raised so far.

The last thing that remains is the lack of a ClockInterface, and that's on the PHP-FIG 🙏

@nicolas-grekas nicolas-grekas force-pushed the clock branch 2 times, most recently from e35de12 to 2876ada Compare June 24, 2022 10:10
@lchrusciel
Copy link
Contributor

I'm looking forward to the newest PSR-20 and this component! At Sylius, we've recently introduced the Calendar component, but we will surely try to migrate to the use yours. Our use case mainly decouples implementation from new in the code and provides ease-of dates testing - we need to freeze the clock and set its time for some given moment. If you are open to SettableClock implementation and interface, I would love to try it.

@nicolas-grekas
Copy link
Member Author

we need to freeze the clock and set its time for some given moment. If you are open to SettableClock implementation and interface, I would love to try it.

Sure we are if this proves needed :)

Copy link
Member

@derrabus derrabus left a comment

Choose a reason for hiding this comment

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

I like the component in its current state. Nice and simple. Let's see how it works for us when we dogfood it in other components.

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Jul 21, 2022

After reading php-fig/fig-standards#1257 I'm wondering if it wouldn't make sense to add withTimeZone()?

That'd allow registering only one clock service and give a contract to users to stick to a TZ.

Loosely related, I'm wondering if we shouldn't swap our approach to naming and add ClockInterface to the component. That'd be the only interface provided by the component.

The benefit of this approach is that it would make us less dependant on the outcome and pace of the FIG.

My preference goes to doing both changes I propose here: one capable ClockInterface in the component and one more specific in PSR20 when it'll be released.

@nicolas-grekas nicolas-grekas force-pushed the clock branch 2 times, most recently from 001bb63 to 34b6c13 Compare July 27, 2022 12:36
@nicolas-grekas
Copy link
Member Author

PR updated with the approach proposed in my previous comment:
The component now provides a ClockInterface with 3 methods: now(), sleep() and withTimeZone().

I also registered a clock service into FrameworkBundle and a corresponding autowiring alias for ClockInterface.
Since we now have withTimeZone(), there is no need to provide several clock services (an UTC one and a default-TZ one.)

When PSR20 will be out (and if it remains compatible with the component), ppl will have the choice to use either the generic and narrow abstraction from the FIG - or the more capable one from the component when in need of extra features.

/cc @symfony/mergers even if you already voted, please vote again if you approve these changes.

Copy link
Member

@yceruto yceruto left a comment

Choose a reason for hiding this comment

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

I loved it!

@fabpot
Copy link
Member

fabpot commented Jul 28, 2022

Thank you @nicolas-grekas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.