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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9faf822274115317d82c5b0e254b7551df79b3f5
|
4
|
+
data.tar.gz: 2f9d06db2359478ca001df1b124eae4a46f84511
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8aa2b4e2059be5955e1f3cf033e0a775db5ffc29fb54947887a709024f9bc8e774ffc00b23d8fc9071685cbaff906b80459ce4908244efd873b981f36703a63f
|
7
|
+
data.tar.gz: 946357abc8c54760e04b0b94f2f66c0238279181fa1b7cda362ae294a20bbf799104079662ef282e9ed6e5ba81aad2a73d600ebe0feefac7aaf13200e7106ad0
|
data/.gitignore
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile ~/.gitignore_global
|
6
|
+
|
7
|
+
# Ignore bundler config
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
# Ignore the default SQLite database.
|
11
|
+
/db/*.sqlite3
|
12
|
+
|
13
|
+
# Ignore all logfiles and tempfiles.
|
14
|
+
/log/*.log
|
15
|
+
/tmp
|
16
|
+
|
17
|
+
.DS_Store
|
18
|
+
.idea
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mantle (2.0.0)
|
5
|
+
redis
|
6
|
+
sidekiq
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
celluloid (0.16.0)
|
12
|
+
timers (~> 4.0.0)
|
13
|
+
coderay (1.1.0)
|
14
|
+
connection_pool (2.1.2)
|
15
|
+
diff-lcs (1.2.5)
|
16
|
+
hitimes (1.2.2)
|
17
|
+
json (1.8.2)
|
18
|
+
method_source (0.8.2)
|
19
|
+
pry (0.10.0)
|
20
|
+
coderay (~> 1.1.0)
|
21
|
+
method_source (~> 0.8.1)
|
22
|
+
slop (~> 3.4)
|
23
|
+
redis (3.2.1)
|
24
|
+
redis-namespace (1.5.1)
|
25
|
+
redis (~> 3.0, >= 3.0.4)
|
26
|
+
rspec (3.2.0)
|
27
|
+
rspec-core (~> 3.2.0)
|
28
|
+
rspec-expectations (~> 3.2.0)
|
29
|
+
rspec-mocks (~> 3.2.0)
|
30
|
+
rspec-core (3.2.2)
|
31
|
+
rspec-support (~> 3.2.0)
|
32
|
+
rspec-expectations (3.2.0)
|
33
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
34
|
+
rspec-support (~> 3.2.0)
|
35
|
+
rspec-mocks (3.2.0)
|
36
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
37
|
+
rspec-support (~> 3.2.0)
|
38
|
+
rspec-support (3.2.2)
|
39
|
+
sidekiq (3.3.2)
|
40
|
+
celluloid (>= 0.16.0)
|
41
|
+
connection_pool (>= 2.1.1)
|
42
|
+
json
|
43
|
+
redis (>= 3.0.6)
|
44
|
+
redis-namespace (>= 1.3.1)
|
45
|
+
slop (3.5.0)
|
46
|
+
timers (4.0.1)
|
47
|
+
hitimes
|
48
|
+
|
49
|
+
PLATFORMS
|
50
|
+
ruby
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
mantle!
|
54
|
+
pry
|
55
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# Mantle
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
gem 'mantle'
|
8
|
+
|
9
|
+
or install manually by:
|
10
|
+
|
11
|
+
$ gem install mantle
|
12
|
+
|
13
|
+
|
14
|
+
## Usage (in Rails App)
|
15
|
+
|
16
|
+
Setup a Rails initializer(`config/initializers/mantle.rb`):
|
17
|
+
|
18
|
+
|
19
|
+
```Ruby
|
20
|
+
require_relative '../../app/models/mantle_message_handler'
|
21
|
+
|
22
|
+
Mantle.configure do |config|
|
23
|
+
config.message_bus_channels = %w[account:update orders]
|
24
|
+
config.message_bus_redis = Redis.new(host: ENV["MESSAGE_BUS_REDIS_URL"] || 'localhost')
|
25
|
+
config.message_handler = MantleMessageHandler
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
The config takes a number of options, many of which have defaults:
|
30
|
+
|
31
|
+
```Ruby
|
32
|
+
Mantle.configure do |config|
|
33
|
+
config.message_bus_channels = ['deal:update', 'create:person'] # default: []
|
34
|
+
config.message_bus_redis = Redis.new(host: 'localhost') # default: localhost
|
35
|
+
config.message_handler = MyMessageHandler # requires implementation
|
36
|
+
config.logger = Rails.logger # default: Logger.new(STDOUT)
|
37
|
+
config.redis_namespace = "my-namespace" # default: no namespace
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
To make the installation of mantle easier, the following command will create
|
42
|
+
these files in a Rails application:
|
43
|
+
|
44
|
+
```
|
45
|
+
$ rails g mantle:install
|
46
|
+
```
|
47
|
+
|
48
|
+
If an application only pushes messages on to the queue and doesn't listen, the
|
49
|
+
following configuration is all that's needed:
|
50
|
+
|
51
|
+
```Ruby
|
52
|
+
Mantle.configure do |config|
|
53
|
+
config.message_bus_redis = Redis.new(host: 'localhost') # default: localhost
|
54
|
+
config.logger = Rails.logger # default: Logger.new(STDOUT)
|
55
|
+
end
|
56
|
+
```
|
57
|
+
|
58
|
+
|
59
|
+
Publish messages to consumers:
|
60
|
+
|
61
|
+
```Ruby
|
62
|
+
Mantle::Message.new("person:create").publish({ id: message['id'], data: message['data'] })
|
63
|
+
```
|
64
|
+
|
65
|
+
The first and only argument to `Mantle::Message.new` is the channel you want to publish the
|
66
|
+
message on. The `#publish` method takes the message payload (in any format you like)
|
67
|
+
and pushes the message on to the message bus pub/sub and also adds it to the
|
68
|
+
catch up queue so offline applications can process the message when they become available.
|
69
|
+
|
70
|
+
Define message handler class with `.receive` method. For example `app/models/my_message_handler.rb`
|
71
|
+
|
72
|
+
```Ruby
|
73
|
+
class MyMessageHandler
|
74
|
+
def self.receive(channel, message)
|
75
|
+
puts channel # => 'order'
|
76
|
+
puts message # => { 'id' => 5, 'name' => 'Brandon' }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
To run the listener:
|
82
|
+
|
83
|
+
```
|
84
|
+
$ bin/mantle
|
85
|
+
```
|
86
|
+
|
87
|
+
or with configuration:
|
88
|
+
|
89
|
+
```
|
90
|
+
$ bin/mantle -c ./config/initializers/other_file.rb
|
91
|
+
```
|
92
|
+
|
93
|
+
To run the processor:
|
94
|
+
|
95
|
+
```
|
96
|
+
$ bin/sidekiq -q mantle
|
97
|
+
```
|
98
|
+
|
99
|
+
If the Sidekiq worker should also listen on another queue, add that to the
|
100
|
+
command with:
|
101
|
+
|
102
|
+
```
|
103
|
+
$ bin/sidekiq -q mantle -q default
|
104
|
+
```
|
105
|
+
|
106
|
+
It will NOT add the `default` queue to processing if there are other queues
|
107
|
+
enumerated using the `-q` option.
|
data/Rakefile
ADDED
data/bin/mantle
ADDED
data/circle.yml
ADDED
data/config.ru
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Mantle
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("../templates", __FILE__)
|
5
|
+
|
6
|
+
desc <<desc
|
7
|
+
description:
|
8
|
+
copy mantle config to a Rails initializer and create default handler
|
9
|
+
desc
|
10
|
+
|
11
|
+
def create_configuration
|
12
|
+
template "mantle.rb", "config/initializers/mantle.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_handler
|
16
|
+
template "mantle_message_handler.rb", "app/models/mantle_message_handler.rb"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# require 'sidekiq-pro'
|
2
|
+
require_relative '../../app/models/mantle_message_handler'
|
3
|
+
|
4
|
+
Mantle.configure do |config|
|
5
|
+
config.message_bus_channels = %w[]
|
6
|
+
config.message_bus_redis = Redis.new(host: ENV["MESSAGE_BUS_REDIS_URL"] || 'localhost')
|
7
|
+
config.message_handler = MantleMessageHandler
|
8
|
+
end
|
9
|
+
|
data/lib/mantle.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'redis'
|
3
|
+
require 'sidekiq'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'pry'
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'mantle/catch_up'
|
12
|
+
require_relative 'mantle/configuration'
|
13
|
+
require_relative 'mantle/error'
|
14
|
+
require_relative 'mantle/local_redis'
|
15
|
+
require_relative 'mantle/message'
|
16
|
+
require_relative 'mantle/message_bus'
|
17
|
+
require_relative 'mantle/message_handler'
|
18
|
+
require_relative 'mantle/message_router'
|
19
|
+
require_relative 'mantle/workers/catch_up_cleanup_worker'
|
20
|
+
require_relative 'mantle/workers/process_worker'
|
21
|
+
require_relative 'mantle/version'
|
22
|
+
|
23
|
+
require_relative 'mantle/railtie' if defined?(Rails)
|
24
|
+
|
25
|
+
module Mantle
|
26
|
+
class << self
|
27
|
+
attr_accessor :configuration
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.configure
|
31
|
+
self.configuration ||= Configuration.new
|
32
|
+
yield(configuration) if block_given?
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.receive_message(channel, message)
|
36
|
+
Mantle.logger.debug("Message received on channel: #{channel}")
|
37
|
+
Mantle.logger.debug("Mantle message: #{message}")
|
38
|
+
|
39
|
+
self.configuration.message_handler.receive(channel, message)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.logger
|
43
|
+
configuration.logger
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Mantle
|
2
|
+
class CatchUp
|
3
|
+
KEY = "mantle:catch_up"
|
4
|
+
HOURS_TO_KEEP = 6
|
5
|
+
CLEANUP_EVERY_MINUTES = 5
|
6
|
+
|
7
|
+
attr_accessor :redis, :message_bus_channels
|
8
|
+
attr_reader :key
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@redis = Mantle.configuration.message_bus_redis
|
12
|
+
@message_bus_channels = Mantle.configuration.message_bus_channels
|
13
|
+
@key = KEY
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_message(channel, message, now = Time.now.utc.to_f)
|
17
|
+
json = serialize_payload(channel, message)
|
18
|
+
redis.zadd(key, now, json)
|
19
|
+
Mantle.logger.debug("Added message to catch up list for channel: #{channel}")
|
20
|
+
now
|
21
|
+
end
|
22
|
+
|
23
|
+
def enqueue_clear_if_ready
|
24
|
+
now = Time.now.utc.to_f
|
25
|
+
five_minutes_ago = now - (CLEANUP_EVERY_MINUTES * 60.0)
|
26
|
+
last_cleanup = Mantle::LocalRedis.last_catch_up_cleanup_at
|
27
|
+
|
28
|
+
if last_cleanup.nil? || last_cleanup < five_minutes_ago
|
29
|
+
Mantle::Workers::CatchUpCleanupWorker.perform_async
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear_expired
|
34
|
+
max_time_to_clear = hours_ago_in_seconds(HOURS_TO_KEEP)
|
35
|
+
redis.zremrangebyscore(key, 0 , max_time_to_clear)
|
36
|
+
end
|
37
|
+
|
38
|
+
def catch_up
|
39
|
+
raise Mantle::Error::MissingRedisConnection unless redis
|
40
|
+
|
41
|
+
if last_success_time.nil?
|
42
|
+
Mantle.logger.info("Skipping catch up because of missing last processed time...")
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
Mantle.logger.info("Catching up from time: #{last_success_time}")
|
47
|
+
|
48
|
+
payloads_with_time = redis.zrangebyscore(key, last_success_time, 'inf', with_scores: true)
|
49
|
+
route_messages(payloads_with_time) if payloads_with_time.any?
|
50
|
+
end
|
51
|
+
|
52
|
+
def last_success_time
|
53
|
+
LocalRedis.last_message_successfully_received_at
|
54
|
+
end
|
55
|
+
|
56
|
+
def route_messages(payloads_with_time)
|
57
|
+
payloads_with_time.each do |payload_with_time|
|
58
|
+
payload, time = payload_with_time
|
59
|
+
channel, message = deserialize_payload(payload)
|
60
|
+
|
61
|
+
if message_bus_channels.include?(channel)
|
62
|
+
Mantle::MessageRouter.new(channel, message).route
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def deserialize_payload(payload)
|
68
|
+
res = JSON.parse(payload)
|
69
|
+
[res.fetch("channel"), res.fetch("message")]
|
70
|
+
end
|
71
|
+
|
72
|
+
def hours_ago_in_seconds(hours)
|
73
|
+
hour_seconds = 60 * 60 * hours
|
74
|
+
Time.now.utc.to_f - hour_seconds
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def serialize_payload(channel, message)
|
80
|
+
payload = { channel: channel, message: message }
|
81
|
+
JSON.generate(payload)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/mantle/cli.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require 'mantle'
|
5
|
+
|
6
|
+
module Mantle
|
7
|
+
class CLI
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@options = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def setup(args = ARGV)
|
14
|
+
parse_options(args)
|
15
|
+
load_config
|
16
|
+
configure_sidekiq
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_options(args)
|
20
|
+
optparser = OptionParser.new do |opts|
|
21
|
+
opts.banner = "Usage: mantle <command> [options]"
|
22
|
+
|
23
|
+
opts.on("-c", "--config CONFIG_FILE",
|
24
|
+
"Path to configuration file (initializer)") do |arg|
|
25
|
+
options[:config] = arg
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
29
|
+
puts opts
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on_tail("--version", "Show version") do
|
34
|
+
puts ::Mantle::VERSION
|
35
|
+
exit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
optparser.parse!(args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def load_config
|
43
|
+
if options[:config]
|
44
|
+
require File.expand_path(options[:config])
|
45
|
+
else
|
46
|
+
require File.expand_path("./config/initializers/mantle")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def configure_sidekiq
|
51
|
+
if namespace = Mantle.configuration.redis_namespace
|
52
|
+
Mantle.logger.info("Configuring Mantle to listen on Redis namespace: #{namespace}")
|
53
|
+
|
54
|
+
Sidekiq.configure_client do |config|
|
55
|
+
config.redis = { url: ENV["REDIS_URL"], namespace: namespace }
|
56
|
+
end
|
57
|
+
|
58
|
+
Sidekiq.configure_server do |config|
|
59
|
+
config.redis = { url: ENV["REDIS_URL"], namespace: namespace }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def listen
|
65
|
+
Mantle::MessageBus.new.listen
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :options
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|