fix(advice): the review findings - the ids, the client leak, the wrong reading
The review of PR #14 found a critical defect and two real bugs. All verified against the live database, not just read. CRITICAL - the advice would never have been stored -------------------------------------------------- `_as_doc_id` accepted only an ObjectId or a string that parses as one. But **all 47 live readings have string `_id`s** - 24 random characters from the original app's importer, like `l78ZidBJ3QaYn98tvB88x4uM`. The helper turned every one of them into None, so the update matched nothing and the advice was silently never stored in production. The mongomock fixture generates ObjectIds, so the tests could not see it. `_as_doc_id` now returns both forms and the query uses `$in` (a plain list would be an exact ARRAY match in Mongo, which is not what is wanted). Verified against the live database: the advice now lands on a real reading, with the brewer's comment untouched. There are now tests for BOTH id shapes, with the string shape built explicitly, because the fixture would not produce it. The advice was written about the wrong reading ---------------------------------------------- `latest` was taken from the date-sorted tail of the run, so editing an OLDER reading generated advice about the NEWEST one. The advice is now written against the reading that was actually saved. A leaked MongoClient per reading -------------------------------- The background thread built a `BeerRepository()` - and therefore a `MongoClient` - for every advice write, and never closed it. A MongoClient spawns three monitor threads and pymongo does NOT reap them on `close()`; they go only when the client is garbage collected. Measured: three threads and a connection pool accumulated per reading recorded. There is now one shared repository for the life of the process, which is what the request-scoped repository does too. Measured again after the fix: 4 threads total, not 16 over five writes. Also ---- - `BeerRepository` gained `close()` and the context-manager protocol. - The advice writer now logs instead of dying silently if the write fails. 164 tests (up from 160).
This commit is contained in:
@@ -89,7 +89,7 @@ def _start_advice(run_id: str, reading_id: str) -> None:
|
||||
for reading in run.readings:
|
||||
points = reading.sg_points
|
||||
readings.append({
|
||||
"_id": reading._id,
|
||||
"_id": str(reading._id),
|
||||
"date": reading.date.isoformat() if reading.date else "",
|
||||
"sg": points,
|
||||
"sg_decimal": reading.sg_decimal,
|
||||
@@ -98,7 +98,14 @@ def _start_advice(run_id: str, reading_id: str) -> None:
|
||||
"final": bool(reading.final_reading),
|
||||
})
|
||||
|
||||
latest = dict(readings[-1]) if readings else {"date": "", "sg": None}
|
||||
# The reading that is actually being advised about. It is not
|
||||
# necessarily the newest one: the brewer may be editing an older
|
||||
# reading, and advice about the newest reading would be about the
|
||||
# wrong number.
|
||||
saved = next((r for r in readings if r.get("_id") == str(reading_id)), None)
|
||||
if saved is None:
|
||||
saved = dict(readings[-1]) if readings else {"date": "", "sg": None}
|
||||
latest = dict(saved)
|
||||
latest["_id"] = reading_id
|
||||
|
||||
except Exception: # noqa: BLE001 - advice must never break the save
|
||||
@@ -106,8 +113,21 @@ def _start_advice(run_id: str, reading_id: str) -> None:
|
||||
return
|
||||
|
||||
def write(read_id: str, ok: bool, text: str) -> None:
|
||||
from mongo.repository import BeerRepository
|
||||
BeerRepository().set_reading_advice(read_id, text, ok)
|
||||
# The client is SHARED, not built per reading. A MongoClient
|
||||
# spawns three monitor threads and pymongo does not reap them on
|
||||
# close(), so building one per advice write accumulated three
|
||||
# threads and a connection pool for every reading recorded.
|
||||
#
|
||||
# The shared client is created on first use and lives for the
|
||||
# life of the process, which is what the request-scoped
|
||||
# repository does too.
|
||||
from mongo.repository import BeerRepository, get_shared_repository
|
||||
|
||||
try:
|
||||
shared = get_shared_repository()
|
||||
shared.set_reading_advice(read_id, text, ok)
|
||||
except Exception: # noqa: BLE001 - never let the thread die noisily
|
||||
log.exception("could not store the generated advice")
|
||||
|
||||
thread = threading.Thread(
|
||||
target=brew_advice.generate_advice_in_background,
|
||||
|
||||
Reference in New Issue
Block a user