-
Notifications
You must be signed in to change notification settings - Fork 0
Subscribers and Events for the Eventbus
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.
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.
** IMPORTANT TIP: All fired events are automatically cloned by the Eventbus. This is important because for example if we fire an event for an experiment we don't want the same instance to be sent to every view. As a result all data items sent through the Eventbus need to implement DeepCloneableInterface. More details on this will be provided below in the Cloning section.
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: Although there can be multiple subscribers for the same event on the same view it is recommended to avoid doing this unless there is a good reason to avoid dealing with racing and synchronization issues.
** IMPORTANT TIP: It is strongly recommend that subscribers 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.
** IMPORTANT TIP: It is strongly recommend that subscribers should be located at a subpackage called subscribers. More details on the View Package and Class Structure wiki page.
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). It 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 ExperimentViewPolicyUpdateSubscriber extends PolicyUpdateSubscriber {
private ExperimentView experimentView;
private ExperimentDAO experimentDAO;
public ExperimentViewPolicyUpdateSubscriber(ExperimentView experimentView, ExperimentDAO experimentDAO) {
super();
this.experimentView = experimentView;
this.experimentDAO = experimentDAO;
}
@Override
public void handleBusEvent(PolicyUpdateBusEvent event) {
if(ExperimentUtils.isSameExperiment(experimentView.getExperiment(), event.getExperiment())) {
synchronized (experimentView.getExperimentLock()) {
updateExperimentInternalValues(event, experimentView.getExperiment());
experimentView.updateComponents();
}
} else {
synchronized (experimentView.getComparisonExperimentLock()) {
updateExperimentInternalValues(event, experimentView.getComparisonExperiment());
experimentView.updateComparisonComponents();
}
}
}
private void updateExperimentInternalValues(PolicyUpdateBusEvent event, Experiment experiment) {
// REFACTOR -> This should all be done in a single ExperimentUtils method as it will have to be replicated
// elsewhere. This is still done this way because the trainingErrorMessage needs to be done after the update.
ExperimentUtils.addOrUpdatePolicies(experiment, event.getPolicies());
ExperimentUtils.updateExperimentInternals(experiment);
experimentDAO.updateTrainingErrorAndMessage(experiment);
ExperimentUtils.updateEarlyStopReason(experiment);
}
@Override
public boolean filterBusEvent(PolicyUpdateBusEvent event) {
return ExperimentUtils.isSameExperiment(experimentView.getExperiment(), event.getExperiment()) ||
ExperimentUtils.isSameExperiment(experimentView.getComparisonExperiment(), event.getExperiment());
}
}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 which is all automatically handled for you.
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. The code will all be wrapped in a PushUtils.push() method at a higher level so you don't need to worry about pushing the update to the gui.
**Again it is recommend to keep subscribers as focused as possible.
All Subscribers should be added by overriding the addEventBusSubscribers method as shown below:
@Override
protected void addEventBusSubscribers() {
EventBus.subscribe(this, getUISupplier(),
new ExperimentViewPolicyUpdateSubscriber(this, experimentDAO),
new ExperimentViewRunUpdateSubscriber(this, experimentDAO));
}The reason it should be added here rather than on the onAttach() method for the view is that if we ever have to do a reroute such as when the user meant to view an existing experiment rather than a new experiment (from say a newExperiment/id bookmark versus experiment/id) then the view will not call onAttach() on the changed view. PathmindDefaultView handles all the details and you can just override the addEventBusSubscribers() method if you need to add Subscribers to a view.
For components you will can use onAttach() as the components are not affected by this.
** IMPORTANT TIP: Keep in mind that if you override the onDetach() method you will need to call super.onDetach() so that any EventBus subscribers are also removed.
Keep in mind that Vaadin doesn't necessarily destroy a view if a page is reloaded. To call the onDetach() the view has to have three failed heartbeats which may take a while. In most cases that shouldn't be an issue but it is important to realize that the onDetach() method may not be called right away as may be expected.
List of events can be found here.
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
-
BusEventTypewhich 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;
}
}All events fired through the Eventbus are automatically cloned. This is important because for example if we fire an event for an experiment we don't want the same instance to be sent to every view. As a result all data items sent through the Eventbus need to implement DeepCloneableInterface which consists of two methods. shallowClone() will only clone the attributes whereas deepClone() will also clone the internal data items. So for example experiment.shallowClone() will only clone the experiment class attributes whereas experiment.deepClone() will also clone all the internal data such as the list of runs (with each run itself being cloned), the project being cloned, and so on.
The EventBus calls PathmindBusEvent.cloneForEventBus() meaning that the cloning method is defined in the event itself. This is done because the event is the best place to determine exactly what needs to be cloned and which type of cloning is required. So for example if the event just had an integer value then there would be no need to clone. In another case we may just pass an experiment but we only need to do a shallowClone() since the Subscriber doesn't need more than that data. In other cases the cloneForEventBus() method will require a deepClone(). Or in the case of PolicyUpdateBusEvent where we can send a list of policies we'll then need to deepClone() each policy as shown below:
public PolicyUpdateBusEvent cloneForEventBus() {
return new PolicyUpdateBusEvent(policies.stream()
.map(policy -> policy.deepClone())
.collect(Collectors.toList()));
}Events can be fired by just calling the post() method on the EventBus such as:
EventBus.post(new RunUpdateBusEvent(run))The post() method is a static method and so can be called from anywhere.
** IMPORTANT TIP: Before firing any event you will want to perform any required database saves. This is because it is possible that a Subscriber may need to load something from the database from a fired event.
** IMPORTANT TIP: Generally you will want to perform any screen updates on the current view before firing an event through the Action classes.