[ norboten ]
python · lab python-06

The Job That Runs Twice

· about 40 minutes · runs on ubuntu-26.04-devops · unrated

An unrated lab. It runs on your machine with no account and no network, and everything about it — the faults, the checks, the hints and the reference solution — is in the repository. An attempt on it is recorded on your profile and never moves a rating: only rated labs do. Rated and unrated labs.

The briefing

/opt/pricing/rebuild.py rebuilds the price list from the catalogue into /var/lib/pricing/prices.json, which the shop reads on every request. pricing-rebuild.timer starts it every minute, and a rebuild takes a few seconds — longer when the catalogue grows.

Twice this month the shop served a price list with half the products in it, and the log showed two rebuilds running at the same time. The job also does not survive being started by hand while the timer's run is still going.

What is expected, and graded — the grader runs rebuild.py itself, through the PRICING_CATALOGUE, PRICING_OUT and PRICING_LOCK variables the script already reads:

  1. A single run rebuilds the price list and exits 0.
  2. A second run started while the first is still working exits non-zero within a second, says on standard error that a rebuild is already running, and does not touch the output.
  3. A reader of /var/lib/pricing/prices.json never sees a half-written file: at any moment it is valid JSON with every product in it, whatever runs or fails.
  4. pricing-rebuild.timer is enabled and active, and the price list is rebuilt from this boot — and still after a reboot.

You have root through sudo. The rebuild keeps taking as long as it takes.

What is graded

The machine is checked, rebooted, and checked again. A check passes only if it passes both times.

CheckObjective
01_a_single_run_still_worksLeave the previous output in place when a run is refused or fails
02_a_second_run_is_refusedKeep two copies of the same job from running at once
03_the_output_is_never_half_writtenLeave the previous output in place when a run is refused or fails
04_the_timer_runs_it_oftenLet a timer start a job as often as it likes without overlapping runs

Start it

  1. 2Labs
  2. python-06select this one
  3. udownload it
  4. sstart the machine

Reading

Where the lab's hints send you, level by level, as you ask for them (h, then l opens a journal section in the TUI).

Theory for this lab

4 questions on the same topic, in the lab's Theory tab. They never affect the lab's grade. Three of them, to answer here:

Question 1

What does this print?

import fcntl, os, subprocess, sys, tempfile
lock = os.path.join(tempfile.mkdtemp(), "job.lock")
fd = os.open(lock, os.O_CREAT | os.O_RDWR, 0o644)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
code = ("import fcntl, os, sys\n"
        f"fd = os.open({lock!r}, os.O_CREAT | os.O_RDWR, 0o644)\n"
        "try:\n"
        "    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)\n"
        "    print('acquired')\n"
        "except OSError as e:\n"
        "    print('refused', e.errno == 11)\n")
print(subprocess.run([sys.executable, "-c", code], capture_output=True, text=True).stdout.strip())
Question 2

A job takes its lock like this: `with open(LOCK, "w") as f: fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)`, and then does its work. What is wrong?

Question 3

What does this print?

import json, os, tempfile
d = tempfile.mkdtemp()
out = os.path.join(d, "prices.json")
with open(out, "w") as f:
    json.dump({"A": 1}, f)
fd, tmp = tempfile.mkstemp(dir=d)
with os.fdopen(fd, "w") as f:
    json.dump({"A": 1, "B": 2}, f)
before = open(out).read()
os.replace(tmp, out)
print(before, open(out).read())