Skip to content

Avoid global state in notification handling #314

@johananl

Description

@johananl

While working on e2e tests for i2gw I've encountered the fact that we're using a global variable to store notifications. This isn't a problem as long as i2gw doesn't use concurrency internally, however in the e2e tests I did use concurrency to drastically reduce the time it takes to execute the test suite which made me stumble upon this.

Right now, we handle "notifications" as follows:

  1. We create a global NotificationAggregator as a global variable:

var NotificationAggr NotificationAggregator

  1. We initialize it here:

func init() {
NotificationAggr = NotificationAggregator{Notifications: map[string][]Notification{}}
}

  1. We then "instrument" provider code with notify() functions which call the DispatchNotification() method on the NotificationAggregator. Example:

func notify(mType notifications.MessageType, message string, callingObject ...client.Object) {
newNotification := notifications.NewNotification(mType, message, callingObject...)
notifications.NotificationAggr.DispatchNotification(newNotification, string(ProviderName))
}

notify(notifications.ErrorNotification, fmt.Sprintf("port is nil, path %v", serverFieldPath), gw)

  1. Eventually we call the CreateNotificationTables() method which returns a map of per-provider notifications formatted as a text table:

func (na *NotificationAggregator) CreateNotificationTables() map[string]string {

  1. The notification tables get printed to stderr:

for _, table := range notificationTablesMap {
fmt.Fprintln(os.Stderr, table)
}

AFAICT there is no strict requirement to store notification state in a global variable. In fact, I'm not even sure we need to store notification state at all.

In the meantime I've found a solution for the concurrency issue affecting the tests, however we might want to reconsider the notifications design at some point.

Potential solutions I can think of:

  1. Dependency injection. Treat notifications similar to logging and inject a "notifier" type as a dependency to all functions which need to produce notifications, thus moving the global state to a local "notifier" variable which can be initialized once in some main function.
  2. No state. Log notifications ad hoc using slog without storing them anywhere. AFAICT, the only reason we store notifications is so that we can format them into a table, which IMO isn't a very strong argument in favor of the global var design.

Lastly, even if we end up not changing the design at all, we might want to at least have a way to clear the notification state from the global var safely. Otherwise notifications stay in the global state after being printed and accumulate forever. Again -- this isn't a problem when running the binary since it exits quickly but in test code (or if i2gw is ever used as a library) it certainly causes problems.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions