sidekiq-unique-jobs 6.0.8 → 6.0.9
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sidekiq-unique-jobs might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.codeclimate.yml +10 -13
- data/.editorconfig +1 -1
- data/.gitignore +11 -22
- data/.mdlrc +1 -0
- data/.reek.yml +1 -0
- data/.rspec +1 -1
- data/.rubocop.yml +58 -30
- data/.travis.yml +14 -7
- data/.yardopts +1 -7
- data/CHANGELOG.md +704 -160
- data/Gemfile +19 -17
- data/Guardfile +0 -29
- data/README.md +77 -65
- data/Rakefile +22 -5
- data/bin/bench +5 -5
- data/bin/uniquejobs +2 -2
- data/examples/custom_queue_job.rb +1 -1
- data/examples/custom_queue_job_with_filter_method.rb +1 -1
- data/examples/custom_queue_job_with_filter_proc.rb +2 -2
- data/examples/my_unique_job_with_filter_method.rb +1 -1
- data/examples/my_unique_job_with_filter_proc.rb +1 -1
- data/examples/unique_job_with_filter_method.rb +1 -1
- data/lib/sidekiq-unique-jobs.rb +1 -1
- data/lib/sidekiq_unique_jobs.rb +32 -107
- data/lib/sidekiq_unique_jobs/cli.rb +15 -10
- data/lib/sidekiq_unique_jobs/client/middleware.rb +1 -1
- data/lib/sidekiq_unique_jobs/constants.rb +25 -20
- data/lib/sidekiq_unique_jobs/digests.rb +5 -4
- data/lib/sidekiq_unique_jobs/lock/base_lock.rb +3 -3
- data/lib/sidekiq_unique_jobs/lock/until_executed.rb +1 -1
- data/lib/sidekiq_unique_jobs/lock/while_executing.rb +1 -1
- data/lib/sidekiq_unique_jobs/locksmith.rb +4 -4
- data/lib/sidekiq_unique_jobs/logging.rb +1 -1
- data/lib/sidekiq_unique_jobs/middleware.rb +9 -4
- data/lib/sidekiq_unique_jobs/normalizer.rb +1 -1
- data/lib/sidekiq_unique_jobs/on_conflict.rb +12 -7
- data/lib/sidekiq_unique_jobs/on_conflict/raise.rb +1 -1
- data/lib/sidekiq_unique_jobs/on_conflict/reject.rb +3 -3
- data/lib/sidekiq_unique_jobs/on_conflict/strategy.rb +1 -1
- data/lib/sidekiq_unique_jobs/options_with_fallback.rb +1 -1
- data/lib/sidekiq_unique_jobs/scripts.rb +5 -5
- data/lib/sidekiq_unique_jobs/sidekiq_unique_ext.rb +1 -1
- data/lib/sidekiq_unique_jobs/sidekiq_unique_jobs.rb +75 -0
- data/lib/sidekiq_unique_jobs/testing.rb +3 -3
- data/lib/sidekiq_unique_jobs/timeout.rb +1 -1
- data/lib/sidekiq_unique_jobs/unique_args.rb +2 -2
- data/lib/sidekiq_unique_jobs/util.rb +3 -3
- data/lib/sidekiq_unique_jobs/version.rb +1 -1
- data/lib/sidekiq_unique_jobs/web.rb +13 -8
- data/lib/sidekiq_unique_jobs/web/helpers.rb +2 -2
- data/lib/sidekiq_unique_jobs/web/views/unique_digests.erb +4 -0
- data/lib/tasks/changelog.rake +23 -0
- data/sidekiq-unique-jobs.gemspec +48 -27
- data/update_docs.sh +37 -0
- metadata +68 -33
- data/.csslintrc +0 -2
- data/.dockerignore +0 -4
- data/.eslintignore +0 -1
- data/.eslintrc +0 -213
data/Gemfile
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source "https://rubygems.org"
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
gem
|
7
|
-
gem
|
8
|
-
gem
|
9
|
-
gem
|
6
|
+
gem "appraisal", "~> 2.2.0"
|
7
|
+
gem "rspec-eventually", require: false
|
8
|
+
gem "rspec-its", require: false
|
9
|
+
gem "rspec-retry", require: false
|
10
10
|
|
11
11
|
platforms :mri_25 do
|
12
|
-
gem
|
13
|
-
gem
|
14
|
-
gem
|
15
|
-
gem
|
16
|
-
gem
|
17
|
-
gem
|
18
|
-
gem
|
19
|
-
gem
|
20
|
-
gem
|
21
|
-
gem
|
22
|
-
gem
|
23
|
-
gem
|
12
|
+
gem "benchmark-ips"
|
13
|
+
gem "fasterer"
|
14
|
+
gem "fuubar"
|
15
|
+
gem "guard"
|
16
|
+
gem "guard-reek"
|
17
|
+
gem "guard-rspec"
|
18
|
+
gem "guard-rubocop"
|
19
|
+
gem "memory_profiler"
|
20
|
+
gem "pry"
|
21
|
+
gem "reek", ">= 5.3"
|
22
|
+
gem "rubocop"
|
23
|
+
gem "rubocop-rspec"
|
24
|
+
gem "simplecov-json"
|
25
|
+
gem "travis"
|
24
26
|
end
|
data/Guardfile
CHANGED
@@ -1,35 +1,7 @@
|
|
1
|
-
# A sample Guardfile
|
2
|
-
# More info at https://github.com/guard/guard#readme
|
3
|
-
|
4
|
-
## Uncomment and set this to only include directories you want to watch
|
5
|
-
# directories %w(app lib config test spec features) \
|
6
|
-
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
-
|
8
|
-
## Note: if you are using the `directories` clause above and you are not
|
9
|
-
## watching the project directory ('.'), then you will want to move
|
10
|
-
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
-
#
|
12
|
-
# $ mkdir config
|
13
|
-
# $ mv Guardfile config/
|
14
|
-
# $ ln -s config/Guardfile .
|
15
|
-
#
|
16
|
-
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
-
|
18
|
-
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
-
# rspec may be run, below are examples of the most common uses.
|
20
|
-
# * bundler: 'bundle exec rspec'
|
21
|
-
# * bundler binstubs: 'bin/rspec'
|
22
|
-
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
-
# installed the spring binstubs per the docs)
|
24
|
-
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
-
# * 'just' rspec: 'rspec'
|
26
|
-
|
27
1
|
guard :rspec, cmd: "env COV=false bundle exec rspec" do
|
28
2
|
require "guard/rspec/dsl"
|
29
3
|
dsl = Guard::RSpec::Dsl.new(self)
|
30
4
|
|
31
|
-
# Feel free to open issues for suggestions and improvements
|
32
|
-
|
33
5
|
# RSpec files
|
34
6
|
rspec = dsl.rspec
|
35
7
|
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
@@ -39,7 +11,6 @@ guard :rspec, cmd: "env COV=false bundle exec rspec" do
|
|
39
11
|
watch(rspec.spec_support) { rspec.spec_dir }
|
40
12
|
watch(rspec.spec_files)
|
41
13
|
|
42
|
-
# Ruby files
|
43
14
|
ruby = dsl.ruby
|
44
15
|
dsl.watch_spec_files_for(ruby.lib_files)
|
45
16
|
end
|
data/README.md
CHANGED
@@ -1,43 +1,45 @@
|
|
1
1
|
# SidekiqUniqueJobs [![Join the chat at https://gitter.im/mhenrixon/sidekiq-unique-jobs](https://badges.gitter.im/mhenrixon/sidekiq-unique-jobs.svg)](https://gitter.im/mhenrixon/sidekiq-unique-jobs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/mhenrixon/sidekiq-unique-jobs.png?branch=master)](https://travis-ci.org/mhenrixon/sidekiq-unique-jobs) [![Code Climate](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs.png)](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs) [![Test Coverage](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs/badges/coverage.svg)](https://codeclimate.com/github/mhenrixon/sidekiq-unique-jobs/coverage)
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
3
|
+
<!-- MarkdownTOC -->
|
4
|
+
|
5
|
+
- [Introduction](#introduction)
|
6
|
+
- [Documentation](#documentation)
|
7
|
+
- [Requirements](#requirements)
|
8
|
+
- [Installation](#installation)
|
9
|
+
- [Support Me](#support-me)
|
10
|
+
- [General Information](#general-information)
|
11
|
+
- [Options](#options)
|
12
|
+
- [Lock Expiration](#lock-expiration)
|
13
|
+
- [Lock Timeout](#lock-timeout)
|
14
|
+
- [Unique Across Queues](#unique-across-queues)
|
15
|
+
- [Unique Across Workers](#unique-across-workers)
|
16
|
+
- [Locks](#locks)
|
17
|
+
- [Until Executing](#until-executing)
|
18
|
+
- [Until Executed](#until-executed)
|
19
|
+
- [Until Timeout](#until-timeout)
|
20
|
+
- [Unique Until And While Executing](#unique-until-and-while-executing)
|
21
|
+
- [While Executing](#while-executing)
|
22
|
+
- [Conflict Strategy](#conflict-strategy)
|
23
|
+
- [Log](#log)
|
24
|
+
- [Raise](#raise)
|
25
|
+
- [Reject](#reject)
|
26
|
+
- [Replace](#replace)
|
27
|
+
- [Reschedule](#reschedule)
|
28
|
+
- [Usage](#usage)
|
29
|
+
- [Finer Control over Uniqueness](#finer-control-over-uniqueness)
|
30
|
+
- [After Unlock Callback](#after-unlock-callback)
|
31
|
+
- [Logging](#logging)
|
32
|
+
- [Cleanup Dead Locks](#cleanup-dead-locks)
|
33
|
+
- [Debugging](#debugging)
|
34
|
+
- [Sidekiq Web](#sidekiq-web)
|
35
|
+
- [Show Unique Digests](#show-unique-digests)
|
36
|
+
- [Show keys for digest](#show-keys-for-digest)
|
37
|
+
- [Communication](#communication)
|
38
|
+
- [Testing](#testing)
|
39
|
+
- [Contributing](#contributing)
|
40
|
+
- [Contributors](#contributors)
|
41
|
+
|
42
|
+
<!-- /MarkdownTOC -->
|
41
43
|
|
42
44
|
## Introduction
|
43
45
|
|
@@ -45,16 +47,16 @@ The goal of this gem is to ensure your Sidekiq jobs are unique. We do this by cr
|
|
45
47
|
|
46
48
|
## Documentation
|
47
49
|
|
48
|
-
This is the documentation for the master branch. You can find the documentation for each release by navigating to its tag:
|
50
|
+
This is the documentation for the master branch. You can find the documentation for each release by navigating to its tag: [v5.0.10][]
|
49
51
|
|
50
52
|
Below are links to the latest major versions (4 & 5):
|
51
53
|
|
52
|
-
- [v5.0.10]
|
53
|
-
- [v4.0.18]
|
54
|
+
- [v5.0.10][]
|
55
|
+
- [v4.0.18][]
|
54
56
|
|
55
57
|
## Requirements
|
56
58
|
|
57
|
-
See
|
59
|
+
See [Sidekiq requirements][] for what is required. Starting from 5.0.0 only sidekiq >= 4 is supported and support for MRI <= 2.1 is dropped. ActiveJob is not supported
|
58
60
|
|
59
61
|
Version 6 requires Redis >= 3 and pure Sidekiq, no ActiveJob supported anymore. See [About ActiveJob](https://github.com/mhenrixon/sidekiq-unique-jobs/wiki/About-ActiveJob) for why.
|
60
62
|
|
@@ -62,20 +64,25 @@ Version 6 requires Redis >= 3 and pure Sidekiq, no ActiveJob supported anymore.
|
|
62
64
|
|
63
65
|
Add this line to your application's Gemfile:
|
64
66
|
|
65
|
-
|
67
|
+
```
|
68
|
+
gem 'sidekiq-unique-jobs'
|
69
|
+
```
|
66
70
|
|
67
71
|
And then execute:
|
68
72
|
|
69
|
-
|
73
|
+
```
|
74
|
+
bundle
|
75
|
+
```
|
70
76
|
|
71
77
|
Or install it yourself as:
|
72
78
|
|
73
|
-
|
74
|
-
|
79
|
+
```
|
80
|
+
gem install sidekiq-unique-jobs
|
81
|
+
```
|
75
82
|
|
76
83
|
## Support Me
|
77
84
|
|
78
|
-
Want to show me some ❤️ for the hard work I do on this gem? You can use the following PayPal link
|
85
|
+
Want to show me some ❤️ for the hard work I do on this gem? You can use the following [PayPal link][]. Any amount is welcome and let me tell you it feels good to be appreciated. Even a dollar makes me super excited about all of this.
|
79
86
|
|
80
87
|
## General Information
|
81
88
|
|
@@ -115,7 +122,7 @@ This configuration option is slightly misleading. It doesn't disregard the queue
|
|
115
122
|
```ruby
|
116
123
|
class Worker
|
117
124
|
include Sidekiq::Worker
|
118
|
-
|
125
|
+
|
119
126
|
sidekiq_options unique_across_queues: true, queue: 'default'
|
120
127
|
|
121
128
|
def perform(args); end
|
@@ -131,7 +138,7 @@ This configuration option is slightly misleading. It doesn't disregard the worke
|
|
131
138
|
```ruby
|
132
139
|
class WorkerOne
|
133
140
|
include Sidekiq::Worker
|
134
|
-
|
141
|
+
|
135
142
|
sidekiq_options unique_across_workers: true, queue: 'default'
|
136
143
|
|
137
144
|
def perform(args); end
|
@@ -139,17 +146,17 @@ end
|
|
139
146
|
|
140
147
|
class WorkerTwo
|
141
148
|
include Sidekiq::Worker
|
142
|
-
|
149
|
+
|
143
150
|
sidekiq_options unique_across_workers: true, queue: 'default'
|
144
151
|
|
145
152
|
def perform(args); end
|
146
153
|
end
|
147
154
|
|
148
155
|
|
149
|
-
WorkerOne.perform_async(1)
|
156
|
+
WorkerOne.perform_async(1)
|
150
157
|
# => 'the jobs unique id'
|
151
158
|
|
152
|
-
WorkerTwo.perform_async(1)
|
159
|
+
WorkerTwo.perform_async(1)
|
153
160
|
# => nil because WorkerOne just stole the lock
|
154
161
|
```
|
155
162
|
|
@@ -220,7 +227,7 @@ In the console you should see something like:
|
|
220
227
|
|
221
228
|
## Conflict Strategy
|
222
229
|
|
223
|
-
Decides how we handle conflict. We can either reject the job to the dead queue or reschedule it. Both are useful for jobs that absolutely need to run and have been configured to use the lock `WhileExecuting` that is used only by the sidekiq server process.
|
230
|
+
Decides how we handle conflict. We can either reject the job to the dead queue or reschedule it. Both are useful for jobs that absolutely need to run and have been configured to use the lock `WhileExecuting` that is used only by the sidekiq server process.
|
224
231
|
|
225
232
|
The last one is log which can be be used with the lock `UntilExecuted` and `UntilExpired`. Now we write a log entry saying the job could not be pushed because it is a duplicate of another job with the same arguments
|
226
233
|
|
@@ -246,9 +253,9 @@ This strategy is intended to be used with `WhileExecuting` and will push the job
|
|
246
253
|
|
247
254
|
This strategy is intended to be used with client locks like `UntilExecuted`.
|
248
255
|
It will delete any existing job for these arguments from retry, schedule and
|
249
|
-
queue and retry the lock again.
|
256
|
+
queue and retry the lock again.
|
250
257
|
|
251
|
-
This is slightly dangerous and should probably only be used for jobs that are
|
258
|
+
This is slightly dangerous and should probably only be used for jobs that are
|
252
259
|
always scheduled in the future. Currently only attempting to retry one time.
|
253
260
|
|
254
261
|
`sidekiq_options lock: :until_executed, on_conflict: :replace`
|
@@ -322,7 +329,7 @@ end
|
|
322
329
|
|
323
330
|
### After Unlock Callback
|
324
331
|
|
325
|
-
If you need to perform any additional work after the lock has been released you can provide an `#after_unlock` instance method. The method will be called when the lock has been unlocked. Most times this means after yield but there are two exceptions to that.
|
332
|
+
If you need to perform any additional work after the lock has been released you can provide an `#after_unlock` instance method. The method will be called when the lock has been unlocked. Most times this means after yield but there are two exceptions to that.
|
326
333
|
|
327
334
|
**Exception 1:** UntilExecuting unlocks and calls back before yielding.
|
328
335
|
**Exception 2:** UntilExpired expires eventually, no after_unlock hook is called.
|
@@ -391,12 +398,11 @@ require 'sidekiq_unique_jobs/web'
|
|
391
398
|
mount Sidekiq::Web, at: '/sidekiq'
|
392
399
|
```
|
393
400
|
|
394
|
-
There is no need to `require 'sidekiq/web'` since `sidekiq_unique_jobs/web`
|
401
|
+
There is no need to `require 'sidekiq/web'` since `sidekiq_unique_jobs/web`
|
395
402
|
already does this.
|
396
403
|
|
397
404
|
To filter/search for keys we can use the wildcard `*`. If we have a unique digest `'uniquejobs:9e9b5ce5d423d3ea470977004b50ff84` we can search for it by enter `*ff84` and it should return all digests that end with `ff84`.
|
398
405
|
|
399
|
-
|
400
406
|
#### Show Unique Digests
|
401
407
|
|
402
408
|
![Unique Digests](assets/unique_digests_1.png)
|
@@ -413,7 +419,7 @@ There is a [![Join the chat at https://gitter.im/mhenrixon/sidekiq-unique-jobs](
|
|
413
419
|
|
414
420
|
This has been probably the most confusing part of this gem. People get really confused with how unreliable the unique jobs have been. I there for decided to do what Mike is doing for sidekiq enterprise. Read the section about unique jobs.
|
415
421
|
|
416
|
-
|
422
|
+
[Enterprise unique jobs][]
|
417
423
|
|
418
424
|
```ruby
|
419
425
|
SidekiqUniqueJobs.configure do |config|
|
@@ -462,12 +468,18 @@ I would strongly suggest you let this gem test uniqueness. If you care about how
|
|
462
468
|
## Contributing
|
463
469
|
|
464
470
|
1. Fork it
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
471
|
+
1. Create your feature branch (`git checkout -b my-new-feature`)
|
472
|
+
1. Commit your changes (`git commit -am 'Add some feature'`)
|
473
|
+
1. Push to the branch (`git push origin my-new-feature`)
|
474
|
+
1. Create new Pull Request
|
469
475
|
|
476
|
+
## Contributors
|
470
477
|
|
471
|
-
|
478
|
+
You can find a list of contributors over on [Contributors][]
|
472
479
|
|
473
|
-
|
480
|
+
[v5.0.10]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v5.0.10.
|
481
|
+
[v4.0.18]: https://github.com/mhenrixon/sidekiq-unique-jobs/tree/v4.0.18
|
482
|
+
[Sidekiq requirements]: https://github.com/mperham/sidekiq#requirements
|
483
|
+
[Enterprise unique jobs]: https://www.dailydrip.com/topics/sidekiq/drips/sidekiq-enterprise-unique-jobs
|
484
|
+
[Contributors]: https://github.com/mhenrixon/sidekiq-unique-jobs/graphs/contributors
|
485
|
+
[Paypal link https://paypal.me/mhenrixon]: https://paypal.me/mhenrixon
|
data/Rakefile
CHANGED
@@ -1,12 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
|
6
|
-
|
7
|
-
require 'rubocop/rake_task'
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
require "rubocop/rake_task"
|
5
|
+
|
6
|
+
Dir.glob("#{File.expand_path(__dir__)}/lib/tasks/**/*.rake").each { |f| import f }
|
8
7
|
|
9
8
|
RuboCop::RakeTask.new(:style)
|
10
9
|
RSpec::Core::RakeTask.new(:spec)
|
11
10
|
|
11
|
+
require "yard"
|
12
|
+
YARD::Rake::YardocTask.new do |t|
|
13
|
+
t.files = %w[lib/sidekiq_unique_jobs/**/*.rb"]
|
14
|
+
t.options = %w[
|
15
|
+
--no-private
|
16
|
+
--markup=markdown
|
17
|
+
--markup-provider=redcarpet
|
18
|
+
--readme README.md
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
12
22
|
task default: [:style, :spec]
|
23
|
+
|
24
|
+
task :release do
|
25
|
+
sh("./update_docs.sh")
|
26
|
+
sh("gem release --tag --push")
|
27
|
+
Rake::Task["changelog"].invoke
|
28
|
+
sh("gem bump --file lib/sidekiq_unique_jobs/version.rb")
|
29
|
+
end
|
data/bin/bench
CHANGED
@@ -3,17 +3,17 @@
|
|
3
3
|
|
4
4
|
# Trap interrupts to quit cleanly. See
|
5
5
|
# https://twitter.com/mitchellh/status/283014103189053442
|
6
|
-
Signal.trap(
|
6
|
+
Signal.trap("INT") { abort }
|
7
7
|
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
8
|
+
require "bundler/setup"
|
9
|
+
require "benchmark/ips"
|
10
|
+
require "sidekiq-unique-jobs"
|
11
11
|
|
12
12
|
ITERATIONS ||= 10_000
|
13
13
|
|
14
14
|
Benchmark.ips do |x|
|
15
15
|
x.config(time: 5, warmup: 2)
|
16
|
-
x.report(
|
16
|
+
x.report("new_shit") do |_times|
|
17
17
|
SidekiqUniqueJobs::Scripts::AcquireLock.execute(nil, SecureRandom.hex, SecureRandom.hex)
|
18
18
|
end
|
19
19
|
x.compare!
|
data/bin/uniquejobs
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# :nocov:
|
4
4
|
|
5
|
-
require_relative
|
5
|
+
require_relative "./custom_queue_job"
|
6
6
|
|
7
7
|
class CustomQueueJobWithFilterProc < CustomQueueJob
|
8
8
|
# slightly contrived example of munging args to the
|
@@ -10,7 +10,7 @@ class CustomQueueJobWithFilterProc < CustomQueueJob
|
|
10
10
|
sidekiq_options lock: :until_expired,
|
11
11
|
unique_args: (lambda do |args|
|
12
12
|
options = args.extract_options!
|
13
|
-
options.delete(
|
13
|
+
options.delete("random")
|
14
14
|
args + [options]
|
15
15
|
end)
|
16
16
|
end
|