Edit and complete the Async Inline Caching Sample Guide.

Refactor the Async Inline Caching Sample Code to include (Asciidoc) documentation markers.

Change the batch time interval in the AEQ batch-size configuration/strategy bean definition to 15 minutes.

Rename the GolfTournament created in the SpringBootApplication class to 'The Masters'.

Refactor the isFinished(..) and isNotFinished() methods in the PgaTourService class to accept a nullable GolfTournament argument.

Refactor the GolferController class to declare the REST API web service endpoints to use '/api/golf/tournament/' as the base Webapp context.
This commit is contained in:
John Blum
2020-12-08 23:50:12 -08:00
parent 816a441d6e
commit 1a33f03e3a
11 changed files with 420 additions and 40 deletions

View File

@@ -46,30 +46,32 @@ link:../index.html#geode-samples[Back to Samples]
== Background
In _Synchronous Inline Caching_, data is immediately read from or written to the primary data source, (a.k.a. the
_System of Record_ (SOR)), before the cache is modified, thereby guaranteeing a degree of consistency. The "synchronous"
arrangement of the _Inline Caching_ pattern is commonly referred to as "_Read/Write-Through_".
_System of Record_ (SOR)), before the cache is modified, thereby guaranteeing a degree of consistency between the cache
and the backend data source. The "synchronous" arrangement of the _Inline Caching_ pattern is commonly referred to as
"_Read/Write-Through_".
With _Asynchronous Inline Caching_, data changes are written to the primary data source asynchronously, after the cache
has already been modified. The "asynchronous" arrangement of the _Inline Caching_ pattern is commonly referred to as
"_Write-Behind_". The cache entry is modified, then, and only then, will the primary data source possibly reflect the
changes sometime later.
"_Write-Behind_". The cache entry is modified, then, and only then, will the primary data source reflect the changes
sometime later.
It is possible for the primary data source (i.e. _System of Record_ (SOR)) and the cache to get out-of-sync. Clearly,
the primary data source may contain information that the cache does not. Another application may be updating the primary
data source and not using the cache. The cache entry change may not be promptly written to the primary data source until
the "_Write-Behind_" operation is triggered, which is often implementation dependent. The data change might violate
a database constraint, fail to commit and be rolled back. All sorts of reasons can cause the primary data source
and the cache to become out-of-sync, or inconsistent.
Due to the asynchronous nature of _Async Inline Caching_, it is possible for the primary data source (i.e. _System
of Record_ (SOR)) and the cache to get out-of-sync. Additionally, the primary data source may contain information that
the cache does not. That is to say, another application may be updating the primary data source and not using the cache.
The cache entry change may not be promptly written to the primary data source until the "_Write-Behind_" operation is
triggered, which is often implementation dependent. A data change could violate a database constraint, fail to commit
and be rolled back. All sorts of reasons can cause the primary data source and the cache to become out-of-sync,
or inconsistent.
For this reason, throughput and latency are the primary applications concerns, rather than consistency, for when to use
_Asynchronous Inline Caching_.
For this reason, throughput and latency are the primary application concerns, rather than consistency, when using
the _Asynchronous Inline Caching_ pattern.
The general pattern of Inline Caching is depicted as follows:
image::{images-dir}/Inline-Caching-Overview.png[]
The layer in the application/system architecture involving _Inline Caching_ logic sits between the cache and the primary
data source:
The layer in the application/system architecture involving the _Inline Caching_ logic sits between the cache
and the primary data source:
image::{images-dir}/Inline-Caching-Layer.png[]
@@ -85,24 +87,379 @@ IMPLEMENTATION
As readers should know, the application cache is backed by an {apache-geode-name} Region.
In _Synchronous_, _Read-Through_ and/or _Write-Through_, _Inline Caching_, a `CacheLoader` configured for the Region
and used to "_Read-Through_" to the backend/primary data source on a cache miss. When a cache entry is written, then
a configured `CacheWriter` for the Region is invoked to "_Write-Through_" to the backend/primary data source. The cache
is only modified if the `CacheWriter` was successful in modify the backend/primary data source.
In _Synchronous_, _Read-Through_ and/or _Write-Through_, _Inline Caching_, a `CacheLoader` is configured for the Region
and used to "_Read-Through_" to the backend/primary data source on a cache miss. When a cache entry is written, a
configured `CacheWriter` for the Region is invoked to "_Write-Through_" to the backend/primary data source. The cache
is only modified if the `CacheWriter` was successful in modifying the backend/primary data source.
Both the `CacheLoader` and `CacheWriter` are optional. That is, you can configure one side of
_Synchronous Inline Caching_, either the "_Read-Through_", or the "_Write-Through_", both, or neither.
Both the `CacheLoader` and `CacheWriter` are optional. That is, you can configure just one side of
_Synchronous Inline Caching_ or the other, either "_Read-Through_" or "_Write-Through_", both, or neither.
With _Asynchronous, Write-Behind, Inline Caching_, you (may) configure the Region with an associated `AsyncEventQueue`
(AEQ) and registered `AsyncEventListener`. When the cache is written to, the entry event is then forwarded and stored
on the AEQ, where at sometime later, the registered `AsyncEventListener` for the AEQ will be invoked, which can then
asynchronously modify the backend/primary data source.
on the AEQ, where at sometime later, the registered `AsyncEventListener` for the AEQ will be invoked to process the
(batch of) `AsyncEvents`, which can then asynchronously modify the backend/primary data source.
Unlike _Synchronous Inline Caching_, _Asynchronous Inline Caching_ does not have an equivalent for "_Read-Through_",
such as "_Read-Behind_".
such as "_Read-Behind_", particularly in a Reactive sense.
NOTE: At some point later, we may consider the development of "_Read-Behind_" with with use of Reactive Programming
and the Reactive Spring Data Repository abstraction.
link:../index.html#geode-samples[Back to Samples]
[[geode-samples-caching-inline-asynchronous-example]]
== Example
For our example, we have built a Golf Tournament application that runs a simulation with a set of professional golfers
playing at _The Masters_. The (12) golfers play 18 holes of golf in pairs and proceed from hole 1 to hole 18 in under
a minute. For each hole played, their score of the whole is calculated. At the end of the round, each golfers final
score is calculated relative to par for the course (72).
The Golf Tournament application is a Spring Boot application using {apache-geode-name} to persist the golfers score in
realtime as the players complete each hole. However, to make the play "official", the golfer's score is recorded to a
backend database, asynchronously using _Asynchronous_, _Write-Behind_, _Inline Caching_. It is assumed that there is
additional validation required (e.g. such as signing scorecards, etc) that goes on before the final score is accepted
and recorded to the _System of Record_ (SOR), in the "history books", so to speak.
Now that the problem context is established, let's review a few of the classes.
NOTE: Each of the application domain classes are code snippets or a preview of the actual class, and not actual code.
See the actual Sample code for more detail.
We start by defining our Golf Tournament application domain model types, starting with the `Golfer` class. Essentially,
the `Golfer` class models a person who plays golf and is defined as:
.`Golfer` class.
[source,java]
----
@Entity
@Table(name = "golfers")
public class Golfer implements Comparable<Golfer> {
@javax.persistence.Id @Id
private String name;
private Integer hole = 0;
private Integer score = 0;
}
----
The `Golfer` class has been annotated with JPA's `@Entity` annotation making it a proper (persistent) entity class.
The `Golfer` class is also annotated with `@Table` to persist instances of `Golfer` into the "_golfers_" table
of the database.
The application also defines a non-entity, `GolfCourse` class to model the golf course, which requires a name
and `List` of pars for each hole (all 18 holes) of the golf course:
.`GolfCourse` class
[source,java]
----
class GolfCourse {
private final String name;
private final List<Integer> parForHole = new ArrayList<>(18);
}
----
Next, a non-entity, `GolfTournament` class has been defined to model the golf tournament being played. It expects a name
for the tournament, the golf course` where the tournament is held and played, and a `Set` of `Golfers` (players)
registered to play.
Additionally, the `GolfTournament` class contains an inner class, the `Pairing` class, to group the registered players
into pairs to play a round.
.`GolfTournament` class
[source,java]
----
class GolfTournament implements Iterable<Pairing> {
private final String name;
private GolfCourse golfCourse;
private final List<Pairing> pairings = new ArrayList<>();
private final Set<Golfer> players = new ArrayList<>();
public static class Pair {
private final Golfer playerOne;
private final Golfer playerTwo;
}
}
----
The `GolfTournament.Pairing` class serves as a composite acting on both players in the pair, such as to advance
the hole of play.
The `GolfTournament` class has additional builder methods to register players, build pairings, enable the tournament
to be played and determine when the tournament is finished (i.e. when all pairs complete all 18 holes of play).
There is a `GolferRepository` interface extending the `JpaRepository` interface to persist the state of each `Golfer`
to the backend database:
.`GolferRepository` interface
[source,java]
----
interface GolferRepository extends JpaRepository<Golfer, String> { }
----
NOTE: While `GolferRepository` extends from the `JpaRepository` interface directly, it is recommended to extend
the `CrudRepository` interface instead, keeping your application SD _Repositories_ agnostic from the underlying
data store. The reason `GolferRepository` extends from the `JpaRepository` interface directly, is to make it absolutely
clear that the `Golfer` state will be persisted to a backend database (RDBMS) using JPA along with Hibernate as the
provider.
The `GolferRepository` will be used by SBDG's _Asynchronous Inline Caching_ framework and infrastructure components.
The _Repository_ is injected into and used by the `AsyncEventListener` registered on the AEQ attached to the "Golfers"
Region to perform asynchronous, _Write-Behind_, _Inline Caching_, operations to the backend database
and _System of Record_ (SOR).
We'll see in a moment how this association is made and how _Asynchronous Inline Caching_ is setup, made simple by SBDG.
To encapsulate the application logic and provide a (possibly transactional) facade to the `Golfer's` state,
a `GolferService` class has been defined:
.`GolferService` class
[source,java]
----
@Service
class GolferService {
@CachePut(cacheNames = "Golfers", key = "#golfer.name")
public Golfer update(Golfer golfer) {
return golfer;
}
public List<Golfer> getAllGolfersFromCache() {
// Use SDG GemfireTemplate to access the "Golfers" Region
}
public List<Golfer> getAllGolfersFromDatabase() {
// Use the GolfersRepository to access the "Golfers" stored in the database.
}
}
----
The `GolferService` class has been marked as a application service using Spring's `@Service` stereotype annotation.
Along with the `GolferService` the application uses a `PgaTourService` class to manage and run a (single)
`GolfTournament`. Its primary method used to run a `GolfTournament` is the `play()` method:
.`PgaTourService` class, `play()` method
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/PgaTourService.java[tags=play]
----
This is a Spring `@Scheduled` service method called every 2.5 seconds after an initial delay of 5 seconds. Essentially,
the service method runs through the pairings, where each `Golfer` plays all 18 holes, their scores are calculated
and recorded for each hole until the round is completed, whether the players score is then calculated relative to par
for course and recorded to the cache, and eventually the database.
To get everything started, a Spring Boot application class (i.e. a class annotated with the `@SpringBootApplication`
annotation) is used to bootstrap the application.
.`BootGeodeAsyncInlineCachingClientApplication` class
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/client/BootGeodeAsyncInlineCachingClientApplication.java[tags=class]
----
The `GolfTournament` is kicked off in the `ApplicationRunner`.
.`ApplicationRunner` bean in the `GolfApplicationConfiguration` class
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/client/BootGeodeAsyncInlineCachingClientApplication.java[tags=application-configuration]
----
As the golf tournament progresses (in the `@Scheduled`, PgaTourService.play()` service method), the updates to the
`Golfers` in the pairs are written to the "_Golfers_" cache (i.e. "_Golfers_" Region) by calling
the `GolferService.update(:Golfer)` service method:
.`GolferService` class, `update(:Golfer)` method
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/client/service/GolferService.java[tags=cache-put]
----
This method simply "puts" the `Golfer` in the cache (i.e. "_Golfers_" Region) mapped to the `Golfer's name` (as a
key/value cache entry).
The cache/Region entry `put` operation results in cache event being added to the AEQ, which will eventually trigger
the SBDG framework-provided `AsyncEventListener` with our injected `GolferRepository` to write the `Golfer's` state
to the backend database.
The configuration of the "_Golfers_" Region (cache) with an AEQ and listener using the `GolferRepository` is defined
as follows:
.`AsyncInlineCachingConfiguration` class
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/config/AsyncInlineCachingConfiguration.java[tags=class]
----
The Spring `@Configuration` class used to enable _Async Inline Caching_ consists of 2 different AEQ configuration
arrangements and bean definitions.
The first is a AEQ configured with a "preference" for being triggered on the *batch size*, i.e. the number of events
present in the AEQ:
.`AsyncInlineCachingConfiguration` class
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/config/AsyncInlineCachingConfiguration.java[tags=queue-batch-size]
----
The second AEQ configuration uses a "preference" for being triggered based on a *batch time interval*, i.e. after
a period of time has elapsed, such as 5 seconds.
NOTE: The default AEQ _batch time interval_ in {apache-geode-name} is *5 milliseconds* (5 ms). However, to demonstrate
the asynchronous nature of the cache to database update, a much longer delay was used. Likewise, the default AEQ
_batch size_ is *100*.
In both AEQ configurations and bean definitions, the _batch size_ and _batch time interval_ have been set (overriding
the {apache-geode-name} defaults) in order to show the effects of each AEQ settings independently. As you can imagine,
particularly in a highly concurrent and transactional application with frequent updates, it would be hard to determine
whether the AEQ event processing (and listener) was triggered by the _batch time interval_ or the _batch size_. And,
with a default *5 millisecond* _batch time interval_, it is hard to witness the asynchronous nature of the cache
to database updates to begin with.
We will have more to say on the AEQ configuration below.
The final class in the golf application is a `GolferController` class annotated with Spring's `@RestController`
annotation in order to expose our golf application functionality as an API in a REST-ful interface:
.`GolferService` class, `update(:Golfer)` method
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/client/web/GolferController.java[tags=class]
----
The Spring Web MVC `@RestController` class exposes two REST-ful API web service endpoints returning JSON data:
* http://localhost:8080/api/golf/tournament/cache - used to get the current state of the golfers from the cache
* http://localhost:8080/api/golf/tournament/database - used to get the current state of the golfers from the database
Both web service endpoints are consumed by the `golf-tournament-view.html` page, which uses jQuery and AJAX to make
periodic HTTP requests to refresh the page.
[[geode-samples-caching-inline-asynchronous-example-run]]
== Run the Example
To run the example, there are few more configuration details we need to cover.
While it is possible to run this example using an {apache-geode-name} client/server topology, we keep things simple
by running the example using a single Spring Boot application class, namely the
`BootGeodeAsyncInlineCachingClientApplication` along with a peer cache configuration.
That is, in our `BootGeodeAsyncInlineCachingClientApplication` class, we also apply the
`PeerCacheApplicationConfiguration` by enabling the Spring Profile, "_peer-cache_":
.`PeerCacheApplicationConfiguration` class
[source,java]
----
include::{samples-dir}/caching/inline-async/src/main/java/example/app/caching/inline/async/client/BootGeodeAsyncInlineCachingClientApplication.java[tags=peer-cache-configuration]
----
It should be noted that an AEQ can only be created and registered on a Region existing on the server-side of
an {apache-geode-name} system. That is, you cannot add an AEQ to a client-side Region. Therefore, in all your
_Async Inline Caching_ Uses Cases (UC), synchronous or asynchronous, it will be the servers in an {apache-geode-name}
cluster that are responsible for _Write-Behind_ functionality to the backend data store, not a Spring Boot,
{apache-geode-name} client application.
However, for demonstration purposes, we override SBDG's default, auto-configuration providing a `ClientCache` instance
by simply enabling the "_peer-cache_" Spring Profile, which replaces the `ClientCache` instance with a peer `Cache`
instance instead.
Finally, when running this application, you must decide on your AEQ management strategy. For example, do you want
the AEQ listener to be triggered by *batch size* (i.e. the number of cache events) or using the *batch time interval*.
Each strategy can be enabled using a Spring Profile, either "_queue-batch-size_" or "_queue-batch-time-interval_".
This allows you to experiment with different AEQ management strategies to observe the effects.
In total, the Spring Profiles you need to enable would appear as follows:
.Spring Profiles to enable when running the application.
[source,txt]
----
-Dspring.profiles.active=peer-cache,queue-batch-size,server
----
Of course, you can replace "_queue-batch-size_" with "_queue-batch-time-interval_".
The final configuration of the Spring Boot application, as seen in IntelliJ IDEA is:
image::{images-dir}/BootGeodeAsyncInlineCachingClientApplication-IntelliJ-IDEA-Run-Configuration.png[]
To access the golf application, simply navigate to:
http://localhost:8080/golf-tournament-view.html
You should see a web page similar to:
image::{images-dir}/Asynchronous-Inline-Caching-Application.png[]
You can also run this example using the SBDG Gradle build from the command-line like so:
.Run the example using Gradle
[source,txt]
----
$ gradlew --no-daemon :spring-geode-sample-caching-inline-async:bootRun
----
This is convenient since the Spring Profiles are already configured for you.
However, when you switch to using the "_queue-batch-time-interval_" you will see a similar effect and behavior, but
with on a slightly different schedule for the database updates, i.e. at a fixed 5 second interval.
[[geode-samples-caching-inline-asynchronous-example-conclusion]]
== Conclusion
_Asynchronous Inline Caching_ can be a powerful pattern of caching applied to your diverse Spring Boot application
workflows depending on the use case and requirements.
If throughput and latency are important to create the necessary responsiveness in your application design and user
experience, and consistency (i.e. between the cache and the backend _System of Record_ (SOR), or database) is not
as important of a concern, then you might want to consider _Asynchronous Inline Caching_.
There are many factors to consider in the configuration of the AEQ that is at the heart of an _Asynchronous Inline
Caching_ configuration, such as the appropriate *batch size* and *batch time interval*. Neither setting is exclusive
from the other, in fact. Both settings are considered when {apache-geode-name} makes a decision of when to trigger
the listener registered on the AEQ to process the events from operations on the Region to which the AEQ is attached.
You must decide on your *batch size*, based on how many events might occur in a given period of time. If the frequency
is quite high, then you might need a smaller *batch size*, for instance. The AEQ is in-memory, therefore you must be
conscious of memory constraints on your system, especially during peak loads. Of course, the AEQ can be configured
to overflow events to disk and even persist events between restarts, but ideally you want these events to be processed
in as near realtime as possible.
However, when the load on your application is low and events occur sporadically, you also must be mindful that the
events do not sit in the AEQ for too long. If you have *batch size* of 1000, and there are currently only 20 events
(well, any number of events less than 1000) sitting in the AEQ waiting to be processed, then the *batch time interval*
becomes important, especially so that these remaining events (less than the configured *batch size*) don't wait in the
queue indefinitely.
Other factors to consider are whether you can conflate the events in the AEQ. This minimizes the number of events for
a single logical Object to the latest update. Additionally, do you need to overflow events to disk after the configured
maximum queue memory is reached, or should events simply be discarded? Do you need to maintain the events in the queue
between restarts (i.e. configure the AEQ to be persistent)? Do the disk writes for overflow and/or persistence need to
be synchronous? Do the events in the queue need to be ordered based on some `OrderPolicy`? Do the events need to be
filtered? How many dispatcher threads do you require? Etc. Etc. There are many important things consider in the
configuration of the AEQ when using _Asynchronous Inline Caching_ for _Write-Behind_ capabilities.
Usually, it is safe to start with the defaults and adjust as needed, and as your measurements and tests dictate.
We hope that you found this guide useful and that it has armed you with more knowledge to tackle difficult problems,
the type of application problems where the _Asynchronous Inline Caching_ pattern can be applied with immediate benefits.
Good luck.
link:../index.html#geode-samples[Back to Samples]

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB