├── .rspec ├── lib └── sidekiq │ ├── postpone │ ├── version.rb │ └── core_ext.rb │ └── postpone.rb ├── Gemfile ├── bin ├── setup ├── console ├── rake ├── rspec ├── bundler └── appraisal ├── gemfiles ├── sidekiq_5.0.gemfile ├── sidekiq_5.1.gemfile ├── sidekiq_5.2.gemfile ├── sidekiq_6.0.gemfile ├── sidekiq_6.1.gemfile ├── sidekiq_6.2.gemfile ├── sidekiq_6.3.gemfile ├── sidekiq_6.4.gemfile ├── sidekiq_6.5.gemfile ├── sidekiq_7.0.gemfile └── sidekiq_master.gemfile ├── .gitignore ├── Rakefile ├── spec ├── support │ └── sidekiq_helper.rb ├── spec_helper.rb ├── sidekiq-postpone_testing_integration_spec.rb └── sidekiq-postpone_spec.rb ├── Appraisals ├── .travis.yml ├── sidekiq-postpone.gemspec ├── LICENSE ├── .github └── workflows │ └── rspec.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/sidekiq/postpone/version.rb: -------------------------------------------------------------------------------- 1 | module Sidekiq 2 | class Postpone 3 | VERSION = "0.3.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in sidekiq-postpone.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 5.0.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_5.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 5.1.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_5.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 5.2.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 6.0.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 6.1.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_6.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 6.2.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_6.3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 6.3.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_6.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 6.4.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_6.5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 6.5.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", "~> 7.0.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /gemfiles/*.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | /vendor/bundle 12 | -------------------------------------------------------------------------------- /gemfiles/sidekiq_master.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "sidekiq", :github => "mperham/sidekiq" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /lib/sidekiq/postpone/core_ext.rb: -------------------------------------------------------------------------------- 1 | class Sidekiq::Postpone 2 | module CoreExt 3 | def raw_push(payloads) 4 | postpone = Thread.current[:sidekiq_postpone] 5 | if postpone 6 | postpone.push(payloads) 7 | else 8 | super 9 | end 10 | end 11 | end 12 | 13 | Sidekiq::Client.prepend CoreExt 14 | end 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "sidekiq/postpone" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |rspec| 5 | rspec.exclude_pattern = 'spec/sidekiq-postpone_testing_integration_spec.rb' 6 | end 7 | 8 | namespace :spec do 9 | RSpec::Core::RakeTask.new(:sidekiq_testing_integration) do |rspec| 10 | rspec.pattern = 'spec/sidekiq-postpone_testing_integration_spec.rb' 11 | end 12 | end 13 | 14 | task :default => :spec 15 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rake' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rake", "rake") 18 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rspec' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rspec-core", "rspec") 18 | -------------------------------------------------------------------------------- /bin/bundler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'bundler' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("bundler", "bundler") 18 | -------------------------------------------------------------------------------- /spec/support/sidekiq_helper.rb: -------------------------------------------------------------------------------- 1 | module SidekiqHelper 2 | def sidekiq_worker(name, &block) 3 | klass = Class.new do 4 | include Sidekiq::Worker 5 | end 6 | Object.const_set(name, klass) 7 | klass.class_eval(&block) if block 8 | (@sidekiq_workers ||= []) << klass 9 | klass 10 | end 11 | 12 | def remove_sidekiq_workers 13 | (@sidekiq_workers || []).each do |klass| 14 | Object.send(:remove_const, klass.name) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/appraisal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'appraisal' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("appraisal", "appraisal") 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'sidekiq/postpone' 3 | require 'support/sidekiq_helper' 4 | 5 | require 'sidekiq/api' 6 | require 'sidekiq/redis_connection' 7 | redis_url = ENV['REDIS_URL'] || 'redis://localhost/15' 8 | 9 | Sidekiq.configure_client do |config| 10 | config.redis = { :url => redis_url } 11 | end 12 | 13 | RSpec.configure do |c| 14 | c.include SidekiqHelper 15 | c.after(:each) { remove_sidekiq_workers } 16 | c.after(:each) do 17 | Sidekiq::RetrySet.new.clear 18 | Sidekiq::ScheduledSet.new.clear 19 | Sidekiq::DeadSet.new.clear 20 | Sidekiq::Queue.all.map(&:clear) 21 | end 22 | 23 | c.order = :random 24 | Kernel.srand c.seed 25 | end 26 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'sidekiq-5.0' do 2 | gem 'sidekiq', '~> 5.0.0' 3 | end 4 | 5 | appraise 'sidekiq-5.1' do 6 | gem 'sidekiq', '~> 5.1.0' 7 | end 8 | 9 | appraise 'sidekiq-5.2' do 10 | gem 'sidekiq', '~> 5.2.0' 11 | end 12 | 13 | appraise 'sidekiq-6.0' do 14 | gem 'sidekiq', '~> 6.0.0' 15 | end 16 | 17 | appraise 'sidekiq-6.1' do 18 | gem 'sidekiq', '~> 6.1.0' 19 | end 20 | 21 | appraise 'sidekiq-6.2' do 22 | gem 'sidekiq', '~> 6.2.0' 23 | end 24 | 25 | appraise 'sidekiq-6.3' do 26 | gem 'sidekiq', '~> 6.3.0' 27 | end 28 | 29 | appraise 'sidekiq-6.4' do 30 | gem 'sidekiq', '~> 6.4.0' 31 | end 32 | 33 | appraise 'sidekiq-6.5' do 34 | gem 'sidekiq', '~> 6.5.0' 35 | end 36 | 37 | appraise 'sidekiq-7.0' do 38 | gem 'sidekiq', '~> 7.0.0' 39 | end 40 | 41 | appraise 'sidekiq-master' do 42 | gem 'sidekiq', github: 'mperham/sidekiq' 43 | end 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | services: 4 | - redis-server 5 | rvm: 6 | - 2.1 7 | - 2.2 8 | - 2.3 9 | - 2.4 10 | - 2.5 11 | gemfile: 12 | - gemfiles/sidekiq_3.0.gemfile 13 | - gemfiles/sidekiq_3.1.gemfile 14 | - gemfiles/sidekiq_3.2.gemfile 15 | - gemfiles/sidekiq_3.3.gemfile 16 | - gemfiles/sidekiq_3.4.gemfile 17 | - gemfiles/sidekiq_3.5.gemfile 18 | - gemfiles/sidekiq_4.0.gemfile 19 | - gemfiles/sidekiq_4.1.gemfile 20 | - gemfiles/sidekiq_4.2.gemfile 21 | - gemfiles/sidekiq_5.0.gemfile 22 | - gemfiles/sidekiq_5.1.gemfile 23 | - gemfiles/sidekiq_master.gemfile 24 | matrix: 25 | fast_finish: true 26 | exclude: 27 | - gemfile: gemfiles/sidekiq_5.1.gemfile 28 | rvm: 2.1 29 | - gemfile: gemfiles/sidekiq_5.0.gemfile 30 | rvm: 2.1 31 | - gemfile: gemfiles/sidekiq_master.gemfile 32 | 33 | before_install: gem install bundler -v 1.15 34 | script: 35 | - bundle exec rake spec 36 | - bundle exec rake spec:sidekiq_testing_integration 37 | -------------------------------------------------------------------------------- /sidekiq-postpone.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'sidekiq/postpone/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "sidekiq-postpone" 8 | spec.version = Sidekiq::Postpone::VERSION 9 | spec.authors = ["Vladimir Kochnev"] 10 | spec.email = ["hashtable@yandex.ru"] 11 | 12 | spec.summary = %q{Bulk-pushes jobs to Sidekiq when you need it to.} 13 | spec.homepage = "https://github.com/marshall-lee/sidekiq-postpone" 14 | 15 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 16 | spec.bindir = "exe" 17 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "sidekiq", ">= 5", "< 8" 21 | 22 | spec.add_development_dependency "bundler", "< 3" 23 | spec.add_development_dependency "rake", "~> 13.0" 24 | spec.add_development_dependency "rspec", "~> 3.0" 25 | spec.add_development_dependency "appraisal", "~> 2.1.0" 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Vladimir Kochnev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/rspec.yml: -------------------------------------------------------------------------------- 1 | name: RSpec 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | services: 13 | redis: 14 | image: redis:7.0 15 | ports: 16 | - 6379/tcp 17 | options: >- 18 | --health-cmd "redis-cli ping" 19 | --health-interval 10s 20 | --health-timeout 5s 21 | --health-retries 5 22 | strategy: 23 | matrix: 24 | ruby: 25 | - "2.7" 26 | - "3.0" 27 | - "3.1" 28 | - "3.2" 29 | gemfile: 30 | - gemfiles/sidekiq_5.0.gemfile 31 | - gemfiles/sidekiq_5.1.gemfile 32 | - gemfiles/sidekiq_5.2.gemfile 33 | - gemfiles/sidekiq_6.0.gemfile 34 | - gemfiles/sidekiq_6.1.gemfile 35 | - gemfiles/sidekiq_6.2.gemfile 36 | - gemfiles/sidekiq_6.3.gemfile 37 | - gemfiles/sidekiq_6.4.gemfile 38 | - gemfiles/sidekiq_6.5.gemfile 39 | - gemfiles/sidekiq_7.0.gemfile 40 | env: 41 | BUNDLE_GEMFILE: ${{ format('{0}/{1}', github.workspace, matrix.gemfile) }} 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v3 45 | - name: Setup ruby 46 | uses: ruby/setup-ruby@v1 47 | with: 48 | ruby-version: ${{ matrix.ruby }} 49 | bundler-cache: true 50 | - name: RSpec 51 | run: bundle exec rake spec 52 | env: 53 | REDIS_URL: redis://localhost:${{ job.services.redis.ports[6379] }} 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/marshall-lee/sidekiq-postpone.svg?branch=master)](https://travis-ci.org/marshall-lee/sidekiq-postpone) 2 | 3 | # sidekiq-postpone 4 | 5 | Bulk-pushes tasks to Redis when you need it to. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'sidekiq-postpone' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install sidekiq-postpone 22 | 23 | ## Usage 24 | 25 | Typical use-case is with database transactions: 26 | 27 | ```ruby 28 | Sidekiq::Postpone.wrap do 29 | ActiveRecord::Base.transaction do 30 | # ... 31 | post = Post.create(params) 32 | ImageProcess.perform_async(post.image) 33 | end 34 | end 35 | # In fact, ImageProcess job will be pushed to the queue only after `wrap { ... }` block finishes. 36 | ``` 37 | 38 | ## Development 39 | 40 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 41 | 42 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 43 | 44 | ## Contributing 45 | 46 | Bug reports and pull requests are welcome on GitHub at https://github.com/marshall-lee/sidekiq-postpone. 47 | 48 | -------------------------------------------------------------------------------- /lib/sidekiq/postpone.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sidekiq" 4 | require "sidekiq/postpone/version" 5 | require "sidekiq/postpone/core_ext" 6 | 7 | class Sidekiq::Postpone 8 | def initialize(*client_args) 9 | @client_args = client_args 10 | setup_queues 11 | setup_schedule 12 | end 13 | 14 | def wrap(join_parent: true, flush: true) 15 | enter! 16 | begin 17 | yield self 18 | rescue 19 | clear! 20 | raise 21 | end.tap do 22 | if join_parent && (parent = Thread.current[:sidekiq_postpone_stack][-2]) 23 | join!(parent) 24 | elsif flush 25 | @flush_on_leave = true 26 | end 27 | end 28 | ensure 29 | leave! 30 | end 31 | 32 | def self.wrap(*client_args, **kwargs, &block) 33 | new(*client_args).wrap(**kwargs, &block) 34 | end 35 | 36 | def push(payloads) 37 | if payloads.first['at'] 38 | @schedule.concat(payloads) 39 | else 40 | q = payloads.first['queue'] 41 | @queues[q].concat(payloads) 42 | end 43 | end 44 | 45 | def clear! 46 | @queues.clear 47 | @schedule.clear 48 | end 49 | 50 | def flush! 51 | current_postpone = Thread.current[:sidekiq_postpone] 52 | return if empty? 53 | 54 | Thread.current[:sidekiq_postpone] = nil # activate real raw_push 55 | 56 | client = Sidekiq::Client.new(*@client_args) 57 | 58 | @queues.each_value { |item| client.raw_push(item) } 59 | client.raw_push(@schedule) unless @schedule.empty? 60 | 61 | clear! 62 | ensure 63 | Thread.current[:sidekiq_postpone] = current_postpone 64 | end 65 | 66 | def join!(other) 67 | return if empty? 68 | 69 | @queues.each do |name, payloads| 70 | other.queues[name].concat(payloads) 71 | end 72 | other.schedule.concat(@schedule) 73 | 74 | clear! 75 | end 76 | 77 | def empty? 78 | @queues.empty? && @schedule.empty? 79 | end 80 | 81 | def all_jobs 82 | [*@queues.values.flatten(1), *@schedule] 83 | end 84 | 85 | def jids 86 | all_jobs.map! { |j| j['jid'] } 87 | end 88 | 89 | protected 90 | 91 | attr_reader :queues, :schedule 92 | 93 | private 94 | 95 | def enter! 96 | if @entered 97 | raise 'Sidekiq::Postpone#wrap is not re-enterable on the same instance' 98 | else 99 | @entered = true 100 | end 101 | Thread.current[:sidekiq_postpone_stack] ||= [] 102 | Thread.current[:sidekiq_postpone_stack].push(self) 103 | Thread.current[:sidekiq_postpone] = self 104 | end 105 | 106 | def leave! 107 | Thread.current[:sidekiq_postpone_stack].pop 108 | head = Thread.current[:sidekiq_postpone_stack].last 109 | Thread.current[:sidekiq_postpone] = head 110 | @entered = false 111 | if @flush_on_leave 112 | @flush_on_leave = false 113 | flush! 114 | end 115 | end 116 | 117 | def setup_queues 118 | @queues = Hash.new do |hash, key| 119 | hash[key] = [] 120 | end 121 | end 122 | 123 | def setup_schedule 124 | @schedule = [] 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/sidekiq-postpone_testing_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'sidekiq/testing' 3 | 4 | describe Sidekiq::Postpone do 5 | let(:client_args) { nil } 6 | let(:postponer) { described_class.new(client_args) } 7 | 8 | before do 9 | sidekiq_worker(:Foo) 10 | 11 | sidekiq_worker(:Bar) do 12 | sidekiq_options queue: :bar 13 | end 14 | 15 | sidekiq_worker(:Baz) do 16 | sidekiq_options queue: :baz 17 | end 18 | end 19 | 20 | describe 'integration with Sidekiq::Worker#jobs' do 21 | before { Sidekiq::Worker.clear_all } 22 | 23 | context 'when inside a #wrap block' do 24 | it 'leaves the job list untouched' do 25 | postponer.wrap do 26 | Foo.perform_async 27 | expect(Foo.jobs.count).to eq 0 28 | end 29 | end 30 | end 31 | 32 | context 'when outside of #wrap block' do 33 | it 'adds a job to the list ' do 34 | expect { 35 | postponer.wrap do 36 | Foo.perform_async 37 | end 38 | }.to change { Foo.jobs.count }.by 1 39 | end 40 | end 41 | end 42 | 43 | describe 'integration with Sidekiq::Queues' do 44 | before do 45 | skip "Sidekiq #{Sidekiq::VERSION} lacks this feature" if Sidekiq::VERSION < '4.0.0' 46 | end 47 | 48 | let(:queue_foo) { Sidekiq::Queues['default'] } 49 | let(:queue_bar) { Sidekiq::Queues['bar'] } 50 | let(:queue_baz) { Sidekiq::Queues['baz'] } 51 | 52 | before { Sidekiq::Queues.clear_all } 53 | 54 | describe 'inside a #wrap block' do 55 | it 'does not push a job' do 56 | postponer.wrap do 57 | Foo.perform_async 58 | expect(queue_foo.size).to eq 0 59 | end 60 | end 61 | end 62 | 63 | describe 'outside of #wrap block' do 64 | it 'pushes a job to the queue' do 65 | expect { 66 | postponer.wrap do 67 | Foo.perform_async 68 | end 69 | }.to change { queue_foo.size }.by 1 70 | end 71 | 72 | it 'pushes jobs to specified queues' do 73 | postponer.wrap do 74 | Foo.perform_async 75 | Bar.perform_async 76 | Foo.perform_async 77 | Bar.perform_async 78 | Foo.perform_async 79 | Baz.perform_async 80 | Baz.perform_async 81 | Foo.perform_async 82 | Baz.perform_async 83 | end 84 | expect(queue_foo.size).to eq 4 85 | expect(queue_bar.size).to eq 2 86 | expect(queue_baz.size).to eq 3 87 | end 88 | end 89 | end 90 | 91 | describe 'integration with Sidekiq::Testing.inline!' do 92 | around { |ex| Sidekiq::Testing.inline!(&ex) } 93 | let(:side_effect) { { foo: 0 } } 94 | 95 | before do 96 | eff = side_effect 97 | Bar.class_eval do 98 | define_method :perform do 99 | eff[:foo] += 1 100 | end 101 | end 102 | Foo.class_eval do 103 | define_method :perform do 104 | Sidekiq::Postpone.wrap do 105 | 2.times { Bar.perform_async } 106 | end 107 | end 108 | end 109 | end 110 | 111 | it 'works well' do 112 | expect { 3.times { Foo.perform_async } } 113 | .to change { side_effect[:foo] }.by 6 114 | expect do 115 | Sidekiq::Postpone.wrap do 116 | expect { 3.times { Foo.perform_async } } 117 | .not_to(change { side_effect[:foo] }) 118 | end 119 | end.to change { side_effect[:foo] }.by 6 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/sidekiq-postpone_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Sidekiq::Postpone do 4 | let(:client_args) { nil } 5 | let(:postponer) { described_class.new(*client_args) } 6 | let(:another_postponer) { described_class.new(*client_args) } 7 | 8 | before do 9 | sidekiq_worker(:Foo) 10 | 11 | sidekiq_worker(:Bar) do 12 | sidekiq_options queue: :bar 13 | end 14 | 15 | sidekiq_worker(:Baz) do 16 | sidekiq_options queue: :baz 17 | end 18 | end 19 | 20 | def sidekiq_postpone_tls 21 | Thread.current[:sidekiq_postpone] 22 | end 23 | 24 | let(:queue_foo) { Sidekiq::Queue.new } 25 | let(:queue_bar) { Sidekiq::Queue.new('bar') } 26 | let(:queue_baz) { Sidekiq::Queue.new('baz') } 27 | let(:scheduled) { Sidekiq::ScheduledSet.new } 28 | 29 | describe '#wrap' do 30 | it 'returns a value of yield' do 31 | expect(postponer.wrap { :vova }).to eq(:vova) 32 | end 33 | 34 | it 'sets a tls variable inside a block' do 35 | postponer.wrap do 36 | expect(sidekiq_postpone_tls).to_not be nil 37 | end 38 | end 39 | 40 | it 'removes a tls variable outside of a block' do 41 | postponer.wrap do 42 | Foo.perform_async 43 | end 44 | expect(sidekiq_postpone_tls).to be nil 45 | end 46 | 47 | it 'removes a tls variable outside of a block even if exception is raised' do 48 | begin 49 | postponer.wrap do 50 | Foo.perform_async 51 | fail 52 | end 53 | rescue 54 | end 55 | expect(sidekiq_postpone_tls).to be nil 56 | end 57 | 58 | context 'with custom client args' do 59 | let(:client_args) { [:yo, :man] } 60 | 61 | it 'passes these args to Sidekiq::Client' do 62 | postponer.wrap do 63 | Foo.perform_async 64 | expect(Sidekiq::Client).to receive(:new).with(*client_args) { double(raw_push: nil) } 65 | end 66 | end 67 | end 68 | 69 | context 'with perform_async' do 70 | it 'pushes a job to the queue' do 71 | expect { 72 | postponer.wrap do 73 | Foo.perform_async 74 | end 75 | }.to change { queue_foo.size }.by 1 76 | end 77 | 78 | it 'pushes multiple jobs to the queue' do 79 | expect { 80 | postponer.wrap do 81 | Foo.perform_async 82 | Foo.perform_async 83 | end 84 | }.to change { queue_foo.size }.by 2 85 | end 86 | 87 | it 'pushes jobs to specified queues' do 88 | postponer.wrap do 89 | Foo.perform_async 90 | Bar.perform_async 91 | Foo.perform_async 92 | Bar.perform_async 93 | Foo.perform_async 94 | Baz.perform_async 95 | Baz.perform_async 96 | Foo.perform_async 97 | Baz.perform_async 98 | end 99 | expect(queue_foo.size).to eq 4 100 | expect(queue_bar.size).to eq 2 101 | expect(queue_baz.size).to eq 3 102 | end 103 | 104 | it 'does not push any job until block exits' do 105 | postponer.wrap do 106 | Foo.perform_async 107 | Bar.perform_async 108 | expect(queue_foo.size).to eq 0 109 | expect(queue_bar.size).to eq 0 110 | end 111 | end 112 | 113 | it 'does not push any job if error is raised' do 114 | expect { 115 | begin 116 | postponer.wrap do 117 | Foo.perform_async 118 | fail 119 | end 120 | rescue 121 | end 122 | }.not_to change { queue_foo.size } 123 | end 124 | end 125 | 126 | context 'with perform_in' do 127 | let(:at) { Time.now + 1 } 128 | 129 | it 'pushes a job to the scheduled set' do 130 | expect { 131 | postponer.wrap do 132 | Foo.perform_in(at) 133 | end 134 | }.to change { scheduled.size }.by 1 135 | end 136 | 137 | it 'pushes multiple jobs to the scheduled set' do 138 | expect { 139 | postponer.wrap do 140 | Foo.perform_in(at + 1) 141 | Bar.perform_in(at + 2) 142 | Foo.perform_in(at + 3) 143 | end 144 | }.to change { scheduled.size }.by 3 145 | end 146 | end 147 | 148 | it 'clears itself after flush' do 149 | postponer.wrap do 150 | Foo.perform_async 151 | Foo.perform_in(Time.now + 1) 152 | expect(postponer).not_to be_empty 153 | end 154 | expect(postponer).to be_empty 155 | end 156 | end 157 | 158 | describe '.wrap' do 159 | it 'creates a postpone object and calls #wrap on it' do 160 | postpone_double = double 161 | expect(Sidekiq::Postpone).to receive(:new) { postpone_double } 162 | expect(postpone_double).to receive(:wrap) 163 | 164 | Sidekiq::Postpone.wrap { } 165 | end 166 | 167 | it 'returns a value of a block' do 168 | expect(Sidekiq::Postpone.wrap { :vova }).to eq :vova 169 | end 170 | 171 | it 'supports nesting' do 172 | expect do 173 | Sidekiq::Postpone.wrap do 174 | Foo.perform_async 175 | expect { 176 | Sidekiq::Postpone.wrap(join_parent: false) do 177 | Bar.perform_async 178 | end 179 | }.to change { queue_bar.size }.by(1) 180 | expect { 181 | Sidekiq::Postpone.wrap(join_parent: true) do 182 | Bar.perform_async 183 | Bar.perform_in(Time.now + 1) 184 | end 185 | }.not_to change { queue_bar.size } 186 | Foo.perform_async 187 | Foo.perform_async 188 | end 189 | end.to change { queue_foo.size }.by(3) 190 | .and change { queue_bar.size }.by(2) 191 | .and change { scheduled.size }.by(1) 192 | end 193 | end 194 | 195 | describe '#flush!' do 196 | it 'works outside of #wrap block' do 197 | expect { 198 | postponer.wrap(flush: false) do 199 | Foo.perform_async 200 | end 201 | }.not_to change { queue_foo.size } 202 | expect { postponer.flush! }.to change { queue_foo.size }.by(1) 203 | end 204 | 205 | it 'keeps the current postpone' do 206 | postponer.wrap(flush: false) do 207 | expect(sidekiq_postpone_tls).to eq postponer 208 | expect do 209 | another_postponer.wrap(flush: false) do 210 | expect(sidekiq_postpone_tls).to eq another_postponer 211 | end 212 | another_postponer.flush! 213 | end.not_to(change { sidekiq_postpone_tls }) 214 | end 215 | end 216 | end 217 | 218 | describe '#clear!' do 219 | context 'with perform_async' do 220 | it 'does not pushe a job to the queue' do 221 | expect { 222 | Sidekiq::Postpone.wrap do |postponer| 223 | Foo.perform_async 224 | postponer.clear! 225 | end 226 | }.not_to change { queue_foo.size } 227 | end 228 | 229 | it 'supports nesting' do 230 | expect { 231 | Sidekiq::Postpone.wrap do |postponer| 232 | Foo.perform_async 233 | Sidekiq::Postpone.wrap(join_parent: true) do 234 | Foo.perform_async 235 | end 236 | postponer.clear! 237 | end 238 | }.not_to change { queue_foo.size } 239 | 240 | expect { 241 | Sidekiq::Postpone.wrap do 242 | Foo.perform_async 243 | Foo.perform_async 244 | Sidekiq::Postpone.wrap do |postponer| 245 | Foo.perform_async 246 | postponer.clear! 247 | end 248 | end 249 | }.to change { queue_foo.size }.by 2 250 | end 251 | end 252 | 253 | context 'with perform_in' do 254 | let(:at) { Time.now + 1 } 255 | 256 | it 'pushes a job to the scheduled set' do 257 | expect { 258 | Sidekiq::Postpone.wrap do |postponer| 259 | Foo.perform_in(at) 260 | postponer.clear! 261 | end 262 | }.not_to change { scheduled.size } 263 | end 264 | end 265 | end 266 | 267 | describe '#jids' do 268 | it 'returns all job ids' do 269 | Sidekiq::Postpone.wrap do |postponer| 270 | jid1 = Foo.perform_async 271 | jid2 = Foo.perform_in(Time.now + 1) 272 | expect(postponer.jids).to contain_exactly(jid1, jid2) 273 | end 274 | end 275 | end 276 | end 277 | --------------------------------------------------------------------------------