replace Redis with an embedded SQLite string->string KV store (group commit, in-tx events, SQL search)
Build and Push Docker Image / build (push) Successful in 2m44s

This commit is contained in:
Worph
2026-10-19 20:51:00 +00:00
parent c53146e514
commit dc5cbdfb9b
80 changed files with 4601 additions and 6077 deletions
+10 -10
View File
@@ -11,7 +11,7 @@ import (
)
const (
// EventsStream is the Redis Stream name for file events
// EventsStream is the stream name for file events
EventsStream = "file:events"
// EventsStreamMaxLen is the approximate cap on stream length. Bounded
@@ -20,7 +20,7 @@ const (
EventsStreamMaxLen = 100_000
)
// Dispatcher sends file events to Redis Stream
// Dispatcher sends file events to the file:events stream
type Dispatcher struct {
mu sync.RWMutex
storageClient *storage.Client
@@ -31,20 +31,20 @@ func NewDispatcher() *Dispatcher {
return &Dispatcher{}
}
// SetStorageClient sets the storage client for Redis Stream publishing
// SetStorageClient sets the storage client for file:events publishing
func (d *Dispatcher) SetStorageClient(client *storage.Client) {
d.mu.Lock()
defer d.mu.Unlock()
d.storageClient = client
log.Println("[Dispatcher] Storage client set for Redis Stream publishing")
log.Println("[Dispatcher] Storage client set for file:events publishing")
}
// Dispatch sends an event to Redis Stream
// Dispatch sends an event to the file:events stream
func (d *Dispatcher) Dispatch(event FileEvent) {
go d.dispatchToStream(event)
}
// dispatchToStream publishes event to Redis Stream for service consumption
// dispatchToStream publishes event to the file:events stream for service consumption
func (d *Dispatcher) dispatchToStream(event FileEvent) {
d.mu.RLock()
client := d.storageClient
@@ -114,7 +114,7 @@ func (d *Dispatcher) EmitReset(watcherID string) error {
// Emit reset event as the first message in the fresh stream
event := FileEvent{
Type: EventTypeReset,
Path: "", // No specific path for reset
Path: "", // No specific path for reset
WatcherID: watcherID,
Timestamp: NowMS(),
}
@@ -125,7 +125,7 @@ func (d *Dispatcher) EmitReset(watcherID string) error {
return nil
}
// GetRecentEvents reads recent events from the Redis stream, newest first.
// GetRecentEvents reads recent events from the stream, newest first.
// If sinceMS > 0, only events strictly newer than that timestamp are returned.
// limit caps the number of returned events (defaults to 100 when <= 0).
func (d *Dispatcher) GetRecentEvents(sinceMS int64, limit int) ([]FileEvent, error) {
@@ -160,7 +160,7 @@ func (d *Dispatcher) GetRecentEvents(sinceMS int64, limit int) ([]FileEvent, err
return events, nil
}
// parseStreamEntry converts a Redis stream entry back into a FileEvent.
// parseStreamEntry converts a stream entry back into a FileEvent.
// All fields are stored as strings by XADD, so numerics need parsing.
func parseStreamEntry(entry storage.StreamEntry) FileEvent {
event := FileEvent{}
@@ -192,7 +192,7 @@ func parseStreamEntry(entry storage.StreamEntry) FileEvent {
}
// Fall back to the stream ID's millisecond prefix if the timestamp
// field is missing or unparseable — every Redis stream ID starts with ms.
// field is missing or unparseable — every stream ID starts with ms.
if event.Timestamp == 0 && entry.ID != "" {
if dash := strings.IndexByte(entry.ID, '-'); dash > 0 {
if n, err := strconv.ParseInt(entry.ID[:dash], 10, 64); err == nil {
+2 -2
View File
@@ -25,7 +25,7 @@ func NewHandlers(watcher *Watcher, dispatcher *Dispatcher) *Handlers {
// RegisterRoutes registers all watcher-related routes
func (h *Handlers) RegisterRoutes(r *mux.Router) {
// Event polling (for services that don't use Redis Streams)
// Event polling (for services that don't consume the SSE stream)
r.HandleFunc("/api/events/poll", h.handlePoll).Methods("GET")
// Deprecated scan endpoints - redirect to /api/watchers
@@ -61,7 +61,7 @@ func (h *Handlers) handlePoll(w http.ResponseWriter, r *http.Request) {
}
}
// Read events from the Redis `file:events` stream rather than the
// Read events from the `file:events` stream rather than the
// in-memory buffer. The buffer is fed by the unused debouncer path,
// so it is always empty in practice — the stream is the source of truth.
events, err := h.dispatcher.GetRecentEvents(sinceMS, limit)
+1 -1
View File
@@ -16,7 +16,7 @@ const (
// FileEvent represents a file system event
type FileEvent struct {
Type FileEventType `json:"type"`
Path string `json:"path"` // Relative to FILES_PATH
Path string `json:"path"` // Relative to FILES_PATH
Size int64 `json:"size,omitempty"`
Timestamp int64 `json:"timestamp"`
MidHash256 string `json:"midhash256,omitempty"` // midhash256 CID (SHA-256 of middle 1MB + file size)
+6 -8
View File
@@ -11,7 +11,6 @@ import (
"github.com/metazla/meta-core/internal/storage"
)
// Watcher provides file scanning and event dispatching
type Watcher struct {
config *config.Config
@@ -26,8 +25,8 @@ type Watcher struct {
}
// SetStorageClient lets the watcher persist the (path, size, mtime, midhash)
// tuple to Redis whenever it computes a midhash. Without this, only the
// in-memory state registry knows about the file — Redis would only learn
// tuple to the store whenever it computes a midhash. Without this, only the
// in-memory state registry knows about the file — the store would only learn
// about it via downstream consumers of the event stream.
func (w *Watcher) SetStorageClient(s *storage.Client) {
w.storage = s
@@ -35,7 +34,7 @@ func (w *Watcher) SetStorageClient(s *storage.Client) {
// writeFileTuple persists path/size/mtime/midhash for a freshly-hashed file.
// Caller already has all four values from its existing os.Stat / FileInfo —
// no extra IO is performed here beyond the Redis writes themselves.
// no extra IO is performed here beyond the store writes themselves.
//
// Flow:
// - Look up the midhash CID in the reverse index (cid:<midhash> → uuid)
@@ -46,7 +45,7 @@ func (w *Watcher) SetStorageClient(s *storage.Client) {
// record the new path in the duplicates set and refresh size/mtime
//
// This is also the inverse-index that lets a fresh meta-core boot skip
// re-mid-hashing files whose (path, size, mtime) match what's in Redis,
// re-mid-hashing files whose (path, size, mtime) match what's stored,
// via HydrateStateFromStorage below.
func (w *Watcher) writeFileTuple(absPath string, size, mtimeNano int64, midhash string) {
if w.storage == nil || midhash == "" {
@@ -270,7 +269,7 @@ func (w *Watcher) ScanDirectoryDiff(root string, watcherID string) DiffScanResul
}
// WatcherRoot pairs a watcher's id with the absolute path it watches. The
// hydrator uses Root to decide which Redis-tracked file belongs to which
// hydrator uses Root to decide which stored file belongs to which
// in-memory WatcherState.
type WatcherRoot struct {
ID string
@@ -278,7 +277,7 @@ type WatcherRoot struct {
}
// HydrateStateFromStorage repopulates the per-watcher in-memory state from
// the (filePath, sizeByte, mtimeNano) tuples in Redis. After this runs, the
// the (filePath, sizeByte, mtimeNano) tuples in the store. After this runs, the
// next scan tick will see existing files as "known" and skip
// ComputeMidHash256 for any whose size+mtime match — turning a cold-start
// rescan from minutes to seconds on a populated library.
@@ -416,4 +415,3 @@ func (w *Watcher) GetRecentEvents(sinceMS int64, limit int) []FileEvent {
func (w *Watcher) GetFilesPath() string {
return w.filesPath
}