How to do clocks right? #4891
Replies: 3 comments 11 replies
-
Nice summary! An additional observation: So no clock is perfect, and sometimes we have to choose between testability and usability with the std concurrency library. On a positive note, if we accidentally use a non-Cpp17Clock with the concurrency library, it is a compile-time error, not a run-time error. And we now have a trait to test for the Cpp17Clock requirements: |
Beta Was this translation helpful? Give feedback.
-
In general I like being explicit with all the inputs (because that makes automated testing easier to manage) but you are right that for the clock it would be excessive. Putting it in a thread local static seems sensible to me. |
Beta Was this translation helpful? Give feedback.
-
Why not just a volatile word-aligned uint64_t that's updated by one thread, and can be read by any thread at any time? You get atomicity for free here. Suppose the updating thread writes values X-1, X, X+1 in sequence. A reading thread might get X-1, X, or X+1 depending on how it's scheduled, but it will get one of those values. And for a clock this is fine? Am I missing something? |
Beta Was this translation helpful? Give feedback.
-
I want to discuss how we handle clocks. First, we have
beast::abstract_clock
, which is like astd::chrono
clock except thatnow()
is a non-static member function instead of a static member function. The idea is that components that need a clock will take a reference to abeast::abstract_clock
, which can be chosen at runtime, instead of calling a global, static function likestd::chrono::steady_clock::now()
, that can't be switched at runtime.There is a subtype of
beast::abstract_clock
calledbeast::manual_clock
that lets a caller manually advance the clock. This capability is required for effectively testing components that depend on a clock, much like the ability to choose a seed is required for effectively testing components that depend on randomness. It is used in many of our tests, e.g. for all the aged containers, which take abeast::abstract_clock&
in their constructor.However, there is also a global function,
beast::get_abstract_clock()
, that returns a non-const reference to a statically-allocatedbeast::abstract_clock
. This function is called in several places, e.g. the constructor ofNetworkOpsImp
, which passes it toRCLConsensus
. These calls cannot be intercepted to use a manual clock, which means the components using them cannot be tested effectively. (There are many more reasons whyNetworkOpsImp
is not testable in its current state, but it's not the only example, and right now I'm focused on removing this obstacle.)In fact, there are three calls of
beast::get_abstract_clock()
insrc/ripple
, one of which is in wrapperripple::stopwatch()
, which itself has 23 more calls. There are even more direct calls into standard library clocks, e.g.std::chrono::system_clock::now()
. Some are through type aliases, which makes them difficult to count.Am I perceiving this situation correctly? That our direct calls to
beast::get_abstract_clock()
andstd::chrono::{system_clock,steady_clock,high_resolution_clock}::now()
make our components untestable?If it is, then I see two ways to fix it:
beast::abstract_clock
the way the aged containers do. This is not my preferred approach. It is parameter drilling, which I hate, and it would expand the blast radius to more components than the next method, which I prefer.But it's not just our components we need to worry about. We need to be able to give manual clocks to all of our time-sensitive dependencies. The only one that comes to mind is Boost.Asio, but it is everywhere. I found an explanation from the creator on how to define and pass a manual clock for
boost::asio::basic_deadline_timer
, but we don't use that. We useboost::asio::basic_waitable_timer
, which accepts a different traits type parameter whose relationship to the one explained in the blog post is unclear to me.Thoughts?
Beta Was this translation helpful? Give feedback.
All reactions