mantle 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +55 -0
- data/README.md +107 -0
- data/Rakefile +5 -0
- data/bin/mantle +14 -0
- data/circle.yml +3 -0
- data/config.ru +7 -0
- data/lib/generators/mantle/install/install_generator.rb +20 -0
- data/lib/generators/mantle/install/templates/mantle.rb +9 -0
- data/lib/generators/mantle/install/templates/mantle_message_handler.rb +7 -0
- data/lib/mantle.rb +45 -0
- data/lib/mantle/catch_up.rb +84 -0
- data/lib/mantle/cli.rb +73 -0
- data/lib/mantle/configuration.rb +27 -0
- data/lib/mantle/error.rb +6 -0
- data/lib/mantle/local_redis.rb +42 -0
- data/lib/mantle/message.rb +21 -0
- data/lib/mantle/message_bus.rb +44 -0
- data/lib/mantle/message_handler.rb +7 -0
- data/lib/mantle/message_router.rb +16 -0
- data/lib/mantle/railtie.rb +6 -0
- data/lib/mantle/version.rb +3 -0
- data/lib/mantle/workers/catch_up_cleanup_worker.rb +15 -0
- data/lib/mantle/workers/process_worker.rb +15 -0
- data/mantle.gemspec +25 -0
- data/spec/lib/mantle/catch_up_spec.rb +174 -0
- data/spec/lib/mantle/configuration_spec.rb +59 -0
- data/spec/lib/mantle/local_redis_spec.rb +29 -0
- data/spec/lib/mantle/message_bus_spec.rb +50 -0
- data/spec/lib/mantle/message_handler_spec.rb +12 -0
- data/spec/lib/mantle/message_router_spec.rb +61 -0
- data/spec/lib/mantle/message_spec.rb +23 -0
- data/spec/lib/mantle/workers/catch_up_cleanup_worker_spec.rb +20 -0
- data/spec/lib/mantle/workers/process_worker_spec.rb +25 -0
- data/spec/lib/mantle_spec.rb +26 -0
- data/spec/spec_helper.rb +18 -0
- metadata +152 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Mantle
|
4
|
+
class Configuration
|
5
|
+
|
6
|
+
attr_accessor :message_bus_channels,
|
7
|
+
:message_bus_redis,
|
8
|
+
:message_handler,
|
9
|
+
:logger,
|
10
|
+
:redis_namespace
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@message_bus_channels = []
|
14
|
+
@message_handler = Mantle::MessageHandler
|
15
|
+
@logger = default_logger
|
16
|
+
@redis_namespace = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def default_logger
|
22
|
+
logger = Logger.new(STDOUT)
|
23
|
+
logger.level = Logger::INFO
|
24
|
+
logger
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/mantle/error.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Mantle
|
2
|
+
class LocalRedis
|
3
|
+
SUCCESSFUL_MESSAGE_KEY = "last_successful_message_received"
|
4
|
+
CATCH_UP_CLEANUP_KEY = "mantle:catch_up:cleanup"
|
5
|
+
|
6
|
+
def self.set_message_successfully_received(time = Time.now.utc.to_f.to_s)
|
7
|
+
Sidekiq.redis { |conn| conn.set(SUCCESSFUL_MESSAGE_KEY, time) }
|
8
|
+
Mantle.logger.debug("Set last successful message received time: #{time}")
|
9
|
+
process_redis_response(time)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.last_message_successfully_received_at
|
13
|
+
result = Sidekiq.redis { |conn| conn.get(SUCCESSFUL_MESSAGE_KEY) }
|
14
|
+
process_redis_response(result)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.set_catch_up_cleanup(time = Time.now.utc.to_f.to_s)
|
18
|
+
Sidekiq.redis { |conn| conn.set(CATCH_UP_CLEANUP_KEY, time) }
|
19
|
+
Mantle.logger.debug("Set last catch up cleanup time: #{time}")
|
20
|
+
process_redis_response(time)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.last_catch_up_cleanup_at
|
24
|
+
result = Sidekiq.redis { |conn| conn.get(CATCH_UP_CLEANUP_KEY) }
|
25
|
+
process_redis_response(result)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def self.process_redis_response(result)
|
31
|
+
if result.nil? || result == ""
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
result.to_f
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.get(key)
|
39
|
+
Sidekiq.redis { |conn| conn.get(key) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Mantle
|
2
|
+
class Message
|
3
|
+
attr_reader :channel
|
4
|
+
attr_writer :message_bus, :catch_up
|
5
|
+
|
6
|
+
def initialize(channel)
|
7
|
+
@channel = channel
|
8
|
+
@message_bus = Mantle::MessageBus.new
|
9
|
+
@catch_up = Mantle::CatchUp.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def publish(message)
|
13
|
+
message_bus.publish(channel, message)
|
14
|
+
catch_up.add_message(channel, message)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :message_bus, :catch_up
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Mantle
|
2
|
+
class MessageBus
|
3
|
+
attr_writer :redis
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@redis = Mantle.configuration.message_bus_redis
|
7
|
+
@channels = Mantle.configuration.message_bus_channels
|
8
|
+
end
|
9
|
+
|
10
|
+
def publish(channel, message)
|
11
|
+
json = JSON.generate(message)
|
12
|
+
redis.publish(channel, json)
|
13
|
+
Mantle.logger.debug("Sent message to message bus channel: #{channel}")
|
14
|
+
end
|
15
|
+
|
16
|
+
def listen
|
17
|
+
Mantle.logger.info("Connecting to message bus redis: #{redis.inspect} ")
|
18
|
+
|
19
|
+
catch_up
|
20
|
+
subscribe_to_channels
|
21
|
+
end
|
22
|
+
|
23
|
+
def catch_up
|
24
|
+
Mantle::CatchUp.new.catch_up
|
25
|
+
end
|
26
|
+
|
27
|
+
def subscribe_to_channels
|
28
|
+
raise Mantle::Error::MissingRedisConnection unless redis
|
29
|
+
|
30
|
+
Mantle.logger.info("Subscribing to message bus for #{channels} ")
|
31
|
+
|
32
|
+
redis.subscribe(channels) do |on|
|
33
|
+
on.message do |channel, json_message|
|
34
|
+
message = JSON.parse(json_message)
|
35
|
+
Mantle::MessageRouter.new(channel, message).route
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :redis, :channels
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mantle
|
2
|
+
class MessageRouter
|
3
|
+
def initialize(channel, message)
|
4
|
+
@channel, @message = channel, message
|
5
|
+
end
|
6
|
+
|
7
|
+
def route
|
8
|
+
return unless @message
|
9
|
+
|
10
|
+
Mantle.logger.debug("Routing message for #{@channel}")
|
11
|
+
Mantle.logger.debug("Message: #{@message}")
|
12
|
+
|
13
|
+
Mantle::Workers::ProcessWorker.perform_async(@channel, @message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mantle
|
2
|
+
module Workers
|
3
|
+
class ProcessWorker
|
4
|
+
include Sidekiq::Worker
|
5
|
+
|
6
|
+
sidekiq_options queue: :mantle
|
7
|
+
|
8
|
+
def perform(channel, message)
|
9
|
+
Mantle.receive_message(channel, message)
|
10
|
+
Mantle::LocalRedis.set_message_successfully_received
|
11
|
+
Mantle::CatchUp.new.enqueue_clear_if_ready
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/mantle.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mantle/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "mantle"
|
8
|
+
gem.version = Mantle::VERSION
|
9
|
+
gem.authors = ["Grant Ammons", "Brandon Hilkert"]
|
10
|
+
gem.email = ["gammons@gmail.com", "brandonhilkert@gmail.com"]
|
11
|
+
gem.description = %q{Ruby application message bus subscriptions with Sidekiq and Redis Pubsub.}
|
12
|
+
gem.summary = %q{Ruby application message bus subscriptions with Sidekiq and Redis Pubsub.}
|
13
|
+
gem.homepage = "https://github.com/PipelineDeals/mantle"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency('redis')
|
21
|
+
gem.add_dependency('sidekiq')
|
22
|
+
|
23
|
+
gem.add_development_dependency('rspec')
|
24
|
+
gem.add_development_dependency('pry')
|
25
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mantle::CatchUp do
|
4
|
+
let(:handler) { Mantle::CatchUp.new }
|
5
|
+
let(:redis) { handler.redis }
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Mantle.logger.level = Logger::WARN
|
9
|
+
Mantle.configuration.message_bus_redis.flushdb
|
10
|
+
end
|
11
|
+
|
12
|
+
after :each do
|
13
|
+
Mantle.configuration.message_bus_redis.flushdb
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#add_message" do
|
17
|
+
it "adds message to redis that expires in 6 hours" do
|
18
|
+
channel = "person:update"
|
19
|
+
message = { id: 1 }
|
20
|
+
json_message = JSON.generate(message)
|
21
|
+
|
22
|
+
catch_up = Mantle::CatchUp.new
|
23
|
+
catch_up.add_message(channel, message)
|
24
|
+
|
25
|
+
json_payload = redis.zrange(catch_up.key, 0, -1).first
|
26
|
+
channel, message = catch_up.deserialize_payload(json_payload)
|
27
|
+
|
28
|
+
expect(channel).to eq(channel)
|
29
|
+
expect(message).to eq(message)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "returns time for message" do
|
33
|
+
catch_up = Mantle::CatchUp.new
|
34
|
+
time = catch_up.add_message("person:update", { id: 1 }, 1234.56)
|
35
|
+
expect(time).to eq(1234.56)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#enqueue_clear_if_ready" do
|
40
|
+
it "enqueues clear job if it was done more than 5 min. ago" do
|
41
|
+
time = Time.now.utc.to_f - ((Mantle::CatchUp::CLEANUP_EVERY_MINUTES + 1) * 60.0)
|
42
|
+
Mantle::LocalRedis.set_catch_up_cleanup(time)
|
43
|
+
|
44
|
+
expect {
|
45
|
+
Mantle::CatchUp.new.enqueue_clear_if_ready
|
46
|
+
}.to change(Mantle::Workers::CatchUpCleanupWorker.jobs, :size).by(1)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "enqueues clear job if no last cleanup time has been recorded" do
|
50
|
+
Mantle::LocalRedis.set_catch_up_cleanup(nil)
|
51
|
+
|
52
|
+
expect {
|
53
|
+
Mantle::CatchUp.new.enqueue_clear_if_ready
|
54
|
+
}.to change(Mantle::Workers::CatchUpCleanupWorker.jobs, :size).by(1)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "doesn't enqueue a clear job if enough time hasn't passed" do
|
58
|
+
time = Time.now.utc.to_f - ((Mantle::CatchUp::CLEANUP_EVERY_MINUTES - 1) * 60.0)
|
59
|
+
Mantle::LocalRedis.set_catch_up_cleanup(time)
|
60
|
+
|
61
|
+
expect {
|
62
|
+
Mantle::CatchUp.new.enqueue_clear_if_ready
|
63
|
+
}.to_not change(Mantle::Workers::CatchUpCleanupWorker.jobs, :size)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#clear_expired" do
|
68
|
+
it "clears expired entries from the catch up list" do
|
69
|
+
cu = Mantle::CatchUp.new
|
70
|
+
cu.add_message("person:update", { id: 1 }, cu.hours_ago_in_seconds(8))
|
71
|
+
cu.add_message("deal:update", { id: 2 }, cu.hours_ago_in_seconds(7))
|
72
|
+
cu.add_message("company:update", { id: 3 }, cu.hours_ago_in_seconds(5))
|
73
|
+
|
74
|
+
cu.clear_expired
|
75
|
+
|
76
|
+
expect(redis.zcount(cu.key, 0, 'inf')).to eq(1)
|
77
|
+
|
78
|
+
json_payload = redis.zrange(cu.key, 0, -1).first
|
79
|
+
channel, message = cu.deserialize_payload(json_payload)
|
80
|
+
|
81
|
+
expect(channel).to eq("company:update")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "catch_up" do
|
86
|
+
it "raises when redis connection is missing" do
|
87
|
+
cu = Mantle::CatchUp.new
|
88
|
+
cu.redis = nil
|
89
|
+
|
90
|
+
expect {
|
91
|
+
cu.catch_up
|
92
|
+
}.to raise_error(Mantle::Error::MissingRedisConnection)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "skips if no successfully processed time has been recorded" do
|
96
|
+
cu = Mantle::CatchUp.new
|
97
|
+
|
98
|
+
cu.add_message("person:update", { id: 1 })
|
99
|
+
cu.add_message("deal:update", { id: 2 })
|
100
|
+
cu.add_message("company:update", { id: 3 })
|
101
|
+
time = cu.add_message("user:update", { id: 3 })
|
102
|
+
|
103
|
+
expect(cu).to_not receive(:route_messages)
|
104
|
+
|
105
|
+
cu.catch_up
|
106
|
+
end
|
107
|
+
|
108
|
+
it "doesn't process anything when system is up to date no last successfully processed message time has been record" do
|
109
|
+
cu = Mantle::CatchUp.new
|
110
|
+
|
111
|
+
cu.add_message("person:update", { id: 1 })
|
112
|
+
cu.add_message("deal:update", { id: 2 })
|
113
|
+
cu.add_message("company:update", { id: 3 })
|
114
|
+
time = cu.add_message("user:update", { id: 3 })
|
115
|
+
|
116
|
+
Mantle::LocalRedis.set_message_successfully_received
|
117
|
+
|
118
|
+
expect(cu).to_not receive(:route_messages)
|
119
|
+
|
120
|
+
cu.catch_up
|
121
|
+
end
|
122
|
+
|
123
|
+
it "handles all messages that need catch up" do
|
124
|
+
cu = Mantle::CatchUp.new
|
125
|
+
|
126
|
+
cu.add_message("person:update", { id: 1 })
|
127
|
+
cu.add_message("deal:update", { id: 2 })
|
128
|
+
cu.add_message("company:update", { id: 3 })
|
129
|
+
|
130
|
+
Mantle::LocalRedis.set_message_successfully_received
|
131
|
+
|
132
|
+
time = cu.add_message("user:update", { id: 3 })
|
133
|
+
|
134
|
+
expect(cu).to receive(:route_messages).with(
|
135
|
+
[["{\"channel\":\"user:update\",\"message\":{\"id\":3}}", time]]
|
136
|
+
)
|
137
|
+
|
138
|
+
cu.catch_up
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#last_success_time" do
|
143
|
+
it "returns time of last successfully process message"do
|
144
|
+
expect(Mantle::LocalRedis).to receive(:last_message_successfully_received_at)
|
145
|
+
Mantle::CatchUp.new.last_success_time
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#route_messages" do
|
150
|
+
it "process messages if listening to that channel" do
|
151
|
+
p =[["{\"channel\":\"user:update\",\"message\":{\"id\":3}}", 1423336645.314663]]
|
152
|
+
cu = Mantle::CatchUp.new
|
153
|
+
cu.message_bus_channels = ["user:update"]
|
154
|
+
|
155
|
+
expect(Mantle::MessageRouter).to receive(:new).with(
|
156
|
+
"user:update", { "id" => 3 }
|
157
|
+
).and_return(double("router", route: true))
|
158
|
+
|
159
|
+
cu.route_messages(p)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "skips messages on channels not subscribed" do
|
163
|
+
p =[["{\"channel\":\"user:update\",\"message\":{\"id\":3}}", 1423336645.314663]]
|
164
|
+
cu = Mantle::CatchUp.new
|
165
|
+
cu.message_bus_channels = ["user:create"]
|
166
|
+
|
167
|
+
expect(Mantle::MessageRouter).to_not receive(:new)
|
168
|
+
|
169
|
+
cu.route_messages(p)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mantle::Configuration do
|
4
|
+
it 'can set/get message_bus_channels' do
|
5
|
+
config = Mantle::Configuration.new
|
6
|
+
config.message_bus_channels = ["update"]
|
7
|
+
expect(config.message_bus_channels).to eq(Array("update"))
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'sets default message_bus_channels' do
|
11
|
+
config = Mantle::Configuration.new
|
12
|
+
expect(config.message_bus_channels).to eq([])
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'can set/get message_bus_redis' do
|
16
|
+
redis = double("redis")
|
17
|
+
config = Mantle::Configuration.new
|
18
|
+
config.message_bus_channels = redis
|
19
|
+
expect(config.message_bus_channels).to eq(redis)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'can set/get message_handler' do
|
23
|
+
FakeHandler = Class.new
|
24
|
+
|
25
|
+
config = Mantle::Configuration.new
|
26
|
+
config.message_handler = FakeHandler
|
27
|
+
expect(config.message_handler).to eq(FakeHandler)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'configures default message handler' do
|
31
|
+
config = Mantle::Configuration.new
|
32
|
+
expect(config.message_handler).to eq(Mantle::MessageHandler)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'can set/get logger' do
|
36
|
+
logger = Logger.new(STDOUT)
|
37
|
+
config = Mantle::Configuration.new
|
38
|
+
config.logger = logger
|
39
|
+
expect(config.logger).to eq(logger)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'configures default logger' do
|
43
|
+
config = Mantle::Configuration.new
|
44
|
+
expect(config.logger.level).to eq(1)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'can set/get namespace for local redis listen' do
|
48
|
+
config = Mantle::Configuration.new
|
49
|
+
config.redis_namespace = "fake"
|
50
|
+
expect(config.redis_namespace).to eq("fake")
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'configures default redis namespace' do
|
54
|
+
config = Mantle::Configuration.new
|
55
|
+
expect(config.redis_namespace).to eq(nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|