· 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.
/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:
/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.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.
The machine is checked, rebooted, and checked again. A check passes only if it passes both times.
| Check | Objective |
|---|---|
| 01_a_single_run_still_works | Leave the previous output in place when a run is refused or fails |
| 02_a_second_run_is_refused | Keep two copies of the same job from running at once |
| 03_the_output_is_never_half_written | Leave the previous output in place when a run is refused or fails |
| 04_the_timer_runs_it_often | Let a timer start a job as often as it likes without overlapping runs |
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).
man 2 flockman 5 systemd.timerman 5 systemd.unit4 questions on the same topic, in the lab's Theory tab. They never affect the lab's grade. Three of them, to answer here:
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())
refused True
The parent holds an exclusive flock on the file and keeps the descriptor open, so the child's non-blocking request fails at once with EWOULDBLOCK (errno 11 on Linux). Without LOCK_NB the child would have waited instead; if the parent had closed its descriptor, the lock would have been released and the child would have acquired it.
https://docs.python.org/3/library/fcntl.html#fcntl.flock · man 2 flock · executed in a sandbox
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?
The with block closes the file, which releases the lock before the work starts
An flock lock belongs to the open file description and is released when the last descriptor for it is closed — which the with block does immediately. The descriptor has to stay open for as long as the lock is needed, usually for the life of the process. LOCK_NB only means "do not wait"; all flock locks are advisory.
man 2 flock
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())
{"A": 1} {"A": 1, "B": 2}
The new content is built in a separate file in the same directory, so the target keeps its old content until os.replace swaps the directory entry in one step. A reader therefore sees the old file or the new one and never a partial write — which is what writing into the live file would have given.
https://docs.python.org/3/library/os.html#os.replace · executed in a sandbox