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