Skip to content

feat: Pub/sub#244

Open
sigurdm wants to merge 20 commits into
googleapis:mainfrom
sigurdm:pub_sub
Open

feat: Pub/sub#244
sigurdm wants to merge 20 commits into
googleapis:mainfrom
sigurdm:pub_sub

Conversation

@sigurdm

@sigurdm sigurdm commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Initial implementation. Only basic functionality implemented (see todos).
Streaming pull is implemented.

Rely on the package:grpc BaseAuthenticator abstraction for authentication. Rexports those.

No built-in retry yet.

@sigurdm sigurdm changed the title Pub sub Pub/sub Apr 20, 2026
@sigurdm sigurdm changed the title Pub/sub feat: Pub/sub Apr 20, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new Google Cloud Pub/Sub client library, including core client functionality, topic and subscription management, and support for the Pub/Sub emulator. The review identified several critical issues: a race condition and token refresh problem in the authentication logic, a resource leak where the auth client is not closed, an overly restrictive project ID requirement for the emulator, an incorrect parsing logic for IPv6 emulator hosts, and code duplication in message mapping. I have recommended addressing these issues to improve the reliability and maintainability of the client.

Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
@sigurdm
sigurdm marked this pull request as ready for review May 21, 2026 14:44
@sigurdm
sigurdm requested a review from brianquinlan May 21, 2026 14:44
@sigurdm
sigurdm requested a review from jonasfj May 21, 2026 14:47
@sigurdm

sigurdm commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds an experimental Google Cloud Pub/Sub client for Dart, supporting topic and subscription management along with message publishing and pull operations. Key feedback includes supporting full resource names for cross-project access, optimizing StreamingPull with bidirectional communication, and correcting Windows-specific environment variable retrieval logic. A minor style correction for the package description was also noted.

Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/pubsub_emulator_host_vm.dart Outdated
Comment thread pkgs/google_cloud_pubsub/pubspec.yaml
Comment thread pkgs/google_cloud_pubsub/lib/google_cloud_pubsub.dart
Comment thread pkgs/google_cloud_pubsub/lib/src/topic.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/message.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/message.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/message.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
Comment thread pkgs/google_cloud_pubsub/lib/src/client.dart Outdated
@sigurdm

sigurdm commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

/gcbrun

@sigurdm
sigurdm requested a review from jonasfj June 12, 2026 12:17

@jonasfj jonasfj left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just nits.

We could consider landing all the generated files first, if you want a more detailed review of the public API.

Comment on lines +53 to +61
ReceivedMessage({
required this.ackId,
required this.messageId,
required this.publishTime,
required this.message,
FutureOr<void> Function(List<String> ackIds)? ackHandler,
FutureOr<void> Function(List<String> ackIds, int seconds)?
modifyDeadlineHandler,
}) : _ackHandler = ackHandler,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a constructor you want to make part of the public API?

Consider making it private and expose a top-level createReceivedMessage and annotated it @internal.

This way, you can avoid exporting createReceivedMessage from lib/google_cloud_pubsub.dart.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the callbacks out of ReceivedMessage. Now it is just a dumb data class, that feels a lot more clean.

You now need to do Subscription.ack(receivedMessage) I think that is acceptable. WDYT?

Comment thread pkgs/google_cloud_pubsub/lib/src/message.dart Outdated
static String? _calculateProjectId(String? projectId, String? emulatorHost) =>
switch ((projectId, emulatorHost)) {
(final String projectId, _) => projectId,
(null, _?) => projectFromEnvironment ?? 'test-project',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we always falling back to test-project? Ah, only when PUBSUB_EMULATOR_HOST is set, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - that's confusing. Added a comment.

Introduce the handwritten client code (PubSub, Topic, Subscription, Message)
on top of the generated gRPC code.

- Implement PubSub client with emulator support.
- Implement Topic and Subscription resource management.
- Implement streaming pull and unary fallback for message acknowledgments.
- Add unit and integration tests.
- Add usage examples in README and example/example.dart.

TAG=agy
CONV=06ddc172-e8da-4e30-bab8-139c539520f9
sigurdm added 3 commits June 19, 2026 08:40
- Make ReceivedMessage constructor private (ReceivedMessage._).
- Introduce package-private createReceivedMessage helper annotated with @internal.
- Update client.dart and unit_test.dart to use the helper.

TAG=agy
CONV=06ddc172-e8da-4e30-bab8-139c539520f9
Simplify ReceivedMessage by removing ack() and modifyAckDeadline() methods,
which removes the need for storing internal callback handlers.

- Make ReceivedMessage constructor public and simple.
- Update client.dart to map ReceivedMessage without callbacks.
- Update unit tests and integration tests to use Subscription.acknowledge() and Subscription.modifyAckDeadline() instead of message methods.
- Fix linter warning in client.dart (use tearoff).

TAG=agy
CONV=06ddc172-e8da-4e30-bab8-139c539520f9
Add inline comment to _calculateProjectId explaining that we only fall back to
'test-project' when the emulator is active.

TAG=agy
CONV=06ddc172-e8da-4e30-bab8-139c539520f9
sigurdm added 2 commits June 22, 2026 10:25
…with latest API design

- Changed acknowledge and modifyAckDeadline to take ReceivedMessage and return void (fire-and-forget).
- Added acknowledgeNow and modifyAckDeadlineNow to support immediate, awaitable operations on List<ReceivedMessage>.
- Updated tests and examples accordingly.

TAG=agy
CONV=bc7c6164-e9cb-4a0f-b326-7752c814e1ce
@@ -0,0 +1,93 @@
// Copyright 2026 Google LLC

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks copied from pkgs/google_cloud_storage/lib/src/storage_emulator_host_vm.dart

Maybe this functionality should be moved into package:google_cloud?

final isEmulator = Platform.environment['PUBSUB_EMULATOR_HOST'] != null;

group('PubSub Integration', () {
PubSub? client;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
PubSub? client;
late final PubSub client;

final resolvedProjectId = _calculateProjectId(projectId, emulatorHost);
if (resolvedProjectId == null) {
throw ArgumentError(
'A project ID is required, but none was provided or could be '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a StateError?

/// For authentication, an [authenticator] can be supplied to obtain
/// and refresh access credentials for authenticating gRPC requests.
///
/// If no [authenticator] is provided, requests are made without

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use ADC like the GCS and the other languages' pub sub clients?

await _publisher.deleteTopic(request, options: await _callOptions);
return true;
} on GrpcError catch (e) {
if (e.code == StatusCode.notFound) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In package:google_cloud_storage, I throw in this case. The other client libraries with exceptions also throw here. I think that we should be consistent, at least within our own packages.

Does an api expert (@natebosch ?) have an opinion?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either design is OK. Since we have precedent I lean towards matching what we already do elsewhere.

options: await _callOptions,
);
return subscriptionName(name);
} on GrpcError catch (e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you want to handle this exception mapping in a function?

// limitations under the License.

/// Base class for all exceptions thrown by the google_cloud_pubsub package.
abstract final class PubSubException implements Exception {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to define your own exception hierarchy? The other languages' clients use their generic rpc exception types (as does package:google_cloud_storage).

///
/// See the [official documentation](https://cloud.google.com/pubsub/docs/reference/rpc/google.pubsub.v1#google.pubsub.v1.Subscriber.ModifyAckDeadline).
Future<void> modifyAckDeadline(
String name,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe call this subscription because, at first, I thought that this was a message name.

@@ -0,0 +1,334 @@
@TestOn('vm')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to have one test file per method under test? This is going to get pretty big quickly.

}

void main() {
group('PubSub Unit Tests (Error Propagation)', () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call this file error_propogation_test.dart.

Comment on lines +156 to +163
if (_isEmulator) {
return CallOptions();
}
final authenticator = _authenticator;
if (authenticator == null) {
return CallOptions();
}
return authenticator.toCallOptions;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional] I like to collapse early return guard clauses when they fit on one line. Also we can use ?? for the second null check.

Suggested change
if (_isEmulator) {
return CallOptions();
}
final authenticator = _authenticator;
if (authenticator == null) {
return CallOptions();
}
return authenticator.toCallOptions;
if (_isEmulator) return CallOptions();
return authenticator?.toCallOptions ?? CallOptions();

await _publisher.deleteTopic(request, options: await _callOptions);
return true;
} on GrpcError catch (e) {
if (e.code == StatusCode.notFound) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think either design is OK. Since we have precedent I lean towards matching what we already do elsewhere.

Comment on lines +17 to +23
import 'package:grpc/grpc.dart';
import 'package:meta/meta.dart';
import '../google_cloud_pubsub.dart';
import 'generated/google/pubsub/v1/pubsub.pbgrpc.dart' as grpc;
import 'pubsub_emulator_host_vm.dart';
import 'subscription.dart' show newSubscription, newSubscriptionName;
import 'topic.dart' show newTopic, newTopicName;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]

Suggested change
import 'package:grpc/grpc.dart';
import 'package:meta/meta.dart';
import '../google_cloud_pubsub.dart';
import 'generated/google/pubsub/v1/pubsub.pbgrpc.dart' as grpc;
import 'pubsub_emulator_host_vm.dart';
import 'subscription.dart' show newSubscription, newSubscriptionName;
import 'topic.dart' show newTopic, newTopicName;
import 'package:grpc/grpc.dart';
import 'package:meta/meta.dart';
import '../google_cloud_pubsub.dart';
import 'generated/google/pubsub/v1/pubsub.pbgrpc.dart' as grpc;
import 'pubsub_emulator_host_vm.dart';
import 'subscription.dart' show newSubscription, newSubscriptionName;
import 'topic.dart' show newTopic, newTopicName;

);
}

if (emulatorHost case String host) {

@natebosch natebosch Jun 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Avoid the appearance of inputs that may be other types by only using the syntax for the null check.

Suggested change
if (emulatorHost case String host) {
if (emulatorHost case final host?) {

/// and the emulator configuration might not be available until the application
/// is launched.
@internal
String? get pubsubEmulatorHost {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] inconsistent capitalization - other code treats "pub" and "sub" as different words but they are joined here.

Comment on lines +61 to +63
if (invocation.memberName == #cancel) {
return Future<void>.value();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about implementing the cancel method instead?

Comment on lines +69 to +71
class FakeResponseStream<T> extends Stream<T>
implements grpc.ResponseStream<T> {
final Stream<T> _stream;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we both extending Stream and including a stream member?

Comment on lines +130 to +131
// Listen to request stream to prevent sender from hanging on close()
request.listen((_) {}, onDone: () {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]

Suggested change
// Listen to request stream to prevent sender from hanging on close()
request.listen((_) {}, onDone: () {});
// Listen to request stream to prevent sender from hanging on close()
request.drain();

Comment on lines +144 to +145
if (acknowledgeBehavior != null) {
acknowledgeBehavior!(request.ackIds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional]

Suggested change
if (acknowledgeBehavior != null) {
acknowledgeBehavior!(request.ackIds)
if (acknowledgeBehavior case final acknowledge?) {
acknowledge(request.ackIds)

Comment on lines +178 to +186
class FakeClientChannel implements grpc.ClientChannel {
@override
Future<void> shutdown() async {
// No-op for testing
}

@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can extend Fake from package:test/test.dart to get this override

Suggested change
class FakeClientChannel implements grpc.ClientChannel {
@override
Future<void> shutdown() async {
// No-op for testing
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
class FakeClientChannel extends Fake implements grpc.ClientChannel {
@override
Future<void> shutdown() async {
// No-op for testing
}
}

Similar in the other fakes.

sigurdm added 2 commits June 25, 2026 13:33
- Fix finalSize check in Windows environment variable reader to handle errors/race conditions.
- Update documentation to follow style guidelines for programmer errors (use "It is an error if..." instead of "Throws...").

TAG=agy
CONV=ad5bf873-d53c-4338-9b86-1b6a21376d4d
- Default to ADC when not using emulator
- Default emulator project ID to test-project
- Parse emulator host as Uri? with validation
- Update deleteTopic and deleteSubscription to throw on notFound
- Rename name parameter to topic/subscription in PubSub client methods
- Centralize GrpcError mapping
- Rename topicId and subscriptionId to id
- Split integration tests into individual files per operation
- Rename unit_test.dart to error_propagation_test.dart and refactor fakes
- Expand and clarify doc comments across public APIs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants