Edit section on Asynchronous (Write-Behind) Inline Cacching in caching chapter.

Resolves gh-58.
This commit is contained in:
John Blum
2020-08-20 12:01:10 -07:00
parent 0c981aad4f
commit 24ac11715a

View File

@@ -496,38 +496,39 @@ from the SBDG test suite. A dedicated sample will be provided in a future releas
[[geode-caching-provider-inline-caching-asynchronous]]
===== Asynchronous Inline Caching
If consistency between the cache and your backend, external data source is not a concern, and you only need to write
from the cache to the backend data store periodically, then you can employ _asynchronous_ (Write-Behind_)
_Inline Caching_.
If consistency between the cache and your external, backend data source is not a concern, and you only need to write
from the cache to the backend data store periodically, then you can employ asynchronous (_Write-Behind_) _Inline Caching_.
As the term implies, a write to the backend data store is "_asynchronous_" and not strictly tied to the cache operation.
As a result, the backend data store will be "_eventually consistent_" with the cache and the cache is primarily used by
the application to access and manage state at runtime. In this case, the backend data store is used to persist the state
of the cache at periodic intervals.
As the term "_Write-Behind_" implies, a write to the backend data store is asynchronous and not strictly tied to the
cache operation. As a result, the backend data store will be in an "_eventually consistent_" state since the cache is
primarily used by the application at runtime to access and manage data. In this case, the backend data store is used
to persist the state of the cache, and that of the application, at periodic intervals.
Of course, if multiple applications or processes are updating the backend data store concurrently, then you could
combine a `CacheLoader` to synchronously "_read-through_" to the backend data store in order to keep the cache
up-to-date as data is accessed and then asynchronously _write-behind_ when the cache is updated to eventually inform
other interested application/processes of the changes. In this capacity, the backend data store (e.g. database) is
still the primary _System of Record_ (SOR).
Of course, if multiple applications are updating the backend data store concurrently, you could combine a `CacheLoader`
to synchronously "_Read-Through_" to the backend data store and keep the cache up-to-date as well as asynchronously
_Write-Behind_ from the cache to the backend data store when the cache is updated to eventually inform other interested
applications of data changes. In this capacity, the backend data store is still the primary _System of Record_ (SOR).
If data processing is not time sensitive, then you can gain a performance boost from periodic, quantity
and/or time-based batch updates.
If data processing is not time sensitive, you can gain a performance advantage from periodic, quantity and/or time-based
batch updates.
[[geode-caching-provider-inline-caching-asynchronous-asynceventlistener]]
====== Implementing an AsyncEventListener for Inline Caching
If you were to implement _asynchronous_ (_Write-Behind_) _Inline Caching_ by hand, with access to and control over
low-level configuration details, then you would need to do the following yourself:
If you were to configure asynchronous (_Write-Behind_) _Inline Caching_ by hand, then you would need to do all of
the following yourself:
1. Implement an `AsyncEventListener` to write to an external, backend data source on cache events
2. Configure and create an `AsyncEventQueue` (AEQ) with the listener
3. Create a Region and attach the AEQ
2. Configure, create and register the listener with an `AsyncEventQueue` (AEQ)
3. Create a Region serving as the source of cache events and attach the AEQ
Following from our _synchronous_, _Read/Write-Through_ _Inline Caching_ example from the prior sections above, our
`AsyncEventListener` implementation might appear as follows:
The advantage of this approach is you have access to and control over low-level configuration details. The disadvantage,
of course, is with more moving parts, it is easier to mess things up.
.Example `AsyncEventListener` for _Inline Caching_
Following on from our synchronous (_Read/Write-Through_) _Inline Caching_ examples from the prior sections above,
our `AsyncEventListener` implementation might appear as follows:
.Example `AsyncEventListener` for Async _Inline Caching_
[source,java]
----
@Component
@@ -542,18 +543,19 @@ class ExampleAsyncEventListener implements AsyncEventListener {
@Override
public boolean processEvents(List<AsyncEvent> events) {
// Iterate over the ordered AsyncEvents and use the DataSource to write to the external, backend DataSource
// Iterate over the ordered AsyncEvents and use the DataSource
// to write to the external, backend DataSource
}
}
----
NOTE: Of course, instead of injecting a `DataSource` object directly, you could use JDBC, Spring's `JdbcTemplate`,
JPA/Hibernate or another data access API/Framework. Further below, we will show how SBDG simplifies the
`AsyncEventListener` using Spring Data _Repositories_.
NOTE: Instead of injecting a `DataSource` into your `AsyncEventListener` directly, you could use JDBC,
Spring's `JdbcTemplate`, JPA/Hibernate or another data access API/Framework. Further below, we will show how SBDG
simplifies the `AsyncEventListener` implementation by using Spring Data _Repositories_.
Then, we need to register this listener with a configured `AsyncEventQueue` (AEQ) (2) and attach it to the target Region
that is the source of the cache events we want to persist asynchronously (3):
Then, we need to register this listener with a `AsyncEventQueue` (AEQ) (#2) and attach it to the target Region
that will be the source of the cache events we want to persist asynchronously (#3):
.Configure and Create an `AsyncEventQueue`
[source,java]
@@ -564,17 +566,16 @@ class GeodeConfiguration {
@Bean
DataSource exampleDataSource() {
// Configure and construct a data store specific DataSource then return it
// Configure and construct a data store specific DataSource
}
@Bean("ExampleListener")
@Bean
ExampleAsyncEventListener exampleAsyncEventListener(DataSource dataSource) {
return new ExampleAsyncEventListener(dataSource);
}
@Bean("ExampleQueue")
AsyncEventQueueFactoryBean exampleAsyncEventQueue(Cache peerCache,
@Qualifier("ExampleListener") ExampleAsyncEventListener listener) {
@Bean
AsyncEventQueueFactoryBean exampleAsyncEventQueue(Cache peerCache, ExampleAsyncEventListener listener) {
AsyncEventQueueFactoryBean asyncEventQueue = new AsyncEventQueueFactoryBean(peerCache, listener);
@@ -587,9 +588,8 @@ class GeodeConfiguration {
return asyncEventQueue;
}
@Bean("ExampleRegion")
PartitionedRegionFactoryBean<?, ?> exampleRegion(Cache peerCache,
@Qualifier("ExampleQueue") AsyncEventQueue queue) {
@Bean("Example")
PartitionedRegionFactoryBean<?, ?> exampleRegion(Cache peerCache, AsyncEventQueue queue) {
PartitionedRegionFactoryBean<?, ?> exampleRegion = new PartitionedRegionFactoryBean<>();
@@ -606,12 +606,12 @@ While this approach affords you the developer a lot of control over the (low-lev
your `AsyncEventListener` implementation, this is a lot of boilerplate code.
TIP: See the {spring-data-geode-javadoc}/org/springframework/data/gemfire/wan/AsyncEventQueueFactoryBean.html[Javadoc]
on SDG's `AsyncEventQueueFactoryBean` for custom configuration of the AEQ.
on SDG's `AsyncEventQueueFactoryBean` for more details on the configuration of the AEQ.
TIP: See {geode-name}'s {apache-geode-docs}/developing/events/implementing_write_behind_event_handler.html[User Guide]
for more details on AEQs and listeners.
Fortunately, with SBDG, there is a better way.
Fortunately, with SBDG, there is a better way!
[[geode-caching-provider-inline-caching-asynchronous-using-spring-data-repositories]]
====== Asynchronous Inline Caching using Spring Data Repositories
@@ -623,16 +623,16 @@ as follows:
[source,java]
----
@SpringBootApplication
@EntityScan(basePackageClasses = Example.class)
@EntityScan(basePackageClasses = ExampleEntity.class)
@EnableJpaRepositories(basePackageClasses = ExampleRepository.class)
@EnableEntityDefinedRegions(basePackageClasses = Example.class)
class SpringBootRdbmsApacheGeodeAsyncInlineCachingApplication {
@EnableEntityDefinedRegions(basePackageClasses = ExampleEnity.class)
class ExampleSpringBootApacheGeodeAsyncInlineCachingApplication {
@Bean
AsyncInlineCachingRegionConfigurer asyncInlineCachingRegionConfigurer(
@Qualifier("ExampleRepository") CrudRepository<?, ?> repository) {
CrudRepository<ExampleEntity, Long> repository) {
return AsyncInlineCachingRegionConfigurer.create(repository, "ExampleRegion")
return AsyncInlineCachingRegionConfigurer.create(repository, "Example")
.withQueueBatchConflationEnabled()
.withQueueBatchSize(50)
.withQueueBatchTimeInterval(Duration.ofSeconds(15))
@@ -642,8 +642,8 @@ class SpringBootRdbmsApacheGeodeAsyncInlineCachingApplication {
----
The `AsyncInlineCachingRegionConfigurer.create(..)` method is overloaded to accept a `Predicate` in place of the `String`
identifying the target Region (by name) on which to configure asynchronous _Inline Caching_ functionality to express
more powerful matching logic, programmatically.
in order to express more powerful matching logic, programmatically, identifying the target Region (by name) on which to
configure asynchronous _Inline Caching_ functionality.
The `AsyncInlineCachingRegionConfigurer` uses the https://en.wikipedia.org/wiki/Builder_pattern[_Builder Software Design Pattern_]
and `withQueue*(..)` builder methods to configure the underlying `AsyncEventQueue` (AEQ) when the queue's configuration
@@ -658,20 +658,24 @@ With the power of Spring Boot _auto-configuration_ and SBDG, the configuration i
[[geode-caching-provider-inline-caching-asynchronous-listener]]
====== About `RepositoryAsyncEventListener`
The SBDG `RepositoryAsyncEventListener` class is the magic behind the integration between the cache and the external,
backend data source. The listener is a specialized https://en.wikipedia.org/wiki/Adapter_pattern[Adpater] that processes
`AsyncEvents` by invoking an appropriate `CrudRepository` method based on the cache entry event operation. The listener
requires an instance of a `CrudRepository`. Therefore the listener supports any external, backend data source supported
by Spring Data's _Repository_ abstraction.
The SBDG `RepositoryAsyncEventListener` class is the magic sauce behind the integration of the cache with an external,
backend data source.
The listener is a specialized https://en.wikipedia.org/wiki/Adapter_pattern[Adpater] that processes `AsyncEvents` by
invoking an appropriate `CrudRepository` method based on the cache operation. The listener requires an instance of
`CrudRepository`. As such, the listener supports any external, backend data source supported by Spring Data's
_Repository_ abstraction.
Of course, backend data store, data access operations (e.g. INSERT, UPDATE, DELETE, etc) triggered by cache events
are performed asynchronously from the cache operation. This means the state of the cache and backend data store
will be "_eventually consistent_".
Given the complex nature of "_eventual consistent_" systems and asynchronous, concurrent processing, the
`RepositoryAsyncEventListener` allows users to register a custom `AsyncEventErrorHandler` in the case that `AsyncEvent`
processing results in an error, perhaps due to a faulty backend data store data access operation
(e.g. `PessimisticLockingFailureException`).
ERROR HANDLING
Given the complex nature of "_eventually consistent_" systems and asynchronous concurrent processing, the
`RepositoryAsyncEventListener` allows users to register a custom `AsyncEventErrorHandler` to handle the errors
that occur during processing of `AsyncEvents`, perhaps due to a faulty backend data store data access operation
(e.g. `OptimisticLockingFailureException`), in an application relevant way.
The `AsyncEventErrorHandler` interface is a `java.util.function.Function` implementation and `@FunctionalInterface`
defined as:
@@ -683,11 +687,12 @@ defined as:
interface AsyncEventErrorHandler implements Function<AsyncEventError, Boolean> { }
----
Since the `AsyncEventErrorHandler` interface implements `Function`, then you would override the `apply(:AsyncEventError)`
method to handle the error with application-specific actions. The handler returns a `boolean` to indicate whether it was
able to handle the error or not.
The `AsyncEventError` class encapsulates `AsyncEvent` along with the `Throwable` that was thrown while processing
the event.
The `AsyncEventError` class encapsulates `AsyncEvent` along with the `Throwable` error thrown while processing the event.
Since the `AsyncEventErrorHandler` interface implements `Function`, then you would override the `apply(:AsyncEventError)`
method to handle the error with application-specific actions. The handler returns a `Boolean` to indicate whether it was
able to handle the error or not.
.Custom `AsyncEventErrorHandler` implementation
[source,java]
@@ -697,12 +702,12 @@ class CustomAsyncEventErrorHandler implements AsyncEventErrorHandler {
@Override
public Boolean apply(AsyncEventError error) {
if (error.getCause() instanceof PessimisticLockingFailureException) {
// handle pessimistic locking failure
if (error.getCause() instanceof OptimisticLockingFailureException) {
// handle optimistic locking failure if you can
return true; // if error was successfully handled.
}
else if (error.getCause() instanceof IncorrectResultSizeDataAccessException) {
// handle no row or too many row update
// handle no row or too many row update if you can
return true; // if error was successfully handled.
}
@@ -728,10 +733,10 @@ class GeodeConfiguration {
@Bean
AsyncInlineCachingRegionConfigurer asyncInlineCachingRegionConfigurer(
CrudRepository<?, ?> repository,
CustomerAsyncEventErrorHandler errorHandler
CustomAsyncEventErrorHandler errorHandler
) {
return AsyncInlineCachingRegionConfigurer.create(repository, "ExampleRegion")
return AsyncInlineCachingRegionConfigurer.create(repository, "Example")
.withAsyncEventErrorHandler(errorHandler);
}
}
@@ -740,6 +745,8 @@ class GeodeConfiguration {
Also, since `AsyncEventErrorHandler` implements `Function`, you can https://en.wikipedia.org/wiki/Composite_pattern["_compose_"]
multiple error handlers using {jdk-javadoc}/java/util/function/Function.html#andThen-java.util.function.Function-[`Function.andThen(:Function)`].
SUPPORTED CACHE OPERATIONS
By default, the `RepositoryAsyncEventListener` handles `CREATE`, `UPDATE` and `REMOVE` cache event, entry operations.
`CREATE` and `UPDATE` translates to `CrudRepository.save(entity)` where the `entity` is derived from
@@ -753,11 +760,11 @@ supported by the `AsyncEventOperationRepositoryFunction` interface, which implem
and is a `@FunctionalInterface`.
This interface becomes useful if and when you want to implement `CrudRepository` method invocations for other
`AsyncEvent` `Operations` not handled by SBDG's `RepositoryAsyncEventListener`, out-of-the-box.
`AsyncEvent` `Operations` not handled by SBDG's `RepositoryAsyncEventListener` out-of-the-box.
The The `AsyncEventOperationRepositoryFunction` interface is defined as:
The `AsyncEventOperationRepositoryFunction` interface is defined as:
.AsyncEventOperationRepositoryFunction interface defintion
.AsyncEventOperationRepositoryFunction interface definition
[source,java]
----
@FunctionalInterface
@@ -769,18 +776,18 @@ interface AsyncEventOperationRepositoryFunction<T, ID> implements Function<Async
}
----
`T` is the class type of the entity and `ID` is the class type of the entity's identifier (ID), possibly defined by
`T` is the class type of the entity and `ID` is the class type of the entity's identifier (ID), possibly declared with
Spring Data's {spring-data-commons-javadoc}/org/springframework/data/annotation/Id.html[`org.springframework.data.annotation.Id`] annotation.
For convenience, SBDG provides the `AbstractAsyncEventOperationRepositoryFunction` class for extension, where you would
provide implementations for the `cacheProcess(:AsyncEvent)` and `doRepositoryOp(entity)` methods.
NOTE: the `AsyncEventOperationRepositoryFunction.apply(:AsyncEvent)` method is already implemented in terms of
NOTE: The `AsyncEventOperationRepositoryFunction.apply(:AsyncEvent)` method is already implemented in terms of
`canProcess(:AsyncEvent)`, `resolveEntity(:AsyncEvent)`, `doRepositoryOp(entity)`, and catching and handling any
`Throwable` (errors) by calling the configured `AsyncEventErrorHandler`.
For example, you might want to handle {apache-geode-javadoc}/org/apache/geode/cache/Operation.html#INVALIDATE[`Operation.INVALIDATE`]
cache events as well, by deleting the entity from the backend data store by invoking the `CrudRepository.delete(entity)`
cache events as well, deleting the entity from the backend data store by invoking the `CrudRepository.delete(entity)`
method:
.Handling `AsyncEvent`, `Operation.INVALIDATE`
@@ -802,7 +809,7 @@ class InvalidateAsyncEventRepositoryFunction
@Override
protected Object doRepositoryOperation(Object entity) {
getRepository.delete(entity);
getRepository().delete(entity);
return null;
}
}
@@ -841,12 +848,8 @@ class GeodeConfiguration {
}
----
This same technique can be applied to the `Operation.CREATE`, `Operation.UPDATE` and `Operation.REMOVE` cache entry
event operations, handled by SBDG out-of-the-box, by default, as well.
By registering your own, user-defined `AsyncEventOperationRepositoryFunctions` for the `CREATE`, `UPDATE` and `REMOVE`
cache `Operations`, you effectively override the default behavior. This is convenient if you want to override
the default behavior.
This same technique can be applied to `CREATE`, `UPDATE` and `REMOVE` cache operations as well, effectively overriding
the default behavior for this cache operations handled by SBDG out-of-the-box.
[[geode-caching-provider-inline-caching-asynchronous-region-configurer]]
====== About `AsyncInlineCachingRegionConfigurer`
@@ -854,7 +857,7 @@ the default behavior.
As we saw in the previous section, it is possible to intercept and post-process key components constructed
and configured by the `AsyncInlineCachingRegionConfigurer` class during initialization.
Out-of-the-box, SBDG's allows you to intercept and post-process the `AsyncEventListener` (i.e. `RepositoryAsyncEventListener`),
Out-of-the-box, SBDG's allows you to intercept and post-process the `AsyncEventListener` (e.g. `RepositoryAsyncEventListener`),
`AsyncEventQueueFactory` and even the `AsyncEventQueue`, created by the `AsyncInlineCachingRegionConfigurer`
(a SDG {spring-data-geode-javadoc}/org/springframework/data/gemfire/config/annotation/RegionConfigurer.html[`RegionConfigurer`])
during Spring `ApplicationContext`, bean initialization.
@@ -866,8 +869,8 @@ any of the following {geode-name} objects:
* `applyToQueue(:Function<AsyncEventQueue, AsyncEventQueue>)`
* `applyToQueueFactory(:Function<AsyncEventQueueFactory, AsyncEventQueueFactory>)`
All of these "_apply_" methods accept a `java.util.function.Function` that accepts and "_applies_" the logic of
the `Function` to the {geode-name} object (e.g. `AsyncEventListener`), returning the object as a result.
All of these "_apply_" methods accept a `java.util.function.Function` that "_applies_" the logic of the `Function` to
the {geode-name} object (e.g. `AsyncEventListener`), returning the object as a result.
TIP: The {geode-name} object returned by the `Function` may be the same object, a proxy, or a completely new object.
Essentially, the returned object can be anything you want. This is the fundamental premise behind
@@ -876,8 +879,8 @@ _Aspect-Oriented Programming_ (AOP) and the https://en.wikipedia.org/wiki/Decora
These "_apply_" methods and the supplied `Function` allow you to decorate, enhance, post-process, whatever you want to,
to the {geode-name} objects created by the listener.
Of course, the `AsyncInlineCachingRegionConfigurer` strictly, adheres to the https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle[Open/Close Principle]
as well, and is flexibly extensible.
Of course, the `AsyncInlineCachingRegionConfigurer` strictly adheres to the https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle[Open/Close Principle]
as well, and is therefore flexibly extensible.
[[geode-caching-provider-advanced-configuration]]
=== Advanced Caching Configuration