Reactica roller coaster image

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 the User and Ride objects used throughout the system.

  • event-generator: Creates new User and Ride objects at certain intervals. These objects are serialized and sent via Events objects to Red Hat AMQ.

  • event-store: Takes the User 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 new Users and Rides or a change to the state of an existing User or Ride.

  • 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: 

Reactica architecture

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 the User 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 a Ride object is created

  • IN_PROGRESS – The Ride has left the station, meaning that all of the Users on the ride transition to the ON_RIDE state

  • COMPLETED – The Ride has returned to the station, meaning that all of the Users on the ride transition to the COMPLETED_RIDE state.

A Ride object does not include a list of Users on it. When a new Ride is created, microservices in the eventstore component take the appropriate number of Users 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: 

The Users and Rides component

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 Users is set to USER_ON_RIDE. (Remember, the Ride does not contain a list of Users.) If 10 people can ride the coaster at once, the status of at most 10 Users is updated. (Obviously if there are only three people in line, only three Users 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 Users 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 of PLANNED.

  • Up to n Users 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 to IN_PROGRESS.

  • After n seconds go by, where n is the duration of the Ride, the state of the Ride is set to COMPLETED.

  • The state of each of the Users on the Ride is set to COMPLETED_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. The User has a name, but no other data is passed to the constructor.

  • The User is passed via the Events class (more on that in a second) with the USER_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 Users 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: 

Reactica event store component

The event-store package contains three verticles: 

  • MainVerticle – Configures the connection between AMQ and the data grid. It also sets up the UserEventReceiverVerticle.

  • UserEventReceiverVerticle – Takes User event data from AMQ and stores it in the data grid.

  • UserMarshaller – As the name implies, marshals User 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: 

Reactica - main data producers

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: 

Reactiva architecture - queue length estimate and current line updater

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 Users 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 Users 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 Users are in line, how many Users 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 Rides (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 Rides 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 Users that have a state of IN_QUEUE; Users 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: 

Reactica architecture - Billboard component

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:  

Reactica web UI

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 Users being generated, and they can clear the waiting line altogether. 

Reactica admin UI

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 Users
DELETE /simulators/users Deletes all Users
POST /simulators/ride Starts or stops generating new Rides

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