Building a reactive system
Now that we've taken a look at reactive programming and the Vert.x framework, it's time to actually get down to the code. The code was written by the awesome team of Clement Escoffier, James Falkner, Thomas Qvarnström, and Rodney Russ. Hail and salute them for their efforts.
Step one, of course, is to clone or fork the repo at github.com/reactica/rhte-demo. The repo contains everything we need to create a Red Hat OpenShift cluster, install the necessary middleware, configure the coaster, and deploy it to the cluster.
But before we run the code, let’s talk about how it works. We’ll go through the different parts in this order:
-
domain-model
: The definition of theUser
andRide
objects used throughout the system. -
event-generator
: Creates newUser
andRide
objects at certain intervals. These objects are serialized and sent viaEvents
objects to Red Hat AMQ. -
event-store
: Takes theUser
events from the AMQ broker and stores them in Red Hat Data Grid. -
queue-length-calculator
: Uses data from the data grid to calculate the estimated wait time for the ride. -
current-line-updater
: Updates the queue whenever something changes, including newUser
s andRide
s or a change to the state of an existingUser
orRide
. -
billboard
: The front end. This is what guests in line for the ride see while they’re waiting, and it represents the results of all of the other microservices working together.
The repo also has two other components: amqp-verticle
, which Clement wrote as an adapter for Vert.x applications to communicate with Red Hat AMQ. That verticle serves as a bridge between the Vert.x event bus and AMQP. The other component is vertx-data-grid-client
, which is, well, a Vert.x client for Red Hat Data Grid. Suffice to say, they handle the interactions between our code and the middleware.
Finally, there’s a bonus directory, vertx-examples
, that contains sample code not used in this exercise.
System architecture
Throughout this article and in the accompanying videos we use this diagram to show how the reactive system works:
The domain model
The domain-model
package defines two classes: User
and Ride
. They’re both straightforward, and they represent the data used throughout our reactive system.
User
A User
is a guest; they have a name, a currentState
, and data that indicates when they got in line and when they finished the ride. The time the User
finished the ride is blank at first. It is updated via a RIDE_COMPLETED
event later. (BTW, both parts of the domain model have unique IDs, but they’re not important to our discussion here.) The possible states of a user are:
-
UNKNOWN
– The default value if none is specified on theUser
constructor -
IN_QUEUE
– They’re in line for the ride -
ON_RIDE
– They’re actually on the coaster (the states are cleverly named, are they not?) -
COMPLETED_RIDE
– They’ve completed the ride
For future purposes, a User
object also includes a field called RIDE_ID
. The current value is “reactica
,” but the system is written so that it could calculate the wait times for other Coderland rides as well. A future park-wide dashboard might give an administrator a quick view of the wait times for multiple rides at a glance.
Feel free to implement the complete suite of microservices for The Compile Driver! That’s a PR we’d love to see.
Ride
A Ride
represents a round-trip of the coaster. The length of time the coaster takes for a complete round-trip is configurable; by default, it is 30 seconds. The number of people who can ride the coaster at once is also configurable. The default number of riders is 5. Changing those values obviously affects the wait time. As fewer people can ride at once and the ride gets longer, the wait times get longer. If the coaster can handle more riders at once and the round-trip is shorter, wait times are shorter. We'll look at how to change those values later.
A Ride
object can have three states that we care about:
-
PLANNED
– The initial state when aRide
object is created -
IN_PROGRESS
– TheRide
has left the station, meaning that all of theUser
s on the ride transition to theON_RIDE
state -
COMPLETED
– TheRide
has returned to the station, meaning that all of theUser
s on the ride transition to theCOMPLETED_RIDE
state.
A Ride
object does not include a list of User
s on it. When a new Ride
is created, microservices in the eventstore
component take the appropriate number of User
s and change each of their states to ON_RIDE
.
Generating events
The event-generator
component contains several verticles that work together to generate events. First is the MainVerticle
, which does any initialization work required and creates instances of other classes and verticles as needed. (With the exception of billboard
, every component has a MainVerticle
.) It implements the Users & Rides box in the diagram:
Events
From an overall application perspective, there are several things we care about:
-
A guest got in line for the roller coaster (
USER_IN_QUEUE
) -
A guest got on the roller coaster (
USER_ON_RIDE
) -
A guest got off the roller coaster (
USER_RIDE_COMPLETED
) -
A ride started (
RIDE_STARTED
) -
A ride ended (
RIDE_COMPLETED
)
When a guest gets in line, the currentState
field of the User
object is set to USER_IN_QUEUE
. When a Ride
starts, its state
field is set to RIDE_STARTED
. When a Ride
leaves the station, the currentState
of some number of User
s is set to USER_ON_RIDE
. (Remember, the Ride
does not contain a list of User
s.) If 10 people can ride the coaster at once, the status of at most 10 User
s is updated. (Obviously if there are only three people in line, only three User
s are modified.) By default the Ride
takes 30 seconds, so 30 seconds after the state of the Ride
is set to RIDE_STARTED
its state
is changed to RIDE_COMPLETED
. In turn, the currentStates
of all the User
s on the ON_RIDE
are set to COMPLETED_RIDE
.
Once the MainVerticle
sets up the connection to AMQ, it deploys a BusinessEventTransformer
verticle. (BTW, some class names include the word “verticle,” others don’t. I’ll be specific as we go along.) The BusinessEventTransformer
code sends ride events to the Vert.x event bus via the queue named “to-ride-event-queue
.” User
events are sent to both the “to-user-queue
” and the “to-enter-event-queue
.” The “to-x-event
” queues let other components track when a User
or Ride
is created. The “to-user-queue
” is used to keep up with changes to the state of a User
. User
state changes are shown in the billboard; Ride
state changes are not.
MainVerticle
deploys two verticles to set up the simulation: RideSimulatorVerticle
and UserSimulatorVerticle
. They are configured to use the data grid as they create events.
The RideSimulatorVerticle
continually creates new Ride
objects as long as the coaster is running. The lifecycle of a Ride
is:
-
The
Ride
is created with a state ofPLANNED
. -
Up to n
User
s are selected for the ride, where n is the maximum number of guests that can ride at once. -
The state of all of the selected users is changed to
ON_RIDE
. -
The state of the
Ride
is set toIN_PROGRESS
. -
After n seconds go by, where n is the duration of the
Ride
, the state of theRide
is set toCOMPLETED
. -
The state of each of the
User
s on theRide
is set toCOMPLETED_RIDE
.
The UserSimulatorVerticle
, on the other hand, creates new User
objects as long as user generation is enabled. The lifecycle of a User
is:
-
The
User
is created. TheUser
has aname
, but no other data is passed to the constructor. -
The
User
is passed via theEvents
class (more on that in a second) with theUSER_IN_QUEUE
flag.
The User
’s state is set to ON_RIDE
and ultimately COMPLETED_RIDE
as other events are generated by components that we’ll look at shortly.
To create a name for the User
, the UserSimulatorVerticle
calls the CuteNameService
class. That class has an array of adjectives and an array of nouns. When the UserSimulatorVerticle
needs a new name, CuteNameService
chooses a random word from each of the two arrays to create a name.
On to the Events
class. It is not a verticle, it is a utility class that creates, as you’d expect, events. An instance of the Events
class contains information about a User
or Ride
object. We'll talk about the JSON structure of Events
objects in just a minute.
Finally, the event-generator
component includes WebVerticle
, a class that implements a REST API for some utility methods used by the BillboardVerticle
class. We’ll talk more about the WebVerticle
when we get to the billboard
component. That’s it as far as the event-generator
component is concerned.
Event format
To keep things simple (simpler, anyway), all of the events have the same JSON format:
{"event": EVENT, "user": USER, "ride": RIDE}
If this is a User
event, the value of EVENT
is user-events
, USER
is the JSON version of a User
object, and RIDE
is null
. Similarly, if this is a Ride
event, the value of EVENT
is ride-events
, USER
is null
, and RIDE
is the JSON version of a Ride
object.
Note: A Ride
object refers to the round-trip journey the cars of the coaster make from the station, along the tracks, and back again, carrying some number of User
s with it. It does not refer to the Reactica roller coaster itself.
Storing events
The next step is to take User
events and store them in the data grid. This is done by the event-store
package, which implements the Event Store box in the architecture diagram:
The event-store
package contains three verticles:
-
MainVerticle
– Configures the connection between AMQ and the data grid. It also sets up theUserEventReceiverVerticle
. -
UserEventReceiverVerticle
– TakesUser
event data from AMQ and stores it in the data grid. -
UserMarshaller
– As the name implies, marshalsUser
objects by converting them to ProtoBuff and vice versa. It uses the Infinispan ProtoStream library to do its work.
With these components up and running, any User
event put into AMQ is stored in the data grid. From there, the current-line-updater
and queue-length-calculator
components use the data grid to deliver the data that the billboard
component needs to do its work.
As with all of the components in the system, type oc get pods
to find the name of the component’s pod, then oc logs [pod name]
to see the log. In the example below, the name of the event-store
pod is event-store-1-p455g
. You can see the parts of the User
lifecycle that are handled by the event-store component
from this log excerpt for a User
whose first name is Quill:
oc logs event-store-1-p455g | grep Quill 17:46:31.325 [vert.x-eventloop-thread-0] INFO UserEventReceiverVerticle - RECEIVED USER EVENT: {"id":"Quill Carpet","name":"Quill Carpet","rideId":"reactica","currentState":"IN_QUEUE","enterQueueTime":1562435191,"completedRideTime":0} 17:46:31.335 [vert.x-eventloop-thread-0] INFO UserEventReceiverVerticle - Saved user with id Quill Carpet to the Data Grid 17:48:58.103 [vert.x-eventloop-thread-0] INFO UserEventReceiverVerticle - RECEIVED USER EVENT: {"id":"Quill Carpet","name":"Quill Carpet","rideId":"reactica","currentState":"ON_RIDE","enterQueueTime":1562435191,"completedRideTime":0} 17:48:58.139 [vert.x-eventloop-thread-0] INFO UserEventReceiverVerticle - Saved user with id Quill Carpet to the Data Grid 17:49:58.917 [vert.x-eventloop-thread-0] INFO UserEventReceiverVerticle - RECEIVED USER EVENT: {"id":"Quill Carpet","name":"Quill Carpet","rideId":"reactica","currentState":"COMPLETED_RIDE","enterQueueTime":1562435191,"completedRideTime":1562435398} 17:49:58.942 [vert.x-eventloop-thread-0] INFO UserEventReceiverVerticle - Saved user with id Quill Carpet to the Data Grid
As you would expect, the User
went through the states IN_QUEUE
, ON_RIDE
, and COMPLETED_RIDE
. Based on the timestamps of the log messages, Quill Carpet waited roughly 27 seconds to board the ride and the ride lasted for roughly 60 seconds. For each event, the UserEventReceiverVerticle
stored the data in the data grid. As we said earlier, a responsive system is message driven. Notice that this verticle doesn’t know what component generated each event; it doesn’t matter. As the events happen, they are moved to the data grid, where they can be handled by other components. Of course, the other verticles don’t know what components consume the events either.
Taking stock
That covers the components that generate events. Those components implement the left-hand side of the original diagram:
The event-generator
component actually creates the User
and Ride
objects. The User
objects are sent to the AMQ broker. In response, the event-store
component gets the User
events from the AMQ broker and stores them in the data grid. Now it’s time to look at the components that consume those events. That happens in two stages: First, the queue-length-calculator
and current-line-updater
components process the information in the data grid and send information to the AMQ broker. Second, the billboard
component receives that information from the AMQ broker and updates the web page that displays the current wait time and the current queue.
The two event consumers we'll focus on next are the current-line-updater
and queue-length-calculator
components shown in this diagram:
We’ll look at those event consumers now.
The current line
The current-line-updater
component starts with the MainVerticle
class. MainVerticle
does some configuration tasks, then deploys CurrentLineUpdaterVerticle
. That verticle sets up a continuous query on all User
s in the data grid that are in the states IN_QUEUE
, ON_RIDE
, or COMPLETED_RIDE
, specifying that no more than 10 users in the COMPLETED_RIDE
status should be included. (We don’t care so much about the delighted guests who have finished the ride; knowing the last 10 is plenty.) Once the query is set up, it registers the UserContinuousQueryListener
class to receive events from the query.
Before we go on, a word about continuous queries. A continuous query is basically an implementation of the Observer pattern. CurrentLineUpdaterVerticle
registers a continuous query with the data grid. When the continuous query is set up, the data grid responds with all the data that matched the query. From there, we use the UserContinuousQueryListener
verticle to process any notifications that come from the data grid. Whenever a new User
is added or the state of an existing User
changes, the data grid notifies the listener. UserContinuousQueryListener
then sends the changed data to the CL_QUEUE
at the AMQ broker. We’ll talk about this in a minute, but ultimately the billboard component responds to the new data by updating the list of User
s in the web UI.
The final class in this package is simply another instance of the UserMarshaller
class.
The queue length
The queue-length-calculator
component calculates how long a User
just now getting in line has to wait before they board the coaster. The calculation is based on how many User
s are in line, how many User
s can ride the coaster at the same time, and how long a single Ride
takes. If there are 20 people in line and 10 people can ride at once and the Ride
lasts for 60 seconds, someone just getting in line will have to wait roughly 3 minutes. In other words, two complete Ride
s (neither of which have started yet) will have to take place before everyone in front of the new User
has been on the coaster. Two complete Ride
s take 2 minutes, so the wait time is roughly 3 minutes. If there are 19 people in line, the new User
can board the second Ride
, so they’ll wait roughly 2 minutes before boarding.
As with all the components so far, queue-length-calculator starts with a MainVerticle
to configure the connections and set up the environment. It then creates a QueueLengthCalculator
verticle to set up a periodic query that queries the data grid every 10 seconds. The query returns all of the User
s that have a state of IN_QUEUE
; User
s that are on the coaster or have finished the ride don’t impact the wait time. Once QueueLengthCalculator
determines the wait time, it sends a message that indicates the wait time to the AMQ queue QLC_QUEUE
.
The only other class in this component is another UserMarshaller
.
The Billboard
The other consumer of events in our reactive system is the billboard
component. Everything we’ve done to this point exists simply to create the data the billboard needs to show park guests how long they’ll have to wait to board the coaster and a list of who is in line. All of the work of the billboard
component is handled by the BillboardVerticle
class. The diagram clearly shows how the billboard component gets its data from the AMQ broker:
The billboard
component watches the AMQ broker for messages in two different queues: CL_QUEUE
, which has information about the current queue from the current-line-updater
component; and QLC_QUEUE
, which has information about the current waiting time from queue-length-calculator
.
To be precise, the AMQ broker sends data from those queues to the Vert.x event bus. In turn, the updated data from the event bus is sent to the BillboardVerticle
class via a web socket. That allows the display to remain up-to-date whenever new data is sent to the CL_QUEUE
or QLC_QUEUE
. The basic display looks like this:
The estimated wait time is in bold type at the top of the display. This example shows 10 riders in the queue. The bottom three, in green, are guests who have completed the ride. The four riders in yellow are currently on the ride, while the three at the top, in blue, are waiting in line. At the top of the panel is an entry field that lets us add a user directly to the queue.
The UI also includes an admin console to control the ride. The administrator can start or stop the coaster, they can start or stop new User
s being generated, and they can clear the waiting line altogether.
The admin interface uses REST endpoints defined by the WebVerticle
class in the event-generator
component. When an administrator inside admin.html
clicks a button such as Start User Generation, the JavaScript inside admin.html
sends the message start-user-simulator
to the address named control
on the Vert.x event bus. The BillboardVerticle
class receives messages on the control
address and invokes a REST endpoint. The admin interface uses the following methods:
HTTP verb | Entry point | Purpose |
---|---|---|
POST |
/user |
Adds a new User (actually used by the entry field in the main UI) |
POST |
/simulators/users |
Starts or stops generating new User s |
DELETE |
/simulators/users |
Deletes all User s |
POST |
/simulators/ride |
Starts or stops generating new Ride s |
What’s next
That completes our tour of the code. Now that we’ve looked at how the reactive system works, it’s time to move on to Part 3, A reactive system in action. That article shows you how to deploy and run the Reactica code.
And, as always, we love to hear from you! We're coderland@redhat.com if you have any comments or questions.
Last updated: April 21, 2021