Edit section on Asynchronous (Write-Behind) Inline Cacching in caching chapter.
Resolves gh-58.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user