Skip to content

Commit fb7eecf

Browse files
fix: Crash in SentryTracer when cancelling timer (#3333)
The SentryTracer starts the timer for the deadline on the main thread and invalidates the timer, not necessarily on the main thread, but invalidate has to be called on the same thread on which the timer was started. This is fixed by starting and invalidating the NSTimer on the main thread. Fixes GH-3320 Co-authored-by: Andrew McKnight <[email protected]>
1 parent 1437c68 commit fb7eecf

File tree

5 files changed

+111
-3
lines changed

5 files changed

+111
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Remove "duplicate library" warning (#3312)
1010
- Fix multiple issues in Reachability (#3338)
1111
- Remove unnecessary build settings (#3325)
12+
- Crash in SentryTracer when cancelling timer (#3333)
1213

1314
## 8.13.0
1415

SentryTestUtils/Invocations.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,10 @@ public class Invocations<T> {
4646
self._invocations.append(invocation)
4747
}
4848
}
49+
50+
public func removeAll() {
51+
queue.async(flags: .barrier) {
52+
self._invocations.removeAll()
53+
}
54+
}
4955
}

Sources/Sentry/SentryTracer.m

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,16 @@ - (void)cancelIdleTimeout
235235
- (void)startDeadlineTimer
236236
{
237237
__weak SentryTracer *weakSelf = self;
238-
[SentryThreadWrapper onMainThread:^{
238+
[_configuration.dispatchQueueWrapper dispatchOnMainQueue:^{
239239
weakSelf.deadlineTimer = [weakSelf.configuration.timerFactory
240240
scheduledTimerWithTimeInterval:SENTRY_AUTO_TRANSACTION_DEADLINE
241241
repeats:NO
242242
block:^(NSTimer *_Nonnull timer) {
243+
if (weakSelf == nil) {
244+
SENTRY_LOG_DEBUG(@"WeakSelf is nil. Not calling "
245+
@"deadlineTimerFired.");
246+
return;
247+
}
243248
[weakSelf deadlineTimerFired];
244249
}];
245250
}];
@@ -267,8 +272,19 @@ - (void)deadlineTimerFired
267272

268273
- (void)cancelDeadlineTimer
269274
{
270-
[self.deadlineTimer invalidate];
271-
self.deadlineTimer = nil;
275+
// If the main thread is busy the tracer could be dealloc ated in between.
276+
__weak SentryTracer *weakSelf = self;
277+
278+
// The timer must be invalidated from the thread on which the timer was installed, see
279+
// https://developer.apple.com/documentation/foundation/nstimer/1415405-invalidate#1770468
280+
[_configuration.dispatchQueueWrapper dispatchOnMainQueue:^{
281+
if (weakSelf == nil) {
282+
SENTRY_LOG_DEBUG(@"WeakSelf is nil. Not invalidating deadlineTimer.");
283+
return;
284+
}
285+
[weakSelf.deadlineTimer invalidate];
286+
weakSelf.deadlineTimer = nil;
287+
}];
272288
}
273289

274290
- (id<SentrySpan>)getActiveSpan

Tests/SentryTests/SentryTests-Bridging-Header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
#import "SentryThreadWrapper.h"
182182
#import "SentryTime.h"
183183
#import "SentryTraceContext.h"
184+
#import "SentryTracer+Private.h"
184185
#import "SentryTracer+Test.h"
185186
#import "SentryTransaction.h"
186187
#import "SentryTransactionContext+Private.h"

Tests/SentryTests/Transaction/SentryTracerTests.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class SentryTracerTests: XCTestCase {
9393
customSamplingContext: [:],
9494
configuration: SentryTracerConfiguration(block: {
9595
$0.waitForChildren = waitForChildren
96+
$0.dispatchQueueWrapper = self.dispatchQueue
9697
$0.timerFactory = self.timerFactory
9798
}))
9899
return tracer
@@ -160,7 +161,9 @@ class SentryTracerTests: XCTestCase {
160161
}
161162

162163
func testDeadlineTimer_FinishesTransactionAndChildren() {
164+
fixture.dispatchQueue.blockBeforeMainBlock = { true }
163165
let sut = fixture.getSut()
166+
164167
let child1 = sut.startChild(operation: fixture.transactionOperation)
165168
let child2 = sut.startChild(operation: fixture.transactionOperation)
166169
let child3 = sut.startChild(operation: fixture.transactionOperation)
@@ -176,6 +179,87 @@ class SentryTracerTests: XCTestCase {
176179
XCTAssertEqual(child2.status, .deadlineExceeded)
177180
XCTAssertEqual(child3.status, .ok)
178181
}
182+
183+
func testDeadlineTimer_StartedAndCancelledOnMainThread() {
184+
fixture.dispatchQueue.blockBeforeMainBlock = { true }
185+
186+
let sut = fixture.getSut()
187+
let child1 = sut.startChild(operation: fixture.transactionOperation)
188+
189+
fixture.timerFactory.fire()
190+
191+
XCTAssertEqual(sut.status, .deadlineExceeded)
192+
XCTAssertEqual(child1.status, .deadlineExceeded)
193+
XCTAssertEqual(2, fixture.dispatchQueue.blockOnMainInvocations.count, "The NSTimer must be started and cancelled on the main thread.")
194+
}
195+
196+
func testCancelDeadlineTimer_TracerDeallocated() {
197+
var invocations = 0
198+
fixture.dispatchQueue.blockBeforeMainBlock = {
199+
// The second invocation the block for invalidating the timer
200+
// which we want to call manually below.
201+
if invocations == 1 {
202+
return false
203+
}
204+
205+
invocations += 1
206+
return true
207+
}
208+
209+
var timer: Timer?
210+
weak var weakSut: SentryTracer?
211+
212+
// Added internal function so the tracer gets deallocated after executing this function.
213+
func startTracer() {
214+
let sut = fixture.getSut()
215+
216+
timer = Dynamic(sut).deadlineTimer.asObject as! Timer?
217+
weakSut = sut
218+
219+
// The TestHub keeps a reference to the tracer in capturedEventsWithScopes.
220+
// We set it to nil to avoid that.
221+
sut.hub = nil
222+
sut.finish()
223+
}
224+
startTracer()
225+
226+
XCTAssertNil(weakSut, "sut was not deallocated")
227+
228+
fixture.timerFactory.fire()
229+
230+
let invalidateTimerBlock = fixture.dispatchQueue.blockOnMainInvocations.last
231+
if invalidateTimerBlock != nil {
232+
invalidateTimerBlock!()
233+
}
234+
235+
// Ensure the timer was not invalidated
236+
XCTAssertTrue(timer?.isValid ?? false)
237+
}
238+
239+
func testDeadlineTimer_WhenCancelling_IsInvalidated() {
240+
fixture.dispatchQueue.blockBeforeMainBlock = { true }
241+
242+
let sut = fixture.getSut()
243+
let timer: Timer? = Dynamic(sut).deadlineTimer
244+
_ = sut.startChild(operation: fixture.transactionOperation)
245+
246+
fixture.timerFactory.fire()
247+
248+
XCTAssertNil(Dynamic(sut).deadlineTimer.asObject, "DeadlineTimer should be nil.")
249+
XCTAssertFalse(timer?.isValid ?? true)
250+
}
251+
252+
func testDeadlineTimer_FiresAfterTracerDeallocated() {
253+
fixture.dispatchQueue.blockBeforeMainBlock = { true }
254+
255+
// Added internal function so the tracer gets deallocated after executing this function.
256+
func startTracer() {
257+
_ = fixture.getSut()
258+
}
259+
startTracer()
260+
261+
fixture.timerFactory.fire()
262+
}
179263

180264
func testFramesofSpans_SetsDebugMeta() {
181265
let sut = fixture.getSut()

0 commit comments

Comments
 (0)