In the previous post, we added actors to handle concurrency in a more sensible manner inside of our Play application. In this post we add another API endpoint, relying further on our existing actor model and creating a new method for operating on Futures.
As before, the source code for this tutorial series is available on github with the code specifically for this post here. Since the code builds on the previous post, you can grab the current state of the project (where part four finished) here.
UPDATE: The original version of this post defined an incorrect projection for the augmented TicketBlock. The code has been fixed, but the commits on the GitHub version do not line up perfectly.
Limitations of the current API
Our existing API for listing ticket blocks works, but it is not very practical. The only way to get information about a ticket block is to either know the id field ahead of time, or list all ticket blocks ever created. If we take a minute to think about how our API is likely to be used, we can safely assume that the user will desire a behavior flow similar to this:
- Select the event they want to attend
- Inspect the block of tickets available to purchase for that event
- Place an order of x tickets from the block
Our current API is not very good for #2, and does not even support displaying if the block still has tickets available for purchase. We need an API endpoint that can supply the ticket block information from a given event, including the amount of tickets still available for purchase.
If we were to create a new API endpoint, it should probably follow this pattern:
GET /events/:eventID/tickets/blocks/
Now that we have our end goal in mind, we can get started. Do not bother adding
the endpoint to the routes
file yet, as we need to create the controller
definition first.
Creating the controller endpoint
Since our resource path root will be /events/
, we will add the new method to
our Events
controller. We will create a method named ticketBlocksForEvent
.
The new method will contain boilerplate code to verify the event exists. Once it
has the correct Event
, it will make a new method call on the Event
instance
requesting a Future
of all TicketBlock
s associated with that event.
A basic implementation
Now that we have defined the requirements for the ticketBlocksWithAvailability
method, we can fill out the basic implementation in our Event
case class.
And add the TicketBlock.listForEvent()
method…
Tell me something new
We now have the remarkably unremarkable code required to retrieve a
TicketBlock
for a given Event
. There is just one small detail we have
ignored. None of this supplies the actual tickets available in a returned
ticket block.
Open TicketIsser.scala
and add the following case class.
This will serve as the message used to communicate with the issuer actors to
request the current availability of a ticket block. We can add support for this
directly in the TicketIssuer
actor. Inside the TicketIssuer
we simply
forward the request to the TicketIssuerWorker
if it exists.
Adding support to TicketIssuerWorker
is slightly more involved, due to the
multiple receive states.
Finding a place for the new data
With the code for AvailabilityCheck
in place, we can revisit the
ticketBlocksWithAvailability
method in the Event
class. Currently, this
method returns a Future[Seq[TicketBlock]]
. Our current design for a
TicketBlock
does not include a count for the available ticket count. We can
update the TicketBlock
model to include an Option
for the quantity of
available tickets. Using an Option
here allows us to include the value when
we have it, and ignore it any other time.
We will also need to update our *
projection in the TicketBlocksTable
class
to handle the fact this field is unrelated to our database table.
UPDATE: There was a mistake in the original version of the code, so the update for this appears later in the GitHub version. Yes, there is a post coming on unit testing.
Setting the stage
Now that our TicketBlock
has been augmented to support this new optional
field, we can update the ticketBlocksWithAvailability
method to actually
include the available quantity. Before adding the core logic, we will start with
some supporting code. We know we will need to perform an ask
request to
actors, which in turn requires a Timeout
. Our existing timeout (for placing an
order) is for an action that people will wait for. When it comes to the
availability of tickets, users are unlikely to be as patient. Create a new
parameter in application.conf
named ticket_availability_ms
.
Since the original timeout is in seconds, we are appending the unit to this
parameter. Now that the parameter is defined, we can add the Timeout
to the
Events
controller.
Gathering all the data
We leave the timeout as an implicit val, which in turn is implicitly passed
into the ask
request in ticketBlocksWithAvailability
.
The code retrieves the list of ticket blocks with our new database method. We
then iterate through the blocks retrieved from the database and make a request
to the TicketIssuer
for the availability of tickets for each block. This
results in a sequence of Future
s, one for each AvailabilityCheck
performed.
Close, but not good enough
Inverting a sequence of futures is so common that the standard library includes
the Future.sequence
method, which our code used because the type signatures
seemed to be what we wanted. Unfortunately, the Future.sequence()
function
gives up at the first sign of trouble.
Any Future
that results in a failure will cause the whole sequence to become a
failure. That is not what we want to happen at all, in fact we would prefer the
opposite. We do not really care if some of the blocks do not respond, some
information coming back to the user is better than no information coming back.
We actually expect some of our Future
s to fail, due to the way we raise
exceptions via the actors Failure
type. To allow for this, we will create a
new Future
-like object called OptimisticFuture
.
Our new code performs a map
on the sequence, wrapping Successful results in an
Option
. We use the recover
method to discard the failures and replace them
with None
. We could easily use a Try
or an Either
instead of Option
here, but we are not really concerned with why the failure occurred here. Once
the Option
sequence exists, we can flatten
it to discard the None
entries.
With this new utility object, we can create the Future[Seq]
we want that only
contains successful futures.
We now have enough implemented to create our route endpoint.
Good enough to test
By making a curl request, we can verify the first event returns the expected ticket blocks.
That was unexpected. There should be a valid response coming back. Just to be
sure it is not our new code, we will run the exact same curl
command again
without restarting our application.
What happened here is that our actor selection that loads the
TicketIssuer
on demand has a problem that it is not readable for a short
period of time while it initializes. This is as designed, but if no other
service has attempted to use this actor, this call will fail. We will be fixing
this in a later post.
In the meantime, it actually serves to illustrate that our failing futures
(generating a TicketBlockUnavailable
failure) will actually be suppressed by
our OptimisticFuture
Until next time…
In this post we covered handling a sequence of future results that are not all
required to build a response and converted them to a single Future
containing
all of the results from the tasks that completed successfully. This is a
useful pattern for aggregating results from disparate 3rd party services that
may or may not ever return results.
In part 6, we will take a break from the back end and give our user interface a sorely needed makeover.