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
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
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :console do
4
+ exec "irb -r mantle -I ./lib"
5
+ end
data/bin/mantle ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'celluloid'
4
+ require 'sidekiq/cli'
5
+ require 'sidekiq/processor'
6
+
7
+ require_relative '../lib/mantle/cli'
8
+
9
+ cli = Mantle::CLI.new
10
+ cli.setup
11
+
12
+ trap('SIGINT') { exit! }
13
+
14
+ cli.listen
data/circle.yml ADDED
@@ -0,0 +1,3 @@
1
+ machine:
2
+ ruby:
3
+ version: '2.2'
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require_relative './lib/mantle'
2
+ require 'sidekiq'
3
+
4
+ Mantle.boot_system!
5
+
6
+ require 'sidekiq/web'
7
+ run Sidekiq::Web
@@ -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
+
@@ -0,0 +1,7 @@
1
+ class MantleMessageHandler
2
+ def self.receive(channel, message)
3
+ puts channel # => 'order'
4
+ puts message # => { 'id' => 5, 'name' => 'Brandon' }
5
+ end
6
+ end
7
+
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
+