Files
duplicati/Duplicati/UnitTest/CompactDisruptionTests.cs
T
Kenneth Skovhede 179d1ceb47 Make controller Async
This PR has a large blast radius because it takes the final step and bumps up the Controller to be fully async.

We have historically done a piece-by-piece update, so all operations were already async but the controller interface was kept synchronous.

With this update, the controller is now fully async and all tests are updated.

Most places where the new C# compiler warns about function names not ending in `Async` were also adressed, giving a massive refactor change.

Functionally, no changes are done.
2026-05-13 15:04:13 +02:00

610 lines
28 KiB
C#

// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Duplicati.Library.Interface;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest
{
public class CompactDisruptionTests : BasicSetupHelper
{
private string VerificationToString(IEnumerable<KeyValuePair<string, IEnumerable<KeyValuePair<TestEntryStatus, string>>>> verifications)
{
StringBuilder res = new StringBuilder();
foreach (var file in verifications)
{
if (file.Value.Any())
{
res.AppendLine($"{file.Key}: {file.Value.Count()} errors");
foreach (var c in file.Value)
{
res.AppendLine($"\t{c.Key}: {c.Value}");
}
}
}
return res.ToString();
}
/// <summary>
/// Each backup run needs a unique timestamp to avoid the same backup being created.
/// During testing we often need to run backups in quick succession, so we need to
/// sleep until the next second to make sure the next backup timestamp is not the same.
/// </summary>
/// <param name="prevTimestamp">The timestamp of the previous backup</param>
private void SleepUntilNextSecond(DateTime prevTimestamp)
{
// Sleep until the next second to make sure the next backup is not the same
var nextSecond = prevTimestamp.AddMilliseconds(-prevTimestamp.Millisecond).AddSeconds(2);
var remainder = Math.Max(2000, (nextSecond - DateTime.Now).TotalMilliseconds);
if (remainder > 0)
{
System.Threading.Thread.Sleep(TimeSpan.FromMilliseconds(remainder));
}
}
/// <summary>
/// Each backup run needs a unique timestamp to avoid the same backup being created.
/// During testing we often need to run backups in quick succession, so we need to
/// sleep until the next second to make sure the next backup timestamp is not the same.
/// </summary>
/// <param name="backupResults">The results of the previous backup</param>
private void SleepUntilNextSecond(IBackupResults backupResults)
=> SleepUntilNextSecond(backupResults.BeginTime);
[Test]
[Category("Disruption"), Category("Bug")]
public async Task InterruptedCompact5184Async()
{
// Reproduction steps from issue #4129 with smaller sizes
var testopts = TestOptions;
testopts["backup-test-samples"] = "0";
testopts["number-of-retries"] = "0";
testopts["dblock-size"] = "20KB";
// Reduce blocksize, because with default blocksize too few blocks fit in a volume
testopts["blocksize"] = "1KB";
testopts["keep-versions"] = "1";
testopts["no-auto-compact"] = "true";
testopts["threshold"] = "5";
// This size seems to be appropriate to cause significant compacting
// in combination with dblock-size
const long filesize = 44971;
var target = "file://" + TARGETFOLDER;
var file1 = Path.Combine(DATAFOLDER, "f1");
var file2 = Path.Combine(DATAFOLDER, "f2");
var file3 = Path.Combine(DATAFOLDER, "f3");
var file4 = Path.Combine(DATAFOLDER, "f4");
var file5 = Path.Combine(DATAFOLDER, "f5");
var file6 = Path.Combine(DATAFOLDER, "f6");
// Add files 1,2
TestUtils.WriteTestFile(file1, filesize);
TestUtils.WriteTestFile(file2, filesize);
using (var c = new Library.Main.Controller(target, testopts, null))
{
var backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 3,4
TestUtils.WriteTestFile(file3, filesize);
TestUtils.WriteTestFile(file4, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 5,6
TestUtils.WriteTestFile(file5, filesize);
TestUtils.WriteTestFile(file6, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Delete 1,3,5
File.Delete(file1);
File.Delete(file3);
File.Delete(file5);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
}
// Deterministic error backend
Library.DynamicLoader.BackendLoader.AddBackend(new DeterministicErrorBackend());
target = new DeterministicErrorBackend().ProtocolKey + "://" + TARGETFOLDER;
// Fail the compact after the first dblock put is completed
bool firstPutCompleted = false;
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
if (firstPutCompleted && action.IsGetOperation)
{
return true;
}
if (action == DeterministicErrorBackend.BackendAction.PutAfter)
{
firstPutCompleted = true;
}
return false;
};
// Expect error from backend
Assert.CatchAsync<DeterministicErrorBackend.DeterministicErrorBackendException>(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
});
DeterministicErrorBackend.ErrorGenerator = null;
testopts["full-remote-verification"] = "true";
testopts["full-block-verification"] = "true";
target = "file://" + TARGETFOLDER;
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Remove the local database
File.Delete(DBFILE);
// Repair to recreate the local database
using (var c = new Library.Main.Controller(target, testopts, null))
{
var repairResults = await c.RepairAsync();
TestUtils.AssertResults(repairResults);
}
// Re-do the full verification
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
}
[Test]
[Category("Disruption"), Category("Bug")]
public async Task InterruptedCompactPlusNormalCompact5184Async()
{
// Reproduction steps from issue #4129 with smaller sizes
var testopts = TestOptions;
testopts["backup-test-samples"] = "0";
testopts["number-of-retries"] = "0";
testopts["dblock-size"] = "20KB";
// Reduce blocksize, because with default blocksize too few blocks fit in a volume
testopts["blocksize"] = "1KB";
testopts["keep-versions"] = "1";
testopts["no-auto-compact"] = "true";
testopts["threshold"] = "5";
// This size seems to be appropriate to cause significant compacting
// in combination with dblock-size
const long filesize = 44971;
var target = "file://" + TARGETFOLDER;
var file1 = Path.Combine(DATAFOLDER, "f1");
var file2 = Path.Combine(DATAFOLDER, "f2");
var file3 = Path.Combine(DATAFOLDER, "f3");
var file4 = Path.Combine(DATAFOLDER, "f4");
var file5 = Path.Combine(DATAFOLDER, "f5");
var file6 = Path.Combine(DATAFOLDER, "f6");
// Add files 1,2
TestUtils.WriteTestFile(file1, filesize);
TestUtils.WriteTestFile(file2, filesize);
using (var c = new Library.Main.Controller(target, testopts, null))
{
var backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 3,4
TestUtils.WriteTestFile(file3, filesize);
TestUtils.WriteTestFile(file4, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 5,6
TestUtils.WriteTestFile(file5, filesize);
TestUtils.WriteTestFile(file6, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Delete 1,3,5
File.Delete(file1);
File.Delete(file3);
File.Delete(file5);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
}
// Deterministic error backend
Library.DynamicLoader.BackendLoader.AddBackend(new DeterministicErrorBackend());
target = new DeterministicErrorBackend().ProtocolKey + "://" + TARGETFOLDER;
// Fail the compact after the first dblock put is completed
bool firstPutCompleted = false;
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
if (firstPutCompleted && action.IsGetOperation)
{
return true;
}
if (action == DeterministicErrorBackend.BackendAction.PutAfter)
{
firstPutCompleted = true;
}
return false;
};
// Expect error from backend
Assert.CatchAsync<DeterministicErrorBackend.DeterministicErrorBackendException>(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
});
DeterministicErrorBackend.ErrorGenerator = null;
testopts["full-remote-verification"] = "true";
testopts["full-block-verification"] = "true";
target = "file://" + TARGETFOLDER;
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Make sure we can compact after the interrupted compact
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
// Make sure there are no errors after success compacting
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Remove the local database
File.Delete(DBFILE);
// Repair to recreate the local database
using (var c = new Library.Main.Controller(target, testopts, null))
{
var repairResults = await c.RepairAsync();
TestUtils.AssertResults(repairResults);
}
// Re-do the full verification
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
}
[Test]
[Category("Disruption"), Category("Bug")]
public async Task DoubleInterruptedCompact5184Async()
{
// Reproduction steps from issue #4129 with smaller sizes
var testopts = TestOptions;
testopts["backup-test-samples"] = "0";
testopts["number-of-retries"] = "0";
testopts["dblock-size"] = "20KB";
// Reduce blocksize, because with default blocksize too few blocks fit in a volume
testopts["blocksize"] = "1KB";
testopts["keep-versions"] = "1";
testopts["no-auto-compact"] = "true";
testopts["threshold"] = "5";
// This size seems to be appropriate to cause significant compacting
// in combination with dblock-size
const long filesize = 44971;
string target = "file://" + TARGETFOLDER;
string file1 = Path.Combine(DATAFOLDER, "f1");
string file2 = Path.Combine(DATAFOLDER, "f2");
string file3 = Path.Combine(DATAFOLDER, "f3");
string file4 = Path.Combine(DATAFOLDER, "f4");
string file5 = Path.Combine(DATAFOLDER, "f5");
string file6 = Path.Combine(DATAFOLDER, "f6");
// Add files 1,2
TestUtils.WriteTestFile(file1, filesize);
TestUtils.WriteTestFile(file2, filesize);
using (var c = new Library.Main.Controller(target, testopts, null))
{
var backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 3,4
TestUtils.WriteTestFile(file3, filesize);
TestUtils.WriteTestFile(file4, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 5,6
TestUtils.WriteTestFile(file5, filesize);
TestUtils.WriteTestFile(file6, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Delete 1,3,5
File.Delete(file1);
File.Delete(file3);
File.Delete(file5);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
}
// Deterministic error backend
Library.DynamicLoader.BackendLoader.AddBackend(new DeterministicErrorBackend());
target = new DeterministicErrorBackend().ProtocolKey + "://" + TARGETFOLDER;
// Fail the compact after the first dblock put is completed
bool firstPutCompleted = false;
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
if (firstPutCompleted && action.IsGetOperation)
{
return true;
}
if (action == DeterministicErrorBackend.BackendAction.PutAfter)
{
firstPutCompleted = true;
}
return false;
};
// Expect error from backend
Assert.CatchAsync<DeterministicErrorBackend.DeterministicErrorBackendException>(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
});
// Expect error from backend, again
Assert.CatchAsync<DeterministicErrorBackend.DeterministicErrorBackendException>(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
});
DeterministicErrorBackend.ErrorGenerator = null;
testopts["full-remote-verification"] = "true";
testopts["full-block-verification"] = "true";
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Make sure we can compact after two interrupted compacts
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
// Make sure there are no errors after success compacting
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Remove the local database
File.Delete(DBFILE);
// Repair to recreate the local database
using (var c = new Library.Main.Controller(target, testopts, null))
{
var repairResults = await c.RepairAsync();
TestUtils.AssertResults(repairResults);
}
// Re-do the full verification
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
}
[Test]
[Category("Disruption"), Category("Bug")]
public async Task RestoreAfterDoubleInterruptedCompact5184Async()
{
// Reproduction steps from issue #4129 with smaller sizes
var testopts = TestOptions;
testopts["backup-test-samples"] = "0";
testopts["number-of-retries"] = "0";
testopts["dblock-size"] = "20KB";
// Reduce blocksize, because with default blocksize too few blocks fit in a volume
testopts["blocksize"] = "1KB";
testopts["keep-versions"] = "1";
testopts["no-auto-compact"] = "true";
testopts["threshold"] = "5";
// This size seems to be appropriate to cause significant compacting
// in combination with dblock-size
const long filesize = 44971;
var target = "file://" + TARGETFOLDER;
var file1 = Path.Combine(DATAFOLDER, "f1");
var file2 = Path.Combine(DATAFOLDER, "f2");
var file3 = Path.Combine(DATAFOLDER, "f3");
var file4 = Path.Combine(DATAFOLDER, "f4");
var file5 = Path.Combine(DATAFOLDER, "f5");
var file6 = Path.Combine(DATAFOLDER, "f6");
// Add files 1,2
TestUtils.WriteTestFile(file1, filesize);
TestUtils.WriteTestFile(file2, filesize);
using (var c = new Library.Main.Controller(target, testopts, null))
{
var backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 3,4
TestUtils.WriteTestFile(file3, filesize);
TestUtils.WriteTestFile(file4, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Add files 5,6
TestUtils.WriteTestFile(file5, filesize);
TestUtils.WriteTestFile(file6, filesize);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
SleepUntilNextSecond(backupResults);
// Delete 1,3,5
File.Delete(file1);
File.Delete(file3);
File.Delete(file5);
backupResults = await c.BackupAsync(new[] { DATAFOLDER });
TestUtils.AssertResults(backupResults);
}
// Deterministic error backend
Library.DynamicLoader.BackendLoader.AddBackend(new DeterministicErrorBackend());
target = new DeterministicErrorBackend().ProtocolKey + "://" + TARGETFOLDER;
// Fail the compact after the first dblock put is completed
bool firstPutCompleted = false;
DeterministicErrorBackend.ErrorGenerator = (DeterministicErrorBackend.BackendAction action, string remotename) =>
{
if (firstPutCompleted && action.IsGetOperation)
{
return true;
}
if (action == DeterministicErrorBackend.BackendAction.PutAfter)
{
firstPutCompleted = true;
}
return false;
};
// Expect error from backend
Assert.CatchAsync<DeterministicErrorBackend.DeterministicErrorBackendException>(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
});
// Expect error from backend, again
Assert.CatchAsync<DeterministicErrorBackend.DeterministicErrorBackendException>(async () =>
{
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
Assert.Greater(compactResults.DownloadedFileCount, 0, "No compact operation was performed");
}
});
DeterministicErrorBackend.ErrorGenerator = null;
target = "file://" + TARGETFOLDER;
testopts["full-remote-verification"] = "true";
testopts["full-block-verification"] = "true";
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Remove the local database
File.Delete(DBFILE);
// Repair to recreate the local database
using (var c = new Library.Main.Controller(target, testopts, null))
{
var repairResults = await c.RepairAsync();
TestUtils.AssertResults(repairResults);
}
// Re-do the full verification
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
// Compact without issues on the repaired database
using (var c = new Library.Main.Controller(target, testopts, null))
{
var compactResults = await c.CompactAsync();
if (compactResults.DownloadedFileCount == 0)
Assert.Inconclusive("No compact operation was performed");
}
// Make sure there are no errors after success compacting
using (var c = new Library.Main.Controller(target, testopts, null))
{
var testResults = await c.TestAsync(long.MaxValue);
// Expect no verification errors
Assert.IsTrue(testResults.Verifications.All(v => !v.Value.Any()),
"Test verification failed:\n {0}", VerificationToString(testResults.Verifications));
}
}
}
}