Services¶
The Guice documentation strongly recommends making Guice modules fast and side effect free. It also provides an example interface for starting and stopping services.
Krail extends that idea with a more comprehensive lifecycle for a Service, and also adds dependency management. For example, in order to start a Database Service, it may be necessary to load configuration values from a file or web service first.
Lifecycle¶
The lifecycle is defined by Service.State
and the standard cycle
comprises states:
- INITIAL, STARTING, RUNNING, STOPPING, STOPPED plus a state of FAILED
The transient states of STARTING and STOPPING are there because some services may take a while to fully start or stop.
Causes¶
package com.example.tutorial.service;
import com.example.tutorial.i18n.LabelKey; import com.google.inject.Inject; import uk.q3c.krail.eventbus.GlobalBusProvider; import uk.q3c.krail.service.AbstractService; import uk.q3c.krail.service.ServiceModel; import uk.q3c.krail.i18n.I18NKey; import uk.q3c.krail.i18n.Translate;
@Singleton public class ServiceA extends AbstractService {
@Inject
protected ServiceA(Translate translate, ServicesModel serviceModel, GlobalBusProvider globalBusProvider) {
super(translate, serviceModel, globalBusProvider);
}
@Override
protected void doStop() throws Exception {
}
@Override
protected void doStart() throws Exception {
}
@Override
public I18NKey getNameKey() {
return LabelKey.ServiceA;
}
}
[source]
----
package com.example.tutorial.service;
import com.example.tutorial.i18n.LabelKey;
import com.google.inject.Inject;
import uk.q3c.krail.eventbus.GlobalBusProvider;
import uk.q3c.krail.service.AbstractService;
import uk.q3c.krail.service.ServiceModel;
import uk.q3c.krail.i18n.I18NKey;
import uk.q3c.krail.i18n.Translate;
@Singleton
public class ServiceB extends AbstractService {
@Inject
protected ServiceB(Translate translate, ServicesModel serviceModel, GlobalBusProvider globalBusProvider) {
super(translate, serviceModel, globalBusProvider);
}
@Override
protected void doStop() throws Exception {
}
@Override
protected void doStart() throws Exception {
}
@Override
public I18NKey getNameKey() {
return LabelKey.ServiceB;
}
}
----
[source]
----
package com.example.tutorial.service;
import com.example.tutorial.i18n.LabelKey;
import com.google.inject.Inject;
import uk.q3c.krail.eventbus.GlobalBusProvider;
import uk.q3c.krail.service.AbstractService;
import uk.q3c.krail.service.ServiceModel;
import uk.q3c.krail.i18n.I18NKey;
import uk.q3c.krail.i18n.Translate;
@Singleton
public class ServiceC extends AbstractService {
@Inject
protected ServiceC(Translate translate, ServicesModel serviceModel, GlobalBusProvider globalBusProvider) {
super(translate, serviceModel, globalBusProvider);
}
@Override
protected void doStop() throws Exception {
}
@Override
protected void doStart() throws Exception {
}
@Override
public I18NKey getNameKey() {
return LabelKey.ServiceC;
}
}
----
[source]
----
package com.example.tutorial.service;
import com.example.tutorial.i18n.LabelKey;
import com.google.inject.Inject;
import uk.q3c.krail.eventbus.GlobalBusProvider;
import uk.q3c.krail.service.AbstractService;
import uk.q3c.krail.service.ServiceModel;
import uk.q3c.krail.i18n.I18NKey;
import uk.q3c.krail.i18n.Translate;
@Singleton
public class ServiceD extends AbstractService {
@Inject
protected ServiceD(Translate translate, ServicesModel serviceModel, GlobalBusProvider globalBusProvider) {
super(translate, serviceModel, globalBusProvider);
}
@Override
protected void doStop() throws Exception {
}
@Override
protected void doStart() throws Exception {
}
@Override
public I18NKey getNameKey() {
return LabelKey.ServiceD;
}
}
----
Note that each has a different name key - this is also used by getServiceKey(), which is used to uniquely identify a Service class. This approach is used to overcome the changes in class name which occur when using enhancers such as Guice AOP. This means that each Service class must have a unique name key.
As Services often are, these are all Singletons, although they do not have to be.
== Registering Services
All Service classes must be registered. We can do that very simply by sub-classing `AbstractServiceModule` and using the methods it provides
* create a new class `TutorialServicesModule` in _com.example.tutorial.service_
* copy the code below
[source]
----
package com.example.tutorial.service;
import com.example.tutorial.i18n.LabelKey;
import uk.q3c.krail.service.AbstractServiceModule;
import uk.q3c.krail.service.Dependency;
public class TutorialServicesModule extends AbstractServiceModule {
@Override
protected void registerServices() {
registerService(LabelKey.ServiceA, ServiceA.class);
registerService(LabelKey.ServiceB, ServiceB.class);
registerService(LabelKey.ServiceC, ServiceC.class);
registerService(LabelKey.ServiceD, ServiceD.class);
}
@Override
protected void defineDependencies() {
}
}
----
* include the module in the `BindingManager`:
[source]
----
@Override
protected void addAppModules(List<Module> baseModules) {
baseModules.add(new TutorialServicesModule());
}
----
== Monitor the Service status
Fur the purposes of the Tutorial, we will create a simple page to monitor the status of the Services.
* In `MyOtherPages` add the entry:
[source,java]
----
addEntry("services", ServicesView.class, LabelKey.Services, PageAccessControl.PUBLIC);
----
* create `ServicesView` in the _com.example.tutorial.pages_ package
[source]
----
package com.example.tutorial.pages;
import com.example.tutorial.i18n.Caption;
import com.example.tutorial.i18n.DescriptionKey;
import com.example.tutorial.i18n.LabelKey;
import com.example.tutorial.service.ServiceA;
import com.example.tutorial.service.ServiceB;
import com.example.tutorial.service.ServiceC;
import com.example.tutorial.service.ServiceD;
import com.google.inject.Inject;
import com.vaadin.ui.Button;
import com.vaadin.ui.Panel;
import com.vaadin.ui.TextArea;
import com.vaadin.ui.VerticalLayout;
import net.engio.mbassy.listener.Handler;
import net.engio.mbassy.listener.Listener;
import uk.q3c.krail.eventbus.GlobalBus;
import uk.q3c.krail.eventbus.SubscribeTo;
import uk.q3c.krail.service.ServiceBusMessage;
import uk.q3c.krail.core.view.Grid3x3ViewBase;
import uk.q3c.krail.core.view.component.ViewChangeBusMessage;
import uk.q3c.krail.i18n.Translate;
@Listener
@SubscribeTo(GlobalBus.class)
public class ServicesView extends Grid3x3ViewBase {
private ServiceA serviceA;
private ServiceB serviceB;
private final ServiceC serviceC;
private final ServiceD serviceD;
@Caption(caption = LabelKey.Start_Service_A, description = DescriptionKey.Start_Service_A)
private Button startABtn;
@Caption(caption = LabelKey.Stop_Service_D, description = DescriptionKey.Stop_Service_D)
private Button stopDBtn;
@Caption(caption = LabelKey.Stop_Service_C, description = DescriptionKey.Stop_Service_C)
private Button stopCBtn;
@Caption(caption = LabelKey.Stop_Service_B, description = DescriptionKey.Stop_Service_B)
private Button stopBBtn;
private Translate translate;
@Caption(caption = LabelKey.State_Changes,description = DescriptionKey.State_Changes)
private TextArea stateChangeLog;
@Caption(caption = LabelKey.Clear,description = DescriptionKey.Clear)
private Button clearBtn;
@Inject
protected ServicesView(Translate translate,ServiceA serviceA, ServiceB serviceB, ServiceC serviceC, ServiceD serviceD) {
super(translate);
this.translate = translate;
this.serviceA = serviceA;
this.serviceB = serviceB;
this.serviceC = serviceC;
this.serviceD = serviceD;
}
@Override
protected void doBuild(ViewChangeBusMessage busMessage) {
super.doBuild(busMessage);
createButtons();
createStateMonitor();
}
private void createStateMonitor() {
stateChangeLog = new TextArea();
stateChangeLog.setSizeFull();
stateChangeLog.setRows(8);
getGridLayout().addComponent(stateChangeLog,0,1,2,1);
clearBtn = new Button();
clearBtn.addClickListener(click->stateChangeLog.clear());
setBottomCentre(clearBtn);
}
@Handler
protected void handleStateChange(ServiceBusMessage serviceBusMessage) {
String serviceName = translate.from(serviceBusMessage.getService()
.getNameKey());
String logEntry = serviceName + " changed from " + serviceBusMessage.getFromState()
.name() + " to " + serviceBusMessage.getToState().name()+", cause: " +
serviceBusMessage.getCause();
String newline = stateChangeLog.getValue().isEmpty() ? "" : "\n";
stateChangeLog.setValue(stateChangeLog.getValue()+newline+logEntry);
}
private void createButtons() {
startABtn = new Button();
startABtn.addClickListener(click -> serviceA.start());
stopDBtn = new Button();
stopDBtn.addClickListener(click -> serviceD.stop());
stopCBtn = new Button();
stopCBtn.addClickListener(click -> serviceC.stop());
stopBBtn = new Button();
stopBBtn.addClickListener(click -> serviceB.stop());
Panel panel = new Panel();
VerticalLayout layout = new VerticalLayout(startABtn, stopDBtn, stopCBtn, stopBBtn);
panel.setContent(layout);
setTopLeft(panel);
}
}
----
* create the enum constants
Here we set up some buttons to start and stop services in `createButtons()`<br>
We use the link:tutorial-event-bus.md[Event Bus] to create a simple monitor for state changes in `createStateMonitor()`
* run the application and try pressing 'Start Service A' - a message will appear in the state changes log
== Defining Dependencies
So far, all the Services operate independently - there are no dependencies specified. Let us assume we want service A to depend on the other 3 services, each with a different one of the 3 dependency types. We will also mix up using Guice and *Dependency* annotations, though you would probably use only one method to avoid confusion.
=== Dependencies with Guice
* add the following to the `defineDependencies()` method in the `TutorialServicesModule`:
[source,java]
----
addDependency(LabelKey.ServiceA,LabelKey.ServiceB, Dependency.Type.ALWAYS_REQUIRED);
addDependency(LabelKey.ServiceA,LabelKey.ServiceC, Dependency.Type.REQUIRED_ONLY_AT_START);
----
=== Dependencies by Annotation
In `ServiceA` we inject `ServiceD` and store in a field in order to annotate it as a dependency (which you would need anyway if you wish to access `ServiceD`).
* Modify ServiceA
[source,java]
----
@Dependency(required = false)
private ServiceD serviceD;
@Inject
protected ServiceA(Translate translate, ServicesModel serviceModel, GlobalBusProvider globalBusProvider, ServiceD serviceD) {
super(translate, serviceModel, globalBusProvider);
this.serviceD = serviceD;
}
----
This marks the dependency, ServiceD, as optional
== Testing Dependencies
* run the application
* navigate to the 'Services' page
* press 'Start Service A'
* Note that all 4 services show in the state changes log as 'STARTED' - `ServiceA` has automatically called all its dependencies to start. The order they start in is arbitrary, as they are started in parallel threads, but `ServiceA` will not start until all its required dependencies have started.
* press 'Clear'
* press 'Start Service A' again - nothing happens. Attempts to start/stop a service which is already started/stopped are ignored.
* press 'Stop ServiceD' - only `ServiceD` stops
* press 'Stop ServiceC' - only `ServiceC` stops
* press 'Stop ServiceB' - `ServiceB` and `ServiceA` stop. `ServiceA` has cause of DEPENDENCY_STOPPED
When `ServiceD` and `ServiceC` are stopped they do not affect `ServiceA`, as they are declared as "optional" and "required only at start".
When `ServiceB` is stopped, however, `ServiceA` also stops because that dependency was declared as "always required"
= Summary
* We have created services by sub-classing `AbstractService`
* We have defined dependencies between services using Guice
* We have defined dependencies between services using the *@Dependency* annotation
* We have demonstrated the interaction between services, when starting and stopping services with different dependency types
= Download from GitHub
To get to this point straight from GitHub, https://github.com/davidsowerby/krail-tutorial[clone] using branch *step11*