Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions deno/booking-api/postgres-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,16 @@ This sample uses a layered defense against double-booking:
Blog post [Concurrency control in Amazon Aurora DSQL](https://aws.amazon.com/blogs/database/concurrency-control-in-amazon-aurora-dsql/)
for details.

**Write-skew caveat.** Aurora DSQL provides strong snapshot isolation and
OCC only conflicts writes to the same physical rows. Two concurrent
transactions inserting *overlapping but distinct* windows (e.g.,
`[9:00–10:00]` and `[9:30–10:30]`) may both pass the SELECT above and
both commit — the unique index catches only identical windows. This is
the classic *write skew* anomaly. For strict serialization of overlapping
writes, maintain a parent `resources` table and acquire
`SELECT ... FOR UPDATE` on the resource row (keyed by its primary key)
before the overlap check. See the AWS Database Blog post
**Write-skew caveat.** Aurora DSQL provides strong snapshot isolation. In this
overlap-check pattern, two concurrent transactions can insert *overlapping but
distinct* windows without conflicting when `SELECT ... FOR UPDATE` is not used.
For example, transactions for `[9:00–10:00]` and `[9:30–10:30]` may both pass
the SELECT above and both commit — the unique index catches only identical
windows. This is the classic *write skew* anomaly. For strict serialization of
overlapping writes, maintain a parent `resources` table and acquire
`SELECT ... FOR UPDATE` on the resource row (keyed by its primary key) before
the overlap check. Each targeted row's primary key counts toward the 10 MiB
transaction-size limit. See the AWS Database Blog post
[Concurrency control in Amazon Aurora DSQL](https://aws.amazon.com/blogs/database/concurrency-control-in-amazon-aurora-dsql/)
(Example 2: `SELECT FOR UPDATE` to manage write skew) and the user-guide
page [Concurrency control in Aurora DSQL](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-concurrency-control.html)
Expand Down
3 changes: 2 additions & 1 deletion deno/booking-api/postgres-js/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,8 @@ async function createBooking(
// Write-skew caveat: two concurrent transactions inserting
// overlapping-but-distinct windows (e.g., 9:00–10:00 and
// 9:30–10:30) may both pass this SELECT and both commit —
// DSQL's OCC only conflicts writes to the same physical rows,
// In this overlap-check pattern, writes to distinct booking rows
// do not conflict merely because their time ranges overlap,
// and the unique index on (resource_name, start_time, end_time)
// catches only identical windows. For strict serialization,
// maintain a `resources` table and `SELECT ... FOR UPDATE` on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Deno.test({
//
// This test codifies the behavior. For strict serialization of overlapping
// writes, use either (a) a coarser grouping key enforced via unique index,
// or (b) an application-level row lock via `SELECT ... FOR UPDATE` on a
// or (b) a commit-time OCC conflict point via `SELECT ... FOR UPDATE` on a
// parent row. See README "Concurrency model — what's serialized and what
// isn't" for the recommended production patterns.
// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion go/pgx/src/transaction/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// - Using occretry for OCC conflict handling
//
// DSQL transaction limits:
// - Maximum 3,000 rows modified per transaction
// - Maximum 3,000 row modifications per transaction
// - Maximum 10 MiB data size per transaction
// - Maximum 5 minute transaction duration
//
Expand Down
2 changes: 1 addition & 1 deletion sample-amazon-aurora-dsql-auth-session-mgmt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ SELECT id, user_id, created_at, expires_at, revoked_at FROM sessions;
- IAM-based database authentication (no static passwords)
- UUIDs generated app-side
- 1 DDL per transaction
- 3,000 row limit per DML transaction
- 3,000 row-modification limit per DML transaction

## Operational Notes

Expand Down
23 changes: 17 additions & 6 deletions typescript/sequelize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,29 @@ For the full list of Aurora DSQL SQL compatibility details, see the [PostgreSQL

### Locking

Aurora DSQL uses optimistic concurrency control (OCC), meaning transactions proceed without locks and conflicts are detected at commit time. The `SELECT FOR UPDATE` clause modifies this behavior by flagging read rows for concurrency checks, which is useful for managing write skew scenarios.
Aurora DSQL uses optimistic concurrency control (OCC). `SELECT ... FOR UPDATE` does not take a blocking row lock; rows targeted by the locking clause participate in commit-time conflict checks. A conflicting transaction fails at commit and the whole transaction must be retried. Each targeted row's primary key counts toward the 10 MiB transaction-size limit.

In Sequelize, only `Transaction.LOCK.UPDATE` is supported. The query must include an equality predicate on the primary key. Queries that lock by non-key columns will fail.
Sequelize `Transaction.LOCK.UPDATE` and `Transaction.LOCK.KEY_SHARE` are supported. `Transaction.LOCK.NO_KEY_UPDATE` and `Transaction.LOCK.SHARE` are not supported. Locking queries may use non-key predicates and may join multiple tables; there is no requirement to use equality predicates on every primary-key column or to query only one table.

```ts
// Works: lock by primary key
await Model.findByPk(id, { lock: Transaction.LOCK.UPDATE, transaction });
// Non-key predicate
await Order.findOne({
where: { tenantId, status: 'pending' },
lock: Transaction.LOCK.UPDATE,
transaction,
});

// Does not work: lock by non-key column
await Model.findOne({ where: { status: 'pending' }, lock: Transaction.LOCK.UPDATE, transaction });
// Joined query
await Order.findOne({
include: [{ model: Customer, required: true }],
where: { tenantId, status: 'pending' },
lock: Transaction.LOCK.UPDATE,
transaction,
});
```

Retry the complete transaction with backoff when a commit conflict returns SQLSTATE `40001`. Keep external side effects outside the retried callback or make them idempotent.

For more details on concurrency control in Aurora DSQL, see [Concurrency control in Amazon Aurora DSQL](https://aws.amazon.com/blogs/database/concurrency-control-in-amazon-aurora-dsql/).

## Additional resources
Expand Down
62 changes: 42 additions & 20 deletions typescript/sequelize/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AuroraDSQLClient } from "@aws/aurora-dsql-node-postgres-connector";
import * as pg from 'pg';
import { Sequelize, DataTypes, Model } from 'sequelize';
import { Sequelize, DataTypes, Model, Transaction } from 'sequelize';

const ADMIN = "admin";
const NON_ADMIN_SCHEMA = "myschema";
Expand Down Expand Up @@ -239,31 +239,53 @@ async function sequelizeExample() {
await sequelize.close();
}

async function executeSqlStatementWithRetry(instance: Sequelize, sqlStatement: string, maxRetries: number = 0): Promise<any> {
let retries = 0;
while (retries <= maxRetries) {
type SequelizeError = Error & {
code?: string;
original?: { code?: string };
parent?: { code?: string };
};

function getSqlState(error: unknown): string | undefined {
if (typeof error !== 'object' || error === null) {
return undefined;
}

const sequelizeError = error as SequelizeError;
return sequelizeError.original?.code ?? sequelizeError.parent?.code ?? sequelizeError.code;
}

export async function executeTransactionWithRetry<T>(
instance: Sequelize,
operation: (transaction: Transaction) => Promise<T>,
maxRetries: number = 3
): Promise<T> {
for (let attempt = 0; ; attempt += 1) {
try {
const result = await instance.transaction(async (transaction) => {
return await instance.query(sqlStatement, {
transaction
});
});
return result;
return await instance.transaction(operation);
} catch (error) {
const err = error as Error;
if (retries === maxRetries) {
throw new Error(`Maximum retries (${maxRetries}) reached. Last error: ${err.message}`);
}
if (err.message.includes('OC001') || err.message.includes('OC000')) {
console.log(`Error occurred when executing statement ${sqlStatement}, executing retry`);
retries += 1;
} else {
throw err;
if (getSqlState(error) !== '40001' || attempt >= maxRetries) {
throw error;
}

const backoffMs = Math.min(100 * 2 ** attempt, 2000) + Math.random() * 100;
console.log('Retrying transaction after SQLSTATE 40001');
await new Promise((resolve) => setTimeout(resolve, backoffMs));
}
}
}

async function executeSqlStatementWithRetry(
instance: Sequelize,
sqlStatement: string,
maxRetries: number = 3
): Promise<unknown> {
return executeTransactionWithRetry(
instance,
(transaction) => instance.query(sqlStatement, { transaction }),
maxRetries
);
}

async function retryExample() {
var sequelize: Sequelize = await getSequelizeConnection();
await sequelize.authenticate();
Expand All @@ -272,7 +294,7 @@ async function retryExample() {
await executeSqlStatementWithRetry(sequelize, "CREATE TABLE IF NOT EXISTS abc (id UUID NOT NULL);")
await executeSqlStatementWithRetry(sequelize, "DROP TABLE IF EXISTS abc;")

// Run statement that will fail, it will not be retried as the error is not OC001 or OC000
// Run statement that will fail, it will not be retried as the error is not SQLSTATE 40001
try {
await executeSqlStatementWithRetry(sequelize, "DROP TABLE abc;")
} catch (err: any) {
Expand Down
Loading