Custom Entity Development
This section details how to create new custom application components or groups as brooklyn entities.
The Entity Lifecycle
- Importance of serialization, ref to How mananagement works
- Parents and Membership (groups)
What to Extend – Implementation Classes
-
entity implementation class hierarchy
SoftwareProcess
as the main starting point for base entities (corresponding to software processes), and subclasses such asVanillaJavaApp
DynamicCluster
(multiple instances of the same entity in a location) andDynamicFabric
(clusters in multiple location) for automatically creating many instances, supplied with anEntityFactory
(e.g.BaseEntityFactory
) in thefactory
flagAbstractGroup
for collecting entities which are parented elsewhere in the hierachyAbstractEntity
if nothing else fits
-
traits (mixins, otherwise known as interfaces with statics) to define available config keys, sensors, and effectors; and conveniences e.g.
StartableMethods.{start,stop}
is useful for entities which implementStartable
-
the
Entities
class provides some generic convenience methods; worth looking at it for any work you do
A common lifecycle pattern is that the start
effector (see more on effectors below) is invoked,
often delegating either to a driver (for software processes) or children entities (for clusters etc).
Configuration
-
AttributeSensorAndConfigKey fields can be automatically converted for
SoftwareProcess
. This is done inpreStart()
. This must be done manually if required for other entities, often withConfigToAttributes.apply(this)
. -
Setting ports is a special challenge, and one which the
AttributeSensorAndConfigKey
is particularly helpful for, cfPortAttributeSensorAndConfigKey
(a subclass), causing ports automatically get assigned from a range and compared with the targetPortSupplied
location.Syntax is as described in the PortRange interface. For example, “8080-8099,8800+” will try port 8080, try sequentially through 8099, then try from 8800 until all ports are exhausted.
This is particularly useful on a contended machine (localhost!). Like ordinary configuration, the config is done by the user, and the actual port used is reported back as a sensor on the entity.
-
Validation of config values can be applied by supplying a
Predicate
to theconstraint
of a ConfigKey builder. Constraints are tested after an entity is initialised and before an entity managed. Useful predicates include:StringPredicates.isNonBlank
: require that a String key is neither null nor empty.ResourcePredicates.urlExists
: require that a URL that is loadable by Brooklyn. Use this to confirm that necessary resources are available to the entity.Predicates.in
: require one of a fixed set of values.Predicates.containsPattern
: require that a value match a regular expression pattern.
An important caveat is that only constraints on config keys that are on an entity’s type hierarchy can be tested automatically. Brooklyn has no knowledge of the true type of other keys until they are retrieved with a
config().get(key)
.
Implementing Sensors
- e.g. HTTP, JMX
Sensors at base entities are often retrieved by feeds which poll the entity’s corresponding instance in the real world.
The SoftwareProcess
provides a good example; by subclassing it and overriding the connectSensors()
method
you could wire some example sensors using the following:
public void connectSensors() {
super.connectSensors()
httpFeed = HttpFeed.builder()
.entity(this)
.period(200)
.baseUri(mgmtUrl)
.poll(new HttpPollConfig<Boolean>(SERVICE_UP)
.onSuccess(HttpValueFunctions.responseCodeEquals(200))
.onError(Functions.constant(false)))
.poll(new HttpPollConfig<Integer>(REQUEST_COUNT)
.onSuccess(HttpValueFunctions.jsonContents("requestCount", Integer.class)))
.build();
}
@Override
protected void disconnectSensors() {
super.disconnectSensors();
if (httpFeed != null) httpFeed.stop();
}
In this example (a simplified version of JBoss7Server
), the url returns metrics in JSON.
We report the entity as up if we get back an http response code of 200, or down if any other response code or exception.
We retrieve the request count from the response body, and convert it to an integer.
Note the first line (super.connectSensors()
); as one descends into specific convenience subclasses (such as for Java web-apps), the work done by the parent class’s overridden methods may be relevant, and will want to be invoked or even added to a resulting list.
For some sensors, and often at compound entities, the values are obtained by monitoring values of other sensors on the same (in the case of a rolling average) or different (in the case of the average of children nodes) entities. This is achieved by policies, described below.
Implementing Effectors
The Entity
interface defines the sensors and effectors available. The entity class provides
wiring for the sensors, and the effector implementations. In simple cases it may be straightforward
to capture the behaviour of the effectors in a simple methods.
For example deploying a WAR to a cluster can be done as follows:
This section is not complete. Feel free to fork the docs and lend a hand.
For some entities, specifically base entities, the implementation of effectors might need other tools (such as SSH), and may vary by location, so having a single implementation is not appropriate.
The problem of multiple inheritance (e.g. SSH functionality and entity inheritance) and multiple implementations (e.g. SSH versus Windows) is handled in brooklyn using delegates called drivers.
In the implementations of JavaWebApp
entities, the behaviour which the entity always does is captured in the entity class (for example, breaking deployment of multiple WARs into atomic actions), whereas implementations which is specific to a particular entity and driver (e.g. using scp to copy the WARs to the right place and install them, which of course is different among appservers, or using an HTTP or JMX management API, again where details vary between appservers) is captured in a driver class.
Routines which are convenient for specific drivers can then be inherited in the driver class hierarchy. For example, when passing JMX environment variables to Java over SSH, JavaSoftwareProcessSshDriver
extends AbstractSoftwareProcessSshDriver
and parents JBoss7SshDriver
.
Testing
- Unit tests can make use of
SimulatedLocation
andTestEntity
, and can extendBrooklynAppUnitTestSupport
. - Integration tests and use a
LocalhostMachineProvisioningLocation
, and can also extendBrooklynAppUnitTestSupport
.
SoftwareProcess Lifecycle
SoftwareProcess
is the common super-type of most integration components (when implementing in Java).
See JBoss7Server
and MySqlNode
for exemplars.
The methods called in a SoftwareProcess
entity’s lifecycle are described below. The most important steps are shown in bold (when writing a new entity, these are the methods most often implemented).
- Initial creation (via
EntitySpec
or YAML):- no-arg constructor
- init
- add locations
- apply initializers
- add enrichers
- add policies
- add children
- manages entity (so is discoverable by other entities)
- Start:
- provisions new machine, if the location is a
MachineProvisioningLocation
- creates new driver
- calls
getDriverInterface
- Infers the concrete driver class from the machine-type, e.g. by default it adds “Ssh” before the word “Driver” in “JBoss7Driver”.
- instantiates the driver, calling the constructor to pass in the entity itself and the machine location
- calls
- sets attributes from config (e.g. for ports being used)
- calls
entity.preStart()
- calls
driver.start()
, which:- runs pre-install command (see config key
pre.install.command
) - uploads install resources (see config keys
files.install
andtemplates.install
) - calls
driver.install()
- runs post-install command (see config key
post.install.command
) - calls
driver.customize()
- uploads runtime resources (see config keys
files.runtime
andtemplates.runtime
) - runs pre-launch command (see config key
pre.launch.command
) - calls
driver.launch()
- runs post-launch command (see config key
post.launch.command
) - calls
driver.postLaunch()
- runs pre-install command (see config key
- calls
entity.postDriverStart()
, which:- calls
enity.waitForEntityStart()
- waits fordriver.isRunning()
to report true
- calls
- calls
entity.connectSensors()
- calls
entity.waitForServicUp()
- calls
entity.postStart()
- provisions new machine, if the location is a
- Restart:
- If restarting machine…
- calls
entity.stop()
, withstopMachine
set to true. - calls start
- restarts children (if configured to do so)
- calls
- Else (i.e. not restarting machine)…
- calls
entity.preRestart()
- calls
driver.restart()
- calls
driver.stop()
- calls
driver.launch()
- calls
driver.postLaunch()
- calls
- restarts children (if configured to do so)
- calls
entity.postDriverStart()
, which:- calls
enity.waitForEntityStart()
- pollsdriver.isRunning()
, waiting for true
- calls
- calls
entity.waitForServicUp()
- calls
entity.postStart()
- calls
- If restarting machine…
- Stop:
- calls
entity.preStopConfirmCustom()
- aborts if exception. - calls
entity.preStop()
- stops the process:
- stops children (if configured to do so)
- calls
driver.stop()
- stops the machine (if configured to do so)
- calls
entity.postStop()
- calls
- Rebind (i.e. when Brooklyn is restarted):
- no-arg constructor
- reconstitutes entity (e.g. setting config and attributes)
- If entity was running…
- calls
entity.rebind()
; if previously started then:- creates the driver (same steps as for start)
- calls
driver.rebind()
- calls
entity.connectSensors()
- calls
- attaches policies, enrichers and persisted feeds
- manages the entity (so is discoverable by other entities)