pub_sub_model_sync 0.1.4 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eda619ab984f99ffe46cfef36fa380aaa6766c1564081dcc4e5e53e6f7976e0
4
- data.tar.gz: dd056d897c96bac38a20c115fee501cbb1383038483f74849bbb0e7217568163
3
+ metadata.gz: 3016ebc69eba9ccb8b168734db1b947b77a4c9400f48d8228f979a7c1ec9eab8
4
+ data.tar.gz: 82187606c0411ece3eb0d78db59a8794019e80acd5374ffd92b95b0eee4368f7
5
5
  SHA512:
6
- metadata.gz: 75796a135c2f820d89303c29c9bb6dc78eaa1e207391a07ba86a45f547a7007b4b00f767bb949d354c5a90c504b5d2d287619fd3106bca1dce71e75588ac6173
7
- data.tar.gz: fe4829f970142bfea0089beb4e86a27c9d1e08049213372ae77f94b9d02d16752218d58e88b25ac1f34848931da3651e4405caa3a8932401c1410d7f04e3d0da
6
+ metadata.gz: f4335695f26d69f091a2ea1726221aab4b07b712827318adfe16f2d1275cde21a0860ce7562ed266f6a0661eb2b8d634abea0956a4478ad55f70e6c199c8a364
7
+ data.tar.gz: 9252e45ab27b332d9b50afc93da62a0a039c707a1bb3b219ff4e60c1c4f3dae8eb7a85024fc3d1dab87c006e448e60cf3d1f3d953dad8cc8fced24bd375b47cd
@@ -1,5 +1,25 @@
1
1
  # Change Log
2
2
 
3
+ # 0.2.3 (April 15, 2020)
4
+ - Improve helper names
5
+ - feat: perform manual sync with custom settings
6
+ - fix for "IO timeout when reading 7 bytes" error (Rabbit)
7
+ - style: do not print processed message when failed
8
+ - feat: retry delivery message when failed (RabbitMQ)
9
+
10
+
11
+ # 0.2.2 (March 27, 2020)
12
+ - fix default value for cattr_accessor in ror < 5.2
13
+ - add callbacks when publishing a message
14
+
15
+ # 0.2.1
16
+ - Add on demand model sync method
17
+
18
+ # 0.2.0
19
+ - Add apache kafka support
20
+ - Add Service interface for future references
21
+ - Improve Services to use a single/common message performer
22
+
3
23
  # 0.1.4
4
24
  - Add attribute aliases when publishing, ```ps_publish(['name:full_name', 'email'])```
5
25
  - Ability to retrieve publisher/subscriber crud settings
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  gem 'rubocop'
4
4
  gem 'bunny' # rabbit-mq
5
5
  gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
6
7
 
7
8
  # Specify your gem's dependencies in pub_sub_model_sync.gemspec
8
9
  gemspec
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pub_sub_model_sync (0.1.4)
4
+ pub_sub_model_sync (0.2.3)
5
5
  activesupport
6
6
  rails
7
7
 
@@ -60,6 +60,7 @@ GEM
60
60
  concurrent-ruby (1.1.6)
61
61
  crass (1.0.6)
62
62
  diff-lcs (1.3)
63
+ digest-crc (0.5.1)
63
64
  erubi (1.9.0)
64
65
  faraday (0.17.3)
65
66
  multipart-post (>= 1.2, < 3)
@@ -81,7 +82,7 @@ GEM
81
82
  googleauth (>= 0.6.2, < 0.10.0)
82
83
  grpc (>= 1.7.2, < 2.0)
83
84
  rly (~> 0.2.3)
84
- google-protobuf (3.11.4-universal-darwin)
85
+ google-protobuf (3.11.4)
85
86
  googleapis-common-protos (1.3.9)
86
87
  google-protobuf (~> 3.0)
87
88
  googleapis-common-protos-types (~> 1.0)
@@ -95,7 +96,7 @@ GEM
95
96
  multi_json (~> 1.11)
96
97
  os (>= 0.9, < 2.0)
97
98
  signet (~> 0.7)
98
- grpc (1.27.0-universal-darwin)
99
+ grpc (1.27.0)
99
100
  google-protobuf (~> 3.11)
100
101
  googleapis-common-protos-types (~> 1.0)
101
102
  grpc-google-iam-v1 (0.6.9)
@@ -180,6 +181,8 @@ GEM
180
181
  rexml
181
182
  ruby-progressbar (~> 1.7)
182
183
  unicode-display_width (>= 1.4.0, < 1.7)
184
+ ruby-kafka (1.0.0)
185
+ digest-crc
183
186
  ruby-progressbar (1.10.1)
184
187
  signet (0.11.0)
185
188
  addressable (~> 2.3)
@@ -214,6 +217,7 @@ DEPENDENCIES
214
217
  rake
215
218
  rspec
216
219
  rubocop
220
+ ruby-kafka
217
221
  sqlite3
218
222
 
219
223
  BUNDLED WITH
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # PubSubModelSync
2
- Permit to sync models data and make calls between rails apps using google or rabbitmq pub/sub service.
2
+ Permit to sync models data and make calls between rails apps using google or rabbitmq or apache kafka pub/sub service.
3
3
 
4
4
  Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man) which for now looks unmaintained.
5
5
 
@@ -14,8 +14,10 @@ Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_
14
14
  Add this line to your application's Gemfile:
15
15
  ```ruby
16
16
  gem 'pub_sub_model_sync'
17
+
17
18
  gem 'google-cloud-pubsub' # to use google pub/sub service
18
19
  gem 'bunny' # to use rabbit-mq pub/sub service
20
+ gem 'ruby-kafka' # to use apache kafka pub/sub service
19
21
  ```
20
22
  And then execute: $ bundle install
21
23
 
@@ -43,6 +45,14 @@ And then execute: $ bundle install
43
45
  ```
44
46
  See details here: https://github.com/ruby-amqp/bunny
45
47
 
48
+ - configuration for Apache Kafka (You need kafka installed)
49
+ ```ruby
50
+ PubSubModelSync::Config.service_name = :kafka
51
+ PubSubModelSync::Config.kafka_connection = [["kafka1:9092", "localhost:2121"], logger: Rails.logger]
52
+ PubSubModelSync::Config.topic_name = 'sample-topic'
53
+ ```
54
+ See details here: https://github.com/zendesk/ruby-kafka
55
+
46
56
  - Add publishers/subscribers to your models (See examples below)
47
57
 
48
58
  - Start subscribers to listen for publishers (Only in the app that has subscribers)
@@ -73,7 +83,10 @@ end
73
83
 
74
84
  # Samples
75
85
  User.create(name: 'test user', email: 'sample@gmail.com') # Review your App 2 to see the created user (only name will be saved)
86
+ User.new(name: 'test user').ps_perform_sync(:create) # similar to above to perform sync on demand
87
+
76
88
  User.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
89
+ PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
77
90
  ```
78
91
 
79
92
  ## Advanced Example
@@ -84,9 +97,13 @@ class User < ActiveRecord::Base
84
97
  include PubSubModelSync::PublisherConcern
85
98
  ps_publish(%i[name:full_name email], actions: %i[update], as_klass: 'Client', id: :client_id)
86
99
 
87
- def ps_skip_for?(_action)
100
+ def ps_skip_callback?(_action)
88
101
  false # here logic with action to skip push message
89
102
  end
103
+
104
+ def ps_skip_sync?(_action)
105
+ false # here logic with action to skip push message
106
+ end
90
107
  end
91
108
 
92
109
  # App 2 (Subscriber)
@@ -102,6 +119,61 @@ class User < ActiveRecord::Base
102
119
  end
103
120
  ```
104
121
 
122
+ Note: Be careful with collision of names
123
+ ```
124
+ class User
125
+ # ps_publish %i[name_data:name name:key] # key will be replaced with name_data
126
+ ps_publish %i[name_data:name key_data:key] # use alias to avoid collision
127
+
128
+ def key_data
129
+ name
130
+ end
131
+ end
132
+ ```
133
+
134
+ ## API
135
+ - Permit to cancel sync called after create/update/destroy (Before initializing sync service)
136
+ ```model.ps_skip_callback?(action)```
137
+ Note: Return true to cancel sync
138
+
139
+ - Callback called before preparing data for sync (Permit to stop sync)
140
+ ```model.ps_skip_sync?(action)```
141
+ Note: return true to cancel sync
142
+
143
+ - Callback called before sync (After preparing data)
144
+ ```model.ps_before_sync(action, data_to_deliver)```
145
+ Note: If the method returns ```:cancel```, the sync will be stopped (message will not be published)
146
+
147
+ - Callback called after sync
148
+ ```model.ps_after_sync(action, data_delivered)```
149
+
150
+ - Perform sync on demand (:create, :update, :destroy):
151
+ The target model will receive a notification to perform the indicated action
152
+ ```my_model.ps_perform_sync(action_name, custom_settings = {})```
153
+ * custom_settings: override default settings defined for action_name ({ attrs: [], as_klass: nil, id: nil })
154
+
155
+ - Class level notification:
156
+ ```User.ps_class_publish(data, action: action_name, as_klass: custom_klass_name)```
157
+ Target class ```User.action_name``` will be called when message is received
158
+ * data: (required, :hash) message value to deliver
159
+ * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
160
+ * as_klass: (optional, :string) same class name as defined in ps_class_subscribe(...)
161
+
162
+ - Class level notification (Same as above: on demand call)
163
+ ```PubSubModelSync::Publisher.new.publish_data(Klass_name, data, action_name)```
164
+ * klass_name: (required, Class) same class name as defined in ps_class_subscribe(...)
165
+ * data: (required, :hash) message value to deliver
166
+ * action_name: (required, :sim) same action name as defined in ps_class_subscribe(...)
167
+
168
+ - Get crud subscription configured for the class
169
+ ```User.ps_subscriber(action_name)```
170
+ * action_name (default :create, :sym): can be :create, :update, :destroy
171
+ - Get crud publisher configured for the class
172
+ ```User.ps_publisher(action_name)```
173
+ * action_name (default :create, :sym): can be :create, :update, :destroy
174
+ - Inspect all configured listeners
175
+ ```PubSubModelSync::Config.listeners```
176
+
105
177
  ## Testing with RSpec
106
178
  - Config: (spec/rails_helper.rb)
107
179
  ```ruby
@@ -109,15 +181,22 @@ end
109
181
  # when using google service
110
182
  require 'pub_sub_model_sync/mock_google_service'
111
183
  config.before(:each) do
112
- pub_sub_mock = PubSubModelSync::MockGoogleService.new
113
- allow(Google::Cloud::Pubsub).to receive(:new).and_return(pub_sub_mock)
184
+ google_mock = PubSubModelSync::MockGoogleService.new
185
+ allow(Google::Cloud::Pubsub).to receive(:new).and_return(google_mock)
114
186
  end
115
187
 
116
188
  # when using rabbitmq service
117
189
  require 'pub_sub_model_sync/mock_rabbit_service'
118
190
  config.before(:each) do
119
- bunny_mock = PubSubModelSync::MockRabbitService.new
120
- allow(Bunny).to receive(:new).and_return(bunny_mock)
191
+ rabbit_mock = PubSubModelSync::MockRabbitService.new
192
+ allow(Bunny).to receive(:new).and_return(rabbit_mock)
193
+ end
194
+
195
+ # when using apache kafka service
196
+ require 'pub_sub_model_sync/mock_kafka_service'
197
+ config.before(:each) do
198
+ kafka_mock = PubSubModelSync::MockKafkaService.new
199
+ allow(Kafka).to receive(:new).and_return(kafka_mock)
121
200
  end
122
201
 
123
202
  ```
@@ -160,15 +239,6 @@ end
160
239
  expect_any_instance_of(publisher).to receive(:publish_data).with('User', data, action)
161
240
  end
162
241
  ```
163
-
164
- There are two special methods to extract crud configuration settings (attrs, id, ...):
165
-
166
- Subscribers: ```User.ps_subscriber```
167
-
168
- Publishers: ```User.ps_publisher```
169
-
170
- Note: Inspect all configured listeners with:
171
- ``` PubSubModelSync::Config.listeners ```
172
242
 
173
243
  ## Contributing
174
244
 
@@ -11,8 +11,11 @@ require 'pub_sub_model_sync/publisher_concern'
11
11
  require 'pub_sub_model_sync/runner'
12
12
  require 'pub_sub_model_sync/connector'
13
13
  require 'pub_sub_model_sync/message_processor'
14
+
15
+ require 'pub_sub_model_sync/service_base'
14
16
  require 'pub_sub_model_sync/service_google'
15
17
  require 'pub_sub_model_sync/service_rabbit'
18
+ require 'pub_sub_model_sync/service_kafka'
16
19
 
17
20
  module PubSubModelSync
18
21
  class Error < StandardError; end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  class Config
5
- cattr_accessor :listeners, default: []
6
- cattr_accessor :publishers, default: []
7
- cattr_accessor :service_name, default: :google
5
+ cattr_accessor(:listeners) { [] }
6
+ cattr_accessor(:publishers) { [] }
7
+ cattr_accessor(:service_name) { :google }
8
8
  cattr_accessor :logger
9
9
 
10
10
  # google service
@@ -13,6 +13,9 @@ module PubSubModelSync
13
13
  # rabbitmq service
14
14
  cattr_accessor :bunny_connection, :queue_name, :topic_name
15
15
 
16
+ # kafka service
17
+ cattr_accessor :kafka_connection, :topic_name
18
+
16
19
  def self.log(msg, kind = :info)
17
20
  msg = "PS_MSYNC ==> #{msg}"
18
21
  if logger == :raise_error
@@ -16,6 +16,8 @@ module PubSubModelSync
16
16
  case Config.service_name
17
17
  when :google
18
18
  PubSubModelSync::ServiceGoogle.new
19
+ when :kafka
20
+ PubSubModelSync::ServiceKafka.new
19
21
  else # :rabbit_mq
20
22
  PubSubModelSync::ServiceRabbit.new
21
23
  end
@@ -13,10 +13,13 @@ module PubSubModelSync
13
13
  end
14
14
 
15
15
  def process
16
+ @failed = false
16
17
  log 'processing message'
17
18
  listeners = filter_listeners
18
- eval_message(listeners) if listeners.any?
19
- log 'processed message'
19
+ return log 'Skipped: No listeners' unless listeners.any?
20
+
21
+ eval_message(listeners)
22
+ log 'processed message' unless @failed
20
23
  end
21
24
 
22
25
  private
@@ -36,6 +39,7 @@ module PubSubModelSync
36
39
  model_class.send(listener[:action], data)
37
40
  rescue => e
38
41
  log("Error listener (#{listener}): #{e.message}", :error)
42
+ @failed = true
39
43
  end
40
44
 
41
45
  # support for: create, update, destroy
@@ -49,13 +53,13 @@ module PubSubModelSync
49
53
  end
50
54
  rescue => e
51
55
  log("Error listener (#{listener}): #{e.message}", :error)
56
+ @failed = true
52
57
  end
53
58
 
54
59
  def find_model(listener)
55
60
  model_class = listener[:klass].constantize
56
61
  identifier = listener[:settings][:id] || :id
57
- model_class.where(identifier => attrs[:id]).first ||
58
- model_class.new(identifier => attrs[:id])
62
+ model_class.where(identifier => attrs[:id]).first_or_initialize
59
63
  end
60
64
 
61
65
  def populate_model(model, listener)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class MockKafkaService
5
+ class MockProducer
6
+ def produce(*_args)
7
+ true
8
+ end
9
+
10
+ def deliver_messages(*_args)
11
+ true
12
+ end
13
+
14
+ def shutdown
15
+ true
16
+ end
17
+ end
18
+
19
+ class MockConsumer
20
+ def each_message(*_args)
21
+ true
22
+ end
23
+
24
+ def stop(*_args)
25
+ true
26
+ end
27
+
28
+ def subscribe(*_args)
29
+ true
30
+ end
31
+ end
32
+
33
+ def producer(*_args)
34
+ MockProducer.new
35
+ end
36
+
37
+ def consumer(*_args)
38
+ MockConsumer.new
39
+ end
40
+
41
+ def close
42
+ true
43
+ end
44
+ end
45
+ end
@@ -30,6 +30,10 @@ module PubSubModelSync
30
30
  def topic(*_args)
31
31
  @topic ||= MockTopic.new
32
32
  end
33
+
34
+ def close
35
+ true
36
+ end
33
37
  end
34
38
 
35
39
  def create_channel(*_args)
@@ -14,19 +14,24 @@ module PubSubModelSync
14
14
 
15
15
  # @param settings (Hash): { attrs: [], as_klass: nil, id: nil }
16
16
  def publish_model(model, action, settings = nil)
17
+ return if model.ps_skip_sync?(action)
18
+
17
19
  settings ||= model.class.ps_publisher_info(action)
18
20
  attributes = build_model_attrs(model, action, settings)
19
21
  data = {}
20
22
  data = build_model_data(model, settings[:attrs]) if action != :destroy
23
+ res_before = model.ps_before_sync(action, data)
24
+ return if res_before == :cancel
25
+
21
26
  connector.publish(data.symbolize_keys, attributes)
27
+ model.ps_after_sync(action, data)
22
28
  end
23
29
 
24
30
  def self.build_attrs(klass, action, id = nil)
25
31
  {
26
32
  klass: klass.to_s,
27
33
  action: action.to_sym,
28
- id: id,
29
- service_model_sync: true
34
+ id: id
30
35
  }
31
36
  end
32
37
 
@@ -6,11 +6,30 @@ module PubSubModelSync
6
6
  base.extend(ClassMethods)
7
7
  end
8
8
 
9
- # Permit to skip a publish callback
10
- def ps_skip_for?(_action)
9
+ # Before initializing sync service (callbacks: after create/update/destroy)
10
+ def ps_skip_callback?(_action)
11
11
  false
12
12
  end
13
13
 
14
+ # before preparing data to sync
15
+ def ps_skip_sync?(_action)
16
+ false
17
+ end
18
+
19
+ # before delivering data
20
+ def ps_before_sync(_action, _data); end
21
+
22
+ # after delivering data
23
+ def ps_after_sync(_action, _data); end
24
+
25
+ # To perform sync on demand
26
+ # @param custom_settings (Hash): { attrs: [], as_klass: nil, id: nil }
27
+ def ps_perform_sync(action = :create, custom_settings = {})
28
+ service = self.class.ps_publisher_service
29
+ model_settings = self.class.ps_publisher_info(action) || {}
30
+ service.publish_model(self, action, model_settings.merge(custom_settings))
31
+ end
32
+
14
33
  module ClassMethods
15
34
  # Permit to publish crud actions (:create, :update, :destroy)
16
35
  # @param settings (Hash): { actions: nil, as_klass: nil, id: nil }
@@ -23,12 +42,14 @@ module PubSubModelSync
23
42
  end
24
43
  end
25
44
 
45
+ # Publisher info for specific action
26
46
  def ps_publisher_info(action = :create)
27
47
  PubSubModelSync::Config.publishers.select do |listener|
28
48
  listener[:klass] == name && listener[:action] == action
29
49
  end.last
30
50
  end
31
51
 
52
+ # On demand class level publisher
32
53
  def ps_class_publish(data, action:, as_klass: nil)
33
54
  as_klass = (as_klass || name).to_s
34
55
  ps_publisher_service.publish_data(as_klass, data, action.to_sym)
@@ -42,7 +63,7 @@ module PubSubModelSync
42
63
 
43
64
  def ps_register_callback(action, info)
44
65
  after_commit(on: action) do |model|
45
- unless model.ps_skip_for?(action)
66
+ unless model.ps_skip_callback?(action)
46
67
  service = model.class.ps_publisher_service
47
68
  service.publish_model(model, action.to_sym, info)
48
69
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class ServiceBase
5
+ SERVICE_KEY = 'service_model_sync'
6
+
7
+ def listen_messages
8
+ raise 'method :listen_messages must be defined in service'
9
+ end
10
+
11
+ def publish(_data, _attributes)
12
+ raise 'method :publish must be defined in service'
13
+ end
14
+
15
+ def stop
16
+ raise 'method :stop must be defined in service'
17
+ end
18
+
19
+ private
20
+
21
+ # @param payload (String JSON): '{"data":{},"attributes":{..}}'
22
+ # refer: PubSubModelSync::Publisher (.publish_model | .publish_data)
23
+ def perform_message(payload)
24
+ data, attrs = parse_message_payload(payload)
25
+ args = [data, attrs[:klass], attrs[:action], attrs]
26
+ PubSubModelSync::MessageProcessor.new(*args).process
27
+ end
28
+
29
+ def parse_message_payload(payload)
30
+ message_payload = JSON.parse(payload).symbolize_keys
31
+ data = message_payload[:data].symbolize_keys
32
+ attrs = message_payload[:attributes].symbolize_keys
33
+ [data, attrs]
34
+ end
35
+ end
36
+ end
@@ -6,7 +6,7 @@ rescue LoadError # rubocop:disable Lint/SuppressedException
6
6
  end
7
7
 
8
8
  module PubSubModelSync
9
- class ServiceGoogle
9
+ class ServiceGoogle < ServiceBase
10
10
  attr_accessor :service, :topic, :subscription, :config, :subscriber
11
11
 
12
12
  def initialize
@@ -30,7 +30,9 @@ module PubSubModelSync
30
30
 
31
31
  def publish(data, attributes)
32
32
  log("Publishing message: #{[data, attributes]}")
33
- topic.publish(data.to_json, attributes)
33
+
34
+ payload = { data: data, attributes: attributes }.to_json
35
+ topic.publish(payload, { SERVICE_KEY => true })
34
36
  end
35
37
 
36
38
  def stop
@@ -47,12 +49,9 @@ module PubSubModelSync
47
49
 
48
50
  def process_message(received_message)
49
51
  message = received_message.message
50
- attrs = message.attributes.symbolize_keys
51
- return unless attrs[:service_model_sync]
52
+ return unless message.attributes[SERVICE_KEY]
52
53
 
53
- data = JSON.parse(message.data).symbolize_keys
54
- args = [data, attrs[:klass], attrs[:action], attrs]
55
- PubSubModelSync::MessageProcessor.new(*args).process
54
+ perform_message(message.data)
56
55
  rescue => e
57
56
  log("Error processing message: #{[received_message, e.message]}", :error)
58
57
  ensure
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'kafka'
5
+ rescue LoadError # rubocop:disable Lint/SuppressedException
6
+ end
7
+
8
+ module PubSubModelSync
9
+ class ServiceKafka < ServiceBase
10
+ cattr_accessor :producer
11
+
12
+ attr_accessor :service, :consumer
13
+ attr_accessor :config
14
+ CONSUMER_GROUP = 'service_model_sync'
15
+
16
+ def initialize
17
+ @config = PubSubModelSync::Config
18
+ @service = Kafka.new(*config.kafka_connection)
19
+ end
20
+
21
+ def listen_messages
22
+ log('Listener starting...')
23
+ start_consumer
24
+ consumer.each_message(&method(:process_message))
25
+ rescue PubSubModelSync::Runner::ShutDown
26
+ raise
27
+ rescue => e
28
+ log("Error listening message: #{[e.message, e.backtrace]}", :error)
29
+ end
30
+
31
+ def publish(data, attributes)
32
+ log("Publishing: #{[data, attributes]}")
33
+ payload = { data: data, attributes: attributes }
34
+ producer.produce(payload.to_json, message_settings)
35
+ producer.deliver_messages
36
+ rescue => e
37
+ info = [data, attributes, e.message, e.backtrace]
38
+ log("Error publishing: #{info}", :error)
39
+ end
40
+
41
+ def stop
42
+ log('Listener stopping...')
43
+ consumer.stop
44
+ end
45
+
46
+ private
47
+
48
+ def message_settings
49
+ { topic: config.topic_name, headers: { SERVICE_KEY => true } }
50
+ end
51
+
52
+ def start_consumer
53
+ @consumer = service.consumer(group_id: CONSUMER_GROUP)
54
+ consumer.subscribe(config.topic_name)
55
+ end
56
+
57
+ def producer
58
+ return self.class.producer if self.class.producer
59
+
60
+ at_exit { self.class.producer.shutdown }
61
+ self.class.producer = service.producer
62
+ end
63
+
64
+ def process_message(message)
65
+ return unless message.headers[SERVICE_KEY]
66
+
67
+ perform_message(message.value)
68
+ rescue => e
69
+ error = [message, e.message, e.backtrace]
70
+ log("Error processing message: #{error}", :error)
71
+ end
72
+
73
+ def log(msg, kind = :info)
74
+ config.log("Kafka Service ==> #{msg}", kind)
75
+ end
76
+ end
77
+ end
@@ -6,10 +6,9 @@ rescue LoadError # rubocop:disable Lint/SuppressedException
6
6
  end
7
7
 
8
8
  module PubSubModelSync
9
- class ServiceRabbit
9
+ class ServiceRabbit < ServiceBase
10
10
  attr_accessor :service, :channel, :queue, :topic
11
11
  attr_accessor :config
12
- SERVICE_KEY = 'service_model_sync'
13
12
 
14
13
  def initialize
15
14
  @config = PubSubModelSync::Config
@@ -29,9 +28,12 @@ module PubSubModelSync
29
28
 
30
29
  def publish(data, attributes)
31
30
  log("Publishing: #{[data, attributes]}")
32
- subscribe_to_queue
33
- payload = { data: data, attributes: attributes }
34
- topic.publish(payload.to_json, routing_key: queue.name, type: SERVICE_KEY)
31
+ deliver_data(data, attributes)
32
+ # TODO: max retry
33
+ rescue Timeout::Error => e
34
+ log("Error publishing (retrying....): #{e.message}", :error)
35
+ initialize
36
+ retry
35
37
  rescue => e
36
38
  info = [data, attributes, e.message, e.backtrace]
37
39
  log("Error publishing: #{info}", :error)
@@ -44,24 +46,19 @@ module PubSubModelSync
44
46
 
45
47
  private
46
48
 
49
+ def message_settings
50
+ { routing_key: queue.name, type: SERVICE_KEY }
51
+ end
52
+
47
53
  def process_message(_delivery_info, meta_info, payload)
48
54
  return unless meta_info[:type] == SERVICE_KEY
49
55
 
50
- data, attrs = parse_message_payload(payload)
51
- args = [data, attrs[:klass], attrs[:action], attrs]
52
- PubSubModelSync::MessageProcessor.new(*args).process
56
+ perform_message(payload)
53
57
  rescue => e
54
58
  error = [payload, e.message, e.backtrace]
55
59
  log("Error processing message: #{error}", :error)
56
60
  end
57
61
 
58
- def parse_message_payload(payload)
59
- message_payload = JSON.parse(payload).symbolize_keys
60
- data = message_payload[:data].symbolize_keys
61
- attrs = message_payload[:attributes].symbolize_keys
62
- [data, attrs]
63
- end
64
-
65
62
  def subscribe_to_queue
66
63
  service.start
67
64
  @channel = service.create_channel
@@ -78,5 +75,16 @@ module PubSubModelSync
78
75
  def log(msg, kind = :info)
79
76
  config.log("Rabbit Service ==> #{msg}", kind)
80
77
  end
78
+
79
+ def deliver_data(data, attributes)
80
+ subscribe_to_queue
81
+ payload = { data: data, attributes: attributes }
82
+ topic.publish(payload.to_json, message_settings)
83
+
84
+ # Ugly fix: "IO timeout when reading 7 bytes"
85
+ # https://stackoverflow.com/questions/39039129/rabbitmq-timeouterror-io-timeout-when-reading-7-bytes
86
+ channel.close
87
+ service.close
88
+ end
81
89
  end
82
90
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- VERSION = '0.1.4'
4
+ VERSION = '0.2.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pub_sub_model_sync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Owen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-25 00:00:00.000000000 Z
11
+ date: 2020-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -119,12 +119,15 @@ files:
119
119
  - lib/pub_sub_model_sync/connector.rb
120
120
  - lib/pub_sub_model_sync/message_processor.rb
121
121
  - lib/pub_sub_model_sync/mock_google_service.rb
122
+ - lib/pub_sub_model_sync/mock_kafka_service.rb
122
123
  - lib/pub_sub_model_sync/mock_rabbit_service.rb
123
124
  - lib/pub_sub_model_sync/publisher.rb
124
125
  - lib/pub_sub_model_sync/publisher_concern.rb
125
126
  - lib/pub_sub_model_sync/railtie.rb
126
127
  - lib/pub_sub_model_sync/runner.rb
128
+ - lib/pub_sub_model_sync/service_base.rb
127
129
  - lib/pub_sub_model_sync/service_google.rb
130
+ - lib/pub_sub_model_sync/service_kafka.rb
128
131
  - lib/pub_sub_model_sync/service_rabbit.rb
129
132
  - lib/pub_sub_model_sync/subscriber_concern.rb
130
133
  - lib/pub_sub_model_sync/tasks/worker.rake