Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File system persistence for sessions #1105

Merged
merged 20 commits into from
Jul 14, 2021
76 changes: 60 additions & 16 deletions src/Sentry/GlobalSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ namespace Sentry
// AKA client mode
internal class GlobalSessionManager : ISessionManager
{
private const string CacheFileName = ".session";

private readonly object _installationIdLock = new();

private readonly ISystemClock _clock;
private readonly SentryOptions _options;

private string? _cachedInstallationId;
private readonly string? _cacheDirectoryPath;

private string? _resolvedInstallationId;
private Session? _currentSession;

// Internal for testing
Expand All @@ -29,6 +33,10 @@ public GlobalSessionManager(SentryOptions options, ISystemClock clock)
{
_options = options;
_clock = clock;

// TODO: session file should really be process-isolated, but we
// don't have a proper mechanism for that right now.
_cacheDirectoryPath = options.TryGetDsnSpecificCacheDirectoryPath();
}

public GlobalSessionManager(SentryOptions options)
Expand All @@ -40,14 +48,9 @@ public GlobalSessionManager(SentryOptions options)
{
try
{
var directoryPath = Path.Combine(
// Store in cache directory or fall back to appdata
!string.IsNullOrWhiteSpace(_options.CacheDirectoryPath)
? _options.CacheDirectoryPath
: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
// Put under "Sentry" subdirectory
"Sentry"
);
var directoryPath =
_options.TryGetDsnSpecificCacheDirectoryPath() ??
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Sentry");

Directory.CreateDirectory(directoryPath);

Expand Down Expand Up @@ -127,9 +130,9 @@ public GlobalSessionManager(SentryOptions options)
private string? TryGetInstallationId()
{
// Installation ID could have already been resolved by this point
if (!string.IsNullOrWhiteSpace(_cachedInstallationId))
if (!string.IsNullOrWhiteSpace(_resolvedInstallationId))
{
return _cachedInstallationId;
return _resolvedInstallationId;
}

// Resolve installation ID in a locked manner to guarantee consistency because ID can be non-deterministic.
Expand All @@ -138,9 +141,9 @@ public GlobalSessionManager(SentryOptions options)
{
// We may have acquired the lock after another thread has already resolved
// installation ID, so check the cache one more time before proceeding with I/O.
if (!string.IsNullOrWhiteSpace(_cachedInstallationId))
if (!string.IsNullOrWhiteSpace(_resolvedInstallationId))
{
return _cachedInstallationId;
return _resolvedInstallationId;
}

var id =
Expand All @@ -161,7 +164,7 @@ public GlobalSessionManager(SentryOptions options)
);
}

return _cachedInstallationId = id;
return _resolvedInstallationId = id;
}
}

Expand Down Expand Up @@ -203,7 +206,29 @@ public GlobalSessionManager(SentryOptions options)
session.Id, session.DistinctId
);

return session.CreateUpdate(true, _clock.GetUtcNow());
var update = session.CreateUpdate(true, _clock.GetUtcNow());

if (!string.IsNullOrWhiteSpace(_cacheDirectoryPath))
{
try
{
Directory.CreateDirectory(_cacheDirectoryPath);

File.WriteAllBytes(
Path.Combine(_cacheDirectoryPath, CacheFileName),
update.WriteToMemory()
);
}
catch (Exception ex)
{
_options.DiagnosticLogger?.LogError(
"Failed to persist session on the file system",
ex
);
}
}

return update;
}

private SessionUpdate EndSession(Session session, DateTimeOffset timestamp, SessionEndStatus status)
Expand All @@ -213,7 +238,26 @@ private SessionUpdate EndSession(Session session, DateTimeOffset timestamp, Sess
session.Id, session.DistinctId, status
);

return session.CreateUpdate(false, timestamp, status);
var update = session.CreateUpdate(false, timestamp, status);

if (!string.IsNullOrWhiteSpace(_cacheDirectoryPath))
{
try
{
File.Delete(
Path.Combine(_cacheDirectoryPath, CacheFileName)
);
}
catch (Exception ex)
{
_options.DiagnosticLogger?.LogError(
"Failed to delete persisted session from the file system",
ex
);
}
}

return update;
}

public SessionUpdate? EndSession(DateTimeOffset timestamp, SessionEndStatus status)
Expand Down
15 changes: 15 additions & 0 deletions src/Sentry/IJsonSerializable.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.IO;
using System.Text.Json;

namespace Sentry
Expand All @@ -16,4 +17,18 @@ public interface IJsonSerializable
/// </remarks>
void WriteTo(Utf8JsonWriter writer);
}

internal static class JsonSerializableExtensions
{
public static byte[] WriteToMemory(this IJsonSerializable serializable)
{
using var buffer = new MemoryStream();
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
using var writer = new Utf8JsonWriter(buffer);

serializable.WriteTo(writer);
writer.Flush();

return buffer.ToArray();
}
}
}
11 changes: 3 additions & 8 deletions src/Sentry/Internal/Http/CachingTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Threading;
using System.Threading.Tasks;
using Sentry.Extensibility;
using Sentry.Internal.Extensions;
using Sentry.Protocol.Envelopes;

namespace Sentry.Internal.Http
Expand Down Expand Up @@ -48,13 +47,9 @@ public CachingTransport(ITransport innerTransport, SentryOptions options)
? _options.MaxCacheItems - 1
: 0; // just in case MaxCacheItems is set to an invalid value somehow (shouldn't happen)

_isolatedCacheDirectoryPath = !string.IsNullOrWhiteSpace(options.CacheDirectoryPath)
? _isolatedCacheDirectoryPath = Path.Combine(
options.CacheDirectoryPath,
"Sentry",
options.Dsn?.GetHashString() ?? "no-dsn"
)
: throw new InvalidOperationException("Cache directory is not set.");
_isolatedCacheDirectoryPath =
options.TryGetProcessSpecificCacheDirectoryPath() ??
throw new InvalidOperationException("Cache directory is not set.");

_processingDirectoryPath = Path.Combine(_isolatedCacheDirectoryPath, "__processing");

Expand Down
22 changes: 22 additions & 0 deletions src/Sentry/SentryOptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Sentry.Extensibility;
using Sentry.Infrastructure;
using Sentry.Integrations;
using Sentry.Internal;
using Sentry.Internal.Extensions;
#if NET461
using Sentry.PlatformAbstractions;
#endif
Expand Down Expand Up @@ -236,5 +238,25 @@ internal static void SetupLogging(this SentryOptions options)
options.DiagnosticLogger = null;
}
}

internal static string? TryGetDsnSpecificCacheDirectoryPath(this SentryOptions options)
{
if (string.IsNullOrWhiteSpace(options.CacheDirectoryPath))
{
return null;
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
}

return Path.Combine(
options.CacheDirectoryPath,
"Sentry",
options.Dsn?.GetHashString() ?? "no-dsn"
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
);
}

internal static string? TryGetProcessSpecificCacheDirectoryPath(this SentryOptions options)
{
// In the future, this will most likely contain process ID
return options.TryGetDsnSpecificCacheDirectoryPath();
}
}
}