Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion go/core/internal/database/client_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"time"

Expand Down Expand Up @@ -712,13 +713,14 @@ func (c *postgresClient) SearchAgentMemory(ctx context.Context, agentName, userI
}
}

// Access-count bookkeeping is best-effort: a failure must not fail the search.
if len(results) > 0 {
ids := make([]string, len(results))
for i, r := range results {
ids[i] = r.ID
}
if err := c.q.IncrementMemoryAccessCount(ctx, ids); err != nil {
return nil, fmt.Errorf("failed to increment access count: %w", err)
log.Printf("failed to increment memory access count: %v", err)
}
Comment thread
supreme-gg-gg marked this conversation as resolved.
}

Expand Down
55 changes: 55 additions & 0 deletions go/core/internal/database/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,58 @@ func TestPruneExpiredMemories(t *testing.T) {
assert.Contains(t, ids, hotMem.ID, "Expired popular memory should have TTL extended and be retained")
assert.Contains(t, ids, liveMem.ID, "Non-expired memory should be retained")
}

// TestSearchAgentMemoryConcurrentAccessCount verifies concurrent searches over
// overlapping rows do not deadlock when incrementing access_count and still
// return results.
func TestSearchAgentMemoryConcurrentAccessCount(t *testing.T) {
db := setupTestDB(t)
client := NewClient(db)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
t.Cleanup(cancel)

agentName := "concurrent-agent"
userID := "concurrent-user"

// Small store so every search hits the same top rows (max overlap).
for i := range 5 {
err := client.StoreAgentMemory(ctx, &dbpkg.Memory{
AgentName: agentName,
UserID: userID,
Content: fmt.Sprintf("shared memory %d", i),
Embedding: makeEmbedding(float32(i+1) * 0.15),
})
require.NoError(t, err)
}

const numGoroutines = 20
const searchesPerGoroutine = 10

var wg sync.WaitGroup
errs := make(chan error, numGoroutines*searchesPerGoroutine)
wg.Add(numGoroutines)

for range numGoroutines {
go func() {
defer wg.Done()
for range searchesPerGoroutine {
results, err := client.SearchAgentMemory(ctx, agentName, userID, makeEmbedding(0.5), 5)
if err != nil {
errs <- err
return
}
if len(results) == 0 {
errs <- fmt.Errorf("expected search results, got none")
return
}
}
}()
}

wg.Wait()
close(errs)

for err := range errs {
require.NoError(t, err, "concurrent memory search must not fail")
}
}
11 changes: 9 additions & 2 deletions go/core/internal/database/gen/memory.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions go/core/internal/database/gen/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions go/core/internal/database/queries/memory.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ ORDER BY embedding <=> $1 ASC
LIMIT $4;

-- name: IncrementMemoryAccessCount :exec
UPDATE memory SET access_count = access_count + 1
WHERE id = ANY($1::text[]);
-- Lock rows in id order to avoid deadlocks between concurrent overlapping increments.
UPDATE memory
SET access_count = access_count + 1
WHERE id IN (
SELECT id FROM memory
WHERE id = ANY($1::text[])
ORDER BY id
FOR UPDATE
);

-- name: ListAgentMemories :many
SELECT * FROM memory
Expand Down
Loading