throttler 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .bundle
2
+ pkg
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,16 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ throttler (0.3.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ rake (0.9.2.2)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ rake (~> 0.9.2)
16
+ throttler!
data/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
1
  (The MIT License)
2
-
3
- Copyright (c) 2010 Paper Cavalier
4
-
2
+
3
+ Copyright (c) 2012 Hakan Ensari
4
+
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
7
7
  'Software'), to deal in the Software without restriction, including
@@ -9,10 +9,10 @@ without limitation the rights to use, copy, modify, merge, publish,
9
9
  distribute, sublicense, and/or sell copies of the Software, and to
10
10
  permit persons to whom the Software is furnished to do so, subject to
11
11
  the following conditions:
12
-
12
+
13
13
  The above copyright notice and this permission notice shall be
14
14
  included in all copies or substantial portions of the Software.
15
-
15
+
16
16
  THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
17
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
18
  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
data/README.md CHANGED
@@ -1,26 +1,40 @@
1
1
  Throttler
2
2
  =========
3
3
 
4
- Throttler is a tired, cranky module that helps you throttle stuff in parallel-running Ruby scripts on a single machine.
4
+ [![travis] [1]] [2]
5
5
 
6
- ![Mapplethorpe](https://github.com/papercavalier/throttler/raw/master/mapplethorpe_chains.jpg)
6
+ Throttler rate-limits code execution across threads or processes on a server.
7
7
 
8
- Example
9
- -------
8
+ ![Mapplethorpe][3]
10
9
 
11
- Our very own use case: We have multiple Resque workers hitting the Amazon IP on multiple addresses and want to make sure they don't fire more than once per locale per IP.
10
+ Installation
11
+ ------------
12
12
 
13
- Here's some pseudo code:
13
+ ```ruby
14
+ # in your Gemfile
15
+ gem 'throttler'
16
+ ```
14
17
 
15
- class Worker
16
- include Throttler
18
+ Throttler works only on platforms that support file locking.
17
19
 
18
- def perform(*args)
19
- scope = "#{locale}-#{interface}"
20
- freq = 1.0
20
+ Usage
21
+ -----
21
22
 
22
- throttle(scope, freq) do
23
- # perform a request
24
- end
25
- end
23
+ The following background job ensures workers will not scrape a site faster than
24
+ once every second per IP address.
25
+
26
+ ```ruby
27
+ class Scrape
28
+ def self.perform(site, ip_address, *ids)
29
+ # The block is syntactic sugar.
30
+ Throttler.limit 1.0, site, ip_address do
31
+ spider = Spider.new site, ip_address
32
+ spider.scrape *ids
26
33
  end
34
+ end
35
+ end
36
+ ```
37
+
38
+ [1]: https://secure.travis-ci.org/hakanensari/throttler.png
39
+ [2]: http://travis-ci.org/hakanensari/throttler
40
+ [3]: http://f.cl.ly/items/2S343U141D0N3b3h1K09/Mapplethorpe.png
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |test|
5
+ test.libs += %w{lib test}
6
+ test.test_files = FileList['test/**/*_test.rb']
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,48 @@
1
+ module Throttler
2
+ class Throttle
3
+ class << self
4
+ attr_accessor :tmp_dir
5
+ end
6
+
7
+ @tmp_dir = '/tmp'
8
+
9
+ def initialize(namespace)
10
+ @namespace = namespace
11
+ @file = File.open(path, File::RDWR|File::CREAT)
12
+ end
13
+
14
+ def lock
15
+ @file.flock(File::LOCK_EX)
16
+ end
17
+
18
+ def path
19
+ "#{self.class.tmp_dir}/.lock-#{@namespace}"
20
+ end
21
+
22
+ def hold(wait)
23
+ sleep [timestamp + wait - now, 0.0].max
24
+ timestamp!
25
+ end
26
+
27
+ def unlock
28
+ @file.flock(File::LOCK_UN)
29
+ @file.close
30
+ end
31
+
32
+ private
33
+
34
+ def now
35
+ Time.now.to_f
36
+ end
37
+
38
+ def timestamp
39
+ @file.rewind
40
+ @file.gets.to_f
41
+ end
42
+
43
+ def timestamp!
44
+ @file.rewind
45
+ @file.write now
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,4 @@
1
- module Throttler #:nodoc:
2
- VERSION = "0.2.4"
1
+ module Throttler
2
+ VERSION = '0.3.0'
3
3
  end
4
4
 
data/lib/throttler.rb CHANGED
@@ -1,16 +1,23 @@
1
- require "throttler/timer"
1
+ require 'throttler/throttle'
2
2
 
3
- # = Throttler
4
- #
5
- # A tired, cranky module that helps you throttle stuff in parallel-running Ruby
6
- # scripts on a single machine. It gets the job done.
3
+ # Throttler rate-limits code execution.
7
4
  module Throttler
8
- def throttle(scope="throttler",freq=1.0)
9
- timer = Timer.new(scope)
10
- timer.lock
11
- sleep [timer.timestamp + freq - Time.now.to_f, 0.0].max
12
- timer.timestamp = Time.now.to_f
13
- timer.unlock
5
+ # Ensures subsequent code, including any given block, is executed at most per
6
+ # every `wait` seconds.
7
+ #
8
+ # Optionally takes a splat of words to namespace the throttle.
9
+ def self.limit(wait, *words)
10
+ words << 'default' if words.empty?
11
+ namespace = words.join '-'
12
+
13
+ begin
14
+ throttle = Throttle.new namespace
15
+ throttle.lock
16
+ throttle.hold wait
17
+ ensure
18
+ throttle.unlock
19
+ end
20
+
14
21
  yield if block_given?
15
22
  end
16
23
  end
@@ -0,0 +1,20 @@
1
+ require 'test/unit'
2
+ require 'benchmark'
3
+ require 'throttler'
4
+
5
+ class Work
6
+ def self.perform
7
+ Throttler.limit 0.1
8
+ end
9
+ end
10
+
11
+ class ThrottlerTest < Test::Unit::TestCase
12
+ def test_throttling
13
+ time = Benchmark.realtime do
14
+ 11.times { Process.fork { Work.perform } }
15
+ Process.waitall
16
+ end
17
+
18
+ assert_equal 1, time.to_i
19
+ end
20
+ end
data/throttler.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'throttler/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'throttler'
7
+ s.version = Throttler::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Hakan Ensari', 'Piotr Łaszewski']
10
+ s.email = ['hakan.ensari@papercavalier.com']
11
+ s.homepage = 'https://github.com/hakanensari/throttler'
12
+ s.summary = 'Rate-limits code execution'
13
+ s.description = 'Throttler rate-limits code execution across threads or processes.'
14
+
15
+ s.add_development_dependency 'rake', '~> 0.9.2'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ['lib']
21
+ end
22
+
metadata CHANGED
@@ -1,101 +1,70 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: throttler
3
- version: !ruby/object:Gem::Version
4
- prerelease: false
5
- segments:
6
- - 0
7
- - 2
8
- - 4
9
- version: 0.2.4
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
10
6
  platform: ruby
11
- authors:
7
+ authors:
12
8
  - Hakan Ensari
13
- - "Piotr \xC5\x81aszewski"
9
+ - Piotr Łaszewski
14
10
  autorequire:
15
11
  bindir: bin
16
12
  cert_chain: []
17
-
18
- date: 2010-11-29 00:00:00 +00:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: rspec
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
13
+ date: 2012-03-16 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ requirement: &70196529042720 !ruby/object:Gem::Requirement
25
18
  none: false
26
- requirements:
19
+ requirements:
27
20
  - - ~>
28
- - !ruby/object:Gem::Version
29
- segments:
30
- - 2
31
- - 1
32
- - 0
33
- version: 2.1.0
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.2
34
23
  type: :development
35
- version_requirements: *id001
36
- description: A tired, cranky module that helps you throttle stuff in parallel-running Ruby scripts on a single machine.
37
- email:
38
- - code@papercavalier.com
24
+ prerelease: false
25
+ version_requirements: *70196529042720
26
+ description: Throttler rate-limits code execution across threads or processes.
27
+ email:
28
+ - hakan.ensari@papercavalier.com
39
29
  executables: []
40
-
41
30
  extensions: []
42
-
43
31
  extra_rdoc_files: []
44
-
45
- files:
46
- - lib/throttler/timer.rb
47
- - lib/throttler/version.rb
48
- - lib/throttler.rb
32
+ files:
33
+ - .gitignore
34
+ - .travis.yml
35
+ - Gemfile
36
+ - Gemfile.lock
49
37
  - LICENSE
50
38
  - README.md
51
- - spec/fixtures/foo.rb
52
- - spec/integration/blocks_with_long_execution_time_spec.rb
53
- - spec/integration/fibers_spec.rb
54
- - spec/integration/loop_spec.rb
55
- - spec/integration/processes_spec.rb
56
- - spec/integration/threads_spec.rb
57
- - spec/spec_helper.rb
58
- - spec/unit/throttler/timer_spec.rb
59
- - spec/unit/throttler_spec.rb
60
- has_rdoc: true
61
- homepage: http://github.com/papercavalier/throttler
39
+ - Rakefile
40
+ - lib/throttler.rb
41
+ - lib/throttler/throttle.rb
42
+ - lib/throttler/version.rb
43
+ - test/throttler_test.rb
44
+ - throttler.gemspec
45
+ homepage: https://github.com/hakanensari/throttler
62
46
  licenses: []
63
-
64
47
  post_install_message:
65
48
  rdoc_options: []
66
-
67
- require_paths:
49
+ require_paths:
68
50
  - lib
69
- required_ruby_version: !ruby/object:Gem::Requirement
51
+ required_ruby_version: !ruby/object:Gem::Requirement
70
52
  none: false
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- segments:
75
- - 0
76
- version: "0"
77
- required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
58
  none: false
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- segments:
83
- - 0
84
- version: "0"
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
85
63
  requirements: []
86
-
87
- rubyforge_project: throttler
88
- rubygems_version: 1.3.7
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.11
89
66
  signing_key:
90
67
  specification_version: 3
91
- summary: A tired, cranky throttler
92
- test_files:
93
- - spec/fixtures/foo.rb
94
- - spec/integration/blocks_with_long_execution_time_spec.rb
95
- - spec/integration/fibers_spec.rb
96
- - spec/integration/loop_spec.rb
97
- - spec/integration/processes_spec.rb
98
- - spec/integration/threads_spec.rb
99
- - spec/spec_helper.rb
100
- - spec/unit/throttler/timer_spec.rb
101
- - spec/unit/throttler_spec.rb
68
+ summary: Rate-limits code execution
69
+ test_files:
70
+ - test/throttler_test.rb
@@ -1,27 +0,0 @@
1
- module Throttler #:nodoc:
2
- class Timer
3
- def initialize(scope)
4
- path = "/tmp/.#{scope}"
5
- @timer = File.open(path, File::RDWR|File::CREAT)
6
- end
7
-
8
- def lock
9
- @timer.flock(File::LOCK_EX)
10
- end
11
-
12
- def timestamp
13
- @timestamp ||= @timer.gets.to_f
14
- end
15
-
16
- def timestamp=(time)
17
- @timer.rewind
18
- @timer.write(time)
19
- @timestamp = time
20
- end
21
-
22
- def unlock
23
- @timer.flock(File::LOCK_UN)
24
- @timer.close
25
- end
26
- end
27
- end
data/spec/fixtures/foo.rb DELETED
@@ -1,18 +0,0 @@
1
- $:.push File.expand_path("../../../lib", __FILE__)
2
- require "throttler"
3
-
4
- class Foo
5
- include Throttler
6
-
7
- def bar
8
- throttle("foo", 2.0) { }
9
- end
10
-
11
- def control; end
12
- end
13
-
14
- if ARGV.size == 0
15
- Foo.new.bar
16
- else
17
- Foo.new.control
18
- end
@@ -1,33 +0,0 @@
1
- require "fileutils"
2
- require "spec_helper"
3
-
4
- describe "Throttler" do
5
- before do
6
- class Foo
7
- include Throttler
8
-
9
- def bar
10
- throttle("foo") do
11
- yield # so we can count
12
- sleep 10
13
- end
14
- end
15
- end
16
-
17
- FileUtils.rm "/tmp/.foo", :force => true
18
- end
19
-
20
- it "throttles threads with long execution times" do
21
- count = 0
22
-
23
- threads = 10.times.collect do
24
- Thread.new do
25
- loop{ Foo.new.bar{ count += 1 } }
26
- end
27
- end
28
- sleep 3.25
29
- threads.each { |t| Thread.kill(t) }
30
-
31
- count.should eql 4
32
- end
33
- end
@@ -1,29 +0,0 @@
1
- require "benchmark"
2
- require "fiber"
3
- require "fileutils"
4
- require "spec_helper"
5
-
6
- describe "Throttler" do
7
- before do
8
- class Foo
9
- include Throttler
10
-
11
- def bar
12
- throttle { }
13
- end
14
- end
15
-
16
- FileUtils.rm "/tmp/.throttler", :force => true
17
- end
18
-
19
- it "throttles fibers" do
20
- time = Benchmark.realtime do
21
- fib = Fiber.new do
22
- loop{ Foo.new.bar; Fiber.yield }
23
- end
24
- 3.times { fib.resume }
25
- end
26
-
27
- time.should be_within(0.1).of(2)
28
- end
29
- end
@@ -1,27 +0,0 @@
1
- require "benchmark"
2
- require "fileutils"
3
- require "spec_helper"
4
-
5
- describe "Throttler" do
6
- before do
7
- class Foo
8
- include Throttler
9
-
10
- def bar
11
- throttle { }
12
- end
13
- end
14
-
15
- FileUtils.rm "/tmp/.throttler", :force => true
16
- end
17
-
18
- it "throttles a loop" do
19
- time = Benchmark.realtime do
20
- 3.times do
21
- Foo.new.bar
22
- end
23
- end
24
-
25
- time.should be_within(0.1).of(2)
26
- end
27
- end
@@ -1,22 +0,0 @@
1
- require "benchmark"
2
- require "fileutils"
3
- require "spec_helper"
4
-
5
-
6
- describe "Throttler" do
7
- before do
8
- FileUtils.rm "/tmp/.foo", :force => true
9
- end
10
-
11
- it "throttles concurrent processes" do
12
- startup_time = Benchmark.realtime do
13
- `ruby #{File.dirname(__FILE__) + "/../fixtures/foo.rb"} control`
14
- end
15
-
16
- time = Benchmark.realtime do
17
- 4.times{ `ruby #{File.dirname(__FILE__) + "/../fixtures/foo.rb"}` }
18
- end
19
-
20
- (time - startup_time).should be_within(0.5).of(6.0)
21
- end
22
- end
@@ -1,51 +0,0 @@
1
- require "fileutils"
2
- require "spec_helper"
3
-
4
- describe "Throttler" do
5
- before do
6
- class Foo
7
- include Throttler
8
-
9
- def bar
10
- throttle { }
11
- end
12
-
13
- def baz
14
- throttle("foo-prime"){ }
15
- end
16
- end
17
-
18
- %w{throttler foo-prime}.each do |file|
19
- FileUtils.rm "/tmp/.#{file}", :force => true
20
- end
21
- end
22
-
23
- it "throttles threads" do
24
- count = 0
25
- threads = 10.times.collect do
26
- Thread.new do
27
- loop { Foo.new.bar; count += 1 }
28
- end
29
- end
30
- sleep 3.25
31
- threads.each { |t| Thread.kill(t) }
32
-
33
- count.should eql 4
34
- end
35
-
36
- it "throttles by scope" do
37
- count = 0
38
- threads = 10.times.collect do
39
- Thread.new do
40
- loop do
41
- Foo.new.send(count % 2 == 0 ? :bar : :baz)
42
- count += 1
43
- end
44
- end
45
- end
46
- sleep 3.25
47
- threads.each { |t| Thread.kill(t) }
48
-
49
- count.should eql 8
50
- end
51
- end
data/spec/spec_helper.rb DELETED
@@ -1,2 +0,0 @@
1
- require "rspec"
2
- require File.expand_path("../../lib/throttler", __FILE__)
@@ -1,65 +0,0 @@
1
- require "spec_helper"
2
- require "benchmark"
3
- require "fileutils"
4
-
5
- module Throttler
6
- describe Timer do
7
- before do
8
- FileUtils.rm "/tmp/.foo", :force => true
9
- end
10
-
11
- let!(:timer) do
12
- Timer.new("foo")
13
- end
14
-
15
- it "creates a file" do
16
- File.exists?("/tmp/.foo").should be_true
17
- end
18
-
19
- context "#lock" do
20
- it "locks the file" do
21
- timer.lock
22
- File.open("/tmp/.foo") do |f|
23
- f.flock(File::LOCK_EX | File::LOCK_NB).should be_false
24
- end
25
- end
26
- end
27
-
28
- context "#timestamp" do
29
- it "gets the timestamp" do
30
- now = Time.now.to_f
31
- File.open("/tmp/.foo", "w") { |f| f.write(now) }
32
- timer.timestamp.should be_within(0.1).of(now)
33
- end
34
-
35
- it "returns 0.0 when file is first created" do
36
- timer.timestamp.should eql(0.0)
37
- end
38
- end
39
-
40
- context "#timestamp=" do
41
- it "sets the timestamp" do
42
- now = Time.now.to_f
43
- timer.timestamp= now
44
-
45
- # We need to close the file first
46
- timer.instance_variable_get(:@timer).close
47
-
48
- File.open("/tmp/.foo") do |f|
49
- f.gets.to_f.should be_within(0.1).of(now)
50
- end
51
- end
52
- end
53
-
54
- context "#unlock" do
55
- it "unlocks the file" do
56
- timer.lock
57
- timer.unlock
58
-
59
- File.open("/tmp/.foo") do |f|
60
- f.flock(File::LOCK_EX | File::LOCK_NB).should_not be_false
61
- end
62
- end
63
- end
64
- end
65
- end
@@ -1,44 +0,0 @@
1
- require "benchmark"
2
- require "fileutils"
3
- require "spec_helper"
4
-
5
- describe Throttler do
6
- before do
7
- class Foo
8
- include Throttler
9
- end
10
-
11
- FileUtils.rm "/tmp/.throttler", :force => true
12
- end
13
-
14
- let!(:foo) do
15
- Foo.new
16
- end
17
-
18
- describe "#throttle" do
19
- it "removes a lock when an exception is raised" do
20
- expect do
21
- foo.throttle { raise }
22
- end.to raise_error
23
-
24
- File.open("/tmp/.throttler") do |f|
25
- f.flock(File::LOCK_EX | File::LOCK_NB).should_not be_false
26
- end
27
- end
28
-
29
- context "by default" do
30
- it "names scope as `throttler`" do
31
- foo.throttle
32
- FileTest.exists?("/tmp/.throttler").should be_true
33
- end
34
-
35
- it "throttles for one second" do
36
- time = Benchmark.realtime do
37
- 2.times{ foo.throttle }
38
- end
39
-
40
- time.should be_within(0.1).of(1)
41
- end
42
- end
43
- end
44
- end