Skip to content
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

Fake time, using a time/capsule #141

Open
onetom opened this issue May 30, 2021 · 9 comments
Open

Fake time, using a time/capsule #141

onetom opened this issue May 30, 2021 · 9 comments

Comments

@onetom
Copy link

onetom commented May 30, 2021

Problem

In automated tests, it's desirable to control time. It is achieved by something
typically called a fake clock.
The Use a fake system clock
article enumerated the expected behaviours of such a clock:

  1. skip ahead to the future
  2. go back to the past
  3. use a fixed date, and a fixed time
  4. use a fixed date, but still let the time vary
  5. increment by one second each time you 'look' at the clock
  6. change the rate at which time passes, by speeding up or slowing down by a certain factor
  7. use the normal system clock without alteration

According to your needs, you may have to use the fake system clock in some or all of these places:

  • your application code
  • your code that interacts with the database
  • your logging output
  • your framework classes

These lists completely matched my expectations and felt common sense to me.
To my surprise, only few of these use-cases were supported out of the box, by
java.time.Clock or this juxt/tick library. I don't see how can I achieve
use-case 4, 5 and 6.

java.time.Clock/fixed solves use-case 1, 2 and 3. tick.core/AtomicClock
kinda solves use-case 4, but at the cost of introducing custom variants of
clojure.core/atom,{reset,swap}{,-vals}!,compare-and-set!. The mentioned
article demonstrates a solution to use-case 5, by a custom implementation of
j.t.Clock.

I've found a Java implementation of a MutableClock
It solves use-case 4, but not 5. It's also introducing extra, custom API
(set, setInstant) for changing the clock; typical parochialism. Its
implementation is complected with concerns like serialization, and the way the
clock might be changed. Neither of these are very desirable in Clojure context.
This MutableClock is also a bit obscure, because I haven't found it mentioned
in higher-level documentations, only in the auto-generated Java docs. I don't
feel confident using it and pulling in this extra library, just for this
simple concept, despite it's authored by the same @jodastephen, who is the
shepherd of java.time and JSR-310.

Solution

To cater for use-case 3 and 4, while maintaining a convenient and idiomatic use
for use-case 7, I propose constructing a time-capsule concept. We can imagine
this capsule having a clock-stand, which holds a concrete clock. This clock
might be standing still at a time we specify, or ticking at the same, or
a different rate as the system clock. During tests, we can replace the clock on
this imaginary stand, with a different one, which might be derived from the one
already on the stand.

From an application's point of view, this capsule should just look like any
other j.t.Clock. Since j.t.Clock is just an abstract class, not a Java
interface, we cannot extend it conveniently from Clojure, without providing a
full-blown implementation for it and getting sucked into OO-land.

Instead, we can treat clocks as something derefable, just like t/AtomicClock
does, to conveniently take a reading of their current time. To adjust the clock
though, we need access to both the clock and the stand itself, to swap!
it, so references to the capsule won't be affected.

Here is a possible implementation, for Clojure-only, for the sake of clarity:

(ns xxx.time
  (:require
    [tick.alpha.api :as t]
    [tick.protocols :as p]))

(defrecord Capsule [clock-stand]
  clojure.lang.IDeref
  (deref [_] (t/instant @clock-stand))

  clojure.lang.IAtom
  (swap [_ f] (swap! clock-stand f))
  (swap [_ f x] (swap! clock-stand f x))
  (swap [_ f x y] (swap! clock-stand f x y))
  (swap [_ f x y args] (apply swap! clock-stand f x y args))

  p/IClock
  (clock [_] @clock-stand))

(defn capsule [clock-like]
  (->Capsule (atom (t/clock clock-like))))

I've also omitted reset!, reset-vals! and swap-vals! for the sake of
simplifying discussion, by focusing on the core idea.

When I tried to print a time/capsule, I got an error about multi-method
ambiguity, which I resolved with:

(prefer-method print-method java.util.Map clojure.lang.IDeref)

I don't have a lot of experience with hierarchies in Clojure, so I'm not sure
how to avoid this ambiguity, or what's better to prefer. I should probably just
provide a (defmethod print-method Capsule) (and its pprint variant), but
I'm not sure yet, what should the implementation look like.

Here is a demonstration of using a time/capsule:

(defn test-capsule [clk]
  (let [cclk (xxx.time/capsule clk)]
    {:capsule cclk
     :time    @cclk
     :clk     (t/clock cclk)
     :new-clk [(swap! cclk t/>> (t/new-duration 5 :minutes))
               (t/clock cclk)]}))

(comment
  (test-capsule (t/clock))
  (test-capsule (t/instant "1000-02-03T04:05:06Z"))
  )

Pros:

  1. convenient programming interface, mimicking a j.t.Clock implementing
    IDeref, as opposed to an (atom (Clock/system.)), which would require
    (t/instant @clk-holder) to be read, or maybe @@clock-holder somehow.
  2. no need for specialized atom operations
  3. no need to drop down to Java-level

Cons:

  1. the value passed by swap! to its transformation function, is the clock,
    which is different from the value we get with deref, which is the time on
    that clock.
  2. properly implementing all methods for IAtom, IAtom2 and the ClojureScript
    IReset and ISwap protocols is quite a lot of boilerplate
  3. wrapping an atom in a record feels superfluous, when an atom is almost
    suitable for our needs
@onetom
Copy link
Author

onetom commented May 30, 2021

In case it's not clear from the main description, (swap! test-clk t/>> (t/new-duration 5 :minutes)) would be the way to change the time within automated tests. I would rather write it as (swap! test-clk t/>> 5 :minutes), but that's an orthogonal issue.

@onetom
Copy link
Author

onetom commented May 30, 2021

Another thing I haven't made clear probably is that my proposal would be the replacement of tick.core/AtomicClock. While it's a funny name, fusing the Clojure atom concept with the java.time.Clock; atomic clock also has a well-defined meaning in the problem-domain of time and I find it misleading. My first impression was that it has something to do with NTP and obtaining some hyper-precise time. That's why I came up with the time-capsule mental-model. I specifically thought of this visualization of a possible warp-drive: https://en.wikipedia.org/wiki/Alcubierre_drive

@onetom
Copy link
Author

onetom commented May 30, 2021

I also forgot to mention the t/with-clock facility, which is described here: https://www.juxt.land/tick/docs/index.html#_substitution

While t/with-clock solves the problem for a single thread, using dynamic vars is trickier in multi-threaded situations. For example, in an integration test, where we spin up a temporary http-kit server for our API on a random port and talk to it via a http-kit client, we are going to deal with multiple threads. Of course, it's questionable, whether we should use fake clocks in such a test, but I found it useful for exploratory, system modelling work.

@henryw374
Copy link
Collaborator

Nice problem statement!

I agree that with-clock has issues, but they are part of the java.time/tick approach to getting the now time - you can choose to pass a clock around, or just use the ambient clock. having a *clock* binding, tick makes the ambient-clock route even more tempting. Still, I think for a lot of use cases that works ok.

Is the intent for the capsule to be referred to explicitly by any code that needs now?

FYI for another reference point... and something will make it into tick eventually, is https://tc39.es/proposal-temporal/docs/index.html#Temporal-now - which serves the same function as java.time Clock. In https://github.com/henryw374/tempo I'm looking at making an api for 'clocks' that works with Temporal.now and java.time.Clock

@henryw374
Copy link
Collaborator

fyi mutable clock in clojure https://gist.github.com/henryw374/2291e787087eeea513f9a8e5a5bd6f69

@onetom
Copy link
Author

onetom commented Oct 4, 2024

is there any significance of using mutable_clock.MutableClock, instead of just MutableClock in the proxy call?

@henryw374
Copy link
Collaborator

good question. I don't think so.... I tried just now without and it seems to work ok. If I had a reason it has been forgotten as this was a few months ago. Also looking at this again, it would be good to be able to atomically set zone and instant. maybe it should just be a zoned-date-time.

Circling back to the capsule idea... I am leaning towards preferring to create instances of java.time.Clock (or in js-land js/Temporal.Now) as these things work with the native APIs (just constructor fns afaik).

I think 1-6 can still be addressed with that constraint. but pls let me know otherwise.

and if so... I guess having a mutable clock in the lib would be handy. and anything more esoteric can be done in userspace.

btw in Tempo there are no zero-args 'now' fns - you have to provide a clock. I've come to prefer working that way with tick - ie not using with-clock at all.

@onetom
Copy link
Author

onetom commented Oct 4, 2024

Very exciting!

In our app we have a :clk component in our system map and everything relies on it explicitly.

We inject it into our Datomic components too, so even the db schema can be transacted in the past virtually, so we can transact theoretical past scenarios in our tests (because Datomic doesn't allow future transactions).

The only thing we couldn't control the clock of is java.net.CookieManager, which just calls System/currentTimeMillis directly in its cleanup routine, which throws away expired cookies, we couldn't test session expiry code in full integration over a web server.

I haven't explored the Tempo lib yet, but we have ended up not really utilizing the cross-platform nature of tick, so I might consider falling back to Tempo, to reduce complexity.

+1 for using java.time.Clock, if possible, for better interop.

I'll try to review this thread in the coming ~2weeks, to give some feedback.

@henryw374
Copy link
Collaborator

another clock implementation https://gist.github.com/henryw374/38d61b20c62cd5450331f36ee9029a61

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

No branches or pull requests

2 participants