osbourne 0.1.3
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/LICENSE +674 -0
- data/README.md +106 -0
- data/Rakefile +30 -0
- data/bin/cli/base.rb +41 -0
- data/bin/osbourne +33 -0
- data/lib/generators/osbourne/install/install_generator.rb +14 -0
- data/lib/generators/osbourne/install/templates/osbourne_initializer_template.template +30 -0
- data/lib/generators/osbourne/install/templates/osbourne_yaml_template.template +15 -0
- data/lib/generators/osbourne/worker/USAGE +11 -0
- data/lib/generators/osbourne/worker/templates/worker_template.template +35 -0
- data/lib/generators/osbourne/worker/worker_generator.rb +20 -0
- data/lib/osbourne.rb +43 -0
- data/lib/osbourne/config/file_loader.rb +22 -0
- data/lib/osbourne/config/shared_configs.rb +37 -0
- data/lib/osbourne/existing_subscriptions.rb +40 -0
- data/lib/osbourne/launcher.rb +60 -0
- data/lib/osbourne/locks/base.rb +69 -0
- data/lib/osbourne/locks/memory.rb +69 -0
- data/lib/osbourne/locks/noop.rb +25 -0
- data/lib/osbourne/locks/redis.rb +56 -0
- data/lib/osbourne/message.rb +55 -0
- data/lib/osbourne/poller.rb +0 -0
- data/lib/osbourne/queue.rb +43 -0
- data/lib/osbourne/railtie.rb +20 -0
- data/lib/osbourne/runner.rb +86 -0
- data/lib/osbourne/services/queue_provisioner.rb +14 -0
- data/lib/osbourne/services/sns.rb +17 -0
- data/lib/osbourne/services/sqs.rb +17 -0
- data/lib/osbourne/subscription.rb +36 -0
- data/lib/osbourne/topic.rb +34 -0
- data/lib/osbourne/version.rb +5 -0
- data/lib/osbourne/worker_base.rb +91 -0
- data/lib/tasks/message_plumber_tasks.rake +6 -0
- metadata +348 -0
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
[](https://codeclimate.com/github/stevenallen05/osbourne/maintainability) [](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
|