Skip to content

Subscribers and Events for the Eventbus

FollowSteph edited this page Sep 16, 2020 · 10 revisions

We'll start by explaining how to use the subscribers/events as an understanding of how the eventbus works is not required to use it. That being said we'll conclude by explaining how the internals of the eventbus works.

Subscribers/Events

Whenever you want to listen to an event across browser tabs you will need to create an event which a subscriber will listen for. By default the subscribers will only be notified of events fired from other UI's (tabbed browsers) unless explicitly told otherwise. Each event that is NOT filtered through the filterBusEvent method will be fired in a thread, therefore events are multi-threaded. This is also why it's important to include the if filtering logic in the filterBusEvent method so as to avoid creating unnecessary threads.

The subscriber/event is comprised of two parts (ignoring the eventbus itself). The subscriber is the class listening for events, filtering them, as well as handling them. The event classes are the actual events being fired, such as an experiment has been updated and so on.

Subscribers

The subscribers is the most interesting portion of the eventbus for the vast majority of developers on the team. These are the classes that will handle the events and fired through the eventbus. In general Subscribers should be very short and succinct. They listen for events, filter for which events they are interested in, and update the UI as required.

** IMPORTANT TIP: There can be multiple subscribers for the same event on the same view. In fact it is highly recommended that each component have it's own subscriber so that subcribers doesn't get overloaded trying to update too many components and become a hodpodge of code. By separating out the subscribers as needed we can keep each subscriber's code very nice and clean, and therefore much much much easier to maintain.

** IMPORTANT TIP: It is strongly recommend that susbcribers NOT be inner classes and that they have their own subscriber classes separate from the component. Not only will this reduce in the merge conflicts but it allows GUI components to be reused in different views and so on.

Subscriber Anatomy

The Subscribers all inherit from the parent EventBusSubscriber class. In general most subscribers will then have an intermediary Subscriber class that will be extended by the actual final subscriber class.

Using the example of the PolicyUpdater all subscribers interested in PolicyUpdateBusEvent events extends the intermediary subscriber class PolicyUpdateSubscriber which defines the getEventType method (which returns the BusEventType.PolicyUpdate BusEventType). The subscribers SimulationMetricsPolicyUpdateSubscriber, PolicyChartPanelPolicyUpdateSubscriber, as well as others extend the intermediary PolicyUpdateSubscriber. It also helps readability that the naming convention includes the intermediary subscriber class.

With that in mind most subscribers consists of three sections of code: the constructor, the handleBusEvent method, and the filterBusEvent method.

public class SimulationMetricsPolicyUpdateSubscriber extends PolicyUpdateSubscriber {

    private SimulationMetricsPanel simulationMetricsPanel;

    public SimulationMetricsPolicyUpdateSubscriber(Supplier<Optional<UI>> getUISupplier, SimulationMetricsPanel simulationMetricsPanel) {
        super(getUISupplier);
        this.simulationMetricsPanel = simulationMetricsPanel;
    }

    @Override
    public void handleBusEvent(PolicyUpdateBusEvent event) {
        PushUtils.push(getUiSupplier(), ui -> {
            // Only for the best policy.
            Policy policy = PolicyUtils.selectBestPolicy(event.getPolicies());
            if (simulationMetricsPanel.isShowSimulationMetrics() && policy!= null && policy.getMetrics() != null && policy.getMetrics().size() > 0) {
                PolicyUtils.updateSimulationMetricsData(policy);
                simulationMetricsPanel.updateSimulationMetrics(policy);
            }
        });
    }

    @Override
    public boolean filterBusEvent(PolicyUpdateBusEvent event) {
        return simulationMetricsPanel.getExperiment().getId() == event.getExperimentId();
    }
}

Constructor

The constructor is generally used to store the components which the subscriber will need to update. In most cases this will be through a push so by default all subscribers will want to include the UI.

Note: we use Supplier<Optional<UI>> instead of the UI directly because a screen that is no longer visible in a browser tab may not have a valid UI and as such the push may not work. This is also why we push the UI through PushUtils.push().

FilterBusEvent

The filterBusEvent method will filter out the events. Filtering needs to be done in this method and NOT through an if condition in the method handleBusEvent method that handles the event.

** IMPORTANT NOTE: Each event that is NOT filtered through the filterBusEvent method will be fired in a thread (events are multi-threaded) which is why it's so important to include the filtering logic in the filterBusEvent method.

By default the subscriber will filter all events fired by it's own UI. As in if you click a button the button listener will most likely do whatever updates on the GUI it needs before it fires an event. Therefore to avoid racing conditions and/or conflicts the eventbus automatically filters out events fired from it's own current UI. However in some cases such as the NavBarItemExperimentUpdatedSubscriber we can force it filter all events by just adding a true argument in the subscriber's constructor for the isListenForEventOnSameUI parameter such as shown below:

public NavBarItemExperimentUpdatedSubscriber(Supplier<Optional<UI>> getUISupplier, ExperimentsNavBarItem experimentsNavBarItem) {
	super(getUISupplier, true);
	this.experimentsNavBarItem = experimentsNavBarItem;
}

HandleBusEvent

The handleBusEvent is where all the interesting action takes place in the subscriber. It's the method that handles the actual event. In most cases the code will all be wrapped in a PushUtils.push() method because in most cases you want to update UI. If you don't have any UI updates then you just omit the push wrapper.

Again it is recommend to keep subscribers as focused as possible, and so for example in the Experiment view we have different subscribers listening to the same events for different components. Each subscriber can therefore keep it's filtering and handling specific to the component it cares about. An extra bonus is that this greatly reduces the merge conflicts.

Adding subscriber to component and/or view

  • Explain onAttach and onDetach
  • TODO

General Guidelines and Tips

  • Explain where code should be located
  • Explain heartbeat
  • TODO

Events

Anatomy of an Event

All events extend the main parent class PathmindBusEvent. The core method event class consists of:

  • attributes related to the event (for example experiment if it's to notify an experiment has been updated) which are added in the constructor
  • BusEventType which is an enum that is used by the eventbus to know which subscribers to notify.
  • getMethods to get the data for the event.

The Event classes themselves should NOT contain any business logic, just the data related to the event as they can be used by many different subscribers. In some cases we do some sanity checks and throw an IllegalStateException if the data is invalid when creating the event but in most cases the event class only states the type of event, stores the data, along with methods to get the stored data. In essence event classes are very dumb and just provide a way of passing the data that has changed, or in some cases it's even just a notification without even any data.

public class ExperimentUpdatedBusEvent implements PathmindBusEvent {

    public enum ExperimentUpdateType {
        ExperimentDataUpdate,
        StartTraining,
        Favorite,
        Archive
    }

    private Experiment experiment;
    private ExperimentUpdateType experimentUpdateType;

    public ExperimentUpdatedBusEvent(Experiment experiment) {
        this(experiment, ExperimentUpdateType.ExperimentDataUpdate);
    }

    public ExperimentUpdatedBusEvent(Experiment experiment, ExperimentUpdateType experimentUpdateType) {
        this.experiment = experiment;
        this.experimentUpdateType = experimentUpdateType;
    }

    @Override
    public BusEventType getEventType() {
        return BusEventType.ExperimentUpdate;
    }

    public Experiment getExperiment() {
        return experiment;
    }

    public long getModelId() {
        return experiment.getModelId();
    }

    public boolean isStartedTrainingEventType() {
        return ExperimentUpdateType.StartTraining.equals(experimentUpdateType);
    }

    public boolean isArchiveEventType() {
        return ExperimentUpdateType.Archive.equals(experimentUpdateType);
    }

    public ExperimentUpdateType getExperimentUpdateType() {
        return experimentUpdateType;
    }
}

How to fire/post an event

  • Explain why saving any data to database before firing event is important.
  • Explain why it's important to update any event data before firing it is important
  • TODO

General Guidelines and Tips

  • TODO

Clone this wiki locally