Git Product home page Git Product logo

sidekiq-lock's Introduction

Sidekiq::Lock

Code Climate Gem Version

Note: This is a complete piece of software, it should work across all future sidekiq & ruby versions.

Redis-based simple locking mechanism for sidekiq. Uses SET command introduced in Redis 2.6.16.

It can be handy if you push a lot of jobs into the queue(s), but you don't want to execute specific jobs at the same time - it provides a lock method that you can use in whatever way you want.

Installation

This gem requires at least:

  • redis 2.6.12
  • sidekiq 6

Add this line to your application's Gemfile:

gem 'sidekiq-lock'

And then execute:

$ bundle

Usage

Sidekiq-lock is a middleware/module combination, let me go through my thought process here :).

In your worker class include Sidekiq::Lock::Worker module and provide lock attribute inside sidekiq_options, for example:

class Worker
  include Sidekiq::Worker
  include Sidekiq::Lock::Worker

  # static lock that expires after one second
  sidekiq_options lock: { timeout: 1000, name: 'lock-worker' }

  def perform
    # ...
  end
end

What will happen is:

  • middleware will setup a Sidekiq::Lock::RedisLock object under Thread.current[Sidekiq::Lock::THREAD_KEY] (it should work in most use cases without any problems - but it's configurable, more below) - assuming you provided lock options, otherwise it will do nothing, just execute your worker's code

  • Sidekiq::Lock::Worker module provides a lock method that just simply points to that thread variable, just as a convenience

So now in your worker class you can call (whenever you need):

  • lock.acquire! - will try to acquire the lock, if returns false on failure (that means some other process / thread took the lock first)
  • lock.acquired? - set to true when lock is successfully acquired
  • lock.release! - deletes the lock (only if it's: acquired by current thread and not already expired)

Lock options

sidekiq_options lock will accept static values or Proc that will be called on argument(s) passed to perform method.

  • timeout - specified expire time, in milliseconds
  • name - name of the redis key that will be used as lock name
  • value - (optional) value of the lock, if not provided it's set to random hex

Dynamic lock example:

class Worker
  include Sidekiq::Worker
  include Sidekiq::Lock::Worker
  sidekiq_options lock: {
    timeout: proc { |user_id, timeout| timeout * 2 },
    name:    proc { |user_id, timeout| "lock:peruser:#{user_id}" },
    value:   proc { |user_id, timeout| "#{user_id}" }
  }

  def perform(user_id, timeout)
    # ...
    # do some work
    # only at this point I want to acquire the lock
    if lock.acquire!
      begin
        # I can do the work
        # ...
      ensure
        # You probably want to manually release lock after work is done
        # This method can be safely called even if lock wasn't acquired
        # by current worker (thread). For more references see RedisLock class
        lock.release!
      end
    else
      # reschedule, raise an error or do whatever you want
    end
  end
end

Just be sure to provide valid redis key as a lock name.

Customizing lock method name

You can change lock to something else (globally) in sidekiq server configuration:

Sidekiq.configure_server do |config|
  config.lock_method = :redis_lock
end

Customizing lock container

If you would like to change default behavior of storing lock instance in Thread.current for whatever reason you can do that as well via server configuration:

# Any thread-safe class that implements .fetch and .store methods will do
class CustomStorage
  def fetch
    # returns stored lock instance
  end
  
  def store(lock_instance)
    # store lock
  end
end

Sidekiq.configure_server do |config|
  config.lock_container = CustomStorage.new
end

Inline testing

As you know middleware is not invoked when testing jobs inline, you can require in your test/spec helper file sidekiq/lock/testing/inline to include two methods that will help you setting / clearing up lock manually:

  • set_sidekiq_lock(worker_class, payload) - note: payload should be an array of worker arguments
  • clear_sidekiq_lock

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

sidekiq-lock's People

Contributors

rwojsznis avatar stympy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

sidekiq-lock's Issues

Unsupported command argument type: TrueClass

I feel like im going crazy, what is wrong with this code?

[1] pry(#<CampaignBatchTestJob>)> lock.acquire!
TypeError: Unsupported command argument type: TrueClass
class CampaignBatchTestJob

  include Sidekiq::Job
  include Sidekiq::Lock::Worker

  sidekiq_options queue: "default", retry: 0, lock: {
    name: proc {|id| "campaign_batch_test_job_#{id}"},
    timeout: 10.minutes.to_i * 1000, # millseconds
  }

  def perform(id, duping = false)
    binding.pry
    return unless lock.acquire!

    sidekiq (7.2.0)
      concurrent-ruby (< 2)
      connection_pool (>= 2.3.0)
      rack (>= 2.2.4)
      redis-client (>= 0.14.0)
    sidekiq-lock (0.6.0)
      redis (>= 3.0.5)
      sidekiq (>= 2.14.0)

patch to work with ActiveJob

Hi,

Thank you for making this gem.

I spent a little time getting it to work with ActiveJob (very small change at the end). I'd be happy to put up a PR for it, but I'm not sure how you would like to do the check. Right now my change exists as a small monkeypatch:

module Sidekiq
  module Lock
    class Middleware
      def call(_worker, msg, _queue)
        klass = msg.fetch('wrapped').constantize
        options = klass.get_sidekiq_options['lock']

        setup_lock(options, msg['args']) unless options.nil?

        yield
      end
    end
  end
end

The issue is that the job class for ActiveJob is always a ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper, rather than the worker class directly, meaning that when it tried to get_sidekiq_options['lock'], they were unavailable. This patch merely gets to the correct class where the options actually exist.

The jobs themselves can be used as documented (make sure to include Sidekiq::Lock::Worker in the job class that needs locking functionality)

Is this behavior desirable? If so, how would you prefer that the conditional check be made?

Sidekiq inline test helper

It would be cool to have some kind of test-helper if you are testing your sidekiq jobs inline (in one way or another).

Support inline locking too

I think Sidekiq lock should still allow workers to be run inline. Locking behavior should be the same

via Worker.new.perform(args)

As of right now lock is nil.

I'm implementing this functionality now. Hopefully you wont mind it being merged back in

Thanks

Rails fails on start

Error

Gem Load Error is: undefined method `configure_server' for Sidekiq:Module

raises on rails startup. I think it's because gem missing require "sidekiq" line.

Simple example does not work?

I'm using this as such:

class BatchIntegrationEventsJob
  include Sidekiq::Job
  include Sidekiq::Lock::Worker

  sidekiq_options({
    retry: 0,
    lock: {timeout: 30.seconds.to_i, name: self.class.to_s}
  })

  def perform
    return unless lock.acquire!
    sleep 10
  ensure
    lock.release!
  end
end

If I enqueue this job once per second I see multiple jobs running at same time, what am I doing wrong?

Define custom lock method name

lock - as a method name - should be configurable globally via sidekiq.configure_... call (don't want to do it per worker, it seems like a overkill).

Is there a way to use n-locks?

I wonder if I can use multiple locks? The main reason - to be able to limit how many same job workers can go in parallel. So say I want to limit 1 job to be run concurrently with only 2 worker threads - can I somehow set 2 locks and then acquire first or second?

Proc method doesnt work for timeout

When trying to use dynamic locks based on passed in parameters to the worker

timeout: proc { |user_id, timeout| timeout * 2 },

I end up with an error before the worker's perform method can be called:

 WARN: undefined method `*' for nil:NilClass

But it seems to work fine if I keep the timeout value equal to a constant and just use the proc on the lock.

sidekiq_options lock: {
      timeout: 1200000,
      name:    proc { |message_id, timeout| "lock:attachmentmsgid:#{message_id}" }
    }

def perform(message_id, timeout = 1200000)
    Rails.logger = Sidekiq::Logging.logger
    if lock.acquire!
    begin

undefined method for 'lock'

Hi,

I just added the gem and I'm getting, NameError: undefined local variable or method `lock'
I see the gem in the gemfile.lock list.
I added the following to my worker

include Sidekiq::Lock::Worker
sidekiq_options lock: { timeout: 1000, name: 'lock-worker' }

Any idea why it may not be working?

"Infinite" timeout

Hello,

Is there a way to make an infinite timeout? I have some tasks that can take hours, I don't want the lock to be released until it's finished :/

Also, maybe it wouldn't hurt to emphasize that you have to lock.release! after your worker is done otherwise new workers won't aquire the lock until timeout is reached.

Test with Sidekiq 3.0

I didn't had time to test it with new sidekiq, creating this issue so I won't forget to do this this week(end) ;].

sidekiq_options lock conflicts with sidekiq-unique-jobs gem lock option

Hello,

Gem sidekiq-lock and sidekiq-unique-jobs use the same option
sidekiq_options lock: :until_executing
sidekiq_options lock: { timeout: 1000, name: 'project_status_lock' }
which causes an error:
NoMethodError: undefined method to_sym for {:timeout=>10000, :name=>"project_status_lock"}:Hash Did you mean? to_s, to_set /usr/local/bundle/gems/sidekiq-unique-jobs-6.0.13/lib/sidekiq_unique_jobs/options_with_fallback.rb:52:in lock_class'
because sidekiq-unique-jobs does not expect Hash as lock option value which need to be defined as hash in sidekiq-lock.

So far, I have not found a way to make these two gems work together.

Sidekiq lock not working

I define a test job

class Job
  include Sidekiq::Worker
  include Sidekiq::Lock::Worker

  sidekiq_options queue: :test, retry: true, unique: :until_executing, lock: {
    timeout: 1.hour,
    name:    'index_lock',
    value:   proc { |klass, id, options= {}| "#{klass}:#{id}" }
  }

  def perform(klass, id, options = {})
    a = lock.acquire!
    puts a

    if a
      begin
        puts "process #{klass}, #{id}, #{options}"
        sleep 5.minutes
      ensure
        puts "release lock #{klass}:#{id}"
        lock.release!
      end
    else
      Job.perform_in 30.seconds, klass, id, options
    end
  ensure
    # GC.start
  end
end

Then I run

Job.perform_async 'a', 1
Job.perform_async 'a', 2
Job.perform_async 'a', 3
Job.perform_async 'a', 4

The log show

true
process a, 1, {}
true
process a, 1, {}
false
true
process a, 1, {}
true
process a, 1, {}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.