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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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