osbourne 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +674 -0
  3. data/README.md +106 -0
  4. data/Rakefile +30 -0
  5. data/bin/cli/base.rb +41 -0
  6. data/bin/osbourne +33 -0
  7. data/lib/generators/osbourne/install/install_generator.rb +14 -0
  8. data/lib/generators/osbourne/install/templates/osbourne_initializer_template.template +30 -0
  9. data/lib/generators/osbourne/install/templates/osbourne_yaml_template.template +15 -0
  10. data/lib/generators/osbourne/worker/USAGE +11 -0
  11. data/lib/generators/osbourne/worker/templates/worker_template.template +35 -0
  12. data/lib/generators/osbourne/worker/worker_generator.rb +20 -0
  13. data/lib/osbourne.rb +43 -0
  14. data/lib/osbourne/config/file_loader.rb +22 -0
  15. data/lib/osbourne/config/shared_configs.rb +37 -0
  16. data/lib/osbourne/existing_subscriptions.rb +40 -0
  17. data/lib/osbourne/launcher.rb +60 -0
  18. data/lib/osbourne/locks/base.rb +69 -0
  19. data/lib/osbourne/locks/memory.rb +69 -0
  20. data/lib/osbourne/locks/noop.rb +25 -0
  21. data/lib/osbourne/locks/redis.rb +56 -0
  22. data/lib/osbourne/message.rb +55 -0
  23. data/lib/osbourne/poller.rb +0 -0
  24. data/lib/osbourne/queue.rb +43 -0
  25. data/lib/osbourne/railtie.rb +20 -0
  26. data/lib/osbourne/runner.rb +86 -0
  27. data/lib/osbourne/services/queue_provisioner.rb +14 -0
  28. data/lib/osbourne/services/sns.rb +17 -0
  29. data/lib/osbourne/services/sqs.rb +17 -0
  30. data/lib/osbourne/subscription.rb +36 -0
  31. data/lib/osbourne/topic.rb +34 -0
  32. data/lib/osbourne/version.rb +5 -0
  33. data/lib/osbourne/worker_base.rb +91 -0
  34. data/lib/tasks/message_plumber_tasks.rake +6 -0
  35. metadata +348 -0
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ [![Maintainability](https://api.codeclimate.com/v1/badges/295897ee565c04ad1aa5/maintainability)](https://codeclimate.com/github/stevenallen05/osbourne/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/295897ee565c04ad1aa5/test_coverage)](https://codeclimate.com/github/stevenallen05/osbourne/test_coverage)
2
+
3
+ # Osbourne
4
+
5
+ A fan-out pubsub message implementation for Rails 5. Named after the world's most famous plumber, Ozzy Osbourne.
6
+
7
+ ### Features
8
+
9
+ * Publish messages via `Osbourne.publish("topic_name", message)`. Messages are expected to either be a `String` or respond to `#to_json`
10
+ * Message processor via the `osbourne` shell command
11
+ * Worker generator via `rails g osbourne:worker worker_name topic`
12
+ * Auto-provisioning of SQS queues, SNS topics, and subscriptions between them
13
+ * Built-in support for locking to prevent accidental duplicate message delivery
14
+
15
+ Inspired heavily by the excellent Shoryuken & Circuitry gems
16
+
17
+ ## Installation
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'osbourne'
22
+ ```
23
+
24
+ And then execute:
25
+ ```bash
26
+ $ bundle install
27
+ $ bundle exec rails g osbourne:install
28
+ ```
29
+
30
+ Installation creates `config/initializers/osbourne.rb` and `config/osbourne.yml`
31
+
32
+ ### AWS credentials
33
+
34
+ There are a few ways to configure the AWS client:
35
+
36
+ * Ensure the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` env vars are set.
37
+ * Create a `~/.aws/credentials` file.
38
+ * Set `Aws.config[:credentials]` or `Osbourne.sqs_client = Aws::SQS::Client.new(...)` and `Osbourne.sns_client = Aws::SNS::Client.new(...)` from Ruby code
39
+ * Use the Instance Profiles feature. The IAM role of the targeted machine must have an adequate SQS Policy.
40
+
41
+ You can read about these in more detail [here](http://docs.aws.amazon.com/sdkforruby/api/Aws/SQS/Client.html).
42
+
43
+ ### Production AWS
44
+
45
+ The default `config/osbourne.yml` production configuration is blank. This is not by accident. Hitting real AWS resources requires no additional configuration.
46
+
47
+ ### Mock AWS for local devlopment
48
+
49
+ This gem has been tested successfully with [localstack](https://github.com/localstack/localstack). The `localstack` tools can succesfully emulate SQS & SNS, as well as subscription marshalling.
50
+
51
+ The following settings in `config/osbourne.yml` will configure Osbourne to use a mocked SQS & SNS on `localhost`:
52
+
53
+ ```yaml
54
+ development:
55
+ publisher:
56
+ endpoint: http://localhost:4575
57
+ subscriber:
58
+ endpoint: http://localhost:4576
59
+ verify_checksums: false
60
+ ```
61
+
62
+ Please note `verify_checksums: false` is required for the `subscriber` configuration.
63
+
64
+ It is recommended to use the `dotenv` gem and add these dummy credentials to your `.env`:
65
+
66
+ ```
67
+ AWS_ACCESS_KEY_ID=AKXXXXXXXXXXXXXXXXXXX
68
+ AWS_SECRET_ACCESS_KEY=YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
69
+ AWS_REGION=us-east-1
70
+ ```
71
+
72
+ There must be a region set. Use whichever is appropriate for your situation.
73
+
74
+ ## Usage
75
+
76
+ ### Publishing a message
77
+
78
+ Publishing a message is simple: `Osbourne.publish("topic_name", message)`
79
+
80
+ Osbourne will automatically provision the SNS topic if it does not already exist. The `message` object is expected to either be a `String` object, or respond to `#to_json`.
81
+
82
+ ### Generating a worker
83
+
84
+ ```bash
85
+ $ bundle exec rails g osbourne:worker worker_name topic1 topic2
86
+ ```
87
+
88
+ This will generate a `WorkerNameWorker` in `app/workers/worker_name_worker.rb`, subscribed to `topic1` and `topic2`.
89
+
90
+ There is some configuration options available within the generated worker. See comments in the worker for options.
91
+
92
+ SNS messages broadcast through an SQS queue will have some layers of envelop wrappings around them. The `message` object passed into the worker's `#perform` method contains some helpers to make parsing this easier. `#parsed_body` is the most important one, as it contains the actual string of the message that was originally broadcast.
93
+
94
+ ### Running workers
95
+
96
+ To run all workers in `app/workers`:
97
+
98
+ ```bash
99
+ $ bundle exec osbourne
100
+ ```
101
+
102
+ This can be easily added to a [Procman](https://github.com/adamcooke/procman) configuration to run alongside your ActiveJob processor (eg: sidekiq)
103
+
104
+
105
+ ## License
106
+ The gem is available as open source under the terms of the [GPL-3.0 License](https://opensource.org/licenses/GPL-3.0).
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "bundler/setup"
5
+ rescue LoadError
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
7
+ end
8
+
9
+ require "rdoc/task"
10
+
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Osbourne"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
17
+ end
18
+
19
+ require "bundler/gem_tasks"
20
+
21
+ require "rake/testtask"
22
+
23
+ Rake::TestTask.new(:test) do |t|
24
+ t.libs << "lib"
25
+ t.libs << "test"
26
+ t.pattern = "test/**/*_test.rb"
27
+ t.verbose = false
28
+ end
29
+
30
+ task default: :test
data/bin/cli/base.rb ADDED
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osbourne
4
+ module CLI
5
+ class Base < Thor
6
+ no_commands do
7
+ def print_table(entries)
8
+ column_sizes = print_columns_size(entries)
9
+
10
+ entries.map do |entry|
11
+ puts entry.map.with_index {|e, i| print_format_column(e, column_sizes[i]) }.join
12
+ end
13
+ end
14
+
15
+ def print_columns_size(entries)
16
+ column_sizes = Hash.new(0)
17
+
18
+ entries.each do |entry|
19
+ entry.each_with_index do |e, i|
20
+ e = e.to_s
21
+ column_sizes[i] = e.size if column_sizes[i] < e.size
22
+ end
23
+ end
24
+
25
+ column_sizes
26
+ end
27
+
28
+ def print_format_column(column, size)
29
+ size_with_padding = size + 4
30
+ column = column.to_s.ljust(size_with_padding)
31
+ column
32
+ end
33
+
34
+ def fail_task(msg, quit=true)
35
+ say "[FAIL] #{msg}", :red
36
+ exit(1) if quit
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
data/bin/osbourne ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ require 'thor'
6
+
7
+ require 'rails'
8
+ require File.expand_path('config/environment.rb')
9
+
10
+ require_relative 'cli/base'
11
+ require_relative '../lib/osbourne/runner'
12
+
13
+ module Osbourne
14
+ module CLI
15
+ class Runner < Thor
16
+ default_task :start
17
+ desc 'start', 'Starts Osbourne'
18
+
19
+ def start
20
+ puts "Launching Osbourne workers"
21
+ Osbourne::Runner.instance.run
22
+ end
23
+
24
+ desc 'version', 'Prints version'
25
+ def version
26
+ say "Osbourne #{Osbourne::VERSION}"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+
33
+ Osbourne::CLI::Runner.start
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Osbourne
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def generate_install
10
+ template "osbourne_yaml_template.template", "config/osbourne.yml"
11
+ template "osbourne_initializer_template.template", "config/initializers/osbourne.rb"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.expand_path("app/workers/**/*.rb")].each {|f| require f }
4
+
5
+ Osbourne.configure do |config|
6
+ # Defaults to a null cache. Uncomment to use the Rails cache,
7
+ # or substitute any rails-cache compatible adapter of your choice
8
+ # config.cache = Rails.cache
9
+
10
+ # Dead letter queues are used to store messages that fail processing for some reason
11
+ # They are enabled by default
12
+ # config.dead_letter = true
13
+
14
+ # A message will be attempted `max_retry_count` times before being
15
+ # sent to the dead letter queue
16
+ # config.max_retry_count = 5
17
+
18
+ # Amount of time each worker will wait between attempting to fetch messages
19
+ # config.sleep_time = 15.seconds
20
+
21
+ config.logger = Rails.logger
22
+
23
+ # The lock strategy to be used
24
+ # Supported lock strategies:
25
+ # * Osbourne::Locks::Redis - requires a redis client or redis config
26
+ # * Osbourne::Locks::NOOP - the default. No locking. Bad for production
27
+ # * Osbourne::Locks::Memory - uses pure memory
28
+ # config.lock = Osbourne::Locks::Redis.new(url: 'redis://localhost:6379')
29
+
30
+ end
@@ -0,0 +1,15 @@
1
+ development:
2
+ publisher:
3
+ endpoint: http://localhost:4575
4
+ subscriber:
5
+ endpoint: http://localhost:4576
6
+ verify_checksums: false
7
+
8
+ test:
9
+ publisher:
10
+ endpoint: http://localhost:4575
11
+ subscriber:
12
+ endpoint: http://localhost:4576
13
+ verify_checksums: false
14
+
15
+ production:
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Generates a fan-out worker that ensures the given queues, topics, and subscriptions are instantiated
3
+
4
+ Example:
5
+ rails generate queue_worker WorkerName topic_1 <other_topics>
6
+
7
+ This will create:
8
+ app/workers/<name>_worker.rb
9
+
10
+ `topic_1` will be the message topic that the queue will listen to. Multiple topics can listed
11
+
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= name.singularize.camelcase %>Worker < Osbourne::WorkerBase
4
+
5
+ worker_config topics: %w[<%= topic.join(" ") %>] #, max_batch_size: 10, max_wait: 10
6
+
7
+ def process(message)
8
+ puts message.raw_body # This will always be a string, representing the entirety of the message
9
+ puts message.parsed_body # If the message came from a SNS broadcast,
10
+ # as opposed to a direct SQS message, it will come from here
11
+ puts message.topic # The topic this message was published to.
12
+ # Useful if this worker subscribes to more than one topic
13
+ puts message.id # The UUID for this message. Useful for validating if this is
14
+ # the first time it has been processed
15
+ end
16
+
17
+ # class << self
18
+ # # override this to set how many times a message will be retried before
19
+ # # being redirected to the dead letter queue (if enabled)
20
+ # def max_retry_count
21
+ # end
22
+
23
+ # # override this to set the worker's dead letter queue name
24
+ # def dead_letter_queue_name
25
+ # end
26
+
27
+ # # override this to `false` to disable the dead letter queu for this worker
28
+ # def dead_letter_queue
29
+ # end
30
+
31
+ # # override this to set the worker queue name
32
+ # def queue_name
33
+ # end
34
+ # end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Osbourne
6
+ class WorkerGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path("templates", __dir__)
8
+ argument :topic, type: :array
9
+
10
+ def generate_worker
11
+ template "worker_template.template", "app/workers/#{name.underscore}_worker.rb"
12
+ end
13
+
14
+ private
15
+
16
+ def config_file
17
+ Rails.root.join("config", "queue", "queue_workers.yml")
18
+ end
19
+ end
20
+ end
data/lib/osbourne.rb ADDED
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "osbourne/railtie"
4
+ require "osbourne/services/sns"
5
+ require "osbourne/services/sqs"
6
+ require "osbourne/topic"
7
+ require "osbourne/queue"
8
+ require "osbourne/subscription"
9
+ require "osbourne/config/shared_configs"
10
+ require "osbourne/worker_base"
11
+ require "osbourne/services/queue_provisioner"
12
+ require "osbourne/launcher"
13
+ require "osbourne/message"
14
+ require "osbourne/existing_subscriptions"
15
+ require "osbourne/locks/base"
16
+ require "osbourne/locks/noop"
17
+ require "osbourne/locks/memory"
18
+ require "osbourne/locks/redis"
19
+
20
+ module Osbourne
21
+ class << self
22
+ include Osbourne::Config::SharedConfigs
23
+ include Osbourne::Services::QueueProvisioner
24
+ include Osbourne::ExistingSubscriptions
25
+ attr_writer :sns_client, :sqs_client
26
+
27
+ def sns_client
28
+ @sns_client ||= Aws::SNS::Client.new(Osbourne.config.sns_config)
29
+ end
30
+
31
+ def sqs_client
32
+ @sqs_client ||= Aws::SQS::Client.new(Osbourne.config.sqs_config)
33
+ end
34
+
35
+ def publish(topic, message)
36
+ Topic.new(topic).publish(message)
37
+ end
38
+
39
+ def configure
40
+ yield config
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "erb"
5
+ require "fileutils"
6
+
7
+ module Osbourne
8
+ module Config
9
+ module FileLoader
10
+ def self.load(cfile, environment="development")
11
+ return nil unless File.exist?(cfile)
12
+
13
+ base_opts = YAML.safe_load(ERB.new(IO.read(cfile)).result) || {}
14
+ env_opts = base_opts[environment] || {}
15
+
16
+ Osbourne.config.sns_config = env_opts["publisher"].symbolize_keys || {}
17
+ Osbourne.config.sqs_config = env_opts["subscriber"].symbolize_keys || {}
18
+ true
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Osbourne
4
+ module Config
5
+ module SharedConfigs
6
+ attr_writer :sqs_config, :sns_config, :aws_credentials, :config
7
+
8
+ def config
9
+ @config ||= ActiveSupport::OrderedOptions.new
10
+ end
11
+
12
+ def cache
13
+ config.cache ||= ActiveSupport::Cache::NullStore.new
14
+ end
15
+
16
+ def dead_letter
17
+ config.dead_letter ||= true
18
+ end
19
+
20
+ def max_retry_count
21
+ @max_retry_count ||= (config.max_retry_count.presence || 5).to_s
22
+ end
23
+
24
+ def logger
25
+ config.logger ||= Logger.new(STDOUT)
26
+ end
27
+
28
+ def lock
29
+ config.lock || Osbourne::Locks::NOOP.new
30
+ end
31
+
32
+ def sleep_time
33
+ config.sleep_time || 15
34
+ end
35
+ end
36
+ end
37
+ end