cloudenvoy 0.1.0.dev → 0.4.0

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