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