Changed transactions to use a wrapper that ensures a transaction is allocated.

This commit is contained in:
Kenneth Skovhede
2025-04-14 12:04:00 +02:00
parent 6e18f7a4d9
commit faede65d8d
12 changed files with 52 additions and 35 deletions
@@ -493,6 +493,23 @@ public static class ExtensionMethods
return cmd;
}
/// <summary>
/// Creates the transaction for the given connection and executes a "BEGIN IMMEDIATE" command to ensure that the transaction can be comitted or rolled back.
/// This works around a quirk in SQLite where the transaction is not initialized until the first command is executed.
/// This means that if the transaction is rolled back or committed with no commands executed, it will fail with an exception.
/// </summary>
/// <param name="self">The connection to create the transaction on</param>
/// <returns>The transaction</returns>
public static IDbTransaction BeginTransactionSafe(this IDbConnection self)
{
var transaction = self.BeginTransaction();
using (var cmd = self.CreateCommand(transaction, "BEGIN IMMEDIATE;"))
cmd.ExecuteNonQuery();
return transaction;
}
/// <summary>
/// Creates a command with the given command string, and adds parameters to fit the input
/// </summary>
@@ -41,7 +41,7 @@ namespace Duplicati.Library.Main.Database
public void Fix()
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand())
{
cmd.Transaction = tr;
@@ -654,7 +654,7 @@ AND Fileset.ID NOT IN
// TODO: Remove this
public IDbTransaction BeginTransaction()
{
return m_connection.BeginTransaction();
return m_connection.BeginTransactionSafe();
}
protected class TemporaryTransactionWrapper : IDisposable
@@ -671,7 +671,7 @@ AND Fileset.ID NOT IN
}
else
{
m_parent = connection.BeginTransaction();
m_parent = connection.BeginTransactionSafe();
m_isTemporary = true;
}
}
@@ -1626,7 +1626,7 @@ AND oldVersion.FilesetID = (SELECT ID FROM Fileset WHERE ID != @FilesetId ORDER
public void PurgeLogData(DateTime threshold)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(threshold);
@@ -1643,7 +1643,7 @@ AND oldVersion.FilesetID = (SELECT ID FROM Fileset WHERE ID != @FilesetId ORDER
public void PurgeDeletedVolumes(DateTime threshold)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
m_removedeletedremotevolumeCommand.SetParameterValue("@Now", Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(threshold))
@@ -1663,7 +1663,7 @@ AND oldVersion.FilesetID = (SELECT ID FROM Fileset WHERE ID != @FilesetId ORDER
{
if (m_connection.State == ConnectionState.Open && !m_hasExecutedVacuum)
{
using (var transaction = m_connection.BeginTransaction())
using (var transaction = m_connection.BeginTransactionSafe())
using (var command = m_connection.CreateCommand(transaction))
{
// SQLite recommends that PRAGMA optimize is run just before closing each database connection.
@@ -112,7 +112,7 @@ namespace Duplicati.Library.Main.Database
m_previousTable = "Previous-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
m_currentTable = "Current-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
m_transaction = m_connection.BeginTransaction();
m_transaction = m_connection.BeginTransactionSafe();
using (var cmd = m_connection.CreateCommand(m_transaction))
{
@@ -289,7 +289,7 @@ namespace Duplicati.Library.Main.Database
public void FixDuplicateMetahash()
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
var sql_count =
@@ -355,7 +355,7 @@ namespace Duplicati.Library.Main.Database
public void FixDuplicateFileentries()
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
var sql_count = @"SELECT COUNT(*) FROM (SELECT ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"", COUNT(*) as ""Duplicates"" FROM ""FileLookup"" GROUP BY ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"") WHERE ""Duplicates"" > 1";
@@ -398,7 +398,7 @@ namespace Duplicati.Library.Main.Database
{
var blocklistbuffer = new byte[blocksize];
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
using (var blockhasher = HashFactory.CreateHasher(blockhashalgorithm))
{
@@ -535,7 +535,7 @@ namespace Duplicati.Library.Main.Database
public void FixDuplicateBlocklistHashes(long blocksize, long hashsize)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
var dup_sql = @"SELECT * FROM (SELECT ""BlocksetID"", ""Index"", COUNT(*) AS ""EC"" FROM ""BlocklistHash"" GROUP BY ""BlocksetID"", ""Index"") WHERE ""EC"" > 1";
@@ -591,7 +591,7 @@ namespace Duplicati.Library.Main.Database
public void CheckAllBlocksAreInVolume(string filename, IEnumerable<KeyValuePair<string, long>> blocks)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
var tablename = "ProbeBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray());
@@ -246,7 +246,7 @@ END ");
// If we get a list of filenames, the lookup table is faster
// unfortunately we cannot do this if the filesystem is case sensitive as
// SQLite only supports ASCII compares
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
{
var p = expression.GetSimpleList();
var m_filenamestable = "Filenames-" + m_temptabsetguid;
@@ -1173,7 +1173,7 @@ ORDER BY ""A"".""TargetPath"", ""BB"".""Index"""));
public DirectBlockMarker(IDbConnection connection, string blocktablename, string filetablename, string statstablename)
{
m_transaction = connection.BeginTransaction();
m_transaction = connection.BeginTransactionSafe();
m_blocktablename = blocktablename;
m_filetablename = filetablename;
@@ -1362,7 +1362,7 @@ WHERE ""FileID"" = @TargetFileId AND ""Index"" = @Index AND ""Hash"" = @Hash AND
public IEnumerable<IFastSource> GetFilesAndSourceBlocksFast(long blocksize)
{
using (var transaction = m_connection.BeginTransaction())
using (var transaction = m_connection.BeginTransactionSafe())
using (var cmdReader = m_connection.CreateCommand())
using (var cmd = m_connection.CreateCommand(transaction))
{
@@ -64,14 +64,14 @@ internal class ReusableTransaction : IDisposable
/// </summary>
/// <param name="message">The log message to use</param>
/// <param name="restart">True if the transaction should be restarted</param>
public void Commit(string? message = null, bool restart = true)
public void Commit(string? message, bool restart = true)
{
if (m_transaction == null)
throw new InvalidOperationException("Transaction is already disposed");
if (m_transaction != null)
{
using (var timer = string.IsNullOrWhiteSpace(message) ? null : new Logging.Timer(LOGTAG, message, "CommitTransaction"))
using (var timer = string.IsNullOrWhiteSpace(message) ? null : new Logging.Timer(LOGTAG, message, $"CommitTransaction: {message}"))
m_transaction.Commit();
m_transaction.Dispose();
m_transaction = null;
@@ -203,7 +203,7 @@ namespace Duplicati.Library.Main.Operation
// Commit as we have uploaded a volume
if (!m_options.Dryrun)
rtr.Commit();
rtr.Commit("CommitCompact");
}
}
}
@@ -317,7 +317,7 @@ namespace Duplicati.Library.Main.Operation
await backend.WaitForEmptyAsync(db, rtr.Transaction, cancellationToken).ConfigureAwait(false);
if (!m_options.Dryrun)
rtr.Commit();
rtr.Commit("CommitDelete");
await foreach (var d in PerformDelete(backend, remoteFilesToRemove, cancellationToken).ConfigureAwait(false))
yield return d;
@@ -94,7 +94,7 @@ namespace Duplicati.Library.Main.Operation
db.UpdateRemoteVolume(f.Key, RemoteVolumeState.Deleting, f.Value, null, rtr.Transaction);
if (!m_options.Dryrun)
rtr.Commit();
rtr.Commit("CommitBeforeDelete");
foreach (var f in lst)
{
@@ -247,7 +247,7 @@ namespace Duplicati.Library.Main.Operation
.DoCompactAsync(cdb, true, ctr, backendManager)
.ConfigureAwait(false);
ctr.Commit(restart: false);
ctr.Commit("PostCompact", restart: false);
}
}
@@ -649,7 +649,7 @@ namespace Duplicati.Library.Main.Operation
using (var rdb = new LocalRecreateDatabase(db, m_options))
RecreateDatabaseHandler.RecreateFilesetFromRemoteList(rdb, tr.Transaction, compressor, entry.Key, m_options, new FilterExpression());
tr.Commit();
tr.Commit("PostRepairFileset");
}
}
@@ -218,7 +218,7 @@ namespace Duplicati.Server.Database
internal void SetMetadata(IDictionary<string, string> values, long id, IDbTransaction? transaction)
{
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
using (var tr = transaction == null ? m_connection.BeginTransactionSafe() : null)
{
OverwriteAndUpdateDb(
tr,
@@ -254,7 +254,7 @@ namespace Duplicati.Server.Database
internal void SetFilters(IEnumerable<IFilter> values, long id, IDbTransaction? transaction = null)
{
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
using (var tr = transaction == null ? m_connection.BeginTransactionSafe() : null)
{
OverwriteAndUpdateDb(
tr,
@@ -292,7 +292,7 @@ namespace Duplicati.Server.Database
internal void SetSettings(IEnumerable<ISetting> values, long id, IDbTransaction? transaction = null)
{
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
using (var tr = transaction == null ? m_connection.BeginTransactionSafe() : null)
{
if (m_encryptSensitiveFields)
values = values.Select(x => new Setting
@@ -338,7 +338,7 @@ namespace Duplicati.Server.Database
internal void SetSources(IEnumerable<string> values, long id, IDbTransaction transaction)
{
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
using (var tr = transaction == null ? m_connection.BeginTransactionSafe() : null)
{
OverwriteAndUpdateDb(
tr,
@@ -595,7 +595,7 @@ namespace Duplicati.Server.Database
{
lock (m_lock)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
{
using (var cmd = m_connection.CreateCommand(tr, @"UPDATE ""Backup"" SET ""DBPath""= @Dbpath WHERE ""ID""= @Id"))
{
@@ -636,7 +636,7 @@ namespace Duplicati.Server.Database
throw new Exception("Unable to generate a unique database file name");
}
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
{
OverwriteAndUpdateDb(
tr,
@@ -728,7 +728,7 @@ namespace Duplicati.Server.Database
internal void AddOrUpdateSchedule(ISchedule item)
{
lock (m_lock)
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
{
AddOrUpdateSchedule(item, tr);
tr.Commit();
@@ -778,7 +778,7 @@ namespace Duplicati.Server.Database
lock (m_lock)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
{
var existing = GetScheduleIDsFromTags(new string[] { "ID=" + ID.ToString() });
if (existing.Any())
@@ -958,7 +958,7 @@ namespace Duplicati.Server.Database
//Workaround to clean up the database after invalid settings update
public void FixInvalidBackupId()
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
cmd.SetCommandAndParameters(@"DELETE FROM ""Option"" WHERE ""BackupID"" = @BackupId")
@@ -1009,7 +1009,7 @@ namespace Duplicati.Server.Database
public void SetUISettings(string scheme, IDictionary<string, string?> values, IDbTransaction? transaction = null)
{
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
using (var tr = transaction == null ? m_connection.BeginTransactionSafe() : null)
{
OverwriteAndUpdateDb(
tr,
@@ -1033,7 +1033,7 @@ namespace Duplicati.Server.Database
public void UpdateUISettings(string scheme, IDictionary<string, string?> values, IDbTransaction? transaction = null)
{
lock (m_lock)
using (var tr = transaction == null ? m_connection.BeginTransaction() : null)
using (var tr = transaction == null ? m_connection.BeginTransactionSafe() : null)
{
OverwriteAndUpdateDb(
tr,
@@ -1086,7 +1086,7 @@ namespace Duplicati.Server.Database
{
var t = Library.Utility.Utility.NormalizeDateTimeToEpochSeconds(purgeDate);
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
using (var cmd = m_connection.CreateCommand(tr))
{
cmd.SetCommandAndParameters(@"DELETE FROM ""ErrorLog"" WHERE ""Timestamp"" < @Time")
@@ -1165,7 +1165,7 @@ namespace Duplicati.Server.Database
{
if (transaction == null)
{
using (var tr = m_connection.BeginTransaction())
using (var tr = m_connection.BeginTransactionSafe())
{
var r = DeleteFromDb(tablename, id, tr);
tr.Commit();