Răsfoiți Sursa

docker improvements

Lukas Goldschmidt 2 săptămâni în urmă
părinte
comite
1fb3e2416a
5 a modificat fișierele cu 66 adăugiri și 9 ștergeri
  1. 2 0
      Dockerfile
  2. 25 0
      POLLER_UPGRADE_PLAN.md
  3. 12 0
      docker-compose.yml
  4. 4 9
      news_mcp/mcp_server_fastmcp.py
  5. 23 0
      test_news_mcp.py

+ 2 - 0
Dockerfile

@@ -16,4 +16,6 @@ COPY . .
 
 EXPOSE 8506
 
+HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8506/health', timeout=3).read()"
+
 CMD ["uvicorn", "news_mcp.mcp_server_fastmcp:app", "--host", "0.0.0.0", "--port", "8506"]

+ 25 - 0
POLLER_UPGRADE_PLAN.md

@@ -0,0 +1,25 @@
+# Poller Upgrade Plan
+
+## Goal
+Remove the poller's direct dependency on `SQLiteClusterStore._conn()` and replace it with a public store method so tests and future store implementations do not need to model a private connection helper.
+
+## Current Problem
+- `news_mcp/jobs/poller.py` clears legacy `feed_state` rows with `with store._conn() as conn:`.
+- That couples the refresh loop to a private SQLite implementation detail.
+- Test doubles now need to expose `_conn()`, which is a sign the contract is too low-level.
+
+## Proposed Refactor
+1. Add a public method to `SQLiteClusterStore` for the legacy cleanup step.
+2. Move the `DELETE FROM feed_state WHERE feed_key LIKE 'newsfeeds:%'` logic into that method.
+3. Update `news_mcp/jobs/poller.py` to call the public method instead of `_conn()`.
+4. Adjust tests to mock the public method, not a private connection handle.
+
+## Verification
+- Re-run the poller-focused tests.
+- Run the repo test script if the change stays small enough to keep coverage cheap.
+- Confirm no other code paths still depend on `store._conn()` outside the store implementation itself.
+
+## Notes
+- Keep the change narrow.
+- Do not alter the feed-hash or clustering behavior in the same patch.
+- Preserve the current legacy-row cleanup behavior exactly; only the access path should change.

+ 12 - 0
docker-compose.yml

@@ -1,5 +1,6 @@
 services:
   news-mcp:
+    image: news-mcp
     build: .
     container_name: news-mcp
     env_file:
@@ -14,4 +15,15 @@ services:
       - ./:/app
     ports:
       - "8506:8506"
+    healthcheck:
+      test:
+        [
+          "CMD",
+          "python",
+          "-c",
+          "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8506/health', timeout=3).read()",
+        ]
+      interval: 30s
+      timeout: 5s
+      retries: 3
     restart: unless-stopped

+ 4 - 9
news_mcp/mcp_server_fastmcp.py

@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import asyncio
 import logging
+import time
 from collections import Counter
 from datetime import datetime, timezone
 from email.utils import parsedate_to_datetime
@@ -34,6 +35,8 @@ logging.basicConfig(
     format="%(asctime)s %(levelname)s %(name)s: %(message)s",
 )
 
+_PROCESS_STARTED_AT = time.monotonic()
+
 mcp = FastMCP(
     "news-mcp",
     transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
@@ -859,13 +862,5 @@ def api_cluster_detail(cluster_id: str):
 def health():
     return {
         "status": "ok",
-        "lookback_hours": DEFAULT_LOOKBACK_HOURS,
-        "db": str(DB_PATH),
-        "last_refresh_at": _shared_store.get_meta("last_refresh_at"),
-        "feeds": _shared_store.get_all_feed_states(),
-        "pruning": _shared_store.get_prune_state(
-            pruning_enabled=NEWS_PRUNING_ENABLED,
-            retention_days=NEWS_RETENTION_DAYS,
-            interval_hours=NEWS_PRUNE_INTERVAL_HOURS,
-        ),
+        "uptime": round(time.monotonic() - _PROCESS_STARTED_AT, 3),
     }

+ 23 - 0
test_news_mcp.py

@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+from contextlib import contextmanager
 import tempfile
 from pathlib import Path
 
@@ -354,12 +355,23 @@ def test_refresh_skips_reprocessing_when_feed_hash_is_unchanged(monkeypatch):
             self.meta = {}
             self.feed_hash = expected_hash
 
+        @contextmanager
+        def _conn(self):
+            class _Conn:
+                def execute(self, *args, **kwargs):
+                    return None
+
+            yield _Conn()
+
         def get_feed_hash(self, feed_key):
             return self.feed_hash
 
         def set_feed_hash(self, feed_key, last_hash):
             self.feed_hash = last_hash
 
+        def set_feed_state(self, feed_key, last_hash, item_count):
+            self.feed_hash = last_hash
+
         def get_cluster_by_id(self, cluster_id):
             return None
 
@@ -584,12 +596,23 @@ def test_poller_persists_clusters_under_post_enrichment_topic(monkeypatch):
         def __init__(self, *args, **kwargs):
             pass
 
+        @contextmanager
+        def _conn(self):
+            class _Conn:
+                def execute(self, *args, **kwargs):
+                    return None
+
+            yield _Conn()
+
         def get_feed_hash(self, feed_key):
             return None
 
         def set_feed_hash(self, feed_key, last_hash):
             pass
 
+        def set_feed_state(self, feed_key, last_hash, item_count):
+            pass
+
         def get_cluster_by_id(self, cluster_id):
             return None