pub_sub_model_sync 0.4.0 → 0.5.0.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +30 -8
  3. data/.rubocop.yml +6 -1
  4. data/CHANGELOG.md +27 -0
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +25 -23
  7. data/README.md +55 -29
  8. data/gemfiles/Gemfile_4 +16 -0
  9. data/gemfiles/Gemfile_5 +14 -0
  10. data/gemfiles/Gemfile_6 +14 -0
  11. data/lib/pub_sub_model_sync.rb +1 -0
  12. data/lib/pub_sub_model_sync/base.rb +17 -0
  13. data/lib/pub_sub_model_sync/config.rb +18 -3
  14. data/lib/pub_sub_model_sync/connector.rb +1 -0
  15. data/lib/pub_sub_model_sync/message_processor.rb +27 -21
  16. data/lib/pub_sub_model_sync/message_publisher.rb +25 -9
  17. data/lib/pub_sub_model_sync/mock_rabbit_service.rb +5 -0
  18. data/lib/pub_sub_model_sync/payload.rb +45 -0
  19. data/lib/pub_sub_model_sync/publisher.rb +1 -0
  20. data/lib/pub_sub_model_sync/publisher_concern.rb +3 -1
  21. data/lib/pub_sub_model_sync/service_base.rb +25 -13
  22. data/lib/pub_sub_model_sync/service_google.rb +4 -18
  23. data/lib/pub_sub_model_sync/service_kafka.rb +4 -17
  24. data/lib/pub_sub_model_sync/service_rabbit.rb +20 -27
  25. data/lib/pub_sub_model_sync/subscriber.rb +3 -3
  26. data/lib/pub_sub_model_sync/subscriber_concern.rb +6 -0
  27. data/lib/pub_sub_model_sync/version.rb +1 -1
  28. data/pub_sub_model_sync.gemspec +1 -1
  29. metadata +11 -29
  30. data/.idea/.gitignore +0 -8
  31. data/.idea/.rakeTasks +0 -7
  32. data/.idea/codeStyles/codeStyleConfig.xml +0 -5
  33. data/.idea/encodings.xml +0 -4
  34. data/.idea/inspectionProfiles/Project_Default.xml +0 -16
  35. data/.idea/misc.xml +0 -7
  36. data/.idea/modules.xml +0 -8
  37. data/.idea/pub_sub_model_sync.iml +0 -96
  38. data/.idea/vcs.xml +0 -6
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 5'
8
+
9
+ group :test do
10
+ gem 'database_cleaner-active_record'
11
+ end
12
+
13
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
14
+ gemspec
@@ -0,0 +1,14 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rubocop'
4
+ gem 'bunny' # rabbit-mq
5
+ gem 'google-cloud-pubsub' # google pub/sub
6
+ gem 'ruby-kafka' # kafka pub/sub
7
+ gem 'rails', '~> 6'
8
+
9
+ group :test do
10
+ gem 'database_cleaner-active_record'
11
+ end
12
+
13
+ # Specify your gem's dependencies in pub_sub_model_sync.gemspec
14
+ gemspec
@@ -5,6 +5,7 @@ require 'active_support'
5
5
 
6
6
  require 'pub_sub_model_sync/railtie'
7
7
  require 'pub_sub_model_sync/config'
8
+ require 'pub_sub_model_sync/base'
8
9
  require 'pub_sub_model_sync/subscriber_concern'
9
10
  require 'pub_sub_model_sync/message_publisher'
10
11
  require 'pub_sub_model_sync/publisher_concern'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Base
5
+ delegate :config, :log, to: self
6
+
7
+ class << self
8
+ def config
9
+ PubSubModelSync::Config
10
+ end
11
+
12
+ def log(message, kind = :info)
13
+ config.log message, kind
14
+ end
15
+ end
16
+ end
17
+ end
@@ -5,16 +5,26 @@ module PubSubModelSync
5
5
  cattr_accessor(:subscribers) { [] }
6
6
  cattr_accessor(:publishers) { [] }
7
7
  cattr_accessor(:service_name) { :google }
8
- cattr_accessor :logger
8
+
9
+ # customizable callbacks
10
+ cattr_accessor(:debug) { false }
11
+ cattr_accessor :logger # LoggerInst
12
+
13
+ cattr_accessor(:on_process_success) { ->(_payload, _subscriber) {} }
14
+ cattr_accessor(:on_process_error) { ->(_exception, _payload) {} }
15
+ cattr_accessor(:on_before_publish) { ->(_payload) {} }
16
+ cattr_accessor(:on_after_publish) { ->(_payload) {} }
17
+ cattr_accessor(:on_publish_error) { ->(_exception, _payload) {} }
18
+ cattr_accessor(:disabled) { false }
9
19
 
10
20
  # google service
11
21
  cattr_accessor :project, :credentials, :topic_name, :subscription_name
12
22
 
13
23
  # rabbitmq service
14
- cattr_accessor :bunny_connection, :queue_name, :topic_name
24
+ cattr_accessor :bunny_connection, :queue_name, :topic_name, :subscription_name
15
25
 
16
26
  # kafka service
17
- cattr_accessor :kafka_connection, :topic_name
27
+ cattr_accessor :kafka_connection, :topic_name, :subscription_name
18
28
 
19
29
  def self.log(msg, kind = :info)
20
30
  msg = "PS_MSYNC ==> #{msg}"
@@ -24,5 +34,10 @@ module PubSubModelSync
24
34
  logger ? logger.send(kind, msg) : puts(msg)
25
35
  end
26
36
  end
37
+
38
+ def self.subscription_key
39
+ subscription_name ||
40
+ (Rails.application.class.parent_name rescue '') # rubocop:disable Style/RescueModifier
41
+ end
27
42
  end
28
43
  end
@@ -3,6 +3,7 @@
3
3
  module PubSubModelSync
4
4
  class Connector
5
5
  attr_accessor :service
6
+
6
7
  delegate :listen_messages, :publish, :stop, to: :service
7
8
 
8
9
  def initialize
@@ -1,40 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- class MessageProcessor
5
- attr_accessor :data, :klass, :action
6
-
7
- # @param data (Hash): any hash value to deliver
8
- def initialize(data, klass, action)
9
- @data = data
10
- @klass = klass
11
- @action = action
4
+ class MessageProcessor < PubSubModelSync::Base
5
+ attr_accessor :payload
6
+
7
+ # @param payload (Payload): payload to be delivered
8
+ # @Deprecated: def initialize(data, klass, action)
9
+ def initialize(payload, klass = nil, action = nil)
10
+ @payload = payload
11
+ return if @payload.is_a?(Payload)
12
+
13
+ # support for deprecated
14
+ log('Deprecated: Use Payload instead of new(data, klass, action)')
15
+ @payload = PubSubModelSync::Payload.new(payload, { klass: klass, action: action })
12
16
  end
13
17
 
14
18
  def process
15
- subscribers = filter_subscribers
16
- subscribers.each { |subscriber| run_subscriber(subscriber) }
19
+ filter_subscribers.each(&method(:run_subscriber))
17
20
  end
18
21
 
19
22
  private
20
23
 
21
24
  def run_subscriber(subscriber)
22
- subscriber.eval_message(data)
23
- log "processed message with: #{[klass, action, data]}"
25
+ subscriber.eval_message(payload.data)
26
+ config.on_process_success.call(payload, subscriber)
27
+ log "processed message with: #{payload}"
24
28
  rescue => e
25
- info = [klass, action, data, e.message, e.backtrace]
26
- log("error processing message: #{info}", :error)
29
+ print_subscriber_error(e)
27
30
  end
28
31
 
29
- def filter_subscribers
30
- PubSubModelSync::Config.subscribers.select do |subscriber|
31
- subscriber.settings[:from_klass].to_s == klass.to_s &&
32
- subscriber.settings[:from_action].to_s == action.to_s
33
- end
32
+ # @param error (Error)
33
+ def print_subscriber_error(error)
34
+ info = [payload, error.message, error.backtrace]
35
+ res = config.on_process_error.call(error, payload)
36
+ log("Error processing message: #{info}", :error) if res != :skip_log
34
37
  end
35
38
 
36
- def log(message, kind = :info)
37
- PubSubModelSync::Config.log message, kind
39
+ def filter_subscribers
40
+ config.subscribers.select do |subscriber|
41
+ subscriber.settings[:from_klass].to_s == payload.klass.to_s &&
42
+ subscriber.settings[:from_action].to_s == payload.action.to_s
43
+ end
38
44
  end
39
45
  end
40
46
  end
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PubSubModelSync
4
- class MessagePublisher
4
+ class MessagePublisher < PubSubModelSync::Base
5
5
  class << self
6
- delegate :publish, to: :connector
7
-
8
6
  def connector
9
7
  @connector ||= PubSubModelSync::Connector.new
10
8
  end
11
9
 
12
10
  def publish_data(klass, data, action)
13
- attrs = { klass: klass.to_s, action: action.to_sym }
14
- publish(data, attrs)
11
+ payload = PubSubModelSync::Payload.new(data, { klass: klass, action: action.to_sym })
12
+ publish(payload)
15
13
  end
16
14
 
17
15
  # @param model: ActiveRecord model
@@ -21,12 +19,30 @@ module PubSubModelSync
21
19
  return if model.ps_skip_sync?(action)
22
20
 
23
21
  publisher ||= model.class.ps_publisher(action)
24
- payload = publisher.payload(model, action)
25
- res_before = model.ps_before_sync(action, payload[:data])
22
+ payload_info = publisher.payload(model, action)
23
+ payload = PubSubModelSync::Payload.new(payload_info[:data], payload_info[:attrs])
24
+ res_before = model.ps_before_sync(action, payload.data)
26
25
  return if res_before == :cancel
27
26
 
28
- publish(payload[:data], payload[:attrs])
29
- model.ps_after_sync(action, payload[:data])
27
+ publish(payload)
28
+ model.ps_after_sync(action, payload.data)
29
+ end
30
+
31
+ def publish(payload)
32
+ log("Publishing message: #{[payload]}") if config.debug
33
+ config.on_before_publish.call(payload)
34
+ connector.publish(payload)
35
+ config.on_after_publish.call(payload)
36
+ rescue => e
37
+ notify_error(e, payload)
38
+ end
39
+
40
+ private
41
+
42
+ def notify_error(exception, payload)
43
+ info = [payload, exception.message, exception.backtrace]
44
+ res = config.on_publish_error.call(exception, payload)
45
+ log("Error publishing: #{info}", :error) if res != :skip_log
30
46
  end
31
47
  end
32
48
  end
@@ -20,12 +20,17 @@ module PubSubModelSync
20
20
  def name
21
21
  'name'
22
22
  end
23
+
24
+ def publish(*_args)
25
+ true
26
+ end
23
27
  end
24
28
 
25
29
  class MockChannel
26
30
  def queue(*_args)
27
31
  @queue ||= MockQueue.new
28
32
  end
33
+ alias fanout queue
29
34
 
30
35
  def topic(*_args)
31
36
  @topic ||= MockTopic.new
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class Payload
5
+ attr_reader :data, :attributes, :headers
6
+
7
+ # @param data (Hash: { any value }):
8
+ # @param attributes (Hash: { klass: string, action: :sym }):
9
+ def initialize(data, attributes, headers = {})
10
+ @data = data
11
+ @attributes = attributes
12
+ @headers = headers
13
+ build_headers
14
+ end
15
+
16
+ def to_h
17
+ { data: data, attributes: attributes, headers: headers }
18
+ end
19
+
20
+ def klass
21
+ attributes[:klass]
22
+ end
23
+
24
+ def action
25
+ attributes[:action]
26
+ end
27
+
28
+ def process!
29
+ publisher = PubSubModelSync::MessageProcessor.new(self)
30
+ publisher.process
31
+ end
32
+
33
+ def publish!
34
+ klass = PubSubModelSync::MessagePublisher
35
+ klass.publish(self)
36
+ end
37
+
38
+ private
39
+
40
+ def build_headers
41
+ headers[:uuid] ||= SecureRandom.uuid
42
+ headers[:app_key] ||= PubSubModelSync::Config.subscription_key
43
+ end
44
+ end
45
+ end
@@ -3,6 +3,7 @@
3
3
  module PubSubModelSync
4
4
  class Publisher
5
5
  attr_accessor :attrs, :actions, :klass, :as_klass
6
+
6
7
  def initialize(attrs, klass, actions = nil, as_klass = nil)
7
8
  @attrs = attrs
8
9
  @klass = klass
@@ -11,6 +11,7 @@ module PubSubModelSync
11
11
  false
12
12
  end
13
13
 
14
+ # TODO: make it using respond_to?(:ps_skip_sync?)
14
15
  # before preparing data to sync
15
16
  def ps_skip_sync?(_action)
16
17
  false
@@ -63,7 +64,8 @@ module PubSubModelSync
63
64
 
64
65
  def ps_register_callback(action, publisher)
65
66
  after_commit(on: action) do |model|
66
- unless model.ps_skip_callback?(action)
67
+ disabled = PubSubModelSync::Config.disabled
68
+ if !disabled && !model.ps_skip_callback?(action)
67
69
  klass = PubSubModelSync::MessagePublisher
68
70
  klass.publish_model(model, action.to_sym, publisher)
69
71
  end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pub_sub_model_sync/payload'
3
4
  module PubSubModelSync
4
- class ServiceBase
5
+ class ServiceBase < PubSubModelSync::Base
5
6
  SERVICE_KEY = 'service_model_sync'
6
7
 
7
8
  def listen_messages
8
9
  raise 'method :listen_messages must be defined in service'
9
10
  end
10
11
 
11
- def publish(_data, _attributes)
12
+ # @param _payload (Payload)
13
+ def publish(_payload)
12
14
  raise 'method :publish must be defined in service'
13
15
  end
14
16
 
@@ -18,19 +20,29 @@ module PubSubModelSync
18
20
 
19
21
  private
20
22
 
21
- # @param payload (String JSON): '{"data":{}, "attributes":{..}}'
22
- # refer: PubSubModelSync::MessagePublisher(.publish_model | .publish_data)
23
- def perform_message(payload)
24
- data, attrs = parse_message_payload(payload)
25
- args = [data, attrs[:klass], attrs[:action]]
26
- PubSubModelSync::MessageProcessor.new(*args).process
23
+ # @param (String: Payload in json format)
24
+ def process_message(payload_info)
25
+ payload = parse_payload(payload_info)
26
+ log("Received message: #{[payload]}") if config.debug
27
+ if same_app_message?(payload)
28
+ log("Skip message from same origin: #{[payload]}") if config.debug
29
+ else
30
+ payload.process!
31
+ end
32
+ rescue => e
33
+ error = [payload, e.message, e.backtrace]
34
+ log("Error parsing received message: #{error}", :error)
27
35
  end
28
36
 
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]
37
+ def parse_payload(payload_info)
38
+ info = JSON.parse(payload_info).deep_symbolize_keys
39
+ ::PubSubModelSync::Payload.new(info[:data], info[:attributes], info[:headers])
40
+ end
41
+
42
+ # @param payload (Payload)
43
+ def same_app_message?(payload)
44
+ key = payload.headers[:app_key]
45
+ key && key == config.subscription_key
34
46
  end
35
47
  end
36
48
  end
@@ -7,10 +7,9 @@ end
7
7
 
8
8
  module PubSubModelSync
9
9
  class ServiceGoogle < ServiceBase
10
- attr_accessor :service, :topic, :subscription, :config, :subscriber
10
+ attr_accessor :service, :topic, :subscription, :subscriber
11
11
 
12
12
  def initialize
13
- @config = PubSubModelSync::Config
14
13
  @service = Google::Cloud::Pubsub.new(project: config.project,
15
14
  credentials: config.credentials)
16
15
  @topic = service.topic(config.topic_name) ||
@@ -28,13 +27,8 @@ module PubSubModelSync
28
27
  log('Listener stopped')
29
28
  end
30
29
 
31
- def publish(data, attributes)
32
- log("Publishing message: #{[data, attributes]}")
33
- payload = { data: data, attributes: attributes }.to_json
34
- topic.publish(payload, { SERVICE_KEY => true })
35
- rescue => e
36
- info = [data, attributes, e.message, e.backtrace]
37
- log("Error publishing: #{info}", :error)
30
+ def publish(payload)
31
+ topic.publish(payload.to_json, { SERVICE_KEY => true })
38
32
  end
39
33
 
40
34
  def stop
@@ -51,17 +45,9 @@ module PubSubModelSync
51
45
 
52
46
  def process_message(received_message)
53
47
  message = received_message.message
54
- return unless message.attributes[SERVICE_KEY]
55
-
56
- perform_message(message.data)
57
- rescue => e
58
- log("Error processing message: #{[received_message, e.message]}", :error)
48
+ super(message.data) if message.attributes[SERVICE_KEY]
59
49
  ensure
60
50
  received_message.acknowledge!
61
51
  end
62
-
63
- def log(msg, kind = :info)
64
- config.log("Google Service ==> #{msg}", kind)
65
- end
66
52
  end
67
53
  end
@@ -8,9 +8,8 @@ end
8
8
  module PubSubModelSync
9
9
  class ServiceKafka < ServiceBase
10
10
  cattr_accessor :producer
11
+ attr_accessor :config, :service, :consumer
11
12
 
12
- attr_accessor :service, :consumer
13
- attr_accessor :config
14
13
  CONSUMER_GROUP = 'service_model_sync'
15
14
 
16
15
  def initialize
@@ -23,19 +22,14 @@ module PubSubModelSync
23
22
  start_consumer
24
23
  consumer.each_message(&method(:process_message))
25
24
  rescue PubSubModelSync::Runner::ShutDown
26
- raise
25
+ log('Listener stopped')
27
26
  rescue => e
28
27
  log("Error listening message: #{[e.message, e.backtrace]}", :error)
29
28
  end
30
29
 
31
- def publish(data, attributes)
32
- log("Publishing: #{[data, attributes]}")
33
- payload = { data: data, attributes: attributes }
30
+ def publish(payload)
34
31
  producer.produce(payload.to_json, message_settings)
35
32
  producer.deliver_messages
36
- rescue => e
37
- info = [data, attributes, e.message, e.backtrace]
38
- log("Error publishing: #{info}", :error)
39
33
  end
40
34
 
41
35
  def stop
@@ -64,14 +58,7 @@ module PubSubModelSync
64
58
  def process_message(message)
65
59
  return unless message.headers[SERVICE_KEY]
66
60
 
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)
61
+ super(message.value)
75
62
  end
76
63
  end
77
64
  end