fix: durcir la validation et fiabiliser flush/arrêt idempotents
Co-authored-by: aider (openrouter/openai/gpt-5.3-codex) <aider@aider.chat>
This commit is contained in:
@ -9,6 +9,8 @@ import (
|
||||
const (
|
||||
// DefaultMaxBufferSize is the default maximum number of events per buffer
|
||||
DefaultMaxBufferSize = 10000
|
||||
// DefaultTimeWindow is used when no valid time window is provided
|
||||
DefaultTimeWindow = time.Second
|
||||
)
|
||||
|
||||
// CorrelationConfig holds the correlation configuration.
|
||||
@ -25,8 +27,8 @@ type CorrelationService struct {
|
||||
mu sync.Mutex
|
||||
bufferA *eventBuffer
|
||||
bufferB *eventBuffer
|
||||
pendingA map[string]*list.Element // key -> list element containing NormalizedEvent
|
||||
pendingB map[string]*list.Element
|
||||
pendingA map[string][]*list.Element // key -> ordered elements containing *NormalizedEvent
|
||||
pendingB map[string][]*list.Element
|
||||
timeProvider TimeProvider
|
||||
}
|
||||
|
||||
@ -60,12 +62,16 @@ func NewCorrelationService(config CorrelationConfig, timeProvider TimeProvider)
|
||||
if config.MaxBufferSize <= 0 {
|
||||
config.MaxBufferSize = DefaultMaxBufferSize
|
||||
}
|
||||
if config.TimeWindow <= 0 {
|
||||
config.TimeWindow = DefaultTimeWindow
|
||||
}
|
||||
|
||||
return &CorrelationService{
|
||||
config: config,
|
||||
bufferA: newEventBuffer(),
|
||||
bufferB: newEventBuffer(),
|
||||
pendingA: make(map[string]*list.Element),
|
||||
pendingB: make(map[string]*list.Element),
|
||||
pendingA: make(map[string][]*list.Element),
|
||||
pendingB: make(map[string][]*list.Element),
|
||||
timeProvider: timeProvider,
|
||||
}
|
||||
}
|
||||
@ -84,20 +90,29 @@ func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLo
|
||||
if event.Source == SourceA && s.config.ApacheAlwaysEmit {
|
||||
return []CorrelatedLog{NewCorrelatedLogFromEvent(event, "A")}
|
||||
}
|
||||
if event.Source == SourceB && s.config.NetworkEmit {
|
||||
return []CorrelatedLog{NewCorrelatedLogFromEvent(event, "B")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var results []CorrelatedLog
|
||||
var (
|
||||
results []CorrelatedLog
|
||||
shouldBuffer bool
|
||||
)
|
||||
|
||||
switch event.Source {
|
||||
case SourceA:
|
||||
results = s.processSourceA(event)
|
||||
results, shouldBuffer = s.processSourceA(event)
|
||||
case SourceB:
|
||||
results = s.processSourceB(event)
|
||||
results, shouldBuffer = s.processSourceB(event)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add the new event to the appropriate buffer
|
||||
s.addEvent(event)
|
||||
if shouldBuffer {
|
||||
s.addEvent(event)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
@ -112,54 +127,46 @@ func (s *CorrelationService) isBufferFull(source EventSource) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *CorrelationService) processSourceA(event *NormalizedEvent) []CorrelatedLog {
|
||||
func (s *CorrelationService) processSourceA(event *NormalizedEvent) ([]CorrelatedLog, bool) {
|
||||
key := event.CorrelationKeyFull()
|
||||
|
||||
// Look for a matching B event
|
||||
if elem, ok := s.pendingB[key]; ok {
|
||||
bEvent := elem.Value.(*NormalizedEvent)
|
||||
if s.eventsMatch(event, bEvent) {
|
||||
// Found a match!
|
||||
correlated := NewCorrelatedLog(event, bEvent)
|
||||
s.bufferB.events.Remove(elem)
|
||||
delete(s.pendingB, key)
|
||||
return []CorrelatedLog{correlated}
|
||||
}
|
||||
// Look for the first matching B event (one-to-one first match)
|
||||
if bEvent := s.findAndPopFirstMatch(s.bufferB, s.pendingB, key, func(other *NormalizedEvent) bool {
|
||||
return s.eventsMatch(event, other)
|
||||
}); bEvent != nil {
|
||||
correlated := NewCorrelatedLog(event, bEvent)
|
||||
return []CorrelatedLog{correlated}, false
|
||||
}
|
||||
|
||||
// No match found
|
||||
if s.config.ApacheAlwaysEmit {
|
||||
orphan := NewCorrelatedLogFromEvent(event, "A")
|
||||
return []CorrelatedLog{orphan}
|
||||
return []CorrelatedLog{orphan}, false
|
||||
}
|
||||
|
||||
// Keep in buffer for potential future match
|
||||
return nil
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (s *CorrelationService) processSourceB(event *NormalizedEvent) []CorrelatedLog {
|
||||
func (s *CorrelationService) processSourceB(event *NormalizedEvent) ([]CorrelatedLog, bool) {
|
||||
key := event.CorrelationKeyFull()
|
||||
|
||||
// Look for a matching A event
|
||||
if elem, ok := s.pendingA[key]; ok {
|
||||
aEvent := elem.Value.(*NormalizedEvent)
|
||||
if s.eventsMatch(aEvent, event) {
|
||||
// Found a match!
|
||||
correlated := NewCorrelatedLog(aEvent, event)
|
||||
s.bufferA.events.Remove(elem)
|
||||
delete(s.pendingA, key)
|
||||
return []CorrelatedLog{correlated}
|
||||
}
|
||||
// Look for the first matching A event (one-to-one first match)
|
||||
if aEvent := s.findAndPopFirstMatch(s.bufferA, s.pendingA, key, func(other *NormalizedEvent) bool {
|
||||
return s.eventsMatch(other, event)
|
||||
}); aEvent != nil {
|
||||
correlated := NewCorrelatedLog(aEvent, event)
|
||||
return []CorrelatedLog{correlated}, false
|
||||
}
|
||||
|
||||
// No match found - B is never emitted alone per spec
|
||||
// No match found
|
||||
if s.config.NetworkEmit {
|
||||
orphan := NewCorrelatedLogFromEvent(event, "B")
|
||||
return []CorrelatedLog{orphan}
|
||||
return []CorrelatedLog{orphan}, false
|
||||
}
|
||||
|
||||
// Keep in buffer for potential future match (but won't be emitted alone)
|
||||
return nil
|
||||
// Keep in buffer for potential future match
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func (s *CorrelationService) eventsMatch(a, b *NormalizedEvent) bool {
|
||||
@ -176,10 +183,10 @@ func (s *CorrelationService) addEvent(event *NormalizedEvent) {
|
||||
switch event.Source {
|
||||
case SourceA:
|
||||
elem := s.bufferA.events.PushBack(event)
|
||||
s.pendingA[key] = elem
|
||||
s.pendingA[key] = append(s.pendingA[key], elem)
|
||||
case SourceB:
|
||||
elem := s.bufferB.events.PushBack(event)
|
||||
s.pendingB[key] = elem
|
||||
s.pendingB[key] = append(s.pendingB[key], elem)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,22 +199,67 @@ func (s *CorrelationService) cleanExpired() {
|
||||
s.cleanBuffer(s.bufferB, s.pendingB, cutoff)
|
||||
}
|
||||
|
||||
// cleanBuffer removes expired events from a buffer (shared logic for A and B).
|
||||
func (s *CorrelationService) cleanBuffer(buffer *eventBuffer, pending map[string]*list.Element, cutoff time.Time) {
|
||||
// cleanBuffer removes expired events from a buffer.
|
||||
func (s *CorrelationService) cleanBuffer(buffer *eventBuffer, pending map[string][]*list.Element, cutoff time.Time) {
|
||||
for elem := buffer.events.Front(); elem != nil; {
|
||||
event := elem.Value.(*NormalizedEvent)
|
||||
if event.Timestamp.Before(cutoff) {
|
||||
next := elem.Next()
|
||||
key := event.CorrelationKeyFull()
|
||||
buffer.events.Remove(elem)
|
||||
if pending[key] == elem {
|
||||
pending[key] = removeElementFromSlice(pending[key], elem)
|
||||
if len(pending[key]) == 0 {
|
||||
delete(pending, key)
|
||||
}
|
||||
elem = next
|
||||
continue
|
||||
}
|
||||
// Events are inserted in arrival order; once we hit a non-expired event, stop.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CorrelationService) findAndPopFirstMatch(
|
||||
buffer *eventBuffer,
|
||||
pending map[string][]*list.Element,
|
||||
key string,
|
||||
matcher func(*NormalizedEvent) bool,
|
||||
) *NormalizedEvent {
|
||||
elements, ok := pending[key]
|
||||
if !ok || len(elements) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for idx, elem := range elements {
|
||||
other := elem.Value.(*NormalizedEvent)
|
||||
if !matcher(other) {
|
||||
continue
|
||||
}
|
||||
|
||||
buffer.events.Remove(elem)
|
||||
updated := append(elements[:idx], elements[idx+1:]...)
|
||||
if len(updated) == 0 {
|
||||
delete(pending, key)
|
||||
} else {
|
||||
break // Events are ordered, so we can stop early
|
||||
pending[key] = updated
|
||||
}
|
||||
|
||||
return other
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeElementFromSlice(elements []*list.Element, target *list.Element) []*list.Element {
|
||||
if len(elements) == 0 {
|
||||
return elements
|
||||
}
|
||||
for i, elem := range elements {
|
||||
if elem == target {
|
||||
return append(elements[:i], elements[i+1:]...)
|
||||
}
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
// Flush forces emission of remaining buffered events (for shutdown).
|
||||
@ -226,11 +278,20 @@ func (s *CorrelationService) Flush() []CorrelatedLog {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit remaining B events as orphans only if explicitly enabled
|
||||
if s.config.NetworkEmit {
|
||||
for elem := s.bufferB.events.Front(); elem != nil; elem = elem.Next() {
|
||||
event := elem.Value.(*NormalizedEvent)
|
||||
orphan := NewCorrelatedLogFromEvent(event, "B")
|
||||
results = append(results, orphan)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear buffers
|
||||
s.bufferA.events.Init()
|
||||
s.bufferB.events.Init()
|
||||
s.pendingA = make(map[string]*list.Element)
|
||||
s.pendingB = make(map[string]*list.Element)
|
||||
s.pendingA = make(map[string][]*list.Element)
|
||||
s.pendingB = make(map[string][]*list.Element)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@ -144,10 +144,14 @@ func TestCorrelationService_Flush(t *testing.T) {
|
||||
SrcPort: 8080,
|
||||
}
|
||||
|
||||
svc.ProcessEvent(apacheEvent)
|
||||
// A est émis immédiatement quand ApacheAlwaysEmit=true
|
||||
results := svc.ProcessEvent(apacheEvent)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 immediate orphan event, got %d", len(results))
|
||||
}
|
||||
|
||||
flushed := svc.Flush()
|
||||
if len(flushed) != 1 {
|
||||
t.Errorf("expected 1 flushed event, got %d", len(flushed))
|
||||
if len(flushed) != 0 {
|
||||
t.Errorf("expected 0 flushed events, got %d", len(flushed))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user