mantle 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +55 -0
  6. data/README.md +107 -0
  7. data/Rakefile +5 -0
  8. data/bin/mantle +14 -0
  9. data/circle.yml +3 -0
  10. data/config.ru +7 -0
  11. data/lib/generators/mantle/install/install_generator.rb +20 -0
  12. data/lib/generators/mantle/install/templates/mantle.rb +9 -0
  13. data/lib/generators/mantle/install/templates/mantle_message_handler.rb +7 -0
  14. data/lib/mantle.rb +45 -0
  15. data/lib/mantle/catch_up.rb +84 -0
  16. data/lib/mantle/cli.rb +73 -0
  17. data/lib/mantle/configuration.rb +27 -0
  18. data/lib/mantle/error.rb +6 -0
  19. data/lib/mantle/local_redis.rb +42 -0
  20. data/lib/mantle/message.rb +21 -0
  21. data/lib/mantle/message_bus.rb +44 -0
  22. data/lib/mantle/message_handler.rb +7 -0
  23. data/lib/mantle/message_router.rb +16 -0
  24. data/lib/mantle/railtie.rb +6 -0
  25. data/lib/mantle/version.rb +3 -0
  26. data/lib/mantle/workers/catch_up_cleanup_worker.rb +15 -0
  27. data/lib/mantle/workers/process_worker.rb +15 -0
  28. data/mantle.gemspec +25 -0
  29. data/spec/lib/mantle/catch_up_spec.rb +174 -0
  30. data/spec/lib/mantle/configuration_spec.rb +59 -0
  31. data/spec/lib/mantle/local_redis_spec.rb +29 -0
  32. data/spec/lib/mantle/message_bus_spec.rb +50 -0
  33. data/spec/lib/mantle/message_handler_spec.rb +12 -0
  34. data/spec/lib/mantle/message_router_spec.rb +61 -0
  35. data/spec/lib/mantle/message_spec.rb +23 -0
  36. data/spec/lib/mantle/workers/catch_up_cleanup_worker_spec.rb +20 -0
  37. data/spec/lib/mantle/workers/process_worker_spec.rb +25 -0
  38. data/spec/lib/mantle_spec.rb +26 -0
  39. data/spec/spec_helper.rb +18 -0
  40. 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
@@ -0,0 +1,6 @@
1
+ module Mantle
2
+ module Error
3
+ MissingRedisConnection = Class.new(StandardError)
4
+ MissingImplementation = Class.new(StandardError)
5
+ end
6
+ end
@@ -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,7 @@
1
+ module Mantle
2
+ class MessageHandler
3
+ def self.receive(channel, message)
4
+ raise Mantle::Error::MissingImplementation.new("Implement self.receive(channel, object) and assign class to the message handler")
5
+ end
6
+ end
7
+ 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,6 @@
1
+ require 'rails'
2
+
3
+ module Mantle
4
+ class Railtie < Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Mantle
2
+ VERSION = '2.0.0'
3
+ end
@@ -0,0 +1,15 @@
1
+ module Mantle
2
+ module Workers
3
+ class CatchUpCleanupWorker
4
+ include Sidekiq::Worker
5
+
6
+ sidekiq_options queue: :mantle
7
+
8
+ def perform
9
+ Mantle::CatchUp.new.clear_expired
10
+ Mantle::LocalRedis.set_catch_up_cleanup
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -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
+