Simplify cluster applications state management
The problem
This issue is about the Vue app used to manage Kubernetes Apps from Gitlab:
While working on implementing a feature to uninstall managed apps, it became very challenging to decide at which point I could display a uninstall button because of the complexity of the application state. Every managed application can be in one of several states: not_installed -> installable -> installing -> updating -> etc
. The UI will change based on the current application state.
The flow described is straightforward, but the problem starts when the application state coming from the server does not represent the desired application state in the client. For example, if the application is installing
, the state coming from the server could be scheduled
or installing
. Besides that, the UI should also take in consideration that the application status does not change immediately in the server and it needs to give instant feedback to the user by changing the "Install" button to "Installing". All those conditions are expressed like this in the code:
isInstalling() {
return (
this.status === APPLICATION_STATUS.SCHEDULED ||
this.status === APPLICATION_STATUS.INSTALLING ||
(this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled)
);
}
This function has a high complexity: 25 = 32 possible paths.
The application_row
component implements several functions with equal or higher complexity:
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED ||
this.status === APPLICATION_STATUS.UPDATED ||
this.status === APPLICATION_STATUS.UPDATING ||
this.status === APPLICATION_STATUS.UPDATE_ERRORED
);
}
installButtonDisabled() {
// Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (
((this.status !== APPLICATION_STATUS.INSTALLABLE &&
this.status !== APPLICATION_STATUS.ERROR) ||
this.isInstalling) &&
this.isKnownStatus
);
}
The causes for this explosion of states are:
- Our attempt to design the client-side state based on the application status coming from the server https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/clusters/constants.js
- We do not have control of the current state of the application because any event received from the server updates the application state: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/clusters/stores/clusters_store.js#L117
The solution
This problem won’t be fixed in a single MR, but we can make a significant first step towards that goal:
- Separate application state from server-side application status.
- User events, like clicking the "Install" button, and server events, like the one indicating that the application is installing, are signals that indicate the client-side application state to transition from one state to another.
The flow above can be expressed clearly with a finite state machine:
const applicationStateMachine = {
installable: {
on: {
install: 'installing' // install is an event triggered by the user
},
},
installing: {
on: {
installed: 'installed', // this events are triggered by the server
error: 'installable', // this events are triggered by the server
},
},
}
If the current application state is installing
and the server emits a scheduled
event, the state machine won’t transition to another state. scheduled
is not a relevant event to the state machine because it is only waiting for the installation process to fail or to succeed. Implementing a method like isInstalling
would change to:
isInstalling() {
return this.status === APPLICATION_STATUS.INSTALLING;
}
Reducing the complexity of the function to 21