From 24ac11715ac16bc1c7776f6ef47c8b961f4840c5 Mon Sep 17 00:00:00 2001 From: John Blum Date: Thu, 20 Aug 2020 12:01:10 -0700 Subject: [PATCH] Edit section on Asynchronous (Write-Behind) Inline Cacching in caching chapter. Resolves gh-58. --- .../src/docs/asciidoc/_includes/caching.adoc | 163 +++++++++--------- 1 file changed, 83 insertions(+), 80 deletions(-) diff --git a/spring-geode-docs/src/docs/asciidoc/_includes/caching.adoc b/spring-geode-docs/src/docs/asciidoc/_includes/caching.adoc index 6be40e5a..6de9e75b 100644 --- a/spring-geode-docs/src/docs/asciidoc/_includes/caching.adoc +++ b/spring-geode-docs/src/docs/asciidoc/_includes/caching.adoc @@ -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 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 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 { } ---- -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 implements Function)` * `applyToQueueFactory(:Function)` -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