Files
pgsql-jellyfin/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/BulkOperationTransaction.cs
T
wjones 57d6342524 Optimize PostgreSQL dead tuple handling for media scans
Introduce bulk operation extensions and transaction helpers for efficient, batched EF Core operations. Refactor MediaStreamInfo schema by splitting audio/video details into dedicated tables, reducing table bloat and improving update performance. Add migration for data movement and schema changes. Update EF Core models and context for new structure. Enhance PostgreSQL provider configuration and add comprehensive documentation and monitoring guides. Includes minor middleware and code cleanups.
2026-04-30 15:52:37 -04:00

141 lines
4.2 KiB
C#

// <copyright file="BulkOperationTransaction.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace Jellyfin.Database.Providers.Postgres;
using System;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
/// <summary>
/// Helper class for managing transactions during bulk operations to reduce dead tuple generation.
/// </summary>
public sealed class BulkOperationTransaction : IAsyncDisposable, IDisposable
{
private readonly JellyfinDbContext _context;
private IDbContextTransaction? _transaction;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="BulkOperationTransaction"/> class.
/// </summary>
/// <param name="context">The database context.</param>
public BulkOperationTransaction(JellyfinDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <summary>
/// Gets a value indicating whether the transaction has been committed.
/// </summary>
public bool IsCommitted { get; private set; }
/// <summary>
/// Begins a new transaction with isolation level optimized for bulk operations.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task BeginAsync(CancellationToken cancellationToken = default)
{
if (_transaction is not null)
{
throw new InvalidOperationException("Transaction is already active. Call Dispose or RollbackAsync first.");
}
// Use READ COMMITTED isolation for bulk operations
// This provides a good balance between consistency and concurrency
_transaction = await _context.Database.BeginTransactionAsync(
System.Data.IsolationLevel.ReadCommitted,
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// Commits the transaction.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task CommitAsync(CancellationToken cancellationToken = default)
{
if (_transaction is null)
{
throw new InvalidOperationException("No active transaction. Call BeginAsync first.");
}
try
{
await _transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
IsCommitted = true;
}
finally
{
_transaction.Dispose();
_transaction = null;
}
}
/// <summary>
/// Rolls back the transaction.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task RollbackAsync(CancellationToken cancellationToken = default)
{
if (_transaction is null)
{
return;
}
try
{
await _transaction.RollbackAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_transaction.Dispose();
_transaction = null;
}
}
/// <summary>
/// Disposes the transaction if not already committed.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
_transaction?.Dispose();
_transaction = null;
_disposed = true;
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes the transaction asynchronously if not already committed.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
if (_transaction is not null)
{
await _transaction.DisposeAsync().ConfigureAwait(false);
}
_transaction = null;
_disposed = true;
GC.SuppressFinalize(this);
}
}