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 +4 -4
- data/CHANGELOG.txt +31 -0
- data/README.md +48 -13
- data/lib/pubsub_client.rb +26 -23
- data/lib/pubsub_client/null_publisher_factory.rb +1 -1
- data/lib/pubsub_client/null_subscriber.rb +34 -0
- data/lib/pubsub_client/null_subscriber_factory.rb +12 -0
- data/lib/pubsub_client/publisher.rb +3 -2
- data/lib/pubsub_client/publisher_factory.rb +15 -11
- data/lib/pubsub_client/subscriber.rb +48 -0
- data/lib/pubsub_client/subscriber_factory.rb +30 -0
- data/lib/pubsub_client/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fcd27cf1ed4e02b4b8bae0e9b2f5227d970f158196b68d903cbfa2b031c2b1eb
|
4
|
+
data.tar.gz: 415e17dfb4465a44ad9ffe3aeabe8b116a8f47b79f77e3a408d67da8c44409fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bacbf023017f2ba125de0be70c2040e697c5c7927f24569f6e5af043d78eefc0eee813af9bfaeadbd4fde3213554bbe135ef011a3343408552b69ea217aa4bac
|
7
|
+
data.tar.gz: 9a84a7a5c32ba17ea5523e3b0831935dc162d89a0cc7dcd89d1a3863951796dee63fa59639d583de72965d75e42047ca0ec275b1dde3541ae4b40d818e1e8359
|
data/CHANGELOG.txt
ADDED
@@ -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.
|
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
|
-
|
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
|
-
|
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`.
|
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
|
|
data/lib/pubsub_client.rb
CHANGED
@@ -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
|
-
|
10
|
+
CredentialsError = Class.new(Error)
|
11
|
+
InvalidTopicError = Class.new(Error)
|
12
|
+
InvalidSubscriptionError = Class.new(Error)
|
11
13
|
|
12
14
|
class << self
|
13
|
-
def
|
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
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
26
|
+
@publisher_factory ||= PublisherFactory.new
|
27
|
+
@publisher_factory.build(topic).publish(message, &block)
|
28
|
+
end
|
22
29
|
|
23
|
-
|
24
|
-
|
25
|
-
|
30
|
+
# @param subscription [String] - The name of the topic to subscribe to.
|
31
|
+
def subscriber(subscription)
|
32
|
+
ensure_credentials!
|
26
33
|
|
27
|
-
@
|
34
|
+
@subscriber_factory ||= SubscriberFactory.new
|
35
|
+
@subscriber_factory.build(subscription)
|
28
36
|
end
|
29
37
|
|
30
|
-
|
31
|
-
raise ConfigurationError, 'PubsubClient is already configured' if @publisher_factory
|
32
|
-
@publisher_factory = NullPublisherFactory.new
|
33
|
-
end
|
38
|
+
private
|
34
39
|
|
35
|
-
def
|
36
|
-
unless
|
37
|
-
raise
|
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
|
@@ -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
|
9
|
-
@topic_name = topic_name
|
8
|
+
def initialize
|
10
9
|
@mutex = Mutex.new
|
10
|
+
@publishers = {}
|
11
11
|
end
|
12
12
|
|
13
|
-
|
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
|
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
|
33
|
-
|
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
|
-
|
39
|
+
publishers[topic_name].publisher
|
39
40
|
end
|
40
41
|
|
41
42
|
private
|
42
43
|
|
43
|
-
|
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
|
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:
|
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-
|
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.
|
121
|
+
rubygems_version: 3.1.4
|
117
122
|
signing_key:
|
118
123
|
specification_version: 4
|
119
124
|
summary: Google Pub/Sub Wrapper
|