├── .rspec ├── lib ├── barrage │ ├── version.rb │ ├── generators.rb │ └── generators │ │ ├── timestamp.rb │ │ ├── pid.rb │ │ ├── sequence.rb │ │ ├── msec.rb │ │ ├── base.rb │ │ └── redis_worker_id.rb └── barrage.rb ├── .travis.yml ├── Rakefile ├── .gitignore ├── Guardfile ├── spec ├── spec_helper.rb ├── barrage │ └── generators │ │ ├── msec_spec.rb │ │ ├── sequence_spec.rb │ │ └── redis_worker_id_spec.rb └── barrage_spec.rb ├── Gemfile ├── CHANGELOG.md ├── LICENSE.txt ├── barrage.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/barrage/version.rb: -------------------------------------------------------------------------------- 1 | class Barrage 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/barrage/generators.rb: -------------------------------------------------------------------------------- 1 | class Barrage 2 | module Generators 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/barrage/generators/timestamp.rb: -------------------------------------------------------------------------------- 1 | class Barrage 2 | module Generators 3 | class Timestamp 4 | def initialize 5 | end 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.0.0 4 | - 2.1.8 5 | - 2.2.4 6 | - 2.3.0 7 | bundler_args: --jobs=2 8 | services: 9 | - redis-server 10 | script: bundle exec rake spec 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | 8 | desc "pry console" 9 | task :console do 10 | require "pry" 11 | require "barrage" 12 | binding.pry 13 | end 14 | -------------------------------------------------------------------------------- /.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 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | -------------------------------------------------------------------------------- /lib/barrage/generators/pid.rb: -------------------------------------------------------------------------------- 1 | require 'barrage/generators/base' 2 | 3 | class Barrage 4 | module Generators 5 | class Pid < Base 6 | def generate 7 | Process.pid & (2 ** length-1) 8 | end 9 | 10 | alias_method :current, :generate 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :rspec, 5 | cmd: "bundle exec rspec", 6 | all_after_pass: true, 7 | all_on_start: true do 8 | watch(%r{^spec/.+_spec\.rb$}) 9 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 10 | watch('spec/spec_helper.rb') { "spec" } 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'bundler/setup' 3 | require 'rspec/its' 4 | require 'delorean' 5 | 6 | require 'coveralls' 7 | Coveralls.wear! 8 | 9 | require 'barrage' 10 | 11 | RSpec.configure do |config| 12 | include Delorean 13 | config.filter_run focus: true 14 | config.run_all_when_everything_filtered = true 15 | end 16 | -------------------------------------------------------------------------------- /lib/barrage/generators/sequence.rb: -------------------------------------------------------------------------------- 1 | require 'barrage/generators/base' 2 | 3 | class Barrage 4 | module Generators 5 | class Sequence < Base 6 | def initialize(options) 7 | @sequence = 0 8 | super 9 | end 10 | 11 | def generate 12 | @sequence = (@sequence + 1) & (2 ** length - 1) 13 | end 14 | 15 | def current 16 | @sequence 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/barrage/generators/msec.rb: -------------------------------------------------------------------------------- 1 | require 'barrage/generators/base' 2 | 3 | class Barrage 4 | module Generators 5 | class Msec < Base 6 | self.required_options += %w(start_at) 7 | 8 | def generate 9 | ((Time.now.to_f * 1000).round - start_at) & (2 ** length - 1) 10 | end 11 | 12 | alias_method :current, :generate 13 | 14 | def start_at 15 | options["start_at"] 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in barrage.gemspec 4 | gemspec 5 | 6 | if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("2.2.2") 7 | gem "activesupport", "< 5.0.0" 8 | end 9 | 10 | if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("2.2.3") 11 | gem "listen", "< 3.1.0" 12 | end 13 | 14 | if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("2.2.5") 15 | gem "ruby_dep", "< 1.4.0" 16 | end 17 | 18 | gem "redis", ">= 4.0.0" 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.2.0 4 | 5 | * Compatible with Redis 5.0 6 | 7 | ## 0.1.1 8 | 9 | * Fix redis client disconnect 10 | 11 | ## 0.1.0 12 | 13 | * Fix RedisWorkerId's worker_id handling 14 | * Release old worker_id only if it is before ttl 15 | * Add required_options to generators 16 | 17 | ## 0.0.4 18 | 19 | * [redis_worker_id generator] Release current worker_id before reaching real ttl 20 | 21 | ## 0.0.3 22 | 23 | * Fix critical bug on redis_worker_id generator 24 | 25 | ## 0.0.2(yanked) 26 | 27 | * Thread-safety 28 | 29 | ## 0.0.1 - First Release(yanked) 30 | -------------------------------------------------------------------------------- /lib/barrage/generators/base.rb: -------------------------------------------------------------------------------- 1 | require 'barrage/generators' 2 | require 'active_support/core_ext/class/attribute' 3 | 4 | class Barrage 5 | module Generators 6 | class Base 7 | attr_reader :options 8 | class_attribute :required_options, :available_options 9 | self.required_options = %w(length) 10 | self.available_options = [] 11 | 12 | def initialize(options = {}) 13 | if (missing = missing_required_options(options)) && !missing.empty? 14 | raise ArgumentError, "Missing Required options: #{missing.join(', ')}" 15 | end 16 | @options = options 17 | end 18 | 19 | def length 20 | options["length"] 21 | end 22 | 23 | def generate 24 | raise NotImplemented, "Please Override" 25 | end 26 | 27 | def current 28 | raise NotImplemented, "Please Override" 29 | end 30 | 31 | private 32 | 33 | def missing_required_options(given_options) 34 | required_options.reject { |k| given_options.has_key?(k) } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Drecom Co., Ltd. 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. 23 | -------------------------------------------------------------------------------- /barrage.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'barrage/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "barrage" 8 | spec.version = Barrage::VERSION 9 | spec.authors = ["gussan"] 10 | spec.email = ["egussan@gmail.com"] 11 | spec.summary = %q{Distributed id generator} 12 | spec.description = %q{Distributed id generator} 13 | spec.homepage = "http://github.com/drecom/barrage" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "activesupport" 22 | 23 | spec.add_development_dependency "bundler" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | spec.add_development_dependency "rspec-its" 27 | spec.add_development_dependency "pry" 28 | spec.add_development_dependency "guard-rspec" 29 | spec.add_development_dependency "redis" 30 | spec.add_development_dependency "fakeredis" 31 | spec.add_development_dependency "delorean" 32 | spec.add_development_dependency "coveralls" 33 | end 34 | -------------------------------------------------------------------------------- /lib/barrage.rb: -------------------------------------------------------------------------------- 1 | require "barrage/version" 2 | require "active_support/core_ext/string/inflections" 3 | 4 | class Barrage 5 | class InvalidOption < StandardError; end 6 | attr_reader :generators 7 | 8 | # 9 | # generators: 10 | # - name: msec 11 | # length: 40 12 | # start_at: 1396278000000 # 2014-04-01 00:00:00 +0900 (msec) 13 | # - name: pid 14 | # length: 16 15 | # - name: sequence 16 | # length: 8 17 | 18 | def initialize(options = {}) 19 | @options = options 20 | @lock = Mutex.new 21 | @generators = @options["generators"].map do |h| 22 | generator_name = h["name"] 23 | require "barrage/generators/#{generator_name}" 24 | "Barrage::Generators::#{generator_name.classify}".constantize.new(h) 25 | end 26 | end 27 | 28 | def generate 29 | @lock.synchronize { 30 | shift_size = length 31 | @generators.inject(0) do |result, generator| 32 | shift_size = shift_size - generator.length 33 | result += generator.generate << shift_size 34 | end 35 | } 36 | end 37 | alias_method :next, :generate 38 | 39 | def current 40 | @lock.synchronize { 41 | shift_size = length 42 | @generators.inject(0) do |result, generator| 43 | shift_size = shift_size - generator.length 44 | result += generator.current << shift_size 45 | end 46 | } 47 | end 48 | 49 | def length 50 | @generators.inject(0) {|sum, g| sum += g.length } 51 | end 52 | end 53 | 54 | require "barrage/generators" 55 | -------------------------------------------------------------------------------- /spec/barrage/generators/msec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'barrage/generators/msec' 3 | 4 | describe Barrage::Generators::Msec do 5 | context "When initialized" do 6 | subject { described_class.new(options) } 7 | 8 | context "with empty hash" do 9 | let(:options) { {} } 10 | 11 | it { expect { subject }.to raise_error(ArgumentError) } 12 | end 13 | 14 | context "with required options" do 15 | let(:options) { {"length" => 16, "start_at" => (Time.now.to_f * 1000).round } } 16 | 17 | it { is_expected.to be_instance_of(Barrage::Generators::Msec) } 18 | its(:length) { is_expected.to eq(options["length"]) } 19 | its(:start_at) { is_expected.to eq(options["start_at"]) } 20 | end 21 | end 22 | 23 | describe "#generate" do 24 | subject { described_class.new(options).generate } 25 | let(:length) { 39 } 26 | let(:start_at) { (Time.parse("2014-01-01 00:00:00").to_f*1000).round } 27 | let(:options) { {"length" => length, "start_at" => start_at} } 28 | 29 | it { is_expected.to be_instance_of(Fixnum) } 30 | it { is_expected.to be < 2 ** length } 31 | 32 | context "generate two numbers between 10 millisecond " do 33 | subject { described_class.new(options) } 34 | 35 | it "numbers difference that generated in interval 10 millisecond should exactly 10" do 36 | t = Time.parse("2014-01-01 00:00:00") 37 | first = time_travel_to(t) { subject.generate } 38 | second = time_travel_to(Time.at(t.to_i, 10000)) { subject.generate } 39 | expect(second - first).to eq(10) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/barrage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Barrage do 4 | it 'has a version number' do 5 | expect(Barrage::VERSION).not_to be nil 6 | end 7 | 8 | context "when initialized" do 9 | subject { Barrage.new(options) } 10 | 11 | context "with empty generators" do 12 | let(:options) { {"generators" => []} } 13 | its(:generators) { is_expected.to be_empty } 14 | end 15 | 16 | context "with generators" do 17 | let(:options) { 18 | { 19 | "generators" => [ 20 | {"name" => "msec", "length" => 39, "start_at" => 1396278000000}, 21 | {"name" => "redis_worker_id", "length" => 16, "ttl" => 300}, 22 | {"name" => "sequence", "length" => 9} 23 | ] 24 | } 25 | } 26 | it { expect(subject.generators.size).to eq(options["generators"].size) } 27 | its(:generators) { 28 | is_expected.to contain_exactly( 29 | be_kind_of(Barrage::Generators::Msec), 30 | be_kind_of(Barrage::Generators::RedisWorkerId), 31 | be_kind_of(Barrage::Generators::Sequence) 32 | ) 33 | } 34 | 35 | describe "#generate" do 36 | let(:barrage) { described_class.new(options) } 37 | subject { barrage.generate } 38 | it { is_expected.to be_kind_of(Integer) } 39 | it { is_expected.to be < 2 ** barrage.length } 40 | end 41 | 42 | describe "#length" do 43 | subject { described_class.new(options).length } 44 | it { is_expected.to eq(options["generators"].map { |h| h["length"]}.inject(:+)) } 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/barrage/generators/sequence_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'barrage/generators/sequence' 3 | 4 | describe Barrage::Generators::Sequence do 5 | context "When initialized" do 6 | subject { described_class.new(options) } 7 | 8 | context "with empty hash" do 9 | let(:options) { {} } 10 | 11 | it { expect { subject }.to raise_error(ArgumentError) } 12 | end 13 | 14 | context "with required options" do 15 | let(:options) { {"length" => 16 } } 16 | 17 | it { is_expected.to be_instance_of(Barrage::Generators::Sequence) } 18 | its(:length) { is_expected.to eq(options["length"]) } 19 | end 20 | end 21 | 22 | describe "#generate" do 23 | subject { described_class.new(options).generate } 24 | let(:length) { 9 } 25 | let(:options) { {"length" => length } } 26 | 27 | it { is_expected.to be_instance_of(Fixnum) } 28 | it { is_expected.to be < 2 ** length } 29 | 30 | context "When sequence is 0" do 31 | before do 32 | subject.instance_variable_set(:@sequence, 0) 33 | end 34 | subject { described_class.new(options) } 35 | 36 | it "should generates '1'" do 37 | expect(subject.generate).to eq(1) 38 | end 39 | 40 | it "if generate twice in a row, generated numbers should be in succession" do 41 | first = subject.generate 42 | second = subject.generate 43 | expect(first.succ).to eq(second) 44 | end 45 | end 46 | 47 | context "When sequence is max" do 48 | before do 49 | subject.instance_variable_set(:@sequence, (2 ** length) - 1) 50 | end 51 | subject { described_class.new(options) } 52 | 53 | it "should generates zero" do 54 | expect(subject.generate).to be_zero 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Barrage 2 | 3 | [![Gem Version](https://badge.fury.io/rb/barrage.svg)](http://badge.fury.io/rb/barrage) 4 | [![Dependency Status](https://gemnasium.com/drecom/barrage.svg)](https://gemnasium.com/drecom/barrage) 5 | [![Coverage Status](https://img.shields.io/coveralls/drecom/barrage.svg)](https://coveralls.io/r/drecom/barrage) 6 | [![Build Status](https://travis-ci.org/drecom/barrage.svg)](https://travis-ci.org/drecom/barrage) 7 | [![Code Climate](https://codeclimate.com/github/drecom/barrage.png)](https://codeclimate.com/github/drecom/barrage) 8 | 9 | Distributed ID generator(like twitter/snowflake) 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'barrage' 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install barrage 24 | 25 | ## Usage 26 | 27 | ### Example 28 | 29 | ```ruby 30 | # 39bit: msec (17.4 years from start_at) 31 | # 16bit: worker_id 32 | # 9bit: sequence 33 | require 'barrage' 34 | 35 | barrage = Barrage.new( 36 | "generators" => [ 37 | {"name" => "msec", "length" => 39, "start_at" => 1396278000000}, 38 | {"name" => "redis_worker_id", "length" => 16, "ttl" => 300}, 39 | {"name" => "sequence", "length" => 9} 40 | ] 41 | ) 42 | barrage.next 43 | # => Generated 64bit ID 44 | ``` 45 | 46 | ### Generators 47 | 48 | #### msec 49 | #### redis_worker_id 50 | requirement: redis 2.6+ 51 | 52 | #### sequence 53 | 54 | ### Creating your own generator 55 | 56 | ```ruby 57 | module Barrage::Generators 58 | class YourOwnGenerator < Base 59 | self.required_options += %w(your_option_value) 60 | def generate 61 | # generated code 62 | end 63 | 64 | def your_option_value 65 | options["your_option_value"] 66 | end 67 | end 68 | end 69 | 70 | barrage = Barrage.new("generators" => [{"name"=>"your_own", "length" => 8, "your_option_value"=>"xxx"}]) 71 | ``` 72 | 73 | ## Contributing 74 | 75 | 1. Fork it ( https://github.com/drecom/barrage/fork ) 76 | 2. Create your feature branch (`git checkout -b my-new-feature`) 77 | 3. Commit your changes (`git commit -am 'Add some feature'`) 78 | 4. Push to the branch (`git push origin my-new-feature`) 79 | 5. Create a new Pull Request 80 | -------------------------------------------------------------------------------- /spec/barrage/generators/redis_worker_id_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'barrage/generators/redis_worker_id' 3 | 4 | describe Barrage::Generators::RedisWorkerId do 5 | context "When initialized" do 6 | subject { described_class.new(options) } 7 | 8 | context "with empty hash" do 9 | let(:options) { {} } 10 | 11 | it { expect { subject }.to raise_error(ArgumentError) } 12 | end 13 | 14 | context "with required options" do 15 | let(:options) { {"length" => 16, "ttl" => 300} } 16 | 17 | it { is_expected.to be_instance_of(Barrage::Generators::RedisWorkerId) } 18 | its(:length) { is_expected.to eq(options["length"]) } 19 | its(:ttl) { is_expected.to eq(options["ttl"]) } 20 | end 21 | end 22 | 23 | describe "#generate" do 24 | subject { described_class.new(options).generate } 25 | let(:length) { 8 } 26 | let(:ttl) { 300 } 27 | let(:options) { {"length" => length, "ttl" => ttl} } 28 | 29 | it { is_expected.to be_instance_of(Fixnum) } 30 | it { is_expected.to be < 2 ** length } 31 | 32 | context "on many instances" do 33 | subject { 3.times.map { described_class.new(options) } } 34 | let(:length) { 8 } 35 | 36 | it "should generate unique numbers" do 37 | numbers = subject.map(&:generate) 38 | expect(numbers.size).to eq(numbers.uniq.size) 39 | end 40 | 41 | it "generates numbers should less than length" do 42 | expect(subject.all? { |s| s.generate < (2 ** length) }).to be true 43 | end 44 | end 45 | end 46 | 47 | describe "#Finalizer" do 48 | subject { described_class::Finalizer.new(data).call(*args) } 49 | 50 | let(:now) { Time.now.to_i } 51 | let(:ttl) { 300 } 52 | let(:redis) { Redis.new } 53 | let(:worker_ttl) { now + ttl / 2 } 54 | let(:real_ttl) { now + ttl } 55 | let(:data) { [redis, worker_ttl, real_ttl] } 56 | let(:args) { {} } 57 | 58 | if Gem.loaded_specs["redis"]&.version >= Gem::Version.new("5.0.0") 59 | before do 60 | redis._client.send(:raw_connection) 61 | end 62 | 63 | it "redis client disconnect" do 64 | subject 65 | 66 | expect(redis.connected?).to be_nil 67 | end 68 | else 69 | before do 70 | redis._client.connect 71 | end 72 | 73 | it "redis client disconnect" do 74 | subject 75 | 76 | expect(redis.connected?).to eq false 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/barrage/generators/redis_worker_id.rb: -------------------------------------------------------------------------------- 1 | require 'redis' 2 | require 'barrage/generators/base' 3 | 4 | class Barrage 5 | module Generators 6 | class RedisWorkerId < Base 7 | RACE_CONDITION_TTL = 30 8 | self.required_options += %w(ttl) 9 | 10 | def initialize(options = {}) 11 | @worker_id = nil 12 | @worker_ttl = 0 13 | @real_ttl = 0 14 | super 15 | @data = [] 16 | @finalizer_proc = Finalizer.new(@data) 17 | ObjectSpace.define_finalizer(self, @finalizer_proc) 18 | end 19 | 20 | def generate 21 | now = Time.now.to_i 22 | if @worker_ttl - now <= 0 23 | @data[1] = @worker_id = renew_worker_id 24 | # check redis after half of real ttl 25 | @worker_ttl = now + ttl / 2 26 | @real_ttl = now + ttl 27 | @data[2] = @real_ttl 28 | end 29 | @worker_id 30 | end 31 | alias_method :current, :generate 32 | 33 | def ttl 34 | options["ttl"] 35 | end 36 | 37 | def redis 38 | @redis ||= @data[0] = Redis.new(options["redis"] || {}) 39 | end 40 | 41 | class Finalizer 42 | def initialize(data) 43 | @pid = $$ 44 | @data = data 45 | end 46 | 47 | def call(*args) 48 | return if @pid != $$ 49 | redis, worker_id, real_ttl = *@data 50 | 51 | if redis.is_a?(Redis) and redis.connected? 52 | redis.del("barrage:worker:#{worker_id}") if real_ttl > Time.now.to_i 53 | 54 | close_redis_connection(redis) 55 | end 56 | end 57 | 58 | private 59 | 60 | def close_redis_connection(redis) 61 | redis_version = Gem.loaded_specs["redis"]&.version 62 | 63 | if redis_version && redis_version >= Gem::Version.new("5.0.0") 64 | redis._client.close 65 | else 66 | redis._client.disconnect 67 | end 68 | end 69 | end 70 | 71 | private 72 | 73 | def renew_worker_id 74 | if @real_ttl - Time.now.to_i - RACE_CONDITION_TTL <= 0 75 | @worker_id = nil 76 | end 77 | new_worker_id = redis.evalsha( 78 | script_sha, 79 | argv: [2 ** length, rand(2 ** length), (@worker_id || 0), ttl, RACE_CONDITION_TTL] 80 | ) 81 | new_worker_id or raise StandardError, "Renew redis worker id failed" 82 | return new_worker_id.to_i 83 | end 84 | 85 | def script_sha 86 | @script_sha ||= 87 | redis.script(:load, <<-EOF.gsub(/^ {12}/, '')) 88 | local max_value = tonumber(ARGV[1]) 89 | local new_worker_id = ARGV[2] 90 | local old_worker_id = ARGV[3] 91 | local ttl = tonumber(ARGV[4]) 92 | local race_condition_ttl = tonumber(ARGV[5]) 93 | local loop_cnt = 0 94 | 95 | local worker_id = nil 96 | local candidate_worker_id = tonumber(new_worker_id) 97 | 98 | if type(old_worker_id) == "string" and string.len(old_worker_id) > 0 and redis.call('EXISTS', "barrage:worker:" .. old_worker_id) == 1 then 99 | redis.call("EXPIRE", "barrage:worker:" .. old_worker_id, ttl + race_condition_ttl) 100 | worker_id = old_worker_id 101 | else 102 | while redis.call("SETNX", "barrage:worker:" .. candidate_worker_id, 1) == 0 and loop_cnt < max_value 103 | do 104 | candidate_worker_id = (candidate_worker_id + 1) % max_value 105 | loop_cnt = loop_cnt + 1 106 | end 107 | if loop_cnt >= max_value then 108 | return nil 109 | else 110 | worker_id = candidate_worker_id 111 | end 112 | redis.call("EXPIRE", "barrage:worker:" .. worker_id, ttl + race_condition_ttl) 113 | end 114 | return worker_id 115 | EOF 116 | end 117 | end 118 | end 119 | end 120 | --------------------------------------------------------------------------------