- Replace hardcoded FLOAT[768] DDL with create_embeddings_ddl() using EMBEDDING_DIM constant - Add with_connection_blocking() async method to avoid blocking tokio runtime - Use spawn_blocking in query_memory and revoke_memory gRPC handlers - Remove unused uuid dependency Closes #114 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.2 KiB
Implementation Plan — Issue #114: Tech debt: minor findings from issue #28 review
Metadata
| Field | Value |
|---|---|
| Issue | #114 |
| Title | Tech debt: minor findings from issue #28 review |
| Milestone | Phase 4: Memory Service |
| Labels | tech-debt |
| Status | PLANNED |
| Language | Rust |
| Related Plans | issue-028.md, issue-033.md |
| Blocked by | — |
Acceptance Criteria
EMBEDDING_DIMconstant andCREATE_EMBEDDINGSDDL cannot drift out of syncwith_connection()does not block the tokio runtime when called from async handlers- Unused dependencies removed from
Cargo.toml
Architecture Analysis
Service Context
- All changes are internal to
services/memory. - No proto or gRPC API changes required.
Existing Patterns
services/memory/src/db/schema.rs— DDL constants andEMBEDDING_DIMservices/memory/src/db/mod.rs—DuckDbManagerwithstd::sync::Mutexservices/memory/Cargo.toml— dependency list
Implementation Steps
1. Sync EMBEDDING_DIM with DDL (schema.rs)
Option A (recommended): Generate DDL dynamically via format!
Replace the static CREATE_EMBEDDINGS constant with a function that builds the DDL string using EMBEDDING_DIM:
fn create_embeddings_ddl() -> String {
format!(
"CREATE TABLE IF NOT EXISTS embeddings (
memory_id VARCHAR NOT NULL REFERENCES memories(id),
embedding_type VARCHAR NOT NULL,
vector FLOAT[{EMBEDDING_DIM}] NOT NULL,
PRIMARY KEY (memory_id, embedding_type)
)"
)
}
Update apply_v1() to call create_embeddings_ddl() instead of referencing the CREATE_EMBEDDINGS constant.
Option B (alternative): Add a compile-time assertion
Keep the static string but add a const_assert! (via static_assertions crate) or a unit test that parses the DDL and verifies the dimension matches EMBEDDING_DIM. This is simpler but less robust.
Decision: Go with Option A. It is straightforward, requires no extra crates, and eliminates the drift risk entirely.
2. Avoid blocking tokio with std::sync::Mutex (mod.rs)
DuckDB's Connection is !Sync, so tokio::sync::Mutex alone would not help (the connection cannot be held across .await points anyway). The correct fix is to use tokio::task::spawn_blocking at the call sites.
Changes:
- Keep
DuckDbManagerusingstd::sync::Mutexinternally (this is correct for!Synctypes). - Add a new async helper method on
DuckDbManager:
/// Execute a closure with the connection on a blocking thread,
/// avoiding stalling the tokio runtime.
pub async fn with_connection_blocking<F, T>(&self, f: F) -> Result<T, DbError>
where
F: FnOnce(&Connection) -> Result<T, DbError> + Send + 'static,
T: Send + 'static,
{
// self needs to be behind Arc for this to work
// Acquire lock inside spawn_blocking
}
Since DuckDbManager is already shared via Arc<DuckDbManager>, wrap the call in spawn_blocking:
DuckDbManagermust beSend + Sync(it already is becauseMutex<Connection>isSync).- Clone the
Arcand move it into thespawn_blockingclosure, then callwith_connectioninside. - Update gRPC handler call sites (in
services/memory/src/service.rs) to use the async variant.
Retain the synchronous with_connection() for use in tests and non-async contexts.
3. Remove unused uuid dependency (Cargo.toml)
- Remove
uuid = { version = "1", features = ["v4"] }from[dependencies]. chronois actively used (timestamp parsing in retrieval and provenance modules), so it stays.
4. Tests
- Item 1: Existing
test_insert_embeddingandtest_vector_similarity_queryinschema.rsalready exercise DDL creation withEMBEDDING_DIM. Verify they still pass after switching to the function. - Item 2: Add a
#[tokio::test]that callswith_connection_blockingto confirm it runs without blocking. Existing sync tests forwith_connectionremain unchanged. - Item 3: Run
cargo build— ifuuidis used anywhere, the build will fail, catching any oversight.
Files to Create/Modify
| File | Action | Purpose |
|---|---|---|
services/memory/src/db/schema.rs |
Modify | Replace CREATE_EMBEDDINGS constant with create_embeddings_ddl() function |
services/memory/src/db/mod.rs |
Modify | Add with_connection_blocking async method |
services/memory/src/service.rs |
Modify | Switch gRPC handlers from with_connection to with_connection_blocking |
services/memory/Cargo.toml |
Modify | Remove uuid dependency |
Risks and Edge Cases
spawn_blockinglifetime requirements: The closure passed tospawn_blockingmust be'static. This means data borrowed from the request must be cloned/owned before entering the closure. gRPC handlers already receive owned types, so this should be straightforward.format!-based DDL: The generated string is created at runtime rather than compile time. This is negligible overhead since it only runs once during migration.
Deviation Log
(Filled during implementation if deviations from plan occur)
| Deviation | Reason |
|---|