fix: a presentation that runs out of memory says so, and the service survives it
On a 4 GB VM the kernel killed the import service mid-conversion. Measured: LibreOffice needs 0.2 GB for a text deck and 1.4-2.1 GB for decks of many different photographs, for about twenty seconds — a spike, not a leak. What made it a problem was the rest: OOMPolicy=stop took the watcher down with LibreOffice, it restarted and resumed the same conversion with no limit, and the library said "Getting this ready" throughout. The service is capped at 40% of memory, is first in line when memory runs short everywhere, and survives a killed LibreOffice (OOMPolicy=continue). A killed conversion is a failed import of kind "memory"; an import interrupted twice is not tried a third time; the card says the computer needed more memory, to close other programs and choose Try again; an import nobody has worked on for 20 minutes shows the same card; and an exclusive lock keeps it to one import at a time. Lab, the real service and LibreOffice: at a 4 GB-machine ceiling the 2.1 GB deck still imported; with no swap and a 300 MB ceiling LibreOffice was killed, the watcher kept its PID and the card offered Try again, which worked; the whole service killed during two attempts gave up after the second; three decks at once never ran two LibreOffices together. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
940f63e0e5
commit
3855989029
@@ -422,6 +422,105 @@ check("normal title bars contain only Close",
|
||||
check("the unfinished Help launcher is absent from the panel",
|
||||
"ekklesia-manual.desktop" not in layout)
|
||||
|
||||
print("17. running out of memory is a state a teacher can act on, never a loop")
|
||||
library.render_pages = fake_render_pages
|
||||
library.render_thumbnail = fake_render_thumbnail
|
||||
library.first_text = lambda _pdf, pages=2: ""
|
||||
# The kernel killing LibreOffice: SIGKILL directly, or 137 through the
|
||||
# soffice shell wrapper.
|
||||
check("a SIGKILL is recognised either way",
|
||||
library.killed(-9) and library.killed(137)
|
||||
and not library.killed(1) and not library.killed(0))
|
||||
|
||||
library.convert_to_pdf = lambda _source, _destination: (False, library.KILLED)
|
||||
heavy = download("Heavy deck.pptx", contents="photographs")
|
||||
starved = importer.import_one(heavy, shelf)
|
||||
check("a conversion killed for memory is a failed import",
|
||||
starved is not None and starved.failed, starved.data if starved else None)
|
||||
check("of the kind a teacher can try again",
|
||||
starved.failure == "memory" and starved.data["error_kind"] == "memory")
|
||||
check("and the downloaded original is kept",
|
||||
starved.managed_copy.read_text() == "photographs")
|
||||
check("the attempt was counted before the work began",
|
||||
starved.data.get("attempts") == 1 and starved.data.get("attempt_started_at"))
|
||||
check("a file that simply will not convert is a different failure",
|
||||
failed_import.failure == "unreadable")
|
||||
check("and cannot be tried again", not library.request_retry(failed_import))
|
||||
|
||||
# A service killed in the middle of an import, found by the next start.
|
||||
library.convert_to_pdf = original_convert
|
||||
|
||||
|
||||
def interrupted(name, attempts):
|
||||
directory = shelf / name
|
||||
(directory / "original").mkdir(parents=True)
|
||||
(directory / "original" / f"{name}.pdf").write_text("%PDF-1.4 " + name)
|
||||
library.Presentation(directory, {
|
||||
"id": name, "state": "importing", "title": name,
|
||||
"original_name": f"{name}.pdf", "source_path": str(downloads / f"{name}.pdf"),
|
||||
"managed_copy": f"original/{name}.pdf", "format": "pdf",
|
||||
"imported_at": library.stamp(), "last_presented_at": None,
|
||||
"times_presented": 0, "thumbnail_page": 1, "pages": 0, "error": None,
|
||||
"attempts": attempts,
|
||||
}).save()
|
||||
return directory
|
||||
|
||||
|
||||
once = interrupted("once", 1)
|
||||
twice = interrupted("twice", 2)
|
||||
importer.sweep(shelf)
|
||||
check("an import interrupted once is resumed",
|
||||
library.load(once).ready)
|
||||
gave_up = library.load(twice)
|
||||
check("an import interrupted twice is not tried a third time",
|
||||
gave_up.failed and gave_up.failure == "memory", gave_up.data)
|
||||
check("and its original is still there to try again from",
|
||||
(twice / "original" / "twice.pdf").exists())
|
||||
|
||||
check("Try again puts it back in the importer's hands",
|
||||
library.request_retry(gave_up) and library.load(twice).state == "retry")
|
||||
check("which counts afresh", library.load(twice).data.get("attempts") == 0)
|
||||
check("and the importer takes it up on its next pass",
|
||||
importer.retry_requested(shelf) == 1 and library.load(twice).ready)
|
||||
|
||||
# The importer not running at all: nothing will ever finish this one.
|
||||
stuck = interrupted("stuck", 0)
|
||||
entry_data = library.load(stuck).data
|
||||
long_ago = library.now() - timedelta(seconds=library.STALLED_SECONDS + 60)
|
||||
entry_data["attempt_started_at"] = library.stamp(long_ago)
|
||||
library.Presentation(stuck, entry_data).save()
|
||||
stuck_card = library.load(stuck)
|
||||
check("an import nobody is working on stops saying Getting this ready",
|
||||
stuck_card.stalled and stuck_card.failure == "memory")
|
||||
check("and offers Try again", library.request_retry(stuck_card))
|
||||
check("a fresh import is not mistaken for a stalled one",
|
||||
not library.load(once).stalled and library.load(once).failure is None)
|
||||
|
||||
held = importer.hold_lock(shelf)
|
||||
check("one importer holds the lock", held is not None)
|
||||
check("a second importer is refused rather than run beside it",
|
||||
importer.hold_lock(shelf) is None)
|
||||
held.close()
|
||||
check("and it is free again once the first has gone",
|
||||
importer.hold_lock(shelf) is not None)
|
||||
check("the lock file is not a presentation",
|
||||
all(p.id != ".importer.lock" for p in library.library(shelf)))
|
||||
|
||||
unit = (root / "systemd" / "user" / "ekklesia-classroom-presentations.service").read_text()
|
||||
check("the service may use at most 40% of the machine's memory",
|
||||
"MemoryMax=40%" in unit)
|
||||
check("a killed converter does not take the watcher with it",
|
||||
"OOMPolicy=continue" in unit)
|
||||
check("and under pressure it goes before the teacher's windows",
|
||||
"OOMScoreAdjust=800" in unit)
|
||||
check("the card says what to do, in a teacher's words",
|
||||
"Close other programs, then choose" in ui and 'label="Try again"' in ui)
|
||||
|
||||
library.render_pages = original_render_pages
|
||||
library.render_thumbnail = original_render_thumbnail
|
||||
library.first_text = original_first_text
|
||||
library.convert_to_pdf = original_convert
|
||||
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
|
||||
print()
|
||||
|
||||
Reference in New Issue
Block a user