-
Notifications
You must be signed in to change notification settings - Fork 1
Transaction Isolation
TMI runs every write transaction at SERIALIZABLE isolation on both of its
supported databases — PostgreSQL (development) and Oracle Autonomous Database
(production). This page explains why, how it is enforced, the important
semantic difference between the two databases, and how to verify it.
The database default on both PostgreSQL and Oracle is READ COMMITTED, which
permits subtle, non-deterministic anomalies — lost updates and read/write skew
— that surface as rare, hard-to-reproduce data-integrity bugs. Some of those
anomalies have a security dimension: write skew can defeat an authorization
invariant that is enforced across more than one row.
TMI's posture is to start from the safe default (serializable) and tune performance afterward, rather than ship weak isolation and discover the anomalies in production.
There are two layers.
The retry wrapper auth/db/retry.go::WithRetryableGormTransaction threads
&sql.TxOptions{Isolation: sql.LevelSerializable} into every transaction it
opens. This is the primary, leak-proof mechanism and the only lever that
exists on Oracle:
- GORM → pgx emits
BEGIN ISOLATION LEVEL SERIALIZABLE. - GORM → godror emits
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE, cached per pooled connection.
The wrapper also classifies serialization failures by error code (not string matching) and retries the whole transaction closure with bounded, jittered backoff:
- PostgreSQL:
SQLSTATE 40001(serialization failure),40P01(deadlock). - Oracle:
ORA-08177(can't serialize access),ORA-00060(deadlock),ORA-00054(resource busy).
Because the closure can run more than once, it must be idempotent — keep non-idempotent side effects (outbound calls, one-time-token consumption) out of the transaction or guard them so a replay is harmless.
⚠️ Never requestRepeatableRead— godror rejects it. And never mixnil-option transactions with serializable ones on the same pooled Oracle connection: godror caches the level per physical connection, so a laterniltransaction reusing it would silently run serializable.
On PostgreSQL only, the server pins the connecting role's default at startup:
ALTER ROLE CURRENT_USER SET default_transaction_isolation = 'serializable';This is a backstop: any connection that somehow bypasses the wrapper still
begins its transactions at SERIALIZABLE instead of READ COMMITTED. It is
installed by dbschema.InstallPostgresDefaultIsolation (invoked from the
server's post-migration hook) and is non-fatal — if it fails, the
per-transaction wrapper still enforces the level.
ALTER ROLE … SET affects sessions opened after it runs; connections
already in the pool keep their session default until they are recycled or the
server restarts. Because this layer is only a backstop, that lag is acceptable.
ALTER ROLE CURRENT_USER is used rather than ALTER DATABASE because it is the
portable form — it works on managed PostgreSQL (e.g. Heroku), where the
connecting role owns its own settings but may not own the database.
Oracle has no equivalent. There is no ALTER DATABASE/ALTER ROLE
default-isolation knob, and ADB blocks the logon-trigger workaround. On Oracle,
enforcement is per-transaction (layer 1) only.
SERIALIZABLE does not mean the same thing on both databases:
| PostgreSQL | Oracle | |
|---|---|---|
| Mechanism | Serializable Snapshot Isolation (SSI) | Snapshot isolation (SI) |
| Tracks read/write dependencies | Yes | No |
| Detects write skew |
Yes — aborts with 40001
|
No |
| Aborts when | A dependency cycle could break serializability | Only when modifying a row another tx committed since the snapshot (ORA-08177) |
Oracle serializable is meaningfully weaker. It will not catch write-skew
anomalies. Therefore, cross-row invariants must also be protected with
explicit SELECT … FOR UPDATE row locks or unique constraints — isolation
alone is not sufficient on Oracle.
-
Keep write transactions short. This reduces both
ORA-08177andORA-01555("snapshot too old") on Oracle, and reduces abort/retry churn on both databases. -
Hot single rows are an anti-pattern under serializable. A counter row
updated on every write serializes all writers and, on Oracle, converts lock
contention into
ORA-08177abort churn. TMI's global ThreatModel alias was moved off such a row onto a non-transactional database sequence for exactly this reason (see issue #452). -
Long read-only/reporting paths may stay on
READ COMMITTED— they do not need serializable and would only add retry overhead. -
Don't run
AutoMigrateDDL concurrently with a serializable workload.
A freshly-opened connection (one that does not go through the wrapper) should inherit the role default:
SELECT current_setting('default_transaction_isolation');
-- expected: serializableThis is exercised in CI by
TestInstallPostgresDefaultIsolation_Integration.
There is no role default to read; verification confirms the per-transaction
mechanism instead — that wrapper transactions issue
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE and that ORA-08177 is
observed and retried under contention. Run the Oracle integration suite against
a real ADB connection (make test-integration-oci).
- Database-Operations -- PostgreSQL and Redis operational commands
- Database-Security-Strategies -- Least-privilege database users
- Performance-and-Scaling -- Throughput tuning and contention
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Identity Linking
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS, OCI, GCP, Azure)
- Deploying TMI Server
- OCI Container Deployment
- Certificate Automation
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Bootstrapping Production
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Configuring Local Development
- Managing Operational Settings
- Content Extractors - Limits and Overrides
- Database Operations
- Database Security Strategies
- Transaction Isolation
- Oracle Content Feedback FK Cleanup
- Security Operations
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Local Development Cluster
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions
- Issue Tracker Integration
- Webhook Integration
- Addon System
- MCP Integration
- Delegated Content Providers
- Setting Up Google Content Providers
- API Clients
- API Client Maintenance
- Database Tool Reference
- TMI Terraform Analyzer
- TMI Promtail Logger
- WebSocket Test Harness