Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/development/planning/theorydb-10of10-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ Guardrails (no denominator games):

**Implementation (in repo)**
- Workflow: `.github/workflows/quality-gates.yml` runs `make rubric` on PRs to `premain` (and on pushes to `premain`).
- Tooling pins: `golangci-lint@v2.5.0`, `govulncheck@v1.1.4`, `gosec@v2.22.11` (plus `go.mod` toolchain `go1.25.6` via `go-version-file`).
- Tooling pins: `golangci-lint@v2.5.0`, `govulncheck@v1.1.4`, `gosec@v2.22.11` (plus `go.mod` toolchain `go1.25.7` via `go-version-file`).
- Integration infra pin: DynamoDB Local uses `amazon/dynamodb-local:3.1.0` (via `docker-compose.yml` and `DYNAMODB_LOCAL_IMAGE`).

---
Expand Down
8 changes: 7 additions & 1 deletion docs/facetheory/isr-cache-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ Lock rule (correctness boundary):
- write new metadata + release lease as one atomic step
- pointer-swap designs where you write a new version item and then update the “current” pointer

(See `docs/facetheory/isr-transaction-recipes.md` once FT-T2 lands.)
(See `docs/facetheory/isr-transaction-recipes.md`.)

## Environment variables (FaceTheory ISR)

- `FACETHEORY_CACHE_TABLE_NAME` is the canonical env var used by FaceTheory ISR cache metadata/lease models.
- If you are wiring this table via **AppTheory**, some constructs may provide `APPTHEORY_CACHE_TABLE_NAME` (and/or other
aliases). Ensure `FACETHEORY_CACHE_TABLE_NAME` is set so FaceTheory docs and examples work without guesswork.

## Runnable model definitions

Expand Down
85 changes: 54 additions & 31 deletions docs/facetheory/isr-transaction-recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ Use this when you only need one metadata record per cache key.
1. Acquire the lease (`LOCK`) for `pk`.
2. Regenerate the body and write it to S3.
3. Publish metadata and release the lock with a single DynamoDB transaction:
- `ConditionCheck` the lease row (`lease_token` matches AND `lease_expires_at > now`)
- `Put` the metadata row (`META`) with the new pointer (`s3_key`), timestamps, etag, ttl
- `Delete` the lease row (optionally conditioned on `lease_token`)
- `Delete` the lease row (`LOCK`) with a condition expression (`lease_token` matches AND `lease_expires_at > now`)

Note: DynamoDB transactions cannot include multiple operations on the same item. Prefer a conditional `Delete`/`Update`
on the lease row over a separate `ConditionCheck` + `Delete` on the same lease key.

### Why the transaction matters

Expand All @@ -46,10 +48,9 @@ Recommended item roles (same `pk`):

Transaction sketch:

1. `ConditionCheck` the lease row (token + not expired).
2. `Put` the new version row (`VER#...`) guarded with `attribute_not_exists(pk)` to avoid duplicate IDs.
3. `Update` the pointer row (`META`) to set `current_sk` to the new version (optionally guard with optimistic `version`).
4. `Delete` the lease row (optionally conditioned on token).
1. `Put` the new version row (`VER#...`) guarded with `attribute_not_exists(pk)` to avoid duplicate IDs.
2. `Update` the pointer row (`META`) to set `current_sk` to the new version (optionally guard with optimistic `version`).
3. `Delete` the lease row (`LOCK`) with a condition expression (token + not expired).

## Stale-writer protection options

Expand Down Expand Up @@ -96,16 +97,13 @@ metaItem := &models.FaceTheoryCacheMetadata{
}

err := db.TransactWrite(ctx, func(tx core.TransactionBuilder) error {
tx.ConditionCheck(
tx.Put(metaItem)

tx.Delete(
leaseItem,
tabletheory.Condition("lease_token", "=", leaseToken),
tabletheory.Condition("lease_expires_at", ">", nowUnix),
)

tx.Put(metaItem)

// Optional: make the delete conditional on the same token.
tx.Delete(leaseItem, tabletheory.Condition("lease_token", "=", leaseToken))
return nil
})
```
Expand All @@ -114,17 +112,6 @@ err := db.TransactWrite(ctx, func(tx core.TransactionBuilder) error {

```ts
await client.transactWrite([
{
kind: 'condition',
model: 'FaceTheoryCacheLease',
key: { pk, sk: 'LOCK' },
conditionExpression: '#tok = :tok AND #exp > :now',
expressionAttributeNames: { '#tok': 'lease_token', '#exp': 'lease_expires_at' },
expressionAttributeValues: {
':tok': { S: leaseToken },
':now': { N: String(nowUnix) },
},
},
{
kind: 'put',
model: 'FaceTheoryCacheMetadata',
Expand All @@ -134,22 +121,56 @@ await client.transactWrite([
kind: 'delete',
model: 'FaceTheoryCacheLease',
key: { pk, sk: 'LOCK' },
conditionExpression: '#tok = :tok AND #exp > :now',
expressionAttributeNames: { '#tok': 'lease_token', '#exp': 'lease_expires_at' },
expressionAttributeValues: {
':tok': { S: leaseToken },
':now': { N: String(nowUnix) },
},
},
]);
```

### TypeScript (TableTheory helper: `FaceTheoryIsrMetaStore`)

TableTheory exports a small helper that implements the FaceTheory ISR metadata + lease operations using:

- `LeaseManager` for acquiring/releasing `LOCK` rows
- `TheorydbClient.transactWrite()` for atomic “publish META + release LOCK” (Recipe A)

```ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { createFaceTheoryIsrMetaStore } from '@theory-cloud/tabletheory-ts';

const ddb = new DynamoDBClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
const isr = createFaceTheoryIsrMetaStore({
ddb,
tableName: process.env.FACETHEORY_CACHE_TABLE_NAME!,
});

const lease = await isr.tryAcquireLease({
cacheKey,
leaseOwner: 'my-app-instance', // optional (for app logs)
leaseDurationMs: 30_000,
});
if (!lease) return; // another contender is regenerating

await isr.commitGeneration({
cacheKey,
leaseOwner: 'my-app-instance', // optional (for app logs)
leaseToken: lease.leaseToken,
htmlPointer: s3Key,
generatedAtMs: Date.now(),
revalidateSeconds: 60,
etag,
});
```

### Python (`Table.transact_write`)

```py
table.transact_write(
[
TransactConditionCheck(
pk=pk,
sk="LOCK",
condition_expression="#tok = :tok AND #exp > :now",
expression_attribute_names={"#tok": "lease_token", "#exp": "lease_expires_at"},
expression_attribute_values={":tok": lease_token, ":now": now_unix},
),
TransactPut(
item=FaceTheoryCacheMetadata(
pk=pk,
Expand All @@ -164,8 +185,10 @@ table.transact_write(
TransactDelete(
pk=pk,
sk="LOCK",
condition_expression="#tok = :tok AND #exp > :now",
expression_attribute_names={"#tok": "lease_token", "#exp": "lease_expires_at"},
expression_attribute_values={":tok": lease_token, ":now": now_unix},
),
]
)
```

3 changes: 3 additions & 0 deletions ts/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,9 @@ export class TheorydbClient {
Delete: {
TableName: model.tableName,
Key: marshalKey(model, a.key),
ConditionExpression: a.conditionExpression,
ExpressionAttributeNames: a.expressionAttributeNames,
ExpressionAttributeValues: a.expressionAttributeValues,
} satisfies Delete,
});
break;
Expand Down
Loading
Loading