Implement event-driven daemon improvements for bd-85

- Add mutation events for label/dep/comment operations
- Create separate export-only and import-only functions
- Add dropped events counter with safety net export
- Complete bd-80 mutation channel implementation

Event-driven mode now:
- Emits mutation events for ALL write operations (not just create/update/close)
- Uses createExportFunc() for mutations (export+commit/push only, no pull)
- Uses createAutoImportFunc() for file changes (pull+import only, no export)
- Tracks dropped events and triggers safety export every 60s if any dropped
- Achieves <500ms latency target by avoiding full sync on each trigger

Behind BEADS_DAEMON_MODE=events flag (poll is still default)
This commit is contained in:
Steve Yegge
2025-10-29 11:22:29 -07:00
parent 55f803a7c9
commit fea86f9b31
5 changed files with 182 additions and 12 deletions

View File

@@ -47,7 +47,8 @@ type Server struct {
// Auto-import single-flight guard
importInProgress atomic.Bool
// Mutation events for event-driven daemon
mutationChan chan MutationEvent
mutationChan chan MutationEvent
droppedEvents atomic.Int64 // Counter for dropped mutation events
}
// MutationEvent represents a database mutation for event-driven sync
@@ -105,7 +106,8 @@ func (s *Server) emitMutation(eventType, issueID string) {
}:
// Event sent successfully
default:
// Channel full, event dropped (not critical - sync will happen eventually)
// Channel full, increment dropped events counter
s.droppedEvents.Add(1)
}
}
@@ -113,3 +115,13 @@ func (s *Server) emitMutation(eventType, issueID string) {
func (s *Server) MutationChan() <-chan MutationEvent {
return s.mutationChan
}
// DroppedEventsCount returns the number of dropped mutation events
func (s *Server) DroppedEventsCount() int64 {
return s.droppedEvents.Load()
}
// ResetDroppedEventsCount resets the dropped events counter and returns the previous value
func (s *Server) ResetDroppedEventsCount() int64 {
return s.droppedEvents.Swap(0)
}

View File

@@ -34,12 +34,15 @@ func (s *Server) handleDepAdd(req *Request) Response {
}
}
// Emit mutation event for event-driven daemon
s.emitMutation("update", depArgs.FromID)
return Response{Success: true}
}
// Generic handler for simple store operations with standard error handling
func (s *Server) handleSimpleStoreOp(req *Request, argsPtr interface{}, argDesc string,
opFunc func(context.Context, storage.Storage, string) error) Response {
opFunc func(context.Context, storage.Storage, string) error, issueID string) Response {
if err := json.Unmarshal(req.Args, argsPtr); err != nil {
return Response{
Success: false,
@@ -57,6 +60,9 @@ func (s *Server) handleSimpleStoreOp(req *Request, argsPtr interface{}, argDesc
}
}
// Emit mutation event for event-driven daemon
s.emitMutation("update", issueID)
return Response{Success: true}
}
@@ -64,21 +70,21 @@ func (s *Server) handleDepRemove(req *Request) Response {
var depArgs DepRemoveArgs
return s.handleSimpleStoreOp(req, &depArgs, "dep remove", func(ctx context.Context, store storage.Storage, actor string) error {
return store.RemoveDependency(ctx, depArgs.FromID, depArgs.ToID, actor)
})
}, depArgs.FromID)
}
func (s *Server) handleLabelAdd(req *Request) Response {
var labelArgs LabelAddArgs
return s.handleSimpleStoreOp(req, &labelArgs, "label add", func(ctx context.Context, store storage.Storage, actor string) error {
return store.AddLabel(ctx, labelArgs.ID, labelArgs.Label, actor)
})
}, labelArgs.ID)
}
func (s *Server) handleLabelRemove(req *Request) Response {
var labelArgs LabelRemoveArgs
return s.handleSimpleStoreOp(req, &labelArgs, "label remove", func(ctx context.Context, store storage.Storage, actor string) error {
return store.RemoveLabel(ctx, labelArgs.ID, labelArgs.Label, actor)
})
}, labelArgs.ID)
}
func (s *Server) handleCommentList(req *Request) Response {
@@ -128,6 +134,9 @@ func (s *Server) handleCommentAdd(req *Request) Response {
}
}
// Emit mutation event for event-driven daemon
s.emitMutation("comment", commentArgs.ID)
data, _ := json.Marshal(comment)
return Response{
Success: true,