Add automatic PostgreSQL database creation and migration
- Added Jellyfin.Database.Providers.Postgres project with full EF Core migration support for PostgreSQL. - Implemented automatic database creation and privilege assignment on startup. - Generated initial migration and model snapshot for PostgreSQL schema. - Updated build, test, and dependency files to include PostgreSQL provider and Npgsql packages. - Added PowerShell script for generating and testing PostgreSQL migrations. - No changes to application logic outside database provider/migration infrastructure.
This commit is contained in:
+86
-15
@@ -43,6 +43,8 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
|
||||
private static ReaderWriterLockSlim DatabaseLock { get; } = new(LockRecursionPolicy.SupportsRecursion);
|
||||
|
||||
private static AsyncLocal<int> WriteLockDepth { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnSaveChanges(JellyfinDbContext context, Action saveChanges)
|
||||
{
|
||||
@@ -83,56 +85,114 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
|
||||
public override InterceptionResult<DbTransaction> TransactionStarting(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result)
|
||||
{
|
||||
DbLock.BeginWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth == 0)
|
||||
{
|
||||
DbLock.BeginWriteLock(this._logger);
|
||||
WriteLockDepth.Value = currentDepth + 1;
|
||||
}
|
||||
|
||||
return base.TransactionStarting(connection, eventData, result);
|
||||
}
|
||||
|
||||
public override ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(DbConnection connection, TransactionStartingEventData eventData, InterceptionResult<DbTransaction> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DbLock.BeginWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth == 0)
|
||||
{
|
||||
DbLock.BeginWriteLock(this._logger);
|
||||
WriteLockDepth.Value = currentDepth + 1;
|
||||
}
|
||||
|
||||
return base.TransactionStartingAsync(connection, eventData, result, cancellationToken);
|
||||
}
|
||||
|
||||
public override void TransactionCommitted(DbTransaction transaction, TransactionEndEventData eventData)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
WriteLockDepth.Value = currentDepth - 1;
|
||||
if (currentDepth == 1)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
base.TransactionCommitted(transaction, eventData);
|
||||
}
|
||||
|
||||
public override Task TransactionCommittedAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
WriteLockDepth.Value = currentDepth - 1;
|
||||
if (currentDepth == 1)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
return base.TransactionCommittedAsync(transaction, eventData, cancellationToken);
|
||||
}
|
||||
|
||||
public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
WriteLockDepth.Value = currentDepth - 1;
|
||||
if (currentDepth == 1)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
base.TransactionFailed(transaction, eventData);
|
||||
}
|
||||
|
||||
public override Task TransactionFailedAsync(DbTransaction transaction, TransactionErrorEventData eventData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
WriteLockDepth.Value = currentDepth - 1;
|
||||
if (currentDepth == 1)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
return base.TransactionFailedAsync(transaction, eventData, cancellationToken);
|
||||
}
|
||||
|
||||
public override void TransactionRolledBack(DbTransaction transaction, TransactionEndEventData eventData)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
WriteLockDepth.Value = currentDepth - 1;
|
||||
if (currentDepth == 1)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
base.TransactionRolledBack(transaction, eventData);
|
||||
}
|
||||
|
||||
public override Task TransactionRolledBackAsync(DbTransaction transaction, TransactionEndEventData eventData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
WriteLockDepth.Value = currentDepth - 1;
|
||||
if (currentDepth == 1)
|
||||
{
|
||||
DbLock.EndWriteLock(this._logger);
|
||||
}
|
||||
}
|
||||
|
||||
return base.TransactionRolledBackAsync(transaction, eventData, cancellationToken);
|
||||
}
|
||||
@@ -206,6 +266,7 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
|
||||
private readonly Action? _action;
|
||||
private bool _disposed;
|
||||
private bool _lockAcquired;
|
||||
|
||||
public DbLock(Action? action = null)
|
||||
{
|
||||
@@ -217,17 +278,23 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
#pragma warning restore IDISP015 // Member should not return created and cached instance
|
||||
{
|
||||
logger.LogTrace("Enter Write for {Caller}:{Line}", callerMemberName, callerNo);
|
||||
if (DatabaseLock.IsWriteLockHeld)
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
|
||||
logger.LogTrace("Write Already Held (Depth: {Depth}) {Caller}:{Line}", currentDepth, callerMemberName, callerNo);
|
||||
return _noLock;
|
||||
}
|
||||
|
||||
BeginWriteLock(logger, command, callerMemberName, callerNo);
|
||||
WriteLockDepth.Value = currentDepth + 1;
|
||||
return new DbLock(() =>
|
||||
{
|
||||
WriteLockDepth.Value = Math.Max(0, WriteLockDepth.Value - 1);
|
||||
EndWriteLock(logger, callerMemberName, callerNo);
|
||||
});
|
||||
})
|
||||
{
|
||||
_lockAcquired = true
|
||||
};
|
||||
}
|
||||
|
||||
#pragma warning disable IDISP015 // Member should not return created and cached instance
|
||||
@@ -235,9 +302,10 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
#pragma warning restore IDISP015 // Member should not return created and cached instance
|
||||
{
|
||||
logger.LogTrace("Enter Read {Caller}:{Line}", callerMemberName, callerNo);
|
||||
if (DatabaseLock.IsWriteLockHeld)
|
||||
var currentDepth = WriteLockDepth.Value;
|
||||
if (currentDepth > 0)
|
||||
{
|
||||
logger.LogTrace("Write Held {Caller}:{Line}", callerMemberName, callerNo);
|
||||
logger.LogTrace("Write Already Held (Depth: {Depth}) {Caller}:{Line}", currentDepth, callerMemberName, callerNo);
|
||||
return _noLock;
|
||||
}
|
||||
|
||||
@@ -245,7 +313,10 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
return new DbLock(() =>
|
||||
{
|
||||
ExitReadLock(logger, callerMemberName, callerNo);
|
||||
});
|
||||
})
|
||||
{
|
||||
_lockAcquired = true
|
||||
};
|
||||
}
|
||||
|
||||
public static void BeginWriteLock(ILogger logger, IDbCommand? command = null, [CallerMemberName] string? callerMemberName = null, [CallerLineNumber] int? callerNo = null)
|
||||
@@ -299,7 +370,7 @@ public class PessimisticLockBehavior : IEntityFrameworkCoreLockingBehavior
|
||||
}
|
||||
|
||||
this._disposed = true;
|
||||
if (this._action is not null)
|
||||
if (this._lockAcquired && this._action is not null)
|
||||
{
|
||||
this._action();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user