generated from amazon-archives/__template_Apache-2.0
-
Notifications
You must be signed in to change notification settings - Fork 73
document how the custom annotations work #1564
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
davecramer
wants to merge
2
commits into
aws:main
Choose a base branch
from
davecramer:update_docs
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+451
−0
Draft
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
264 changes: 264 additions & 0 deletions
264
examples/SpringHibernateBalancedReaderOneDataSourceExample/CUSTOM_ANNOTATIONS.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| # Custom Annotations Documentation | ||
|
|
||
| ## Overview | ||
|
|
||
| The SpringHibernateBalancedReaderOneDataSourceExample demonstrates a **single datasource approach** for load-balanced read-write and read-only connections using the AWS Advanced JDBC Driver's built-in Read/Write Splitting Plugin. Unlike the TwoDataSourceExample, this approach does **not** use custom annotations for datasource routing. | ||
|
|
||
| ## Key Difference from TwoDataSourceExample | ||
|
|
||
| | Aspect | OneDataSourceExample | TwoDataSourceExample | | ||
| |--------|---------------------|----------------------| | ||
| | **Datasources** | Single datasource | Two separate datasources (writer + reader) | | ||
| | **Routing Mechanism** | AWS JDBC Driver's Read/Write Splitting Plugin | Custom `@WithLoadBalancedReaderDataSource` annotation | | ||
| | **Configuration** | Connection string with `wrapperProfileName=F0` | Spring's `AbstractRoutingDataSource` + AOP | | ||
| | **Custom Annotations** | None (uses standard Spring annotations) | Custom annotation with AspectJ interception | | ||
| | **Complexity** | Simpler, driver-managed | More complex, application-managed | | ||
|
|
||
| ## Standard Annotations Used | ||
|
|
||
| ### @Transactional(readOnly = true) | ||
|
|
||
| The key to routing in this example is Spring's standard `@Transactional` annotation with the `readOnly` parameter: | ||
|
|
||
| ```java | ||
| @Retryable(value = { ShouldRetryTransactionException.class }, maxAttempts = 3) | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) | ||
| public int getNumOfBooksTransactional() { | ||
| // AWS JDBC Driver automatically routes this to a reader instance | ||
| return this.repository.findAll().stream().mapToInt(Book::getQuantityAvailable).sum(); | ||
| } | ||
| ``` | ||
|
|
||
| **How it works:** | ||
| - The AWS JDBC Driver's Read/Write Splitting Plugin detects the `readOnly` flag on the transaction | ||
| - When `readOnly = true`, the driver automatically routes the connection to a reader instance | ||
| - When `readOnly = false` (or not specified), the driver routes to the writer instance | ||
|
|
||
| ### @Transactional (Write Operations) | ||
|
|
||
| For write operations, use `@Transactional` without the `readOnly` flag: | ||
|
|
||
| ```java | ||
| @Retryable(value = { ShouldRetryTransactionException.class, TransactionSystemException.class}, maxAttempts = 3) | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void updateBookAvailabilityTransactional() { | ||
| // AWS JDBC Driver automatically routes this to the writer instance | ||
| final List<Book> allBooks = this.repository.findAll(); | ||
| this.repository.saveAll(allBooks); | ||
| } | ||
| ``` | ||
|
|
||
| ### @Retryable | ||
|
|
||
| Used to handle transient failures during failover: | ||
|
|
||
| ```java | ||
| @Retryable(value = { ShouldRetryTransactionException.class }, maxAttempts = 3) | ||
| ``` | ||
|
|
||
| **Purpose:** | ||
| - Automatically retries failed transactions up to 3 times | ||
| - Handles `ShouldRetryTransactionException` thrown during failover events | ||
| - Works with the custom `HibernateExceptionTranslator` to convert JDBC connection exceptions | ||
|
|
||
| ### @EnableRetry | ||
|
|
||
| Enables Spring's retry mechanism at the application level: | ||
|
|
||
| ```java | ||
| @SpringBootApplication | ||
| @EnableRetry | ||
| public class SpringHibernateBalancedReaderOneDataSourceExampleApplication { | ||
| // ... | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### Connection String Configuration | ||
|
|
||
| The datasource is configured with the AWS JDBC Driver's Read/Write Splitting Plugin: | ||
|
|
||
| ```yaml | ||
| spring: | ||
| datasource: | ||
| load-balanced-writer-and-reader-datasource: | ||
| url: jdbc:aws-wrapper:postgresql://cluster.XYZ.us-east-2.rds.amazonaws.com:5432/postgres?wrapperProfileName=F0&readerHostSelectorStrategy=roundRobin | ||
| driver-class-name: software.amazon.jdbc.Driver | ||
| ``` | ||
|
|
||
| **Key Parameters:** | ||
| - `wrapperProfileName=F0`: Enables the Read/Write Splitting Plugin with internal connection pooling | ||
| - `readerHostSelectorStrategy=roundRobin`: Distributes read traffic evenly across reader instances | ||
|
|
||
| ### Custom Exception Translation | ||
|
|
||
| The `Config` class includes a custom `HibernateExceptionTranslator` to handle failover exceptions: | ||
|
|
||
| ```java | ||
| @Bean | ||
| public HibernateExceptionTranslator hibernateExceptionTranslator(){ | ||
| return new HibernateExceptionTranslator() { | ||
| @Override | ||
| public DataAccessException translateExceptionIfPossible(RuntimeException ex) { | ||
| // Convert JDBC connection exceptions to retryable exceptions | ||
| if (ex.getCause() != null && ex.getCause() instanceof JDBCConnectionException) { | ||
| return new ShouldRetryTransactionException(ex); | ||
| } | ||
| return super.translateExceptionIfPossible(ex); | ||
| } | ||
| }; | ||
| } | ||
| ``` | ||
|
|
||
| **Purpose:** | ||
| - Converts `JDBCConnectionException` (including failover exceptions) to `ShouldRetryTransactionException` | ||
| - Enables `@Retryable` to automatically retry failed transactions | ||
| - Provides seamless failover handling | ||
|
|
||
| ## Complete Flow | ||
|
|
||
| ### Read Operation Flow | ||
|
|
||
| 1. **Method Call**: Application calls a method with `@Transactional(readOnly = true)` | ||
| 2. **Transaction Start**: Spring starts a read-only transaction | ||
| 3. **Connection Request**: Hibernate requests a connection from the datasource | ||
| 4. **Driver Detection**: AWS JDBC Driver detects the read-only transaction flag | ||
| 5. **Reader Routing**: Driver routes the connection to a reader instance (using round-robin strategy) | ||
| 6. **Query Execution**: Read queries execute on the reader instance | ||
| 7. **Transaction Commit**: Spring commits the read-only transaction | ||
| 8. **Connection Return**: Connection returns to the pool | ||
|
|
||
| ### Write Operation Flow | ||
|
|
||
| 1. **Method Call**: Application calls a method with `@Transactional` (no readOnly flag) | ||
| 2. **Transaction Start**: Spring starts a read-write transaction | ||
| 3. **Connection Request**: Hibernate requests a connection from the datasource | ||
| 4. **Driver Detection**: AWS JDBC Driver detects the read-write transaction | ||
| 5. **Writer Routing**: Driver routes the connection to the writer instance | ||
| 6. **Query Execution**: Read and write queries execute on the writer instance | ||
| 7. **Transaction Commit**: Spring commits the transaction | ||
| 8. **Connection Return**: Connection returns to the pool | ||
|
|
||
| ### Failover Handling Flow | ||
|
|
||
| 1. **Failover Event**: Database failover occurs during a transaction | ||
| 2. **Exception Thrown**: Driver throws `FailoverSQLException` or `JDBCConnectionException` | ||
| 3. **Exception Translation**: `HibernateExceptionTranslator` converts to `ShouldRetryTransactionException` | ||
| 4. **Retry Triggered**: `@Retryable` catches the exception and retries the transaction | ||
| 5. **New Connection**: Driver establishes a connection to the new writer/reader | ||
| 6. **Transaction Retry**: Transaction executes again on the new instance | ||
|
|
||
| ## Benefits of Single Datasource Approach | ||
|
|
||
| 1. **Simplicity**: No custom annotations or AOP configuration needed | ||
| 2. **Driver-Managed**: AWS JDBC Driver handles all routing logic | ||
| 3. **Automatic Detection**: Uses standard Spring `readOnly` flag | ||
| 4. **Internal Pooling**: Driver manages connection pools for both writer and readers | ||
| 5. **Failover Support**: Built-in failover handling with retry logic | ||
| 6. **Less Code**: Fewer components to maintain | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Always Use readOnly Flag**: Explicitly set `readOnly = true` for read operations | ||
| ```java | ||
| @Transactional(readOnly = true) | ||
| ``` | ||
|
|
||
| 2. **Use Configuration Profiles**: Leverage predefined profiles (F0, D, E, F) for optimal settings | ||
| ``` | ||
| wrapperProfileName=F0 | ||
| ``` | ||
|
|
||
| 3. **Configure Reader Selection**: Choose appropriate reader selection strategy | ||
| ``` | ||
| readerHostSelectorStrategy=roundRobin // or random, leastConnections | ||
| ``` | ||
|
|
||
| 4. **Enable Retry Logic**: Always use `@Retryable` for failover resilience | ||
| ```java | ||
| @Retryable(value = { ShouldRetryTransactionException.class }, maxAttempts = 3) | ||
| ``` | ||
|
|
||
| 5. **Custom Exception Translation**: Implement `HibernateExceptionTranslator` to convert failover exceptions | ||
|
|
||
| ## Example: Complete Method Annotations | ||
|
|
||
| ### Read-Only Method | ||
| ```java | ||
| @Retryable(value = { ShouldRetryTransactionException.class }, maxAttempts = 3) | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) | ||
| public int getNumOfBooksTransactional() { | ||
| // Automatically routed to reader instance | ||
| return this.repository.findAll().stream().mapToInt(Book::getQuantityAvailable).sum(); | ||
| } | ||
| ``` | ||
|
|
||
| ### Read-Write Method | ||
| ```java | ||
| @Retryable(value = { ShouldRetryTransactionException.class, TransactionSystemException.class}, maxAttempts = 3) | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void updateBookAvailabilityTransactional() { | ||
| // Automatically routed to writer instance | ||
| final List<Book> allBooks = this.repository.findAll(); | ||
| for (Book book : allBooks) { | ||
| book.setQuantityAvailable(book.getQuantityAvailable() + 1); | ||
| } | ||
| this.repository.saveAll(allBooks); | ||
| } | ||
| ``` | ||
|
|
||
| ## Comparison: When to Use Each Approach | ||
|
|
||
| ### Performance Considerations | ||
|
|
||
| **OneDataSource Performance Trade-offs:** | ||
| - Uses a **single connection pool** where the driver switches the underlying physical connection between writer and reader instances | ||
| - **Connection switching overhead** occurs when transitioning between read and write operations | ||
| - **Pool contention** - writer and reader operations compete for connections from the same pool | ||
| - Less optimal for high-throughput applications with frequent read/write interleaving | ||
|
|
||
| **TwoDataSource Performance Advantages:** | ||
| - **Dedicated connection pools** for writer and readers eliminate switching overhead | ||
| - **No connection switching** - connections stay bound to their instance type | ||
| - **Better resource isolation** - read-heavy workloads don't impact write connection availability | ||
| - **Independent pool tuning** - optimize writer pool (smaller, longer-lived) and reader pool (larger, more aggressive) separately | ||
|
|
||
| ### Use OneDataSourceExample (This Example) When: | ||
| - You want simplicity and less code | ||
| - You're comfortable with driver-managed routing | ||
| - You have low to moderate traffic | ||
| - Read/write operations are infrequent or not heavily interleaved | ||
| - You prefer configuration to code | ||
|
|
||
| ### Use TwoDataSourceExample When: | ||
| - You need fine-grained control over datasource selection | ||
| - You have high-throughput, performance-critical workloads | ||
| - Your application frequently interleaves read and write operations | ||
| - You want to use external connection pools (e.g., HikariCP) for each datasource | ||
| - You need to optimize pool configurations separately for writer and readers | ||
| - You have complex routing requirements beyond read/write splitting | ||
| - You need to route based on custom business logic | ||
| - You're integrating with existing multi-datasource infrastructure | ||
|
|
||
| **Recommendation:** Start with OneDataSource for simplicity. If performance profiling reveals connection switching overhead or pool contention as bottlenecks, migrate to TwoDataSource for better performance at the cost of increased complexity. | ||
|
|
||
| ## Configuration Profiles | ||
|
|
||
| The example uses `wrapperProfileName=F0`, which includes: | ||
| - Read/Write Splitting Plugin | ||
| - Failover Plugin | ||
| - Host Monitoring Plugin | ||
| - Internal Connection Pooling | ||
|
|
||
| Other available profiles: | ||
| - **D**: Basic read/write splitting without internal pooling | ||
| - **E**: Read/write splitting with enhanced monitoring | ||
| - **F**: Read/write splitting with failover support (no internal pooling) | ||
| - **F0**: Read/write splitting with failover and internal pooling (recommended) | ||
|
|
||
| See [Configuration Profiles](../../docs/using-the-jdbc-driver/UsingTheJdbcDriver.md#configuration-profiles) for details. | ||
|
|
||
| ## Summary | ||
|
|
||
| The SpringHibernateBalancedReaderOneDataSourceExample demonstrates a **driver-managed approach** to read/write splitting using standard Spring annotations. The AWS JDBC Driver automatically routes connections based on the `@Transactional(readOnly)` flag, eliminating the need for custom annotations or AOP configuration. This approach is simpler, requires less code, and is ideal for most use cases. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably need to replace
ShouldRetryTransactionExceptionwithFailoverSQLException