Add batch DELETE/UPDATE samples for datasets exceeding 3k row limit#698
Add batch DELETE/UPDATE samples for datasets exceeding 3k row limit#698rmconstantin wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Can you add the pycache path to gitignore?
| while (true) { | ||
| try (Connection conn = pool.getConnection()) { | ||
| conn.setAutoCommit(false); | ||
| String sql = "UPDATE " + table + " SET " + setClause + ", updated_at = NOW()" |
There was a problem hiding this comment.
How does this ensure progress over all of the source items?
| * gradle run --args="--endpoint <cluster-endpoint> [--user admin] | ||
| * [--batch-size 1000] [--num-workers 4]" | ||
| */ | ||
| public class Main { |
There was a problem hiding this comment.
Could you add an integ test that runs these batch ops?
| ); | ||
|
|
||
| -- Create an asynchronous index on the category column. | ||
| -- Aurora DSQL requires CREATE INDEX ASYNC for tables with existing rows. |
There was a problem hiding this comment.
For all tables, maybe delete this comment
| @@ -0,0 +1,52 @@ | |||
| # Aurora DSQL Batch Operations | |||
There was a problem hiding this comment.
I think we might be better organizing these examples under the specific language/driver pairing instead of having it as a top level dir.
Can we also add integ tests for each example? There should be patterns for how to do that in each language
| * @param connection a JDBC connection (autoCommit should be false) | ||
| * @param operation the database operation to execute | ||
| * @param maxRetries maximum retry attempts (default 3) | ||
| * @param baseDelay base delay in seconds for backoff (default 0.1) |
There was a problem hiding this comment.
Nit: can we make baseDelay milliseconds instead?
| */ | ||
| public class Repopulate { | ||
|
|
||
| private static final String INSERT_SQL = |
There was a problem hiding this comment.
What's going on with the repopulate fn vs the batch setup script?
8bc247c to
a3525c4
Compare
|
Updated the code to address all comments.
Ready for another look. |
63b7d16 to
94efc68
Compare
There was a problem hiding this comment.
What's this jar file for? Should we be shipping it?
There was a problem hiding this comment.
The gradle-wrapper.jar is the Gradle Wrapper bootstrap JAR. It allows anyone to build the project without having Gradle pre-installed — the wrapper downloads the correct Gradle version automatically. Shipping it in version control is the recommended Gradle convention. The other Java projects in this repo (java/pgjdbc, java/spring_boot) follow the same pattern.
I also updated java/.gitignore to stop ignoring these wrapper files (previously they were gitignored but force-tracked, which was inconsistent).
There was a problem hiding this comment.
Should this and gradelw be checked in or gitignored?
There was a problem hiding this comment.
Yes, gradlew and gradlew.bat should be checked in. They are the Gradle Wrapper scripts (Unix and Windows respectively) that bootstrap the build — users run ./gradlew build instead of installing Gradle globally. This is the standard Gradle convention and matches java/pgjdbc and java/spring_boot in this repo.
I cleaned up java/.gitignore in the latest commit to remove the **/gradlew, **/gradlew.bat, and **/gradle/ patterns that were incorrectly ignoring these files.
Demonstrates sequential and parallel batch processing patterns for Aurora DSQL with OCC retry logic and hashtext() partitioning. Includes Python (psycopg2), Java (pgJDBC), and Node.js (node-postgres) implementations.
- Add SELECT COUNT(*) post-check after each batch loop to verify all matching rows were processed (sequential and parallel, all 3 languages) - Update integration tests to seed data via psql -f batch_test_setup.sql - Add connect_timeout to Python pool creation for IPv6 fallback
The gradle wrapper (gradle-wrapper.jar, gradlew, gradlew.bat) should be committed to version control per Gradle convention. This allows anyone to build the project without pre-installing Gradle. Consistent with existing java/pgjdbc and java/spring_boot projects in this repo. Removed **/gradle/, **/gradlew, and **/gradlew.bat from .gitignore. The .gradle/ (build cache) pattern remains correctly ignored.
9d687d7 to
d67e3ff
Compare
|
Rebased on main — conflicts resolved. CI has two failures:
Ready for another look when you get a chance. |
|
|
||
| ```bash | ||
| pip install -r requirements.txt | ||
| ``` |
There was a problem hiding this comment.
Missing requirements.txt.
This README instructs pip install -r requirements.txt, but the PR doesn't add one. The sample imports aurora_dsql_psycopg2 and psycopg2, but we cannot install from that dir.
Can you addrequirements.txt or switch topyproject.toml?
| disjoint subset, avoiding OCC conflicts between workers. | ||
| """ | ||
|
|
||
| import threading |
There was a problem hiding this comment.
Unused threading isn't referenced in this module (however theparallel* modules use it).
| total_deleted = 0 | ||
| consecutive_failures = 0 | ||
| partition_condition = ( | ||
| f"{condition} AND abs(hashtext(id::text)) % {num_workers} = {worker_id}" |
There was a problem hiding this comment.
condition gets inserted without parentheses before appending the partition filter:
... {condition} AND abs(hashtext(...)) % N = i
That becomes a problem if someone passes a condition with an OR, like:
category = 'food' OR status = 'expired'
Because AND has higher precedence than OR, it can cause multiple workers to process the same rows again, defeating the purpose of partitioning and bringing back OCC conflicts.
The current demo is fine since it only uses simple equality conditions, but this is easy for users to run into when adapting the sample. We can do this:
partition_condition = (
f"({condition}) AND abs(hashtext(id::text)) % {num_workers} = {worker_id}"
)
Which is better, the same fix should also be applied in:
parallel_batch_update.py
BatchDelete.java
BatchUpdate.java
batchDelete.js
batchUpdate.js
| if attempt >= max_retries: | ||
| raise MaxRetriesExceeded(max_retries) | ||
| delay_ms = base_delay_ms * (2 ** attempt) | ||
| logger.warning( |
There was a problem hiding this comment.
No jitter in backoff, delay_ms = base_delay_ms * (2 ** attempt) is just exp backoff, what about:
import random
delay_ms = base_delay_ms * (2 ** attempt) * (0.5 + random.random())
the same applies to OccRetry.java and occRetry.js
| -- ============================================================================= | ||
|
|
||
| INSERT INTO batch_test (category, status, value) | ||
| SELECT |
There was a problem hiding this comment.
Nit: this 1,000-row INSERT block is repeated 25x and the whole file is duplicated under java/batch_operations/ and javascript/batch_operations/ (3 times copied). Options?
- Replace the 25 copy-pasted INSERTs with a psql
\setloop, or call from the host language driver (committing each batch as a separate transaction to respect the 3k mut limit). - Keep one copy and reference it from each language's README?
| * | ||
| * @param {import('pg').PoolClient} client - A node-postgres pool client. | ||
| * @param {(client: import('pg').PoolClient) => Promise<*>} operation - Async | ||
| * function that performs database work. Should NOT commit. |
There was a problem hiding this comment.
JSDoc says the operation "Should NOT commit," but the operations passed in batchDelete.js and batchUpdate.js call BEGIN inside the operation (and the caller does COMMIT after executeWithRetry returns). It works — each retry begins a fresh tx after the rollback — but the responsibility split is inconsistent. Either move BEGIN into executeWithRetry, or update the doc to say "operation must call BEGIN; caller commits after success."
| public class OccRetry { | ||
|
|
||
| private static final Logger logger = Logger.getLogger(OccRetry.class.getName()); | ||
| private static final String SERIALIZATION_FAILURE = "40001"; |
There was a problem hiding this comment.
This class logs via java.util.logging.Logger, but BatchDelete, BatchUpdate, and Main use System.out.println everywhere. Pick one?
|
|
||
| @Test | ||
| public void testBatchOperations() { | ||
| assertAll(() -> Main.main(new String[]{ |
There was a problem hiding this comment.
Main.main calls System.exit(1) on any SQLException / OccRetry.MaxRetriesExceededException.
If the integration test hits a db error, JVM exits before JUnit can report it as a failure (exit code 1 with no test output). Take the main body into a helper that throws, and have both the CLI entry and the test call it?
Demonstrates sequential and parallel batch processing patterns for Aurora DSQL with OCC retry logic and recommended connection management. Includes Python (psycopg2), Java (pgJDBC), and Node.js (node-postgres) implementations.
Fixes #693 .
By submitting this pull request, I confirm that my contribution is made under
the terms of the MIT-0 license.
Thank you for your contribution!