Skip to content

Commit 774f2c1

Browse files
committed
feat(pubsub): Add exponential backoff, batching, and parallel streaming pull
- Exponential Backoff: Configurable retry parameters for transient network failures. - Background Batching: Automatic batching for publish, acknowledge, and modifyAckDeadline. - Immediate RPCs: acknowledgeNow and modifyAckDeadlineNow for explicit awaitable RPCs. - Parallel Streaming Pull: Added maxConcurrentStreams to streamingPull to increase throughput. - Added comprehensive unit tests and documentation examples. TAG=agy CONV=bc7c6164-e9cb-4a0f-b326-7752c814e1ce
1 parent 6681e92 commit 774f2c1

12 files changed

Lines changed: 1038 additions & 112 deletions

File tree

pkgs/google_cloud_pubsub/README.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,30 @@ void main() async {
3030
// Application Default Credentials (ADC).
3131
final pubSub = PubSub(projectId: 'your-project-id');
3232
33-
// Create a topic.
34-
final topic = await pubSub.topic('put-your-topic-name-here').create();
33+
// Create a topic (with optional batching and retry settings).
34+
final topic = await pubSub
35+
.topic(
36+
'put-your-topic-name-here',
37+
publishSettings: const PublishSettings(
38+
batching: BatchingSettings(
39+
maxMessages: 100,
40+
maxDelay: Duration(milliseconds: 10),
41+
),
42+
),
43+
)
44+
.create();
3545
3646
// Create a subscription to that topic.
3747
final subscription = await pubSub
38-
.subscription('put-your-subscription-name-here')
48+
.subscription(
49+
'put-your-subscription-name-here',
50+
ackSettings: const AckSettings(
51+
batching: BatchingSettings(maxDelay: Duration(milliseconds: 50)),
52+
),
53+
)
3954
.create(topic: topic.name);
4055
41-
// Publish a message.
56+
// Publish a message. This is automatically batched and retried.
4257
await topic.publish(utf8.encode('message 1'));
4358
4459
// Pull messages from the subscription.
@@ -47,16 +62,19 @@ void main() async {
4762
for (final receivedMessage in messages) {
4863
print('Received message: ${utf8.decode(receivedMessage.data)}');
4964
50-
// Acknowledge the message.
51-
await subscription.acknowledgeNow([receivedMessage]);
65+
// Acknowledge the message in the background.
66+
subscription.acknowledge(receivedMessage);
5267
}
5368
5469
print(
5570
'Your topic is available at:\n'
5671
'https://pubsub.googleapis.com/v1/${topic.name}',
5772
);
5873
59-
// Clean up.
74+
// Clean up and flush any pending batches.
75+
subscription.close();
76+
topic.close();
77+
6078
await subscription.delete();
6179
await topic.delete();
6280
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:async';
16+
import 'package:google_cloud_pubsub/google_cloud_pubsub.dart';
17+
18+
Future<void> streamingPullExample(PubSub pubsub) async {
19+
// #docregion streaming_pull_example
20+
final subscription = pubsub.subscription('my-subscription');
21+
22+
// Establish a streaming pull with 3 parallel streams for high throughput
23+
// and custom retry settings for handling transient connection drops.
24+
final stream = subscription.streamingPull(
25+
maxConcurrentStreams: 3,
26+
streamAckDeadlineSeconds: 30,
27+
retry: const RetrySettings(
28+
maxRetries: 10,
29+
initialDelay: Duration(seconds: 1),
30+
maxDelay: Duration(seconds: 30),
31+
),
32+
);
33+
34+
final listener = stream.listen(
35+
(ReceivedMessage message) {
36+
print('Received message: ${message.message.data}');
37+
38+
// Acknowledge the message. This buffers the ACK in the background
39+
// and batches it with others, sending it over one of the active
40+
// gRPC streams or falling back to a unary RPC if streams are down.
41+
subscription.acknowledge(message);
42+
},
43+
onError: (Object error) {
44+
print('Stream encountered a permanent error: $error');
45+
},
46+
onDone: () {
47+
print('Stream closed.');
48+
},
49+
);
50+
51+
// Later, to stop receiving messages and clean up resources:
52+
await listener.cancel();
53+
// #enddocregion streaming_pull_example
54+
}
55+
56+
Future<void> acknowledgeNowExample(PubSub pubsub) async {
57+
// #docregion acknowledge_now_example
58+
final subscription = pubsub.subscription('my-subscription');
59+
60+
// Pull up to 10 messages from the subscription.
61+
final messages = await subscription.pull(maxMessages: 10);
62+
63+
if (messages.isNotEmpty) {
64+
try {
65+
// Acknowledge all pulled messages immediately and wait for the RPC
66+
// to complete. This bypasses background batching.
67+
await subscription.acknowledgeNow(messages);
68+
print('Successfully acknowledged ${messages.length} messages.');
69+
} on Exception catch (e) {
70+
print('Failed to acknowledge messages: $e');
71+
}
72+
}
73+
// #enddocregion acknowledge_now_example
74+
}

pkgs/google_cloud_pubsub/example/example.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,30 @@ Future<void> main() async {
2323
final pubsub = PubSub(projectId: 'my-project-id');
2424

2525
try {
26-
final topic = pubsub.topic('my-topic');
26+
final topic = pubsub.topic(
27+
'my-topic',
28+
publishSettings: const PublishSettings(
29+
batching: BatchingSettings(
30+
maxMessages: 50,
31+
maxDelay: Duration(milliseconds: 20),
32+
),
33+
),
34+
);
2735
print('Successfully initialized client for topic: ${topic.id}');
36+
37+
final subscription = pubsub.subscription(
38+
'my-subscription',
39+
ackSettings: const AckSettings(
40+
batching: BatchingSettings(
41+
maxMessages: 50,
42+
maxDelay: Duration(milliseconds: 20),
43+
),
44+
),
45+
);
46+
print('Successfully initialized subscription: ${subscription.id}');
47+
48+
topic.close();
49+
subscription.close();
2850
} finally {
2951
await pubsub.close();
3052
}

pkgs/google_cloud_pubsub/lib/google_cloud_pubsub.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export 'package:grpc/grpc.dart'
2020
ServiceAccountAuthenticator,
2121
applicationDefaultCredentialsAuthenticator;
2222

23+
export 'src/batching.dart' show BatchingSettings;
2324
export 'src/client.dart' show PubSub;
2425
export 'src/message.dart' show Message, ReceivedMessage;
25-
export 'src/subscription.dart' show Subscription;
26-
export 'src/topic.dart' show Topic;
26+
export 'src/retry.dart' show RetrySettings;
27+
export 'src/subscription.dart' show AckSettings, Subscription;
28+
export 'src/topic.dart' show PublishSettings, Topic;
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:async';
16+
17+
import 'package:meta/meta.dart';
18+
19+
/// Settings for batching operations.
20+
final class BatchingSettings {
21+
/// The maximum number of items in a batch.
22+
final int maxMessages;
23+
24+
/// The maximum size in bytes for a batch.
25+
final int maxBytes;
26+
27+
/// The maximum time to wait before sending a batch.
28+
final Duration maxDelay;
29+
30+
const BatchingSettings({
31+
this.maxMessages = 100,
32+
this.maxBytes = 1024 * 1024, // 1 MB
33+
this.maxDelay = const Duration(milliseconds: 10),
34+
});
35+
}
36+
37+
/// Generic batcher that accumulates items of type [T] and fires batches of [T]
38+
/// according to [BatchingSettings].
39+
@internal
40+
class Batcher<T> {
41+
final BatchingSettings settings;
42+
final int Function(T) itemSize;
43+
final Future<void> Function(List<T>) onBatch;
44+
45+
final List<T> _buffer = [];
46+
int _currentSizeBytes = 0;
47+
Timer? _timer;
48+
49+
Batcher({
50+
required this.settings,
51+
required this.itemSize,
52+
required this.onBatch,
53+
});
54+
55+
/// Adds an item to the batch.
56+
void add(T item) {
57+
_buffer.add(item);
58+
_currentSizeBytes += itemSize(item);
59+
60+
if (_buffer.length >= settings.maxMessages ||
61+
_currentSizeBytes >= settings.maxBytes) {
62+
_flush();
63+
} else {
64+
_timer ??= Timer(settings.maxDelay, _flush);
65+
}
66+
}
67+
68+
void _flush() {
69+
_timer?.cancel();
70+
_timer = null;
71+
72+
if (_buffer.isEmpty) return;
73+
74+
final batch = List<T>.from(_buffer);
75+
_buffer.clear();
76+
_currentSizeBytes = 0;
77+
78+
// Fire and forget
79+
onBatch(batch).catchError((_) {
80+
// Errors should be handled by onBatch (e.g. failing the completers for
81+
// the items).
82+
});
83+
}
84+
85+
/// Closes the batcher, flushing any remaining items immediately.
86+
void close() {
87+
_flush();
88+
}
89+
}

0 commit comments

Comments
 (0)