diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..0929c17 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,73 @@ +version: 2.1 +jobs: + build: + docker: + - image: cimg/ruby:3.2.0 + + working_directory: ~/ratelimit + + steps: + - checkout + + - run: + name: "Ruby Bundler: Configure bundler" + command: | + bundle config set path '.bundle' + + - restore_cache: + keys: + - v1-ruby-3.2.0-deps-{{ checksum "Gemfile.lock" }} + # fallback to using the latest cache if no exact match is found + - v1-ruby-3.2.0-deps- + + - run: + name: install dependencies + command: | + bundle install --jobs=4 --retry=3 + bundle clean --force + + - save_cache: + paths: + - .bundle + key: v1-ruby-3.2.0-deps-{{ checksum "Gemfile.lock" }} + + - run: + name: run tests + command: | + mkdir /tmp/test-results + + bundle exec rspec --format progress \ + --format RspecJunitFormatter \ + --out /tmp/test-results/rspec.xml \ + --format progress + + - store_test_results: + path: /tmp/test-results + - store_artifacts: + path: /tmp/test-results + destination: test-results + deploy: + docker: + - image: cimg/ruby:3.2.0 + working_directory: ~/ratelimit + steps: + - checkout + - run: + name: Release Gem + command: | + gem build ratelimit.gemspec + gem push --host https://rubygems.pkg.github.com/buyapowa *.gem + +workflows: + version: 2 + build-and-deploy: + jobs: + - build + - deploy: + context: + - gem-deploy + requires: + - build + filters: + branches: + only: master diff --git a/.gitignore b/.gitignore index 31cafb5..e74cb7b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .bundle .config .yardoc -Gemfile.lock InstalledFiles _yardoc coverage diff --git a/.ruby-gemset b/.ruby-gemset deleted file mode 100644 index 6dcedfe..0000000 --- a/.ruby-gemset +++ /dev/null @@ -1 +0,0 @@ -ratelimit diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index ec6b00f..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -ruby-2.1.2 diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..c23af94 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.2.0 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c3a1ed7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -script: "bundle exec rspec" -rvm: - - 1.9.2 - - 1.9.3 - - 2.0.0 - - 2.1.1 - - 2.1.2 - - rbx-2 - - jruby-19mode diff --git a/Gemfile b/Gemfile index 4cff8c5..fa75df1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,3 @@ source 'https://rubygems.org' -# Specify your gem's dependencies in ratelimit.gemspec gemspec - -gem 'coveralls', require: false diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b162b75 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,57 @@ +PATH + remote: . + specs: + ratelimit (1.1.1) + redis (< 5.0.0) + +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.5.0) + fakeredis (0.8.0) + redis (~> 4.1) + maruku (0.7.3) + psych (5.0.2) + stringio + rake (13.0.6) + rdoc (6.5.0) + psych (>= 4.0.0) + redis (4.8.0) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.0) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) + stringio (3.0.4) + timecop (0.9.6) + webrick (1.7.0) + yard (0.9.28) + webrick (~> 1.7.0) + +PLATFORMS + ruby + +DEPENDENCIES + bundler + fakeredis + maruku + rake + ratelimit! + rdoc + rspec + rspec_junit_formatter (~> 0.6) + timecop + yard + +BUNDLED WITH + 2.4.5 diff --git a/Rakefile b/Rakefile index 3c4590c..204a896 100644 --- a/Rakefile +++ b/Rakefile @@ -34,5 +34,4 @@ namespace :doc do task :clean do rm_r doc_dir if File.exists?(doc_destination) end - end diff --git a/lib/ratelimit.rb b/lib/ratelimit.rb index 607df69..d74ce75 100644 --- a/lib/ratelimit.rb +++ b/lib/ratelimit.rb @@ -1,5 +1,4 @@ require 'redis' -require 'redis-namespace' class Ratelimit @@ -11,10 +10,12 @@ class Ratelimit # @option options [Integer] :bucket_interval (5) How many seconds each bucket represents # @option options [Integer] :bucket_expiry (@bucket_span) How long we keep data in each bucket before it is auto expired. Cannot be larger than the bucket_span. # @option options [Redis] :redis (nil) Redis client if you need to customize connection options + # @option options [Lambda] :checkout_redis_with (nil) Lambda that yields to a passed block with a Redis instance, for use with a Redis connection pool # # @return [RateLimit] RateLimit instance # def initialize(key, options = {}) + @namespace = "ratelimit" @key = key unless options.is_a?(Hash) raise ArgumentError.new("Redis object is now passed in via the options hash - options[:redis]") @@ -30,6 +31,7 @@ def initialize(key, options = {}) raise ArgumentError.new("Cannot have less than 3 buckets") end @redis = options[:redis] + @checkout_redis_with = options[:checkout_redis_with] end # Add to the counter for a given subject. @@ -40,13 +42,15 @@ def initialize(key, options = {}) # @return [Integer] The counter value def add(subject, count = 1) bucket = get_bucket - subject = "#{@key}:#{subject}" - redis.pipelined do - redis.hincrby(subject, bucket, count) - redis.hdel(subject, (bucket + 1) % @bucket_count) - redis.hdel(subject, (bucket + 2) % @bucket_count) - redis.expire(subject, @bucket_expiry) - end.first + subject = [@namespace, @key, subject].join(":") + use_redis do |r| + r.multi do |pipeline| + pipeline.hincrby(subject, bucket, count) + pipeline.hdel(subject, (bucket + 1) % @bucket_count) + pipeline.hdel(subject, (bucket + 2) % @bucket_count) + pipeline.expire(subject, @bucket_expiry) + end.first + end end # Returns the count for a given subject and interval @@ -57,12 +61,15 @@ def count(subject, interval) bucket = get_bucket interval = [interval, @bucket_interval].max count = (interval / @bucket_interval).floor - subject = "#{@key}:#{subject}" + subject = [@namespace, @key, subject].join(":") keys = (0..count - 1).map do |i| (bucket - i) % @bucket_count end - return redis.hmget(subject, *keys).inject(0) {|a, i| a + i.to_i} + + return use_redis do |r| + r.hmget(subject, *keys).inject(0) { |a, i| a + i.to_i } + end end # Check if the rate limit has been exceeded. @@ -113,7 +120,15 @@ def get_bucket(time = Time.now.to_i) ((time % @bucket_span) / @bucket_interval).floor end - def redis - @redis ||= Redis::Namespace.new(:ratelimit, :redis => @redis || Redis.new) + def use_redis + if @checkout_redis_with + @checkout_redis_with.call { |redis| yield(redis) } + else + yield single_redis_instance + end + end + + def single_redis_instance + @redis ||= Redis.new end end diff --git a/lib/ratelimit/version.rb b/lib/ratelimit/version.rb index d3c5917..c34d32b 100644 --- a/lib/ratelimit/version.rb +++ b/lib/ratelimit/version.rb @@ -1,3 +1,3 @@ class Ratelimit - VERSION = "1.0.2" + VERSION = "1.1.1" end diff --git a/ratelimit.gemspec b/ratelimit.gemspec index 13c8a16..29ead44 100644 --- a/ratelimit.gemspec +++ b/ratelimit.gemspec @@ -18,9 +18,11 @@ Gem::Specification.new do |spec| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.add_dependency "redis", ">= 2.0.0" - spec.add_dependency "redis-namespace", ">= 1.0.0" - spec.add_development_dependency "bundler", "~> 1.6" + spec.metadata["allowed_push_host"] = "https://rubygems.pkg.github.com" + spec.metadata["github_repo"] = "ssh://github.com/Buyapowa/ratelimit" + + spec.add_dependency "redis", "< 5.0.0" + spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "fakeredis" spec.add_development_dependency "timecop" @@ -28,4 +30,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "yard" spec.add_development_dependency "maruku" spec.add_development_dependency "rdoc" + spec.add_development_dependency "rspec_junit_formatter", "~> 0.6" end diff --git a/spec/ratelimit_spec.rb b/spec/ratelimit_spec.rb index d390cd3..3a985ce 100644 --- a/spec/ratelimit_spec.rb +++ b/spec/ratelimit_spec.rb @@ -1,10 +1,9 @@ require 'spec_helper' describe Ratelimit do - before do @r = Ratelimit.new("key") - @r.send(:redis).flushdb + @r.send(:use_redis) { |r| r.flushdb } end it "should set_bucket_expiry to the bucket_span if not defined" do @@ -84,7 +83,7 @@ @value = nil expect do - timeout(1) do + Timeout.timeout(1) do @r.exec_within_threshold("key", {:threshold => 30, :interval => 30}) do @value = 2 end @@ -106,4 +105,20 @@ expect(@r.count('value1', 10)).to eql(1) end + context "using the checkout_redis_with option" do + let(:redis) { Redis.new } + let(:redis_checkout_lambda) do + lambda do |&block| + block.call(redis) + end + end + + let(:key) { SecureRandom.hex(6) } + subject { Ratelimit.new(key, checkout_redis_with: redis_checkout_lambda) } + + it "adds correctly" do + subject.add("value1", 3) + expect(subject.count("value1", 1)).to eq(3) + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0a8bc39..921aa1d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,11 +14,10 @@ # users commonly want. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -require 'coveralls' -Coveralls.wear! require 'fakeredis' require 'timecop' require 'ratelimit' +require 'securerandom' RSpec.configure do |config| # The settings below are suggested to provide a good initial experience