diff --git a/pkg/cmd/initialize/sigs.go b/pkg/cmd/initialize/sigs.go index 25a464e3bc6d..4255da57199f 100644 --- a/pkg/cmd/initialize/sigs.go +++ b/pkg/cmd/initialize/sigs.go @@ -98,6 +98,7 @@ func CreateEventsFromSignatures(startId events.ID, sigs []detect.Signature) map[ []events.Probe{}, []events.TailCall{}, events.Capabilities{}, + []events.DependenciesFallback{}, ), []trace.ArgMeta{}, properties, diff --git a/pkg/cmd/initialize/sigs_test.go b/pkg/cmd/initialize/sigs_test.go index 7d66220b4d9b..1b49e52ed88a 100644 --- a/pkg/cmd/initialize/sigs_test.go +++ b/pkg/cmd/initialize/sigs_test.go @@ -48,6 +48,7 @@ func Test_CreateEventsFromSigs(t *testing.T) { []events.Probe{}, []events.TailCall{}, events.Capabilities{}, + []events.DependenciesFallback{}, ), []trace.ArgMeta{}, nil, @@ -84,6 +85,7 @@ func Test_CreateEventsFromSigs(t *testing.T) { []events.Probe{}, []events.TailCall{}, events.Capabilities{}, + []events.DependenciesFallback{}, ), []trace.ArgMeta{}, nil, @@ -107,6 +109,7 @@ func Test_CreateEventsFromSigs(t *testing.T) { []events.Probe{}, []events.TailCall{}, events.Capabilities{}, + []events.DependenciesFallback{}, ), []trace.ArgMeta{}, nil, @@ -145,6 +148,7 @@ func Test_CreateEventsFromSigs(t *testing.T) { []events.Probe{}, []events.TailCall{}, events.Capabilities{}, + []events.DependenciesFallback{}, ), []trace.ArgMeta{}, nil, diff --git a/pkg/ebpf/tracee.go b/pkg/ebpf/tracee.go index 2f6b5c01d021..40564f87199f 100644 --- a/pkg/ebpf/tracee.go +++ b/pkg/ebpf/tracee.go @@ -211,6 +211,33 @@ func (t *Tracee) addDependencyEventToState(evtID events.ID, dependentEvts []even } } +// updateDependenciesStateRecursive change all dependencies submit states to match +// their submit states, and current submit state of their dependents. +// This should be called in the case of a fallback dependencies, as the events +// dependencies change, on the older dependencies. +// This should make sure that their submit will match their new dependents and +// emit state. +func (t *Tracee) updateDependenciesStateRecursive(eventNode *dependencies.EventNode) { + for _, dependencyEventID := range eventNode.GetDependencies().GetIDs() { + dependencyNode, err := t.eventsDependencies.GetEvent(dependencyEventID) + if err != nil { // event does not exist anymore in dependencies + t.removeEventFromState(dependencyEventID) + continue + } + dependencyState := t.eventsState[dependencyEventID] + newState := events.EventState{ + Emit: dependencyState.Emit, + Submit: dependencyState.Emit, + } + for _, dependantID := range dependencyNode.GetDependents() { + dependantState := t.eventsState[dependantID] + newState.Submit |= dependantState.Submit + } + t.eventsState[dependencyEventID] = newState + t.updateDependenciesStateRecursive(dependencyNode) + } +} + func (t *Tracee) removeEventFromState(evtID events.ID) { logger.Debugw("Remove event from state", "event", events.Core.GetDefinitionByID(evtID).GetName()) delete(t.eventsState, evtID) @@ -270,6 +297,23 @@ func New(cfg config.Config) (*Tracee, error) { t.removeEventFromState(eventNode.GetID()) return nil }) + t.eventsDependencies.SubscribeChange( + dependencies.EventNodeType, + func(oldNode interface{}, newNode interface{}) []dependencies.Action { + oldEventNode, ok := oldNode.(*dependencies.EventNode) + if !ok { + logger.Errorw("Got node from type not requested") + return nil + } + newEventNode, ok := newNode.(*dependencies.EventNode) + if !ok { + logger.Errorw("Got node from type not requested") + return nil + } + t.updateDependenciesStateRecursive(oldEventNode) + t.addDependenciesToStateRecursive(newEventNode) + return nil + }) // Initialize capabilities rings soon @@ -365,6 +409,18 @@ func New(cfg config.Config) (*Tracee, error) { if err != nil { return t, errfmt.WrapError(err) } + // Also add all capabilities required by fallbacks + for _, fallback := range deps.GetFallbacks() { + fallbackCaps := fallback.GetDependencies().GetCapabilities() + err = caps.BaseRingAdd(fallbackCaps.GetBase()...) + if err != nil { + return t, errfmt.WrapError(err) + } + err = caps.BaseRingAdd(fallbackCaps.GetEBPF()...) + if err != nil { + return t, errfmt.WrapError(err) + } + } } } @@ -1076,7 +1132,7 @@ func (t *Tracee) validateKallsymsDependencies() { for eventId := range t.eventsState { if !validateEvent(eventId) { // Cancel the event, its dependencies and its dependent events - err := t.eventsDependencies.RemoveEvent(eventId) + _, err := t.eventsDependencies.FailEvent(eventId) if err != nil { logger.Warnw("Failed to remove event from dependencies manager", "remove reason", "missing ksymbols", "error", err) } @@ -1312,7 +1368,7 @@ func (t *Tracee) attachProbes() error { for eventID := range t.eventsState { err := t.attachEvent(eventID) if err != nil { - err := t.eventsDependencies.RemoveEvent(eventID) + _, err = t.eventsDependencies.FailEvent(eventID) if err != nil { logger.Warnw("Failed to remove event from dependencies manager", "remove reason", "failed probes attachment", "error", err) } diff --git a/pkg/events/definition_dependencies.go b/pkg/events/definition_dependencies.go index 563189d076cc..ce709569136e 100644 --- a/pkg/events/definition_dependencies.go +++ b/pkg/events/definition_dependencies.go @@ -13,6 +13,7 @@ type Dependencies struct { probes []Probe tailCalls []TailCall capabilities Capabilities + fallbacks []DependenciesFallback } func NewDependencies( @@ -21,6 +22,7 @@ func NewDependencies( givenProbes []Probe, givenTailCalls []TailCall, givenCapabilities Capabilities, + fallbacks []DependenciesFallback, ) Dependencies { return Dependencies{ ids: givenIDs, @@ -28,6 +30,7 @@ func NewDependencies( probes: givenProbes, tailCalls: givenTailCalls, capabilities: givenCapabilities, + fallbacks: fallbacks, } } @@ -73,6 +76,13 @@ func (d Dependencies) GetCapabilities() Capabilities { return d.capabilities } +func (d Dependencies) GetFallbacks() []DependenciesFallback { + if d.fallbacks == nil { + return []DependenciesFallback{} + } + return d.fallbacks +} + // Probe type Probe struct { @@ -176,3 +186,21 @@ func (tc TailCall) GetMapName() string { func (tc TailCall) GetProgName() string { return tc.progName } + +// DependenciesFallback is a struct representing a set of fallback dependencies +// to be utilized in case of issues with the event's primary dependencies. +// This struct envelopes the dependencies, providing the flexibility to incorporate +// future logic for determining whether to employ the fallback or not +type DependenciesFallback struct { + dependencies Dependencies +} + +func NewDependenciesFallback(dependencies Dependencies) DependenciesFallback { + return DependenciesFallback{ + dependencies: dependencies, + } +} + +func (df DependenciesFallback) GetDependencies() Dependencies { + return df.dependencies +} diff --git a/pkg/events/dependencies/errors.go b/pkg/events/dependencies/errors.go index e96c1df2a41c..fa2e503de041 100644 --- a/pkg/events/dependencies/errors.go +++ b/pkg/events/dependencies/errors.go @@ -24,6 +24,11 @@ func (cancelErr *ErrNodeAddCancelled) Error() string { return fmt.Sprintf("node add was cancelled, reasons: \"%s\"", strings.Join(errorsStrings, "\", \"")) } +func (cancelErr *ErrNodeAddCancelled) Is(err error) bool { + _, ok := err.(*ErrNodeAddCancelled) + return ok +} + func (cancelErr *ErrNodeAddCancelled) AddReason(reason error) { cancelErr.Reasons = append(cancelErr.Reasons, reason) } diff --git a/pkg/events/dependencies/event.go b/pkg/events/dependencies/event.go index 79e35e203085..a4607836b4d5 100644 --- a/pkg/events/dependencies/event.go +++ b/pkg/events/dependencies/event.go @@ -14,7 +14,8 @@ type EventNode struct { dependencies events.Dependencies // There won't be more than a couple of dependents, so a slice is better for // both performance and supporting efficient thread-safe operation in the future - dependents []events.ID + dependents []events.ID + currentFallback int // The index of current fallback dependencies } func newDependenciesNode(id events.ID, dependencies events.Dependencies, chosenDirectly bool) *EventNode { @@ -23,6 +24,7 @@ func newDependenciesNode(id events.ID, dependencies events.Dependencies, chosenD explicitlySelected: chosenDirectly, dependencies: dependencies, dependents: make([]events.ID, 0), + currentFallback: -1, } } @@ -31,7 +33,11 @@ func (en *EventNode) GetID() events.ID { } func (en *EventNode) GetDependencies() events.Dependencies { - return en.dependencies + if en.currentFallback < 0 { + return en.dependencies + } + fallbacks := en.dependencies.GetFallbacks() + return fallbacks[en.currentFallback].GetDependencies() } func (en *EventNode) GetDependents() []events.ID { @@ -71,3 +77,22 @@ func (en *EventNode) removeDependent(dependent events.ID) { } } } + +func (en *EventNode) fallback() bool { + fallbacks := en.dependencies.GetFallbacks() + if (en.currentFallback + 1) >= len(fallbacks) { + return false + } + en.currentFallback += 1 + return true +} + +func (en *EventNode) clone() *EventNode { + clone := &EventNode{ + id: en.id, + explicitlySelected: en.explicitlySelected, + dependencies: en.dependencies, + dependents: slices.Clone[[]events.ID](en.dependents), + } + return clone +} diff --git a/pkg/events/dependencies/manager.go b/pkg/events/dependencies/manager.go index dae0fc918524..d2bfdfaeb19e 100644 --- a/pkg/events/dependencies/manager.go +++ b/pkg/events/dependencies/manager.go @@ -1,6 +1,7 @@ package dependencies import ( + "errors" "fmt" "reflect" @@ -28,6 +29,7 @@ type Manager struct { probes map[probes.Handle]*ProbeNode onAdd map[NodeType][]func(node interface{}) []Action onRemove map[NodeType][]func(node interface{}) []Action + onChange map[NodeType][]func(previousNode interface{}, newNode interface{}) []Action dependenciesGetter func(events.ID) events.Dependencies } @@ -37,6 +39,7 @@ func NewDependenciesManager(dependenciesGetter func(events.ID) events.Dependenci probes: make(map[probes.Handle]*ProbeNode), onAdd: make(map[NodeType][]func(node interface{}) []Action), onRemove: make(map[NodeType][]func(node interface{}) []Action), + onChange: make(map[NodeType][]func(previousNode interface{}, newNode interface{}) []Action), dependenciesGetter: dependenciesGetter, } } @@ -53,6 +56,12 @@ func (m *Manager) SubscribeRemove(subscribeType NodeType, onRemove func(node int m.onRemove[subscribeType] = append([]func(node interface{}) []Action{onRemove}, m.onRemove[subscribeType]...) } +// SubscribeChange adds a watcher function called upon the change of an event in the tree. +// Change watchers are called in the order of their subscription. +func (m *Manager) SubscribeChange(subscribeType NodeType, onChange func(previousNode interface{}, newNode interface{}) []Action) { + m.onChange[subscribeType] = append([]func(previousNode interface{}, newNode interface{}) []Action{onChange}, m.onChange[subscribeType]...) +} + // GetEvent returns the dependencies of the given event. func (m *Manager) GetEvent(id events.ID) (*EventNode, error) { node := m.getEventNode(id) @@ -105,10 +114,30 @@ func (m *Manager) RemoveEvent(id events.ID) error { if node == nil { return ErrNodeNotFound } - m.removeEventNodeFromDependencies(node) - m.removeNode(node) - m.removeEventDependents(node) - return nil + return m.removeEvent(node) +} + +// FailEvent is similar to RemoveEvent, except for the fact that instead of +// removing the current event it will try to use its fallback dependencies. +// The old events dependencies of it will be removed in any case. +// The event will be removed if it has no fallback though, and with it the events +// that depend on it. +// The return value specifies if the event was removed or not from the tree +func (m *Manager) FailEvent(id events.ID) (bool, error) { + node := m.getEventNode(id) + if node == nil { + return false, ErrNodeNotFound + } + fallback, err := m.failEvent(node) + if err != nil { + // The more crucial error is the one of the failure + _ = m.removeEvent(node) + return true, err + } + if !fallback { + return true, m.removeEvent(node) + } + return false, nil } // buildEvent adds a new node for the given event if it does not exist in the tree. @@ -136,15 +165,26 @@ func (m *Manager) buildEvent(id events.ID, dependentEvents []events.ID) (*EventN } _, err := m.buildEventNode(node) if err != nil { - m.removeEventNodeFromDependencies(node) - return nil, err + // Try to fallback on dependencies before cancelling addition + fallback, failError := m.failEvent(node) + if failError != nil { + return nil, failError + } + if !fallback { + return nil, err + } } err = m.addNode(node) if err != nil { - m.removeEventNodeFromDependencies(node) - // As the add watchers were called, remove watchers need to be called to clean after them. - m.triggerOnRemove(node) - return nil, err + // Try to fallback on dependencies before cancelling addition + fallback, failError := m.failEvent(node) + if failError != nil || !fallback { + m.triggerOnRemove(node) + if failError != nil { + return nil, failError + } + return nil, err + } } return node, nil } @@ -208,9 +248,22 @@ func (m *Manager) addNode(node interface{}) error { err = m.triggerOnAdd(node) if err != nil { - return err + if errors.Is(err, &ErrNodeAddCancelled{}) && nodeType == EventNodeType { + fallback, failErr := m.failEvent(node.(*EventNode)) + if failErr != nil { + return failErr + } + // If succeeded to fallback - move on with addition + if fallback { + goto addNode + } + return nil + } else { + return err + } } +addNode: switch nodeType { case EventNodeType: m.addEventNode(node.(*EventNode)) @@ -290,6 +343,43 @@ func (m *Manager) triggerOnRemove(node interface{}) { } } +// triggerOnChange triggers all on-change watchers +func (m *Manager) triggerOnChange(previousNode interface{}, newNode interface{}) error { + nodeType, err := getNodeType(newNode) + if err != nil { + logger.Debugw("failed to get node type", "error", err) + return ErrNodeType + } + + var actions []Action + changeWatchers := m.onChange[nodeType] + for _, onChange := range changeWatchers { + actions = append(actions, onChange(previousNode, newNode)...) + } + changeWatchers = m.onChange[AllNodeTypes] + for _, onChange := range changeWatchers { + actions = append(actions, onChange(previousNode, newNode)...) + } + + var cancelNodeAddErr *ErrNodeAddCancelled + shouldCancel := false + for _, action := range actions { + switch typedAction := action.(type) { + case *CancelNodeAddAction: + shouldCancel = true + if cancelNodeAddErr == nil { + err = NewErrNodeAddCancelled([]error{typedAction.Reason}) + } else { + cancelNodeAddErr.AddReason(typedAction.Reason) + } + } + } + if shouldCancel { + return cancelNodeAddErr + } + return nil +} + func getNodeType(node interface{}) (NodeType, error) { switch node.(type) { case *EventNode: @@ -375,3 +465,39 @@ func (m *Manager) addProbe(probeNode *ProbeNode) { func (m *Manager) removeProbe(handle *ProbeNode) { delete(m.probes, handle.GetHandle()) } + +// The return value specifies if the event succeeded in fallback +func (m *Manager) failEvent(eventNode *EventNode) (bool, error) { + if eventNode == nil { + return false, ErrNodeNotFound + } + // Event can have multiple fallbacks, so try in a loop until fail to fallback + for { + clonedNode := eventNode.clone() + m.removeEventNodeFromDependencies(eventNode) + if !eventNode.fallback() { + return false, nil + } + _, err := m.buildEventNode(eventNode) + if err != nil { + return false, err + } + err = m.triggerOnChange(clonedNode, eventNode) + if err != nil { + if errors.Is(err, &ErrNodeAddCancelled{}) { + continue + } + return false, err + } + // Fallback succeeded + break + } + return true, nil +} + +func (m *Manager) removeEvent(eventNode *EventNode) error { + m.removeEventNodeFromDependencies(eventNode) + m.removeNode(eventNode) + m.removeEventDependents(eventNode) + return nil +} diff --git a/pkg/events/dependencies/manager_test.go b/pkg/events/dependencies/manager_test.go index bf6fb6584c29..d7f4f49651cb 100644 --- a/pkg/events/dependencies/manager_test.go +++ b/pkg/events/dependencies/manager_test.go @@ -21,9 +21,11 @@ func getTestDependenciesFunc(deps map[events.ID]events.Dependencies) func(events func TestManager_AddEvent(t *testing.T) { testCases := []struct { - name string - eventToAdd events.ID - deps map[events.ID]events.Dependencies + name string + eventToAdd events.ID + deps map[events.ID]events.Dependencies + fallbackKeptEvents []events.ID + fallbackUniqueEvents []events.ID }{ { name: "empty dependency", @@ -44,6 +46,7 @@ func TestManager_AddEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(2): {}, }, @@ -61,6 +64,7 @@ func TestManager_AddEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -70,10 +74,55 @@ func TestManager_AddEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, }, + { + name: "event with a fallback", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): events.NewDependencies( + []events.ID{events.ID(2)}, + nil, + []events.Probe{ + events.NewProbe(probes.SchedProcessExec, true), + events.NewProbe(probes.SchedProcessExit, true), + }, + nil, + events.Capabilities{}, + []events.DependenciesFallback{ + events.NewDependenciesFallback( + events.NewDependencies( + []events.ID{events.ID(4)}, + nil, + []events.Probe{ + events.NewProbe(probes.SchedProcessFork, true), + }, + nil, + events.Capabilities{}, + nil, + ), + ), + }, + ), + events.ID(2): events.NewDependencies( + []events.ID{events.ID(3)}, + nil, + []events.Probe{ + events.NewProbe(probes.SchedProcessExec, true), + }, + nil, + events.Capabilities{}, + nil, + ), + events.ID(3): {}, + events.ID(4): {}, + }, + fallbackKeptEvents: []events.ID{events.ID(1), events.ID(4)}, + fallbackUniqueEvents: []events.ID{events.ID(4)}, + }, } t.Run("Sanity", func(t *testing.T) { @@ -99,7 +148,11 @@ func TestManager_AddEvent(t *testing.T) { depProbes := make(map[probes.Handle][]events.ID) for id, expDep := range testCase.deps { evtNode, err := m.GetEvent(id) - assert.NoError(t, err) + if slices.Contains(testCase.fallbackUniqueEvents, id) { + assert.ErrorIs(t, err, dependencies.ErrNodeNotFound, id) + continue + } + assert.NoError(t, err, id) dep := evtNode.GetDependencies() assert.ElementsMatch(t, expDep.GetIDs(), dep.GetIDs()) @@ -169,22 +222,53 @@ func TestManager_AddEvent(t *testing.T) { }, ) + fallbacks := testCase.deps[testCase.eventToAdd].GetFallbacks() _, err := m.SelectEvent(testCase.eventToAdd) - require.IsType(t, &dependencies.ErrNodeAddCancelled{}, err) - + if fallbacks != nil { + require.NoError(t, err) + } else { + require.IsType(t, &dependencies.ErrNodeAddCancelled{}, err) + } // Check that all the dependencies were cancelled - depProbes := make(map[probes.Handle][]events.ID) - for id := range testCase.deps { - _, err := m.GetEvent(id) - assert.ErrorIs(t, err, dependencies.ErrNodeNotFound, id) + var removedDepProbes, addedDepProbes []probes.Handle + for id, deps := range testCase.deps { + _, err = m.GetEvent(id) + // If the event is kept after failed event moved to fallback + if slices.Contains(testCase.fallbackKeptEvents, id) { + assert.NoError(t, err, id) + depProbes := deps.GetProbes() + // The added event moved to fallback, so use it instead + if id == testCase.eventToAdd { + depProbes = fallbacks[len(fallbacks)-1].GetDependencies().GetProbes() + } + for _, probe := range depProbes { + addedDepProbes = append(addedDepProbes, probe.GetHandle()) + } + } else { + assert.ErrorIs(t, err, dependencies.ErrNodeNotFound, id) + for _, probe := range deps.GetProbes() { + removedDepProbes = append(removedDepProbes, probe.GetHandle()) + } + } } - for handle := range depProbes { + for _, handle := range removedDepProbes { + if slices.Contains(addedDepProbes, handle) { + continue + } _, err := m.GetProbe(handle) assert.ErrorIs(t, err, dependencies.ErrNodeNotFound, handle) } - assert.Len(t, eventsAdditions, len(testCase.deps)) - assert.Len(t, eventsRemove, len(testCase.deps)) - assert.ElementsMatch(t, eventsAdditions, eventsRemove) + + for _, handle := range addedDepProbes { + _, err := m.GetProbe(handle) + assert.NoError(t, err, handle) + } + // TODO: Test additions and removes with fallbacks + if fallbacks == nil { + assert.Len(t, eventsAdditions, len(testCase.deps)) + assert.Len(t, eventsRemove, len(testCase.deps)) + assert.ElementsMatch(t, eventsAdditions, eventsRemove) + } }, ) } @@ -219,6 +303,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(2): {}, }, @@ -237,6 +322,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -246,6 +332,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, @@ -265,6 +352,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(1): events.NewDependencies( []events.ID{events.ID(2)}, @@ -275,6 +363,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -284,6 +373,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, @@ -303,6 +393,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(1): events.NewDependencies( []events.ID{events.ID(2)}, @@ -313,6 +404,7 @@ func TestManager_RemoveEvent(t *testing.T) { }, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -320,6 +412,7 @@ func TestManager_RemoveEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, @@ -412,6 +505,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(2): {}, }, @@ -427,6 +521,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -434,6 +529,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, @@ -450,6 +546,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(1): events.NewDependencies( []events.ID{events.ID(2)}, @@ -457,6 +554,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -464,6 +562,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, @@ -480,6 +579,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(1): events.NewDependencies( []events.ID{events.ID(2)}, @@ -487,6 +587,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(2): events.NewDependencies( []events.ID{events.ID(3)}, @@ -494,6 +595,7 @@ func TestManager_UnselectEvent(t *testing.T) { nil, nil, events.Capabilities{}, + nil, ), events.ID(3): {}, }, @@ -538,3 +640,296 @@ func TestManager_UnselectEvent(t *testing.T) { }) } } + +func TestManager_FailEvent(t *testing.T) { + testCases := []struct { + name string + preAddedEvents []events.ID + eventToAdd events.ID + deps map[events.ID]events.Dependencies + expectedRemovedEvents []events.ID + expectedExistingEvents map[events.ID][]events.ID + }{ + { + name: "no dependencies with no fallback", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): {}, + }, + expectedRemovedEvents: []events.ID{events.ID(1)}, + expectedExistingEvents: map[events.ID][]events.ID{}, + }, + { + name: "no dependencies with empty fallback", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): events.NewDependencies( + []events.ID{}, + nil, + nil, + nil, + events.Capabilities{}, + []events.DependenciesFallback{ + events.NewDependenciesFallback(events.Dependencies{}), + }, + ), + }, + expectedRemovedEvents: []events.ID{}, + expectedExistingEvents: map[events.ID][]events.ID{ + 1: {}, + }, + }, + { + name: "no dependencies with fallback with dependencies", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): events.NewDependencies( + []events.ID{}, + nil, + nil, + nil, + events.Capabilities{}, + []events.DependenciesFallback{ + events.NewDependenciesFallback( + events.NewDependencies( + []events.ID{2}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + ), + }, + ), + events.ID(2): {}, + }, + expectedRemovedEvents: []events.ID{}, + expectedExistingEvents: map[events.ID][]events.ID{ + 1: {2}, + 2: {}, + }, + }, + { + name: "event with dependency with empty dependency fallback", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): events.NewDependencies( + []events.ID{events.ID(2)}, + nil, + nil, + nil, + events.Capabilities{}, + []events.DependenciesFallback{ + events.NewDependenciesFallback(events.Dependencies{}), + }, + ), + events.ID(2): {}, + }, + expectedRemovedEvents: []events.ID{events.ID(2)}, + expectedExistingEvents: map[events.ID][]events.ID{ + 1: {}, + }, + }, + { + name: "event with multiple dependencies without fallback", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): events.NewDependencies( + []events.ID{events.ID(2)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(2): events.NewDependencies( + []events.ID{events.ID(3)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(3): {}, + }, + expectedRemovedEvents: []events.ID{events.ID(1), events.ID(2), events.ID(3)}, + expectedExistingEvents: map[events.ID][]events.ID{}, + }, + { + name: "event with fallback event with dependencies", + eventToAdd: events.ID(1), + deps: map[events.ID]events.Dependencies{ + events.ID(1): events.NewDependencies( + []events.ID{}, + nil, + nil, + nil, + events.Capabilities{}, + []events.DependenciesFallback{ + events.NewDependenciesFallback( + events.NewDependencies( + []events.ID{2}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + ), + }, + ), + events.ID(2): events.NewDependencies( + []events.ID{events.ID(3)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(3): {}, + }, + expectedRemovedEvents: []events.ID{}, + expectedExistingEvents: map[events.ID][]events.ID{ + 1: {2}, + 2: {3}, + 3: {}, + }, + }, + { + name: "multi levels dependency event with no fallback which is a dependency", + eventToAdd: events.ID(1), + preAddedEvents: []events.ID{events.ID(4)}, + deps: map[events.ID]events.Dependencies{ + events.ID(4): events.NewDependencies( + []events.ID{events.ID(1)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(1): events.NewDependencies( + []events.ID{events.ID(2)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(2): events.NewDependencies( + []events.ID{events.ID(3)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(3): {}, + }, + expectedRemovedEvents: []events.ID{1, 2, 3, 4}, + expectedExistingEvents: map[events.ID][]events.ID{}, + }, + { + name: "multi levels dependency event with no fallback which shares dependency", + eventToAdd: events.ID(1), + preAddedEvents: []events.ID{events.ID(4)}, + deps: map[events.ID]events.Dependencies{ + events.ID(4): events.NewDependencies( + []events.ID{events.ID(3)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(1): events.NewDependencies( + []events.ID{events.ID(2)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(2): events.NewDependencies( + []events.ID{events.ID(3)}, + nil, + nil, + nil, + events.Capabilities{}, + nil, + ), + events.ID(3): {}, + }, + expectedRemovedEvents: []events.ID{events.ID(1), events.ID(2)}, + expectedExistingEvents: map[events.ID][]events.ID{ + 4: {3}, + 3: {}, + }, + }, + } + for _, testCase := range testCases { + t.Run( + testCase.name, func(t *testing.T) { + // Create a new Manager instance + m := dependencies.NewDependenciesManager(getTestDependenciesFunc(testCase.deps)) + + var eventsRemove []events.ID + + // Count removes + m.SubscribeRemove( + dependencies.EventNodeType, + func(node interface{}) []dependencies.Action { + removeEventNode := node.(*dependencies.EventNode) + eventsRemove = append(eventsRemove, removeEventNode.GetID()) + return nil + }, + ) + + hasChanged := false + // Count removes + m.SubscribeChange( + dependencies.EventNodeType, + func(previousNode interface{}, newNode interface{}) []dependencies.Action { + hasChanged = true + return nil + }, + ) + + for _, preAddedEvent := range testCase.preAddedEvents { + _, err := m.SelectEvent(preAddedEvent) + require.NoError(t, err) + } + + _, err := m.SelectEvent(testCase.eventToAdd) + require.NoError(t, err) + + removed, err := m.FailEvent(testCase.eventToAdd) + require.NoError(t, err) + if slices.Contains(testCase.expectedRemovedEvents, testCase.eventToAdd) { + assert.True(t, removed) + } else { + assert.False(t, removed) + assert.True(t, hasChanged) + } + + for _, id := range testCase.expectedRemovedEvents { + _, err := m.GetEvent(id) + assert.ErrorIs(t, err, dependencies.ErrNodeNotFound, id) + + // Test indirect addition watcher logic + assert.Contains(t, eventsRemove, id) + } + + for id, deps := range testCase.expectedExistingEvents { + node, err := m.GetEvent(id) + require.NoError(t, err, id) + + assert.ElementsMatch(t, deps, node.GetDependencies().GetIDs(), id) + + // Test indirect addition watcher logic + assert.NotContains(t, eventsRemove, id) + } + }) + } +}