fix(conf,llm,compose): the DuckDB replica lives in data/replica/ (refs P48)
The llm container mounted the whole ./data directory read-only just to read one snapshot. Snapshots now get their own directory, published with mkdir -p, and compose mounts only ./data/replica — the primary databases are no longer visible inside the container.
This commit is contained in:
@@ -680,12 +680,12 @@ services:
|
|||||||
- data
|
- data
|
||||||
- observability
|
- observability
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data:ro # DuckDB read replica for code valuations (directory mount survives replica re-publish)
|
- ./data/replica:/app/data/replica:ro # DuckDB read replica only (directory mount survives replica re-publish)
|
||||||
environment:
|
environment:
|
||||||
- LLM_OLLAMA_HOSTS=${LLM_OLLAMA_HOSTS_IN_CONTAINER:-http://ollama:11434}
|
- LLM_OLLAMA_HOSTS=${LLM_OLLAMA_HOSTS_IN_CONTAINER:-http://ollama:11434}
|
||||||
- LLM_PG_HOST=postgres
|
- LLM_PG_HOST=postgres
|
||||||
- LLM_DB_PASSWORD=${LLM_DB_PASSWORD}
|
- LLM_DB_PASSWORD=${LLM_DB_PASSWORD}
|
||||||
- LLM_DUCKDB_REPLICA=/app/data/aco.ro.duckdb
|
- LLM_DUCKDB_REPLICA=/app/data/replica/aco.ro.duckdb
|
||||||
- OTEL_SERVICE_NAME=llm
|
- OTEL_SERVICE_NAME=llm
|
||||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ with duckdb_batch("aco") as con: # retries, then names the lock holder
|
|||||||
## Notebooks read a replica, not the primary
|
## Notebooks read a replica, not the primary
|
||||||
|
|
||||||
Read-only opens through `conf.connect.duckdb()` resolve to the snapshot
|
Read-only opens through `conf.connect.duckdb()` resolve to the snapshot
|
||||||
`data/aco.ro.duckdb` when it exists — so notebook kernels never hold the
|
`data/replica/aco.ro.duckdb` when it exists — so notebook kernels never hold the
|
||||||
primary's lock at all, and ingests stop caring how many notebooks are open.
|
primary's lock at all, and ingests stop caring how many notebooks are open.
|
||||||
|
|
||||||
- **Refresh cadence**: every ingest republishes the snapshot as its last step
|
- **Refresh cadence**: every ingest republishes the snapshot as its last step
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ from conf import ROOT, cfg, path
|
|||||||
|
|
||||||
|
|
||||||
def _replica_path(name: str) -> Any:
|
def _replica_path(name: str) -> Any:
|
||||||
"""data/<name>.ro.duckdb next to the primary."""
|
"""data/replica/<name>.ro.duckdb — its own directory, so a container
|
||||||
return path(f"db.{name}").with_suffix(".ro.duckdb")
|
can mount the snapshots without seeing the primary databases."""
|
||||||
|
primary = path(f"db.{name}")
|
||||||
|
return primary.parent / "replica" / f"{primary.stem}.ro.duckdb"
|
||||||
|
|
||||||
|
|
||||||
def duckdb(
|
def duckdb(
|
||||||
@@ -46,7 +48,8 @@ def duckdb(
|
|||||||
read_only
|
read_only
|
||||||
Open in read-only mode (default ``True``).
|
Open in read-only mode (default ``True``).
|
||||||
replica
|
replica
|
||||||
Read-only opens resolve to the ``<name>.ro.duckdb`` snapshot
|
Read-only opens resolve to the ``replica/<name>.ro.duckdb``
|
||||||
|
snapshot
|
||||||
when it exists — long-running readers (notebook kernels) then
|
when it exists — long-running readers (notebook kernels) then
|
||||||
never hold the primary's single-writer lock, so ingests stop
|
never hold the primary's single-writer lock, so ingests stop
|
||||||
failing (#510). Staleness is bounded by the refresh cadence:
|
failing (#510). Staleness is bounded by the refresh cadence:
|
||||||
@@ -136,7 +139,7 @@ def duckdb_batch(
|
|||||||
|
|
||||||
|
|
||||||
def publish_replica(name: str = "aco") -> Any:
|
def publish_replica(name: str = "aco") -> Any:
|
||||||
"""Publish/refresh the read-only snapshot ``<name>.ro.duckdb``.
|
"""Publish/refresh the read-only snapshot ``replica/<name>.ro.duckdb``.
|
||||||
|
|
||||||
Called after each ingest so notebook readers (see :func:`duckdb`)
|
Called after each ingest so notebook readers (see :func:`duckdb`)
|
||||||
work from a lock-free snapshot instead of contending with writers
|
work from a lock-free snapshot instead of contending with writers
|
||||||
@@ -152,7 +155,9 @@ def publish_replica(name: str = "aco") -> Any:
|
|||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
src = path(f"db.{name}")
|
src = path(f"db.{name}")
|
||||||
dst = _replica_path(name).resolve()
|
dst = _replica_path(name)
|
||||||
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
dst = dst.resolve()
|
||||||
tmp = dst.with_suffix(".tmp")
|
tmp = dst.with_suffix(".tmp")
|
||||||
with duckdb_batch(name) as con:
|
with duckdb_batch(name) as con:
|
||||||
con.execute("CHECKPOINT")
|
con.execute("CHECKPOINT")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Env contract:
|
|||||||
LLM_PG_HOST beats [llm].pg_host (containers set this to "postgres")
|
LLM_PG_HOST beats [llm].pg_host (containers set this to "postgres")
|
||||||
LLM_DB_PASSWORD required for pg_url(); lives in .env, never stack.toml
|
LLM_DB_PASSWORD required for pg_url(); lives in .env, never stack.toml
|
||||||
LLM_BUILD_ANN_INDEX beats [llm].build_ann_index ("1"/"true"/"yes" = True)
|
LLM_BUILD_ANN_INDEX beats [llm].build_ann_index ("1"/"true"/"yes" = True)
|
||||||
LLM_DUCKDB_REPLICA beats [llm].duckdb_replica (containers set /app/data/aco.ro.duckdb)
|
LLM_DUCKDB_REPLICA beats [llm].duckdb_replica (containers set /app/data/replica/aco.ro.duckdb)
|
||||||
|
|
||||||
Ollama has no GPU-size API, so VRAM is *declared* per host; liveness is
|
Ollama has no GPU-size API, so VRAM is *declared* per host; liveness is
|
||||||
probed at request time (``HostPool.check``). Together they implement
|
probed at request time (``HostPool.check``). Together they implement
|
||||||
@@ -44,7 +44,7 @@ class LlmConfig:
|
|||||||
default_factory=lambda: dict(_DEFAULT_K_PER_KIND)
|
default_factory=lambda: dict(_DEFAULT_K_PER_KIND)
|
||||||
)
|
)
|
||||||
top_n: int = 8
|
top_n: int = 8
|
||||||
duckdb_replica: str = "data/aco.ro.duckdb"
|
duckdb_replica: str = "data/replica/aco.ro.duckdb"
|
||||||
valuation_years: int = 4
|
valuation_years: int = 4
|
||||||
code_cited_per_code: int = 3
|
code_cited_per_code: int = 3
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ def load() -> LlmConfig:
|
|||||||
k_per_kind=k_per_kind,
|
k_per_kind=k_per_kind,
|
||||||
top_n=int(_opt(section, "top_n", 8)),
|
top_n=int(_opt(section, "top_n", 8)),
|
||||||
duckdb_replica=os.environ.get("LLM_DUCKDB_REPLICA")
|
duckdb_replica=os.environ.get("LLM_DUCKDB_REPLICA")
|
||||||
or str(_opt(section, "duckdb_replica", "data/aco.ro.duckdb")),
|
or str(_opt(section, "duckdb_replica", "data/replica/aco.ro.duckdb")),
|
||||||
valuation_years=int(_opt(section, "valuation_years", 4)),
|
valuation_years=int(_opt(section, "valuation_years", 4)),
|
||||||
code_cited_per_code=int(_opt(section, "code_cited_per_code", 3)),
|
code_cited_per_code=int(_opt(section, "code_cited_per_code", 3)),
|
||||||
embed_dim=int(section.embed_dim),
|
embed_dim=int(section.embed_dim),
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ pg_user = "llm"
|
|||||||
recency_half_life_days = 365
|
recency_half_life_days = 365
|
||||||
recency_weight = 0.3
|
recency_weight = 0.3
|
||||||
top_n = 8
|
top_n = 8
|
||||||
duckdb_replica = "data/aco.ro.duckdb" # read-only DuckDB replica the chat reads valuations from
|
duckdb_replica = "data/replica/aco.ro.duckdb" # read-only DuckDB replica the chat reads valuations from
|
||||||
valuation_years = 4 # final-rule vintages shown per code (plus the newest NPRM)
|
valuation_years = 4 # final-rule vintages shown per code (plus the newest NPRM)
|
||||||
code_cited_per_code = 3 # rule paragraphs literally citing each detected code
|
code_cited_per_code = 3 # rule paragraphs literally citing each detected code
|
||||||
|
|
||||||
|
|||||||
@@ -289,6 +289,8 @@ class TestReplica:
|
|||||||
|
|
||||||
dst = connect.publish_replica("custom")
|
dst = connect.publish_replica("custom")
|
||||||
assert dst.name == "custom.ro.duckdb"
|
assert dst.name == "custom.ro.duckdb"
|
||||||
|
# its own directory, so the container mount cannot see the primary
|
||||||
|
assert dst.parent == db_file.parent / "replica"
|
||||||
import duckdb as _duckdb
|
import duckdb as _duckdb
|
||||||
|
|
||||||
con = _duckdb.connect(str(dst), read_only=True)
|
con = _duckdb.connect(str(dst), read_only=True)
|
||||||
@@ -344,17 +346,16 @@ class TestReplica:
|
|||||||
self._patch_path(monkeypatch, db_file)
|
self._patch_path(monkeypatch, db_file)
|
||||||
|
|
||||||
target = shared / "custom.ro.duckdb"
|
target = shared / "custom.ro.duckdb"
|
||||||
|
link = tmp_path / "replica" / "custom.ro.duckdb"
|
||||||
connect.publish_replica("custom") # seed a real file to link to
|
connect.publish_replica("custom") # seed a real file to link to
|
||||||
(tmp_path / "custom.ro.duckdb").rename(target)
|
link.rename(target)
|
||||||
(tmp_path / "custom.ro.duckdb").symlink_to(target)
|
link.symlink_to(target)
|
||||||
|
|
||||||
with connect.duckdb_batch("custom") as con:
|
with connect.duckdb_batch("custom") as con:
|
||||||
con.execute("UPDATE t SET x = 7")
|
con.execute("UPDATE t SET x = 7")
|
||||||
dst = connect.publish_replica("custom")
|
dst = connect.publish_replica("custom")
|
||||||
|
|
||||||
assert (tmp_path / "custom.ro.duckdb").is_symlink(), (
|
assert link.is_symlink(), "publish detached the symlink into a standalone copy"
|
||||||
"publish detached the symlink into a standalone copy"
|
|
||||||
)
|
|
||||||
import duckdb as _duckdb
|
import duckdb as _duckdb
|
||||||
|
|
||||||
con = _duckdb.connect(str(target), read_only=True)
|
con = _duckdb.connect(str(target), read_only=True)
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ class TestNewKnobs:
|
|||||||
def test_valuation_knobs_defaults(self, monkeypatch):
|
def test_valuation_knobs_defaults(self, monkeypatch):
|
||||||
monkeypatch.delenv("LLM_DUCKDB_REPLICA", raising=False)
|
monkeypatch.delenv("LLM_DUCKDB_REPLICA", raising=False)
|
||||||
cfg = llm_config.load()
|
cfg = llm_config.load()
|
||||||
assert cfg.duckdb_replica == "data/aco.ro.duckdb"
|
assert cfg.duckdb_replica == "data/replica/aco.ro.duckdb"
|
||||||
assert cfg.valuation_years == 4
|
assert cfg.valuation_years == 4
|
||||||
assert cfg.code_cited_per_code == 3
|
assert cfg.code_cited_per_code == 3
|
||||||
|
|
||||||
def test_duckdb_replica_env_override(self, monkeypatch):
|
def test_duckdb_replica_env_override(self, monkeypatch):
|
||||||
monkeypatch.setenv("LLM_DUCKDB_REPLICA", "/app/data/aco.ro.duckdb")
|
monkeypatch.setenv("LLM_DUCKDB_REPLICA", "/app/data/replica/aco.ro.duckdb")
|
||||||
assert llm_config.load().duckdb_replica == "/app/data/aco.ro.duckdb"
|
assert llm_config.load().duckdb_replica == "/app/data/replica/aco.ro.duckdb"
|
||||||
|
|||||||
Reference in New Issue
Block a user