├── .document ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── lib ├── rubykiq.rb └── rubykiq │ ├── client.rb │ ├── connection.rb │ └── version.rb ├── rubykiq.gemspec └── spec ├── spec_helper.rb ├── support ├── codeclimate.rb ├── pry.rb └── timecop.rb └── unit ├── client_spec.rb ├── connection_spec.rb └── rubykiq_spec.rb /.document: -------------------------------------------------------------------------------- 1 | LICENSE.md 2 | README.md 3 | lib/**/*.rb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .devnotes 6 | .greenbar 7 | .yardoc 8 | Gemfile.lock 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | bin -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Includes: 3 | - Rakefile 4 | - Gemfile 5 | Excludes: 6 | - script/** 7 | - vendor/** 8 | - bin/** 9 | LineLength: 10 | Enabled: false 11 | MethodLength: 12 | Enabled: false 13 | ClassLength: 14 | Enabled: false 15 | Documentation: 16 | Enabled: false 17 | Encoding: 18 | Enabled: false 19 | Blocks: 20 | Enabled: false 21 | AlignParameters: 22 | Enabled: false 23 | HashSyntax: 24 | EnforcedStyle: ruby19 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | bundler_args: --without development 4 | rvm: 5 | - ruby-head 6 | - ruby 7 | - jruby-head 8 | - jruby 9 | - 2.1.0 10 | - 2.0.0 11 | - 1.9.3 12 | - rbx-2 13 | matrix: 14 | fast_finish: true 15 | allow_failures: 16 | - rvm: ruby-head 17 | - rvm: ruby 18 | - rvm: jruby-head 19 | - rvm: jruby 20 | - rvm: rbx-2 21 | notifications: 22 | email: false 23 | services: 24 | - redis-server 25 | env: 26 | - CODECLIMATE_REPO_TOKEN=04a9fef09063cdef2f62cf46531329ad01a9383e3de8270e112480052774f676 -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | - 3 | CHANGELOG.md 4 | CONTRIBUTING.md 5 | LICENSE.md 6 | README.md -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Next Release 2 | ============ 3 | * Your contribution here. 4 | 5 | 1.0.2 (02/08/2015) 6 | ================== 7 | * [Include `enqueued_at` in payload](https://github.com/karlfreeman/rubykiq/pull/2) - [@amiel](https://github.com/amiel). 8 | 9 | 1.0.1 (18/03/2014) 10 | ================== 11 | * [Ensure loading a client is Thread.exclusive](https://github.com/karlfreeman/rubykiq/commit/0a68c9dc670f94efe8a344869db0b7ba4a97d1d7) - [@karlfreeman](https://github.com/karlfreeman). 12 | 13 | 1.0.0 (03/03/2014) 14 | ================== 15 | * Initial public release - [@karlfreeman](https://github.com/karlfreeman). 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | In the spirit of [free software][free-sw], **everyone** is encouraged to help 3 | improve this project. 4 | 5 | [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html 6 | 7 | Here are some ways *you* can contribute: 8 | 9 | * by using alpha, beta, and prerelease versions 10 | * by reporting bugs 11 | * by suggesting new features 12 | * by writing or editing documentation 13 | * by writing specifications 14 | * by writing code (**no patch is too small**: fix typos, add comments, clean up 15 | inconsistent whitespace) 16 | * by refactoring code 17 | * by closing [issues][] 18 | * by reviewing patches 19 | 20 | [issues]: https://github.com/karlfreeman/rubykiq/issues 21 | 22 | ## Submitting an Issue 23 | We use the [GitHub issue tracker][issues] to track bugs and features. Before 24 | submitting a bug report or feature request, check to make sure it hasn't 25 | already been submitted. When submitting a bug report, please include a [Gist][] 26 | that includes a stack trace and any details that may be necessary to reproduce 27 | the bug, including your gem version, Ruby version, and operating system. 28 | Ideally, a bug report should include a pull request with failing specs. 29 | 30 | [gist]: https://gist.github.com/ 31 | 32 | ## Submitting a Pull Request 33 | 1. [Fork the repository.][fork] 34 | 2. [Create a topic branch.][branch] 35 | 3. Add specs for your unimplemented feature or bug fix. 36 | 4. Run `bundle exec rake spec`. If your specs pass, return to step 3. 37 | 5. Implement your feature or bug fix. 38 | 6. Run `bundle exec rake spec`. If your specs fail, return to step 5. 39 | 7. Add, commit, and push your changes. 40 | 8. [Submit a pull request.][pr] 41 | 42 | [fork]: http://help.github.com/fork-a-repo/ 43 | [branch]: http://learn.github.com/p/branching.html 44 | [pr]: http://help.github.com/send-pull-requests/ 45 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'hiredis', '>= 0.4.5' 6 | gem 'em-synchrony' 7 | 8 | group :test do 9 | gem 'rake', '~> 10.0' 10 | gem 'rspec' 11 | gem 'rspec-its' 12 | gem 'timecop' 13 | gem 'codeclimate-test-reporter' 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Karl Freeman 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rubykiq 2 | 3 | [Sidekiq](http://mperham.github.com/sidekiq) agnostic enqueuing using Redis. 4 | 5 | Sidekiq is a fantastic message processing library which has a simple and stable message format. `Rubykiq` aims to be a portable library to push jobs in to Sidekiq with as little overhead as possible whilst having feature parity on `Sidekiq::Client`'s conventions. 6 | 7 | ## Installation 8 | 9 | ```ruby 10 | gem 'rubykiq', '~> 1.0' 11 | ``` 12 | 13 | ```ruby 14 | require 'rubykiq' 15 | ``` 16 | ## Features / Usage Examples 17 | 18 | * [Redis](http://redis.io) has support for [alternative drivers](https://github.com/redis/redis-rb#alternate-drivers), Rubykiq is tested with these in mind. (eg `:synchrony`) 19 | * the `:class` parameter can be a `Class` or a `String` of a Class (eg push jobs to Sidekiq from anywhere, not just where you have Sidekiq classes loaded) 20 | * The `:at` parameter supports `Time`, `Date` and any `Time.parse`-able strings. 21 | * Pushing multiple and singular jobs have the same interface (simply nest args) 22 | * Slightly less gem dependecies, and by that I mean `Sidekiq::Client` without `Celluloid` (which is already very light!) 23 | * Easier configuration (IMO) 24 | 25 | ```ruby 26 | # will also detect REDIS_URL, REDIS_PROVIDER and REDISTOGO_URL ENV variables 27 | Rubykiq.url = 'redis://127.0.0.1:6379' 28 | 29 | # alternative driver support ( :ruby, :hiredis, :synchrony ) 30 | Rubykiq.driver = :synchrony 31 | 32 | # defaults to nil 33 | Rubykiq.namespace = 'background' 34 | 35 | # uses 'default' queue unless specified 36 | Rubykiq.push(class: 'Worker', args: ['foo', 1, bat: 'bar']) 37 | 38 | # args are optionally set to empty 39 | Rubykiq.push(class: 'Scheduler', queue: 'scheduler') 40 | 41 | # will batch up multiple jobs 42 | Rubykiq.push(class: 'Worker', args: [['foo'], ['bar']]) 43 | 44 | # at param can be a 'Time', 'Date' or any 'Time.parse'-able strings 45 | Rubykiq.push(class: 'DelayedHourMailer', at: Time.now + 3600) 46 | Rubykiq.push(class: 'DelayedDayMailer', at: DateTime.now.next_day) 47 | Rubykiq.push(class: 'DelayedMailer', at: '2013-01-01T09:00:00Z') 48 | 49 | # alias based sugar 50 | job = { class: 'Worker' } 51 | Rubykiq << job 52 | 53 | # create multiple Rubykiq clients with their own drivers 54 | ruby_client = Rubykiq::Client.new 55 | hiredis_client = Rubykiq::Client.new(driver: :hiredis) 56 | 57 | # create multiple Rubykiq clients with their own namespaces 58 | foo_client = Rubykiq::Client.new(namespace: 'foo') 59 | bar_client = Rubykiq::Client.new(namespace: 'bar') 60 | ``` 61 | 62 | ## Caveats 63 | 64 | * It's advised that using [Sidekiq::Client's push](https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/client.rb#L36) method when already a dependency is better in most everyday cases 65 | * If you rely on any [Sidekiq Middleware](https://github.com/mperham/sidekiq/wiki/middleware), Rubykiq is not aware of them so defaults will not be applied to the job hash. 66 | 67 | ## Build & Dependency Status 68 | 69 | [![Gem Version](http://img.shields.io/gem/v/rubykiq.svg)][gem] 70 | [![Build Status](http://img.shields.io/travis/karlfreeman/rubykiq.svg)][travis] 71 | [![Code Quality](http://img.shields.io/codeclimate/github/karlfreeman/rubykiq.svg)][codeclimate] 72 | [![Code Coverage](http://img.shields.io/codeclimate/coverage/github/karlfreeman/rubykiq.svg)][codeclimate] 73 | [![Gittip](http://img.shields.io/gittip/karlfreeman.svg)][gittip] 74 | 75 | ## Supported Redis Drivers 76 | 77 | * [Ruby](https://github.com/redis/redis-rb#alternate-drivers) 78 | * [Hiredis](https://github.com/redis/hiredis) 79 | * [Synchrony](https://github.com/igrigorik/em-synchrony) 80 | 81 | ## Supported Ruby Versions 82 | 83 | This library aims to support and is [tested against][travis] the following Ruby 84 | implementations: 85 | 86 | - Ruby 2.1.0 (drivers: ruby, hiredis, synchrony) 87 | - Ruby 2.0.0 (drivers: ruby, hiredis, synchrony) 88 | - Ruby 1.9.3 (drivers: ruby, hiredis, synchrony) 89 | - [JRuby][jruby] (drivers: ruby) 90 | - [Rubinius][rubinius] (drivers: ruby) 91 | 92 | # Credits 93 | 94 | Inspiration: 95 | 96 | - [Michael Grosser's Enqueue into Sidkiq post](http://grosser.it/2013/01/17/enqueue-into-sidekiq-via-pure-redis-without-loading-sidekiq) 97 | 98 | Cribbed: 99 | 100 | - [Sidekiq's internal client class](https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/client.rb) 101 | - [Sidekiq's internal redis class](https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/redis_connection.rb) 102 | - [Sidekiq's FAQ](https://github.com/mperham/sidekiq/wiki/FAQ) 103 | 104 | [gem]: https://rubygems.org/gems/rubykiq 105 | [travis]: http://travis-ci.org/karlfreeman/rubykiq 106 | [codeclimate]: https://codeclimate.com/github/karlfreeman/rubykiq 107 | [gittip]: https://www.gittip.com/karlfreeman 108 | [jruby]: http://www.jruby.org 109 | [rubinius]: http://rubini.us 110 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup 3 | Bundler::GemHelper.install_tasks 4 | 5 | require 'rspec/core/rake_task' 6 | desc 'Run all examples' 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | begin 10 | require 'yard' 11 | YARD::Rake::YardocTask.new 12 | rescue LoadError 13 | end 14 | 15 | begin 16 | require 'rubocop/rake_task' 17 | desc 'Run rubocop' 18 | RuboCop::RakeTask.new(:rubocop) 19 | rescue LoadError 20 | end 21 | 22 | task default: :spec 23 | task test: :spec 24 | -------------------------------------------------------------------------------- /lib/rubykiq.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'forwardable' 3 | require 'rubykiq/client' 4 | require 'rubykiq/connection' 5 | 6 | module Rubykiq 7 | extend SingleForwardable 8 | 9 | def_delegators :client, :<<, :push, :connection_pool, :connection_pool= 10 | 11 | # delegate all VALID_OPTIONS_KEYS accessors to the client 12 | def_delegators :client, *Rubykiq::Client::VALID_OPTIONS_KEYS 13 | 14 | # delegate all VALID_OPTIONS_KEYS setters to the client ( hacky I know... ) 15 | def_delegators :client, *(Rubykiq::Client::VALID_OPTIONS_KEYS.dup.map! { |key| "#{key}=".to_sym; }) 16 | 17 | # Fetch the Rubykiq::Client 18 | # 19 | # @return [Rubykiq::Client] 20 | def self.client(options = {}) 21 | initialize_client(options) unless defined?(@client) 22 | @client 23 | end 24 | 25 | private 26 | 27 | def self.initialize_client(options = {}) 28 | Thread.exclusive do 29 | @client = Rubykiq::Client.new(options) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/rubykiq/client.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | require 'thread' 3 | require 'multi_json' 4 | require 'securerandom' 5 | require 'connection_pool' 6 | 7 | module Rubykiq 8 | class Client 9 | # An array of valid keys in the options hash when configuring a `Rubykiq::Client` 10 | VALID_OPTIONS_KEYS = [ 11 | :redis_pool_size, 12 | :redis_pool_timeout, 13 | :url, 14 | :namespace, 15 | :driver, 16 | :retry, 17 | :queue 18 | ] 19 | 20 | # A hash of valid options and their default values 21 | DEFAULT_OPTIONS = { 22 | redis_pool_size: 1, 23 | redis_pool_timeout: 1, 24 | url: nil, 25 | namespace: nil, 26 | driver: :ruby, 27 | retry: true, 28 | queue: 'default' 29 | } 30 | 31 | # Bang open the valid options 32 | attr_accessor(*VALID_OPTIONS_KEYS) 33 | 34 | # allow the connection_pool to be set 35 | attr_writer :connection_pool 36 | 37 | # Initialize a new Client object 38 | # 39 | # @param options [Hash] 40 | def initialize(options = {}) 41 | reset_options 42 | options.each_pair do |key, value| 43 | send("#{key}=", value) if VALID_OPTIONS_KEYS.include?(key) 44 | end 45 | end 46 | 47 | # Fetch the ::ConnectionPool of Rubykiq::Connections 48 | # 49 | # @return [::ConnectionPool] 50 | def connection_pool(options = {}, &block) 51 | options = valid_options.merge(options) 52 | initialize_connection_pool(options) unless defined?(@connection_pool) 53 | 54 | if block_given? 55 | @connection_pool.with(&block) 56 | else 57 | @connection_pool 58 | end 59 | end 60 | 61 | # Push a Sidekiq job to Redis. Accepts a number of options: 62 | # 63 | # :class - the worker class to call, required. 64 | # :queue - the named queue to use, optional ( default: 'default' ) 65 | # :args - an array of simple arguments to the perform method, must be JSON-serializable, optional ( default: [] ) 66 | # :retry - whether to retry this job if it fails, true or false, default true, optional ( default: true ) 67 | # :at - when the job should be executed. This can be a `Time`, `Date` or any `Time.parse`-able strings, optional. 68 | # 69 | # Returns nil if not pushed to Redis. In the case of an indvidual job a job ID will be returned, 70 | # if multiple jobs are pushed the size of the jobs will be returned 71 | # 72 | # Example: 73 | # Rubykiq.push(:class => 'Worker', :args => ['foo', 1, :bat => 'bar']) 74 | # Rubykiq.push(:class => 'Scheduler', :queue => 'scheduler') 75 | # Rubykiq.push(:class => 'DelayedMailer', :at => '2013-01-01T09:00:00Z') 76 | # Rubykiq.push(:class => 'Worker', :args => [['foo'], ['bar']]) 77 | # 78 | # @param items [Array] 79 | def push(items) 80 | fail(ArgumentError, 'Message must be a Hash') unless items.is_a?(Hash) 81 | fail(ArgumentError, 'Message args must be an Array') if items[:args] && !items[:args].is_a?(Array) 82 | 83 | # args are optional 84 | items[:args] ||= [] 85 | 86 | # determine if this items arg's is a nested array 87 | items[:args].first.is_a?(Array) ? push_many(items) : push_one(items) 88 | end 89 | alias_method :<<, :push 90 | 91 | private 92 | 93 | # Create a hash of options and their values 94 | def valid_options 95 | VALID_OPTIONS_KEYS.reduce({}) { |a, e| a.merge!(e => send(e)) } 96 | end 97 | 98 | # Create a hash of the default options and their values 99 | def default_options 100 | DEFAULT_OPTIONS 101 | end 102 | 103 | # Set the VALID_OPTIONS_KEYS with their DEFAULT_OPTIONS 104 | def reset_options 105 | VALID_OPTIONS_KEYS.each do |key| 106 | send("#{key}=", default_options[key]) 107 | end 108 | end 109 | 110 | # when only one item is needed to persisted to redis 111 | def push_one(item) 112 | # we're expecting item to be a single item so simply normalize it 113 | payload = normalize_item(item) 114 | 115 | # if successfully persisted to redis return this item's `jid` 116 | pushed = false 117 | pushed = raw_push([payload]) if payload 118 | pushed ? payload[:jid] : nil 119 | end 120 | 121 | # when multiple item's are needing to be persisted to redis 122 | def push_many(items) 123 | # we're expecting items to have an nested array of args, lets take each one and correctly normalize them 124 | payloads = items[:args].map do |args| 125 | fail ArgumentError, "Bulk arguments must be an Array of Arrays: [[:foo => 'bar'], [:foo => 'foo']]" unless args.is_a?(Array) 126 | # clone the original items (for :queue, :class, etc..) 127 | item = items.clone 128 | # merge this item's args (eg the nested `arg` array) 129 | item.merge!(args: args) unless args.empty? 130 | # normalize this individual item 131 | normalize_item(item) 132 | end.compact 133 | 134 | # if successfully persisted to redis return the size of the jobs 135 | pushed = false 136 | pushed = raw_push(payloads) unless payloads.empty? 137 | pushed ? payloads.size : nil 138 | end 139 | 140 | # persist the job message(s) 141 | def raw_push(payloads) 142 | pushed = false 143 | connection_pool do |connection| 144 | if payloads.first[:at] 145 | pushed = connection.zadd('schedule', payloads.map { |item| [item[:at].to_s, ::MultiJson.encode(item)] }) 146 | else 147 | q = payloads.first[:queue] 148 | to_push = payloads.map { |item| ::MultiJson.encode(item) } 149 | _, pushed = connection.multi do 150 | connection.sadd('queues', q) 151 | connection.lpush("queue:#{q}", to_push) 152 | end 153 | end 154 | end 155 | pushed 156 | end 157 | 158 | def normalize_item(item) 159 | fail(ArgumentError, 'Message must be a Hash') unless item.is_a?(Hash) 160 | fail(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item[:class] || !item[:args] 161 | fail(ArgumentError, 'Message args must be an Array') if item[:args] && !item[:args].is_a?(Array) 162 | fail(ArgumentError, 'Message class must be a String representation of the class name') unless item[:class].is_a?(String) 163 | 164 | # normalize the time 165 | item[:at] = normalize_time(item[:at]) if item[:at] 166 | pre_normalized_item = item.clone 167 | 168 | # args are optional 169 | pre_normalized_item[:args] ||= [] 170 | 171 | # apply the default options 172 | [:retry, :queue].each do |key| 173 | pre_normalized_item[key] = send("#{key}") unless pre_normalized_item.key?(key) 174 | end 175 | 176 | # provide a job ID 177 | pre_normalized_item[:jid] = ::SecureRandom.hex(12) 178 | 179 | # Sidekiq::Queue#latency (used in sidekiq web), requires `enqueued_at` 180 | pre_normalized_item[:enqueued_at] = Time.now.to_f 181 | 182 | pre_normalized_item 183 | end 184 | 185 | # Given an object meant to represent time, try to convert it intelligently to a float 186 | def normalize_time(time) 187 | # if the time param is a `Date` / `String` convert it to a `Time` object 188 | if time.is_a?(Date) 189 | normalized_time = time.to_time 190 | elsif time.is_a?(String) 191 | normalized_time = Time.parse(time) 192 | else 193 | normalized_time = time 194 | end 195 | 196 | # convert the `Time` object to a float (if necessary) 197 | normalized_time = normalized_time.to_f unless normalized_time.is_a?(Numeric) 198 | 199 | normalized_time 200 | end 201 | 202 | private 203 | 204 | def initialize_connection_pool(options = {}) 205 | Thread.exclusive do 206 | @connection_pool = ::ConnectionPool.new(timeout: redis_pool_timeout, size: redis_pool_size) do 207 | Rubykiq::Connection.new(options) 208 | end 209 | end 210 | end 211 | end 212 | end 213 | -------------------------------------------------------------------------------- /lib/rubykiq/connection.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'redis/namespace' 3 | 4 | module Rubykiq 5 | class Connection 6 | extend Forwardable 7 | def_delegators :@redis_connection, :multi, :namespace, :sadd, :zadd, :lpush, :lpop, :lrange, :llen, :zcard, :zrange, :flushdb 8 | def_delegators :@redis_client, :host, :port, :db, :password 9 | 10 | # Initialize a new Connection object 11 | # 12 | # @param options [Hash] 13 | def initialize(options = {}) 14 | url = options.delete(:url) { determine_redis_provider } 15 | namespace = options.delete(:namespace) 16 | driver = options.delete(:driver) 17 | @redis_connection = initialize_conection(url, namespace, driver) 18 | @redis_client = @redis_connection.client 19 | @redis_connection 20 | end 21 | 22 | private 23 | 24 | def determine_redis_provider 25 | # lets try and fallback to another redis url 26 | ENV['REDISTOGO_URL'] || ENV['REDIS_PROVIDER'] || ENV['REDIS_URL'] || 'redis://localhost:6379/0' 27 | end 28 | 29 | def initialize_conection(url, namespace, driver) 30 | client = ::Redis.new(url: url, driver: driver) 31 | ::Redis::Namespace.new(namespace, redis: client) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/rubykiq/version.rb: -------------------------------------------------------------------------------- 1 | module Rubykiq 2 | VERSION = '1.0.2' unless defined?(Rubykiq::VERSION) 3 | end 4 | -------------------------------------------------------------------------------- /rubykiq.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'rubykiq/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'rubykiq' 8 | spec.version = Rubykiq::VERSION 9 | spec.authors = ['Karl Freeman'] 10 | spec.email = ['karlfreeman@gmail.com'] 11 | spec.summary = %q{Sidekiq agnostic enqueuing using Redis} 12 | spec.description = %q{Sidekiq agnostic enqueuing using Redis} 13 | spec.homepage = 'https://github.com/karlfreeman/rubykiq' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | spec.required_ruby_version = '>= 1.9.3' 21 | 22 | spec.add_dependency 'redis', '>= 3.0' 23 | spec.add_dependency 'redis-namespace', '>= 1.0' 24 | spec.add_dependency 'multi_json', '>= 1.0' 25 | spec.add_dependency 'connection_pool', '>= 1.0' 26 | 27 | spec.add_development_dependency 'bundler', '~> 1.5' 28 | spec.add_development_dependency 'rake', '~> 10.0' 29 | spec.add_development_dependency 'kramdown', '>= 0.14' 30 | spec.add_development_dependency 'rubocop', '~> 0.19' 31 | spec.add_development_dependency 'pry' 32 | spec.add_development_dependency 'yard' 33 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | 3 | require 'bundler' 4 | Bundler.setup 5 | 6 | %w(support).each do |dir| 7 | Dir.glob(File.expand_path("../#{dir}/**/*.rb", __FILE__), &method(:require)) 8 | end 9 | 10 | require 'rubykiq' 11 | require 'rspec/its' 12 | 13 | RSpec.configure do |config| 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | 18 | def jruby? 19 | defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' 20 | end 21 | 22 | def wrap_in_synchrony?(driver) 23 | yield unless driver == :synchrony 24 | EM.synchrony do 25 | yield if block_given? 26 | EM.stop 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/codeclimate.rb: -------------------------------------------------------------------------------- 1 | require 'codeclimate-test-reporter' 2 | CodeClimate::TestReporter.start 3 | -------------------------------------------------------------------------------- /spec/support/pry.rb: -------------------------------------------------------------------------------- 1 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby' 2 | begin 3 | require 'pry' unless ENV['CI'] 4 | rescue LoadError 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/timecop.rb: -------------------------------------------------------------------------------- 1 | require 'timecop' 2 | Timecop.freeze 3 | -------------------------------------------------------------------------------- /spec/unit/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'hiredis' 3 | require 'em-synchrony' 4 | 5 | describe Rubykiq::Client do 6 | before(:all) { Timecop.freeze } 7 | after(:all) { Timecop.return } 8 | let(:ruby_client) { Rubykiq::Client.new(driver: :ruby, namespace: :ruby) } 9 | let(:hiredis_client) { Rubykiq::Client.new(driver: :hiredis, namespace: :hiredis) } 10 | let(:synchrony_client) { Rubykiq::Client.new(driver: :synchrony, namespace: :synchrony) } 11 | 12 | # eg with a variety of drivers 13 | [:ruby, :hiredis, :synchrony].each do |driver| 14 | # skip incompatible drivers when running in JRuby 15 | next if jruby? && (driver == :hiredis || :synchrony) 16 | context "using driver '#{driver}'" do 17 | # make sure the let is the current client being tested 18 | let(:client) { send("#{driver}_client") } 19 | describe :defaults do 20 | subject { client } 21 | its(:namespace) { should eq driver } 22 | its(:driver) { should be driver } 23 | its(:retry) { should eq true } 24 | its(:queue) { should eq 'default' } 25 | end 26 | 27 | describe :push do 28 | context :validations do 29 | context 'with an incorrect message type' do 30 | it 'raises an ArgumentError' do 31 | expect { client.push([]) }.to raise_error(ArgumentError, /Message must be a Hash/) 32 | expect { client.push('{}') }.to raise_error(ArgumentError, /Message must be a Hash/) 33 | expect { client.push(Class.new) }.to raise_error(ArgumentError, /Message must be a Hash/) 34 | end 35 | end 36 | 37 | context 'without a class' do 38 | it 'raises an ArgumentError' do 39 | expect { client.push(args: ['foo', 1, { bat: 'bar' }]) }.to raise_error(ArgumentError, /Message must include a class/) 40 | end 41 | end 42 | 43 | context 'with an incorrect args type' do 44 | it 'raises an ArgumentError' do 45 | expect { client.push(class: 'MyWorker', args: { bat: 'bar' }) }.to raise_error(ArgumentError, /Message args must be an Array/) 46 | end 47 | end 48 | 49 | context 'with an incorrect class type' do 50 | it 'raises an ArgumentError' do 51 | expect { client.push(class: Class, args: ['foo', 1, { bat: 'bar' }]) }.to raise_error(ArgumentError, /Message class must be a String representation of the class name/) 52 | end 53 | end 54 | end 55 | 56 | # eg singular and batch 57 | arguments = [[{ bat: 'bar' }], [[{ bat: 'bar' }], [{ bat: 'foo' }]]] 58 | arguments.each do |args| 59 | context "with args #{args}" do 60 | it "should create #{args.length} job(s)" do 61 | wrap_in_synchrony?(driver) do 62 | client.connection_pool(&:flushdb) 63 | 64 | expect { client.push(class: 'MyWorker', args: args) }.to change { 65 | client.connection_pool { |connection| connection.llen('queue:default'); } 66 | }.from(0).to(args.length) 67 | 68 | raw_jobs = client.connection_pool { |connection| connection.lrange('queue:default', 0, args.length); } 69 | raw_jobs.each do |job| 70 | job = MultiJson.decode(job, symbolize_keys: true) 71 | expect(job).to have_key(:jid) 72 | expect(job[:enqueued_at]).to be_within(1).of(Time.now.to_f) 73 | end 74 | end 75 | end 76 | 77 | # eg with a variety of different time types 78 | times = [Time.now, DateTime.now, Time.now.utc.iso8601, Time.now.to_f] 79 | times.each do |time| 80 | context "with time #{time} (#{time.class})" do 81 | it "should create #{args.length} job(s)" do 82 | wrap_in_synchrony?(driver) do 83 | client.connection_pool(&:flushdb) 84 | 85 | expect { client.push(class: 'MyWorker', args: args, at: time) }.to change { 86 | client.connection_pool { |connection| connection.zcard('schedule'); } 87 | }.from(0).to(args.length) 88 | 89 | raw_jobs = client.connection_pool { |connection| connection.zrange('schedule', 0, args.length); } 90 | raw_jobs.each do |job| 91 | job = MultiJson.decode(job, symbolize_keys: true) 92 | expect(job).to have_key(:at) 93 | expect(job[:at]).to be_within(1).of(Time.now.to_f) 94 | end 95 | end 96 | end 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/unit/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Rubykiq::Connection do 4 | describe :defaults do 5 | subject { Rubykiq::Connection.new } 6 | its(:namespace) { should be_nil } 7 | its(:host) { should eq 'localhost' } 8 | its(:port) { should be 6379 } 9 | its(:db) { should be 0 } 10 | its(:password) { should be_nil } 11 | end 12 | 13 | describe :options do 14 | context :custom do 15 | subject { Rubykiq::Connection.new(namespace: 'yyy') } 16 | its(:namespace) { should eq 'yyy' } 17 | end 18 | 19 | context :inherited_settings do 20 | it 'should work' do 21 | client = Rubykiq::Client.new(namespace: 'xxx') 22 | client.connection_pool do |connection| 23 | expect(connection.namespace).to eq 'xxx' 24 | end 25 | end 26 | end 27 | end 28 | 29 | describe :env do 30 | subject { Rubykiq::Connection.new } 31 | [{ name: 'REDISTOGO_URL', value: 'redistogo' }, { name: 'REDIS_PROVIDER', value: 'redisprovider' }, { name: 'REDIS_URL', value: 'redisurl' }].each do |test_case| 32 | context "with ENV[#{test_case[:name]}]" do 33 | before do 34 | ENV[test_case[:name]] = "redis://#{test_case[:value]}:6379/0" 35 | end 36 | after do 37 | ENV[test_case[:name]] = nil 38 | end 39 | its(:host) { should eq test_case[:value] } 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/rubykiq_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Rubykiq do 4 | describe :client do 5 | it 'should return a Client' do 6 | expect(Rubykiq.client).to be_kind_of(Rubykiq::Client) 7 | end 8 | 9 | it 'should be thread safe' do 10 | t1 = Thread.new { client = Rubykiq.client; sleep 0.1; client } 11 | t2 = Thread.new { Rubykiq.client } 12 | expect(t1.value).to eql t2.value 13 | end 14 | end 15 | 16 | describe :connection_pool do 17 | it 'should return a ConnectionPool' do 18 | expect(Rubykiq.connection_pool).to be_kind_of(::ConnectionPool) 19 | end 20 | it 'should be thread safe' do 21 | t1 = Thread.new { connection_pool = Rubykiq.connection_pool; sleep 0.1; connection_pool } 22 | t2 = Thread.new { Rubykiq.connection_pool } 23 | expect(t1.value).to eql t2.value 24 | end 25 | end 26 | 27 | # for every valid option 28 | Rubykiq::Client::VALID_OPTIONS_KEYS.each do |key| 29 | describe key do 30 | subject { Rubykiq } 31 | it { should respond_to key } 32 | it { should respond_to "#{key}=".to_sym } 33 | end 34 | end 35 | end 36 | --------------------------------------------------------------------------------