├── .gitignore ├── .travis.yml ├── CHANGES.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── sidekiq-middleware.rb └── sidekiq-middleware │ ├── client │ └── unique_jobs.rb │ ├── core_ext.rb │ ├── helpers.rb │ ├── middleware.rb │ ├── server │ └── unique_jobs.rb │ └── version.rb ├── sidekiq-middleware.gemspec └── test ├── helper.rb ├── test_core_ext.rb └── test_unique_jobs.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | dump.rdb 19 | .ruby-* 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0 4 | - 2.1 5 | env: 6 | matrix: 7 | - SIDEKIQ_VERSION="~> 2.12" 8 | - SIDEKIQ_VERSION="~> 2.17" 9 | - SIDEKIQ_VERSION="~> 3.0" 10 | - SIDEKIQ_VERSION="~> 3.1" 11 | branches: 12 | only: 13 | - master 14 | services: 15 | - redis 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 0.2.1 2 | ----------- 3 | 4 | - Make middleware work properly even for ```Sidekiq::Extensions``` workers ([dimko](https://github.com/dimko)) 5 | 6 | 0.2.0 7 | ----------- 8 | 9 | - Fix redundant scheduled jobs locking when ```unique``` is not set to ```:all``` ([dimko](https://github.com/dimko)) 10 | 11 | 0.1.4 12 | ----------- 13 | 14 | - Make sure that scheduled unique jobs correctly move from the queue to work ([Sutto](https://github.com/Sutto)) 15 | 16 | 0.1.3 17 | ----------- 18 | 19 | - Constantize string worker_class in client middleware, require newest Sidekiq ([dimko](https://github.com/dimko)) 20 | 21 | 0.1.2 22 | ----------- 23 | 24 | - Fixed unique jobs server middleware to clear lock only when unique is enabled 25 | 26 | 0.1.1 27 | ----------- 28 | 29 | - Improved lock expiration period for scheduled jobs 30 | 31 | 0.1.0 32 | ----------- 33 | 34 | - Added ability to set custom lock key ([dimko](https://github.com/dimko)) 35 | - Removed forever option due to race condition issues. Added ability to manually operate unique locks instead 36 | 37 | 0.0.6 38 | ----------- 39 | 40 | - Now all unique locks are prefixed with "locks:unique:" and could be found using wildcard 41 | 42 | 0.0.5 43 | ----------- 44 | 45 | - Fixed arguments passed to Hash#slice to be convinient with ActiveSupport slice 46 | 47 | 0.0.4 48 | ----------- 49 | 50 | - Fixed Hash#slice ([bnorton](https://github.com/bnorton)) 51 | 52 | 0.0.3 53 | ----------- 54 | 55 | - Refactored and simplified the UniqueJobs middleware server and client as well as only enforcing the uniqueness of the payload across the keys for class, queue, args, and at ([bnorton](https://github.com/bnorton)) 56 | 57 | 0.0.2 58 | ----------- 59 | 60 | - Added tests 61 | 62 | Initial release! 63 | ----------- 64 | 65 | - UniqueJobs -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | gem 'sidekiq', ENV['SIDEKIQ_VERSION'] if ENV['SIDEKIQ_VERSION'] 5 | 6 | group :test do 7 | gem 'simplecov', require: false 8 | gem 'coveralls', require: false 9 | end 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Dmitry Krasnoukhov 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 | # Additional sidekiq middleware 2 | 3 | [![Gem Version](https://badge.fury.io/rb/sidekiq-middleware.png)](http://badge.fury.io/rb/sidekiq-middleware) 4 | [![Dependency Status](https://gemnasium.com/krasnoukhov/sidekiq-middleware.png)](https://gemnasium.com/krasnoukhov/sidekiq-middleware) 5 | [![Code Climate](https://codeclimate.com/github/krasnoukhov/sidekiq-middleware.png)](https://codeclimate.com/github/krasnoukhov/sidekiq-middleware) 6 | [![Build Status](https://secure.travis-ci.org/krasnoukhov/sidekiq-middleware.png)](http://travis-ci.org/krasnoukhov/sidekiq-middleware) 7 | [![Coverage Status](https://coveralls.io/repos/krasnoukhov/sidekiq-middleware/badge.png)](https://coveralls.io/r/krasnoukhov/sidekiq-middleware) 8 | 9 | 10 | This gem provides additional middleware for [Sidekiq](https://github.com/mperham/sidekiq). 11 | 12 | See [Sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Middleware) for more details. 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | gem 'sidekiq-middleware' 19 | 20 | And then execute: 21 | 22 | $ bundle 23 | 24 | Or install it yourself as: 25 | 26 | $ gem install sidekiq-middleware 27 | 28 | ## Contents 29 | 30 | ### UniqueJobs 31 | 32 | Provides uniqueness for jobs. 33 | 34 | **Usage** 35 | 36 | Example worker: 37 | 38 | ```ruby 39 | class UniqueWorker 40 | include Sidekiq::Worker 41 | 42 | sidekiq_options({ 43 | # Should be set to true (enables uniqueness for async jobs) 44 | # or :all (enables uniqueness for both async and scheduled jobs) 45 | unique: :all, 46 | 47 | # Unique expiration (optional, default is 30 minutes) 48 | # For scheduled jobs calculates automatically based on schedule time and expiration period 49 | expiration: 24 * 60 * 60 50 | }) 51 | 52 | def perform 53 | # Your code goes here 54 | end 55 | end 56 | ``` 57 | 58 | Custom lock key and manual expiration: 59 | 60 | ```ruby 61 | class UniqueWorker 62 | include Sidekiq::Worker 63 | 64 | sidekiq_options({ 65 | unique: :all, 66 | expiration: 24 * 60 * 60, 67 | 68 | # Set this to true when you need to handle locks manually. 69 | # You'll be able to handle unique expiration inside your worker. 70 | # Please see example below. 71 | manual: true 72 | }) 73 | 74 | # Implement your own lock string 75 | def self.lock(id) 76 | "locks:unique:#{id}" 77 | end 78 | 79 | # Implement method to handle lock removing manually 80 | def self.unlock!(id) 81 | lock = self.lock(id) 82 | Sidekiq.redis { |conn| conn.del(lock) } 83 | end 84 | 85 | def perform(id) 86 | # Your code goes here 87 | # You are able to re-schedule job from perform method, 88 | # Just remove lock manually before performing job again. 89 | sleep 5 90 | 91 | # Re-schedule! 92 | self.class.unlock!(id) 93 | self.class.perform_async(id) 94 | end 95 | end 96 | ``` 97 | 98 | 99 | ## Contributing 100 | 101 | 1. Fork it 102 | 2. Create your feature branch (`git checkout -b my-new-feature`) 103 | 3. Commit your changes (`git commit -am 'Added some feature'`) 104 | 4. Push to the branch (`git push origin my-new-feature`) 105 | 5. Create new Pull Request 106 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "rake/testtask" 4 | Rake::TestTask.new(:test) do |test| 5 | test.libs << 'test' 6 | test.pattern = 'test/**/test_*.rb' 7 | end 8 | 9 | task :default => :test -------------------------------------------------------------------------------- /lib/sidekiq-middleware.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require 'sidekiq-middleware/version' 3 | require 'sidekiq-middleware/core_ext' 4 | require 'sidekiq-middleware/helpers' 5 | require 'sidekiq-middleware/server/unique_jobs' 6 | require 'sidekiq-middleware/client/unique_jobs' 7 | require 'sidekiq-middleware/middleware' 8 | -------------------------------------------------------------------------------- /lib/sidekiq-middleware/client/unique_jobs.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | module Middleware 3 | module Client 4 | class UniqueJobs 5 | def call(worker_class, item, queue, redis_pool = nil) 6 | begin 7 | worker_class = worker_class.constantize if worker_class.is_a?(String) 8 | enabled = Sidekiq::Middleware::Helpers.unique_enabled?(worker_class, item) 9 | rescue NameError 10 | enabled = false 11 | end 12 | 13 | if enabled 14 | expiration = Sidekiq::Middleware::Helpers.unique_expiration(worker_class) 15 | job_id = item['jid'] 16 | 17 | # Scheduled 18 | if item.has_key?('at') 19 | # Use expiration period as specified in configuration, 20 | # but relative to job schedule time 21 | expiration += (item['at'].to_i - Time.now.to_i) 22 | end 23 | 24 | unique_key = Sidekiq::Middleware::Helpers.unique_digest(worker_class, item) 25 | 26 | # Sidekiq >= 3.0 27 | unique = if redis_pool 28 | redis_pool.with { |conn| status(conn, unique_key, expiration, job_id) } 29 | else 30 | Sidekiq.redis { |conn| status(conn, unique_key, expiration, job_id) } 31 | end 32 | 33 | yield if unique 34 | else 35 | yield 36 | end 37 | end 38 | 39 | def status(conn, unique_key, expiration, job_id) 40 | unique = false 41 | conn.watch(unique_key) 42 | 43 | locked_job_id = conn.get(unique_key) 44 | if locked_job_id && locked_job_id != job_id 45 | conn.unwatch 46 | else 47 | unique = conn.multi do 48 | conn.setex(unique_key, expiration, job_id) 49 | end 50 | end 51 | 52 | unique 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/sidekiq-middleware/core_ext.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | def slice(*items) 3 | items = items.to_a.flatten 4 | 5 | {}.tap do |hash| 6 | items.each do |item| 7 | hash[item] = self[item] if self.key?(item) 8 | end 9 | end 10 | end unless new.respond_to?(:slice) 11 | end 12 | -------------------------------------------------------------------------------- /lib/sidekiq-middleware/helpers.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | module Middleware 3 | module Helpers 4 | extend self 5 | 6 | UNIQUE_EXPIRATION = 30 * 60 # 30 minutes 7 | 8 | def unique_digest(klass, item) 9 | if klass.respond_to?(:lock) 10 | args = item['args'] 11 | klass.lock(*args) 12 | else 13 | dumped = Sidekiq.dump_json(item.slice('class', 'queue', 'args')) 14 | digest = Digest::MD5.hexdigest(dumped) 15 | 16 | "locks:unique:#{digest}" 17 | end 18 | end 19 | 20 | def unique_expiration(klass) 21 | klass.get_sidekiq_options['expiration'] || UNIQUE_EXPIRATION 22 | end 23 | 24 | def unique_enabled?(klass, item) 25 | enabled = klass.get_sidekiq_options['unique'] 26 | if item.has_key?('at') && enabled != :all 27 | enabled = false 28 | end 29 | enabled 30 | end 31 | 32 | def unique_manual?(klass) 33 | klass.get_sidekiq_options['manual'] 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/sidekiq-middleware/middleware.rb: -------------------------------------------------------------------------------- 1 | Sidekiq.configure_server do |config| 2 | config.server_middleware do |chain| 3 | chain.add Sidekiq::Middleware::Server::UniqueJobs 4 | end 5 | config.client_middleware do |chain| 6 | chain.add Sidekiq::Middleware::Client::UniqueJobs 7 | end 8 | end 9 | 10 | Sidekiq.configure_client do |config| 11 | config.client_middleware do |chain| 12 | chain.add Sidekiq::Middleware::Client::UniqueJobs 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/sidekiq-middleware/server/unique_jobs.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | module Middleware 3 | module Server 4 | class UniqueJobs 5 | def call(worker_instance, item, queue) 6 | worker_class = worker_instance.class 7 | enabled = Sidekiq::Middleware::Helpers.unique_enabled?(worker_class, item) 8 | 9 | if enabled 10 | begin 11 | yield 12 | ensure 13 | unless Sidekiq::Middleware::Helpers.unique_manual?(worker_class) 14 | clear(worker_class, item) 15 | end 16 | end 17 | else 18 | yield 19 | end 20 | end 21 | 22 | def clear(worker_class, item) 23 | Sidekiq.redis do |conn| 24 | conn.del Sidekiq::Middleware::Helpers.unique_digest(worker_class, item) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/sidekiq-middleware/version.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | module Middleware 3 | VERSION = "0.3.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /sidekiq-middleware.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/sidekiq-middleware/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Dmitry Krasnoukhov"] 6 | gem.email = ["dmitry@krasnoukhov.com"] 7 | gem.description = gem.summary = "Additional sidekiq middleware" 8 | gem.homepage = "http://github.com/krasnoukhov/sidekiq-middleware" 9 | 10 | gem.files = `git ls-files`.split($\) 11 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 12 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 13 | gem.name = "sidekiq-middleware" 14 | gem.require_paths = ["lib"] 15 | gem.version = Sidekiq::Middleware::VERSION 16 | 17 | gem.add_dependency 'sidekiq', '>= 2.12.4' 18 | gem.add_development_dependency 'rake' 19 | gem.add_development_dependency 'bundler', '~> 1.0' 20 | gem.add_development_dependency 'minitest', '~> 3' 21 | gem.add_development_dependency 'timecop' 22 | end 23 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'test' 2 | if ENV.has_key?('SIMPLECOV') 3 | require 'simplecov' 4 | SimpleCov.start 5 | end 6 | 7 | require 'coveralls' 8 | Coveralls.wear! 9 | 10 | require 'minitest/unit' 11 | require 'minitest/pride' 12 | require 'minitest/autorun' 13 | 14 | require 'celluloid' 15 | Celluloid.logger = nil 16 | 17 | require 'sidekiq' 18 | require 'sidekiq/cli' 19 | require 'sidekiq/processor' 20 | require 'sidekiq/util' 21 | require 'sidekiq-middleware' 22 | Sidekiq.logger.level = Logger::ERROR 23 | 24 | require 'sidekiq/redis_connection' 25 | REDIS = Sidekiq::RedisConnection.create(:url => "redis://localhost/15", :namespace => 'testy') -------------------------------------------------------------------------------- /test/test_core_ext.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'sidekiq-middleware' 3 | 4 | class TestCoreExt < MiniTest::Unit::TestCase 5 | describe 'for an empty array' do 6 | it 'should be an empty hash' do 7 | assert_equal({}, {:foo => "bar"}.slice([])) 8 | end 9 | end 10 | 11 | describe 'for items not in the hash' do 12 | it 'should be an empty hash' do 13 | assert_equal({}, {:foo => "bar", :foobar => "baz"}.slice(:baz, :foobaz)) 14 | end 15 | end 16 | 17 | describe 'for items in the hash' do 18 | it 'should be the attributes' do 19 | assert_equal({:foo => "bar"}, {:foo => "bar", :foobar => "baz"}.slice(:foo)) 20 | end 21 | end 22 | 23 | describe 'for keys in the hash' do 24 | it 'should be the attributes' do 25 | assert_equal({:foo => nil}, {:foo => nil, :foobar => "baz"}.slice(:foo)) 26 | end 27 | end 28 | 29 | describe 'when all items are in the hash' do 30 | it 'should be the hash' do 31 | assert_equal({:foo => "bar", :foobar => "baz"}, {:foo => "bar", :foobar => "baz"}.slice(:foo, :foobar)) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_unique_jobs.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'timecop' 3 | require 'sidekiq/client' 4 | require 'sidekiq/worker' 5 | require 'sidekiq/processor' 6 | require 'sidekiq-middleware' 7 | 8 | class TestUniqueJobs < MiniTest::Unit::TestCase 9 | describe 'with real redis' do 10 | before do 11 | Celluloid.boot 12 | @boss = MiniTest::Mock.new 13 | @processor = ::Sidekiq::Processor.new(@boss) 14 | 15 | Sidekiq.redis = REDIS 16 | Sidekiq.redis {|c| c.flushdb } 17 | end 18 | 19 | UnitOfWork = Struct.new(:queue, :message) do 20 | def acknowledge 21 | # nothing to do 22 | end 23 | 24 | def queue_name 25 | queue 26 | end 27 | 28 | def requeue 29 | # nothing to do 30 | end 31 | end 32 | 33 | describe 'when unique option is disabled (unique: false)' do 34 | class NotUniqueWorker 35 | include Sidekiq::Worker 36 | sidekiq_options queue: :not_unique_queue, unique: false 37 | 38 | def perform(x) 39 | end 40 | end 41 | 42 | it 'duplicates messages' do 43 | 5.times { NotUniqueWorker.perform_async('args') } 44 | assert_equal 5, Sidekiq.redis { |c| c.llen('queue:not_unique_queue') } 45 | end 46 | 47 | it 'duplicates scheduled messages' do 48 | at = Time.now.to_f + 10 49 | 5.times { Sidekiq::Client.push('class' => NotUniqueWorker, 'args' => ['args'], 'at' => at) } 50 | assert_equal 5, Sidekiq.redis { |c| c.zcard('schedule') } 51 | end 52 | end 53 | 54 | describe 'when unique option is enabled (unique: true)' do 55 | class UniqueWorker 56 | include Sidekiq::Worker 57 | sidekiq_options queue: :unique_queue, unique: true 58 | 59 | def perform(x) 60 | end 61 | end 62 | 63 | it 'does not duplicate messages' do 64 | 5.times { UniqueWorker.perform_async('args') } 65 | assert_equal 1, Sidekiq.redis { |c| c.llen('queue:unique_queue') } 66 | end 67 | 68 | it 'discards non critical information about the message' do 69 | 5.times { Sidekiq::Client.push('class' => UniqueWorker, 'args' => ['critical'], 'sent_at' => Time.now.to_f, 'non' => 'critical') } 70 | assert_equal 1, Sidekiq.redis { |c| c.llen('queue:unique_queue') } 71 | end 72 | 73 | it 'duplicates scheduled messages' do 74 | at = Time.now.to_f + 10 75 | 5.times { Sidekiq::Client.push('class' => UniqueWorker, 'args' => ['args'], 'at' => at) } 76 | assert_equal 5, Sidekiq.redis { |c| c.zcard('schedule') } 77 | end 78 | end 79 | 80 | describe 'when unique option is enabled (unique: :all)' do 81 | class UniqueScheduledWorker 82 | include Sidekiq::Worker 83 | sidekiq_options queue: :unique_scheduled_queue, unique: :all 84 | 85 | def perform(x) 86 | end 87 | end 88 | 89 | it 'does not duplicate scheduled messages' do 90 | 5.times { |t| UniqueScheduledWorker.perform_in((t+1)*60, 'args') } 91 | assert_equal 1, Sidekiq.redis { |c| c.zcard('schedule') } 92 | end 93 | 94 | it 'does correctly handle adding to the worker queue when scheduled' do 95 | start_time = Time.now - 60 96 | queue_time = start_time + 30 97 | Timecop.travel start_time do 98 | UniqueScheduledWorker.perform_at queue_time, 'x' 99 | end 100 | assert_equal 1, Sidekiq.redis { |c| c.zcard('schedule') } 101 | assert_equal 0, Sidekiq.redis { |c| c.llen('queue:unique_scheduled_queue') } 102 | Sidekiq::Scheduled::Poller.new.poll 103 | assert_equal 0, Sidekiq.redis { |c| c.zcard('schedule') } 104 | assert_equal 1, Sidekiq.redis { |c| c.llen('queue:unique_scheduled_queue') } 105 | end 106 | 107 | it 'allows the job to be re-scheduled after processing' do 108 | # Schedule 109 | 5.times { |t| UniqueScheduledWorker.perform_in((t+1)*60, 'args') } 110 | assert_equal 1, Sidekiq.redis { |c| c.zcard('schedule') } 111 | 112 | # Process 113 | msg = Sidekiq.dump_json('class' => UniqueScheduledWorker.to_s, 'queue' => 'unique_scheduled_queue', 'args' => ['args']) 114 | actor = MiniTest::Mock.new 115 | actor.expect(:processor_done, nil, [@processor]) 116 | actor.expect(:real_thread, nil, [nil, Celluloid::Thread]) 117 | 2.times { @boss.expect(:async, actor, []) } 118 | work = UnitOfWork.new('default', msg) 119 | @processor.process(work) 120 | 121 | # Re-schedule 122 | 5.times { |t| UniqueScheduledWorker.perform_in((t+1)*60, 'args') } 123 | assert_equal 2, Sidekiq.redis { |c| c.zcard('schedule') } 124 | end 125 | end 126 | 127 | describe 'when unique and manual options are enabled (unique: :all, manual: true)' do 128 | class CustomUniqueWorker 129 | include Sidekiq::Worker 130 | sidekiq_options queue: :custom_unique_queue, unique: :all, manual: true 131 | 132 | def self.lock(id, unlock) 133 | "custom:unique:lock:#{id}" 134 | end 135 | 136 | def self.unlock!(id, unlock) 137 | lock = self.lock(id, unlock) 138 | Sidekiq.redis { |conn| conn.del(lock) } 139 | end 140 | 141 | def perform(id, unlock) 142 | self.class.unlock!(id, unlock) if unlock 143 | CustomUniqueWorker.perform_in(60, id, unlock) 144 | end 145 | end 146 | 147 | it 'does not duplicate messages with custom unique lock key' do 148 | 5.times { CustomUniqueWorker.perform_async('args', false) } 149 | assert_equal 1, Sidekiq.redis { |c| c.llen('queue:custom_unique_queue') } 150 | job = Sidekiq.load_json Sidekiq.redis { |c| c.lpop('queue:custom_unique_queue') } 151 | assert job['jid'] 152 | assert_equal job['jid'], Sidekiq.redis { |c| c.get('custom:unique:lock:args') } 153 | end 154 | 155 | it 'does not allow the job to be duplicated when processing job' do 156 | 5.times { 157 | msg = Sidekiq.dump_json('class' => CustomUniqueWorker.to_s, 'args' => ['something', false]) 158 | actor = MiniTest::Mock.new 159 | actor.expect(:processor_done, nil, [@processor]) 160 | actor.expect(:real_thread, nil, [nil, Celluloid::Thread]) 161 | 2.times { @boss.expect(:async, actor, []) } 162 | work = UnitOfWork.new('default', msg) 163 | @processor.process(work) 164 | } 165 | assert_equal 1, Sidekiq.redis { |c| c.zcard('schedule') } 166 | end 167 | 168 | it 'discards non critical information about the message' do 169 | 5.times {|i| 170 | msg = Sidekiq.dump_json('class' => CustomUniqueWorker.to_s, 'args' => ['something', false], 'sent_at' => (Time.now + i*60).to_f) 171 | actor = MiniTest::Mock.new 172 | actor.expect(:processor_done, nil, [@processor]) 173 | actor.expect(:real_thread, nil, [nil, Celluloid::Thread]) 174 | 2.times { @boss.expect(:async, actor, []) } 175 | work = UnitOfWork.new('default', msg) 176 | @processor.process(work) 177 | } 178 | assert_equal 1, Sidekiq.redis { |c| c.zcard('schedule') } 179 | end 180 | 181 | it 'allows a job to be rescheduled when processing using unlock' do 182 | 5.times { 183 | msg = Sidekiq.dump_json('class' => CustomUniqueWorker.to_s, 'args' => ['something', true]) 184 | actor = MiniTest::Mock.new 185 | actor.expect(:processor_done, nil, [@processor]) 186 | actor.expect(:real_thread, nil, [nil, Celluloid::Thread]) 187 | 2.times { @boss.expect(:async, actor, []) } 188 | work = UnitOfWork.new('default', msg) 189 | @processor.process(work) 190 | } 191 | assert_equal 5, Sidekiq.redis { |c| c.zcard('schedule') } 192 | end 193 | end 194 | 195 | describe 'when the Worker is not defined' do 196 | it 'does not crash' do 197 | Sidekiq::Client.push( 198 | 'queue' => 'default', 199 | 'class' => 'UndefinedWorker', 200 | 'args' => ['no_argument']) 201 | end 202 | end 203 | end 204 | end 205 | --------------------------------------------------------------------------------