rubykiq 0.0.1
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 +15 -0
- data/.document +3 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +18 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +0 -0
- data/CONTRIBUTING.md +46 -0
- data/Gemfile +33 -0
- data/LICENSE.md +22 -0
- data/README.md +81 -0
- data/Rakefile +27 -0
- data/lib/rubykiq/client.rb +213 -0
- data/lib/rubykiq/connection.rb +38 -0
- data/lib/rubykiq/version.rb +3 -0
- data/lib/rubykiq.rb +23 -0
- data/rubykiq.gemspec +23 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/pry.rb +3 -0
- data/spec/support/simplecov.rb +15 -0
- data/spec/support/timecop.rb +2 -0
- data/spec/unit/client_spec.rb +141 -0
- data/spec/unit/connection_spec.rb +49 -0
- data/spec/unit/rubykiq_spec.rb +31 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YWIyYTZlNTdiMjRmZDEyMmM0ZDAyZjAxNDg0MThjMzhkZWJhMGEzYQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NmFjNWNjY2MzNjBlZTFjYTRiN2ZhYTc1N2EwMGExMzJmYmY3NTdjOQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YjA2NmU0YjZhMzZmM2M4MTc1YWYyOTg1NmYxNGUyNWMzMGRkMDVmNzMzNDQw
|
10
|
+
NmZiZGM4NmViMGI0YjMzOGRmOWIyMTNjYjczMmViMjYzNTJiODQ1YzY3MDU0
|
11
|
+
YzUzYTc3Nzg1MzJhZGY2ZDViMzNjM2RlOWNkOGFlMzI4Y2M0ZjU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NzE1NTkwODZmMzU0YWU1N2I3MmQzZjliMzVhZThkM2QzNTMzMTEyNmJhOThh
|
14
|
+
MTUwYzE2NDgyNjQxYjNiNWRjZDRiMmQxMTMyOTI5ZWE5Y2UyMzJmMGQ2OTNk
|
15
|
+
YWZlN2FiYzMwOTg1MWIyM2UxMjY0YzE4ODM5M2I1Y2RjMDBmYTY=
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
bundler_args: --without development
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- rbx-18mode
|
5
|
+
- rbx-19mode
|
6
|
+
- jruby-18mode
|
7
|
+
- jruby-19mode
|
8
|
+
- 1.8.7
|
9
|
+
- 1.9.2
|
10
|
+
- 1.9.3
|
11
|
+
- 2.0.0
|
12
|
+
services:
|
13
|
+
- redis-server
|
14
|
+
matrix:
|
15
|
+
allow_failures:
|
16
|
+
- rvm: rbx-18mode
|
17
|
+
- rvm: jruby-18mode
|
18
|
+
- rvm: 1.8.7
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
File without changes
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,46 @@
|
|
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. Run `open coverage/index.html`. If your changes are not completely covered
|
40
|
+
by your tests, return to step 3.
|
41
|
+
8. Add, commit, and push your changes.
|
42
|
+
9. [Submit a pull request.][pr]
|
43
|
+
|
44
|
+
[fork]: http://help.github.com/fork-a-repo/
|
45
|
+
[branch]: http://learn.github.com/p/branching.html
|
46
|
+
[pr]: http://help.github.com/send-pull-requests/
|
data/Gemfile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
gem "rake", ">= 1.2"
|
4
|
+
gem "yard"
|
5
|
+
|
6
|
+
# platforms :ruby_18 do
|
7
|
+
# end
|
8
|
+
# platforms :ruby, :mswin, :mingw do
|
9
|
+
# end
|
10
|
+
# platforms :jruby do
|
11
|
+
# end
|
12
|
+
|
13
|
+
gem "hiredis", "~> 0.4.5", :require => false
|
14
|
+
gem "em-synchrony", :require => false
|
15
|
+
|
16
|
+
group :development do
|
17
|
+
gem "kramdown", ">= 0.14"
|
18
|
+
gem "pry"
|
19
|
+
gem "pry-debugger", :platforms => :mri_19
|
20
|
+
gem "awesome_print"
|
21
|
+
end
|
22
|
+
|
23
|
+
group :test do
|
24
|
+
gem "rspec"
|
25
|
+
gem "rspec-smart-formatter"
|
26
|
+
gem "vcr"
|
27
|
+
gem "timecop"
|
28
|
+
gem "simplecov", :require => false
|
29
|
+
gem "coveralls", :require => false
|
30
|
+
gem "cane"
|
31
|
+
end
|
32
|
+
|
33
|
+
gemspec
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 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.
|
data/README.md
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# Rubykiq ( WIP ) [][gem] [][travis] [][gemnasium] [][coveralls] [][codeclimate]
|
2
|
+
|
3
|
+
[Sidekiq] agnostic enqueuing using Redis.
|
4
|
+
|
5
|
+
[gem]: https://rubygems.org/gems/rubykiq
|
6
|
+
[travis]: http://travis-ci.org/karlfreeman/rubykiq
|
7
|
+
[gemnasium]: https://gemnasium.com/karlfreeman/rubykiq
|
8
|
+
[coveralls]: https://coveralls.io/r/karlfreeman/rubykiq
|
9
|
+
[codeclimate]: https://codeclimate.com/github/karlfreeman/rubykiq
|
10
|
+
|
11
|
+
[sidekiq]: http://mperham.github.com/sidekiq/
|
12
|
+
|
13
|
+
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.
|
14
|
+
|
15
|
+
## Features / Usage Examples
|
16
|
+
|
17
|
+
* [Redis][] has support for [alternative drivers](https://github.com/redis/redis-rb#alternate-drivers), Rubykiq is tested with these in mind. ( eg :synchrony )
|
18
|
+
* The `:at` parameter supports `Time`, `Date` and any `Time.parse`-able strings.
|
19
|
+
* Pushing multiple and singular jobs has the same interface, simply nest args.
|
20
|
+
* Slightly less gem dependecies, and by that I mean Sidekiq::Client without Celluloid ( which is already very light! )
|
21
|
+
|
22
|
+
[redis]: https://github.com/redis/redis-rb
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require "rubykiq"
|
26
|
+
|
27
|
+
# will also detect REDIS_URL, REDIS_PROVIDER and REDISTOGO_URL ENV variables
|
28
|
+
Rubykiq.url = "redis://127.0.0.1:6379"
|
29
|
+
|
30
|
+
# alternative driver support ( :ruby, :hiredis, :synchrony )
|
31
|
+
Rubykiq.driver = :synchrony
|
32
|
+
|
33
|
+
# defaults to nil
|
34
|
+
Rubykiq.namespace = "background"
|
35
|
+
|
36
|
+
# uses "default" queue unless specified
|
37
|
+
Rubykiq.push(:class => "Worker", :args => ["foo", 1, :bat => "bar"])
|
38
|
+
|
39
|
+
# args are optionally set to empty
|
40
|
+
Rubykiq.push(:class => "Scheduler", :queue => "scheduler")
|
41
|
+
|
42
|
+
# will batch up multiple jobs
|
43
|
+
Rubykiq.push(:class => "Worker", :args => [["foo"], ["bar"]])
|
44
|
+
|
45
|
+
# at param can be a "Time", "Date" or any "Time.parse"-able strings
|
46
|
+
Rubykiq.push(:class => "DelayedHourMailer", :at => Time.now + 3600)
|
47
|
+
Rubykiq.push(:class => "DelayedDayMailer", :at => DateTime.now.next_day)
|
48
|
+
Rubykiq.push(:class => "DelayedMailer", :at => "2013-01-01T09:00:00Z")
|
49
|
+
|
50
|
+
# alias based sugar
|
51
|
+
job = { :class => "Worker" }
|
52
|
+
Rubykiq << job
|
53
|
+
```
|
54
|
+
__It's advised that using [Sidekiq::Client's push] method when already a dependency is going to be better in most everyday cases__
|
55
|
+
|
56
|
+
[sidekiq::client's push]: https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/client.rb#L36
|
57
|
+
|
58
|
+
## Supported Ruby Versions
|
59
|
+
This library aims to support and is [tested against][travis] the following Ruby
|
60
|
+
implementations:
|
61
|
+
|
62
|
+
* Ruby 1.9.2 (drivers: ruby, hiredis, synchrony)
|
63
|
+
* Ruby 1.9.3 (drivers: ruby, hiredis, synchrony)
|
64
|
+
* Ruby 2.0.0 (drivers: ruby, hiredis, synchrony)
|
65
|
+
* [JRuby][] (drivers: ruby)
|
66
|
+
* [Rubinius][] (drivers: ruby)
|
67
|
+
|
68
|
+
[jruby]: http://www.jruby.org/
|
69
|
+
[rubinius]: http://rubini.us/
|
70
|
+
|
71
|
+
# Credits
|
72
|
+
|
73
|
+
Inspiration:
|
74
|
+
|
75
|
+
- [Michael Grosser's Enqueue into Sidkiq post](http://grosser.it/2013/01/17/enqueue-into-sidekiq-via-pure-redis-without-loading-sidekiq/)
|
76
|
+
|
77
|
+
Cribbed:
|
78
|
+
|
79
|
+
- [Sidekiq's internal client class](https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/client.rb)
|
80
|
+
- [Sidekiq's internal redis class](https://github.com/mperham/sidekiq/blob/master/lib/sidekiq/redis_connection.rb)
|
81
|
+
- [Sidekiq's FAQ](https://github.com/mperham/sidekiq/wiki/FAQ)
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler.setup
|
3
|
+
Bundler::GemHelper.install_tasks
|
4
|
+
|
5
|
+
require "yard"
|
6
|
+
YARD::Rake::YardocTask.new
|
7
|
+
|
8
|
+
require "rspec/core/rake_task"
|
9
|
+
desc "Run all examples"
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
11
|
+
|
12
|
+
begin
|
13
|
+
require "cane/rake_task"
|
14
|
+
namespace :metric do
|
15
|
+
desc "Analyze for code quality"
|
16
|
+
Cane::RakeTask.new(:quality) do |cane|
|
17
|
+
cane.abc_max = 27
|
18
|
+
cane.no_doc = true
|
19
|
+
cane.style_measure = 200
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
warn "Cane is not available, metric:quality task not provided."
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => :spec
|
27
|
+
task :test => :spec
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'connection_pool'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'multi_json'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module Rubykiq
|
7
|
+
|
8
|
+
class Client
|
9
|
+
|
10
|
+
# An array of valid keys in the options hash when configuring a `Rubykiq::Client`
|
11
|
+
VALID_OPTIONS_KEYS = [
|
12
|
+
:redis_pool_size,
|
13
|
+
:redis_pool_timeout,
|
14
|
+
:url,
|
15
|
+
:namespace,
|
16
|
+
:driver,
|
17
|
+
:retry,
|
18
|
+
:queue
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
# A hash of valid options and their default values
|
22
|
+
DEFAULT_OPTIONS = {
|
23
|
+
:redis_pool_size => 1,
|
24
|
+
:redis_pool_timeout => 1,
|
25
|
+
:url => nil,
|
26
|
+
:namespace => nil,
|
27
|
+
:driver => :ruby,
|
28
|
+
:retry => true,
|
29
|
+
:queue => "default"
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
# Bang open the valid options
|
33
|
+
attr_accessor(*VALID_OPTIONS_KEYS)
|
34
|
+
|
35
|
+
# Initialize a new Client object
|
36
|
+
#
|
37
|
+
# @param options [Hash]
|
38
|
+
def initialize(options = {})
|
39
|
+
reset_options
|
40
|
+
options.each_pair do |key, value|
|
41
|
+
send("#{key}=", value) if VALID_OPTIONS_KEYS.include?(key)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetch the ::ConnectionPool of Rubykiq::Connections
|
46
|
+
#
|
47
|
+
# @return [::ConnectionPool]
|
48
|
+
def connection_pool(options={}, &block)
|
49
|
+
options = valid_options.merge(options)
|
50
|
+
|
51
|
+
@connection_pool ||= ::ConnectionPool.new(:timeout => redis_pool_timeout, :size => redis_pool_size) do
|
52
|
+
Rubykiq::Connection.new(options)
|
53
|
+
end
|
54
|
+
|
55
|
+
if block_given?
|
56
|
+
@connection_pool.with(&block)
|
57
|
+
else
|
58
|
+
return @connection_pool
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Push a Sidekiq job to Redis. Accepts a number of options:
|
64
|
+
#
|
65
|
+
# :class - the worker class to call, required.
|
66
|
+
# :queue - the named queue to use, optional ( default: "default" )
|
67
|
+
# :args - an array of simple arguments to the perform method, must be JSON-serializable, optional ( default: [] )
|
68
|
+
# :retry - whether to retry this job if it fails, true or false, default true, optional ( default: true )
|
69
|
+
# :at - when the job should be executed. This can be a `Time`, `Date` or any `Time.parse`-able strings, optional.
|
70
|
+
#
|
71
|
+
# Returns nil if not pushed to Redis. In the case of an indvidual job a job ID will be returned,
|
72
|
+
# if multiple jobs are pushed the size of the jobs will be returned
|
73
|
+
#
|
74
|
+
# Example:
|
75
|
+
# Rubykiq.push(:class => "Worker", :args => ["foo", 1, :bat => "bar"])
|
76
|
+
# Rubykiq.push(:class => "Scheduler", :queue => "scheduler")
|
77
|
+
# Rubykiq.push(:class => "DelayedMailer", :at => "2013-01-01T09:00:00Z")
|
78
|
+
# Rubykiq.push(:class => "Worker", :args => [["foo"], ["bar"]])
|
79
|
+
#
|
80
|
+
# @param items [Array]
|
81
|
+
def push(items)
|
82
|
+
raise(ArgumentError, "Message must be a Hash") unless items.is_a?(Hash)
|
83
|
+
raise(ArgumentError, "Message args must be an Array") if items[:args] && !items[:args].is_a?(Array)
|
84
|
+
|
85
|
+
# args are optional
|
86
|
+
items[:args] ||= []
|
87
|
+
|
88
|
+
# determine if this items arg's is a nested array
|
89
|
+
items[:args].first.is_a?(Array) ? push_many(items) : push_one(items)
|
90
|
+
|
91
|
+
end
|
92
|
+
alias_method :<<, :push
|
93
|
+
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
# Create a hash of options and their values
|
98
|
+
def valid_options
|
99
|
+
VALID_OPTIONS_KEYS.inject({}){|o,k| o.merge!(k => send(k)) }
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create a hash of the default options and their values
|
103
|
+
def default_options
|
104
|
+
DEFAULT_OPTIONS
|
105
|
+
end
|
106
|
+
|
107
|
+
# Set the VALID_OPTIONS_KEYS with their DEFAULT_OPTIONS
|
108
|
+
def reset_options
|
109
|
+
VALID_OPTIONS_KEYS.each do |key|
|
110
|
+
send("#{key}=", default_options[key])
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# when only one item is needed to persisted to redis
|
115
|
+
def push_one(item)
|
116
|
+
|
117
|
+
# we're expecting item to be a single item so simply normalize it
|
118
|
+
payload = normalize_item(item)
|
119
|
+
|
120
|
+
# if successfully persisted to redis return this item's `jid`
|
121
|
+
pushed = false
|
122
|
+
pushed = raw_push([payload]) if payload
|
123
|
+
pushed ? payload[:jid] : nil
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
# when multiple item's are needing to be persisted to redis
|
128
|
+
def push_many(items)
|
129
|
+
|
130
|
+
# we're expecting items to have an nested array of args, lets take each one and correctly normalize them
|
131
|
+
payloads = items[:args].map do |args|
|
132
|
+
raise ArgumentError, "Bulk arguments must be an Array of Arrays: [[:foo => 'bar'], [:foo => 'foo']]" unless args.is_a?(Array)
|
133
|
+
# clone the original items ( for :queue, :class, etc.. )
|
134
|
+
item = items.clone
|
135
|
+
# merge this item's args ( eg the nested `arg` array )
|
136
|
+
item.merge!(:args => args) unless args.empty?
|
137
|
+
# normalize this individual item
|
138
|
+
item = normalize_item(item)
|
139
|
+
end.compact
|
140
|
+
|
141
|
+
# if successfully persisted to redis return the size of the jobs
|
142
|
+
pushed = false
|
143
|
+
pushed = raw_push(payloads) unless payloads.empty?
|
144
|
+
pushed ? payloads.size : nil
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
# persist the job message(s)
|
149
|
+
def raw_push(payloads)
|
150
|
+
pushed = false
|
151
|
+
Rubykiq.connection_pool do |connection|
|
152
|
+
if payloads.first[:at]
|
153
|
+
pushed = connection.zadd("schedule", payloads.map {|item| [ item[:at].to_s, ::MultiJson.encode(item) ]})
|
154
|
+
else
|
155
|
+
q = payloads.first[:queue]
|
156
|
+
to_push = payloads.map { |item| ::MultiJson.encode(item) }
|
157
|
+
_, pushed = connection.multi do
|
158
|
+
connection.sadd("queues", q)
|
159
|
+
connection.lpush("queue:#{q}", to_push)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
pushed
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
def normalize_item(item)
|
168
|
+
raise(ArgumentError, "Message must be a Hash") unless item.is_a?(Hash)
|
169
|
+
raise(ArgumentError, "Message must include a class and set of arguments: #{item.inspect}") if !item[:class] || !item[:args]
|
170
|
+
raise(ArgumentError, "Message args must be an Array") if item[:args] && !item[:args].is_a?(Array)
|
171
|
+
raise(ArgumentError, "Message class must be a String representation of the class name") unless item[:class].is_a?(String)
|
172
|
+
|
173
|
+
# normalize the time
|
174
|
+
item[:at] = normalize_time(item[:at]) if item[:at]
|
175
|
+
pre_normalized_item = item.clone
|
176
|
+
|
177
|
+
# args are optional
|
178
|
+
pre_normalized_item[:args] ||= []
|
179
|
+
|
180
|
+
# apply the default options
|
181
|
+
[:retry, :queue].each do |key|
|
182
|
+
pre_normalized_item[key] = send("#{key}") unless pre_normalized_item.has_key?(key)
|
183
|
+
end
|
184
|
+
|
185
|
+
# provide a job ID
|
186
|
+
pre_normalized_item[:jid] = ::SecureRandom.hex(12)
|
187
|
+
|
188
|
+
return pre_normalized_item
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
# Given an object meant to represent time, try to convert it intelligently to a float
|
193
|
+
def normalize_time(time)
|
194
|
+
|
195
|
+
# if the time param is a `Date` / `String` convert it to a `Time` object
|
196
|
+
if time.is_a?(Date)
|
197
|
+
normalized_time = time.to_time
|
198
|
+
elsif time.is_a?(String)
|
199
|
+
normalized_time = Time.parse(time)
|
200
|
+
else
|
201
|
+
normalized_time = time
|
202
|
+
end
|
203
|
+
|
204
|
+
# convert the `Time` object to a float ( if necessary )
|
205
|
+
normalized_time = normalized_time.to_f unless normalized_time.is_a?(Numeric)
|
206
|
+
|
207
|
+
return normalized_time
|
208
|
+
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'redis'
|
2
|
+
require 'redis/namespace'
|
3
|
+
|
4
|
+
module Rubykiq
|
5
|
+
|
6
|
+
class Connection
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :@redis_connection, :multi, :namespace, :sadd, :zadd, :lpush, :lpop, :lrange, :llen, :zcard, :zrange, :flushdb
|
10
|
+
def_delegators :@redis_client, :host, :port, :db, :password
|
11
|
+
|
12
|
+
# Initialize a new Connection object
|
13
|
+
#
|
14
|
+
# @param options [Hash]
|
15
|
+
def initialize(options = {})
|
16
|
+
url = options.delete(:url) { determine_redis_provider }
|
17
|
+
namespace = options.delete(:namespace)
|
18
|
+
driver = options.delete(:driver)
|
19
|
+
@redis_connection ||= build_conection(url, namespace, driver)
|
20
|
+
@redis_client ||= @redis_connection.client
|
21
|
+
return @redis_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# lets try and fallback to another redis url
|
27
|
+
def determine_redis_provider
|
28
|
+
ENV["REDISTOGO_URL"] || ENV["REDIS_PROVIDER"] || ENV["REDIS_URL"] || "redis://localhost:6379/0"
|
29
|
+
end
|
30
|
+
|
31
|
+
# construct a namespaced redis connection
|
32
|
+
def build_conection(url, namespace, driver)
|
33
|
+
client = ::Redis.connect(:url => url, :driver => driver)
|
34
|
+
return ::Redis::Namespace.new(namespace, :redis => client)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/rubykiq.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "rubykiq/client"
|
3
|
+
require "rubykiq/connection"
|
4
|
+
|
5
|
+
module Rubykiq
|
6
|
+
extend SingleForwardable
|
7
|
+
|
8
|
+
def_delegators :client, :<<, :push, :connection_pool
|
9
|
+
|
10
|
+
# delegate all VALID_OPTIONS_KEYS accessors to the client
|
11
|
+
def_delegators :client, *Rubykiq::Client::VALID_OPTIONS_KEYS
|
12
|
+
|
13
|
+
# delegate all VALID_OPTIONS_KEYS setters to the client ( Hacky I know... )
|
14
|
+
def_delegators :client, *(Rubykiq::Client::VALID_OPTIONS_KEYS.dup.collect! do |key| "#{key}=".to_sym; end)
|
15
|
+
|
16
|
+
# Fetch the Rubykiq::Client
|
17
|
+
#
|
18
|
+
# @return [Rubykiq::Client]
|
19
|
+
def self.client(options={})
|
20
|
+
@client ||= Rubykiq::Client.new(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/rubykiq.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rubykiq/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.add_dependency "redis", "~> 3.0"
|
7
|
+
gem.add_dependency "redis-namespace", "~> 1.0"
|
8
|
+
gem.add_dependency "multi_json", "~> 1.0"
|
9
|
+
gem.add_dependency "connection_pool", "~> 1.0"
|
10
|
+
gem.add_development_dependency "bundler", "~> 1.0"
|
11
|
+
gem.name = "rubykiq"
|
12
|
+
gem.version = Rubykiq::VERSION
|
13
|
+
gem.authors = ["Karl Freeman"]
|
14
|
+
gem.email = ["karlfreeman@gmail.com"]
|
15
|
+
gem.license = "MIT"
|
16
|
+
gem.description = %q{Sidekiq agnostic enqueuing using Redis}
|
17
|
+
gem.summary = %q{Sidekiq agnostic enqueuing using Redis}
|
18
|
+
gem.homepage = "https://github.com/karlfreeman/rubykiq"
|
19
|
+
gem.files = `git ls-files`.split("\n")
|
20
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
21
|
+
gem.require_paths = ["lib"]
|
22
|
+
gem.required_ruby_version = ">= 1.9.2"
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
|
3
|
+
require "bundler"
|
4
|
+
Bundler.setup
|
5
|
+
begin; require "awesome_print"; rescue LoadError; end
|
6
|
+
|
7
|
+
require "rspec"
|
8
|
+
|
9
|
+
require "support/pry"
|
10
|
+
require "support/timecop"
|
11
|
+
require "support/simplecov"
|
12
|
+
|
13
|
+
require "rubykiq"
|
14
|
+
|
15
|
+
# used as a stupid mixin class
|
16
|
+
class DummyClass
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.expect_with :rspec do |c|
|
22
|
+
c.syntax = :expect
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
def jruby?
|
28
|
+
defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
def wrap_in_synchrony?(driver)
|
33
|
+
|
34
|
+
yield unless driver == :synchrony
|
35
|
+
|
36
|
+
EM.synchrony do
|
37
|
+
yield if block_given?
|
38
|
+
EM.stop
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/spec/support/pry.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
|
2
|
+
|
3
|
+
require "simplecov"
|
4
|
+
|
5
|
+
if ENV["CI"]
|
6
|
+
require "coveralls"
|
7
|
+
Coveralls.wear!
|
8
|
+
SimpleCov.formatter = Coveralls::SimpleCov::Formatter
|
9
|
+
end
|
10
|
+
|
11
|
+
SimpleCov.start do
|
12
|
+
add_filter "vendor"
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "hiredis"
|
3
|
+
require "em-synchrony"
|
4
|
+
|
5
|
+
describe Rubykiq::Client do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
Timecop.freeze
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:all) do
|
12
|
+
Timecop.return
|
13
|
+
end
|
14
|
+
|
15
|
+
let (:ruby_client) { Rubykiq::Client.new }
|
16
|
+
let (:hiredis_client) { Rubykiq::Client.new(:driver => :hiredis) }
|
17
|
+
let (:synchrony_client) { Rubykiq::Client.new(:driver => :synchrony) }
|
18
|
+
|
19
|
+
# eg with a variety of drivers
|
20
|
+
[:ruby, :hiredis, :synchrony].each do |driver|
|
21
|
+
|
22
|
+
# skip incompatible drivers when running in JRuby
|
23
|
+
next if jruby? && (driver == :hiredis || :synchrony)
|
24
|
+
|
25
|
+
context "with #{driver}" do
|
26
|
+
|
27
|
+
# make sure the let is the current client being tested
|
28
|
+
let(:client) { self.send("#{driver}_client") }
|
29
|
+
|
30
|
+
describe :defaults do
|
31
|
+
subject { client }
|
32
|
+
its(:namespace) { should be_nil }
|
33
|
+
its(:driver) { should be driver }
|
34
|
+
its(:retry) { should be_true }
|
35
|
+
its(:queue) { should eq "default" }
|
36
|
+
end
|
37
|
+
|
38
|
+
describe :push do
|
39
|
+
|
40
|
+
context :validations do
|
41
|
+
|
42
|
+
context "with an incorrect message type" do
|
43
|
+
it "raises an ArgumentError" do
|
44
|
+
expect{ client.push([]) }.to raise_error(ArgumentError, /Message must be a Hash/)
|
45
|
+
expect{ client.push("{}") }.to raise_error(ArgumentError, /Message must be a Hash/)
|
46
|
+
expect{ client.push(DummyClass.new) }.to raise_error(ArgumentError, /Message must be a Hash/)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "without a class" do
|
51
|
+
it "raises an ArgumentError" do
|
52
|
+
expect{ client.push(:args => ['foo', 1, { :bat => "bar" }]) }.to raise_error(ArgumentError, /Message must include a class/)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "with an incorrect args type" do
|
57
|
+
it "raises an ArgumentError" do
|
58
|
+
expect{ client.push(:class => "MyWorker", :args => { :bat => "bar" }) }.to raise_error(ArgumentError, /Message args must be an Array/)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with an incorrect class type" do
|
63
|
+
it "raises an ArgumentError" do
|
64
|
+
expect{ client.push(:class => DummyClass, :args => ["foo", 1, { :bat => "bar" }]) }.to raise_error(ArgumentError, /Message class must be a String representation of the class name/)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
# eg singular and batch
|
71
|
+
args = [[{:bat => "bar"}],[[{:bat => "bar"}],[{:bat => "foo"}]]]
|
72
|
+
args.each do |args|
|
73
|
+
|
74
|
+
context "with args #{args}" do
|
75
|
+
|
76
|
+
it "should create #{args.length} job(s)" do
|
77
|
+
|
78
|
+
wrap_in_synchrony?(driver) do
|
79
|
+
|
80
|
+
client.connection_pool do |connection|
|
81
|
+
connection.flushdb
|
82
|
+
end
|
83
|
+
|
84
|
+
expect { client.push(:class => "MyWorker", :args => args) }.to change {
|
85
|
+
client.connection_pool do |connection| connection.llen("queue:default"); end
|
86
|
+
}.from(0).to(args.length)
|
87
|
+
|
88
|
+
raw_jobs = client.connection_pool do |connection| connection.lrange("queue:default", 0, args.length); end
|
89
|
+
raw_jobs.each do |job|
|
90
|
+
job = MultiJson.decode(job, :symbolize_keys => true)
|
91
|
+
expect(job).to have_key(:jid)
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
# eg with a variety of different time types
|
99
|
+
times = [ Time.now, DateTime.now, Time.now.utc.iso8601, Time.now.to_f ]
|
100
|
+
times.each do |time|
|
101
|
+
|
102
|
+
context "with time #{time} ( #{time.class} )" do
|
103
|
+
|
104
|
+
it "should create #{args.length} job(s)" do
|
105
|
+
|
106
|
+
wrap_in_synchrony?(driver) do
|
107
|
+
|
108
|
+
client.connection_pool do |connection|
|
109
|
+
connection.flushdb
|
110
|
+
end
|
111
|
+
|
112
|
+
expect { client.push(:class => "MyWorker", :args => args, :at => time) }.to change {
|
113
|
+
client.connection_pool do |connection| connection.zcard("schedule"); end
|
114
|
+
}.from(0).to(args.length)
|
115
|
+
|
116
|
+
raw_jobs = client.connection_pool do |connection| connection.zrange("schedule", 0, args.length); end
|
117
|
+
raw_jobs.each do |job|
|
118
|
+
job = MultiJson.decode(job, :symbolize_keys => true)
|
119
|
+
expect(job).to have_key(:at)
|
120
|
+
expect(job[:at]).to be_within(1).of(Time.now.to_f)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rubykiq::Connection do
|
4
|
+
|
5
|
+
describe :defaults do
|
6
|
+
subject { Rubykiq::Connection.new }
|
7
|
+
its(:namespace) { should be_nil }
|
8
|
+
its(:host) { should eq "localhost" }
|
9
|
+
its(:port) { should be 6379 }
|
10
|
+
its(:db) { should be 0 }
|
11
|
+
its(:password) { should be_nil }
|
12
|
+
end
|
13
|
+
|
14
|
+
describe :options do
|
15
|
+
|
16
|
+
context :custom do
|
17
|
+
subject { Rubykiq::Connection.new(:namespace => "yyy") }
|
18
|
+
its(:namespace) { should eq "yyy" }
|
19
|
+
end
|
20
|
+
|
21
|
+
context :inherited_settings do
|
22
|
+
it "should work" do
|
23
|
+
client = Rubykiq::Client.new(:namespace => "xxx")
|
24
|
+
client.connection_pool do |connection|
|
25
|
+
expect(connection.namespace).to eq "xxx"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
describe :env do
|
33
|
+
subject { Rubykiq::Connection.new }
|
34
|
+
|
35
|
+
[{:name => "REDISTOGO_URL", :value => "redistogo"}, {:name => "REDIS_PROVIDER", :value => "redisprovider"}, {:name => "REDIS_URL", :value => "redisurl"} ].each do | test_case |
|
36
|
+
context "with ENV[#{test_case[:name]}]" do
|
37
|
+
before do
|
38
|
+
ENV[test_case[:name]] = "redis://#{test_case[:value]}:6379/0"
|
39
|
+
end
|
40
|
+
after do
|
41
|
+
ENV[test_case[:name]] = nil
|
42
|
+
end
|
43
|
+
its(:host) { should eq test_case[:value] }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Rubykiq do
|
4
|
+
|
5
|
+
describe :version do
|
6
|
+
subject { Rubykiq::VERSION }
|
7
|
+
it { should be_kind_of(String) }
|
8
|
+
end
|
9
|
+
|
10
|
+
describe :client do
|
11
|
+
subject { Rubykiq.client }
|
12
|
+
it { should be_kind_of(Rubykiq::Client) }
|
13
|
+
end
|
14
|
+
|
15
|
+
describe :connection_pool do
|
16
|
+
subject { Rubykiq.connection_pool }
|
17
|
+
it { should be_kind_of(::ConnectionPool) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# for every valid option
|
21
|
+
Rubykiq::Client::VALID_OPTIONS_KEYS.each do |key|
|
22
|
+
|
23
|
+
describe key do
|
24
|
+
subject { Rubykiq }
|
25
|
+
it {should respond_to key }
|
26
|
+
it {should respond_to "#{key}=".to_sym }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubykiq
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Karl Freeman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-04-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: redis
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redis-namespace
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: multi_json
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: connection_pool
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
description: Sidekiq agnostic enqueuing using Redis
|
84
|
+
email:
|
85
|
+
- karlfreeman@gmail.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- .document
|
91
|
+
- .gitignore
|
92
|
+
- .rspec
|
93
|
+
- .travis.yml
|
94
|
+
- .yardopts
|
95
|
+
- CHANGELOG.md
|
96
|
+
- CONTRIBUTING.md
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.md
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- lib/rubykiq.rb
|
102
|
+
- lib/rubykiq/client.rb
|
103
|
+
- lib/rubykiq/connection.rb
|
104
|
+
- lib/rubykiq/version.rb
|
105
|
+
- rubykiq.gemspec
|
106
|
+
- spec/spec_helper.rb
|
107
|
+
- spec/support/pry.rb
|
108
|
+
- spec/support/simplecov.rb
|
109
|
+
- spec/support/timecop.rb
|
110
|
+
- spec/unit/client_spec.rb
|
111
|
+
- spec/unit/connection_spec.rb
|
112
|
+
- spec/unit/rubykiq_spec.rb
|
113
|
+
homepage: https://github.com/karlfreeman/rubykiq
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.9.2
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 2.0.1
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: Sidekiq agnostic enqueuing using Redis
|
137
|
+
test_files:
|
138
|
+
- spec/spec_helper.rb
|
139
|
+
- spec/support/pry.rb
|
140
|
+
- spec/support/simplecov.rb
|
141
|
+
- spec/support/timecop.rb
|
142
|
+
- spec/unit/client_spec.rb
|
143
|
+
- spec/unit/connection_spec.rb
|
144
|
+
- spec/unit/rubykiq_spec.rb
|
145
|
+
has_rdoc:
|