#!/usr/bin/env python3
"""Measuring a duration with the wall clock can go NEGATIVE; the monotonic clock can't. DDIA 2e Ch. 9.

A duration is `elapsed = end - start`. Read `start` and `end` from a time-of-day
clock (System.currentTimeMillis / time.time()) and the number is only meaningful
if nothing moved the clock in between. But a time-of-day clock is a SYNCHRONIZED
value: NTP can step it BACKWARD at any moment to correct drift. Step it backward
mid-measurement and `end - start` comes out NEGATIVE -- and a lease/timeout check
built on that duration silently mis-fires (a "10 s remaining" lease looks
un-expired forever).

A monotonic clock (time.monotonic, CLOCK_MONOTONIC) answers a different question
-- "how much time has passed" -- from a local counter with no external authority,
so it is guaranteed never to go backward. Its absolute value is meaningless, but
its DIFFERENCE is always a valid duration: an NTP step is simply invisible to it.

This script measures the SAME modeled interval with BOTH clocks:
  - 2 s of work elapses (both clocks advance 2 s);
  - NTP then steps the time-of-day clock BACK 5 s -- the monotonic clock has no
    external authority to jump, so it ignores the step.
Both clocks are modeled deterministically, so the whole transcript reproduces
byte-for-byte every run.

Pure standard library.
"""

# One modeled interval, timed by both clocks.
WORK_SECONDS = 2.0
NTP_STEP_BACK = 5.0        # a correction that pulls the time-of-day clock BACKWARD 5 s
LEASE_SECONDS = 10.0       # a lease/timeout we will check the elapsed against


class WallClock:
    """A mock time-of-day clock.

    `time()` reads the current wall-clock value. It advances when work happens
    (advance()), but an external authority -- NTP -- can STEP it in either
    direction with step(). Stepping it backward models the correction that makes
    elapsed-time measurement go wrong. Absolute value here is a Unix timestamp,
    exactly what time.time() returns.
    """

    def __init__(self, now):
        self._now = now

    def time(self):
        return self._now

    def advance(self, seconds):
        # Real time passing: work happened, the clock moved forward.
        self._now += seconds

    def step(self, seconds):
        # An NTP correction: the external authority jumps the clock. A negative
        # argument pulls it BACKWARD -- the clock now reports an earlier instant.
        self._now += seconds


class MonotonicClock:
    """A mock monotonic clock.

    `time()` reads a local counter of elapsed ticks from an arbitrary origin
    (uptime, say) -- so its absolute value is meaningless, you cannot read civil
    time off it. It advances with real work (advance()), but step() is a NO-OP:
    a monotonic clock has no external authority, so an NTP correction cannot jump
    it. That is exactly why its DIFFERENCE is always a valid duration.
    """

    def __init__(self, now):
        self._now = now

    def time(self):
        return self._now

    def advance(self, seconds):
        self._now += seconds

    def step(self, seconds):
        # An NTP correction reaches the time-of-day clock, never this one. The
        # monotonic clock only ever counts up; the step is invisible to it.
        pass


def measure(clock, label):
    """Time the identical modeled interval with whichever clock is passed in.

    start -> 2 s of work -> NTP steps the clock back 5 s -> end.
    The wall clock obeys the step; the monotonic clock ignores it.
    """
    start = clock.time()
    print(f"{label}:")
    print(f"    start = clock.time()            -> {start:.3f}")

    clock.advance(WORK_SECONDS)  # 2 s of real work elapses -- both clocks move
    print(f"    ... {WORK_SECONDS:.0f} s of work happens, clock advances to {clock.time():.3f}")

    before_step = clock.time()
    clock.step(-NTP_STEP_BACK)   # NTP decides the clock is 5 s fast, yanks it back
    stepped = clock.time()
    if stepped == before_step:
        print(f"    ... NTP steps BACK {NTP_STEP_BACK:.0f} s -> IGNORED, clock stays {stepped:.3f}")
    else:
        print(f"    ... NTP steps the clock BACK {NTP_STEP_BACK:.0f} s -> {stepped:.3f}")

    end = clock.time()
    elapsed = end - start
    print(f"    end   = clock.time()            -> {end:.3f}")
    print(f"    elapsed = end - start           -> {elapsed:+.3f} s")
    return elapsed


def lease_check(elapsed, label):
    """A lease/timeout decision built on a measured duration.

    Real code everywhere: "if more than LEASE_SECONDS have elapsed, the lease is
    expired -- give it up." The check is only as trustworthy as `elapsed`.
    """
    expired = elapsed > LEASE_SECONDS
    verdict = "EXPIRED, release it" if expired else "still valid, KEEP holding"
    print(f"    lease check ({label}): elapsed {elapsed:+.3f} s > {LEASE_SECONDS:.0f} s ? "
          f"{expired}  -> {verdict}")
    return expired


def main():
    print("Measuring one modeled interval with two clocks. A duration is")
    print("elapsed = end - start; it must always be >= 0. Watch the wall clock")
    print("violate that while the monotonic clock times the same interval correctly.\n")

    # Wall clock: a plausible Unix timestamp (2024-05-15 ~12:00 UTC).
    wall = WallClock(now=1_715_774_400.0)
    wall_elapsed = measure(wall, "wall clock (time-of-day, mock so the NTP step reproduces)")
    print()

    # Monotonic clock: an arbitrary tick count (uptime-since-boot), not wall time.
    mono = MonotonicClock(now=918_273.000)
    mono_elapsed = measure(mono, "monotonic clock (mock, no external authority -- ignores NTP)")
    print()

    print("the same lease check on each measured duration:")
    lease_check(wall_elapsed, "wall clock")
    lease_check(mono_elapsed, "monotonic ")
    print()

    print("verdict:")
    print(f"    wall-clock elapsed = {wall_elapsed:+.3f} s  "
          f"({'NEGATIVE -- impossible for a real duration' if wall_elapsed < 0 else 'ok'})")
    print(f"    monotonic  elapsed = {mono_elapsed:+.3f} s  "
          f"({'>= 0, the true 2 s interval' if mono_elapsed >= 0 else 'NEGATIVE'})")
    if wall_elapsed < 0:
        print("    a negative duration makes 'elapsed > lease' false forever: the")
        print("    lease never looks expired, so the holder never releases it.")


if __name__ == "__main__":
    main()
