Merge pull request #6173 from duplicati/feature/dont-upload-dlist-on-failure

Simple fix to prevent uploading dlist on failures
This commit is contained in:
Kenneth Skovhede
2025-04-23 15:24:11 +02:00
committed by GitHub
14 changed files with 171 additions and 41 deletions
@@ -75,6 +75,10 @@ partial class BackendManager
/// </summary>
public required bool TrackedInDb { get; init; }
/// <summary>
/// A callback that is invoked when the database needs to be updated
/// </summary>
public required Func<Task> OnDbUpdate { get; init; }
/// <summary>
/// The encryption and hashing task, the return value indicates if the hash and size was updated
/// </summary>
private Task<(string Hash, long Size)>? encryptionAndHashingTask;
@@ -94,6 +98,10 @@ partial class BackendManager
/// A callback that is invoked when the index volume is finished
/// </summary>
public required Action? IndexVolumeFinishedCallback { get; init; }
/// <summary>
/// The default callback for database updates (no-op)
/// </summary>
public static Func<Task> OnDbUpdateDefault = () => Task.CompletedTask;
/// <summary>
/// Creates a new put operation
@@ -201,12 +209,16 @@ partial class BackendManager
// On first upload attempt, calculate the hash and size
if (operationState == OperationState.None && TrackedInDb)
{
Context.Database.LogRemoteVolumeUpdated(RemoteFilename, RemoteVolumeState.Uploading, size, hash);
await OnDbUpdate().ConfigureAwait(false);
}
// First attempt here, finish the index file now that all information is known
if (OriginalIndexFile != null && indexOperation == null)
{
Context.Database.LogRemoteVolumeUpdated(OriginalIndexFile.RemoteFilename, RemoteVolumeState.Uploading, -1, null);
await OnDbUpdate().ConfigureAwait(false);
// Prepare an upload operation for the index file
var req2 = new PutOperation(OriginalIndexFile.RemoteFilename, Context, false, cancelToken)
@@ -215,9 +227,11 @@ partial class BackendManager
TrackedInDb = TrackedInDb,
Unencrypted = Unencrypted,
IndexVolumeFinishedCallback = null,
OriginalIndexFile = null
OriginalIndexFile = null,
OnDbUpdate = OnDbUpdate
};
OriginalIndexFile.FinishVolume(hash, size);
IndexVolumeFinishedCallback?.Invoke();
OriginalIndexFile.Close();
@@ -226,8 +240,11 @@ partial class BackendManager
}
// If we have previously attempted to upload, we need to rename the file
if (operationState == OperationState.Uploading)
if (operationState == OperationState.Uploading && TrackedInDb)
{
RenameFileAfterError(size);
await OnDbUpdate().ConfigureAwait(false);
}
// Flag for next attempt, if any
operationState = OperationState.Uploading;
@@ -263,6 +280,8 @@ partial class BackendManager
{
Context.Database.LogRemoteOperation("put", RemoteFilename, JsonConvert.SerializeObject(new { Size = Size, Hash = Hash }));
Context.Statwriter.SendEvent(BackendActionType.Put, BackendEventType.Started, RemoteFilename, Size);
if (TrackedInDb)
await OnDbUpdate().ConfigureAwait(false);
var begin = DateTime.Now;
@@ -280,7 +299,10 @@ partial class BackendManager
Logging.Log.WriteProfilingMessage(LOGTAG, "UploadSpeed", "Uploaded {0} in {1}, {2}/s", Library.Utility.Utility.FormatSizeString(Size), duration, Library.Utility.Utility.FormatSizeString((long)(Size / duration.TotalSeconds)));
if (TrackedInDb)
{
Context.Database.LogRemoteVolumeUpdated(RemoteFilename, RemoteVolumeState.Uploaded, Size, Hash);
await OnDbUpdate().ConfigureAwait(false);
}
Context.Statwriter.SendEvent(BackendActionType.Put, BackendEventType.Completed, RemoteFilename, Size);
@@ -249,9 +249,10 @@ internal partial class BackendManager : IBackendManager
/// <param name="indexVolume">The index volume to upload, if any</param>
/// <param name="indexVolumeFinished">The callback to call when the index volume is finished</param>
/// <param name="waitForComplete">True if the operation should wait for the file to actually be uploaded. If this argument is false, the task will complete once the operation is queued</param>
/// <param name="onDbUpdate">The callback to call when the database is updated</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task PutAsync(VolumeWriterBase volume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, CancellationToken cancelToken)
public async Task PutAsync(VolumeWriterBase volume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, Func<Task>? onDbUpdate, CancellationToken cancelToken)
{
volume.Close();
@@ -261,7 +262,8 @@ internal partial class BackendManager : IBackendManager
OriginalIndexFile = indexVolume,
Unencrypted = false,
TrackedInDb = true,
IndexVolumeFinishedCallback = indexVolumeFinished
IndexVolumeFinishedCallback = indexVolumeFinished,
OnDbUpdate = onDbUpdate ?? PutOperation.OnDbUpdateDefault,
};
// Prepare encryption
@@ -285,7 +287,8 @@ internal partial class BackendManager : IBackendManager
Unencrypted = true, // Avoid encrypting
TrackedInDb = false, // Not tracked
OriginalIndexFile = null,
IndexVolumeFinishedCallback = null
IndexVolumeFinishedCallback = null,
OnDbUpdate = PutOperation.OnDbUpdateDefault,
};
// Sets the task as already completed
@@ -294,20 +297,43 @@ internal partial class BackendManager : IBackendManager
await op.GetResult().ConfigureAwait(false);
}
/// <summary>
/// Flushes the database messages to the database
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns></returns>
public Task FlushPendingMessagesAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken)
{
context.Database.FlushPendingMessages(database, transaction);
return Task.CompletedTask;
}
/// <summary>
/// Waits for the backend queue to be empty
/// </summary>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task WaitForEmptyAsync(CancellationToken cancellationToken)
{
var op = new WaitForEmptyOperation(context, cancellationToken);
await QueueTask(op).ConfigureAwait(false);
await op.GetResult().ConfigureAwait(false);
}
/// <summary>
/// Waits for the backend queue to be empty and flushes the database messages
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
public async Task WaitForEmptyAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken)
{
context.Database.FlushPendingMessages(database, transaction);
var op = new WaitForEmptyOperation(context, cancellationToken);
await QueueTask(op).ConfigureAwait(false);
await op.GetResult().ConfigureAwait(false);
context.Database.FlushPendingMessages(database, transaction);
await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false);
await WaitForEmptyAsync(cancellationToken).ConfigureAwait(false);
await FlushPendingMessagesAsync(database, transaction, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@@ -318,7 +344,8 @@ internal partial class BackendManager : IBackendManager
public async Task StopRunnerAndFlushMessages(LocalDatabase database, IDbTransaction? transaction)
{
await requestChannel.RetireAsync().ConfigureAwait(false);
context.Database.FlushPendingMessages(database, transaction);
await FlushPendingMessagesAsync(database, transaction, CancellationToken.None).ConfigureAwait(false);
if (queueRunner.IsFaulted)
Logging.Log.WriteWarningMessage(LOGTAG, "BackendManagerShutdown", queueRunner.Exception, "Backend manager queue runner crashed");
}
+18 -1
View File
@@ -25,9 +25,10 @@ internal interface IBackendManager : IDisposable
/// <param name="indexVolume">The index volume to upload, if any</param>
/// <param name="indexVolumeFinished">The action to call when the index volume is finished</param>
/// <param name="waitForComplete">Whether to wait for the upload to complete</param>
/// <param name="onDbUpdate">The action to call when the database should be updated</param>
/// <param name="cancelToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
Task PutAsync(VolumeWriterBase blockVolume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, CancellationToken cancelToken);
Task PutAsync(VolumeWriterBase blockVolume, IndexVolumeWriter? indexVolume, Action? indexVolumeFinished, bool waitForComplete, Func<Task>? onDbUpdate, CancellationToken cancelToken);
/// <summary>
/// Uploads a file to the backend without encryption
@@ -38,6 +39,13 @@ internal interface IBackendManager : IDisposable
/// <returns>An awaitable task</returns>
Task PutVerificationFileAsync(string remotename, TempFile tempFile, CancellationToken cancelToken);
/// <summary>
/// Waits for the backend queue to be empty
/// </summary>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns>An awaitable task</returns>
Task WaitForEmptyAsync(CancellationToken cancellationToken);
/// <summary>
/// Waits for the backend queue to be empty and flushes any pending messages to the database
/// </summary>
@@ -128,4 +136,13 @@ internal interface IBackendManager : IDisposable
/// </summary>
long LastWriteSize { get; }
/// <summary>
/// Flushes the database messages to the database
/// </summary>
/// <param name="database">The database to write to</param>
/// <param name="transaction">The transaction to use</param>
/// <param name="cancellationToken">The cancellation token</param>
/// <returns></returns>
Task FlushPendingMessagesAsync(LocalDatabase database, IDbTransaction? transaction, CancellationToken cancellationToken);
}
@@ -127,8 +127,6 @@ namespace Duplicati.Library.Main.Operation.Backup
blockvolume.Close();
await database.CommitTransactionAsync("CommitAddBlockToOutputFlush");
IndexVolumeWriter indexVolumeCopy = null;
if (indexvolume != null)
{
@@ -143,7 +141,8 @@ namespace Duplicati.Library.Main.Operation.Backup
blockvolume = null;
indexvolume = null;
await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, taskreader.ProgressToken);
await database.CommitTransactionAsync("CommitAddBlockToOutputFlush");
await backendManager.PutAsync(blockVolumeCopy, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken);
}
@@ -47,7 +47,8 @@ internal static class RecreateMissingIndexFiles
return;
await database.UpdateRemoteVolumeAsync(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null).ConfigureAwait(false);
await backendManager.PutAsync(w, null, null, false, taskreader.ProgressToken).ConfigureAwait(false);
await database.CommitTransactionAsync("RecreateMissingIndexFile").ConfigureAwait(false);
await backendManager.PutAsync(w, null, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
@@ -172,7 +172,8 @@ namespace Duplicati.Library.Main.Operation.Backup
await database.AddIndexBlockLinkAsync(indexVolumeCopy.VolumeID, target.BlockVolume.VolumeID).ConfigureAwait(false);
}
await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, taskreader.ProgressToken).ConfigureAwait(false);
await database.CommitTransactionAsync("UploadSpillVolume").ConfigureAwait(false);
await backendManager.PutAsync(target.BlockVolume, indexVolumeCopy, null, false, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false);
}
}
}
@@ -67,7 +67,7 @@ internal static class UploadRealFilelist
await db.UpdateRemoteVolumeAsync(filesetvolume.RemoteFilename, RemoteVolumeState.Uploading, -1, null).ConfigureAwait(false);
await db.CommitTransactionAsync("CommitUpdateRemoteVolume").ConfigureAwait(false);
await backendManager.PutAsync(filesetvolume, null, null, false, taskreader.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(filesetvolume, null, null, false, () => db.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken).ConfigureAwait(false);
}
}
else
@@ -115,8 +115,7 @@ namespace Duplicati.Library.Main.Operation.Backup
if (!string.IsNullOrWhiteSpace(lastTempFilelist.Name) && (lastTempFilelist.State == RemoteVolumeState.Uploading || lastTempFilelist.State == RemoteVolumeState.Temporary))
await database.UpdateRemoteVolumeAsync(lastTempFilelist.Name, RemoteVolumeState.Deleting, -1, null);
await database.CommitTransactionAsync("CommitUpdateFilelistVolume");
await backendManager.PutAsync(fsw, null, null, false, taskreader.ProgressToken);
await backendManager.PutAsync(fsw, null, null, true, () => database.FlushBackendMessagesAndCommitAsync(backendManager), taskreader.ProgressToken);
}
catch
{
@@ -375,7 +375,8 @@ namespace Duplicati.Library.Main.Operation
throw;
}
return new PreBackupVerifyResult(database, lastTempFilelist);
// Return the updated version of the fileset, in case it was actually complete
return new PreBackupVerifyResult(database, database.GetLastIncompleteFilesetVolume(null));
}
catch
{
@@ -594,14 +595,16 @@ namespace Duplicati.Library.Main.Operation
return new AggregateException(ex.First().Message, ex);
}
private static async Task<long> FlushBackend(LocalDatabase database, IDbTransaction transaction, BackupResults result, IBackendManager backendManager)
private static async Task<long> FlushBackend(Backup.BackupDatabase database, BackupResults result, IBackendManager backendManager)
{
// Wait for upload completion
result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_WaitForUpload);
try
{
await backendManager.WaitForEmptyAsync(database, transaction, result.TaskControl.ProgressToken).ConfigureAwait(false);
await database.FlushBackendMessagesAndCommitAsync(backendManager).ConfigureAwait(false);
await backendManager.WaitForEmptyAsync(result.TaskControl.ProgressToken).ConfigureAwait(false);
await database.FlushBackendMessagesAndCommitAsync(backendManager).ConfigureAwait(false);
// Grab the size of the last uploaded volume
return backendManager.LastWriteSize;
}
@@ -709,12 +712,14 @@ namespace Duplicati.Library.Main.Operation
using (new Logging.Timer(LOGTAG, "VerifyConsistency", "VerifyConsistency"))
await db.VerifyConsistencyAsync(m_options.Blocksize, m_options.BlockhashSize, false);
await FlushBackend(db, m_result, backendManager).ConfigureAwait(false);
// Send the actual filelist
await Backup.UploadRealFilelist.Run(m_result, db, backendManager, m_options, filesetvolume, filesetid, m_result.TaskControl, lastTempVolumeIncomplete);
// Wait for upload completion
m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Backup_WaitForUpload);
var lastVolumeSize = await FlushBackend(m_database, null, m_result, backendManager).ConfigureAwait(false);
var lastVolumeSize = await FlushBackend(db, m_result, backendManager).ConfigureAwait(false);
if (!m_options.Dryrun)
database.TerminatedWithActiveUploads = false;
@@ -21,6 +21,7 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Duplicati.Library.Main.Database;
@@ -66,6 +67,15 @@ namespace Duplicati.Library.Main.Operation.Common
return RunOnMain(() => m_db.UpdateRemoteVolume(name, state, size, hash, suppressCleanup, deleteGraceTime, GetTransaction()));
}
public async Task FlushBackendMessagesAndCommitAsync(IBackendManager backendManager)
{
await FlushPendingBackendMessagesAsync(backendManager).ConfigureAwait(false);
await CommitTransactionAsync("FlushBackendMessagesAndCommitAsync").ConfigureAwait(false);
}
private Task FlushPendingBackendMessagesAsync(IBackendManager backendManager)
=> RunOnMain(() => backendManager.FlushPendingMessagesAsync(m_db, GetTransaction(), CancellationToken.None).ConfigureAwait(false));
public Task CommitTransactionAsync(string message, bool restart = true)
{
return RunOnMain(() =>
@@ -183,7 +183,7 @@ namespace Duplicati.Library.Main.Operation
if (newvol.Filesize > (m_options.VolumeSize - m_options.Blocksize))
{
await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes);
await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes, rtr);
newvol = new BlockVolumeWriter(m_options);
newvol.VolumeID = db.RegisterRemoteVolume(newvol.RemoteFilename, RemoteVolumeType.Blocks, RemoteVolumeState.Temporary, rtr.Transaction);
@@ -215,7 +215,7 @@ namespace Duplicati.Library.Main.Operation
if (blocksInVolume > 0)
{
await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes).ConfigureAwait(false);
await FinishVolumeAndUpload(db, backendManager, newvol, newvolindex, uploadedVolumes, rtr).ConfigureAwait(false);
}
else
{
@@ -323,7 +323,7 @@ namespace Duplicati.Library.Main.Operation
yield return d;
}
private async Task FinishVolumeAndUpload(LocalDeleteDatabase db, IBackendManager backendManager, BlockVolumeWriter newvol, IndexVolumeWriter newvolindex, List<KeyValuePair<string, long>> uploadedVolumes)
private async Task FinishVolumeAndUpload(LocalDeleteDatabase db, IBackendManager backendManager, BlockVolumeWriter newvol, IndexVolumeWriter newvolindex, List<KeyValuePair<string, long>> uploadedVolumes, ReusableTransaction rtr)
{
Action indexVolumeFinished = () =>
{
@@ -339,8 +339,16 @@ namespace Duplicati.Library.Main.Operation
uploadedVolumes.Add(new KeyValuePair<string, long>(newvol.RemoteFilename, newvol.Filesize));
if (newvolindex != null)
uploadedVolumes.Add(new KeyValuePair<string, long>(newvolindex.RemoteFilename, newvolindex.Filesize));
// TODO: The upload here does not flush the database messages,
// and this can leave the database in a state where it does not know of the remote file
// To fix it, we need thread-safe access to the daabase and transaction
// Once fixed, we can perhaps let he backend manager simply call the database directly
if (!m_options.Dryrun)
await backendManager.PutAsync(newvol, newvolindex, indexVolumeFinished, false, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
{
rtr.Commit("CommitUpload");
await backendManager.PutAsync(newvol, newvolindex, indexVolumeFinished, false, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
}
else
Logging.Log.WriteDryrunMessage(LOGTAG, "WouldUploadGeneratedBlockset", "Would upload generated blockset of size {0}", Library.Utility.Utility.FormatSizeString(newvol.Filesize));
}
@@ -213,7 +213,11 @@ namespace Duplicati.Library.Main.Operation
db.UpdateRemoteVolume(f.Key, RemoteVolumeState.Deleting, f.Value, null, tr);
tr.Commit();
await backendManager.PutAsync(vol, null, null, true, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
await backendManager.PutAsync(vol, null, null, true, async () =>
{
await backendManager.FlushPendingMessagesAsync(db, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
}, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
await backendManager.DeleteAsync(prevfilename, -1, true, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
await backendManager.WaitForEmptyAsync(db, null, m_result.TaskControl.ProgressToken).ConfigureAwait(false);
}
@@ -350,7 +350,8 @@ namespace Duplicati.Library.Main.Operation
fsw.VolumeID = db.RegisterRemoteVolume(fsw.RemoteFilename, RemoteVolumeType.Files, -1, RemoteVolumeState.Temporary);
db.LinkFilesetToVolume(filesetId, fsw.VolumeID, null);
await backendManager.PutAsync(fsw, null, null, false, cancellationToken).ConfigureAwait(false);
// TODO: Rewrite to use transactions and flush db messages as needed
await backendManager.PutAsync(fsw, null, null, false, null, cancellationToken).ConfigureAwait(false);
}
if (anyDlistUploads)
@@ -415,7 +416,8 @@ namespace Duplicati.Library.Main.Operation
else
{
db.UpdateRemoteVolume(volumeWriter.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
await backendManager.PutAsync(volumeWriter, null, null, false, cancellationToken).ConfigureAwait(false);
// TODO: Rewrite to use transactions and flush db messages as needed
await backendManager.PutAsync(volumeWriter, null, null, false, null, cancellationToken).ConfigureAwait(false);
}
}
else if (n.Type == RemoteVolumeType.Index)
@@ -456,7 +458,8 @@ namespace Duplicati.Library.Main.Operation
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
await backendManager.PutAsync(w, null, null, false, cancellationToken).ConfigureAwait(false);
// TODO: Rewrite to use transactions and flush db messages as needed
await backendManager.PutAsync(w, null, null, false, null, cancellationToken).ConfigureAwait(false);
}
}
else if (n.Type == RemoteVolumeType.Blocks)
@@ -552,7 +555,8 @@ namespace Duplicati.Library.Main.Operation
else
{
db.UpdateRemoteVolume(w.RemoteFilename, RemoteVolumeState.Uploading, -1, null, null);
await backendManager.PutAsync(w, null, null, false, cancellationToken).ConfigureAwait(false);
// TODO: Rewrite to use transactions and flush db messages as needed
await backendManager.PutAsync(w, null, null, false, null, cancellationToken).ConfigureAwait(false);
}
}
}
+39 -6
View File
@@ -898,12 +898,15 @@ namespace Duplicati.UnitTest
using (var c = new Controller("file://" + TARGETFOLDER, testopts.Expand(new { full_remote_verification = true }), null))
TestUtils.AssertResults(c.Test(long.MaxValue));
// If we fail on the dlist after the upload, it is promoted to a regular backup
var expectedFilesets = before || failOnLastDblock ? 2 : 1;
// Make a new backup, should continue as normal
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var backupResults = c.Backup(new string[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
Assert.AreEqual(2, c.List().Filesets.Count());
Assert.AreEqual(expectedFilesets, c.List().Filesets.Count());
}
// Test that we can recreate
@@ -916,12 +919,12 @@ namespace Duplicati.UnitTest
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
TestUtils.AssertResults(c.Repair());
// Check that we have 2 versions
// Check that we have the correct versions
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
var listResults = c.List();
TestUtils.AssertResults(listResults);
Assert.AreEqual(2, listResults.Filesets.Count());
Assert.AreEqual(expectedFilesets, listResults.Filesets.Count());
}
}
@@ -984,12 +987,15 @@ namespace Duplicati.UnitTest
// Make spacing for the next backup
Thread.Sleep(3000);
// If we fail on the dlist after the upload, it is promoted to a regular backup
var expectedFilesets = before || failOnLastDblock ? 2 : 1;
// Make a new backup, should continue as normal
using (var c = new Controller("file://" + TARGETFOLDER, testopts, null))
{
IBackupResults backupResults = c.Backup(new string[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
Assert.AreEqual(2, c.List().Filesets.Count());
Assert.AreEqual(expectedFilesets, c.List().Filesets.Count());
}
// Verify that all is in order
@@ -1011,7 +1017,7 @@ namespace Duplicati.UnitTest
{
var listResults = c.List();
TestUtils.AssertResults(listResults);
Assert.AreEqual(2, listResults.Filesets.Count());
Assert.AreEqual(expectedFilesets, listResults.Filesets.Count());
}
}
@@ -1066,12 +1072,39 @@ namespace Duplicati.UnitTest
[TestCase(false, true, 3, false)]
[TestCase(true, true, 3, true)]
[TestCase(false, true, 3, true)]
[TestCase(false, false, 3, true)]
[TestCase(true, false, 3, true)]
public void TestMultiExceptionOnDlist(bool before, bool modifyInBetween, int runs, bool withBase)
{
var testopts = TestOptions;
testopts["number-of-retries"] = "0";
testopts["dblock-size"] = "10mb";
var expectedFilesets = withBase ? 3 : 2;
// We always have at least 1 backup at the end
var expectedFilesets = 1;
// If the files are modified, this will create multiple versions
if (modifyInBetween)
{
// If we prevent the file from being uploaded,
// it will be a temporary file, and a new synthetic file
// will be attempted, but that fails as well
// If it is after, then the upload completes,
// and it will be promoted to a regular uploaded backup
expectedFilesets += before ? 1 : runs;
// If we modify with a base, then that creates a version as well
if (withBase)
expectedFilesets += 1;
}
else if (before && !withBase)
{
// Because an incomplete fileset is treated as a partial fileset,
// the final non-interrupted backup will see the syntheticly
// created fileset as a partial fileset, and create a new non-partial
// fileset, even though there are no changes between the two
// This does not happen with a base, because it simply does not
// attempt to upload the failing filesets
expectedFilesets += 1;
}
if (withBase)
{