pub_sub_model_sync 0.1.0 → 0.1.5
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 +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/Gemfile +4 -1
- data/Gemfile.lock +11 -3
- data/README.md +78 -37
- data/lib/pub_sub_model_sync.rb +5 -0
- data/lib/pub_sub_model_sync/config.rb +17 -2
- data/lib/pub_sub_model_sync/connector.rb +12 -43
- data/lib/pub_sub_model_sync/message_processor.rb +0 -6
- data/lib/pub_sub_model_sync/mock_google_service.rb +5 -0
- data/lib/pub_sub_model_sync/mock_kafka_service.rb +45 -0
- data/lib/pub_sub_model_sync/mock_rabbit_service.rb +48 -0
- data/lib/pub_sub_model_sync/publisher.rb +14 -11
- data/lib/pub_sub_model_sync/publisher_concern.rb +19 -16
- data/lib/pub_sub_model_sync/runner.rb +4 -1
- data/lib/pub_sub_model_sync/service_base.rb +36 -0
- data/lib/pub_sub_model_sync/service_google.rb +65 -0
- data/lib/pub_sub_model_sync/service_kafka.rb +77 -0
- data/lib/pub_sub_model_sync/service_rabbit.rb +76 -0
- data/lib/pub_sub_model_sync/subscriber_concern.rb +13 -10
- data/lib/pub_sub_model_sync/version.rb +1 -1
- data/pub_sub_model_sync.gemspec +0 -1
- metadata +9 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd18a29b1e99da881b9c362dbc5541f6aeee0211988bcf301336691ac071e96d
|
4
|
+
data.tar.gz: cbc025ff0301e1bf4fd35d78fa2b70f06ff8a3c7c1dbfa12448f1cd3a53e592f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3036baf17c9f78c6b4b445eb5a3a2f7be63c2a78cb36dcf9aad1abcf1eff8b691979cd1ff79e1bac916b44b1996e0f80bedf841be102ad1ec29684c1437910a2
|
7
|
+
data.tar.gz: aead45d302ed53d84457376dc9fa64f05d267b7441125722d4621d4d2fc9d7097fa24f3aa693c50862b616f5a25b32e77a202a0503143d347f80290728701680
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Change Log
|
2
|
+
|
3
|
+
# 0.1.5
|
4
|
+
- Add apache kafka support
|
5
|
+
- Add Service interface for future references
|
6
|
+
- Improve Services to use a single/common message performer
|
7
|
+
|
8
|
+
# 0.1.4
|
9
|
+
- Add attribute aliases when publishing, ```ps_publish(['name:full_name', 'email'])```
|
10
|
+
- Ability to retrieve publisher/subscriber crud settings
|
11
|
+
|
12
|
+
# 0.1.3
|
13
|
+
- shorter publisher/subscriber methods: ps_msync_subscribe into ps_subscribe
|
14
|
+
|
15
|
+
# 0.1.2
|
16
|
+
- fix not found pub/sub library (buggy)
|
17
|
+
|
18
|
+
# 0.1.1
|
19
|
+
- Add rabbitmq pub/sub service support
|
20
|
+
- Reformat to support multiple pub/sub services
|
21
|
+
|
22
|
+
# 0.1.0
|
23
|
+
- Google pub/sub support
|
data/Gemfile
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
|
-
gem 'rubocop'
|
3
|
+
gem 'rubocop'
|
4
|
+
gem 'bunny' # rabbit-mq
|
5
|
+
gem 'google-cloud-pubsub' # google pub/sub
|
6
|
+
gem 'ruby-kafka' # kafka pub/sub
|
4
7
|
|
5
8
|
# Specify your gem's dependencies in pub_sub_model_sync.gemspec
|
6
9
|
gemspec
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
pub_sub_model_sync (0.1.
|
4
|
+
pub_sub_model_sync (0.1.5)
|
5
5
|
activesupport
|
6
|
-
google-cloud-pubsub
|
7
6
|
rails
|
8
7
|
|
9
8
|
GEM
|
@@ -52,12 +51,16 @@ GEM
|
|
52
51
|
tzinfo (~> 1.1)
|
53
52
|
addressable (2.7.0)
|
54
53
|
public_suffix (>= 2.0.2, < 5.0)
|
54
|
+
amq-protocol (2.3.0)
|
55
55
|
arel (9.0.0)
|
56
56
|
ast (2.4.0)
|
57
57
|
builder (3.2.4)
|
58
|
+
bunny (2.14.3)
|
59
|
+
amq-protocol (~> 2.3, >= 2.3.0)
|
58
60
|
concurrent-ruby (1.1.6)
|
59
61
|
crass (1.0.6)
|
60
62
|
diff-lcs (1.3)
|
63
|
+
digest-crc (0.5.1)
|
61
64
|
erubi (1.9.0)
|
62
65
|
faraday (0.17.3)
|
63
66
|
multipart-post (>= 1.2, < 3)
|
@@ -178,6 +181,8 @@ GEM
|
|
178
181
|
rexml
|
179
182
|
ruby-progressbar (~> 1.7)
|
180
183
|
unicode-display_width (>= 1.4.0, < 1.7)
|
184
|
+
ruby-kafka (1.0.0)
|
185
|
+
digest-crc
|
181
186
|
ruby-progressbar (1.10.1)
|
182
187
|
signet (0.11.0)
|
183
188
|
addressable (~> 2.3)
|
@@ -206,10 +211,13 @@ PLATFORMS
|
|
206
211
|
|
207
212
|
DEPENDENCIES
|
208
213
|
bundler
|
214
|
+
bunny
|
215
|
+
google-cloud-pubsub
|
209
216
|
pub_sub_model_sync!
|
210
217
|
rake
|
211
218
|
rspec
|
212
|
-
rubocop
|
219
|
+
rubocop
|
220
|
+
ruby-kafka
|
213
221
|
sqlite3
|
214
222
|
|
215
223
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -1,55 +1,80 @@
|
|
1
1
|
# PubSubModelSync
|
2
|
-
Permit to sync models between rails apps
|
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
|
Note: This gem is based on [MultipleMan](https://github.com/influitive/multiple_man) which for now looks unmaintained.
|
4
5
|
|
5
|
-
|
6
|
-
- Sync CRUD
|
6
|
+
## Features
|
7
|
+
- Sync CRUD operations between Rails apps. So, all changes made on App1, will be reflected on App2, App3.
|
7
8
|
Example: If User is created on App1, this user will be created on App2 too with the accepted attributes.
|
8
|
-
- Ability to make class level communication
|
9
|
-
Example: If User from App1 wants to generate_email, this can be listened on App2 to make corresponding actions
|
9
|
+
- Ability to make class level communication
|
10
|
+
Example: If User from App1 wants to generate_email, this can be listened on App2, App3, ... to make corresponding actions
|
11
|
+
- Change pub/sub service at any time
|
10
12
|
|
11
13
|
## Installation
|
12
14
|
Add this line to your application's Gemfile:
|
13
15
|
```ruby
|
14
16
|
gem 'pub_sub_model_sync'
|
17
|
+
|
18
|
+
gem 'google-cloud-pubsub' # to use google pub/sub service
|
19
|
+
gem 'bunny' # to use rabbit-mq pub/sub service
|
20
|
+
gem 'ruby-kafka' # to use apache kafka pub/sub service
|
15
21
|
```
|
16
22
|
And then execute: $ bundle install
|
17
23
|
|
18
24
|
|
19
25
|
## Usage
|
20
26
|
|
21
|
-
-
|
27
|
+
- Configuration for google pub/sub (You need google pub/sub service account)
|
22
28
|
```ruby
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
29
|
+
# initializers/pub_sub_config.rb
|
30
|
+
PubSubModelSync::Config.service_name = :google
|
31
|
+
PubSubModelSync::Config.project = 'project-id'
|
32
|
+
PubSubModelSync::Config.credentials = 'path-to-the-config'
|
33
|
+
PubSubModelSync::Config.topic_name = 'sample-topic'
|
34
|
+
PubSubModelSync::Config.subscription_name = 'p1-subscriber'
|
28
35
|
```
|
29
36
|
See details here:
|
30
37
|
https://github.com/googleapis/google-cloud-ruby/tree/master/google-cloud-pubsub
|
31
38
|
|
39
|
+
- configuration for RabbitMq (You need rabbitmq installed)
|
40
|
+
```ruby
|
41
|
+
PubSubModelSync::Config.service_name = :rabbitmq
|
42
|
+
PubSubModelSync::Config.bunny_connection = 'amqp://guest:guest@localhost'
|
43
|
+
PubSubModelSync::Config.queue_name = ''
|
44
|
+
PubSubModelSync::Config.topic_name = 'sample-topic'
|
45
|
+
```
|
46
|
+
See details here: https://github.com/ruby-amqp/bunny
|
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", "kafka2:9092"], logger: Rails.logger]
|
52
|
+
PubSubModelSync::Config.topic_name = 'sample-topic'
|
53
|
+
```
|
54
|
+
See details here: https://github.com/zendesk/ruby-kafka
|
55
|
+
|
32
56
|
- Add publishers/subscribers to your models (See examples below)
|
33
57
|
|
34
|
-
- Start
|
58
|
+
- Start subscribers to listen for publishers (Only in the app that has subscribers)
|
35
59
|
```ruby
|
36
60
|
rake pub_sub_model_sync:start
|
37
61
|
```
|
62
|
+
Note: Publishers do not need todo this
|
38
63
|
|
39
64
|
## Examples
|
40
65
|
```ruby
|
41
|
-
# App 1
|
66
|
+
# App 1 (Publisher)
|
42
67
|
# attributes: name email age
|
43
68
|
class User < ActiveRecord::Base
|
44
69
|
include PubSubModelSync::PublisherConcern
|
45
|
-
|
70
|
+
ps_publish(%i[name email])
|
46
71
|
end
|
47
72
|
|
48
|
-
# App 2
|
73
|
+
# App 2 (Subscriber)
|
49
74
|
class User < ActiveRecord::Base
|
50
75
|
include PubSubModelSync::SubscriberConcern
|
51
|
-
|
52
|
-
|
76
|
+
ps_subscribe(%i[name])
|
77
|
+
ps_class_subscribe(:greeting)
|
53
78
|
|
54
79
|
def self.greeting(data)
|
55
80
|
puts 'Class message called'
|
@@ -57,29 +82,30 @@ class User < ActiveRecord::Base
|
|
57
82
|
end
|
58
83
|
|
59
84
|
# Samples
|
60
|
-
User.create(name: 'test user') # Review your App 2 to see the created user (only name will be saved)
|
61
|
-
User.
|
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.ps_class_publish({ msg: 'Hello' }, action: :greeting) # User.greeting method (Class method) will be called in App2
|
87
|
+
PubSubModelSync::Publisher.new.publish_data(User, { msg: 'Hello' }, :greeting) # similar to above when not included publisher concern
|
62
88
|
```
|
63
89
|
|
64
90
|
## Advanced Example
|
65
91
|
```ruby
|
66
|
-
# App 1
|
92
|
+
# App 1 (Publisher)
|
67
93
|
class User < ActiveRecord::Base
|
68
94
|
self.table_name = 'publisher_users'
|
69
95
|
include PubSubModelSync::PublisherConcern
|
70
|
-
|
96
|
+
ps_publish(%i[name:full_name email], actions: %i[update], as_klass: 'Client', id: :client_id)
|
71
97
|
|
72
|
-
def
|
98
|
+
def ps_skip_for?(_action)
|
73
99
|
false # here logic with action to skip push message
|
74
100
|
end
|
75
101
|
end
|
76
102
|
|
77
|
-
# App 2
|
103
|
+
# App 2 (Subscriber)
|
78
104
|
class User < ActiveRecord::Base
|
79
105
|
self.table_name = 'subscriber_users'
|
80
106
|
include PubSubModelSync::SubscriberConcern
|
81
|
-
|
82
|
-
|
107
|
+
ps_subscribe(%i[name], actions: %i[update], as_klass: 'Client', id: :custom_id)
|
108
|
+
ps_class_subscribe(:greeting, as_action: :custom_greeting, as_klass: 'CustomUser')
|
83
109
|
|
84
110
|
def self.greeting(data)
|
85
111
|
puts 'Class message called through custom_greeting'
|
@@ -87,16 +113,31 @@ class User < ActiveRecord::Base
|
|
87
113
|
end
|
88
114
|
```
|
89
115
|
|
90
|
-
## Testing
|
91
|
-
-
|
116
|
+
## Testing with RSpec
|
117
|
+
- Config: (spec/rails_helper.rb)
|
92
118
|
```ruby
|
93
|
-
|
94
|
-
#
|
119
|
+
|
120
|
+
# when using google service
|
95
121
|
require 'pub_sub_model_sync/mock_google_service'
|
96
122
|
config.before(:each) do
|
97
|
-
|
98
|
-
allow(Google::Cloud::Pubsub).to receive(:new).and_return(
|
123
|
+
google_mock = PubSubModelSync::MockGoogleService.new
|
124
|
+
allow(Google::Cloud::Pubsub).to receive(:new).and_return(google_mock)
|
125
|
+
end
|
126
|
+
|
127
|
+
# when using rabbitmq service
|
128
|
+
require 'pub_sub_model_sync/mock_rabbit_service'
|
129
|
+
config.before(:each) do
|
130
|
+
rabbit_mock = PubSubModelSync::MockRabbitService.new
|
131
|
+
allow(Bunny).to receive(:new).and_return(rabbit_mock)
|
99
132
|
end
|
133
|
+
|
134
|
+
# when using apache kafka service
|
135
|
+
require 'pub_sub_model_sync/mock_kafka_service'
|
136
|
+
config.before(:each) do
|
137
|
+
kafka_mock = PubSubModelSync::MockKafkaService.new
|
138
|
+
allow(Kafka).to receive(:new).and_return(kafka_mock)
|
139
|
+
end
|
140
|
+
|
100
141
|
```
|
101
142
|
- Examples:
|
102
143
|
```ruby
|
@@ -124,7 +165,7 @@ end
|
|
124
165
|
publisher = PubSubModelSync::Publisher
|
125
166
|
data = { name: 'hello'}
|
126
167
|
action = :create
|
127
|
-
User.
|
168
|
+
User.ps_class_publish(data, action: action)
|
128
169
|
user = User.create(name: 'name', email: 'email')
|
129
170
|
expect_any_instance_of(publisher).to receive(:publish_model).with(user, :create, anything)
|
130
171
|
end
|
@@ -133,23 +174,23 @@ end
|
|
133
174
|
publisher = PubSubModelSync::Publisher
|
134
175
|
data = {msg: 'hello'}
|
135
176
|
action = :greeting
|
136
|
-
User.
|
177
|
+
User.ps_class_publish(data, action: action)
|
137
178
|
expect_any_instance_of(publisher).to receive(:publish_data).with('User', data, action)
|
138
179
|
end
|
139
180
|
```
|
140
181
|
|
141
|
-
There are two special methods to extract
|
182
|
+
There are two special methods to extract crud configuration settings (attrs, id, ...):
|
142
183
|
|
143
|
-
Subscribers: ```User.
|
184
|
+
Subscribers: ```User.ps_subscriber```
|
144
185
|
|
145
|
-
Publishers: ```User.
|
186
|
+
Publishers: ```User.ps_publisher```
|
146
187
|
|
147
188
|
Note: Inspect all configured listeners with:
|
148
189
|
``` PubSubModelSync::Config.listeners ```
|
149
190
|
|
150
191
|
## Contributing
|
151
192
|
|
152
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
193
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/owen2345/pub_sub_model_sync. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
153
194
|
|
154
195
|
## License
|
155
196
|
|
data/lib/pub_sub_model_sync.rb
CHANGED
@@ -12,6 +12,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
14
|
|
15
|
+
require 'pub_sub_model_sync/service_base'
|
16
|
+
require 'pub_sub_model_sync/service_google'
|
17
|
+
require 'pub_sub_model_sync/service_rabbit'
|
18
|
+
require 'pub_sub_model_sync/service_kafka'
|
19
|
+
|
15
20
|
module PubSubModelSync
|
16
21
|
class Error < StandardError; end
|
17
22
|
# Your code goes here...
|
@@ -3,11 +3,26 @@
|
|
3
3
|
module PubSubModelSync
|
4
4
|
class Config
|
5
5
|
cattr_accessor :listeners, default: []
|
6
|
-
cattr_accessor :
|
6
|
+
cattr_accessor :publishers, default: []
|
7
|
+
cattr_accessor :service_name, default: :google
|
7
8
|
cattr_accessor :logger
|
9
|
+
|
10
|
+
# google service
|
11
|
+
cattr_accessor :project, :credentials, :topic_name, :subscription_name
|
12
|
+
|
13
|
+
# rabbitmq service
|
14
|
+
cattr_accessor :bunny_connection, :queue_name, :topic_name
|
15
|
+
|
16
|
+
# kafka service
|
17
|
+
cattr_accessor :kafka_connection, :topic_name
|
18
|
+
|
8
19
|
def self.log(msg, kind = :info)
|
9
20
|
msg = "PS_MSYNC ==> #{msg}"
|
10
|
-
|
21
|
+
if logger == :raise_error
|
22
|
+
kind == :error ? raise(msg) : puts(msg)
|
23
|
+
else
|
24
|
+
logger ? logger.send(kind, msg) : puts(msg)
|
25
|
+
end
|
11
26
|
end
|
12
27
|
end
|
13
28
|
end
|
@@ -3,55 +3,24 @@
|
|
3
3
|
require 'google/cloud/pubsub'
|
4
4
|
module PubSubModelSync
|
5
5
|
class Connector
|
6
|
-
attr_accessor :service
|
6
|
+
attr_accessor :service
|
7
|
+
delegate :listen_messages, :publish, :stop, to: :service
|
7
8
|
|
8
9
|
def initialize
|
9
|
-
@
|
10
|
-
@service = Google::Cloud::Pubsub.new(project: config.project,
|
11
|
-
credentials: config.credentials)
|
12
|
-
@topic = service.topic(config.topic_name) ||
|
13
|
-
service.create_topic(config.topic_name)
|
14
|
-
end
|
15
|
-
|
16
|
-
def listen_messages
|
17
|
-
@subscription = subscribe_to_topic
|
18
|
-
@subscriber = subscription.listen(&method(:process_message))
|
19
|
-
log('Listener starting...')
|
20
|
-
subscriber.start
|
21
|
-
log('Listener started')
|
22
|
-
sleep
|
23
|
-
subscriber.stop.wait!
|
24
|
-
log('Listener stopped')
|
25
|
-
end
|
26
|
-
|
27
|
-
def stop
|
28
|
-
log('Listener stopping...')
|
29
|
-
subscriber.stop!
|
10
|
+
@service = build_service
|
30
11
|
end
|
31
12
|
|
32
13
|
private
|
33
14
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
data = JSON.parse(message.data).symbolize_keys
|
45
|
-
args = [data, attrs[:klass], attrs[:action], attrs]
|
46
|
-
PubSubModelSync::MessageProcessor.new(*args).process
|
47
|
-
rescue => e
|
48
|
-
log("Error processing message: #{[received_message, e.message]}")
|
49
|
-
ensure
|
50
|
-
received_message.acknowledge!
|
51
|
-
end
|
52
|
-
|
53
|
-
def log(msg)
|
54
|
-
config.log(msg)
|
15
|
+
def build_service
|
16
|
+
case Config.service_name
|
17
|
+
when :google
|
18
|
+
PubSubModelSync::ServiceGoogle.new
|
19
|
+
when :kafka
|
20
|
+
PubSubModelSync::ServiceKafka.new
|
21
|
+
else # :rabbit_mq
|
22
|
+
PubSubModelSync::ServiceRabbit.new
|
23
|
+
end
|
55
24
|
end
|
56
25
|
end
|
57
26
|
end
|
@@ -40,7 +40,6 @@ module PubSubModelSync
|
|
40
40
|
|
41
41
|
# support for: create, update, destroy
|
42
42
|
def call_listener(listener)
|
43
|
-
listener_add_crud_settings(listener)
|
44
43
|
model = find_model(listener)
|
45
44
|
if attrs[:action].to_sym == :destroy
|
46
45
|
model.destroy!
|
@@ -74,11 +73,6 @@ module PubSubModelSync
|
|
74
73
|
end
|
75
74
|
end
|
76
75
|
|
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
76
|
def log(message, kind = :info)
|
83
77
|
PubSubModelSync::Config.log "#{message} ==> #{[data, attrs]}", kind
|
84
78
|
end
|
@@ -7,6 +7,7 @@ module PubSubModelSync
|
|
7
7
|
true
|
8
8
|
end
|
9
9
|
end
|
10
|
+
|
10
11
|
class MockSubscriber
|
11
12
|
def start
|
12
13
|
true
|
@@ -15,12 +16,15 @@ module PubSubModelSync
|
|
15
16
|
def stop
|
16
17
|
@stop ||= MockStop.new
|
17
18
|
end
|
19
|
+
alias stop! stop
|
18
20
|
end
|
21
|
+
|
19
22
|
class MockSubscription
|
20
23
|
def listen(*_args)
|
21
24
|
@listen ||= MockSubscriber.new
|
22
25
|
end
|
23
26
|
end
|
27
|
+
|
24
28
|
class MockTopic
|
25
29
|
def subscription(*_args)
|
26
30
|
@subscription ||= MockSubscription.new
|
@@ -31,6 +35,7 @@ module PubSubModelSync
|
|
31
35
|
true
|
32
36
|
end
|
33
37
|
end
|
38
|
+
|
34
39
|
def topic(*_args)
|
35
40
|
@topic ||= MockTopic.new
|
36
41
|
end
|
@@ -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
|
@@ -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
|
@@ -9,34 +9,37 @@ module PubSubModelSync
|
|
9
9
|
|
10
10
|
def publish_data(klass, data, action)
|
11
11
|
attributes = self.class.build_attrs(klass, action)
|
12
|
-
publish(data, attributes)
|
12
|
+
connector.publish(data, attributes)
|
13
13
|
end
|
14
14
|
|
15
15
|
# @param settings (Hash): { attrs: [], as_klass: nil, id: nil }
|
16
16
|
def publish_model(model, action, settings = nil)
|
17
|
-
settings ||= model.class.
|
17
|
+
settings ||= model.class.ps_publisher_info(action)
|
18
18
|
attributes = build_model_attrs(model, action, settings)
|
19
19
|
data = {}
|
20
|
-
if action !=
|
21
|
-
|
22
|
-
end
|
23
|
-
publish(data.symbolize_keys, attributes)
|
20
|
+
data = build_model_data(model, settings[:attrs]) if action != :destroy
|
21
|
+
connector.publish(data.symbolize_keys, attributes)
|
24
22
|
end
|
25
23
|
|
26
24
|
def self.build_attrs(klass, action, id = nil)
|
27
25
|
{
|
28
26
|
klass: klass.to_s,
|
29
27
|
action: action.to_sym,
|
30
|
-
id: id
|
31
|
-
service_model_sync: true
|
28
|
+
id: id
|
32
29
|
}
|
33
30
|
end
|
34
31
|
|
35
32
|
private
|
36
33
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
34
|
+
def build_model_data(model, model_props)
|
35
|
+
source_props = model_props.map { |prop| prop.to_s.split(':').first }
|
36
|
+
data = model.as_json(only: source_props, methods: source_props)
|
37
|
+
aliased_props = model_props.select { |prop| prop.to_s.include?(':') }
|
38
|
+
aliased_props.each do |prop|
|
39
|
+
source, target = prop.to_s.split(':')
|
40
|
+
data[target] = data.delete(source)
|
41
|
+
end
|
42
|
+
data.symbolize_keys
|
40
43
|
end
|
41
44
|
|
42
45
|
def build_model_attrs(model, action, settings)
|
@@ -7,41 +7,44 @@ module PubSubModelSync
|
|
7
7
|
end
|
8
8
|
|
9
9
|
# Permit to skip a publish callback
|
10
|
-
def
|
10
|
+
def ps_skip_for?(_action)
|
11
11
|
false
|
12
12
|
end
|
13
13
|
|
14
14
|
module ClassMethods
|
15
15
|
# Permit to publish crud actions (:create, :update, :destroy)
|
16
16
|
# @param settings (Hash): { actions: nil, as_klass: nil, id: nil }
|
17
|
-
def
|
17
|
+
def ps_publish(attrs, settings = {})
|
18
18
|
actions = settings.delete(:actions) || %i[create update destroy]
|
19
|
-
|
20
|
-
|
19
|
+
actions.each do |action|
|
20
|
+
info = settings.merge(klass: name, action: action, attrs: attrs)
|
21
|
+
PubSubModelSync::Config.publishers << info
|
22
|
+
ps_register_callback(action.to_sym, info)
|
23
|
+
end
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
24
|
-
|
26
|
+
def ps_publisher_info(action = :create)
|
27
|
+
PubSubModelSync::Config.publishers.select do |listener|
|
28
|
+
listener[:klass] == name && listener[:action] == action
|
29
|
+
end.last
|
25
30
|
end
|
26
31
|
|
27
|
-
def
|
32
|
+
def ps_class_publish(data, action:, as_klass: nil)
|
28
33
|
as_klass = (as_klass || name).to_s
|
29
|
-
|
34
|
+
ps_publisher_service.publish_data(as_klass, data, action.to_sym)
|
30
35
|
end
|
31
36
|
|
32
|
-
def
|
37
|
+
def ps_publisher_service
|
33
38
|
PubSubModelSync::Publisher.new
|
34
39
|
end
|
35
40
|
|
36
41
|
private
|
37
42
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
publisher.publish_model(model, action.to_sym)
|
44
|
-
end
|
43
|
+
def ps_register_callback(action, info)
|
44
|
+
after_commit(on: action) do |model|
|
45
|
+
unless model.ps_skip_for?(action)
|
46
|
+
service = model.class.ps_publisher_service
|
47
|
+
service.publish_model(model, action.to_sym, info)
|
45
48
|
end
|
46
49
|
end
|
47
50
|
end
|
@@ -6,6 +6,10 @@ module PubSubModelSync
|
|
6
6
|
class ShutDown < StandardError; end
|
7
7
|
attr_accessor :connector
|
8
8
|
|
9
|
+
def initialize
|
10
|
+
@connector = PubSubModelSync::Connector.new
|
11
|
+
end
|
12
|
+
|
9
13
|
def run
|
10
14
|
trap_signals!
|
11
15
|
preload_framework!
|
@@ -17,7 +21,6 @@ module PubSubModelSync
|
|
17
21
|
private
|
18
22
|
|
19
23
|
def start_listeners
|
20
|
-
@connector = PubSubModelSync::Connector.new
|
21
24
|
connector.listen_messages
|
22
25
|
end
|
23
26
|
|
@@ -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
|
@@ -0,0 +1,65 @@
|
|
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 < ServiceBase
|
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
|
+
|
34
|
+
payload = { data: data, attributes: attributes }.to_json
|
35
|
+
topic.publish(payload, { SERVICE_KEY => true })
|
36
|
+
end
|
37
|
+
|
38
|
+
def stop
|
39
|
+
log('Listener stopping...')
|
40
|
+
subscriber.stop!
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def subscribe_to_topic
|
46
|
+
topic.subscription(config.subscription_name) ||
|
47
|
+
topic.subscribe(config.subscription_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_message(received_message)
|
51
|
+
message = received_message.message
|
52
|
+
return unless message.attributes[SERVICE_KEY]
|
53
|
+
|
54
|
+
perform_message(message.data)
|
55
|
+
rescue => e
|
56
|
+
log("Error processing message: #{[received_message, e.message]}", :error)
|
57
|
+
ensure
|
58
|
+
received_message.acknowledge!
|
59
|
+
end
|
60
|
+
|
61
|
+
def log(msg, kind = :info)
|
62
|
+
config.log("Google Service ==> #{msg}", kind)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -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
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bunny'
|
5
|
+
rescue LoadError # rubocop:disable Lint/SuppressedException
|
6
|
+
end
|
7
|
+
|
8
|
+
module PubSubModelSync
|
9
|
+
class ServiceRabbit < ServiceBase
|
10
|
+
attr_accessor :service, :channel, :queue, :topic
|
11
|
+
attr_accessor :config
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@config = PubSubModelSync::Config
|
15
|
+
@service = Bunny.new(*config.bunny_connection)
|
16
|
+
end
|
17
|
+
|
18
|
+
def listen_messages
|
19
|
+
log('Listener starting...')
|
20
|
+
subscribe_to_queue
|
21
|
+
log('Listener started')
|
22
|
+
queue.subscribe(block: true, manual_ack: false, &method(:process_message))
|
23
|
+
rescue PubSubModelSync::Runner::ShutDown
|
24
|
+
raise
|
25
|
+
rescue => e
|
26
|
+
log("Error listening message: #{[e.message, e.backtrace]}", :error)
|
27
|
+
end
|
28
|
+
|
29
|
+
def publish(data, attributes)
|
30
|
+
log("Publishing: #{[data, attributes]}")
|
31
|
+
subscribe_to_queue
|
32
|
+
payload = { data: data, attributes: attributes }
|
33
|
+
topic.publish(payload.to_json, message_settings)
|
34
|
+
rescue => e
|
35
|
+
info = [data, attributes, e.message, e.backtrace]
|
36
|
+
log("Error publishing: #{info}", :error)
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
log('Listener stopping...')
|
41
|
+
service.close
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def message_settings
|
47
|
+
{ routing_key: queue.name, type: SERVICE_KEY }
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_message(_delivery_info, meta_info, payload)
|
51
|
+
return unless meta_info[:type] == SERVICE_KEY
|
52
|
+
|
53
|
+
perform_message(payload)
|
54
|
+
rescue => e
|
55
|
+
error = [payload, e.message, e.backtrace]
|
56
|
+
log("Error processing message: #{error}", :error)
|
57
|
+
end
|
58
|
+
|
59
|
+
def subscribe_to_queue
|
60
|
+
service.start
|
61
|
+
@channel = service.create_channel
|
62
|
+
queue_settings = { durable: true, auto_delete: false }
|
63
|
+
@queue = channel.queue(config.queue_name, queue_settings)
|
64
|
+
subscribe_to_topic
|
65
|
+
end
|
66
|
+
|
67
|
+
def subscribe_to_topic
|
68
|
+
@topic = channel.topic(config.topic_name)
|
69
|
+
queue.bind(topic, routing_key: queue.name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def log(msg, kind = :info)
|
73
|
+
config.log("Rabbit Service ==> #{msg}", kind)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -8,32 +8,35 @@ module PubSubModelSync
|
|
8
8
|
|
9
9
|
module ClassMethods
|
10
10
|
# @param settings (Hash): { as_klass: nil, actions: nil, id: nil }
|
11
|
-
def
|
12
|
-
|
11
|
+
def ps_subscribe(attrs, settings = {})
|
12
|
+
as_klass = (settings[:as_klass] || name).to_s
|
13
13
|
actions = settings.delete(:actions) || %i[create update destroy]
|
14
|
-
|
14
|
+
settings = settings.merge(attrs: attrs)
|
15
15
|
actions.each do |action|
|
16
|
-
|
16
|
+
add_ps_subscriber(as_klass, action, action, false, settings)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
21
|
-
|
20
|
+
def ps_class_subscribe(action, as_action: nil, as_klass: nil)
|
21
|
+
add_ps_subscriber(as_klass, action, as_action, true, {})
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
|
24
|
+
def ps_subscriber(action = :create)
|
25
|
+
PubSubModelSync::Config.listeners.select do |listener|
|
26
|
+
listener[:klass] == name && listener[:action] == action
|
27
|
+
end.last
|
26
28
|
end
|
27
29
|
|
28
30
|
private
|
29
31
|
|
30
|
-
def
|
32
|
+
def add_ps_subscriber(as_klass, action, as_action, direct_mode, settings)
|
31
33
|
listener = {
|
32
34
|
klass: name,
|
33
35
|
as_klass: (as_klass || name).to_s,
|
34
36
|
action: action.to_sym,
|
35
37
|
as_action: (as_action || action).to_sym,
|
36
|
-
direct_mode: direct_mode
|
38
|
+
direct_mode: direct_mode,
|
39
|
+
settings: settings
|
37
40
|
}
|
38
41
|
PubSubModelSync::Config.listeners << listener
|
39
42
|
end
|
data/pub_sub_model_sync.gemspec
CHANGED
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
|
+
version: 0.1.5
|
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-
|
11
|
+
date: 2020-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,20 +24,6 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: google-cloud-pubsub
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
27
|
- !ruby/object:Gem::Dependency
|
42
28
|
name: rails
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -119,6 +105,7 @@ files:
|
|
119
105
|
- ".rspec"
|
120
106
|
- ".rubocop.yml"
|
121
107
|
- ".travis.yml"
|
108
|
+
- CHANGELOG.md
|
122
109
|
- CODE_OF_CONDUCT.md
|
123
110
|
- Gemfile
|
124
111
|
- Gemfile.lock
|
@@ -132,10 +119,16 @@ files:
|
|
132
119
|
- lib/pub_sub_model_sync/connector.rb
|
133
120
|
- lib/pub_sub_model_sync/message_processor.rb
|
134
121
|
- lib/pub_sub_model_sync/mock_google_service.rb
|
122
|
+
- lib/pub_sub_model_sync/mock_kafka_service.rb
|
123
|
+
- lib/pub_sub_model_sync/mock_rabbit_service.rb
|
135
124
|
- lib/pub_sub_model_sync/publisher.rb
|
136
125
|
- lib/pub_sub_model_sync/publisher_concern.rb
|
137
126
|
- lib/pub_sub_model_sync/railtie.rb
|
138
127
|
- lib/pub_sub_model_sync/runner.rb
|
128
|
+
- lib/pub_sub_model_sync/service_base.rb
|
129
|
+
- lib/pub_sub_model_sync/service_google.rb
|
130
|
+
- lib/pub_sub_model_sync/service_kafka.rb
|
131
|
+
- lib/pub_sub_model_sync/service_rabbit.rb
|
139
132
|
- lib/pub_sub_model_sync/subscriber_concern.rb
|
140
133
|
- lib/pub_sub_model_sync/tasks/worker.rake
|
141
134
|
- lib/pub_sub_model_sync/version.rb
|