Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
Expand Down
1 change: 0 additions & 1 deletion .ruby-gemset

This file was deleted.

1 change: 0 additions & 1 deletion .ruby-version

This file was deleted.

1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ruby 3.2.0
9 changes: 0 additions & 9 deletions .travis.yml

This file was deleted.

3 changes: 0 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in ratelimit.gemspec
gemspec

gem 'coveralls', require: false
57 changes: 57 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,4 @@ namespace :doc do
task :clean do
rm_r doc_dir if File.exists?(doc_destination)
end

end
39 changes: 27 additions & 12 deletions lib/ratelimit.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require 'redis'
require 'redis-namespace'

class Ratelimit

Expand All @@ -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]")
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion lib/ratelimit/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class Ratelimit
VERSION = "1.0.2"
VERSION = "1.1.1"
end
9 changes: 6 additions & 3 deletions ratelimit.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ 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"
spec.add_development_dependency "rspec"
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
21 changes: 18 additions & 3 deletions spec/ratelimit_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
3 changes: 1 addition & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down