Skip to content
C.W.K.
Stream
Lesson 02 of 04 · published

One Loop, Three Markets, One Window

~12 min · scheduler, asyncio, simplicity, operations

Level 0Raw Ore
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
The obvious design here is three schedulers. The right one is one, and the reason is a property of the data rather than of the code.

The problem, stated the tempting way

Three markets in three timezones. New York closes, then Seoul opens, then Tokyo, and none of them agree on what today is. The instinctive design writes itself: one scheduled job per market, each firing after its own close, each with its own holiday calendar.

Now count what you have signed up for. Three schedules to keep in sync as daylight saving shifts one of them twice a year. Three holiday calendars, two of which you will get wrong at least once. Three failure surfaces. Three places to look when a number did not update. And a new class of question that did not exist before: what happens when two of them overlap?

Why one window is enough

The unlock is not clever scheduling. It is a property established elsewhere in the design: each snapshot row carries its own data-date, asked of the provider. Once that is true, the run time stops being load-bearing.

One pass at one quiet hour asks every source for its latest reading. A market that has already closed contributes today's close. A market that is mid-session, or has not opened, contributes its previous close — correctly labelled with that previous date, because the label comes from the market rather than from the clock the loop happens to be running on. The append-only store deduplicates naturally: tomorrow's pass sees the same row for a market that has not moved, and a market that has moved gets a new dated row.

So the loop does not need to know when any market closes. It only needs to run at an hour when asking is useful, which is a much weaker requirement — and one that a single number in a config file satisfies.

Make the data carry the fact, and the schedule stops mattering. A great deal of scheduling complexity exists to make sure a job runs at the moment when the answer will be right. If each answer states which moment it belongs to, the job can run whenever, and an entire category of timing bugs becomes unreachable.

In-process, and never a second one

The loop is a single asyncio task inside the serving process — not a cron entry, not a separate service. That is family doctrine, and it buys three concrete things: the loop shares the engine's single writer so there is no cross-process write coordination; it can read live application state; and it cannot drift out of sync with the code it schedules, because it ships in the same process.

The maintenance slot rides the same pass: after the refresh, the database backup rotation runs, then the log size cap. Ordering matters and is deliberate — backups run after the refresh so the day's rows are inside the day's backup.

Failures must not kill the loop. Each pass writes its own summary into stored state, and neither the refresh nor the maintenance step raises out of the loop. A crashed loop is the worst outcome available: it takes down tomorrow's data too, and it does it silently. A survived-but-failed pass shows up as a number that stopped advancing, which is visible in the one place a user is already looking.

Code

The whole scheduling primitive — one target time, one timezone·python
_TZ = zoneinfo.ZoneInfo("Asia/Seoul")


def seconds_until_next_run(
        now: datetime.datetime | None = None) -> float:
    now = now or datetime.datetime.now(_TZ)
    target = now.replace(hour=settings.daily_refresh_hour,
                         minute=settings.daily_refresh_minute,
                         second=0, microsecond=0)
    if target <= now:
        target += datetime.timedelta(days=1)
    return (target - now).total_seconds()


# Default 07:11 Asia/Seoul: safely after the US close in both DST and
# winter, before the KR open. That is the ENTIRE market-timing logic
# in the product.
#
# What is NOT here, because rows are dated by the market:
#   - three exchange calendars
#   - per-market holiday tables
#   - DST transition handling for New York and Tokyo
#   - overlap resolution between concurrent per-market jobs

External links

Exercise

Find a scheduled job in your own system that has to run at a particular moment for its output to be correct. Ask whether the output could instead carry a field stating which moment it belongs to. If it could, work out what would then be deletable: retry windows, calendar tables, ordering constraints between jobs. Most timing complexity is a workaround for data that does not state its own time.
Hint
The symptom is a job whose correctness you can only verify by knowing when it ran. If you have to consult a schedule to interpret a row, the row is under-specified — and every consumer of it inherits that ambiguity.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.