osbourne 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|