pub_sub_model_sync 0.5.10 → 1.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +43 -0
  3. data/.github/workflows/ruby.yml +1 -1
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +34 -1
  6. data/Dockerfile +6 -0
  7. data/Gemfile.lock +150 -134
  8. data/README.md +372 -192
  9. data/docker-compose.yaml +12 -0
  10. data/docs/notifications-diagram.png +0 -0
  11. data/lib/pub_sub_model_sync.rb +3 -1
  12. data/lib/pub_sub_model_sync/base.rb +4 -7
  13. data/lib/pub_sub_model_sync/config.rb +17 -8
  14. data/lib/pub_sub_model_sync/initializers/before_commit.rb +23 -0
  15. data/lib/pub_sub_model_sync/message_processor.rb +34 -10
  16. data/lib/pub_sub_model_sync/message_publisher.rb +90 -29
  17. data/lib/pub_sub_model_sync/mock_google_service.rb +4 -0
  18. data/lib/pub_sub_model_sync/mock_kafka_service.rb +13 -0
  19. data/lib/pub_sub_model_sync/payload.rb +35 -16
  20. data/lib/pub_sub_model_sync/payload_builder.rb +62 -0
  21. data/lib/pub_sub_model_sync/publisher_concern.rb +77 -47
  22. data/lib/pub_sub_model_sync/railtie.rb +6 -0
  23. data/lib/pub_sub_model_sync/run_subscriber.rb +108 -0
  24. data/lib/pub_sub_model_sync/service_base.rb +19 -37
  25. data/lib/pub_sub_model_sync/service_google.rb +53 -17
  26. data/lib/pub_sub_model_sync/service_kafka.rb +40 -13
  27. data/lib/pub_sub_model_sync/service_rabbit.rb +41 -33
  28. data/lib/pub_sub_model_sync/subscriber.rb +14 -66
  29. data/lib/pub_sub_model_sync/subscriber_concern.rb +23 -23
  30. data/lib/pub_sub_model_sync/tasks/worker.rake +11 -0
  31. data/lib/pub_sub_model_sync/transaction.rb +73 -0
  32. data/lib/pub_sub_model_sync/version.rb +1 -1
  33. data/samples/README.md +50 -0
  34. data/samples/app1/.gitattributes +8 -0
  35. data/samples/app1/.gitignore +28 -0
  36. data/samples/app1/Dockerfile +13 -0
  37. data/samples/app1/Gemfile +37 -0
  38. data/samples/app1/Gemfile.lock +171 -0
  39. data/samples/app1/README.md +24 -0
  40. data/samples/app1/Rakefile +6 -0
  41. data/samples/app1/app/models/application_record.rb +3 -0
  42. data/samples/app1/app/models/concerns/.keep +0 -0
  43. data/samples/app1/app/models/post.rb +19 -0
  44. data/samples/app1/app/models/user.rb +29 -0
  45. data/samples/app1/bin/bundle +114 -0
  46. data/samples/app1/bin/rails +5 -0
  47. data/samples/app1/bin/rake +5 -0
  48. data/samples/app1/bin/setup +33 -0
  49. data/samples/app1/bin/spring +14 -0
  50. data/samples/app1/config.ru +6 -0
  51. data/samples/app1/config/application.rb +40 -0
  52. data/samples/app1/config/boot.rb +4 -0
  53. data/samples/app1/config/credentials.yml.enc +1 -0
  54. data/samples/app1/config/database.yml +25 -0
  55. data/samples/app1/config/environment.rb +5 -0
  56. data/samples/app1/config/environments/development.rb +63 -0
  57. data/samples/app1/config/environments/production.rb +105 -0
  58. data/samples/app1/config/environments/test.rb +57 -0
  59. data/samples/app1/config/initializers/application_controller_renderer.rb +8 -0
  60. data/samples/app1/config/initializers/backtrace_silencers.rb +8 -0
  61. data/samples/app1/config/initializers/cors.rb +16 -0
  62. data/samples/app1/config/initializers/filter_parameter_logging.rb +6 -0
  63. data/samples/app1/config/initializers/inflections.rb +16 -0
  64. data/samples/app1/config/initializers/mime_types.rb +4 -0
  65. data/samples/app1/config/initializers/pubsub.rb +4 -0
  66. data/samples/app1/config/initializers/wrap_parameters.rb +14 -0
  67. data/samples/app1/config/locales/en.yml +33 -0
  68. data/samples/app1/config/puma.rb +43 -0
  69. data/samples/app1/config/routes.rb +3 -0
  70. data/samples/app1/config/spring.rb +6 -0
  71. data/samples/app1/db/migrate/20210513080700_create_users.rb +12 -0
  72. data/samples/app1/db/migrate/20210513134332_create_posts.rb +11 -0
  73. data/samples/app1/db/schema.rb +34 -0
  74. data/samples/app1/db/seeds.rb +7 -0
  75. data/samples/app1/docker-compose.yml +32 -0
  76. data/samples/app1/log/.keep +0 -0
  77. data/samples/app2/.gitattributes +8 -0
  78. data/samples/app2/.gitignore +28 -0
  79. data/samples/app2/Dockerfile +13 -0
  80. data/samples/app2/Gemfile +37 -0
  81. data/samples/app2/Gemfile.lock +171 -0
  82. data/samples/app2/README.md +24 -0
  83. data/samples/app2/Rakefile +6 -0
  84. data/samples/app2/app/models/application_record.rb +9 -0
  85. data/samples/app2/app/models/concerns/.keep +0 -0
  86. data/samples/app2/app/models/customer.rb +28 -0
  87. data/samples/app2/app/models/post.rb +10 -0
  88. data/samples/app2/bin/bundle +114 -0
  89. data/samples/app2/bin/rails +5 -0
  90. data/samples/app2/bin/rake +5 -0
  91. data/samples/app2/bin/setup +33 -0
  92. data/samples/app2/bin/spring +14 -0
  93. data/samples/app2/config.ru +6 -0
  94. data/samples/app2/config/application.rb +40 -0
  95. data/samples/app2/config/boot.rb +4 -0
  96. data/samples/app2/config/credentials.yml.enc +1 -0
  97. data/samples/app2/config/database.yml +25 -0
  98. data/samples/app2/config/environment.rb +5 -0
  99. data/samples/app2/config/environments/development.rb +63 -0
  100. data/samples/app2/config/environments/production.rb +105 -0
  101. data/samples/app2/config/environments/test.rb +57 -0
  102. data/samples/app2/config/initializers/application_controller_renderer.rb +8 -0
  103. data/samples/app2/config/initializers/backtrace_silencers.rb +8 -0
  104. data/samples/app2/config/initializers/cors.rb +16 -0
  105. data/samples/app2/config/initializers/filter_parameter_logging.rb +6 -0
  106. data/samples/app2/config/initializers/inflections.rb +16 -0
  107. data/samples/app2/config/initializers/mime_types.rb +4 -0
  108. data/samples/app2/config/initializers/pubsub.rb +4 -0
  109. data/samples/app2/config/initializers/wrap_parameters.rb +14 -0
  110. data/samples/app2/config/locales/en.yml +33 -0
  111. data/samples/app2/config/puma.rb +43 -0
  112. data/samples/app2/config/routes.rb +3 -0
  113. data/samples/app2/config/spring.rb +6 -0
  114. data/samples/app2/db/migrate/20210513080956_create_customers.rb +10 -0
  115. data/samples/app2/db/migrate/20210513135203_create_posts.rb +10 -0
  116. data/samples/app2/db/schema.rb +31 -0
  117. data/samples/app2/db/seeds.rb +7 -0
  118. data/samples/app2/docker-compose.yml +20 -0
  119. data/samples/app2/log/.keep +0 -0
  120. metadata +97 -3
  121. data/lib/pub_sub_model_sync/publisher.rb +0 -40
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class PayloadBuilder < PubSubModelSync::Base
5
+ attr_accessor :model, :action, :data, :mapping, :headers, :as_klass
6
+
7
+ # @param model (ActiveRecord::Base)
8
+ # @param action (@see PublishConcern::ps_publish)
9
+ # @param settings (@see PublishConcern::ps_publish): { data:, mapping:, headers:, as_klass: }
10
+ def initialize(model, action, settings = {})
11
+ @model = model
12
+ @action = action
13
+ @data = settings[:data] || {}
14
+ @mapping = settings[:mapping] || []
15
+ @headers = settings[:headers] || {}
16
+ @as_klass = settings[:as_klass] || model.class.name
17
+ end
18
+
19
+ # @return (Payload)
20
+ def call
21
+ values = compute_value(data)
22
+ values = self.class.parse_mapping_for(model, mapping).merge(values)
23
+ PubSubModelSync::Payload.new(values, settings_data, headers_data)
24
+ end
25
+
26
+ def self.ordering_key_for(model)
27
+ [model.class.name, model.id || SecureRandom.uuid].join('/')
28
+ end
29
+
30
+ # @param model (ActiveRecord::Base)
31
+ # @param mapping (@see PublishConcern::ps_publish -> mapping)
32
+ # @return (Hash) Hash with the corresponding values for each attribute
33
+ # Sample: parse_mapping_for(my_model, %w[id name:full_name])
34
+ # ==> { id: 10, full_name: 'model.name value' }
35
+ def self.parse_mapping_for(model, mapping)
36
+ mapping.map do |prop|
37
+ source, target = prop.to_s.split(':')
38
+ [target || source, model.send(source.to_sym)]
39
+ end.to_h.symbolize_keys
40
+ end
41
+
42
+ private
43
+
44
+ def headers_data
45
+ klass_name = model.class.name
46
+ key = [klass_name, action, model.id || SecureRandom.uuid].join('/')
47
+ def_data = { ordering_key: self.class.ordering_key_for(model), key: key }
48
+ def_data.merge(compute_value(headers))
49
+ end
50
+
51
+ def compute_value(value)
52
+ res = value
53
+ res = model.send(value, action) if value.is_a?(Symbol) # method name
54
+ res = value.call(model, action) if value.is_a?(Proc)
55
+ res
56
+ end
57
+
58
+ def settings_data
59
+ { klass: as_klass, action: action }
60
+ end
61
+ end
62
+ end
@@ -2,73 +2,103 @@
2
2
 
3
3
  module PubSubModelSync
4
4
  module PublisherConcern
5
- def self.included(base)
6
- base.extend(ClassMethods)
7
- end
8
-
9
- # Before initializing sync service (callbacks: after create/update/destroy)
10
- def ps_skip_callback?(_action)
11
- false
12
- end
5
+ extend ActiveSupport::Concern
13
6
 
14
- # before preparing data to sync
15
- def ps_skip_sync?(_action)
16
- false
7
+ included do
8
+ extend ClassMethods
9
+ ps_init_transaction_callbacks if self <= ActiveRecord::Base
17
10
  end
18
11
 
19
12
  # before delivering data (return :cancel to cancel sync)
20
- def ps_before_sync(_action, _data); end
13
+ def ps_before_publish(_action, _payload); end
14
+ alias ps_before_sync ps_before_publish # @deprecated
21
15
 
22
16
  # after delivering data
23
- def ps_after_sync(_action, _data); end
17
+ def ps_after_publish(_action, _payload); end
18
+ alias ps_after_sync ps_after_publish # @deprecated
19
+
20
+ # Delivers a notification via pubsub
21
+ # @param action (Sym|String) Sample: create|update|save|destroy|<any_other_key>
22
+ # @param mapping? (Array<String>) If present will generate data using the mapping and added to the payload.
23
+ # Sample: ["id", "full_name:name"]
24
+ # @param data? (Hash|Symbol|Proc)
25
+ # Hash: Data to be added to the payload
26
+ # Symbol: Method name to be called to retrieve payload data (must return a hash value, receives :action name)
27
+ # Proc: Block to be called to retrieve payload data
28
+ # @param headers? (Hash|Symbol|Proc): (All available attributes in Payload.headers)
29
+ # Hash: Data that will be merged with default header values
30
+ # Symbol: Method name that will be called to retrieve header values (must return a hash, receives :action name)
31
+ # Proc: Block to be called to retrieve header values
32
+ # @param as_klass? (String): Output class name used instead of current class name
33
+ def ps_publish(action, data: {}, mapping: [], headers: {}, as_klass: self.class.name)
34
+ p_klass = PubSubModelSync::MessagePublisher
35
+ p_klass.publish_model(self, action, data: data, mapping: mapping, headers: headers, as_klass: as_klass)
36
+ end
37
+ delegate :ps_class_publish, to: :class
38
+
39
+ # Permits to perform manually the callback for a specific action
40
+ # @param action (Symbol, default: :create) Only :create|:update|:destroy
41
+ def ps_perform_publish(action = :create)
42
+ items = self.class.ps_cache_publish_callbacks.select { |item| item[:actions].include?(action) }
43
+ raise(StandardError, "No callback found for action :#{action}") if items.empty?
24
44
 
25
- # To perform sync on demand
26
- # @param attrs (Array, optional): custom attrs to be used
27
- # @param as_klass (Array, optional): custom klass name to be used
28
- # @param publisher (Publisher, optional): custom publisher object
29
- def ps_perform_sync(action = :create, attrs: nil, as_klass: nil,
30
- publisher: nil)
31
- publisher ||= self.class.ps_publisher(action).dup
32
- publisher.attrs = attrs if attrs
33
- publisher.as_klass = as_klass if as_klass
34
- PubSubModelSync::MessagePublisher.publish_model(self, action, publisher)
45
+ items.each { |item| instance_exec(action, &item[:callback]) }
46
+ self
35
47
  end
36
48
 
37
49
  module ClassMethods
38
- # Permit to configure to publish crud actions (:create, :update, :destroy)
39
- def ps_publish(attrs, actions: %i[create update destroy], as_klass: nil)
40
- klass = PubSubModelSync::Publisher
41
- publisher = klass.new(attrs, name, actions, as_klass)
42
- PubSubModelSync::Config.publishers << publisher
50
+ # Publishes a class level notification via pubsub
51
+ # @param data (Hash): Data of the notification
52
+ # @param action (Symbol): action name of the notification
53
+ # @param as_klass (String, default current class name): Class name of the notification
54
+ # @param headers (Hash, optional): header settings (More in Payload.headers)
55
+ def ps_class_publish(data, action:, as_klass: nil, headers: {})
56
+ klass = PubSubModelSync::MessagePublisher
57
+ klass.publish_data((as_klass || name).to_s, data, action.to_sym, headers: headers)
58
+ end
59
+
60
+ # @param crud_actions (Symbol|Array<Symbol>): :create, :update, :destroy
61
+ # @param method_name (Symbol, optional) method to be called
62
+ def ps_after_action(crud_actions, method_name = nil, &block)
63
+ actions = Array(crud_actions).map(&:to_sym)
64
+ callback = ->(action) { method_name ? send(method_name, action) : instance_exec(action, &block) }
65
+ ps_cache_publish_callbacks({ actions: actions, callback: callback })
43
66
  actions.each do |action|
44
- ps_register_callback(action.to_sym, publisher)
67
+ if action == :destroy
68
+ after_destroy { instance_exec(action, &callback) }
69
+ else
70
+ ps_define_commit_action(action, callback)
71
+ end
45
72
  end
46
73
  end
47
74
 
48
- # On demand class level publisher
49
- def ps_class_publish(data, action:, as_klass: nil)
50
- as_klass = (as_klass || name).to_s
51
- klass = PubSubModelSync::MessagePublisher
52
- klass.publish_data(as_klass, data, action.to_sym)
75
+ def ps_cache_publish_callbacks(new_value = nil)
76
+ @ps_cache_publish_callbacks ||= []
77
+ @ps_cache_publish_callbacks << new_value if new_value
78
+ @ps_cache_publish_callbacks
53
79
  end
54
80
 
55
- # Publisher info for specific action
56
- def ps_publisher(action = :create)
57
- PubSubModelSync::Config.publishers.find do |publisher|
58
- publisher.klass == name && publisher.actions.include?(action)
81
+ private
82
+
83
+ def ps_define_commit_action(action, callback)
84
+ if PubSubModelSync::Config.enable_rails4_before_commit # rails 4 compatibility
85
+ define_method("ps_before_#{action}_commit") { instance_exec(action, &callback) }
86
+ else
87
+ commit_name = respond_to?(:before_commit) ? :before_commit : :after_commit
88
+ send(commit_name, on: action) { instance_exec(action, &callback) }
59
89
  end
60
90
  end
61
91
 
62
- private
63
-
64
- def ps_register_callback(action, publisher)
65
- after_commit(on: action) do |model|
66
- disabled = PubSubModelSync::Config.disabled_callback_publisher.call(model, action)
67
- if !disabled && !model.ps_skip_callback?(action)
68
- klass = PubSubModelSync::MessagePublisher
69
- klass.publish_model(model, action.to_sym, publisher)
70
- end
92
+ # Initialize calls to start and end pub_sub transactions and deliver all them in the same order
93
+ def ps_init_transaction_callbacks
94
+ start_transaction = lambda do
95
+ @ps_transaction = PubSubModelSync::MessagePublisher.init_transaction(nil)
71
96
  end
97
+ before_create start_transaction, prepend: true
98
+ before_update start_transaction, prepend: true
99
+ before_destroy start_transaction, prepend: true
100
+ after_commit { @ps_transaction&.finish }
101
+ after_rollback(prepend: true) { @ps_transaction&.rollback }
72
102
  end
73
103
  end
74
104
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'pub_sub_model_sync'
4
4
  require 'rails'
5
+ require 'active_record'
6
+ require 'pub_sub_model_sync/config'
5
7
  module PubSubModelSync
6
8
  class Railtie < ::Rails::Railtie
7
9
  railtie_name :pub_sub_model_sync
@@ -9,5 +11,9 @@ module PubSubModelSync
9
11
  rake_tasks do
10
12
  load 'pub_sub_model_sync/tasks/worker.rake'
11
13
  end
14
+
15
+ configure do
16
+ require 'pub_sub_model_sync/initializers/before_commit' if PubSubModelSync::Config.enable_rails4_before_commit
17
+ end
12
18
  end
13
19
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PubSubModelSync
4
+ class RunSubscriber < Base
5
+ attr_accessor :subscriber, :payload, :model
6
+
7
+ delegate :settings, to: :subscriber
8
+
9
+ # @param subscriber(Subscriber)
10
+ # @param payload(Payload)
11
+ def initialize(subscriber, payload)
12
+ @subscriber = subscriber
13
+ @payload = payload
14
+ end
15
+
16
+ def call
17
+ klass_subscription? ? run_class_message : run_model_message
18
+ end
19
+
20
+ private
21
+
22
+ def klass_subscription?
23
+ subscriber.settings[:mode] == :klass
24
+ end
25
+
26
+ def run_class_message
27
+ model_class = subscriber.klass.constantize
28
+ model_class.ps_processing_payload = payload # TODO: review for parallel notifications
29
+ call_action(model_class) if ensure_sync(model_class)
30
+ end
31
+
32
+ # support for: create, update, destroy
33
+ def run_model_message
34
+ @model = find_model
35
+ model.ps_processing_payload = payload
36
+ return unless ensure_sync(model)
37
+
38
+ populate_model
39
+ model.send(:ps_before_save_sync) if model.respond_to?(:ps_before_save_sync)
40
+ call_action(model)
41
+ end
42
+
43
+ def ensure_sync(object)
44
+ res = true
45
+ res = false if settings[:if] && !parse_condition(settings[:if], object)
46
+ res = false if settings[:unless] && parse_condition(settings[:unless], object)
47
+ log("Cancelled save sync by subscriber condition : #{[payload]}") if !res && debug?
48
+ res
49
+ end
50
+
51
+ def call_action(object)
52
+ callback = settings[:to_action]
53
+ callback.is_a?(Proc) ? object.instance_exec(payload.data, &callback) : call_action_method(object)
54
+ end
55
+
56
+ def call_action_method(object)
57
+ method_name = settings[:to_action]
58
+ method_name = :save! if %i[create update].include?(method_name.to_sym)
59
+ method_name = :destroy! if method_name.to_sym == :destroy
60
+ is_crud_action = %i[save! destroy!].include?(method_name)
61
+ is_crud_action ? object.send(method_name) : object.send(method_name, payload.data)
62
+ end
63
+
64
+ def parse_condition(condition, object)
65
+ proc_args = klass_subscription? ? [] : [object]
66
+ case condition
67
+ when Proc then condition.call(*proc_args)
68
+ when Array then condition.all? { |method_name| object.send(method_name) }
69
+ else # method name
70
+ object.send(condition)
71
+ end
72
+ end
73
+
74
+ def find_model
75
+ model_class = subscriber.klass.constantize
76
+ return model_class.ps_find_model(payload.data) if model_class.respond_to?(:ps_find_model)
77
+
78
+ error_msg = 'No values provided for identifiers:'
79
+ raise(StandardError, "#{error_msg} #{[settings[:id], payload]}") if model_identifiers.empty?
80
+
81
+ model_class.where(model_identifiers).first_or_initialize
82
+ end
83
+
84
+ # @param mappings (Array<String,Symbol>) supports aliasing, sample: ["id", "full_name:name"]
85
+ # @return (Hash) hash with the correct attr names and its values
86
+ def parse_mapping(mappings)
87
+ mappings.map do |prop|
88
+ source, target = prop.to_s.split(':')
89
+ key = (target || source).to_sym
90
+ next unless payload.data.key?(source.to_sym)
91
+
92
+ [key, payload.data[source.to_sym]]
93
+ end.compact.to_h.symbolize_keys
94
+ end
95
+
96
+ # @return (Hash) hash including identifiers and its values
97
+ def model_identifiers
98
+ @model_identifiers ||= parse_mapping(Array(settings[:id]))
99
+ end
100
+
101
+ def populate_model
102
+ values = parse_mapping(subscriber.mapping).except(model_identifiers.keys)
103
+ values.each do |attr, value|
104
+ model.send("#{attr}=", value)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -4,50 +4,46 @@ require 'pub_sub_model_sync/payload'
4
4
  module PubSubModelSync
5
5
  class ServiceBase < PubSubModelSync::Base
6
6
  SERVICE_KEY = 'service_model_sync'
7
- PUBLISH_SETTINGS = {}.freeze
8
- LISTEN_SETTINGS = {}.freeze
9
7
 
10
8
  def listen_messages
11
- raise 'method :listen_messages must be defined in service'
9
+ raise NoMethodError, 'method :listen_messages must be defined in service'
12
10
  end
13
11
 
14
12
  # @param _payload (Payload)
15
13
  def publish(_payload)
16
- raise 'method :publish must be defined in service'
14
+ raise NoMethodError, 'method :publish must be defined in service'
17
15
  end
18
16
 
19
17
  def stop
20
- raise 'method :stop must be defined in service'
18
+ raise NoMethodError, 'method :stop must be defined in service'
21
19
  end
22
20
 
23
21
  private
24
22
 
23
+ # @param payload (Payload)
24
+ # @return (String): Json Format
25
+ def encode_payload(payload)
26
+ data = payload.to_h
27
+ not_important_keys = %i[ordering_key topic_name forced_ordering_key]
28
+ reduce_payload_size = !config.debug
29
+ data[:headers].except!(*not_important_keys) if reduce_payload_size
30
+ data.to_json
31
+ end
32
+
25
33
  # @param (String: Payload in json format)
26
34
  def process_message(payload_info)
27
- retries ||= 0
28
- payload = parse_payload(payload_info)
35
+ payload = decode_payload(payload_info)
29
36
  return payload.process unless same_app_message?(payload)
30
37
 
31
38
  log("Skipping message from same origin: #{[payload]}") if config.debug
32
39
  rescue => e
33
- retry if can_retry_process_message?(e, payload, retries += 1)
34
- end
35
-
36
- def can_retry_process_message?(error, payload, retries)
37
- error_payload = [payload, error.message, error.backtrace]
38
- if retries == 1
39
- log("Error while starting to process message (retrying...): #{error_payload}", :error)
40
- rescue_database_connection if lost_db_connection_err?(error)
41
- else
42
- log("Retried 1 time and error persists, exiting...: #{error_payload}", :error)
43
- Process.exit!(true)
44
- end
45
- retries == 1
40
+ error_payload = [payload, e.message, e.backtrace]
41
+ log("Error while starting to process a message: #{error_payload}", :error)
46
42
  end
47
43
 
48
- def parse_payload(payload_info)
49
- info = JSON.parse(payload_info).deep_symbolize_keys
50
- payload = ::PubSubModelSync::Payload.new(info[:data], info[:attributes], info[:headers])
44
+ # @return Payload
45
+ def decode_payload(payload_info)
46
+ payload = ::PubSubModelSync::Payload.from_payload_data(JSON.parse(payload_info))
51
47
  log("Received message: #{[payload]}") if config.debug
52
48
  payload
53
49
  end
@@ -57,19 +53,5 @@ module PubSubModelSync
57
53
  key = payload.headers[:app_key]
58
54
  key && key == config.subscription_key
59
55
  end
60
-
61
- def lost_db_connection_err?(error)
62
- return true if error.class.name == 'PG::UnableToSend' # rubocop:disable Style/ClassEqualityComparison
63
-
64
- error.message.match?(/lost connection/i)
65
- end
66
-
67
- def rescue_database_connection
68
- log('Lost DB connection. Attempting to reconnect...', :warn)
69
- ActiveRecord::Base.connection.reconnect!
70
- rescue
71
- log('Cannot reconnect to database, exiting...', :error)
72
- Process.exit!(true)
73
- end
74
56
  end
75
57
  end
@@ -7,50 +7,86 @@ end
7
7
 
8
8
  module PubSubModelSync
9
9
  class ServiceGoogle < ServiceBase
10
- LISTEN_SETTINGS = { threads: { callback: 1 }, message_ordering: true }.freeze
10
+ LISTEN_SETTINGS = { message_ordering: true }.freeze
11
+ PUBLISH_SETTINGS = {}.freeze
11
12
  TOPIC_SETTINGS = {}.freeze
12
13
  SUBSCRIPTION_SETTINGS = { message_ordering: true }.freeze
13
- attr_accessor :service, :topic, :subscription, :subscriber
14
+
15
+ # @!attribute topics (Hash): { key: Topic1, ... }
16
+ # @!attribute publish_topics (Hash): { key: Topic1, ... }
17
+ attr_accessor :service, :topics, :subscribers, :publish_topics
14
18
 
15
19
  def initialize
16
20
  @service = Google::Cloud::Pubsub.new(project: config.project,
17
21
  credentials: config.credentials)
18
- @topic = service.topic(config.topic_name) ||
19
- service.create_topic(config.topic_name, TOPIC_SETTINGS)
20
- topic.enable_message_ordering!
22
+ Array(config.topic_name || 'model_sync').each(&method(:init_topic))
21
23
  end
22
24
 
23
25
  def listen_messages
24
- @subscription = subscribe_to_topic
25
- @subscriber = subscription.listen(LISTEN_SETTINGS, &method(:process_message))
26
26
  log('Listener starting...')
27
- subscriber.start
27
+ @subscribers = subscribe_to_topics
28
28
  log('Listener started')
29
29
  sleep
30
- subscriber.stop.wait!
30
+ subscribers.each { |subscriber| subscriber.stop.wait! }
31
31
  log('Listener stopped')
32
32
  end
33
33
 
34
+ # @param payload (PubSubModelSync::Payload)
34
35
  def publish(payload)
35
- topic.publish_async(payload.to_json, message_headers) do |res|
36
- raise 'Failed to publish the message.' unless res.succeeded?
36
+ p_topic_names = Array(payload.headers[:topic_name] || config.default_topic_name)
37
+ message_topics = p_topic_names.map(&method(:find_topic))
38
+ message_topics.each do |topic|
39
+ topic.publish_async(encode_payload(payload), message_headers(payload)) do |res|
40
+ raise StandardError, 'Failed to publish the message.' unless res.succeeded?
41
+ end
37
42
  end
38
43
  end
39
44
 
40
45
  def stop
41
46
  log('Listener stopping...')
42
- subscriber.stop!
47
+ subscribers.each(&:stop!)
43
48
  end
44
49
 
45
50
  private
46
51
 
47
- def message_headers
48
- { SERVICE_KEY => true, ordering_key: SERVICE_KEY }.merge(PUBLISH_SETTINGS)
52
+ def find_topic(topic_name)
53
+ topic_name = topic_name.to_s
54
+ return topics.values.first unless topic_name.present?
55
+
56
+ topics[topic_name] || publish_topics[topic_name] || init_topic(topic_name, only_publish: true)
49
57
  end
50
58
 
51
- def subscribe_to_topic
52
- topic.subscription(config.subscription_key) ||
53
- topic.subscribe(config.subscription_key, SUBSCRIPTION_SETTINGS)
59
+ # @param only_publish (Boolean): if false is used to listen and publish messages
60
+ # @return (Topic): returns created or loaded topic
61
+ def init_topic(topic_name, only_publish: false)
62
+ topic_name = topic_name.to_s
63
+ @topics ||= {}
64
+ @publish_topics ||= {}
65
+ topic = service.topic(topic_name) || service.create_topic(topic_name, TOPIC_SETTINGS)
66
+ topic.enable_message_ordering!
67
+ publish_topics[topic_name] = topic if only_publish
68
+ topics[topic_name] = topic unless only_publish
69
+ topic
70
+ end
71
+
72
+ # @param payload (PubSubModelSync::Payload)
73
+ def message_headers(payload)
74
+ {
75
+ SERVICE_KEY => true,
76
+ ordering_key: payload.headers[:ordering_key]
77
+ }.merge(PUBLISH_SETTINGS)
78
+ end
79
+
80
+ # @return [Subscriber]
81
+ def subscribe_to_topics
82
+ topics.map do |key, topic|
83
+ subs_name = "#{config.subscription_key}_#{key}"
84
+ subscription = topic.subscription(subs_name) || topic.subscribe(subs_name, SUBSCRIPTION_SETTINGS)
85
+ subscriber = subscription.listen(LISTEN_SETTINGS, &method(:process_message))
86
+ subscriber.start
87
+ log("Subscribed to topic: #{topic.name} as: #{subs_name}")
88
+ subscriber
89
+ end
54
90
  end
55
91
 
56
92
  def process_message(received_message)