CrackedRuby CrackedRuby

Transactions and Isolation Levels

Overview

Transactions provide a mechanism for grouping database operations into atomic units that either complete entirely or fail entirely, maintaining data integrity across multiple operations. A transaction represents a sequence of one or more SQL operations treated as a single logical unit of work. The concept originates from database management systems research in the 1970s, particularly the work on System R at IBM.

The transaction model addresses the fundamental challenge of concurrent access to shared data. When multiple processes or threads access the same database simultaneously, without proper coordination, operations can interleave in ways that corrupt data or produce inconsistent results. Transactions solve this by providing guarantees about how operations interact with each other and with system failures.

Isolation levels define the degree to which concurrent transactions are isolated from each other. Different isolation levels offer different trade-offs between consistency guarantees and system performance. A stricter isolation level prevents more anomalies but typically reduces concurrency and throughput. The SQL standard defines four isolation levels, though not all databases implement all levels.

# Basic transaction in Ruby with ActiveRecord
ActiveRecord::Base.transaction do
  account1.update!(balance: account1.balance - 100)
  account2.update!(balance: account2.balance + 100)
end
# Both updates succeed or both roll back

Transactions matter in any system where data consistency is required across multiple operations. Financial applications, inventory management, booking systems, and any domain where race conditions could corrupt data require proper transaction management. The choice of isolation level significantly impacts both correctness and performance characteristics of the application.

Key Principles

Transactions follow the ACID properties: Atomicity, Consistency, Isolation, and Durability. Atomicity guarantees that all operations within a transaction complete successfully or none do. If any operation fails, the database rolls back all changes made by the transaction, leaving the database in its original state. This prevents partial updates that could leave data in an inconsistent state.

Consistency ensures that a transaction transforms the database from one valid state to another valid state. The database enforces constraints, triggers, and cascades during transaction execution. If a transaction would violate any integrity constraint, the entire transaction fails and rolls back. This preserves business rules and data invariants encoded in the database schema.

Isolation determines how transaction integrity is visible to other concurrent transactions. This is where isolation levels come into play. Perfect isolation would mean that concurrent transactions execute as if they were serial, one after another. However, perfect isolation severely limits concurrency. Isolation levels relax certain guarantees to improve performance while still preventing specific types of anomalies.

Durability guarantees that once a transaction commits, its changes persist even in the face of system failures. Committed data survives power losses, crashes, and other catastrophic events. Database systems typically achieve durability through write-ahead logging, where changes are written to a persistent log before being applied to the main database files.

The SQL standard defines four isolation levels, each preventing different read phenomena:

Read Uncommitted allows transactions to read uncommitted changes from other transactions. This is the weakest isolation level and permits dirty reads, non-repeatable reads, and phantom reads. Most applications avoid this level due to the risk of reading data that will be rolled back.

Read Committed ensures transactions only read committed data. This prevents dirty reads but allows non-repeatable reads and phantom reads. A transaction at this level might read different values for the same row at different times if another transaction commits changes between reads. This is the default isolation level for PostgreSQL and many other databases.

Repeatable Read guarantees that if a transaction reads a row, subsequent reads within the same transaction will return the same values. This prevents dirty reads and non-repeatable reads but allows phantom reads. A transaction might see new rows that match its search criteria appear between reads, but existing rows remain consistent.

Serializable provides the strictest isolation by ensuring transactions execute as if they were run serially. This prevents all read phenomena: dirty reads, non-repeatable reads, and phantom reads. Serializable isolation typically involves locking or optimistic concurrency control mechanisms that detect conflicts and abort transactions that would violate serializability.

Three primary read phenomena illustrate why isolation levels matter:

A dirty read occurs when a transaction reads data written by a concurrent uncommitted transaction. If that transaction rolls back, the reading transaction has seen data that never existed in any committed state. This can lead to cascading failures and incorrect business logic execution.

A non-repeatable read happens when a transaction reads the same row twice and gets different values because another transaction modified and committed changes between the reads. This violates the expectation that data remains stable within a transaction.

A phantom read occurs when a transaction executes the same query twice and gets different sets of rows. Another transaction inserted or deleted rows that match the query criteria between executions. This differs from a non-repeatable read in that it involves the appearance or disappearance of entire rows rather than changes to existing rows.

Ruby Implementation

Ruby applications typically interact with transactions through database adapter libraries. ActiveRecord, the ORM included with Rails, provides a high-level transaction API. The transaction method wraps a block of code in a database transaction, automatically committing on success or rolling back on exceptions.

# ActiveRecord transaction with explicit exception handling
User.transaction do
  user = User.create!(name: "Alice", email: "alice@example.com")
  Account.create!(user: user, balance: 1000)
  AuditLog.create!(action: "user_created", user_id: user.id)
end
# All three records created or none created

ActiveRecord transactions propagate exceptions to trigger rollbacks. Any exception raised within the transaction block causes a rollback. The exception then propagates up the call stack unless caught. This behavior encourages using exceptions for flow control in transaction contexts.

# Transaction with explicit rollback
User.transaction do
  user = User.create!(name: "Bob")
  
  if user.email.blank?
    raise ActiveRecord::Rollback
  end
  
  Account.create!(user: user)
end
# ActiveRecord::Rollback is caught by transaction and doesn't propagate

ActiveRecord::Rollback is a special exception that triggers a rollback without propagating outside the transaction block. This allows conditional rollback logic without affecting error handling in calling code.

Nested transactions in ActiveRecord use savepoints rather than true nested transactions. When a transaction block is nested inside another transaction block, ActiveRecord creates a savepoint before the inner block and rolls back to that savepoint if the inner transaction fails.

# Nested transaction using savepoints
User.transaction do
  user = User.create!(name: "Charlie")
  
  begin
    User.transaction(requires_new: true) do
      # This creates a savepoint
      user.update!(email: "invalid-email")
    end
  rescue ActiveRecord::RecordInvalid
    # Inner transaction rolled back to savepoint
    # Outer transaction continues
  end
  
  Account.create!(user: user)
end

The requires_new option forces creation of a savepoint even when already inside a transaction. Without this option, nested transaction blocks simply execute within the existing transaction without creating savepoints.

Setting isolation levels in ActiveRecord depends on the database adapter. PostgreSQL and MySQL adapters support isolation level configuration through adapter-specific methods.

# Setting isolation level in ActiveRecord (PostgreSQL)
ActiveRecord::Base.transaction(isolation: :serializable) do
  account = Account.lock.find(account_id)
  account.balance += 100
  account.save!
end

The isolation parameter accepts symbols corresponding to standard isolation levels: :read_uncommitted, :read_committed, :repeatable_read, and :serializable. Not all databases support all levels, and behavior varies by database system.

The Sequel gem provides similar transaction functionality with a slightly different API:

# Sequel transaction
DB.transaction do
  DB[:users].insert(name: "Dave")
  DB[:accounts].insert(user_id: DB[:users].max(:id), balance: 500)
end

Sequel supports explicit savepoints with the savepoint method:

# Sequel savepoint
DB.transaction do
  DB[:users].insert(name: "Eve")
  
  DB.transaction(savepoint: true) do
    DB[:audit_log].insert(action: "user_created")
  end
end

For applications using the pg gem directly without an ORM, transaction management requires explicit SQL:

# Raw transaction with pg gem
conn = PG.connect(dbname: 'myapp')

begin
  conn.exec("BEGIN")
  conn.exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
  
  result = conn.exec_params(
    "UPDATE accounts SET balance = balance - $1 WHERE id = $2",
    [100, account1_id]
  )
  
  conn.exec_params(
    "UPDATE accounts SET balance = balance + $1 WHERE id = $2",
    [100, account2_id]
  )
  
  conn.exec("COMMIT")
rescue PG::Error => e
  conn.exec("ROLLBACK") rescue nil
  raise
ensure
  conn.close
end

This approach provides fine-grained control but requires explicit error handling and transaction lifecycle management.

Implementation Approaches

Pessimistic Locking acquires locks on database rows to prevent concurrent modifications. When a transaction reads a row with a lock, other transactions must wait until the lock releases before accessing that row. This approach guarantees consistency but can create contention and reduce throughput.

# Pessimistic locking in ActiveRecord
Account.transaction do
  account = Account.lock.find(account_id)
  account.balance -= 100
  account.save!
end
# SELECT ... FOR UPDATE acquires row lock

The lock method generates a SELECT FOR UPDATE query, which acquires an exclusive lock on selected rows. Other transactions attempting to lock the same rows block until the first transaction commits or rolls back. Deadlocks can occur when transactions acquire locks in different orders.

Optimistic Locking assumes conflicts are rare and detects them after they occur. Each row includes a version column that increments on every update. When updating, the transaction checks that the version hasn't changed since the row was read. If the version changed, another transaction modified the row, and the update fails.

# Optimistic locking in ActiveRecord
# Requires lock_version column
class Account < ApplicationRecord
end

account = Account.find(account_id)
account.balance -= 100
account.save!
# Raises ActiveRecord::StaleObjectError if version changed

ActiveRecord automatically handles optimistic locking when a lock_version integer column exists in the table. The application must catch StaleObjectError and implement retry logic or notify the user of the conflict.

Application-Level Transaction Coordination manages distributed transactions across multiple databases or services. This requires implementing two-phase commit protocols or using eventual consistency patterns. Ruby applications often use message queues or event sourcing to coordinate distributed transactions.

# Saga pattern for distributed transactions
class TransferService
  def transfer(from_account_id, to_account_id, amount)
    withdrawal = withdraw(from_account_id, amount)
    
    begin
      deposit(to_account_id, amount)
    rescue => e
      compensate_withdrawal(withdrawal)
      raise
    end
  end
  
  private
  
  def withdraw(account_id, amount)
    Account.transaction do
      account = Account.find(account_id)
      account.balance -= amount
      account.save!
      Withdrawal.create!(account: account, amount: amount)
    end
  end
  
  def compensate_withdrawal(withdrawal)
    Account.transaction do
      account = withdrawal.account
      account.balance += withdrawal.amount
      account.save!
      withdrawal.update!(compensated: true)
    end
  end
end

The saga pattern breaks a distributed transaction into a sequence of local transactions, each with a compensating transaction that undoes its effects. If any step fails, compensating transactions execute in reverse order to maintain consistency.

Read-Only Transactions optimize read-heavy workloads by declaring transactions that only read data. Databases can optimize read-only transactions by avoiding write locks and using snapshot isolation more efficiently.

# Read-only transaction in PostgreSQL
ActiveRecord::Base.transaction do
  ActiveRecord::Base.connection.execute(
    "SET TRANSACTION READ ONLY"
  )
  
  accounts = Account.where(balance: ...100).to_a
  total = accounts.sum(&:balance)
end

Some databases allow setting a transaction as read-only at the start, enabling optimizations like reading from replicas or using more aggressive caching strategies.

Database-Specific Features provide advanced transaction capabilities beyond standard SQL. PostgreSQL supports serializable snapshot isolation (SSI), which provides full serializability with better performance than traditional two-phase locking. MySQL's InnoDB storage engine uses multi-version concurrency control (MVCC) to implement isolation levels efficiently.

Common Pitfalls

Deadlocks occur when two or more transactions wait for each other to release locks, creating a circular dependency that prevents progress. Database systems detect deadlocks and abort one transaction to break the cycle. Applications must handle deadlock exceptions and implement retry logic.

# Deadlock scenario
# Transaction 1
Account.transaction do
  account1 = Account.lock.find(1)
  sleep(0.1)  # Simulating work
  account2 = Account.lock.find(2)  # Deadlock here
end

# Transaction 2 (concurrent)
Account.transaction do
  account2 = Account.lock.find(2)
  sleep(0.1)
  account1 = Account.lock.find(1)  # Deadlock here
end

Preventing deadlocks requires acquiring locks in a consistent order across all transactions. Always lock resources in the same sequence, such as by ascending primary key value.

Lost Updates happen when two transactions read the same row, modify it, and write it back, with the second write overwriting the first. This occurs even at Read Committed isolation level because reads don't prevent concurrent modifications.

# Lost update example
# Initial balance: 1000

# Transaction 1
account = Account.find(1)  # Reads balance: 1000
account.balance += 100
account.save!  # Writes balance: 1100

# Transaction 2 (concurrent, starts before T1 commits)
account = Account.find(1)  # Reads balance: 1000
account.balance -= 50
account.save!  # Writes balance: 950 (lost the +100 update)

Preventing lost updates requires either pessimistic locking (SELECT FOR UPDATE) or optimistic locking with version checking. Repeatable Read isolation alone doesn't prevent lost updates because it only affects reads, not writes.

Long-Running Transactions hold locks or snapshot references for extended periods, blocking other transactions and consuming resources. Long transactions increase the likelihood of conflicts, deadlocks, and resource exhaustion.

# Problematic long transaction
User.transaction do
  users = User.all
  
  users.each do |user|
    # Expensive operation inside transaction
    external_api_call(user)  # Bad: Network I/O in transaction
    sleep(0.5)  # Simulating slow processing
    user.update!(last_processed: Time.now)
  end
end

Move expensive operations, network calls, and file I/O outside transactions. Keep transactions focused on the minimal set of database operations required for consistency.

Incorrect Exception Handling can cause transactions to commit when they should roll back. In ActiveRecord, only exceptions that propagate out of the transaction block trigger rollbacks. Catching and suppressing exceptions within the block allows the transaction to commit despite errors.

# Incorrect exception handling
User.transaction do
  user = User.create!(name: "Frank")
  
  begin
    Account.create!(user: user, balance: -1000)  # Fails validation
  rescue ActiveRecord::RecordInvalid
    # Exception caught, transaction continues and commits
    puts "Account creation failed"
  end
end
# User record committed without account

Let exceptions propagate out of the transaction block to trigger rollback, or explicitly raise ActiveRecord::Rollback after logging the error.

Serialization Failures at Serializable isolation level occur when the database detects conflicts that would violate serializability. Applications must catch these exceptions and retry the transaction.

# Handling serialization failures
MAX_RETRIES = 3

def transfer_with_retry(from_id, to_id, amount)
  retries = 0
  
  begin
    Account.transaction(isolation: :serializable) do
      from_account = Account.find(from_id)
      to_account = Account.find(to_id)
      
      from_account.balance -= amount
      to_account.balance += amount
      
      from_account.save!
      to_account.save!
    end
  rescue ActiveRecord::SerializationFailure => e
    retries += 1
    retry if retries < MAX_RETRIES
    raise
  end
end

PostgreSQL's serializable isolation uses predicate locking and can abort transactions that would create serialization anomalies. Applications should implement exponential backoff when retrying failed transactions.

Insufficient Isolation allows data anomalies that violate business logic. Using Read Committed isolation when Repeatable Read is required can lead to inconsistent aggregations or reports.

# Insufficient isolation for accounting report
Account.transaction do
  total_deposits = Transaction.where(type: "deposit").sum(:amount)
  total_withdrawals = Transaction.where(type: "withdrawal").sum(:amount)
  
  # New transactions committed by other processes between queries
  # Totals may not reflect a consistent snapshot
  
  Report.create!(
    deposits: total_deposits,
    withdrawals: total_withdrawals,
    net: total_deposits - total_withdrawals
  )
end

Use Repeatable Read or Serializable isolation for reports or operations that require a consistent view of data across multiple queries.

Performance Considerations

Isolation level selection directly impacts database performance and scalability. Stricter isolation levels provide stronger consistency guarantees but reduce concurrency by holding locks longer or detecting more conflicts. The choice depends on the specific consistency requirements of each transaction.

Read Committed offers the best performance for most workloads. This isolation level allows high concurrency because it holds locks only for the duration of individual statements, not the entire transaction. Reads don't block writes, and writes don't block reads (except for the specific row being written). Most web applications use Read Committed as the default isolation level.

Repeatable Read degrades performance compared to Read Committed because it must maintain a consistent snapshot of data throughout the transaction. In MVCC databases like PostgreSQL, this means keeping older versions of rows that might be needed for currently running transactions. This increases storage requirements and can bloat tables if long-running transactions prevent old versions from being cleaned up.

# Performance comparison of isolation levels
require 'benchmark'

Benchmark.bm do |x|
  x.report("read_committed:") do
    1000.times do
      Account.transaction(isolation: :read_committed) do
        Account.where("balance > ?", 100).count
      end
    end
  end
  
  x.report("repeatable_read:") do
    1000.times do
      Account.transaction(isolation: :repeatable_read) do
        Account.where("balance > ?", 100).count
      end
    end
  end
  
  x.report("serializable:") do
    1000.times do |i|
      begin
        Account.transaction(isolation: :serializable) do
          Account.where("balance > ?", 100).count
        end
      rescue ActiveRecord::SerializationFailure
        # Serializable may abort transactions
      end
    end
  end
end

Serializable isolation has the highest performance cost. Traditional two-phase locking implementations acquire many locks and hold them for the entire transaction duration, severely limiting concurrency. PostgreSQL's serializable snapshot isolation performs better by detecting conflicts without explicit locking, but still aborts transactions when conflicts are detected, requiring application-level retry logic.

Lock Contention increases with stricter isolation levels and pessimistic locking. When multiple transactions compete for locks on the same rows, they must wait in sequence rather than executing concurrently. This serializes execution and reduces throughput.

# Lock contention measurement
start_time = Time.now
threads = 10.times.map do
  Thread.new do
    Account.transaction do
      account = Account.lock.find(1)  # All threads contend for same row
      account.balance += 1
      account.save!
    end
  end
end
threads.each(&:join)
elapsed = Time.now - start_time
# Higher elapsed time indicates more lock contention

Reducing lock contention requires minimizing the time locks are held and reducing the number of transactions that access the same resources concurrently. Partition data to avoid hot spots where many transactions access the same rows.

Connection Pool Exhaustion occurs when transactions hold database connections for extended periods, preventing other requests from acquiring connections. Long-running transactions at higher isolation levels exacerbate this problem.

# Connection pool configuration
ActiveRecord::Base.establish_connection(
  adapter: "postgresql",
  pool: 5,  # Number of concurrent connections
  checkout_timeout: 5  # Seconds to wait for available connection
)

# Each long transaction blocks a connection
Account.transaction do
  Account.all.each do |account|
    # This holds a connection for a long time
    process_account(account)
  end
end

Keep transactions short and focused. Process large datasets in batches outside of transactions, or use multiple smaller transactions with savepoints.

Snapshot Maintenance in MVCC systems requires maintaining multiple versions of rows. Higher isolation levels keep snapshots longer, increasing storage overhead and slowing down queries that must check multiple row versions to find the correct visible version.

# Long transaction keeping old snapshots alive
Account.transaction(isolation: :repeatable_read) do
  initial_total = Account.sum(:balance)
  
  # Many updates happen in other transactions
  sleep(60)
  
  # This transaction still sees the old snapshot
  # Database must maintain old row versions
  final_total = Account.sum(:balance)
end

Avoid long-running transactions at Repeatable Read or Serializable isolation. Use Read Committed for operations that can tolerate seeing committed changes from concurrent transactions.

Index Usage affects transaction performance significantly. Transactions that scan large numbers of rows hold locks longer and create more work for the MVCC system. Proper indexing reduces the number of rows accessed and the duration of lock holds.

# Without index: full table scan, many row locks
Account.transaction do
  accounts = Account.where("email LIKE ?", "%@example.com%")
  accounts.update_all(verified: true)
end

# With index on email: fewer rows accessed, faster execution
# CREATE INDEX idx_accounts_email ON accounts(email);

Transaction performance optimization requires balancing consistency requirements against concurrency needs. Profile transaction execution times, measure lock wait times, and adjust isolation levels and transaction scope based on actual application behavior.

Tools & Ecosystem

ActiveRecord is the default ORM for Ruby on Rails applications and provides the most commonly used transaction API in the Ruby ecosystem. ActiveRecord abstracts database-specific transaction behavior and provides a consistent interface across different database adapters (PostgreSQL, MySQL, SQLite, etc.).

ActiveRecord's transaction instrumentation integrates with Rails logging and monitoring tools. Transaction events appear in Rails logs with timing information, allowing developers to identify slow or problematic transactions.

# ActiveRecord with detailed logging
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = Logger::DEBUG

User.transaction do
  # Logs show transaction BEGIN, queries, and COMMIT/ROLLBACK
  User.create!(name: "Grace")
end

Sequel is an alternative ORM that provides more direct SQL access and finer control over database operations. Sequel's transaction API is similar to ActiveRecord's but offers more flexibility for advanced use cases.

# Sequel transaction with manual rollback control
DB.transaction do
  DB[:accounts].where(id: account_id).update(balance: Sequel[:balance] - 100)
  
  # Explicit rollback
  raise Sequel::Rollback if some_condition
end

Sequel supports database-specific features more directly than ActiveRecord, making it a good choice for applications that need to use advanced database capabilities.

database_cleaner gem helps manage database state in test environments. It provides strategies for cleaning the database between tests while maintaining transactional test behavior when possible.

# database_cleaner configuration
RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
  
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

The transaction strategy wraps each test in a transaction that rolls back after the test completes, providing fast test cleanup without actually deleting data.

pg gem provides direct PostgreSQL access for Ruby applications. When using pg without an ORM, transaction management requires explicit SQL commands but offers maximum control over transaction behavior.

# Advanced transaction control with pg gem
conn = PG.connect(dbname: 'myapp')

conn.transaction do |c|
  c.exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE")
  
  # Query runs on a consistent snapshot, potentially waiting for a
  # suitable snapshot to become available (DEFERRABLE)
  result = c.exec("SELECT * FROM accounts")
end

mysql2 gem serves a similar role for MySQL databases, providing low-level access to MySQL-specific transaction features.

Apartment gem manages multi-tenant databases with separate schemas or databases per tenant. It handles transaction propagation across tenant contexts.

# Apartment transactions across tenants
Apartment::Tenant.switch!('tenant1')

Account.transaction do
  account = Account.find(account_id)
  account.balance -= 100
  account.save!
  
  # Switch tenant within transaction
  Apartment::Tenant.switch!('tenant2')
  AuditLog.create!(action: "transfer", amount: 100)
end

Makara gem provides database connection pooling with master/replica awareness. It routes write transactions to master databases and read-only queries to replicas.

# Makara configuration
production:
  adapter: postgresql_makara
  makara:
    master_ttl: 5
    blacklist_duration: 5
    connections:
      - role: master
        host: master.db.example.com
      - role: slave
        host: replica1.db.example.com
      - role: slave
        host: replica2.db.example.com

Makara automatically sends transactions to the master database while distributing read-only queries across replicas, improving performance and scalability.

ActiveRecord Import gem provides bulk insert capabilities that work within transactions. This is useful for importing large datasets while maintaining transactional safety.

# Bulk import within transaction
User.transaction do
  users = 1000.times.map { |i| User.new(name: "User#{i}") }
  User.import users, validate: false
  
  # All 1000 users inserted or none
end

Reference

Isolation Levels

Isolation Level Dirty Reads Non-Repeatable Reads Phantom Reads Performance Default In
Read Uncommitted Allowed Allowed Allowed Highest Rarely used
Read Committed Prevented Allowed Allowed High PostgreSQL, Oracle
Repeatable Read Prevented Prevented Allowed Medium MySQL
Serializable Prevented Prevented Prevented Lowest When strict consistency required

ActiveRecord Transaction Methods

Method Description Usage
transaction Wraps block in database transaction Model.transaction { operations }
transaction(requires_new: true) Forces new savepoint in nested transaction Inner transaction block
transaction(isolation: level) Sets isolation level for transaction Model.transaction(isolation: :serializable)
lock Acquires FOR UPDATE lock on records Model.lock.find(id)
with_lock Reloads record with lock record.with_lock { operations }

Common Transaction Patterns

Pattern Use Case Implementation
Pessimistic Locking High contention, updates must not conflict Model.lock.find(id) in transaction
Optimistic Locking Low contention, conflicts rare Add lock_version column, handle StaleObjectError
Retry Logic Serialization failures Rescue SerializationFailure and retry with backoff
Savepoints Partial rollback in complex transaction Nested transaction blocks
Read-Only Consistent snapshots, no writes SET TRANSACTION READ ONLY

PostgreSQL Transaction Commands

Command Purpose
BEGIN Start transaction
COMMIT Commit transaction
ROLLBACK Abort transaction
SAVEPOINT name Create savepoint
ROLLBACK TO name Rollback to savepoint
SET TRANSACTION ISOLATION LEVEL Set isolation level
SET TRANSACTION READ ONLY Mark transaction read-only

Exception Types

Exception Meaning Response
ActiveRecord::Rollback Explicit rollback signal Caught by transaction, doesn't propagate
ActiveRecord::RecordInvalid Validation failure Triggers rollback if not caught
ActiveRecord::StaleObjectError Optimistic locking conflict Reload and retry operation
ActiveRecord::SerializationFailure Serialization conflict detected Retry transaction with backoff
PG::TRDeadlockDetected Deadlock detected by database Retry transaction, fix lock ordering

Transaction Configuration Options

Option Values Effect
isolation :read_uncommitted, :read_committed, :repeatable_read, :serializable Sets transaction isolation level
requires_new true, false Forces savepoint creation in nested transaction
joinable true, false Controls whether transaction can be joined by nested calls

Performance Metrics to Monitor

Metric Indicates Action
Transaction duration Lock hold time Reduce transaction scope
Lock wait time Contention level Optimize locking order, reduce locked operations
Deadlock frequency Lock ordering problems Standardize lock acquisition order
Serialization failure rate Conflict frequency at serializable level Consider lower isolation level or retry logic
Connection pool saturation Long-running transactions Shorten transactions, increase pool size
MVCC bloat Long transactions keeping snapshots Terminate old transactions, tune autovacuum