Skip to content
About
Main content

Watermark with an idle partition

Simulate a modelNeeds Python

Problem

A global is the minimum of the ' watermarks, so one partition that stops sending holds every open: the system cannot know whether the quiet partition has old events still to come. An excludes a quiet partition from the minimum so that windows close, which is correct only if the partition really has nothing old to send; if it comes back with a backlog, those events arrive after their windows closed.

Write a small simulation of 3 partitions where one stops sending. Compute the global watermark as the minimum, and record when windows close. Add an idleness timeout (2 minutes) and record what changes. Then make the idle partition send its old records after the timeout and count how many become late.

The question. How long do windows wait while a partition is idle, and how many of its events become late once a timeout lets windows close?


Predict

With a 2-minute idleness timeout, how many of the idle partition's events arrive after their windows have closed?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    watermark-with-an-idle-partition.py53 linesDownload
    View the code
    watermark-with-an-idle-partition.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """A watermark with an idle partition.
    
    Three partitions send one event per second each, with up to 0.2 minutes of
    skew. Partition 3 goes quiet at minute 10; at minute 20 it sends the events it
    held back (event times from minute 10 to 20), then carries on. The global
    watermark is the minimum of the partition watermarks (max event time seen
    minus a delay). Records how late each 1-minute window closes, without an
    idleness timeout and with one, and counts events that arrive after their
    window closed.
    """
    import argparse, json, random
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--minutes", type=int, default=30, help="Simulated minutes.")
    p.add_argument("--idle-at", type=int, default=10, help="Minute at which partition 3 goes quiet.")
    p.add_argument("--resume-at", type=int, default=20, help="Minute at which partition 3 sends its backlog.")
    p.add_argument("--timeout", type=float, default=2.0, help="Idleness timeout (minutes).")
    p.add_argument("--delay", type=float, default=0.5, help="Watermark delay (minutes).")
    p.add_argument("--seed", type=int, default=0, help="Random seed.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    def run(timeout):
        rng = random.Random(args.seed)
        seen = {0: 0.0, 1: 0.0, 2: 0.0}; last = {0: 0.0, 1: 0.0, 2: 0.0}
        closed = 0; late = 0; delays = []; backlog_sent = False
        for s in range(args.minutes * 60):
            now = s / 60; arrivals = []
            for part in (0, 1, 2):
                if part == 2 and args.idle_at <= now < args.resume_at: continue                 # quiet
                arrivals.append((part, max(0.0, now - rng.uniform(0, 0.2))))
            if now >= args.resume_at and not backlog_sent:                                       # the held-back events, all at once
                arrivals += [(2, args.idle_at + i / 60) for i in range((args.resume_at - args.idle_at) * 60)]; backlog_sent = True
            for part, ev in arrivals:
                if ev < closed: late += 1                                                        # its window has already closed
                seen[part] = max(seen[part], ev); last[part] = now
            active = [seen[p_] for p_ in seen if timeout is None or now - last[p_] <= timeout]
            wm = min(active) - args.delay
            while closed + 1 <= wm: closed += 1; delays.append(now - closed)                     # window [closed-1, closed) closes
        return {"idleness_timeout": f"{timeout:g} min" if timeout is not None else "none", "windows_closed": closed,
                "longest_close_delay_min": max(delays), "late_events": late}
    rows = [run(None), run(args.timeout)]
    for r in rows: print(f"timeout {r['idleness_timeout']:<6}: {r['windows_closed']} windows closed, longest wait past a window's end {r['longest_close_delay_min']:.1f} min, late events {r['late_events']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"runs": rows}}, f)
    
  3. 3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.

    Terminal
    uv run watermark-with-an-idle-partition.py
    uv run watermark-with-an-idle-partition.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--minutes30 Simulated minutes.
--idle-at10 Minute at which partition 3 goes quiet.
--resume-at20 Minute at which partition 3 sends its backlog.
--timeout2 Idleness timeout (minutes).
--delay0.5 Watermark delay (minutes).
--seed0 Random seed.

Our run

Partition 3 quiet from minute 10 to 20

Partition 3 quiet from minute 10 to 20
Idleness timeoutLongest wait past a window end (min)Late events
none100
2 min2540
Reference runApple M2 · 8 cores · 8 GB
Output · uv run watermark-with-an-idle-partition.py
timeout none  : 29 windows closed, longest wait past a window's end 10.0 min, late events 0
timeout 2 min : 29 windows closed, longest wait past a window's end 2.0 min, late events 540
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

Without a timeout, windows waited up to 10 minutes past their end, the whole silence, and no event was late (0). With a 2-minute timeout, the wait was at most 2 minutes, and when the partition sent its backlog, 540 of its events arrived after their windows had closed.


Read it

An idleness timeout trades a stall for late data, and which is right depends on the source: a partition that is quiet because nothing happens can be skipped; one that is quiet because it is down and buffering cannot. Know which kind of silence each source has, and give the late path somewhere to go.