cloudenvoy 0.1.0.dev → 0.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/.github/workflows/test.yml +41 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +1 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +32 -0
- data/Gemfile.lock +215 -1
- data/README.md +581 -7
- data/app/controllers/cloudenvoy/application_controller.rb +8 -0
- data/app/controllers/cloudenvoy/subscriber_controller.rb +59 -0
- data/cloudenvoy.gemspec +15 -2
- data/config/routes.rb +5 -0
- data/examples/rails/.ruby-version +1 -0
- data/examples/rails/Gemfile +15 -0
- data/examples/rails/Gemfile.lock +207 -0
- data/examples/rails/Procfile +1 -0
- data/examples/rails/README.md +31 -0
- data/examples/rails/Rakefile +8 -0
- data/examples/rails/app/assets/config/manifest.js +2 -0
- data/examples/rails/app/assets/images/.keep +0 -0
- data/examples/rails/app/assets/stylesheets/application.css +15 -0
- data/examples/rails/app/channels/application_cable/channel.rb +6 -0
- data/examples/rails/app/channels/application_cable/connection.rb +6 -0
- data/examples/rails/app/controllers/application_controller.rb +4 -0
- data/examples/rails/app/controllers/concerns/.keep +0 -0
- data/examples/rails/app/helpers/application_helper.rb +4 -0
- data/examples/rails/app/javascript/packs/application.js +15 -0
- data/examples/rails/app/jobs/application_job.rb +9 -0
- data/examples/rails/app/mailers/application_mailer.rb +6 -0
- data/examples/rails/app/models/application_record.rb +5 -0
- data/examples/rails/app/models/concerns/.keep +0 -0
- data/examples/rails/app/publishers/hello_publisher.rb +34 -0
- data/examples/rails/app/subscribers/hello_subscriber.rb +16 -0
- data/examples/rails/app/views/layouts/application.html.erb +14 -0
- data/examples/rails/app/views/layouts/mailer.html.erb +13 -0
- data/examples/rails/app/views/layouts/mailer.text.erb +1 -0
- data/examples/rails/bin/rails +6 -0
- data/examples/rails/bin/rake +6 -0
- data/examples/rails/bin/setup +35 -0
- data/examples/rails/config.ru +7 -0
- data/examples/rails/config/application.rb +19 -0
- data/examples/rails/config/boot.rb +7 -0
- data/examples/rails/config/cable.yml +10 -0
- data/examples/rails/config/credentials.yml.enc +1 -0
- data/examples/rails/config/database.yml +25 -0
- data/examples/rails/config/environment.rb +7 -0
- data/examples/rails/config/environments/development.rb +65 -0
- data/examples/rails/config/environments/production.rb +114 -0
- data/examples/rails/config/environments/test.rb +50 -0
- data/examples/rails/config/initializers/application_controller_renderer.rb +9 -0
- data/examples/rails/config/initializers/assets.rb +14 -0
- data/examples/rails/config/initializers/backtrace_silencers.rb +8 -0
- data/examples/rails/config/initializers/cloudenvoy.rb +22 -0
- data/examples/rails/config/initializers/content_security_policy.rb +29 -0
- data/examples/rails/config/initializers/cookies_serializer.rb +7 -0
- data/examples/rails/config/initializers/filter_parameter_logging.rb +6 -0
- data/examples/rails/config/initializers/inflections.rb +17 -0
- data/examples/rails/config/initializers/mime_types.rb +5 -0
- data/examples/rails/config/initializers/wrap_parameters.rb +16 -0
- data/examples/rails/config/locales/en.yml +33 -0
- data/examples/rails/config/master.key +1 -0
- data/examples/rails/config/puma.rb +37 -0
- data/examples/rails/config/routes.rb +4 -0
- data/examples/rails/config/spring.rb +8 -0
- data/examples/rails/config/storage.yml +34 -0
- data/examples/rails/db/development.sqlite3 +0 -0
- data/examples/rails/db/test.sqlite3 +0 -0
- data/examples/rails/lib/assets/.keep +0 -0
- data/examples/rails/log/.keep +0 -0
- data/examples/rails/public/404.html +67 -0
- data/examples/rails/public/422.html +67 -0
- data/examples/rails/public/500.html +66 -0
- data/examples/rails/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/rails/public/apple-touch-icon.png +0 -0
- data/examples/rails/public/favicon.ico +0 -0
- data/examples/rails/storage/.keep +0 -0
- data/gemfiles/rails_5.2.gemfile +7 -0
- data/gemfiles/rails_5.2.gemfile.lock +251 -0
- data/gemfiles/rails_6.0.gemfile +7 -0
- data/gemfiles/rails_6.0.gemfile.lock +267 -0
- data/gemfiles/semantic_logger_3.4.gemfile +7 -0
- data/gemfiles/semantic_logger_3.4.gemfile.lock +265 -0
- data/gemfiles/semantic_logger_4.6.gemfile +7 -0
- data/gemfiles/semantic_logger_4.6.gemfile.lock +265 -0
- data/gemfiles/semantic_logger_4.7.0.gemfile +7 -0
- data/gemfiles/semantic_logger_4.7.0.gemfile.lock +265 -0
- data/gemfiles/semantic_logger_4.7.2.gemfile +7 -0
- data/gemfiles/semantic_logger_4.7.2.gemfile.lock +265 -0
- data/lib/cloudenvoy.rb +96 -2
- data/lib/cloudenvoy/authentication_error.rb +6 -0
- data/lib/cloudenvoy/authenticator.rb +57 -0
- data/lib/cloudenvoy/backend/google_pub_sub.rb +146 -0
- data/lib/cloudenvoy/backend/memory_pub_sub.rb +89 -0
- data/lib/cloudenvoy/config.rb +165 -0
- data/lib/cloudenvoy/engine.rb +20 -0
- data/lib/cloudenvoy/invalid_subscriber_error.rb +6 -0
- data/lib/cloudenvoy/logger_wrapper.rb +167 -0
- data/lib/cloudenvoy/message.rb +96 -0
- data/lib/cloudenvoy/middleware/chain.rb +250 -0
- data/lib/cloudenvoy/pub_sub_client.rb +76 -0
- data/lib/cloudenvoy/publisher.rb +211 -0
- data/lib/cloudenvoy/publisher_logger.rb +32 -0
- data/lib/cloudenvoy/subscriber.rb +222 -0
- data/lib/cloudenvoy/subscriber_logger.rb +26 -0
- data/lib/cloudenvoy/subscription.rb +19 -0
- data/lib/cloudenvoy/testing.rb +106 -0
- data/lib/cloudenvoy/topic.rb +19 -0
- data/lib/cloudenvoy/version.rb +1 -1
- data/lib/tasks/cloudenvoy.rake +61 -0
- metadata +263 -6
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudenvoy
|
|
4
|
+
# Interface to publishing backend (GCP, emulator or memory backend)
|
|
5
|
+
class PubSubClient
|
|
6
|
+
#
|
|
7
|
+
# Return the backend to use for sending messages.
|
|
8
|
+
#
|
|
9
|
+
# @return [Module<Cloudenvoy::Backend::MemoryPubSub, Cloudenvoy::Backend::GoogleCloudTask>] The backend class.
|
|
10
|
+
#
|
|
11
|
+
def self.backend
|
|
12
|
+
# Re-evaluate backend every time if testing mode enabled
|
|
13
|
+
@backend = nil if defined?(Cloudenvoy::Testing)
|
|
14
|
+
|
|
15
|
+
@backend ||= begin
|
|
16
|
+
if defined?(Cloudenvoy::Testing) && Cloudenvoy::Testing.in_memory?
|
|
17
|
+
require 'cloudenvoy/backend/memory_pub_sub'
|
|
18
|
+
Backend::MemoryPubSub
|
|
19
|
+
else
|
|
20
|
+
require 'cloudenvoy/backend/google_pub_sub'
|
|
21
|
+
Backend::GooglePubSub
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Publish a message to a topic.
|
|
28
|
+
#
|
|
29
|
+
# @param [String] topic The name of the topic
|
|
30
|
+
# @param [Hash, String] payload The message content.
|
|
31
|
+
# @param [Hash] attrs The message attributes.
|
|
32
|
+
#
|
|
33
|
+
# @return [Cloudenvoy::Message] The created message.
|
|
34
|
+
#
|
|
35
|
+
def self.publish(topic, payload, attrs = {})
|
|
36
|
+
backend.publish(topic, payload, attrs)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
#
|
|
40
|
+
# Create or update a subscription for a specific topic.
|
|
41
|
+
#
|
|
42
|
+
# @param [String] topic The name of the topic
|
|
43
|
+
# @param [String] name The name of the subscription
|
|
44
|
+
# @param [Hash] opts The subscription configuration options
|
|
45
|
+
# @option opts [Integer] :deadline The maximum number of seconds after a subscriber receives a message
|
|
46
|
+
# before the subscriber should acknowledge the message.
|
|
47
|
+
# @option opts [Boolean] :retain_acked Indicates whether to retain acknowledged messages. If true,
|
|
48
|
+
# then messages are not expunged from the subscription's backlog, even if they are acknowledged,
|
|
49
|
+
# until they fall out of the retention window. Default is false.
|
|
50
|
+
# @option opts [<Type>] :retention How long to retain unacknowledged messages in the subscription's
|
|
51
|
+
# backlog, from the moment a message is published. If retain_acked is true, then this also configures
|
|
52
|
+
# the retention of acknowledged messages, and thus configures how far back in time a Subscription#seek
|
|
53
|
+
# can be done. Cannot be more than 604,800 seconds (7 days) or less than 600 seconds (10 minutes).
|
|
54
|
+
# Default is 604,800 seconds (7 days).
|
|
55
|
+
# @option opts [String] :filter An expression written in the Cloud Pub/Sub filter language.
|
|
56
|
+
# If non-empty, then only Message instances whose attributes field matches the filter are delivered
|
|
57
|
+
# on this subscription. If empty, then no messages are filtered out. Optional.
|
|
58
|
+
#
|
|
59
|
+
# @return [Cloudenvoy::Subscription] The upserted subscription.
|
|
60
|
+
#
|
|
61
|
+
def self.upsert_subscription(topic, name, opts = {})
|
|
62
|
+
backend.upsert_subscription(topic, name, opts)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
# Create or update a topic.
|
|
67
|
+
#
|
|
68
|
+
# @param [String] topic The topic name.
|
|
69
|
+
#
|
|
70
|
+
# @return [Cloudenvoy::Topic] The upserted/topic.
|
|
71
|
+
#
|
|
72
|
+
def self.upsert_topic(topic)
|
|
73
|
+
backend.upsert_topic(topic)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudenvoy
|
|
4
|
+
# Use this module to define publishers. The module provides
|
|
5
|
+
# a simple DSL for transforming and publishing data objects to
|
|
6
|
+
# Pub/Sub.
|
|
7
|
+
#
|
|
8
|
+
# Publishers must at least implement the `payload` method, which defines
|
|
9
|
+
# how arguments are mapped to a Hash or String message.
|
|
10
|
+
#
|
|
11
|
+
# E.g.
|
|
12
|
+
#
|
|
13
|
+
# class UserPublisher
|
|
14
|
+
# include Cloudenvoy::Publisher
|
|
15
|
+
#
|
|
16
|
+
# # Specify publishing options
|
|
17
|
+
# cloudenvoy_options topic: 'my-topic'
|
|
18
|
+
#
|
|
19
|
+
# # Format message objects
|
|
20
|
+
# def payload(user)
|
|
21
|
+
# {
|
|
22
|
+
# id: user.id,
|
|
23
|
+
# name: user.name
|
|
24
|
+
# }
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
module Publisher
|
|
29
|
+
# Add class method to including class
|
|
30
|
+
def self.included(base)
|
|
31
|
+
base.extend(ClassMethods)
|
|
32
|
+
base.attr_accessor :msg_args, :message, :publishing_started_at, :publishing_ended_at
|
|
33
|
+
|
|
34
|
+
# Register subscriber
|
|
35
|
+
Cloudenvoy.publishers.add(base)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Module class methods
|
|
39
|
+
module ClassMethods
|
|
40
|
+
#
|
|
41
|
+
# Set the publisher runtime options.
|
|
42
|
+
#
|
|
43
|
+
# @param [Hash] opts The publisher options.
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash] The options set.
|
|
46
|
+
#
|
|
47
|
+
def cloudenvoy_options(opts = {})
|
|
48
|
+
opt_list = opts&.map { |k, v| [k.to_sym, v] } || [] # symbolize
|
|
49
|
+
@cloudenvoy_options_hash = Hash[opt_list]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# Return the publisher runtime options.
|
|
54
|
+
#
|
|
55
|
+
# @return [Hash] The publisher runtime options.
|
|
56
|
+
#
|
|
57
|
+
def cloudenvoy_options_hash
|
|
58
|
+
@cloudenvoy_options_hash || {}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#
|
|
62
|
+
# Return the default topic this publisher publishes to.
|
|
63
|
+
# Raises an error if no default topic has been defined.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] The default topic.
|
|
66
|
+
#
|
|
67
|
+
def default_topic
|
|
68
|
+
cloudenvoy_options_hash[:topic]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# Format and publish objects to Pub/Sub.
|
|
73
|
+
#
|
|
74
|
+
# @param [Any] *args The publisher arguments
|
|
75
|
+
#
|
|
76
|
+
# @return [Cloudenvoy::Message] The created message.
|
|
77
|
+
#
|
|
78
|
+
def publish(*args)
|
|
79
|
+
new(msg_args: args).publish
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
#
|
|
83
|
+
# Setup the default topic for this publisher.
|
|
84
|
+
#
|
|
85
|
+
# @return [Cloudenvoy::Topic] The upserted/topic.
|
|
86
|
+
#
|
|
87
|
+
def setup
|
|
88
|
+
return nil unless default_topic
|
|
89
|
+
|
|
90
|
+
PubSubClient.upsert_topic(default_topic)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#
|
|
95
|
+
# Build a new publisher instance.
|
|
96
|
+
#
|
|
97
|
+
# @param [Array<any>] msg_args The list of payload args.
|
|
98
|
+
#
|
|
99
|
+
def initialize(msg_args: nil)
|
|
100
|
+
@msg_args = msg_args || []
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
#
|
|
104
|
+
# Return the topic to publish to. The topic name
|
|
105
|
+
# can be dynamically evaluated at runtime based on
|
|
106
|
+
# publishing arguments.
|
|
107
|
+
#
|
|
108
|
+
# Defaults to the topic specified via cloudenvoy_options.
|
|
109
|
+
#
|
|
110
|
+
# @param [Any] *_args The publisher arguments.
|
|
111
|
+
#
|
|
112
|
+
# @return [String] The topic name.
|
|
113
|
+
#
|
|
114
|
+
def topic(*_args)
|
|
115
|
+
self.class.default_topic
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
# Publisher can optionally define message attributes (metadata).
|
|
120
|
+
# Message attributes are sent to Pub/Sub and can be used
|
|
121
|
+
# for filtering.
|
|
122
|
+
#
|
|
123
|
+
# @param [Any] *_args The publisher arguments.
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash] The message attributes.
|
|
126
|
+
#
|
|
127
|
+
def metadata(*_args)
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
#
|
|
132
|
+
# Return the Cloudenvoy logger instance.
|
|
133
|
+
#
|
|
134
|
+
# @return [Logger, any] The cloudenvoy logger.
|
|
135
|
+
#
|
|
136
|
+
def logger
|
|
137
|
+
@logger ||= PublisherLogger.new(self)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#
|
|
141
|
+
# Return the time taken (in seconds) to format and publish the message. This duration
|
|
142
|
+
# includes the middlewares and the actual publish method.
|
|
143
|
+
#
|
|
144
|
+
# @return [Float] The time taken in seconds as a floating point number.
|
|
145
|
+
#
|
|
146
|
+
def publishing_duration
|
|
147
|
+
return 0.0 unless publishing_ended_at && publishing_started_at
|
|
148
|
+
|
|
149
|
+
(publishing_ended_at - publishing_started_at).ceil(3)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
#
|
|
153
|
+
# Send the instantiated Publisher (message) to
|
|
154
|
+
# Pub/Sub.
|
|
155
|
+
#
|
|
156
|
+
# @return [Cloudenvoy::Message] The created message.
|
|
157
|
+
#
|
|
158
|
+
def publish
|
|
159
|
+
# Format and publish message
|
|
160
|
+
resp = execute_middleware_chain
|
|
161
|
+
|
|
162
|
+
# Log job completion and return result
|
|
163
|
+
logger.info("Published message in #{publishing_duration}s") { { duration: publishing_duration } }
|
|
164
|
+
resp
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
logger.info("Publishing failed after #{publishing_duration}s") { { duration: publishing_duration } }
|
|
167
|
+
raise(e)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
#=============================
|
|
171
|
+
# Private
|
|
172
|
+
#=============================
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
#
|
|
176
|
+
# Internal logic used to build, publish and capture message on the
|
|
177
|
+
# publisher.
|
|
178
|
+
#
|
|
179
|
+
# @return [Cloudenvoy::Message] The published message
|
|
180
|
+
#
|
|
181
|
+
def publish_message
|
|
182
|
+
# Publish message to pub/sub and attach created
|
|
183
|
+
# message to publisher
|
|
184
|
+
self.message = PubSubClient.publish(
|
|
185
|
+
topic(*msg_args),
|
|
186
|
+
payload(*msg_args),
|
|
187
|
+
metadata(*msg_args)
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
#
|
|
192
|
+
# Execute the subscriber process method through the middleware chain.
|
|
193
|
+
#
|
|
194
|
+
# @return [Any] The result of the perform method.
|
|
195
|
+
#
|
|
196
|
+
def execute_middleware_chain
|
|
197
|
+
self.publishing_started_at = Time.now
|
|
198
|
+
|
|
199
|
+
Cloudenvoy.config.publisher_middleware.invoke(self) do
|
|
200
|
+
begin
|
|
201
|
+
publish_message
|
|
202
|
+
rescue StandardError => e
|
|
203
|
+
try(:on_error, e)
|
|
204
|
+
return raise(e)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
ensure
|
|
208
|
+
self.publishing_ended_at = Time.now
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudenvoy
|
|
4
|
+
# Logger configuration for publishers
|
|
5
|
+
class PublisherLogger < LoggerWrapper
|
|
6
|
+
#
|
|
7
|
+
# The subscriber default context processor.
|
|
8
|
+
#
|
|
9
|
+
# @return [Proc] The context processor proc.
|
|
10
|
+
#
|
|
11
|
+
def self.default_context_processor
|
|
12
|
+
@default_context_processor ||= ->(loggable) { loggable.message&.to_h&.slice(:id, :metadata, :topic) || {} }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
#
|
|
16
|
+
# Format main log message.
|
|
17
|
+
#
|
|
18
|
+
# @param [String] msg The message to log.
|
|
19
|
+
#
|
|
20
|
+
# @return [String] The formatted log message
|
|
21
|
+
#
|
|
22
|
+
def formatted_message(msg)
|
|
23
|
+
[
|
|
24
|
+
'[Cloudenvoy]',
|
|
25
|
+
"[#{loggable.class}]",
|
|
26
|
+
loggable.message&.id ? "[#{loggable.message.id}]" : nil,
|
|
27
|
+
' ',
|
|
28
|
+
msg
|
|
29
|
+
].compact.join
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cloudenvoy
|
|
4
|
+
# Use this module to define subscribers. Subscribers must implement
|
|
5
|
+
# the message processsing logic in the `process` method.
|
|
6
|
+
#
|
|
7
|
+
# E.g.
|
|
8
|
+
#
|
|
9
|
+
# class UserSubscriber
|
|
10
|
+
# include Cloudenvoy::Subscriber
|
|
11
|
+
#
|
|
12
|
+
# # Specify subscription options
|
|
13
|
+
# cloudenvoy_options topics: ['my-topic']
|
|
14
|
+
#
|
|
15
|
+
# # Process message objects
|
|
16
|
+
# def process(message)
|
|
17
|
+
# ...do something...
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
module Subscriber
|
|
22
|
+
# Add class method to including class
|
|
23
|
+
def self.included(base)
|
|
24
|
+
base.extend(ClassMethods)
|
|
25
|
+
base.attr_accessor :message, :process_started_at, :process_ended_at
|
|
26
|
+
|
|
27
|
+
# Register subscriber
|
|
28
|
+
Cloudenvoy.subscribers.add(base)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# Return the subscriber class for the provided
|
|
33
|
+
# class name.
|
|
34
|
+
#
|
|
35
|
+
# @param [String] sub_uri The subscription uri.
|
|
36
|
+
#
|
|
37
|
+
# @return [Class] The subscriber class
|
|
38
|
+
#
|
|
39
|
+
def self.from_sub_uri(sub_uri)
|
|
40
|
+
klass_name = Subscriber.parse_sub_uri(sub_uri)[0]
|
|
41
|
+
|
|
42
|
+
# Check that subscriber class is a valid subscriber
|
|
43
|
+
sub_klass = Object.const_get(klass_name.camelize)
|
|
44
|
+
|
|
45
|
+
sub_klass.include?(self) ? sub_klass : nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#
|
|
49
|
+
# Parse the subscription name and return the subscriber name and topic.
|
|
50
|
+
#
|
|
51
|
+
# @param [String] sub_uri The subscription URI
|
|
52
|
+
#
|
|
53
|
+
# @return [Array<String,String>] A tuple [subscriber_name, topic ]
|
|
54
|
+
#
|
|
55
|
+
def self.parse_sub_uri(sub_uri)
|
|
56
|
+
sub_uri.split('/').last.split('.').last(2)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# Execute a subscriber from a payload object received from
|
|
61
|
+
# Pub/Sub.
|
|
62
|
+
#
|
|
63
|
+
# @param [Hash] input_payload The Pub/Sub webhook hash describing
|
|
64
|
+
# the message to process.
|
|
65
|
+
#
|
|
66
|
+
# @return [Any] The subscriber processing return value.
|
|
67
|
+
#
|
|
68
|
+
def self.execute_from_descriptor(input_payload)
|
|
69
|
+
message = Message.from_descriptor(input_payload)
|
|
70
|
+
subscriber = message.subscriber || raise(InvalidSubscriberError)
|
|
71
|
+
subscriber.execute
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Module class methods
|
|
75
|
+
module ClassMethods
|
|
76
|
+
#
|
|
77
|
+
# Set the subscriber runtime options.
|
|
78
|
+
#
|
|
79
|
+
# @param [Hash] opts The subscriber options.
|
|
80
|
+
#
|
|
81
|
+
# @return [Hash] The options set.
|
|
82
|
+
#
|
|
83
|
+
def cloudenvoy_options(opts = {})
|
|
84
|
+
opt_list = opts&.map { |k, v| [k.to_sym, v] } || [] # symbolize
|
|
85
|
+
@cloudenvoy_options_hash = Hash[opt_list]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#
|
|
89
|
+
# Return the subscriber runtime options.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] The subscriber runtime options.
|
|
92
|
+
#
|
|
93
|
+
def cloudenvoy_options_hash
|
|
94
|
+
@cloudenvoy_options_hash || {}
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
#
|
|
98
|
+
# Return the list of topics this subscriber listens
|
|
99
|
+
# to.
|
|
100
|
+
#
|
|
101
|
+
# @return [Array<Hash>] The list of subscribed topics.
|
|
102
|
+
#
|
|
103
|
+
def topics
|
|
104
|
+
@topics ||= [cloudenvoy_options_hash[:topic], cloudenvoy_options_hash[:topics]].flatten.compact.map do |t|
|
|
105
|
+
t.is_a?(String) ? { name: t } : t
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
#
|
|
110
|
+
# Return the subscription name used by this subscriber
|
|
111
|
+
# to subscribe to a specific topic.
|
|
112
|
+
#
|
|
113
|
+
# @return [String] The subscription name.
|
|
114
|
+
#
|
|
115
|
+
def subscription_name(topic)
|
|
116
|
+
[
|
|
117
|
+
Cloudenvoy.config.gcp_sub_prefix.tr('.', '-'),
|
|
118
|
+
to_s.underscore,
|
|
119
|
+
topic
|
|
120
|
+
].join('.')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#
|
|
124
|
+
# Create the Subscriber subscription in Pub/Sub.
|
|
125
|
+
#
|
|
126
|
+
# @return [Array<Cloudenvoy::Subscription>] The upserted subscription.
|
|
127
|
+
#
|
|
128
|
+
def setup
|
|
129
|
+
topics.map do |t|
|
|
130
|
+
topic_name = t[:name] || t['name']
|
|
131
|
+
sub_opts = t.reject { |k, _| k.to_sym == :name }
|
|
132
|
+
PubSubClient.upsert_subscription(topic_name, subscription_name(topic_name), sub_opts)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
#
|
|
138
|
+
# Build a new subscriber instance.
|
|
139
|
+
#
|
|
140
|
+
# @param [Cloudenvoy::Message] message The message to process.
|
|
141
|
+
#
|
|
142
|
+
def initialize(message:)
|
|
143
|
+
@message = message
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#
|
|
147
|
+
# Return the Cloudenvoy logger instance.
|
|
148
|
+
#
|
|
149
|
+
# @return [Logger, any] The cloudenvoy logger.
|
|
150
|
+
#
|
|
151
|
+
def logger
|
|
152
|
+
@logger ||= SubscriberLogger.new(self)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
#
|
|
156
|
+
# Return the time taken (in seconds) to process the message. This duration
|
|
157
|
+
# includes the middlewares and the actual process method.
|
|
158
|
+
#
|
|
159
|
+
# @return [Float] The time taken in seconds as a floating point number.
|
|
160
|
+
#
|
|
161
|
+
def process_duration
|
|
162
|
+
return 0.0 unless process_ended_at && process_started_at
|
|
163
|
+
|
|
164
|
+
(process_ended_at - process_started_at).ceil(3)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
#
|
|
168
|
+
# Execute the subscriber's logic.
|
|
169
|
+
#
|
|
170
|
+
# @return [Any] The logic return value
|
|
171
|
+
#
|
|
172
|
+
def execute
|
|
173
|
+
logger.info('Processing message...')
|
|
174
|
+
|
|
175
|
+
# Process message
|
|
176
|
+
resp = execute_middleware_chain
|
|
177
|
+
|
|
178
|
+
# Log processing completion and return result
|
|
179
|
+
logger.info("Processing done after #{process_duration}s") { { duration: process_duration } }
|
|
180
|
+
resp
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
logger.info("Processing failed after #{process_duration}s") { { duration: process_duration } }
|
|
183
|
+
raise(e)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
#
|
|
187
|
+
# Equality operator.
|
|
188
|
+
#
|
|
189
|
+
# @param [Any] other The object to compare.
|
|
190
|
+
#
|
|
191
|
+
# @return [Boolean] True if the object is equal.
|
|
192
|
+
#
|
|
193
|
+
def ==(other)
|
|
194
|
+
other.is_a?(self.class) && other.message == message
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
#=============================
|
|
198
|
+
# Private
|
|
199
|
+
#=============================
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
#
|
|
203
|
+
# Execute the subscriber process method through the middleware chain.
|
|
204
|
+
#
|
|
205
|
+
# @return [Any] The result of the perform method.
|
|
206
|
+
#
|
|
207
|
+
def execute_middleware_chain
|
|
208
|
+
self.process_started_at = Time.now
|
|
209
|
+
|
|
210
|
+
Cloudenvoy.config.subscriber_middleware.invoke(self) do
|
|
211
|
+
begin
|
|
212
|
+
process(message)
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
try(:on_error, e)
|
|
215
|
+
return raise(e)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
ensure
|
|
219
|
+
self.process_ended_at = Time.now
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|