Repository: googleapis/google-cloud-ruby (Ruby)

Issue:
Allow creating Publisher without GetTopic API lookup to prevent Admin Quota exhaustion
 **Is your feature request related to a problem? Please describe.**

This request addresses a regression in functionality introduced in google-cloud-pubsub version 3.0 regarding the initialization of Topic/Publisher references.

In version 2.x, it was possible to create a reference to a Pub/Sub topic without triggering an immediate gRPC API call. However, with the shift to the Publisher class in v3.0, the library now enforces a lookup (specifically topic_admin.get_topic) every time a publisher is instantiated.

This behavior causes significant operational issues in environments with a high volume of topics and services. Specifically, Google Cloud imposes a strict system limit on "Administrator operations per minute." During a rolling restart of our application—where many services attempt to re-initialize publishers for various topics simultaneously—we rapidly exhaust this quota. This results in temporary outages and startup failures purely due to these mandatory, yet redundant, existence checks.

 **Describe the solution you'd like**

I would like to request the restoration of the ability to create a Publisher instance using a known topic name without performing a lookup validation (the GetTopic API call).

This could be achieved by introducing a skip_lookup (or similarly named) option to the factory method.

Below is a proposed implementation sketch demonstrating how this could be achieved in Google::Cloud::PubSub::Project and Google::Cloud::PubSub::Publisher:

In Google::Cloud::PubSub::Project:

```ruby
def publisher topic_name, project: nil, async: nil, skip_lookup: false
  ensure_service!
  options = { project: project, async: async }
  
  # Allow returning a Publisher immediately if we trust the topic name exists
  return Publisher.from_name topic_name, service, options if skip_lookup

  grpc = topic_admin.get_topic topic: service.topic_path(topic_name, options)
  Publisher.from_grpc grpc, service, async: async
end
```

In Google::Cloud::PubSub::Publisher:

```ruby
def name
  return @resource_name if reference?
  @grpc.name
end

def reference?
  @grpc.nil?
end

# Factory method to create a Publisher without a grpc object
def self.from_name name, service, options = {}
  name = service.topic_path name, options
  from_grpc(nil, service, async: options[:async]).tap do |t|
    t.instance_variable_set :@resource_name, name
  end
end
```

 **Describe alternatives you've considered**

1. Remaining on v2.x: This prevents us from utilizing new features and security patches in the long term and creates technical debt.
2. Monkey-patching/Wrapping the library: We have considered implementing a custom wrapper or monkey-patching the library to manually inject the from_name logic proposed above. However, relying on internal implementation details or private forks is fragile and increases the maintenance burden significantly.

 **Additional context**

Our architecture relies on a services mesh communicating via a large number of distinct Pub/Sub topics. The current strict requirement to verify topic existence upon client initialization turns a standard application restart into a "Distributed Denial of Service" attack against our own Google Cloud Project quotas. Restoring the "reference-only" instantiation is critical for reliability at scale.


Discussion: Ack, thanks for the report and PR! I will get it figured out this week.



Fix (patch):
diff --git a/google-cloud-pubsub/OVERVIEW.md b/google-cloud-pubsub/OVERVIEW.md
index 13d2da170f04..361c8d95076f 100644
--- a/google-cloud-pubsub/OVERVIEW.md
+++ b/google-cloud-pubsub/OVERVIEW.md
@@ -36,7 +36,7 @@ into Admin Operations and Data Plane Operations.
 * **Data Plane Operations**: For the core functionality of publishing and receiving messages.
 
 To learn more about Pub/Sub, read the [Google Cloud Pub/Sub Overview
-](https://cloud.google.com/pubsub/overview).
+](https://docs.cloud.google.com/pubsub/docs/overview).
 
 ## Admin Operations
 
diff --git a/google-cloud-pubsub/lib/google/cloud/pubsub/project.rb b/google-cloud-pubsub/lib/google/cloud/pubsub/project.rb
index 1bf890d7609c..460837f3ad56 100644
--- a/google-cloud-pubsub/lib/google/cloud/pubsub/project.rb
+++ b/google-cloud-pubsub/lib/google/cloud/pubsub/project.rb
@@ -133,12 +133,17 @@ def iam
         # @param [Hash] async A hash of values to configure the topic's
         #   {AsyncPublisher} that is created when {Publisher#publish_async}
         #   is called. Optional.
+        # @param [Boolean] skip_lookup Optionally create a {Publisher} object
+        #   without verifying the topic resource exists on the Pub/Sub
+        #   service. Calls made on this object will raise errors if the service
+        #   resource does not exist. Default is `false`.
         #
         # @return [Google::Cloud::PubSub::Publisher]
         #
-        def publisher topic_name, project: nil, async: nil
+        def publisher topic_name, project: nil, async: nil, skip_lookup: nil
           ensure_service!
           options = { project: project, async: async }
+          return Publisher.from_name topic_name, service, options if skip_lookup
           grpc = topic_admin.get_topic topic: service.topic_path(topic_name, options)
           Publisher.from_grpc grpc, service, async: async
         end
diff --git a/google-cloud-pubsub/lib/google/cloud/pubsub/publisher.rb b/google-cloud-pubsub/lib/google/cloud/pubsub/publisher.rb
index 61e1e9c37ceb..605494c6c27b 100644
--- a/google-cloud-pubsub/lib/google/cloud/pubsub/publisher.rb
+++ b/google-cloud-pubsub/lib/google/cloud/pubsub/publisher.rb
@@ -87,9 +87,56 @@ def async_publisher
         #   `projects/{project_id}/topics/{topic_id}`.
         #
         def name
+          return @resource_name if reference?
           @grpc.name
         end
 
+        ##
+        # Determines whether the publisher object was created with a resource
+        # representation from the Pub/Sub service.
+        #
+        # @return [Boolean] `true` when the publisher was created with a resource
+        #   representation, `false` otherwise.
+        #
+        def reference?
+          @grpc.nil?
+        end
+
+        ##
+        # Determines whether the publisher object was created with a resource
+        # representation from the Pub/Sub service.
+        #
+        # @return [Boolean] `true` when the publisher was created with a resource
+        #   representation, `false` otherwise.
+        #
+        def resource?
+          !@grpc.nil?
+        end
+
+
+        ##
+        # Reloads the publisher with current data from the Pub/Sub service.
+        #
+        # @return [Google::Cloud::PubSub::Publisher] Returns the reloaded
+        #   publisher
+        #
+        # @example
+        #   require "google/cloud/pubsub"
+        #
+        #   pubsub = Google::Cloud::PubSub.new
+        #
+        #   publisher = pubsub.publisher "my-topic", skip_lookup: true
+        #
+        #   publisher.reload!
+        #
+        def reload!
+          ensure_service!
+          topic_path = service.topic_path name
+          @grpc = service.topic_admin.get_topic topic: topic_path
+          @resource_name = nil
+          self
+        end
+
         ##
         # Publishes one or more messages to the publisher.
         #
@@ -166,7 +213,7 @@ def name
         #
         def publish data = nil, attributes = nil, ordering_key: nil, compress: nil, compression_bytes_threshold: nil,
                     **extra_attrs, &block
-          ensure_service!
+          ensure_grpc!
           batch = BatchPublisher.new data,
                                      attributes,
                                      ordering_key,
@@ -290,8 +337,7 @@ def publish data = nil, attributes = nil, ordering_key: nil, compress: nil, comp
         #   publisher.async_publisher.stop!
         #
         def publish_async data = nil, attributes = nil, ordering_key: nil, **extra_attrs, &callback
-          ensure_service!
-
+          ensure_grpc!
           @async_publisher ||= AsyncPublisher.new name, service, **@async_opts
           @async_publisher.publish data, attributes, ordering_key: ordering_key, **extra_attrs, &callback
         end
@@ -351,6 +397,16 @@ def self.from_grpc grpc, service, async: nil
           end
         end
 
+        ##
+        # @private New reference {Publisher} from a topic name without making
+        # a HTTP request.
+        def self.from_name name, service, options = {}
+          name = service.topic_path name, options
+          from_grpc(nil, service, async: options[:async]).tap do |t|
+            t.instance_variable_set :@resource_name, name
+          end
+        end
+
         protected
 
         ##
@@ -364,6 +420,7 @@ def ensure_service!
         # Ensures a Google::Cloud::PubSub::V1::Topic object exists.
         def ensure_grpc!
           ensure_service!
+          reload! if reference?
         end
       end
     end