Files
next.orly.dev/pkg/neo4j/access_tracking.go
woikos 8a14cec3cd Add archive relay query augmentation and access-based GC (v0.45.0)
- Add async archive relay querying (local results immediate, archives in background)
- Add query caching with filter normalization to avoid repeated requests
- Add session-deduplicated access tracking for events
- Add continuous garbage collection based on access patterns
- Auto-detect storage limit (80% of filesystem) when ORLY_MAX_STORAGE_BYTES=0
- Support NIP-50 search queries to archive relays

New environment variables:
- ORLY_ARCHIVE_ENABLED: Enable archive relay query augmentation
- ORLY_ARCHIVE_RELAYS: Comma-separated archive relay URLs
- ORLY_ARCHIVE_TIMEOUT_SEC: Archive query timeout
- ORLY_ARCHIVE_CACHE_TTL_HRS: Query deduplication window
- ORLY_GC_ENABLED: Enable access-based garbage collection
- ORLY_MAX_STORAGE_BYTES: Max storage (0=auto 80%)
- ORLY_GC_INTERVAL_SEC: GC check interval
- ORLY_GC_BATCH_SIZE: Events per GC cycle

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 19:35:16 +01:00

95 lines
2.4 KiB
Go

package neo4j
import (
"context"
"fmt"
"time"
)
// RecordEventAccess updates access tracking for an event in Neo4j.
// This creates or updates an AccessTrack node for the event.
func (n *N) RecordEventAccess(serial uint64, connectionID string) error {
cypher := `
MERGE (a:AccessTrack {serial: $serial})
ON CREATE SET a.lastAccess = $now, a.count = 1
ON MATCH SET a.lastAccess = $now, a.count = a.count + 1`
params := map[string]any{
"serial": int64(serial), // Neo4j uses int64
"now": time.Now().Unix(),
}
_, err := n.ExecuteWrite(context.Background(), cypher, params)
if err != nil {
return fmt.Errorf("failed to record event access: %w", err)
}
return nil
}
// GetEventAccessInfo returns access information for an event.
func (n *N) GetEventAccessInfo(serial uint64) (lastAccess int64, accessCount uint32, err error) {
cypher := "MATCH (a:AccessTrack {serial: $serial}) RETURN a.lastAccess AS lastAccess, a.count AS count"
params := map[string]any{"serial": int64(serial)}
result, err := n.ExecuteRead(context.Background(), cypher, params)
if err != nil {
return 0, 0, fmt.Errorf("failed to get event access info: %w", err)
}
ctx := context.Background()
if result.Next(ctx) {
record := result.Record()
if record != nil {
if la, found := record.Get("lastAccess"); found {
if v, ok := la.(int64); ok {
lastAccess = v
}
}
if c, found := record.Get("count"); found {
if v, ok := c.(int64); ok {
accessCount = uint32(v)
}
}
}
}
return lastAccess, accessCount, nil
}
// GetLeastAccessedEvents returns event serials sorted by coldness.
func (n *N) GetLeastAccessedEvents(limit int, minAgeSec int64) (serials []uint64, err error) {
cutoffTime := time.Now().Unix() - minAgeSec
cypher := `
MATCH (a:AccessTrack)
WHERE a.lastAccess < $cutoff
RETURN a.serial AS serial, a.lastAccess AS lastAccess, a.count AS count
ORDER BY (a.lastAccess + a.count * 3600) ASC
LIMIT $limit`
params := map[string]any{
"cutoff": cutoffTime,
"limit": limit,
}
result, err := n.ExecuteRead(context.Background(), cypher, params)
if err != nil {
return nil, fmt.Errorf("failed to get least accessed events: %w", err)
}
ctx := context.Background()
for result.Next(ctx) {
record := result.Record()
if record != nil {
if s, found := record.Get("serial"); found {
if v, ok := s.(int64); ok {
serials = append(serials, uint64(v))
}
}
}
}
return serials, nil
}