· about 35 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.
ingest.service runs /opt/ingest/worker.py, which takes each file dropped into
/srv/ingest/queue, appends a row to /var/lib/ingest/ledger.csv, and prints ingested <name>.
Two complaints. journalctl -fu ingest shows nothing for hours, although the ledger grows, so
nobody can tell whether the worker is alive. And every deploy — which stops the service — costs a
broken ledger row and a handful of files ingested twice; last month that meant duplicate invoices.
What is expected, and graded — the grader runs worker.py itself, through the INGEST_QUEUE,
INGEST_LEDGER and INGEST_STATE variables the script already reads:
ingested <name> line reaches the reader — a pipe, as the journal is — within a couple of
seconds of the file being ingested, not when the program ends.SIGTERM, the worker exits within five seconds and leaves no half-written row in
the ledger and no unreadable state file.ingest.service is running and its journal shows ingested files from this boot — and still does
after a reboot.You have root through sudo. The queue keeps its files; they are removed by a different job.
The machine is checked, rebooted, and checked again. A check passes only if it passes both times.
| Check | Objective |
|---|---|
| 01_progress_appears_while_it_runs | Make a long-running program's output appear while it runs, not when it ends |
| 02_a_stop_leaves_no_half_row | Stop on SIGTERM without leaving half-written work behind |
| 03_nothing_is_ingested_twice | Record what is finished so that a restart does not do it twice |
| 04_the_service_reports_progress | Make a long-running program's output appear while it runs, not when it ends |
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 5 systemd.killman 1 journalctlman 5 systemd.exec4 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 subprocess, sys
code = "import sys; print('to a pipe'); print(sys.stdout.line_buffering, sys.stdout.isatty())"
out = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True).stdout
print(" ".join(out.split()))
to a pipe False False
capture_output gives the child a pipe, so stdout is not a terminal and Python block-buffers it: isatty() is False and line_buffering is False. The text still arrives here because the child exits and flushes; a long-running service would hold it until the buffer filled. The three values are printed on the two captured lines, joined by a space here.
https://docs.python.org/3/library/sys.html#sys.stdout · executed in a sandbox
A Python program with no signal handlers is running `time.sleep(30)` inside a `try: … finally: cleanup()` when it receives SIGTERM. What happens?
The process ends at once and cleanup() does not run
SIGTERM's default action terminates the process, so no finally block, atexit handler or buffer flush happens. SIGINT is the one Python turns into an exception (KeyboardInterrupt). To stop cleanly, install a SIGTERM handler that sets a flag and check it where stopping is safe.
https://docs.python.org/3/library/signal.html
A worker appends a ledger row for each item, then records the item in a state file it rewrites with `json.dump(done, open(state, "w"))`. It is stopped between the two. What does the next start do?
It ingests that item a second time
The row is on disk and the state does not name the item, so the restart ingests it again — a duplicate, which is the safer of the two failures. (A stop during the json.dump itself is worse: the file is already truncated, so the state is unreadable and everything is re-ingested. That is why the state file is written to a temporary file and moved into place with os.replace.)
https://docs.python.org/3/library/os.html#os.replace