pub_sub_model_sync 0.1.2
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +24 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +8 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +220 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/pub_sub_model_sync.rb +20 -0
- data/lib/pub_sub_model_sync/config.rb +20 -0
- data/lib/pub_sub_model_sync/connector.rb +24 -0
- data/lib/pub_sub_model_sync/message_processor.rb +86 -0
- data/lib/pub_sub_model_sync/mock_google_service.rb +44 -0
- data/lib/pub_sub_model_sync/mock_rabbit_service.rb +48 -0
- data/lib/pub_sub_model_sync/publisher.rb +47 -0
- data/lib/pub_sub_model_sync/publisher_concern.rb +50 -0
- data/lib/pub_sub_model_sync/railtie.rb +13 -0
- data/lib/pub_sub_model_sync/runner.rb +40 -0
- data/lib/pub_sub_model_sync/service_google.rb +66 -0
- data/lib/pub_sub_model_sync/service_rabbit.rb +81 -0
- data/lib/pub_sub_model_sync/subscriber_concern.rb +42 -0
- data/lib/pub_sub_model_sync/tasks/worker.rake +8 -0
- data/lib/pub_sub_model_sync/version.rb +5 -0
- data/pub_sub_model_sync.gemspec +42 -0
- metadata +160 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'pub_sub_model_sync'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pub_sub_model_sync/version'
|
4
|
+
require 'active_support'
|
5
|
+
|
6
|
+
require 'pub_sub_model_sync/railtie'
|
7
|
+
require 'pub_sub_model_sync/config'
|
8
|
+
require 'pub_sub_model_sync/subscriber_concern'
|
9
|
+
require 'pub_sub_model_sync/publisher'
|
10
|
+
require 'pub_sub_model_sync/publisher_concern'
|
11
|
+
require 'pub_sub_model_sync/runner'
|
12
|
+
require 'pub_sub_model_sync/connector'
|
13
|
+
require 'pub_sub_model_sync/message_processor'
|
14
|
+
require 'pub_sub_model_sync/service_google'
|
15
|
+
require 'pub_sub_model_sync/service_rabbit'
|
16
|
+
|
17
|
+
module PubSubModelSync
|
18
|
+
class Error < StandardError; end
|
19
|
+
# Your code goes here...
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class Config
|
5
|
+
cattr_accessor :listeners, default: []
|
6
|
+
cattr_accessor :service_name, default: :google
|
7
|
+
cattr_accessor :logger
|
8
|
+
|
9
|
+
# google service
|
10
|
+
cattr_accessor :project, :credentials, :topic_name, :subscription_name
|
11
|
+
|
12
|
+
# rabbitmq service
|
13
|
+
cattr_accessor :bunny_connection, :queue_name, :topic_name
|
14
|
+
|
15
|
+
def self.log(msg, kind = :info)
|
16
|
+
msg = "PS_MSYNC ==> #{msg}"
|
17
|
+
logger ? logger.send(kind, msg) : puts(msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'google/cloud/pubsub'
|
4
|
+
module PubSubModelSync
|
5
|
+
class Connector
|
6
|
+
attr_accessor :service
|
7
|
+
delegate :listen_messages, :publish, :stop, to: :service
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@service = build_service
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def build_service
|
16
|
+
case Config.service_name
|
17
|
+
when :google
|
18
|
+
PubSubModelSync::ServiceGoogle.new
|
19
|
+
else # :rabbit_mq
|
20
|
+
PubSubModelSync::ServiceRabbit.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class MessageProcessor
|
5
|
+
attr_accessor :data, :attrs, :settings
|
6
|
+
|
7
|
+
# @param data (Hash): any hash value to deliver
|
8
|
+
# @param settings (optional): { id: id_val }
|
9
|
+
def initialize(data, klass, action, settings = {})
|
10
|
+
@data = data
|
11
|
+
@settings = settings
|
12
|
+
@attrs = settings.merge(klass: klass, action: action)
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
log 'processing message'
|
17
|
+
listeners = filter_listeners
|
18
|
+
eval_message(listeners) if listeners.any?
|
19
|
+
log 'processed message'
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def eval_message(listeners)
|
25
|
+
listeners.each do |listener|
|
26
|
+
if listener[:direct_mode]
|
27
|
+
call_class_listener(listener)
|
28
|
+
else
|
29
|
+
call_listener(listener)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def call_class_listener(listener)
|
35
|
+
model_class = listener[:klass].constantize
|
36
|
+
model_class.send(listener[:action], data)
|
37
|
+
rescue => e
|
38
|
+
log("Error listener (#{listener}): #{e.message}", :error)
|
39
|
+
end
|
40
|
+
|
41
|
+
# support for: create, update, destroy
|
42
|
+
def call_listener(listener)
|
43
|
+
listener_add_crud_settings(listener)
|
44
|
+
model = find_model(listener)
|
45
|
+
if attrs[:action].to_sym == :destroy
|
46
|
+
model.destroy!
|
47
|
+
else
|
48
|
+
populate_model(model, listener)
|
49
|
+
model.save!
|
50
|
+
end
|
51
|
+
rescue => e
|
52
|
+
log("Error listener (#{listener}): #{e.message}", :error)
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_model(listener)
|
56
|
+
model_class = listener[:klass].constantize
|
57
|
+
identifier = listener[:settings][:id] || :id
|
58
|
+
model_class.where(identifier => attrs[:id]).first ||
|
59
|
+
model_class.new(identifier => attrs[:id])
|
60
|
+
end
|
61
|
+
|
62
|
+
def populate_model(model, listener)
|
63
|
+
values = data.slice(*listener[:settings][:attrs])
|
64
|
+
values.each do |attr, value|
|
65
|
+
model.send("#{attr}=", value)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def filter_listeners
|
70
|
+
listeners = PubSubModelSync::Config.listeners
|
71
|
+
listeners.select do |listener|
|
72
|
+
listener[:as_klass].to_s == attrs[:klass].to_s &&
|
73
|
+
listener[:as_action].to_s == attrs[:action].to_s
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def listener_add_crud_settings(listener)
|
78
|
+
model_class = listener[:klass].constantize
|
79
|
+
listener[:settings] = model_class.ps_msync_subscriber_settings
|
80
|
+
end
|
81
|
+
|
82
|
+
def log(message, kind = :info)
|
83
|
+
PubSubModelSync::Config.log "#{message} ==> #{[data, attrs]}", kind
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class MockGoogleService
|
5
|
+
class MockStop
|
6
|
+
def wait!
|
7
|
+
true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class MockSubscriber
|
12
|
+
def start
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
@stop ||= MockStop.new
|
18
|
+
end
|
19
|
+
alias stop! stop
|
20
|
+
end
|
21
|
+
|
22
|
+
class MockSubscription
|
23
|
+
def listen(*_args)
|
24
|
+
@listen ||= MockSubscriber.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class MockTopic
|
29
|
+
def subscription(*_args)
|
30
|
+
@subscription ||= MockSubscription.new
|
31
|
+
end
|
32
|
+
alias subscribe subscription
|
33
|
+
|
34
|
+
def publish(*_args)
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def topic(*_args)
|
40
|
+
@topic ||= MockTopic.new
|
41
|
+
end
|
42
|
+
alias create_topic topic
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class MockRabbitService
|
5
|
+
class MockTopic
|
6
|
+
def publish(*_args)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class MockQueue
|
12
|
+
def bind(*_args)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def subscribe(*_args)
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
'name'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class MockChannel
|
26
|
+
def queue(*_args)
|
27
|
+
@queue ||= MockQueue.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def topic(*_args)
|
31
|
+
@topic ||= MockTopic.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_channel(*_args)
|
36
|
+
@create_channel ||= MockChannel.new
|
37
|
+
end
|
38
|
+
alias channel create_channel
|
39
|
+
|
40
|
+
def start
|
41
|
+
true
|
42
|
+
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
class Publisher
|
5
|
+
attr_accessor :connector
|
6
|
+
def initialize
|
7
|
+
@connector = PubSubModelSync::Connector.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def publish_data(klass, data, action)
|
11
|
+
attributes = self.class.build_attrs(klass, action)
|
12
|
+
connector.publish(data, attributes)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param settings (Hash): { attrs: [], as_klass: nil, id: nil }
|
16
|
+
def publish_model(model, action, settings = nil)
|
17
|
+
settings ||= model.class.ps_msync_publisher_settings
|
18
|
+
attributes = build_model_attrs(model, action, settings)
|
19
|
+
data = {}
|
20
|
+
if action != 'destroy'
|
21
|
+
data = model.as_json(only: settings[:attrs], methods: settings[:attrs])
|
22
|
+
end
|
23
|
+
connector.publish(data.symbolize_keys, attributes)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.build_attrs(klass, action, id = nil)
|
27
|
+
{
|
28
|
+
klass: klass.to_s,
|
29
|
+
action: action.to_sym,
|
30
|
+
id: id,
|
31
|
+
service_model_sync: true
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_model_attrs(model, action, settings)
|
38
|
+
as_klass = (settings[:as_klass] || model.class.name).to_s
|
39
|
+
id_val = model.send(settings[:id] || :id)
|
40
|
+
self.class.build_attrs(as_klass, action, id_val)
|
41
|
+
end
|
42
|
+
|
43
|
+
def log(msg)
|
44
|
+
PubSubModelSync::Config.log(msg)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PubSubModelSync
|
4
|
+
module PublisherConcern
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Permit to skip a publish callback
|
10
|
+
def ps_msync_skip_for?(_action)
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
# Permit to publish crud actions (:create, :update, :destroy)
|
16
|
+
# @param settings (Hash): { actions: nil, as_klass: nil, id: nil }
|
17
|
+
def ps_msync_publish(attrs, settings = {})
|
18
|
+
actions = settings.delete(:actions) || %i[create update destroy]
|
19
|
+
@ps_msync_publisher_settings = settings.merge(attrs: attrs)
|
20
|
+
ps_msync_register_callbacks(actions)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ps_msync_publisher_settings
|
24
|
+
@ps_msync_publisher_settings
|
25
|
+
end
|
26
|
+
|
27
|
+
def ps_msync_class_publish(data, action:, as_klass: nil)
|
28
|
+
as_klass = (as_klass || name).to_s
|
29
|
+
ps_msync_publisher.publish_data(as_klass, data, action.to_sym)
|
30
|
+
end
|
31
|
+
|
32
|
+
def ps_msync_publisher
|
33
|
+
PubSubModelSync::Publisher.new
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def ps_msync_register_callbacks(actions)
|
39
|
+
actions.each do |action|
|
40
|
+
after_commit(on: action) do |model|
|
41
|
+
unless model.ps_msync_skip_for?(action)
|
42
|
+
publisher = model.class.ps_msync_publisher
|
43
|
+
publisher.publish_model(model, action.to_sym)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pub_sub_model_sync'
|
4
|
+
require 'rails'
|
5
|
+
module PubSubModelSync
|
6
|
+
class Railtie < ::Rails::Railtie
|
7
|
+
railtie_name :pub_sub_model_sync
|
8
|
+
|
9
|
+
rake_tasks do
|
10
|
+
load 'pub_sub_model_sync/tasks/worker.rake'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module'
|
4
|
+
module PubSubModelSync
|
5
|
+
class Runner
|
6
|
+
class ShutDown < StandardError; end
|
7
|
+
attr_accessor :connector
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@connector = PubSubModelSync::Connector.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
trap_signals!
|
15
|
+
preload_framework!
|
16
|
+
start_listeners
|
17
|
+
rescue ShutDown
|
18
|
+
connector.stop
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def start_listeners
|
24
|
+
connector.listen_messages
|
25
|
+
end
|
26
|
+
|
27
|
+
def trap_signals!
|
28
|
+
handler = proc do |signal|
|
29
|
+
puts "received #{Signal.signame(signal)}"
|
30
|
+
raise ShutDown
|
31
|
+
end
|
32
|
+
%w[INT QUIT TERM].each { |signal| Signal.trap(signal, handler) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def preload_framework!
|
36
|
+
Rails.application.try(:eager_load!) if defined?(Rails)
|
37
|
+
Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk::Loader)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'google/cloud/pubsub'
|
5
|
+
rescue LoadError # rubocop:disable Lint/SuppressedException
|
6
|
+
end
|
7
|
+
|
8
|
+
module PubSubModelSync
|
9
|
+
class ServiceGoogle
|
10
|
+
attr_accessor :service, :topic, :subscription, :config, :subscriber
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@config = PubSubModelSync::Config
|
14
|
+
@service = Google::Cloud::Pubsub.new(project: config.project,
|
15
|
+
credentials: config.credentials)
|
16
|
+
@topic = service.topic(config.topic_name) ||
|
17
|
+
service.create_topic(config.topic_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def listen_messages
|
21
|
+
@subscription = subscribe_to_topic
|
22
|
+
@subscriber = subscription.listen(&method(:process_message))
|
23
|
+
log('Listener starting...')
|
24
|
+
subscriber.start
|
25
|
+
log('Listener started')
|
26
|
+
sleep
|
27
|
+
subscriber.stop.wait!
|
28
|
+
log('Listener stopped')
|
29
|
+
end
|
30
|
+
|
31
|
+
def publish(data, attributes)
|
32
|
+
log("Publishing message: #{[data, attributes]}")
|
33
|
+
topic.publish(data.to_json, attributes)
|
34
|
+
end
|
35
|
+
|
36
|
+
def stop
|
37
|
+
log('Listener stopping...')
|
38
|
+
subscriber.stop!
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def subscribe_to_topic
|
44
|
+
topic.subscription(config.subscription_name) ||
|
45
|
+
topic.subscribe(config.subscription_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_message(received_message)
|
49
|
+
message = received_message.message
|
50
|
+
attrs = message.attributes.symbolize_keys
|
51
|
+
return unless attrs[:service_model_sync]
|
52
|
+
|
53
|
+
data = JSON.parse(message.data).symbolize_keys
|
54
|
+
args = [data, attrs[:klass], attrs[:action], attrs]
|
55
|
+
PubSubModelSync::MessageProcessor.new(*args).process
|
56
|
+
rescue => e
|
57
|
+
log("Error processing message: #{[received_message, e.message]}")
|
58
|
+
ensure
|
59
|
+
received_message.acknowledge!
|
60
|
+
end
|
61
|
+
|
62
|
+
def log(msg)
|
63
|
+
config.log("Google Service ==> #{msg}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|