fix: postgres/neon connection settings and search index dedupe

- Reduce db_pool_recycle from 3600s to 180s for Neon scale-to-zero
- Add connect_args for Neon serverless (statement cache, timeouts, app name)
- Dedupe observation permalinks in search indexing to avoid unique constraint violations
- Add tests for duplicate observation permalink handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2025-11-29 11:24:08 -06:00
parent 830775276d
commit b5d4fb559c
4 changed files with 205 additions and 4 deletions
+2 -2
View File
@@ -112,8 +112,8 @@ class BasicMemoryConfig(BaseSettings):
gt=0,
)
db_pool_recycle: int = Field(
default=3600,
description="Recycle connections after N seconds to prevent stale connections (Postgres only)",
default=180,
description="Recycle connections after N seconds to prevent stale connections. Default 180s works well with Neon's ~5 minute scale-to-zero (Postgres only)",
gt=0,
)
+14
View File
@@ -201,6 +201,7 @@ def _create_postgres_engine(db_url: str, config: BasicMemoryConfig) -> AsyncEngi
Configured async engine for Postgres
"""
# Postgres with asyncpg - pool sized for concurrent operations
# connect_args tuned for Neon serverless which scales to zero after ~5 minutes
engine = create_async_engine(
db_url,
echo=False,
@@ -208,6 +209,19 @@ def _create_postgres_engine(db_url: str, config: BasicMemoryConfig) -> AsyncEngi
pool_size=config.db_pool_size,
max_overflow=config.db_pool_overflow,
pool_recycle=config.db_pool_recycle,
connect_args={
# Disable statement cache to avoid issues with prepared statements on reconnect
"statement_cache_size": 0,
# Allow 10s for commands (Neon cold start can take 2-5s)
"command_timeout": 10,
# Allow 10s for initial connection (Neon wake-up time)
"timeout": 10,
"server_settings": {
"application_name": "basic-memory",
# Statement timeout for queries (10s to allow for cold start)
"statement_timeout": "10s",
},
},
)
logger.debug(
f"Created Postgres engine with pool_size={config.db_pool_size}, "
+10 -2
View File
@@ -284,8 +284,16 @@ class SearchService:
)
)
# Add observation rows
# Add observation rows - dedupe by permalink to avoid unique constraint violations
# Two observations with same entity/category/content generate identical permalinks
seen_permalinks: set[str] = {entity.permalink} if entity.permalink else set()
for obs in entity.observations:
obs_permalink = obs.permalink
if obs_permalink in seen_permalinks:
logger.debug(f"Skipping duplicate observation permalink: {obs_permalink}")
continue
seen_permalinks.add(obs_permalink)
# Index with parent entity's file path since that's where it's defined
obs_content_stems = "\n".join(
p for p in self._generate_variants(obs.content) if p and p.strip()
@@ -297,7 +305,7 @@ class SearchService:
title=f"{obs.category}: {obs.content[:100]}...",
content_stems=obs_content_stems,
content_snippet=obs.content,
permalink=obs.permalink,
permalink=obs_permalink,
file_path=entity.file_path,
category=obs.category,
entity_id=entity.id,