0%

Capstone: A Practice Report Tool · practice

Freshness Needs One Injected Time

The cache now says when it was fetched. Deciding whether that is recent enough sounds like a one-line comparison, and it nearly is. The interesting part is where the current time comes from.

The obvious version, and why it hurts

Here is what most people write first:

def cache_is_stale(fetched_at, max_age):
    return int(time.time()) - fetched_at > max_age

It works. Now try to test it.

You want to check that a cache which is one second past its limit counts as stale. So you need time.time() to return a particular . But it returns now, and now moves. You could write the cache with a timestamp calculated backwards from the current time, but then your test is doing arithmetic to compensate for a clock, and any test that has to compensate for something is a test that can drift.

You could make the test sleep. Now your suite is slower on purpose and still not exact.

The is not hard to test because testing is hard. It is hard to test because it reaches out and grabs a value nobody gave it.

Ask for the time instead

Make the current time a :

def cache_is_stale(fetched_at, now_epoch, max_age):
    ...

That is the entire technique. It has a name, dependency injection, which sounds much grander than what it is: pass the thing in rather than fetching it yourself.

Now a test says exactly what it means. Cache written at 100, it is now 110, the limit is 10 seconds. Is that stale? No: 110 minus 100 is 10, which is not more than 10. What about at 111? Yes, by one second. Both cases run instantly and give the same answer forever.

Where the clock actually gets read

Something has to call time.time() eventually. The rule is that it happens once, high up, at the layer that orchestrates:

if now_epoch is None:
    now_epoch = int(time.time())

The orchestrating function samples the clock once when nobody supplied a time, and passes that single value down through everything else. Tests supply now_epoch and the clock is never touched.

Sampling once matters more than it looks. If three different functions each call time.time(), they get three slightly different answers, and a program can decide a cache is fresh in one place and stale in another during the same run. One read, one value, passed down.

Decide the boundary out loud

“Fresh for an hour” has an edge case, and vague code makes a silent choice about it. Say it explicitly instead:

  • age less than max_age: fresh

  • age exactly equal to max_age: fresh

  • age one second more than max_age: stale

So the comparison is age > max_age, not >=. This is the same > versus >= decision you debugged in Chapter 9, which should tell you something about how often it comes up.

A timestamp from the future is not fresh

One case remains, and the tempting answer is wrong.

What if fetched_at is later than now_epoch? The age is negative. Negative is certainly less than max_age, so a naive comparison calls it fresh, and a cache with an invalid timestamp can be used until that future timestamp plus max_age has passed.

A future timestamp is not fresh data. It is broken data: a clock changed, a file was edited, something went wrong. Raise and let the caller treat it as an invalid cache and refetch.

All three values must be genuine non-negative integers, with the type(value) is int check from the previous lesson, because True is still an integer as far as isinstance is concerned.

Your turn

Implement cache_is_stale(fetched_at, now_epoch, max_age) in practice_report.py.

Return False for fresh, True for stale, and raise ValueError for any value that is not a non-negative integer, or when fetched_at is in the future.

Do not call time.time() inside it. Do not look at file modification times. The three numbers you were given are the whole input.

Why does cache_is_stale take now_epoch as a parameter instead of calling time.time()?

A cache says it was fetched 60 seconds from now. What should cache_is_stale do?

Why does the orchestration layer read the clock once and pass the value down?

Task

Add the exact injected freshness boundary and cover epoch zero, exact age, one-second stale, future time, and adversaries without sleeping.