Skip to content

Commit 3730654

Browse files
Added auto migration from v5
1 parent efaa83e commit 3730654

File tree

3 files changed

+197
-14
lines changed

3 files changed

+197
-14
lines changed

src/Sentry/Internal/Http/CachingTransport.cs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ namespace Sentry.Internal.Http;
1717
/// </remarks>
1818
internal class CachingTransport : ITransport, IDisposable
1919
{
20+
internal const int ResultMigrationOk = 0;
21+
internal const int ResultMigrationNoBaseCacheDir = 1;
22+
internal const int ResultMigrationAlreadyMigrated = 2;
23+
internal const int ResultMigrationLockNotAcquired = 3;
24+
internal const int ResultMigrationCancelled = 4;
25+
2026
private const string EnvelopeFileExt = "envelope";
2127
private const string ProcessingFolder = "__processing";
2228

@@ -107,7 +113,11 @@ private void Initialize(bool startWorker)
107113
_worker = Task.Run(CachedTransportBackgroundTaskAsync);
108114

109115
// Start a recycler in the background so as not to block initialisation
110-
_recycler = Task.Run(() => SalvageAbandonedCacheSessions(_workerCts.Token), _workerCts.Token);
116+
_recycler = Task.Run(() =>
117+
{
118+
SalvageAbandonedCacheSessions(_workerCts.Token);
119+
MigrateVersion5Cache(_workerCts.Token);
120+
}, _workerCts.Token);
111121
}
112122
else
113123
{
@@ -249,6 +259,65 @@ private void MoveUnprocessedFilesBackToCache()
249259
}
250260
}
251261

262+
/// <summary>
263+
/// When migrating from version 5 to version 6, the cache directory structure changed.
264+
/// In version 5, there were no isolated subdirectories.
265+
/// </summary>
266+
/// <param name="cancellationToken"></param>
267+
/// <returns>An integer return code to facillitate testing</returns>
268+
internal int MigrateVersion5Cache(CancellationToken cancellationToken)
269+
{
270+
if (_options.GetBaseCacheDirectoryPath() is not { } baseCacheDir)
271+
{
272+
return ResultMigrationNoBaseCacheDir;
273+
}
274+
275+
// This code needs to execute only once during the first initialization of version 6.
276+
var markerFile = Path.Combine(baseCacheDir, ".migrated");
277+
if (_fileSystem.FileExists(markerFile))
278+
{
279+
return ResultMigrationAlreadyMigrated;
280+
}
281+
282+
var migrationLock = Path.Combine(baseCacheDir, "migration");
283+
using var coordinator = new CacheDirectoryCoordinator(migrationLock, _options.DiagnosticLogger, _options.FileSystem);
284+
if (!coordinator.TryAcquire())
285+
{
286+
return ResultMigrationLockNotAcquired;
287+
}
288+
289+
const string searchPattern = $"*.{EnvelopeFileExt}";
290+
var processingDirectoryPath = Path.Combine(baseCacheDir, ProcessingFolder);
291+
foreach (var cacheDir in new[] { baseCacheDir, processingDirectoryPath })
292+
{
293+
if (cancellationToken.IsCancellationRequested)
294+
{
295+
return ResultMigrationCancelled; // graceful exit on cancellation
296+
}
297+
298+
if (!_fileSystem.DirectoryExists(cacheDir))
299+
{
300+
continue;
301+
}
302+
303+
foreach (var filePath in _fileSystem.EnumerateFiles(cacheDir, searchPattern, SearchOption.TopDirectoryOnly))
304+
{
305+
if (cancellationToken.IsCancellationRequested)
306+
{
307+
return ResultMigrationCancelled; // graceful exit on cancellation
308+
}
309+
310+
var destinationPath = Path.Combine(_isolatedCacheDirectoryPath, Path.GetFileName(filePath));
311+
_options.LogDebug("Migrating version 5 cache file: {0} to {1}.", filePath, destinationPath);
312+
MoveFileWithRetries(filePath, destinationPath, "version 5");
313+
}
314+
}
315+
316+
// Leave a marker file so we don't try to migrate again
317+
_fileSystem.WriteAllTextToFile(markerFile, "6.0.0");
318+
return ResultMigrationOk;
319+
}
320+
252321
private void MoveFileWithRetries(string filePath, string destinationPath, string moveType)
253322
{
254323
const int maxAttempts = 3;
@@ -257,6 +326,7 @@ private void MoveFileWithRetries(string filePath, string destinationPath, string
257326
try
258327
{
259328
_fileSystem.MoveFile(filePath, destinationPath);
329+
260330
break;
261331
}
262332
catch (Exception ex)

test/Sentry.Tests/Internals/CacheDirectoryCoordinatorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public void TryAcquire_Locked_Fails()
6262
}
6363

6464
[Fact]
65-
public void TryAcquireMultiple_Released_Succeeds()
65+
public void TryAcquire_AfterReleasedSut_Succeeds()
6666
{
6767
// Arrange
6868
var c1 = _fixture.GetSut();

test/Sentry.Tests/Internals/Http/CachingTransportTests.cs

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.IO.Abstractions.TestingHelpers;
21
using Sentry.Internal.Http;
32

43
namespace Sentry.Tests.Internals.Http;
@@ -644,7 +643,7 @@ public async Task SalvageAbandonedCacheSessions_MovesEnvelopesFromOtherIsolatedD
644643
var baseCacheDir = Directory.GetParent(currentIsolated)!.FullName;
645644

646645
// Create two abandoned isolated cache directories with envelope files (including in nested folder)
647-
string CreateAbandonedDir(int index)
646+
void CreateAbandonedDir(int index)
648647
{
649648
var abandoned = Path.Combine(baseCacheDir, $"isolated_abandoned_{index}");
650649
_options.FileSystem.CreateDirectory(abandoned);
@@ -653,17 +652,10 @@ string CreateAbandonedDir(int index)
653652

654653
// root file
655654
var rootFile = Path.Combine(abandoned, $"root_{index}.envelope");
656-
if (_options.FileSystem.CreateFileForWriting(rootFile, out var s1))
657-
{
658-
s1.Dispose();
659-
}
655+
_options.FileSystem.WriteAllTextToFile(rootFile, "dummy content");
660656
// nested file
661657
var nestedFile = Path.Combine(nested, $"nested_{index}.envelope");
662-
if (_options.FileSystem.CreateFileForWriting(nestedFile, out var s2))
663-
{
664-
s2.Dispose();
665-
}
666-
return abandoned;
658+
_options.FileSystem.WriteAllTextToFile(nestedFile, "dummy content");
667659
}
668660

669661
CreateAbandonedDir(1);
@@ -699,7 +691,7 @@ public async Task SalvageAbandonedCacheSessions_IgnoresCurrentDirectory()
699691
var currentFile = Path.Combine(currentIsolated, "current.envelope");
700692
if (_options.FileSystem.CreateFileForWriting(currentFile, out var stream))
701693
{
702-
stream.Dispose();
694+
await stream.DisposeAsync();
703695
}
704696
_options.FileSystem.FileExists(currentFile).Should().BeTrue();
705697

@@ -745,4 +737,125 @@ public async Task SalvageAbandonedCacheSessions_SkipsDirectoriesWithActiveLock()
745737
moved.Should().BeFalse();
746738
}
747739
}
740+
741+
[Fact]
742+
public async Task MigrateVersion5Cache_MovesEnvelopesFromBaseAndProcessing()
743+
{
744+
// Arrange
745+
using var innerTransport = new FakeTransport();
746+
await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false);
747+
748+
var isolatedCacheDir = _options.GetIsolatedCacheDirectoryPath()!;
749+
750+
var baseCacheDir = _options.GetBaseCacheDirectoryPath()!;
751+
var rootFile = Path.Combine(baseCacheDir, "v5_root.envelope");
752+
_options.FileSystem.CreateDirectory(baseCacheDir);
753+
_options.FileSystem.WriteAllTextToFile(rootFile, "dummy content");
754+
755+
var processingDir = Path.Combine(baseCacheDir, "__processing");
756+
var procFile = Path.Combine(processingDir, "v5_proc.envelope");
757+
_options.FileSystem.CreateDirectory(processingDir);
758+
_options.FileSystem.WriteAllTextToFile(procFile, "dummy content");
759+
760+
// Act
761+
transport.MigrateVersion5Cache(CancellationToken.None);
762+
763+
// Assert
764+
var markerFile = Path.Combine(baseCacheDir, ".migrated");
765+
_options.FileSystem.FileExists(markerFile).Should().BeTrue();
766+
_options.FileSystem.FileExists(rootFile).Should().BeFalse();
767+
_options.FileSystem.FileExists(procFile).Should().BeFalse();
768+
769+
var movedRoot = Path.Combine(isolatedCacheDir, "v5_root.envelope");
770+
var movedProc = Path.Combine(isolatedCacheDir, "v5_proc.envelope");
771+
_options.FileSystem.FileExists(movedRoot).Should().BeTrue();
772+
_options.FileSystem.FileExists(movedProc).Should().BeTrue();
773+
}
774+
775+
[Fact]
776+
public async Task MigrateVersion5Cache_AlreadyMigrated_Skipped()
777+
{
778+
// Arrange
779+
using var innerTransport = new FakeTransport();
780+
await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false);
781+
782+
var baseCacheDir = _options.GetBaseCacheDirectoryPath()!;
783+
var rootFile = Path.Combine(baseCacheDir, "v5_root.envelope");
784+
_options.FileSystem.CreateDirectory(baseCacheDir);
785+
_options.FileSystem.WriteAllTextToFile(rootFile, "dummy content");
786+
787+
var processingDir = Path.Combine(baseCacheDir, "__processing");
788+
var procFile = Path.Combine(processingDir, "v5_proc.envelope");
789+
_options.FileSystem.CreateDirectory(processingDir);
790+
_options.FileSystem.WriteAllTextToFile(procFile, "dummy content");
791+
792+
var marker = Path.Combine(baseCacheDir, ".migrated");
793+
_options.FileSystem.WriteAllTextToFile(marker, "6.0.0");
794+
795+
// Act
796+
var result = transport.MigrateVersion5Cache(CancellationToken.None);
797+
798+
// Assert
799+
result.Should().Be(CachingTransport.ResultMigrationAlreadyMigrated);
800+
_options.FileSystem.FileExists(rootFile).Should().BeTrue();
801+
_options.FileSystem.FileExists(procFile).Should().BeTrue();
802+
}
803+
804+
[Fact]
805+
public async Task MigrateVersion5Cache_MigrationLockHeld_Skipped()
806+
{
807+
// Arrange
808+
using var innerTransport = new FakeTransport();
809+
await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false);
810+
811+
var baseCacheDir = _options.GetBaseCacheDirectoryPath()!;
812+
var rootFile = Path.Combine(baseCacheDir, "v5_root.envelope");
813+
_options.FileSystem.CreateDirectory(baseCacheDir);
814+
_options.FileSystem.WriteAllTextToFile(rootFile, "dummy content");
815+
816+
var processingDir = Path.Combine(baseCacheDir, "__processing");
817+
var procFile = Path.Combine(processingDir, "v5_proc.envelope");
818+
_options.FileSystem.CreateDirectory(processingDir);
819+
_options.FileSystem.WriteAllTextToFile(procFile, "dummy content");
820+
821+
var migrationLockPath = Path.Combine(baseCacheDir, "migration");
822+
using var coordinator = new CacheDirectoryCoordinator(migrationLockPath, _options.DiagnosticLogger, _options.FileSystem);
823+
coordinator.TryAcquire().Should().BeTrue("test must hold the migration lock");
824+
825+
// Act
826+
var result = transport.MigrateVersion5Cache(CancellationToken.None);
827+
828+
// Assert
829+
result.Should().Be(CachingTransport.ResultMigrationLockNotAcquired);
830+
_options.FileSystem.FileExists(rootFile).Should().BeTrue();
831+
_options.FileSystem.FileExists(procFile).Should().BeTrue();
832+
}
833+
834+
[Fact]
835+
public async Task MigrateVersion5Cache_Cancelled_Skipped()
836+
{
837+
// Arrange
838+
using var innerTransport = new FakeTransport();
839+
await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false);
840+
841+
var baseCacheDir = _options.GetBaseCacheDirectoryPath()!;
842+
var rootFile = Path.Combine(baseCacheDir, "v5_root.envelope");
843+
_options.FileSystem.CreateDirectory(baseCacheDir);
844+
_options.FileSystem.WriteAllTextToFile(rootFile, "dummy content");
845+
846+
var processingDir = Path.Combine(baseCacheDir, "__processing");
847+
var procFile = Path.Combine(processingDir, "v5_proc.envelope");
848+
_options.FileSystem.CreateDirectory(processingDir);
849+
_options.FileSystem.WriteAllTextToFile(procFile, "dummy content");
850+
851+
// Act
852+
using var cts = new CancellationTokenSource();
853+
await cts.CancelAsync();
854+
var result = transport.MigrateVersion5Cache(cts.Token);
855+
856+
// Assert
857+
result.Should().Be(CachingTransport.ResultMigrationCancelled);
858+
_options.FileSystem.FileExists(rootFile).Should().BeTrue();
859+
_options.FileSystem.FileExists(procFile).Should().BeTrue();
860+
}
748861
}

0 commit comments

Comments
 (0)