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
117 changes: 101 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 PersistedSessionFileName = ".session";

private readonly object _installationIdLock = new();

private readonly ISystemClock _clock;
private readonly SentryOptions _options;

private string? _cachedInstallationId;
private readonly string? _persistanceDirectoryPath;

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.
_persistanceDirectoryPath = 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,81 @@ public GlobalSessionManager(SentryOptions options)
);
}

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

private void PersistSession(SessionUpdate update, bool isPaused = false)
{
if (string.IsNullOrWhiteSpace(_persistanceDirectoryPath))
{
return;
}

try
{
Directory.CreateDirectory(_persistanceDirectoryPath);

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

private void DeletePersistedSession()
{
if (string.IsNullOrWhiteSpace(_persistanceDirectoryPath))
{
return;
}

try
{
File.Delete(
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
Path.Combine(_persistanceDirectoryPath, PersistedSessionFileName)
);
}
catch (Exception ex)
{
_options.DiagnosticLogger?.LogError(
"Failed to delete persisted session from the file system",
ex
);
}
}

public SessionUpdate? TryRecoverPersistedSession()
{
if (string.IsNullOrWhiteSpace(_persistanceDirectoryPath))
{
return null;
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
}

try
{
var data = File.ReadAllBytes(Path.Combine(_persistanceDirectoryPath, PersistedSessionFileName));
var update = SessionUpdate.FromJson(Json.Parse(data));

// Switch status to abnormal and initial to false
// TODO: crashed for paused sessions
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved
return new SessionUpdate(update, false, SessionEndStatus.Abnormal);
}
catch (Exception ex)
{
_options.DiagnosticLogger?.LogError(
"Failed to recover persisted session from the file system",
ex
);

return null;
}
}

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

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

PersistSession(update);

return update;
}

private SessionUpdate EndSession(Session session, DateTimeOffset timestamp, SessionEndStatus status)
Expand All @@ -213,7 +294,11 @@ 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);

DeletePersistedSession();

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();
}
}
}
2 changes: 2 additions & 0 deletions src/Sentry/ISessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ internal interface ISessionManager
{
bool IsSessionActive { get; }

SessionUpdate? TryRecoverPersistedSession();

SessionUpdate? StartSession();

SessionUpdate? EndSession(DateTimeOffset timestamp, SessionEndStatus status);
Expand Down
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/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class Hub : IHub, IDisposable
private readonly Enricher _enricher;

private DateTimeOffset? _sessionPauseTimestamp;
private int _isPersistedSessionRecovered;

// Internal for testability
internal ConditionalWeakTable<Exception, ISpan> ExceptionToSpanMap { get; } = new();
Expand Down Expand Up @@ -177,6 +178,27 @@ public void BindException(Exception exception, ISpan span)

public void StartSession()
{
// Attempt to recover persisted session left over from previous run
if (Interlocked.Exchange(ref _isPersistedSessionRecovered, 1) != 1)
{
try
{
var recoveredSessionUpdate = _sessionManager.TryRecoverPersistedSession();
if (recoveredSessionUpdate is not null)
{
CaptureSession(recoveredSessionUpdate);
}
}
catch (Exception ex)
{
_options.DiagnosticLogger?.LogError(
"Failed to recover persisted session.",
ex
);
}
}
Tyrrrz marked this conversation as resolved.
Show resolved Hide resolved

// Start a new session
try
{
var sessionUpdate = _sessionManager.StartSession();
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();
}
}
}
12 changes: 10 additions & 2 deletions src/Sentry/SessionUpdate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,21 @@ public class SessionUpdate : ISession, IJsonSerializable
/// <summary>
/// Initializes a new instance of <see cref="SessionUpdate"/>.
/// </summary>
public SessionUpdate(SessionUpdate sessionUpdate, bool isInitial)
public SessionUpdate(SessionUpdate sessionUpdate, bool isInitial, SessionEndStatus? endStatus)
: this(
sessionUpdate,
isInitial,
sessionUpdate.Timestamp,
sessionUpdate.SequenceNumber,
sessionUpdate.EndStatus)
endStatus)
{
}

/// <summary>
/// Initializes a new instance of <see cref="SessionUpdate"/>.
/// </summary>
public SessionUpdate(SessionUpdate sessionUpdate, bool isInitial)
: this(sessionUpdate, isInitial, sessionUpdate.EndStatus)
{
}

Expand Down