pubsub_client 0.1.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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