mantle 2.0.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.
- 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
|
+
|