Skip to content

Commit cc80714

Browse files
denrasebuenaflor
andauthored
Add maxQueueSize to limit the number of unawaited events sent to Sentry (#1868)
* introduce task queue * handle trow in task * handle throwing tasks * Add documentation * add changelog entry --------- Co-authored-by: Giancarlo Buenaflor <[email protected]>
1 parent 202b83f commit cc80714

File tree

5 files changed

+171
-1
lines changed

5 files changed

+171
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
### Features
1111

1212
- Use `recordHttpBreadcrumbs` to set iOS `enableNetworkBreadcrumbs` ([#1884](https://github.com/getsentry/sentry-dart/pull/1884))
13+
- Add `maxQueueSize` to limit the number of unawaited events sent to Sentry ([#1868]((https://github.com/getsentry/sentry-dart/pull/1868))
1314

1415
### Improvements
1516

dart/lib/src/sentry_client.dart

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'sentry_stack_trace_factory.dart';
1717
import 'transport/http_transport.dart';
1818
import 'transport/noop_transport.dart';
1919
import 'transport/spotlight_http_transport.dart';
20+
import 'transport/task_queue.dart';
2021
import 'utils/isolate_utils.dart';
2122
import 'version.dart';
2223
import 'sentry_envelope.dart';
@@ -32,6 +33,10 @@ const _defaultIpAddress = '{{auto}}';
3233
/// Logs crash reports and events to the Sentry.io service.
3334
class SentryClient {
3435
final SentryOptions _options;
36+
late final _taskQueue = TaskQueue<SentryId?>(
37+
_options.maxQueueSize,
38+
_options.logger,
39+
);
3540

3641
final Random? _random;
3742

@@ -514,6 +519,9 @@ class SentryClient {
514519
Future<SentryId?> _attachClientReportsAndSend(SentryEnvelope envelope) {
515520
final clientReport = _options.recorder.flush();
516521
envelope.addClientReport(clientReport);
517-
return _options.transport.send(envelope);
522+
return _taskQueue.enqueue(
523+
() => _options.transport.send(envelope),
524+
SentryId.empty(),
525+
);
518526
}
519527
}

dart/lib/src/sentry_options.dart

+14
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,20 @@ class SentryOptions {
8181
_maxSpans = maxSpans;
8282
}
8383

84+
int _maxQueueSize = 30;
85+
86+
/// Returns the max number of events Sentry will send when calling capture
87+
/// methods in a tight loop. Default is 30.
88+
int get maxQueueSize => _maxQueueSize;
89+
90+
/// Sets how many unawaited events can be sent by Sentry. (e.g. capturing
91+
/// events in a tight loop) at once. If you need to send more, please use the
92+
/// await keyword.
93+
set maxQueueSize(int count) {
94+
assert(count > 0);
95+
_maxQueueSize = count;
96+
}
97+
8498
/// Configures up to which size request bodies should be included in events.
8599
/// This does not change whether an event is captured.
86100
MaxRequestBodySize maxRequestBodySize = MaxRequestBodySize.never;
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'dart:async';
2+
3+
import '../../sentry.dart';
4+
5+
typedef Task<T> = Future<T> Function();
6+
7+
class TaskQueue<T> {
8+
TaskQueue(this._maxQueueSize, this._logger);
9+
10+
final int _maxQueueSize;
11+
final SentryLogger _logger;
12+
13+
int _queueCount = 0;
14+
15+
Future<T> enqueue(Task<T> task, T fallbackResult) async {
16+
if (_queueCount >= _maxQueueSize) {
17+
_logger(SentryLevel.warning,
18+
'Task dropped due to backpressure. Avoid capturing in a tight loop.');
19+
return fallbackResult;
20+
} else {
21+
_queueCount++;
22+
try {
23+
return await task();
24+
} finally {
25+
_queueCount--;
26+
}
27+
}
28+
}
29+
}
+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import 'dart:async';
2+
3+
import 'package:sentry/sentry.dart';
4+
import 'package:sentry/src/transport/task_queue.dart';
5+
import 'package:test/test.dart';
6+
7+
import '../mocks.dart';
8+
9+
void main() {
10+
group("called sync", () {
11+
late Fixture fixture;
12+
13+
setUp(() {
14+
fixture = Fixture();
15+
});
16+
17+
test("enqueue only executed `maxQueueSize` times when not awaiting",
18+
() async {
19+
final sut = fixture.getSut(maxQueueSize: 5);
20+
21+
var completedTasks = 0;
22+
23+
for (int i = 0; i < 10; i++) {
24+
unawaited(sut.enqueue(() async {
25+
print('Task $i');
26+
await Future.delayed(Duration(milliseconds: 1));
27+
completedTasks += 1;
28+
return 1 + 1;
29+
}, -1));
30+
}
31+
32+
// This will always await the other futures, even if they are running longer, as it was scheduled after them.
33+
print('Started waiting for first 5 tasks');
34+
await Future.delayed(Duration(milliseconds: 1));
35+
print('Stopped waiting for first 5 tasks');
36+
37+
expect(completedTasks, 5);
38+
});
39+
40+
test("enqueue picks up tasks again after await in-between", () async {
41+
final sut = fixture.getSut(maxQueueSize: 5);
42+
43+
var completedTasks = 0;
44+
45+
for (int i = 1; i <= 10; i++) {
46+
unawaited(sut.enqueue(() async {
47+
print('Started task $i');
48+
await Future.delayed(Duration(milliseconds: 1));
49+
print('Completed task $i');
50+
completedTasks += 1;
51+
return 1 + 1;
52+
}, -1));
53+
}
54+
55+
print('Started waiting for first 5 tasks');
56+
await Future.delayed(Duration(milliseconds: 1));
57+
print('Stopped waiting for first 5 tasks');
58+
59+
for (int i = 6; i <= 15; i++) {
60+
unawaited(sut.enqueue(() async {
61+
print('Started task $i');
62+
await Future.delayed(Duration(milliseconds: 1));
63+
print('Completed task $i');
64+
completedTasks += 1;
65+
return 1 + 1;
66+
}, -1));
67+
}
68+
69+
print('Started waiting for second 5 tasks');
70+
await Future.delayed(Duration(milliseconds: 5));
71+
print('Stopped waiting for second 5 tasks');
72+
73+
expect(completedTasks, 10); // 10 were dropped
74+
});
75+
76+
test("enqueue executes all tasks when awaiting", () async {
77+
final sut = fixture.getSut(maxQueueSize: 5);
78+
79+
var completedTasks = 0;
80+
81+
for (int i = 0; i < 10; i++) {
82+
await sut.enqueue(() async {
83+
print('Task $i');
84+
await Future.delayed(Duration(milliseconds: 1));
85+
completedTasks += 1;
86+
return 1 + 1;
87+
}, -1);
88+
}
89+
expect(completedTasks, 10);
90+
});
91+
92+
test("throwing tasks still execute as expected", () async {
93+
final sut = fixture.getSut(maxQueueSize: 5);
94+
95+
var completedTasks = 0;
96+
97+
for (int i = 0; i < 10; i++) {
98+
try {
99+
await sut.enqueue(() async {
100+
completedTasks += 1;
101+
throw Error();
102+
}, -1);
103+
} catch (_) {
104+
// Ignore
105+
}
106+
}
107+
expect(completedTasks, 10);
108+
});
109+
});
110+
}
111+
112+
class Fixture {
113+
final options = SentryOptions(dsn: fakeDsn);
114+
115+
TaskQueue<int> getSut({required int maxQueueSize}) {
116+
return TaskQueue(maxQueueSize, options.logger);
117+
}
118+
}

0 commit comments

Comments
 (0)