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