pubsub_client 0.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2508e7f65206f72e0092d048d71416a77bf1391fcda8a13e43fbd50c2a4ae561
4
- data.tar.gz: 7d1f9e9b1e0f9e6af43d732da015a08cb04cc7983237fd6ed4e5a24cd94f58c9
3
+ metadata.gz: fcd27cf1ed4e02b4b8bae0e9b2f5227d970f158196b68d903cbfa2b031c2b1eb
4
+ data.tar.gz: 415e17dfb4465a44ad9ffe3aeabe8b116a8f47b79f77e3a408d67da8c44409fa
5
5
  SHA512:
6
- metadata.gz: 016b31843467a3de64bf9331650ce2ca371fd11c3ebe19e6c2b99e905407ad745e793c56ec08831e20e173f45e4cc13f89eb36a07f5eae33669fc4488c2acbc0
7
- data.tar.gz: d07b99e406fe71164285e94624b53c0355ad8c9ac9c2fd2cbc2738f38fd19a3b1baef6845053000bf20cd21aa4ca4cc723c1d1e006da4b2a9c15859aad9145bb
6
+ metadata.gz: bacbf023017f2ba125de0be70c2040e697c5c7927f24569f6e5af043d78eefc0eee813af9bfaeadbd4fde3213554bbe135ef011a3343408552b69ea217aa4bac
7
+ data.tar.gz: 9a84a7a5c32ba17ea5523e3b0831935dc162d89a0cc7dcd89d1a3863951796dee63fa59639d583de72965d75e42047ca0ec275b1dde3541ae4b40d818e1e8359
@@ -0,0 +1,31 @@
1
+ Version 1.4.0 2020-10-26
2
+ ------------------------
3
+ 60b8403 Publish attributes
4
+
5
+ Version 1.3.0 2020-10-23
6
+ ------------------------
7
+ e9e87f7 This adds the ability to subscribe to a Pub/Sub topic. Of note, we expose two additional configuration params:
8
+ - `concurrency`: the number of threads the subscriber will run to process messages (defaults to 8 threads)
9
+ - `auto_ack`: flag to auto ack messages (default is `true` and _will_ ack messages)
10
+
11
+ These changes come with a handful of useful checks:
12
+ - ensures credentials are configured prior to subscribing
13
+ - raises an error if the target subscription does not exist
14
+ - raises an error if attempting to subscribe to a topic that has already been subscribed to
15
+
16
+ Version 1.2.0 2020-09-25
17
+ ------------------------
18
+ aede3ab Raise custom error if GOOGLE_APPLICATION_CREDENTIALS not set
19
+
20
+ Version 1.1.0 2020-09-25
21
+ ------------------------
22
+ d3f8ed1 Throw error if topic does not exist
23
+ a95430f Bump major version and update change log
24
+
25
+ Version 1.0.0 2020-09-24
26
+ ------------------------
27
+ 1d7dc1c Allow publishing to any topic
28
+
29
+ Version 0.1.0 2020-08-25
30
+ ------------------------
31
+ 92a2e7a Initial release
data/README.md CHANGED
@@ -20,30 +20,25 @@ Or install it yourself as:
20
20
 
21
21
  ## Configuration
22
22
 
23
- In order to use this gem, the environment variable `GOOGLE_APPLICATION_CREDENTIALS` must be set and point to the credentials JSON file. Additionally, here are configuration settings that may need to be set:
24
- - `topic_name` (required unless stubbed) - name of the Google Cloud Pub/Sub topic to publish messages to.
23
+ In order to use this gem, the environment variable `GOOGLE_APPLICATION_CREDENTIALS` must be set and point to the credentials JSON file.
25
24
 
26
- If there are environments where setting up credentials is too burdensome and/or publishing messages is not desired, `PubsubClient` can be stubbed out with `PubsubClient.stub!`
27
-
28
- E.g.
25
+ If there are environments where setting up credentials is too burdensome and/or publishing messages is not desired, `PubsubClient` can be stubbed out with `PubsubClient.stub!`, e.g.
29
26
 
30
27
  ```ruby
31
28
  if test_env?
32
29
  PubsubClient.stub!
33
- else
34
- PubsubClient.configure do |config|
35
- config.topic_name = 'some-topic'
36
- end
37
30
  end
38
31
  ```
39
32
 
40
33
  ## Usage
41
34
 
42
- To publish a message to Pub/Sub, call `PubsubClient.publish(message)`. This method takes any serializable object as an argument and yields a result object to a block. The `result` object has a method `#succeeded?` that returns `true` if the message was successfully published, otherwise `false`. In the latter case, there is a method `#error` that returns the error.
35
+ ### Publishing
36
+
37
+ To publish a message to Pub/Sub, call `PubsubClient.publish(message, 'the-topic')`. This method takes any serializable object as an argument and yields a result object to a block. The `result` object has a method `#succeeded?` that returns `true` if the message was successfully published, otherwise `false`. In the latter case, there is a method `#error` that returns the error.
43
38
 
44
- ### Example
39
+ #### Example
45
40
  ```ruby
46
- PubsubClient.publish(message) do |result|
41
+ PubsubClient.publish(message, 'some-topic') do |result|
47
42
  if result.succeeded?
48
43
  puts 'yay!'
49
44
  else
@@ -52,14 +47,54 @@ PubsubClient.publish(message) do |result|
52
47
  end
53
48
  ```
54
49
 
50
+ ### Subscribing
51
+
52
+ To subscribe to a topic, a client must first get a handle to the subscriber object. After doing so, a call to `subscriber.listener` will yield two arguments: the data (most clients will only need this) and the full Pub/Sub message (for anything more robust). Documentation for the full message can be found [here](https://googleapis.dev/ruby/google-cloud-pubsub/latest/Google/Cloud/PubSub/ReceivedMessage.html).
53
+
54
+ Optionally, a client can choose to handle exceptions raised by the subscriber. If a client chooses to do so, the listener **must** be configured before `on_error` since the latter needs a handler to the underlying listener.
55
+
56
+ #### Example
57
+ ```ruby
58
+ subscriber = PubsubClient.subscriber('some-topic')
59
+
60
+ subscriber.listener(concurrency: 4, auto_ack: false) do |data, received_message|
61
+ # Most clients will only need the first yielded arg.
62
+ # It is the same as calling received_message.data
63
+ end
64
+
65
+ # Optional
66
+ subscriber.on_error do |ex|
67
+ # Do something with the exception.
68
+ end
69
+
70
+ subscriber.subscribe # This will sleep
71
+ ```
72
+
73
+ By default, the underlying subscriber will use a concurrency of `8` threads and will acknowledge all messages. If these defaults are acceptable to the client, no arguments need to be passed in the call to `listener`.
74
+ ```ruby
75
+ subscriber.listener do |data, received_message|
76
+ # Do something
77
+ end
78
+ ```
79
+
55
80
  ## Development
56
81
 
57
82
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
58
83
 
59
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
84
+ To install this gem onto your local machine, run `bundle exec rake install`.
60
85
 
61
86
  ## Contributing
62
87
 
88
+ To contribute, open a pull request against `main`. Note that once your changes have been made, you should _not_ manually modify the `version.rb` or `CHANGELOG` as these will get updated automatically as part of the release process.
89
+
90
+ To release a new version, after you have merged your changes into `main`:
91
+ 1. Run the `gem-release` script. This can be found [here](https://github.com/apartmentlist/scripts/blob/main/bin/gem-release).
92
+ ```
93
+ path/to/gem-release [major/minor/patch] "Short message with changes"
94
+ ```
95
+ Note that the `Short message with changes` is what gets reflected in the releases of the repo.
96
+ 1. Run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). This step will update the `version.rb` and `CHANGELOG` files.
97
+
63
98
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pubsub_client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/pubsub_client/blob/master/CODE_OF_CONDUCT.md).
64
99
 
65
100
 
@@ -1,43 +1,46 @@
1
1
  require 'pubsub_client/version'
2
2
  require 'pubsub_client/null_publisher_factory'
3
+ require 'pubsub_client/null_subscriber_factory'
3
4
  require 'pubsub_client/publisher_factory'
5
+ require 'pubsub_client/subscriber_factory'
4
6
 
5
7
  module PubsubClient
6
8
  Error = Class.new(StandardError)
7
- CredentialsError = Class.new(Error)
8
9
  ConfigurationError = Class.new(Error)
9
-
10
- Config = Struct.new(:topic_name)
10
+ CredentialsError = Class.new(Error)
11
+ InvalidTopicError = Class.new(Error)
12
+ InvalidSubscriptionError = Class.new(Error)
11
13
 
12
14
  class << self
13
- def configure(&block)
14
- raise ConfigurationError, 'PubsubClient is already configured' if @publisher_factory
15
+ def stub!
16
+ raise ConfigurationError, 'PubsubClient is already configured' if @publisher_factory || @subscriber_factory
17
+ @publisher_factory = NullPublisherFactory.new
18
+ @subscriber_factory = NullSubscriberFactory.new
19
+ end
15
20
 
16
- unless ENV['GOOGLE_APPLICATION_CREDENTIALS']
17
- raise CredentialsError, 'GOOGLE_APPLICATION_CREDENTIALS must be set.'
18
- end
21
+ # @param message [String] The message to publish.
22
+ # @param topic [String] The name of the topic to publish to.
23
+ def publish(message, topic, &block)
24
+ ensure_credentials!
19
25
 
20
- config = Config.new
21
- yield config
26
+ @publisher_factory ||= PublisherFactory.new
27
+ @publisher_factory.build(topic).publish(message, &block)
28
+ end
22
29
 
23
- unless config.topic_name
24
- raise ConfigurationError, 'The topic_name must be configured.'
25
- end
30
+ # @param subscription [String] - The name of the topic to subscribe to.
31
+ def subscriber(subscription)
32
+ ensure_credentials!
26
33
 
27
- @publisher_factory = PublisherFactory.new(config.topic_name)
34
+ @subscriber_factory ||= SubscriberFactory.new
35
+ @subscriber_factory.build(subscription)
28
36
  end
29
37
 
30
- def stub!
31
- raise ConfigurationError, 'PubsubClient is already configured' if @publisher_factory
32
- @publisher_factory = NullPublisherFactory.new
33
- end
38
+ private
34
39
 
35
- def publish(message, &block)
36
- unless @publisher_factory
37
- raise ConfigurationError, 'PubsubClient must be configured or stubbed'
40
+ def ensure_credentials!
41
+ unless ENV['GOOGLE_APPLICATION_CREDENTIALS']
42
+ raise CredentialsError, 'GOOGLE_APPLICATION_CREDENTIALS must be set'
38
43
  end
39
-
40
- @publisher_factory.build.publish(message, &block)
41
44
  end
42
45
  end
43
46
  end
@@ -5,7 +5,7 @@ require_relative 'null_publisher'
5
5
  module PubsubClient
6
6
  # A null object to act as a publisher factory when clients are in dev or test
7
7
  class NullPublisherFactory
8
- def build
8
+ def build(*)
9
9
  NullPublisher.new
10
10
  end
11
11
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubsubClient
4
+ # A null object to act as a subscriber when clients are in dev or test
5
+ class NullSubscriber
6
+ # This adds a subset of the available methods on the
7
+ # Google::Cloud::PubSub::ReceivedMessage, which is what
8
+ # gets yielded by the subscription when configuring the listener.
9
+ # For a list of methods, see the following link:
10
+ # https://googleapis.dev/ruby/google-cloud-pubsub/latest/Google/Cloud/PubSub/ReceivedMessage.html
11
+ NullResult = Struct.new(:acknowledge!) do
12
+ def data
13
+ '{"key":"value"}'
14
+ end
15
+
16
+ def published_at
17
+ Time.now
18
+ end
19
+ end
20
+
21
+ def listener(*, &block)
22
+ res = NullResult.new
23
+ yield res.data, res
24
+ end
25
+
26
+ def subscribe
27
+ # no-op
28
+ end
29
+
30
+ def on_error(&block)
31
+ # no-op
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'null_subscriber'
4
+
5
+ module PubsubClient
6
+ # A null object to act as a subscriber factory when clients are in dev or test
7
+ class NullSubscriberFactory
8
+ def build(*)
9
+ NullSubscriber.new
10
+ end
11
+ end
12
+ end
@@ -4,12 +4,13 @@ require 'google/cloud/pubsub'
4
4
 
5
5
  module PubsubClient
6
6
  class Publisher
7
+ # @param topic [Google::Cloud::PubSub::Topic]
7
8
  def initialize(topic)
8
9
  @topic = topic
9
10
  end
10
11
 
11
- def publish(message, &block)
12
- topic.publish_async(message, &block)
12
+ def publish(message, attributes = {}, &block)
13
+ topic.publish_async(message, attributes, &block)
13
14
  end
14
15
 
15
16
  def flush
@@ -5,12 +5,14 @@ require_relative 'publisher'
5
5
  module PubsubClient
6
6
  # Build and memoize the Publisher, accounting for GRPC's requirements around forking.
7
7
  class PublisherFactory
8
- def initialize(topic_name)
9
- @topic_name = topic_name
8
+ def initialize
10
9
  @mutex = Mutex.new
10
+ @publishers = {}
11
11
  end
12
12
 
13
- def build
13
+ # @param topic_name [String]
14
+ # @return [Publisher]
15
+ def build(topic_name)
14
16
  # GRPC fails when attempting to use a connection created in a process that gets
15
17
  # forked with the message
16
18
  #
@@ -20,27 +22,28 @@ module PubsubClient
20
22
  # PubSub.
21
23
  #
22
24
  # To prevent incurring overhead, memoize the publisher per process.
23
- return @publisher if @publisher_pid == current_pid
25
+ return publishers[topic_name].publisher if publishers[topic_name]&.pid == current_pid
24
26
 
25
27
  # We are in a multi-threaded world and need to be careful not to build the publisher
26
28
  # in multiple threads. Lock the mutex so that only one thread can enter this block
27
29
  # at a time.
28
- mutex.synchronize do
30
+ @mutex.synchronize do
29
31
  # It's possible two threads made it to this point, but since we have a lock we
30
32
  # know that one will have built the publisher before the second is able to enter.
31
33
  # If we detect that case, then bail out so as to not rebuild the publisher.
32
- unless @publisher_pid == current_pid
33
- @publisher = build_publisher
34
- @publisher_pid = Process.pid
34
+ unless publishers[topic_name]&.pid == current_pid
35
+ publishers[topic_name] = Memo.new(build_publisher(topic_name), Process.pid)
35
36
  end
36
37
  end
37
38
 
38
- @publisher
39
+ publishers[topic_name].publisher
39
40
  end
40
41
 
41
42
  private
42
43
 
43
- attr_reader :mutex, :topic_name
44
+ attr_accessor :publishers
45
+
46
+ Memo = Struct.new(:publisher, :pid)
44
47
 
45
48
  # Used for testing to simulate when a process is forked. In those cases,
46
49
  # this helps us test that the `.build` method creates different publishers.
@@ -48,9 +51,10 @@ module PubsubClient
48
51
  Process.pid
49
52
  end
50
53
 
51
- def build_publisher
54
+ def build_publisher(topic_name)
52
55
  pubsub = Google::Cloud::PubSub.new
53
56
  topic = pubsub.topic(topic_name)
57
+ raise InvalidTopicError, "The topic #{topic_name} does not exist" unless topic
54
58
  publisher = Publisher.new(topic)
55
59
 
56
60
  at_exit { publisher.flush }
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'google/cloud/pubsub'
4
+
5
+ module PubsubClient
6
+ class Subscriber
7
+ DEFAULT_CONCURRENCY = 8
8
+
9
+ # @param subscription [Google::Cloud::PubSub::Subscription]
10
+ def initialize(subscription)
11
+ @subscription = subscription
12
+ end
13
+
14
+ # @param concurrency [Integer] - The number of threads to run the subscriber with. Default is 8.
15
+ # @param auto_ack [Boolean] - Flag to acknowledge the Pub/Sub message. A message must be acked
16
+ # to remove it from the topic. Default is `true`.
17
+ #
18
+ # @return [Google::Cloud::PubSub::Subscriber]
19
+ def listener(concurrency: DEFAULT_CONCURRENCY, auto_ack: true, &block)
20
+ @listener ||= begin
21
+ @subscription.listen(threads: { callback: concurrency }) do |received_message|
22
+ yield received_message.data, received_message
23
+ received_message.acknowledge! if auto_ack
24
+ end
25
+ end
26
+ end
27
+
28
+ def subscribe
29
+ raise ConfigurationError, 'A listener must be configured' unless @listener
30
+
31
+ begin
32
+ @listener.start
33
+
34
+ sleep
35
+ rescue SignalException
36
+ @listener.stop.wait!
37
+ end
38
+ end
39
+
40
+ def on_error(&block)
41
+ raise ConfigurationError, 'A listener must be configured' unless @listener
42
+
43
+ @listener.on_error do |exception|
44
+ yield exception
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'subscriber'
4
+
5
+ module PubsubClient
6
+ class SubscriberFactory
7
+ def initialize
8
+ @subscribers = {}
9
+ end
10
+
11
+ # @param subscription_name [String]
12
+ # @retrun [Subscriber]
13
+ def build(subscription_name)
14
+ if @subscribers.key?(subscription_name)
15
+ raise ConfigurationError, "PubsubClient already subscribed to #{subscription_name}"
16
+ end
17
+
18
+ @subscribers[subscription_name] = build_subscriber(subscription_name)
19
+ end
20
+
21
+ private
22
+
23
+ def build_subscriber(subscription_name)
24
+ pubsub = Google::Cloud::PubSub.new
25
+ subscription = pubsub.subscription(subscription_name)
26
+ raise InvalidSubscriptionError, "The subscription #{subscription_name} does not exist" unless subscription
27
+ Subscriber.new(subscription)
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,3 @@
1
1
  module PubsubClient
2
- VERSION = '0.1.0'
2
+ VERSION = '1.4.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pubsub_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Apartment List Platforms
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-25 00:00:00.000000000 Z
11
+ date: 2020-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -76,6 +76,7 @@ files:
76
76
  - ".ci"
77
77
  - ".gitignore"
78
78
  - ".rspec"
79
+ - CHANGELOG.txt
79
80
  - CODEOWNERS
80
81
  - CODE_OF_CONDUCT.md
81
82
  - Gemfile
@@ -87,8 +88,12 @@ files:
87
88
  - lib/pubsub_client.rb
88
89
  - lib/pubsub_client/null_publisher.rb
89
90
  - lib/pubsub_client/null_publisher_factory.rb
91
+ - lib/pubsub_client/null_subscriber.rb
92
+ - lib/pubsub_client/null_subscriber_factory.rb
90
93
  - lib/pubsub_client/publisher.rb
91
94
  - lib/pubsub_client/publisher_factory.rb
95
+ - lib/pubsub_client/subscriber.rb
96
+ - lib/pubsub_client/subscriber_factory.rb
92
97
  - lib/pubsub_client/version.rb
93
98
  - pubsub_client.gemspec
94
99
  homepage: https://github.com/apartmentlist/pubsub_client
@@ -113,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
118
  - !ruby/object:Gem::Version
114
119
  version: '0'
115
120
  requirements: []
116
- rubygems_version: 3.0.6
121
+ rubygems_version: 3.1.4
117
122
  signing_key:
118
123
  specification_version: 4
119
124
  summary: Google Pub/Sub Wrapper