a75318b811
Stage-0 caching from docs/scalability.md. The Go API now caches hot product-detail and search-result reads in Redis with a fail-open, epoch-versioned scheme; Python ingestion bumps the epoch after a write run to invalidate the cache globally in O(1). - api/internal/cache: fail-open Cache (GetJSON/SetJSON) namespaced by an epoch counter (og:cache:epoch); disabled when Redis is unconfigured. - store: ProductByID/ProductByGTIN (24h TTL) and SearchProducts (1h TTL) read-through the cache via WithCache. - ingestion: bump_cache_epoch() called after update_off/seed_off/ import_bypos/dedup commits when rows changed; best-effort, never fails a run. Adds redis dependency. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
74 lines
2.6 KiB
Python
74 lines
2.6 KiB
Python
"""Incremental Open Food Facts update.
|
|
|
|
Fetches products modified since the persisted watermark, loads them, then
|
|
advances the watermark to the newest ``last_modified_t`` processed so the next
|
|
run only sees what changed.
|
|
|
|
Usage:
|
|
python -m opengoods.jobs.update_off --max-pages 5
|
|
python -m opengoods.jobs.update_off --since 1700000000 # override watermark
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
import psycopg
|
|
|
|
from opengoods.adapters.openfoodfacts import SOURCE_NAME, OpenFoodFactsAdapter
|
|
from opengoods.cache import bump_cache_epoch
|
|
from opengoods.etl.load import default_dsn, ensure_source, load_record_safe
|
|
from opengoods.etl.state import get_watermark, set_watermark
|
|
from opengoods.etl.transform import transform
|
|
|
|
|
|
def run(args: argparse.Namespace) -> int:
|
|
adapter = OpenFoodFactsAdapter(min_interval=args.min_interval)
|
|
loaded = skipped = errored = 0
|
|
high_watermark = 0
|
|
with psycopg.connect(args.dsn, autocommit=False) as conn:
|
|
source_id = ensure_source(conn)
|
|
since = args.since if args.since is not None else get_watermark(conn, SOURCE_NAME)
|
|
high_watermark = since
|
|
for raw in adapter.fetch_modified_since(
|
|
since, page_size=args.page_size, max_pages=args.max_pages
|
|
):
|
|
high_watermark = max(high_watermark, int(raw.get("last_modified_t") or 0))
|
|
rec = transform(raw)
|
|
if rec is None:
|
|
skipped += 1
|
|
continue
|
|
if load_record_safe(conn, rec, source_id, raw):
|
|
loaded += 1
|
|
else:
|
|
errored += 1
|
|
set_watermark(
|
|
conn,
|
|
SOURCE_NAME,
|
|
high_watermark,
|
|
stats={"loaded": loaded, "skipped": skipped, "errored": errored, "since": since},
|
|
)
|
|
conn.commit()
|
|
if loaded:
|
|
bump_cache_epoch()
|
|
print(
|
|
f"since={since} loaded={loaded} skipped={skipped} "
|
|
f"errored={errored} watermark={high_watermark}"
|
|
)
|
|
return 0
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Incremental OFF update")
|
|
parser.add_argument("--since", type=int, default=None, help="override watermark (unix ts)")
|
|
parser.add_argument("--page-size", type=int, default=100, help="search page size")
|
|
parser.add_argument("--max-pages", type=int, default=10, help="max pages to scan")
|
|
parser.add_argument("--min-interval", type=float, default=4.0, help="API throttle seconds")
|
|
parser.add_argument("--dsn", default=default_dsn(), help="PostgreSQL DSN")
|
|
return run(parser.parse_args(argv))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|