Skip to content
About
Main content

The pointer swap: two writers, one winner

Break it on purposeNeeds Python

Problem

A table format commits a new version by moving one pointer from version N to version N + 1. Two writers that both read N and both write N + 1 would each believe they committed, and one change would vanish. The commit is safe only if moving the pointer is conditional: it succeeds only if the pointer still says N, so the loser learns it lost and retries on top of the new version.

Simulate a table commit with a pointer on the local file system. Each table version is a file v1.json, v2.json, and so on; the "pointer" is "the highest version number". Creating v(N+1).json with the exclusive-create flag works as a , because only one creator can succeed. Two writer threads commit 50 changes each. Then replace the exclusive create with a plain write.

The question. With an exclusive create and with a plain write, how many of the 100 commits survive, and how many retries does it take?


Predict

Two writers, 100 commits, each a plain write that creates or overwrites the pointer: how many commits survive?

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.

    the-pointer-swap-two-writers-one.py64 linesDownload
    View the code
    the-pointer-swap-two-writers-one.py
    # /// script
    # requires-python = ">=3.10"
    # ///
    # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT
    # https://thedataengineering.com/lab-code/LICENSE.txt
    """The pointer swap: two writers, one table.
    
    A table commit on the local file system: each version is a file vN.json and
    the "pointer" is the highest version number. Creating v(N+1).json with the
    exclusive-create flag acts as a compare-and-swap. Two writer threads commit 50
    changes each and retry when they lose. Then the same with a plain write
    (create-or-overwrite) in place of the exclusive create.
    """
    import argparse, json, os, tempfile, threading
    
    p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    p.add_argument("--commits", type=int, default=50, help="Commits per writer.")
    p.add_argument("--json", help="Also write the results to this JSON file.")
    args = p.parse_args()
    
    def commit(table_dir, base_version, change, exclusive):
        new_version = base_version + 1
        path = os.path.join(table_dir, f"v{new_version}.json")
        flags = os.O_CREAT | os.O_WRONLY | (os.O_EXCL if exclusive else os.O_TRUNC)
        try:
            fd = os.open(path, flags)                                          # with O_EXCL: fails if the file exists
        except FileExistsError:
            return False                                                       # lost the race
        with os.fdopen(fd, "w") as f:
            json.dump({"version": new_version, "change": change}, f)
        return True
    
    def latest(table_dir):
        return max(int(n[1:-5]) for n in os.listdir(table_dir) if n.startswith("v") and n.endswith(".json"))
    
    def writer(table_dir, name, results, retries, exclusive):
        for i in range(args.commits):
            while True:
                base = latest(table_dir)
                if commit(table_dir, base, f"{name}-{i}", exclusive):
                    results[name] += 1
                    break                                                      # else: retry on top of the new latest
                retries[name] += 1
    
    rows = []
    for exclusive, label in ((True, "exclusive create (compare-and-swap)"), (False, "plain write (create or overwrite)")):
        d = tempfile.mkdtemp(); open(os.path.join(d, "v0.json"), "w").write("{}")
        results, retries = {"A": 0, "B": 0}, {"A": 0, "B": 0}
        ts = [threading.Thread(target=writer, args=(d, n, results, retries, exclusive)) for n in "AB"]
        [t.start() for t in ts]; [t.join() for t in ts]
        stored = []
        for n in os.listdir(d):
            if n != "v0.json":
                try: stored.append(json.load(open(os.path.join(d, n)))["change"])
                except (json.JSONDecodeError, KeyError): stored.append("<torn>")
        claimed = results["A"] + results["B"]
        rows.append({"commit": label, "claimed": claimed, "latest": latest(d), "stored": len(set(stored) - {"<torn>"}),
                     "lost": claimed - len(set(stored) - {"<torn>"}), "retries": retries["A"] + retries["B"]})
    for r in rows: print(f"{r['commit']:<38} writers think they committed {r['claimed']}; latest = v{r['latest']}; changes stored {r['stored']}; lost {r['lost']}; retries {r['retries']}")
    
    if args.json:
        with open(args.json, "w") as f:
            json.dump({"tables": {"commits": 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 the-pointer-swap-two-writers-one.py
    uv run the-pointer-swap-two-writers-one.py --help     # every knob
Knobs
Knob · defaultWhat it changes
--commits50 Commits per writer.

Our run

Two writers, 50 commits each

Two writers, 50 commits each
CommitWriters think they committedLatest versionChanges storedLostRetries
exclusive create (compare-and-swap)100100100051
plain write (create or overwrite)1006666340
Reference runApple M2 · 8 cores · 8 GB
Output · uv run the-pointer-swap-two-writers-one.py
exclusive create (compare-and-swap)    writers think they committed 100; latest = v100; changes stored 100; lost 0; retries 51
plain write (create or overwrite)      writers think they committed 100; latest = v66; changes stored 66; lost 34; retries 0
macOS 26.6.2 · Python 3.13.3 · run 2026-10-20

What we found

With the exclusive create, all 100 changes were stored, after 51 retries. With a plain write the writers still believed they had committed 100 changes, but only 66 were stored: 34 were overwritten without any error.


Read it

Everything else in a table format (, , ) rests on this one conditional write. Object stores that lacked it forced table formats to add an external lock or ; a store that offers it (conditional PUT) lets the format commit with no other coordinator.