glebprop 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/glebprop.gemspec +24 -0
- data/lib/glebprop/version.rb +3 -0
- data/lib/glebprop.rb +6 -0
- data/lib/prop.rb +116 -0
- metadata +72 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/glebprop.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "glebprop/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "glebprop"
|
7
|
+
s.version = Glebprop::VERSION
|
8
|
+
s.authors = ["Gleb Pomykalov"]
|
9
|
+
s.email = ["gleb.pomykalov@skype.net"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Implement rate limiting}
|
12
|
+
s.description = %q{See gem called Prop - this one is just a copy}
|
13
|
+
|
14
|
+
s.rubyforge_project = "glebprop"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
24
|
+
end
|
data/lib/glebprop.rb
ADDED
data/lib/prop.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def define_prop_class_method(name, &blk)
|
5
|
+
(class << self; self; end).instance_eval { define_method(name, &blk) }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Prop
|
10
|
+
class RateLimitExceededError < RuntimeError
|
11
|
+
attr_accessor :handle, :retry_after
|
12
|
+
|
13
|
+
def self.create(handle, key, threshold)
|
14
|
+
error = new("#{handle} threshold of #{threshold} exceeded for key '#{key}'")
|
15
|
+
error.handle = handle
|
16
|
+
error.retry_after = threshold - Time.now.to_i % threshold if threshold > 0
|
17
|
+
raise error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :handles, :reader, :writer
|
23
|
+
|
24
|
+
def read(&blk)
|
25
|
+
self.reader = blk
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(&blk)
|
29
|
+
self.writer = blk
|
30
|
+
end
|
31
|
+
|
32
|
+
def increment(&blk)
|
33
|
+
self.incrementer = blk
|
34
|
+
end
|
35
|
+
|
36
|
+
def incrementer=(value)
|
37
|
+
@incrementer = value
|
38
|
+
end
|
39
|
+
|
40
|
+
def incrementer
|
41
|
+
@incrementer ? Proc.new { |key, inc| @incrementer.call(key, inc) } :
|
42
|
+
Proc.new { |key, inc| self.writer.call(key, (self.reader.call(key) || 0).to_i + inc) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def defaults(handle, defaults)
|
46
|
+
raise RuntimeError.new("Invalid threshold setting") unless defaults[:threshold].to_i > 0
|
47
|
+
raise RuntimeError.new("Invalid interval setting") unless defaults[:interval].to_i > 0
|
48
|
+
|
49
|
+
self.handles ||= {}
|
50
|
+
self.handles[handle] = defaults
|
51
|
+
end
|
52
|
+
|
53
|
+
def throttle!(handle, key = nil, options = {})
|
54
|
+
options = sanitized_prop_options(handle, key, options)
|
55
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
56
|
+
counter = reader.call(cache_key).to_i
|
57
|
+
|
58
|
+
incrementer.call(cache_key, [ 1, options[:increment].to_i ].max)
|
59
|
+
|
60
|
+
if counter >= options[:threshold]
|
61
|
+
raise Prop::RateLimitExceededError.create(handle, normalize_cache_key(key), options[:threshold])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def throttled?(handle, key = nil, options = {})
|
66
|
+
options = sanitized_prop_options(handle, key, options)
|
67
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
68
|
+
|
69
|
+
reader.call(cache_key).to_i >= options[:threshold]
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset(handle, key = nil, options = {})
|
73
|
+
options = sanitized_prop_options(handle, key, options)
|
74
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
75
|
+
|
76
|
+
writer.call(cache_key, 0)
|
77
|
+
end
|
78
|
+
|
79
|
+
def query(handle, key = nil, options = {})
|
80
|
+
options = sanitized_prop_options(handle, key, options)
|
81
|
+
cache_key = sanitized_prop_key(key, options[:interval])
|
82
|
+
|
83
|
+
reader.call(cache_key).to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Builds the expiring cache key
|
89
|
+
def sanitized_prop_key(key, interval)
|
90
|
+
window = (Time.now.to_i / interval)
|
91
|
+
cache_key = "#{normalize_cache_key(key)}/#{ window }"
|
92
|
+
"prop/#{Digest::MD5.hexdigest(cache_key)}"
|
93
|
+
end
|
94
|
+
|
95
|
+
# Sanitizes the option set and sets defaults
|
96
|
+
def sanitized_prop_options(handle, key, options)
|
97
|
+
defaults = (handles || {})[handle] || {}
|
98
|
+
return {
|
99
|
+
:key => normalize_cache_key(key),
|
100
|
+
:increment => defaults[:increment],
|
101
|
+
:threshold => defaults[:threshold].to_i,
|
102
|
+
:interval => defaults[:interval].to_i
|
103
|
+
}.merge(options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Simple key expansion only supports arrays and primitives
|
107
|
+
def normalize_cache_key(key)
|
108
|
+
if key.is_a?(Array)
|
109
|
+
key.map { |part| normalize_cache_key(part) }.join('/')
|
110
|
+
else
|
111
|
+
key.to_s
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: glebprop
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Gleb Pomykalov
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-29 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: See gem called Prop - this one is just a copy
|
22
|
+
email:
|
23
|
+
- gleb.pomykalov@skype.net
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- .gitignore
|
32
|
+
- Gemfile
|
33
|
+
- Rakefile
|
34
|
+
- glebprop.gemspec
|
35
|
+
- lib/glebprop.rb
|
36
|
+
- lib/glebprop/version.rb
|
37
|
+
- lib/prop.rb
|
38
|
+
homepage: ""
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements: []
|
65
|
+
|
66
|
+
rubyforge_project: glebprop
|
67
|
+
rubygems_version: 1.8.6
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Implement rate limiting
|
71
|
+
test_files: []
|
72
|
+
|