This is an automated email from the ASF dual-hosted git repository.

wilfred-s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/yunikorn-core.git


The following commit(s) were added to refs/heads/master by this push:
     new a86f91db [YUNIKORN-3336] event system shutdown leaks go routine or 
panics (#1111)
a86f91db is described below

commit a86f91db7e5d4aabd0428a56f20c7567b28180af
Author: Wilfred Spiegelenburg <[email protected]>
AuthorDate: Wed Jul 29 11:29:23 2026 +1000

    [YUNIKORN-3336] event system shutdown leaks go routine or panics (#1111)
    
    Calling StartService for an existing stopped EventPublisher does not
    create a new stop channel. Event publisher will ignore start request
    when already running. Calling stop twice on the EventPublisher panics as
    it tries to close an already closed channel.
    
    Move config callback setup for event system into the Init() allowing a
    start from config even when system start did not start the event system.
    Move stop channel creation into start and stop functions
    
    Cleanup layout in event system and publisher
    Remove exports from functions and objects where possible.
    Fix event loss after restart and race in atomic stop flag
    Change stop flag check to `CompareAndSwap` removing the small race that
    could occur using `Load` & `Save`.
    Remove reset of the callback as the callback never changes and is needed
    for turning on event system if turned off via config.
    
    Additional metrics for dropped events (created but not channelled)
    Not channelled events are events generated when the event system is
    turned off. Dropped events are created when the channel is not open
    or full while the event system is running.
    
    created == channelled + not channelled + dropped
    
    Fixed new data race seen during config reload. Move configuration
    settings to Init() instead of start for consistent processing.
    
    Closes: #1111
    
    Signed-off-by: Wilfred Spiegelenburg <[email protected]>
---
 pkg/events/event_publisher.go      |  42 ++++--
 pkg/events/event_publisher_test.go |  36 +++--
 pkg/events/event_system.go         | 284 ++++++++++++++++++++-----------------
 pkg/events/event_system_test.go    |  75 ++++++++--
 pkg/metrics/event.go               |  88 +++++++++++-
 5 files changed, 365 insertions(+), 160 deletions(-)

diff --git a/pkg/events/event_publisher.go b/pkg/events/event_publisher.go
index a1d95f6a..41e90690 100644
--- a/pkg/events/event_publisher.go
+++ b/pkg/events/event_publisher.go
@@ -19,6 +19,7 @@
 package events
 
 import (
+       "sync/atomic"
        "time"
 
        "go.uber.org/zap"
@@ -30,33 +31,42 @@ import (
 // stores the push event internal
 var defaultPushEventInterval = 2 * time.Second
 
-type EventPublisher struct {
+type eventPublisher struct {
        store             *EventStore
        pushEventInterval time.Duration
-       stop              chan struct{}
+       stopCh            chan struct{}
+       stopped           atomic.Bool
 }
 
-func CreateShimPublisher(store *EventStore) *EventPublisher {
-       publisher := &EventPublisher{
+func createShimPublisher(store *EventStore) *eventPublisher {
+       publisher := &eventPublisher{
                store:             store,
                pushEventInterval: defaultPushEventInterval,
-               stop:              make(chan struct{}),
        }
+       publisher.stopped.Store(true)
        return publisher
 }
 
-func (sp *EventPublisher) StartService() {
-       log.Log(log.Events).Info("Starting shim event publisher")
+func (sp *eventPublisher) start() {
+       log.Log(log.Events).Info("Starting event publisher")
+       // handle a restart correctly
+       if !sp.stopped.CompareAndSwap(true, false) {
+               log.Log(log.Events).Info("Event publisher already running")
+               return
+       }
+       sp.stopCh = make(chan struct{})
        go func() {
                for {
                        select {
-                       case <-sp.stop:
+                       case <-sp.stopCh:
+                               log.Log(log.Events).Info("Event publisher 
exiting")
                                return
                        case <-time.After(sp.pushEventInterval):
                                messages := sp.store.CollectEvents()
                                if len(messages) > 0 {
                                        if eventPlugin := 
plugins.GetResourceManagerCallbackPlugin(); eventPlugin != nil {
-                                               
log.Log(log.Events).Debug("Sending eventChannel", zap.Int("number of messages", 
len(messages)))
+                                               
log.Log(log.Events).Debug("Sending eventChannel",
+                                                       zap.Int("number of 
messages", len(messages)))
                                                eventPlugin.SendEvent(messages)
                                        }
                                }
@@ -65,11 +75,17 @@ func (sp *EventPublisher) StartService() {
        }()
 }
 
-func (sp *EventPublisher) Stop() {
-       log.Log(log.Events).Info("Stopping shim event publisher")
-       close(sp.stop)
+func (sp *eventPublisher) stop() {
+       if !sp.stopped.CompareAndSwap(false, true) {
+               log.Log(log.Events).Info("Event publisher already stopped")
+               return
+       }
+       log.Log(log.Events).Info("Stopping event publisher")
+       sp.stopCh <- struct{}{}
+       close(sp.stopCh)
+       sp.stopCh = nil
 }
 
-func (sp *EventPublisher) getEventStore() *EventStore {
+func (sp *eventPublisher) getEventStore() *EventStore {
        return sp.store
 }
diff --git a/pkg/events/event_publisher_test.go 
b/pkg/events/event_publisher_test.go
index 78680b59..722c18f4 100644
--- a/pkg/events/event_publisher_test.go
+++ b/pkg/events/event_publisher_test.go
@@ -19,6 +19,7 @@
 package events
 
 import (
+       "runtime"
        "testing"
        "time"
 
@@ -32,25 +33,38 @@ import (
 
 // creating a Publisher with nil store should still provide a non-nil object
 func TestCreateShimPublisher(t *testing.T) {
-       publisher := CreateShimPublisher(nil)
+       publisher := createShimPublisher(nil)
        assert.Assert(t, publisher != nil, "publisher should not be nil")
 }
 
-// StartService() and Stop() functions should not cause panic
+// StartService() and stop() functions should not cause panic
 func TestServiceStartStopInternal(t *testing.T) {
        store := newEventStore(1000)
-       publisher := CreateShimPublisher(store)
-       publisher.StartService()
-       defer publisher.Stop()
+       publisher := createShimPublisher(store)
+       defer publisher.stop()
        assert.Equal(t, publisher.getEventStore(), store)
+       // start and stop simulate a restart
+       before := runtime.NumGoroutine()
+       publisher.start()
+       publisher.stop()
+       time.Sleep(10 * time.Millisecond) // tiny sleep to yield
+       assert.Equal(t, before, runtime.NumGoroutine(), "expected no new go 
routine after start and stop")
+
+       // start should not fail or panic
+       before = runtime.NumGoroutine()
+       publisher.start()
+       after := runtime.NumGoroutine()
+       assert.Equal(t, before+1, after, "expected 1 new go routine")
+       publisher.start()
+       assert.Equal(t, after, runtime.NumGoroutine(), "Already started should 
not create new go routine")
 }
 
 func TestNoFillWithoutEventPluginRegistered(t *testing.T) {
        store := newEventStore(1000)
-       publisher := CreateShimPublisher(store)
+       publisher := createShimPublisher(store)
        publisher.pushEventInterval = time.Millisecond
-       publisher.StartService()
-       defer publisher.Stop()
+       publisher.start()
+       defer publisher.stop()
 
        event := &si.EventRecord{
                Type:          si.EventRecord_REQUEST,
@@ -80,10 +94,10 @@ func TestPublisherSendsEvent(t *testing.T) {
        }
 
        store := newEventStore(1000)
-       publisher := CreateShimPublisher(store)
+       publisher := createShimPublisher(store)
        publisher.pushEventInterval = time.Millisecond
-       publisher.StartService()
-       defer publisher.Stop()
+       publisher.start()
+       defer publisher.stop()
 
        event := &si.EventRecord{
                Type:          si.EventRecord_REQUEST,
diff --git a/pkg/events/event_system.go b/pkg/events/event_system.go
index 117669e1..631db365 100644
--- a/pkg/events/event_system.go
+++ b/pkg/events/event_system.go
@@ -21,6 +21,7 @@ package events
 import (
        "fmt"
        "sync"
+       "sync/atomic"
        "time"
 
        "go.uber.org/zap"
@@ -33,8 +34,10 @@ import (
        "github.com/apache/yunikorn-scheduler-interface/lib/go/si"
 )
 
-var once sync.Once
-var ev EventSystem
+var (
+       once sync.Once
+       ev   EventSystem
+)
 
 type EventSystem interface {
        // AddEvent adds an event record to the event system for processing:
@@ -77,17 +80,54 @@ type EventSystem interface {
        GetEventStreams() []EventStreamData
 }
 
+// GetEventSystem returns the event system instance. Initialization happens 
during the first call.
+// This does not start the service or publisher. Call StartService or 
StartServiceWithPublisher
+// on the returned system to start the service.
+func GetEventSystem() EventSystem {
+       once.Do(func() {
+               Init()
+       })
+       return ev
+}
+
+// Init Initializes the event system.
+// Only exported for testing.
+func Init() {
+       // load from config for setting in two places
+       confRequestCapacity := getRequestCapacity()
+       confRingBufferCapacity := getRingBufferCapacity()
+
+       store := newEventStore(confRequestCapacity)
+       buffer := newEventRingBuffer(confRingBufferCapacity)
+       evImpl := &EventSystemImpl{
+               Store:              store,
+               eventBuffer:        buffer,
+               eventSystemId:      fmt.Sprintf("event-system-%d", 
time.Now().Unix()),
+               streaming:          NewEventStreaming(buffer),
+               trackingEnabled:    isTrackingEnabled(),
+               requestCapacity:    confRequestCapacity,
+               ringBufferCapacity: confRingBufferCapacity,
+       }
+       evImpl.stopped.Store(true)
+       // start the callback for this instance: must be done
+       configs.AddConfigMapCallback(evImpl.eventSystemId, func() {
+               go evImpl.reloadConfig()
+       })
+
+       ev = evImpl
+}
+
 // EventSystemImpl main implementation of the event system which is used for 
history tracking.
 type EventSystemImpl struct {
        eventSystemId string
-       Store         *EventStore // storing eventChannel
-       publisher     *EventPublisher
+       Store         *EventStore // storing eventChannel, exported for test
+       publisher     *eventPublisher
        eventBuffer   *eventRingBuffer
        streaming     *EventStreaming
 
        channel chan *si.EventRecord // channelling input eventChannel
-       stop    chan bool            // whether the service is stopped
-       stopped bool
+       stop    chan struct{}        // channel to stop the system
+       stopped atomic.Bool          // whether the service is stopped
 
        trackingEnabled    bool
        requestCapacity    uint64
@@ -111,14 +151,6 @@ func (ec *EventSystemImpl) GetEventsFromID(id, count 
uint64) ([]*si.EventRecord,
        return ec.eventBuffer.GetEventsFromID(id, count)
 }
 
-// GetEventSystem returns the event system instance. Initialization happens 
during the first call.
-func GetEventSystem() EventSystem {
-       once.Do(func() {
-               Init()
-       })
-       return ev
-}
-
 // IsEventTrackingEnabled whether history tracking is currently enabled or not.
 func (ec *EventSystemImpl) IsEventTrackingEnabled() bool {
        ec.RLock()
@@ -126,40 +158,61 @@ func (ec *EventSystemImpl) IsEventTrackingEnabled() bool {
        return ec.trackingEnabled
 }
 
-// GetRequestCapacity returns the capacity of an intermediate storage which is 
used by the shim publisher.
-func (ec *EventSystemImpl) GetRequestCapacity() uint64 {
-       ec.RLock()
-       defer ec.RUnlock()
-       return ec.requestCapacity
+// StartService starts the event processing in the background. See the 
interface for details.
+func (ec *EventSystemImpl) StartService() {
+       ec.StartServiceWithPublisher(true)
 }
 
-// GetRingBufferCapacity returns the capacity of the buffer which stores 
historical elements.
-func (ec *EventSystemImpl) GetRingBufferCapacity() uint64 {
-       ec.RLock()
-       defer ec.RUnlock()
-       return ec.ringBufferCapacity
-}
+// Stop stops the event system, including the shim publisher if it was started.
+func (ec *EventSystemImpl) Stop() {
+       ec.Lock()
+       defer ec.Unlock()
+       // no need to stop twice
+       if !ec.stopped.CompareAndSwap(false, true) {
+               return
+       }
+       log.Log(log.Events).Info("Stopping event system handler")
 
-// Init Initializes the event system.
-// Only exported for testing.
-func Init() {
-       store := newEventStore(getRequestCapacity())
-       buffer := newEventRingBuffer(getRingBufferCapacity())
-       ev = &EventSystemImpl{
-               Store:         store,
-               channel:       make(chan *si.EventRecord, 
configs.DefaultEventChannelSize),
-               stop:          make(chan bool),
-               stopped:       false,
-               eventBuffer:   buffer,
-               eventSystemId: fmt.Sprintf("event-system-%d", 
time.Now().Unix()),
-               publisher:     CreateShimPublisher(store),
-               streaming:     NewEventStreaming(buffer),
+       ec.stop <- struct{}{}
+       if ec.channel != nil {
+               close(ec.channel)
+               ec.channel = nil
+       }
+       if ec.publisher != nil {
+               ec.publisher.stop()
+               ec.publisher = nil
        }
 }
 
-// StartService starts the event processing in the background. See the 
interface for details.
-func (ec *EventSystemImpl) StartService() {
-       ec.StartServiceWithPublisher(true)
+// GetEventStreams returns the current active event streams.
+func (ec *EventSystemImpl) GetEventStreams() []EventStreamData {
+       return ec.streaming.GetEventStreams()
+}
+
+// AddEvent adds an event record to the event system. See the interface for 
details.
+func (ec *EventSystemImpl) AddEvent(event *si.EventRecord) {
+       if event != nil {
+               event.Message = truncateEventMessage(event.Message)
+       }
+       // all events get tracked, even ones we drop
+       metrics.GetEventMetrics().IncEventsCreated()
+       ec.RLock()
+       defer ec.RUnlock()
+       // not running just track the metric
+       if ec.stopped.Load() {
+               metrics.GetEventMetrics().IncEventsNotChanneled()
+               return
+       }
+
+       select {
+       case ec.channel <- event:
+               metrics.GetEventMetrics().IncEventsChanneled()
+       default:
+               // make sure we do not drop events when running. events 
generated while turned off are not "dropped"
+               log.Log(log.Events).Info("Event dropped due to channel full or 
closed",
+                       zap.Int("channelSize", len(ec.channel)))
+               metrics.GetEventMetrics().IncEventsDropped()
+       }
 }
 
 // StartServiceWithPublisher starts the event processing background routines.
@@ -167,14 +220,13 @@ func (ec *EventSystemImpl) StartService() {
 func (ec *EventSystemImpl) StartServiceWithPublisher(withPublisher bool) {
        ec.Lock()
        defer ec.Unlock()
-
-       configs.AddConfigMapCallback(ec.eventSystemId, func() {
-               go ec.reloadConfig()
-       })
-
+       if !ec.stopped.CompareAndSwap(true, false) {
+               log.Log(log.Events).Info("Event system is already running")
+               return
+       }
        ec.trackingEnabled = isTrackingEnabled()
-       ec.ringBufferCapacity = getRingBufferCapacity()
-       ec.requestCapacity = getRequestCapacity()
+       ec.stop = make(chan struct{})
+       ec.channel = make(chan *si.EventRecord, configs.DefaultEventChannelSize)
 
        go func() {
                log.Log(log.Events).Info("Starting event system handler")
@@ -196,60 +248,74 @@ func (ec *EventSystemImpl) 
StartServiceWithPublisher(withPublisher bool) {
                }
        }()
        if withPublisher {
-               ec.publisher.StartService()
+               ec.publisher = createShimPublisher(ec.Store)
+               ec.publisher.start()
        }
 }
 
-// Stop stops the event system.
-func (ec *EventSystemImpl) Stop() {
-       ec.Lock()
-       defer ec.Unlock()
-
-       configs.RemoveConfigMapCallback(ec.eventSystemId)
+// getRequestCapacity returns the capacity of an intermediate storage which is 
used by the shim publisher.
+func (ec *EventSystemImpl) getRequestCapacity() uint64 {
+       ec.RLock()
+       defer ec.RUnlock()
+       return ec.requestCapacity
+}
 
-       if ec.stopped {
-               return
-       }
+// getRingBufferCapacity returns the capacity of the buffer which stores 
historical elements.
+func (ec *EventSystemImpl) getRingBufferCapacity() uint64 {
+       ec.RLock()
+       defer ec.RUnlock()
+       return ec.ringBufferCapacity
+}
 
-       ec.stop <- true
-       if ec.channel != nil {
-               close(ec.channel)
-               ec.channel = nil
-       }
-       ec.publisher.Stop()
-       ec.stopped = true
+// isRestartNeeded returns true if the tracking mode has switched from off to 
on or vice versa.
+// Returns false if tracking mode has not changed
+func (ec *EventSystemImpl) isRestartNeeded() bool {
+       ec.RLock()
+       defer ec.RUnlock()
+       return isTrackingEnabled() != ec.trackingEnabled
 }
 
-func (ec *EventSystemImpl) isStopped() bool {
-       return ec.stopped || ec.channel == nil
+// restart restarts the event system, used during config update.
+func (ec *EventSystemImpl) restart() {
+       ec.Stop()
+       ec.StartServiceWithPublisher(true)
 }
 
-// AddEvent adds an event record to the event system. See the interface for 
details.
-func (ec *EventSystemImpl) AddEvent(event *si.EventRecord) {
-       if event != nil {
-               event.Message = truncateEventMessage(event.Message)
+// CloseAllStreams closes all existing streams.
+// VisibleForTesting
+func (ec *EventSystemImpl) CloseAllStreams() {
+       ec.streaming.Lock()
+       defer ec.streaming.Unlock()
+       for consumer := range ec.streaming.eventStreams {
+               ec.streaming.removeEventStream(consumer)
        }
+}
 
-       metrics.GetEventMetrics().IncEventsCreated()
+// reloadConfig function called by the config
+func (ec *EventSystemImpl) reloadConfig() {
+       // load from config for setting in two places
+       confRequestCapacity := getRequestCapacity()
+       confRingBufferCapacity := getRingBufferCapacity()
 
-       ec.RLock()
-       defer ec.RUnlock()
+       ec.Lock()
+       ec.requestCapacity = confRequestCapacity
+       ec.ringBufferCapacity = confRingBufferCapacity
+       ec.Unlock()
 
-       if ec.isStopped() {
-               metrics.GetEventMetrics().IncEventsNotChanneled()
-               return
-       }
+       // resize the ring buffer & event store with new capacity
+       ec.Store.SetStoreSize(confRequestCapacity)
+       ec.eventBuffer.Resize(confRingBufferCapacity)
 
-       select {
-       case ec.channel <- event:
-               metrics.GetEventMetrics().IncEventsChanneled()
-       default:
-               log.Log(log.Events).Debug("could not add Event to channel")
-               metrics.GetEventMetrics().IncEventsNotChanneled()
+       if ec.isRestartNeeded() {
+               log.Log(log.Events).Info("Restarting event system handler on 
config reload")
+               ec.Lock()
+               ec.trackingEnabled = isTrackingEnabled()
+               ec.Unlock()
+               ec.restart()
        }
 }
 
-// truncates event message to 1024 characters for k8s compatibility
+// truncateEventMessage limits the event message to 1024 characters for k8s 
compatibility
 func truncateEventMessage(message string) string {
        const k8sEventMessageLimit = 1024
        if len(message) <= k8sEventMessageLimit {
@@ -258,10 +324,13 @@ func truncateEventMessage(message string) string {
        return message[:k8sEventMessageLimit-3] + "..."
 }
 
+// isTrackingEnabled gets the current state of tracking from the configuration.
 func isTrackingEnabled() bool {
        return common.GetConfigurationBool(configs.GetConfigMap(), 
configs.CMEventTrackingEnabled, configs.DefaultEventTrackingEnabled)
 }
 
+// getRequestCapacity returns the size of an intermediate storage from the 
configuration, using the
+// configs.DefaultEventRequestCapacity if 0 or not defined.
 func getRequestCapacity() uint64 {
        capacity := common.GetConfigurationUint(configs.GetConfigMap(), 
configs.CMEventRequestCapacity, configs.DefaultEventRequestCapacity)
        if capacity == 0 {
@@ -273,6 +342,8 @@ func getRequestCapacity() uint64 {
        return capacity
 }
 
+// getRingBufferCapacity returns the ring buffer capacity from the 
configuration, using the
+// configs.DefaultEventRingBufferCapacity if 0 or not defined.
 func getRingBufferCapacity() uint64 {
        capacity := common.GetConfigurationUint(configs.GetConfigMap(), 
configs.CMEventRingBufferCapacity, configs.DefaultEventRingBufferCapacity)
        if capacity == 0 {
@@ -283,44 +354,3 @@ func getRingBufferCapacity() uint64 {
        }
        return capacity
 }
-
-func (ec *EventSystemImpl) isRestartNeeded() bool {
-       ec.RLock()
-       defer ec.RUnlock()
-       return isTrackingEnabled() != ec.trackingEnabled
-}
-
-// Restart restarts the event system, used during config update.
-func (ec *EventSystemImpl) Restart() {
-       ec.Stop()
-       ec.StartServiceWithPublisher(true)
-}
-
-// GetEventStreams returns the current active event streams.
-func (ec *EventSystemImpl) GetEventStreams() []EventStreamData {
-       return ec.streaming.GetEventStreams()
-}
-
-// VisibleForTesting
-func (ec *EventSystemImpl) CloseAllStreams() {
-       ec.streaming.Lock()
-       defer ec.streaming.Unlock()
-       for consumer := range ec.streaming.eventStreams {
-               ec.streaming.removeEventStream(consumer)
-       }
-}
-
-func (ec *EventSystemImpl) reloadConfig() {
-       ec.Lock()
-       ec.requestCapacity = getRequestCapacity()
-       ec.ringBufferCapacity = getRingBufferCapacity()
-       ec.Unlock()
-
-       // resize the ring buffer & event store with new capacity
-       ec.Store.SetStoreSize(ec.requestCapacity)
-       ec.eventBuffer.Resize(ec.ringBufferCapacity)
-
-       if ec.isRestartNeeded() {
-               ec.Restart()
-       }
-}
diff --git a/pkg/events/event_system_test.go b/pkg/events/event_system_test.go
index 18b24ae4..ff3d1320 100644
--- a/pkg/events/event_system_test.go
+++ b/pkg/events/event_system_test.go
@@ -19,6 +19,7 @@
 package events
 
 import (
+       "runtime"
        "strconv"
        "strings"
        "sync"
@@ -29,6 +30,7 @@ import (
 
        "github.com/apache/yunikorn-core/pkg/common"
        "github.com/apache/yunikorn-core/pkg/common/configs"
+       "github.com/apache/yunikorn-core/pkg/metrics"
        "github.com/apache/yunikorn-scheduler-interface/lib/go/si"
 )
 
@@ -38,11 +40,18 @@ func TestSimpleStartAndStop(t *testing.T) {
        eventSystem := GetEventSystem()
        // adding event to stopped eventSystem does not cause panic
        eventSystem.AddEvent(nil)
+       time.Sleep(10 * time.Millisecond) // tiny sleep to yield
+       before := runtime.NumGoroutine()
        eventSystem.StartService()
        defer eventSystem.Stop()
+       after := runtime.NumGoroutine()
+       assert.Equal(t, before+2, after, "expected 2 new go routines: handler 
and publisher")
        // add an event
        eventSystem.AddEvent(nil)
        eventSystem.Stop()
+       time.Sleep(10 * time.Millisecond) // tiny sleep to yield
+       after = runtime.NumGoroutine()
+       assert.Equal(t, before, after, "expected all go routines to exit")
        // adding event to stopped eventSystem does not cause panic
        eventSystem.AddEvent(nil)
 }
@@ -51,7 +60,8 @@ func TestSimpleStartAndStop(t *testing.T) {
 // should be retrieved from the EventStore
 func TestSingleEventStoredCorrectly(t *testing.T) {
        Init()
-       eventSystem := GetEventSystem().(*EventSystemImpl) //nolint:errcheck
+       eventSystem, ok := GetEventSystem().(*EventSystemImpl)
+       assert.Assert(t, ok, "expected an EventSystemImpl")
        // don't run publisher, because it can collect the event while we're 
waiting
        eventSystem.StartServiceWithPublisher(false)
        defer eventSystem.Stop()
@@ -84,7 +94,8 @@ func TestSingleEventStoredCorrectly(t *testing.T) {
 
 func TestGetEvents(t *testing.T) {
        Init()
-       eventSystem := GetEventSystem().(*EventSystemImpl) //nolint:errcheck
+       eventSystem, ok := GetEventSystem().(*EventSystemImpl)
+       assert.Assert(t, ok, "expected an EventSystemImpl")
        eventSystem.StartServiceWithPublisher(false)
        defer eventSystem.Stop()
 
@@ -116,13 +127,14 @@ func TestConfigUpdate(t *testing.T) {
        defer configs.SetConfigMap(map[string]string{})
 
        Init()
-       eventSystem := GetEventSystem().(*EventSystemImpl) //nolint:errcheck
+       eventSystem, ok := GetEventSystem().(*EventSystemImpl)
+       assert.Assert(t, ok, "expected an EventSystemImpl")
        eventSystem.StartService()
        defer eventSystem.Stop()
 
        assert.Assert(t, eventSystem.IsEventTrackingEnabled())
-       assert.Equal(t, eventSystem.GetRingBufferCapacity(), 
uint64(configs.DefaultEventRingBufferCapacity))
-       assert.Equal(t, eventSystem.GetRequestCapacity(), 
uint64(configs.DefaultEventRequestCapacity))
+       assert.Equal(t, eventSystem.getRingBufferCapacity(), 
uint64(configs.DefaultEventRingBufferCapacity))
+       assert.Equal(t, eventSystem.getRequestCapacity(), 
uint64(configs.DefaultEventRequestCapacity))
        assert.Equal(t, eventSystem.eventBuffer.capacity, 
uint64(configs.DefaultEventRingBufferCapacity))
 
        // update config and wait for refresh
@@ -142,8 +154,8 @@ func TestConfigUpdate(t *testing.T) {
        )
        assert.NilError(t, err, "timed out waiting for config refresh")
 
-       assert.Equal(t, eventSystem.GetRingBufferCapacity(), 
newRingBufferCapacity)
-       assert.Equal(t, eventSystem.GetRequestCapacity(), newRequestCapacity)
+       assert.Equal(t, eventSystem.getRingBufferCapacity(), 
newRingBufferCapacity)
+       assert.Equal(t, eventSystem.getRequestCapacity(), newRequestCapacity)
        assert.Equal(t, eventSystem.eventBuffer.capacity, newRingBufferCapacity)
 }
 
@@ -182,6 +194,9 @@ func TestRequestCapacity(t *testing.T) {
        configs.SetConfigMap(config)
        capacity = getRequestCapacity()
        assert.Equal(t, uint64(configs.DefaultEventRequestCapacity), capacity)
+
+       // reset to empty
+       configs.SetConfigMap(map[string]string{})
 }
 
 func TestRingBufferCapacity(t *testing.T) {
@@ -206,6 +221,9 @@ func TestRingBufferCapacity(t *testing.T) {
        configs.SetConfigMap(config)
        capacity = getRingBufferCapacity()
        assert.Equal(t, uint64(configs.DefaultEventRingBufferCapacity), 
capacity)
+
+       // reset to empty
+       configs.SetConfigMap(map[string]string{})
 }
 
 func TestTruncateEventMessage(t *testing.T) {
@@ -246,8 +264,10 @@ func getTestString(stringLength int) string {
 // TestAddEventConcurrentStop verifies AddEvent and Stop can run concurrently 
without data races.
 func TestAddEventConcurrentStop(t *testing.T) {
        Init()
-       eventSystem := GetEventSystem().(*EventSystemImpl) //nolint:errcheck
+       eventSystem, ok := GetEventSystem().(*EventSystemImpl)
+       assert.Assert(t, ok, "expected an EventSystemImpl")
        eventSystem.StartServiceWithPublisher(false)
+       defer eventSystem.Stop()
 
        var wg sync.WaitGroup
        wg.Add(2)
@@ -270,3 +290,42 @@ func TestAddEventConcurrentStop(t *testing.T) {
 
        wg.Wait()
 }
+
+// TestAddEventConcurrentStop verifies AddEvent and Stop can run concurrently 
without data races.
+func TestAddEventAfterRestart(t *testing.T) {
+       Init()
+       eventSystem, ok := GetEventSystem().(*EventSystemImpl)
+       assert.Assert(t, ok, "expected an EventSystemImpl")
+       metrics.GetEventMetrics().Reset()
+       eventSystem.StartServiceWithPublisher(false)
+       defer eventSystem.Stop()
+
+       var wg sync.WaitGroup
+       wg.Add(2)
+
+       eventCount := 10000
+       go func() {
+               defer wg.Done()
+               for i := range eventCount {
+                       eventSystem.AddEvent(&si.EventRecord{
+                               Type:    si.EventRecord_REQUEST,
+                               Message: strconv.Itoa(i),
+                       })
+               }
+       }()
+
+       go func() {
+               defer wg.Done()
+               time.Sleep(time.Millisecond)
+               eventSystem.restart()
+       }()
+
+       wg.Wait()
+       counted := metrics.GetEventMetrics().GetEventsDropped()
+       assert.Equal(t, counted, 0, "should not have dropped an event")
+       counted = metrics.GetEventMetrics().GetEventsCreated()
+       assert.Equal(t, counted, eventCount, "number of created events 
incorrect")
+       counted = metrics.GetEventMetrics().GetEventsChanneled()
+       notCounted := metrics.GetEventMetrics().GetEventsNotChanneled()
+       assert.Equal(t, counted+notCounted, eventCount, "total number of 
(not)channeled events incorrect")
+}
diff --git a/pkg/metrics/event.go b/pkg/metrics/event.go
index d54a007d..281b2937 100644
--- a/pkg/metrics/event.go
+++ b/pkg/metrics/event.go
@@ -18,12 +18,16 @@
 
 package metrics
 
-import "github.com/prometheus/client_golang/prometheus"
+import (
+       "github.com/prometheus/client_golang/prometheus"
+       dto "github.com/prometheus/client_model/go"
+)
 
 type EventMetrics struct {
        totalEventsCreated      prometheus.Gauge
        totalEventsChanneled    prometheus.Gauge
        totalEventsNotChanneled prometheus.Gauge
+       totalEventsDropped      prometheus.Gauge
        totalEventsProcessed    prometheus.Gauge
        totalEventsStored       prometheus.Gauge
        totalEventsNotStored    prometheus.Gauge
@@ -54,6 +58,13 @@ func initEventMetrics() *EventMetrics {
                        Name:      "total_not_channeled",
                        Help:      "total events not channeled",
                })
+       metrics.totalEventsDropped = prometheus.NewGauge(
+               prometheus.GaugeOpts{
+                       Namespace: Namespace,
+                       Subsystem: EventSubsystem,
+                       Name:      "total_dropped",
+                       Help:      "total events dropped",
+               })
        metrics.totalEventsProcessed = prometheus.NewGauge(
                prometheus.GaugeOpts{
                        Namespace: Namespace,
@@ -93,6 +104,7 @@ func (em *EventMetrics) Reset() {
        em.totalEventsCreated.Set(0)
        em.totalEventsChanneled.Set(0)
        em.totalEventsNotChanneled.Set(0)
+       em.totalEventsDropped.Set(0)
        em.totalEventsStored.Set(0)
        em.totalEventsNotStored.Set(0)
        em.totalEventsProcessed.Set(0)
@@ -110,6 +122,10 @@ func (em *EventMetrics) IncEventsNotChanneled() {
        em.totalEventsNotChanneled.Inc()
 }
 
+func (em *EventMetrics) IncEventsDropped() {
+       em.totalEventsDropped.Inc()
+}
+
 func (em *EventMetrics) IncEventsProcessed() {
        em.totalEventsProcessed.Inc()
 }
@@ -125,3 +141,73 @@ func (em *EventMetrics) IncEventsNotStored() {
 func (em *EventMetrics) AddEventsCollected(collectedEvents int) {
        em.totalEventsCollected.Add(float64(collectedEvents))
 }
+
+// Event system metrics
+
+func (em *EventMetrics) GetEventsCreated() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsCreated.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+func (em *EventMetrics) GetEventsChanneled() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsChanneled.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+func (em *EventMetrics) GetEventsNotChanneled() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsNotChanneled.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+func (em *EventMetrics) GetEventsDropped() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsDropped.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+// Publisher metrics
+
+func (em *EventMetrics) GetEventsProcessed() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsProcessed.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+// Event store metrics
+
+func (em *EventMetrics) GetEventsStored() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsStored.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+func (em *EventMetrics) GetEventsNotStored() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsNotStored.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}
+
+func (em *EventMetrics) GetEventsCollected() int {
+       metricDto := &dto.Metric{}
+       if err := em.totalEventsCollected.Write(metricDto); err == nil {
+               return int(*metricDto.Gauge.Value)
+       }
+       return -1
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to