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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +41 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +1 -0
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +32 -0
  7. data/Gemfile.lock +215 -1
  8. data/README.md +581 -7
  9. data/app/controllers/cloudenvoy/application_controller.rb +8 -0
  10. data/app/controllers/cloudenvoy/subscriber_controller.rb +59 -0
  11. data/cloudenvoy.gemspec +15 -2
  12. data/config/routes.rb +5 -0
  13. data/examples/rails/.ruby-version +1 -0
  14. data/examples/rails/Gemfile +15 -0
  15. data/examples/rails/Gemfile.lock +207 -0
  16. data/examples/rails/Procfile +1 -0
  17. data/examples/rails/README.md +31 -0
  18. data/examples/rails/Rakefile +8 -0
  19. data/examples/rails/app/assets/config/manifest.js +2 -0
  20. data/examples/rails/app/assets/images/.keep +0 -0
  21. data/examples/rails/app/assets/stylesheets/application.css +15 -0
  22. data/examples/rails/app/channels/application_cable/channel.rb +6 -0
  23. data/examples/rails/app/channels/application_cable/connection.rb +6 -0
  24. data/examples/rails/app/controllers/application_controller.rb +4 -0
  25. data/examples/rails/app/controllers/concerns/.keep +0 -0
  26. data/examples/rails/app/helpers/application_helper.rb +4 -0
  27. data/examples/rails/app/javascript/packs/application.js +15 -0
  28. data/examples/rails/app/jobs/application_job.rb +9 -0
  29. data/examples/rails/app/mailers/application_mailer.rb +6 -0
  30. data/examples/rails/app/models/application_record.rb +5 -0
  31. data/examples/rails/app/models/concerns/.keep +0 -0
  32. data/examples/rails/app/publishers/hello_publisher.rb +34 -0
  33. data/examples/rails/app/subscribers/hello_subscriber.rb +16 -0
  34. data/examples/rails/app/views/layouts/application.html.erb +14 -0
  35. data/examples/rails/app/views/layouts/mailer.html.erb +13 -0
  36. data/examples/rails/app/views/layouts/mailer.text.erb +1 -0
  37. data/examples/rails/bin/rails +6 -0
  38. data/examples/rails/bin/rake +6 -0
  39. data/examples/rails/bin/setup +35 -0
  40. data/examples/rails/config.ru +7 -0
  41. data/examples/rails/config/application.rb +19 -0
  42. data/examples/rails/config/boot.rb +7 -0
  43. data/examples/rails/config/cable.yml +10 -0
  44. data/examples/rails/config/credentials.yml.enc +1 -0
  45. data/examples/rails/config/database.yml +25 -0
  46. data/examples/rails/config/environment.rb +7 -0
  47. data/examples/rails/config/environments/development.rb +65 -0
  48. data/examples/rails/config/environments/production.rb +114 -0
  49. data/examples/rails/config/environments/test.rb +50 -0
  50. data/examples/rails/config/initializers/application_controller_renderer.rb +9 -0
  51. data/examples/rails/config/initializers/assets.rb +14 -0
  52. data/examples/rails/config/initializers/backtrace_silencers.rb +8 -0
  53. data/examples/rails/config/initializers/cloudenvoy.rb +22 -0
  54. data/examples/rails/config/initializers/content_security_policy.rb +29 -0
  55. data/examples/rails/config/initializers/cookies_serializer.rb +7 -0
  56. data/examples/rails/config/initializers/filter_parameter_logging.rb +6 -0
  57. data/examples/rails/config/initializers/inflections.rb +17 -0
  58. data/examples/rails/config/initializers/mime_types.rb +5 -0
  59. data/examples/rails/config/initializers/wrap_parameters.rb +16 -0
  60. data/examples/rails/config/locales/en.yml +33 -0
  61. data/examples/rails/config/master.key +1 -0
  62. data/examples/rails/config/puma.rb +37 -0
  63. data/examples/rails/config/routes.rb +4 -0
  64. data/examples/rails/config/spring.rb +8 -0
  65. data/examples/rails/config/storage.yml +34 -0
  66. data/examples/rails/db/development.sqlite3 +0 -0
  67. data/examples/rails/db/test.sqlite3 +0 -0
  68. data/examples/rails/lib/assets/.keep +0 -0
  69. data/examples/rails/log/.keep +0 -0
  70. data/examples/rails/public/404.html +67 -0
  71. data/examples/rails/public/422.html +67 -0
  72. data/examples/rails/public/500.html +66 -0
  73. data/examples/rails/public/apple-touch-icon-precomposed.png +0 -0
  74. data/examples/rails/public/apple-touch-icon.png +0 -0
  75. data/examples/rails/public/favicon.ico +0 -0
  76. data/examples/rails/storage/.keep +0 -0
  77. data/gemfiles/rails_5.2.gemfile +7 -0
  78. data/gemfiles/rails_5.2.gemfile.lock +251 -0
  79. data/gemfiles/rails_6.0.gemfile +7 -0
  80. data/gemfiles/rails_6.0.gemfile.lock +267 -0
  81. data/gemfiles/semantic_logger_3.4.gemfile +7 -0
  82. data/gemfiles/semantic_logger_3.4.gemfile.lock +265 -0
  83. data/gemfiles/semantic_logger_4.6.gemfile +7 -0
  84. data/gemfiles/semantic_logger_4.6.gemfile.lock +265 -0
  85. data/gemfiles/semantic_logger_4.7.0.gemfile +7 -0
  86. data/gemfiles/semantic_logger_4.7.0.gemfile.lock +265 -0
  87. data/gemfiles/semantic_logger_4.7.2.gemfile +7 -0
  88. data/gemfiles/semantic_logger_4.7.2.gemfile.lock +265 -0
  89. data/lib/cloudenvoy.rb +96 -2
  90. data/lib/cloudenvoy/authentication_error.rb +6 -0
  91. data/lib/cloudenvoy/authenticator.rb +57 -0
  92. data/lib/cloudenvoy/backend/google_pub_sub.rb +146 -0
  93. data/lib/cloudenvoy/backend/memory_pub_sub.rb +89 -0
  94. data/lib/cloudenvoy/config.rb +165 -0
  95. data/lib/cloudenvoy/engine.rb +20 -0
  96. data/lib/cloudenvoy/invalid_subscriber_error.rb +6 -0
  97. data/lib/cloudenvoy/logger_wrapper.rb +167 -0
  98. data/lib/cloudenvoy/message.rb +96 -0
  99. data/lib/cloudenvoy/middleware/chain.rb +250 -0
  100. data/lib/cloudenvoy/pub_sub_client.rb +76 -0
  101. data/lib/cloudenvoy/publisher.rb +211 -0
  102. data/lib/cloudenvoy/publisher_logger.rb +32 -0
  103. data/lib/cloudenvoy/subscriber.rb +222 -0
  104. data/lib/cloudenvoy/subscriber_logger.rb +26 -0
  105. data/lib/cloudenvoy/subscription.rb +19 -0
  106. data/lib/cloudenvoy/testing.rb +106 -0
  107. data/lib/cloudenvoy/topic.rb +19 -0
  108. data/lib/cloudenvoy/version.rb +1 -1
  109. data/lib/tasks/cloudenvoy.rake +61 -0
  110. 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