Skip to content

Commit 172bf5e

Browse files
authored
feat: Bump to Sentry SDK for .NET 6 (#2425)
1 parent ad92e4d commit 172bf5e

File tree

5 files changed

+159
-75
lines changed

5 files changed

+159
-75
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22

33
## Unreleased
44

5+
## Fixes
6+
7+
- Fixed race conditions that could cause crashes when targeting Android, especially in concurrent scenarios. The ThreadPool used to sync scope to the native layer could prematurely detach threads from the JVM. This is solved by using a dedicated background worker thread that properly manages JNI lifecycle. ([#2426](https://github.com/getsentry/sentry-unity/pull/2426))
8+
9+
### Bump to Sentry SDK for .NET v6.0.0-rc.1
10+
11+
#### Features
12+
13+
- The SDK now provides an `IsSessionActive` property to allow checking the session state ([#4662](https://github.com/getsentry/sentry-dotnet/pull/4662))
14+
- Implemented instance isolation so that multiple instances of the Sentry SDK can be instantiated inside the same process when using the Caching Transport ([#4498](https://github.com/getsentry/sentry-dotnet/pull/4498))
15+
16+
#### Fixes
17+
18+
- Fixed memory leak when finishing an unsampled Transaction that has started unsampled Spans ([#4717](https://github.com/getsentry/sentry-dotnet/pull/4717))
19+
- The SDK now avoids redundant scope sync after transaction finish ([#4623](https://github.com/getsentry/sentry-dotnet/pull/4623))
20+
521
### Dependencies
622

723
- Bump Java SDK from v8.25.0 to v8.26.0 ([#2419](https://github.com/getsentry/sentry-unity/pull/2419))
@@ -10,6 +26,9 @@
1026
- Bump CLI from v2.58.1 to v2.58.2 ([#2418](https://github.com/getsentry/sentry-unity/pull/2418))
1127
- [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2582)
1228
- [diff](https://github.com/getsentry/sentry-cli/compare/2.58.1...2.58.2)
29+
- Bump Cocoa SDK from v8.57.2 to v8.57.3 ([#4704](https://github.com/getsentry/sentry-dotnet/pull/4704), [#4738](https://github.com/getsentry/sentry-dotnet/pull/4738))
30+
- [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8573)
31+
- [diff](https://github.com/getsentry/sentry-cocoa/compare/8.57.2...8.57.3)
1332

1433
## 4.0.0-beta.5
1534

global.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"sdk": {
3-
"version": "10.0.100-rc.2.25502.107",
4-
"workloadVersion": "10.0.100-rc.2.25513.4",
3+
"version": "10.0.100",
4+
"workloadVersion": "10.0.100",
55
"rollForward": "disable",
66
"allowPrerelease": false
77
}

src/Sentry.Unity.Android/SentryJava.cs

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Runtime.CompilerServices;
34
using System.Threading;
45
using Sentry.Extensibility;
@@ -54,13 +55,24 @@ internal class SentryJava : ISentryJava
5455
private readonly IAndroidJNI _androidJNI;
5556
private readonly IDiagnosticLogger? _logger;
5657

58+
private readonly CancellationTokenSource _scopeSyncShutdownSource;
59+
private readonly AutoResetEvent _scopeSyncEvent;
60+
private readonly Thread _scopeSyncThread;
61+
private readonly ConcurrentQueue<(Action action, string actionName)> _scopeSyncItems = new();
62+
private volatile bool _closed;
63+
5764
private static AndroidJavaObject GetInternalSentryJava() => new AndroidJavaClass("io.sentry.android.core.InternalSentrySdk");
58-
protected virtual AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry");
65+
private static AndroidJavaObject GetSentryJava() => new AndroidJavaClass("io.sentry.Sentry");
5966

6067
public SentryJava(IDiagnosticLogger? logger, IAndroidJNI? androidJNI = null)
6168
{
6269
_logger = logger;
6370
_androidJNI ??= androidJNI ?? AndroidJNIAdapter.Instance;
71+
72+
_scopeSyncEvent = new AutoResetEvent(false);
73+
_scopeSyncShutdownSource = new CancellationTokenSource();
74+
_scopeSyncThread = new Thread(SyncScope) { IsBackground = true, Name = "SentryScopeSyncWorkerThread" };
75+
_scopeSyncThread.Start();
6476
}
6577

6678
public bool? IsEnabled()
@@ -197,25 +209,6 @@ public void Init(SentryUnityOptions options)
197209
return null;
198210
}
199211

200-
public void Close()
201-
{
202-
if (!MainThreadData.IsMainThread())
203-
{
204-
_logger?.LogError("Calling Close() on Android SDK requires running on MainThread");
205-
return;
206-
}
207-
208-
try
209-
{
210-
using var sentry = GetSentryJava();
211-
sentry.CallStatic("close");
212-
}
213-
catch (Exception e)
214-
{
215-
_logger?.LogError(e, "Calling 'SentryJava.Close' failed.");
216-
}
217-
}
218-
219212
public void WriteScope(
220213
int? GpuId,
221214
string? GpuName,
@@ -365,6 +358,12 @@ public void SetTrace(SentryId traceId, SpanId spanId)
365358

366359
internal void RunJniSafe(Action action, [CallerMemberName] string actionName = "", bool? isMainThread = null)
367360
{
361+
if (_closed)
362+
{
363+
_logger?.LogInfo("Scope sync is closed, skipping '{0}'", actionName);
364+
return;
365+
}
366+
368367
isMainThread ??= MainThreadData.IsMainThread();
369368
if (isMainThread is true)
370369
{
@@ -379,24 +378,74 @@ internal void RunJniSafe(Action action, [CallerMemberName] string actionName = "
379378
}
380379
else
381380
{
382-
ThreadPool.QueueUserWorkItem(state =>
383-
{
384-
var (androidJNI, logger, name) = ((IAndroidJNI, IDiagnosticLogger?, string))state;
381+
_scopeSyncItems.Enqueue((action, actionName));
382+
_scopeSyncEvent.Set();
383+
}
384+
}
385385

386-
androidJNI.AttachCurrentThread();
387-
try
388-
{
389-
action.Invoke();
390-
}
391-
catch (Exception)
386+
private void SyncScope()
387+
{
388+
_androidJNI.AttachCurrentThread();
389+
390+
try
391+
{
392+
var waitHandles = new[] { _scopeSyncEvent, _scopeSyncShutdownSource.Token.WaitHandle };
393+
394+
while (true)
395+
{
396+
var index = WaitHandle.WaitAny(waitHandles);
397+
if (index > 0)
392398
{
393-
logger?.LogError("Calling '{0}' failed.", name);
399+
// Shutdown requested
400+
break;
394401
}
395-
finally
402+
403+
while (_scopeSyncItems.TryDequeue(out var workItem))
396404
{
397-
androidJNI.DetachCurrentThread();
405+
var (action, actionName) = workItem;
406+
try
407+
{
408+
action.Invoke();
409+
}
410+
catch (Exception)
411+
{
412+
_logger?.LogError("Calling '{0}' failed.", actionName);
413+
}
398414
}
399-
}, (_androidJNI, _logger, actionName));
415+
}
416+
}
417+
finally
418+
{
419+
_androidJNI.DetachCurrentThread();
420+
}
421+
}
422+
423+
public void Close()
424+
{
425+
_closed = true;
426+
427+
_scopeSyncShutdownSource.Cancel();
428+
_scopeSyncThread.Join();
429+
430+
// Note: We intentionally don't dispose _scopeSyncEvent to avoid race conditions
431+
// where other threads might call RunJniSafe() after Close() but before disposal.
432+
// The memory overhead of a single AutoResetEvent is negligible.
433+
_scopeSyncShutdownSource.Dispose();
434+
435+
if (!MainThreadData.IsMainThread())
436+
{
437+
_logger?.LogError("Calling Close() on Android SDK requires running on MainThread");
438+
return;
439+
}
440+
441+
try
442+
{
443+
using var sentry = GetSentryJava();
444+
sentry.CallStatic("close");
445+
}
446+
catch (Exception e)
447+
{
448+
_logger?.LogError(e, "Calling 'SentryJava.Close' failed.");
400449
}
401450
}
402451
}

src/sentry-dotnet

Submodule sentry-dotnet updated 97 files

test/Sentry.Unity.Android.Tests/SentryJavaTests.cs

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public void SetUp()
2121
}
2222

2323
[Test]
24-
public void RunJniSafe_OnMainThread_ExecutesActionWithoutAttachingDetaching()
24+
public void RunJniSafe_OnMainThread_ExecutesAction()
2525
{
2626
// Arrange
2727
var actionExecuted = false;
@@ -32,33 +32,30 @@ public void RunJniSafe_OnMainThread_ExecutesActionWithoutAttachingDetaching()
3232

3333
// Assert
3434
Assert.That(actionExecuted, Is.True);
35-
Assert.That(_androidJni.AttachCalled, Is.False); // Sanity Check
36-
Assert.That(_androidJni.DetachCalled, Is.False); // Sanity Check
3735
}
3836

3937
[Test]
40-
public void RunJniSafe_NotMainThread_ExecutesOnThreadPool()
38+
public void RunJniSafe_NotMainThread_ExecutesOnWorkerThread()
4139
{
4240
// Arrange
4341
var actionExecuted = false;
42+
var executionThreadId = 0;
4443
var resetEvent = new ManualResetEvent(false);
45-
var detachResetEvent = new ManualResetEvent(false);
46-
_androidJni.OnDetachCalled = () => detachResetEvent.Set();
4744
var action = new Action(() =>
4845
{
4946
actionExecuted = true;
47+
executionThreadId = Thread.CurrentThread.ManagedThreadId;
5048
resetEvent.Set();
5149
});
5250

5351
// Act
5452
_sut.RunJniSafe(action, isMainThread: false);
5553

5654
// Assert
57-
Assert.That(resetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Action should execute within timeout");
58-
Assert.That(detachResetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Detach should execute within timeout");
55+
Assert.That(resetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True);
5956
Assert.That(actionExecuted, Is.True);
60-
Assert.That(_androidJni.AttachCalled, Is.True);
61-
Assert.That(_androidJni.DetachCalled, Is.True, "Detach should be called");
57+
Assert.That(executionThreadId, Is.Not.EqualTo(Thread.CurrentThread.ManagedThreadId),
58+
"Action should execute on worker thread, not the calling thread");
6259
}
6360

6461
[Test]
@@ -71,8 +68,7 @@ public void RunJniSafe_ActionThrowsOnMainThread_CatchesExceptionAndLogsError()
7168
// Act
7269
_sut.RunJniSafe(action, "TestAction", isMainThread: true);
7370

74-
Assert.That(_androidJni.AttachCalled, Is.False);
75-
Assert.That(_androidJni.DetachCalled, Is.False);
71+
// Assert
7672
Assert.IsTrue(_logger.Logs.Any(log =>
7773
log.logLevel == SentryLevel.Error &&
7874
log.message.Contains("Calling 'TestAction' failed.")));
@@ -84,8 +80,6 @@ public void RunJniSafe_ActionThrowsOnNonMainThread_CatchesExceptionAndLogsError(
8480
// Arrange
8581
var exception = new InvalidOperationException("Test exception");
8682
var resetEvent = new ManualResetEvent(false);
87-
var detachResetEvent = new ManualResetEvent(false);
88-
_androidJni.OnDetachCalled = () => detachResetEvent.Set();
8983
var action = new Action(() =>
9084
{
9185
try
@@ -102,52 +96,74 @@ public void RunJniSafe_ActionThrowsOnNonMainThread_CatchesExceptionAndLogsError(
10296
_sut.RunJniSafe(action, "TestAction", isMainThread: false);
10397

10498
// Assert
105-
Assert.That(resetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Action should execute within timeout");
106-
Assert.That(detachResetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Detach should execute within timeout");
107-
Assert.That(_androidJni.AttachCalled, Is.True);
108-
Assert.That(_androidJni.DetachCalled, Is.True);
99+
Assert.That(resetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True);
109100
Assert.IsTrue(_logger.Logs.Any(log =>
110101
log.logLevel == SentryLevel.Error &&
111102
log.message.Contains("Calling 'TestAction' failed.")));
112103
}
113104

114105
[Test]
115-
public void RunJniSafe_ActionThrowsOnNonMainThread_DetachIsAlwaysCalled()
106+
public void WorkerThread_AttachesOnCreationAndDetachesOnClose()
116107
{
117108
// Arrange
118-
var exception = new InvalidOperationException("Test exception");
119-
var resetEvent = new ManualResetEvent(false);
109+
var logger = new TestLogger();
110+
var androidJni = new TestAndroidJNI();
111+
var attachResetEvent = new ManualResetEvent(false);
120112
var detachResetEvent = new ManualResetEvent(false);
121-
_androidJni.OnDetachCalled = () => detachResetEvent.Set();
122-
var action = new Action(() =>
123-
{
124-
try
125-
{
126-
throw exception;
127-
}
128-
finally
129-
{
130-
resetEvent.Set();
131-
}
132-
});
113+
androidJni.OnAttachCalled = () => attachResetEvent.Set();
114+
androidJni.OnDetachCalled = () => detachResetEvent.Set();
115+
116+
// Act - Create instance (should start worker thread and attach)
117+
var sut = new SentryJava(logger, androidJni);
118+
119+
// Trigger worker thread initialization by queuing work
120+
var workExecuted = new ManualResetEvent(false);
121+
sut.RunJniSafe(() => workExecuted.Set(), isMainThread: false);
122+
123+
// Assert - Worker thread should be attached
124+
Assert.That(workExecuted.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Work should execute to initialize worker thread");
125+
Assert.That(attachResetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "AttachCurrentThread should be called on worker thread creation");
126+
Assert.That(androidJni.AttachCalled, Is.True);
127+
128+
// Act - Close/Dispose (should stop worker thread and detach)
129+
sut.Close();
130+
131+
// Assert - Worker thread should be detached
132+
Assert.That(detachResetEvent.WaitOne(TimeSpan.FromSeconds(2)), Is.True, "DetachCurrentThread should be called when closing");
133+
Assert.That(androidJni.DetachCalled, Is.True);
134+
}
135+
136+
[Test]
137+
public void RunJniSafe_AfterClose_SkipsActionAndLogsWarning()
138+
{
139+
// Arrange
140+
var actionExecuted = false;
141+
_sut.Close();
133142

134143
// Act
135-
_sut.RunJniSafe(action, isMainThread: false);
144+
_sut.RunJniSafe(() => actionExecuted = true, "TestAction", isMainThread: false);
136145

137146
// Assert
138-
Assert.That(resetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Action should execute within timeout");
139-
Assert.That(detachResetEvent.WaitOne(TimeSpan.FromSeconds(1)), Is.True, "Detach should execute within timeout");
140-
Assert.That(_androidJni.AttachCalled, Is.True);
141-
Assert.That(_androidJni.DetachCalled, Is.True, "DetachCurrentThread should always be called");
147+
Assert.That(actionExecuted, Is.False, "Action should not execute after Close()");
148+
Assert.That(_logger.Logs.Any(log =>
149+
log.logLevel == SentryLevel.Info &&
150+
log.message.Contains("Scope sync is closed, skipping 'TestAction'")),
151+
Is.True,
152+
"Should log warning when trying to queue action after Close()");
142153
}
143154

144155
internal class TestAndroidJNI : IAndroidJNI
145156
{
146157
public bool AttachCalled { get; private set; }
147158
public bool DetachCalled { get; private set; }
159+
public Action? OnAttachCalled { get; set; }
148160
public Action? OnDetachCalled { get; set; }
149161

150-
public void AttachCurrentThread() => AttachCalled = true;
162+
public void AttachCurrentThread()
163+
{
164+
AttachCalled = true;
165+
OnAttachCalled?.Invoke();
166+
}
151167

152168
public void DetachCurrentThread()
153169
{

0 commit comments

Comments
 (0)