net-http-follow_tail 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -f doc
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'exponential-backoff'
4
+ gem 'reindeer'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ gem 'webmock'
10
+ end
11
+
12
+ gem 'pry', group: 'development'
data/Gemfile.lock ADDED
@@ -0,0 +1,38 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.3.4)
5
+ coderay (1.0.7)
6
+ crack (0.3.2)
7
+ diff-lcs (1.2.3)
8
+ exponential-backoff (0.0.2)
9
+ method_source (0.8)
10
+ pry (0.9.12.1)
11
+ coderay (~> 1.0.5)
12
+ method_source (~> 0.8)
13
+ slop (~> 3.4)
14
+ rake (10.0.4)
15
+ reindeer (0.0.1)
16
+ rspec (2.13.0)
17
+ rspec-core (~> 2.13.0)
18
+ rspec-expectations (~> 2.13.0)
19
+ rspec-mocks (~> 2.13.0)
20
+ rspec-core (2.13.1)
21
+ rspec-expectations (2.13.0)
22
+ diff-lcs (>= 1.1.3, < 2.0)
23
+ rspec-mocks (2.13.1)
24
+ slop (3.4.4)
25
+ webmock (1.11.0)
26
+ addressable (>= 2.2.7)
27
+ crack (>= 0.3.2)
28
+
29
+ PLATFORMS
30
+ ruby
31
+
32
+ DEPENDENCIES
33
+ exponential-backoff
34
+ pry
35
+ rake
36
+ reindeer
37
+ rspec
38
+ webmock
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # Net::HTTP::FollowTail
2
+
3
+ Fulfils the same role as `tail -f` but for files over HTTP. That is to
4
+ say if you have log files (e.g IRC logs) available at a URL you could
5
+ follow them with this module.
6
+
7
+ # Usage
8
+
9
+ ```ruby
10
+ require 'net/http/follow_tail'
11
+
12
+ Net::HTTP::FollowTail.follow(uri: 'http://example.com/irc.log') do |result, tailer|
13
+ puts "Someone on IRC said: ", result.content
14
+ end
15
+ ```
16
+
17
+ # Interface
18
+
19
+ If you're desiring of a URI's tail then the simplest way of using this
20
+ module is to use the `follow` class method on `Net::HTTP::FollowTail`.
21
+ It's first argument should be a hash, or array of hashes, containing
22
+ at least `uri` key with a value that's either a `URI::HTTP` instance
23
+ or something that would `URI.parse` to one. It also expects a block
24
+ which gets executed whenever new data appears at the tail at any of
25
+ the URIs. That's demonstrated in the *Usage* example above.
26
+
27
+ Other data that can be passed in the hash argument(s) are:
28
+
29
+ - `wait_in_seconds`: How long to wait in seconds between polls.
30
+ - `offset`: An offset in `Fixnum` bytes to start at.
31
+ - `max_retries`: The number of times to retry in the face of failure
32
+ before giving up.
33
+ - `always_callback`: A boolean indicating that the callback should be
34
+ called even the tail request wasn't successful.
35
+
36
+ The callback is called with two arguments - a
37
+ `Net::HTTP::FollowTail::Result` instance and a
38
+ `Net::HTTP::FollowTail::Tailer` instance respectively. The former
39
+ exposing the result of the most recent tail request at the latter the
40
+ current tailing state. By default it is only called when the tail
41
+ request was successful.
42
+
43
+ # Author
44
+
45
+ Dan Brook `<dan@broquaint.com>`
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec) do |config|
4
+ # config.rcov = true
5
+ end
6
+
7
+ task :default => :spec
@@ -0,0 +1,156 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'exponential_backoff'
4
+ require 'reindeer'
5
+
6
+ class Net::HTTP::FollowTail
7
+ class Result < Reindeer
8
+ has :state, is_a: Symbol
9
+ has :method, is_a: Symbol
10
+ has :response, is_a: Net::HTTPSuccess
11
+
12
+ def has_response?
13
+ not @response.nil?
14
+ end
15
+
16
+ def is_success?
17
+ @state == :success
18
+ end
19
+ def is_error?
20
+ @state == :error
21
+ end
22
+
23
+ def content
24
+ response.body
25
+ end
26
+ end
27
+
28
+ class Tailer < Reindeer
29
+ has :uri, required: true
30
+ has :offset, is_a: Fixnum, default: -> { 0 }
31
+ has :wait_in_seconds, is_a: Fixnum, default: -> { 60 }
32
+ has :exponential_backoff, lazy_build: true
33
+ has :max_retries, is_a: Fixnum, default: -> { 5 }
34
+ has :retries_so_far, is_a: Fixnum, default: -> { 0 }
35
+ has :still_following, is: :rw, default: -> { true }
36
+
37
+ def build(opts)
38
+ @uri = opts[:uri].kind_of?(URI::HTTP) ? opts[:uri] : URI.parse(opts[:uri])
39
+ end
40
+
41
+ def still_following?
42
+ @still_following
43
+ end
44
+
45
+ # This and regular_wait need new names!
46
+ def error_wait
47
+ if exponential_backoff.length > 1
48
+ exponential_backoff.shift
49
+ else
50
+ exponential_backoff.first
51
+ end
52
+ end
53
+ def regular_wait
54
+ @exponential_backoff = get_backoff_list
55
+ @retries_so_far = 0
56
+ wait_in_seconds
57
+ end
58
+
59
+ def update_offset(offset_increment)
60
+ @offset += offset_increment.to_i
61
+ end
62
+
63
+ def head_request
64
+ http = Net::HTTP.new(uri.host, uri.port)
65
+ http.request( Net::HTTP::Head.new(uri.to_s) )
66
+ end
67
+
68
+ def get_request(size_now)
69
+ http = Net::HTTP.new(uri.host, uri.port)
70
+ req = Net::HTTP::Get.new(uri.to_s)
71
+ req.initialize_http_header('Range' => "bytes=#{offset}-#{size_now}")
72
+ http.request(req)
73
+ end
74
+
75
+ def tail
76
+ @retries_so_far += 1
77
+
78
+ begin
79
+ head_response = head_request
80
+ rescue Timeout::Error, SocketError, EOFError, Errno::ETIMEDOUT
81
+ return Result.new(state: :error, method: :head)
82
+ end
83
+
84
+ # TODO invoke head_response.value to check for non 200s.
85
+
86
+ size_now = head_response.content_length
87
+ if size_now == offset
88
+ return Result.new(state: :no_change, method: :head, response: head_response)
89
+ end
90
+
91
+ begin
92
+ get_response = get_request(size_now)
93
+ rescue Timeout::Error, SocketError, EOFError
94
+ return Result.new(state: :error, method: :get)
95
+ end
96
+
97
+ update_offset get_response.content_length
98
+
99
+ # yield get_response, offset_for(uri)
100
+ return Result.new(state: :success, method: :get, response: get_response)
101
+ end
102
+
103
+ private
104
+
105
+ def get_backoff_list
106
+ ExponentialBackoff.new(
107
+ wait_in_seconds, wait_in_seconds ** 2
108
+ ).intervals_for(0 .. max_retries)
109
+ end
110
+
111
+ def build_exponential_backoff
112
+ get_backoff_list
113
+ end
114
+ end
115
+
116
+ def self.follow(opts, &block)
117
+ tailers = normalize_options(opts).collect do |o|
118
+ {t: Tailer.new(o), ac: o[:always_callback]}
119
+ end
120
+
121
+ while tailers.any? {|h| h[:t].still_following?}
122
+ for tailer in tailers.select{|h| h[:t].still_following?}
123
+ get_tail tailer[:t], tailer[:ac], block
124
+ end
125
+ end
126
+ end
127
+
128
+ def self.normalize_options(opts)
129
+ return [opts] if opts.is_a?(Hash)
130
+
131
+ raise ArgumentError, "Expected a Hash or Array not a #{opts}" unless opts.is_a? Array
132
+
133
+ opts
134
+ end
135
+
136
+ def self.get_tail(tailer, always_callback, block)
137
+ result = tailer.tail
138
+
139
+ while result.is_error?
140
+ block.call(result, tailer) if always_callback
141
+ return unless tailer.still_following?
142
+
143
+ if tailer.retries_so_far >= tailer.max_retries
144
+ # Would throw an exception but that breaks out of the #follow loop too.
145
+ tailer.still_following = false
146
+ return
147
+ end
148
+ sleep tailer.error_wait
149
+ result = tailer.tail
150
+ end
151
+
152
+ block.call(result, tailer) if result.is_success? or always_callback
153
+
154
+ sleep tailer.regular_wait
155
+ end
156
+ end
@@ -0,0 +1,95 @@
1
+ require 'net/http/follow_tail'
2
+ require 'spec_helper'
3
+
4
+ describe Net::HTTP::FollowTail do
5
+ # Not sure if this is appropriate but it does the job.
6
+ before(:each, simple_stub_request: true) do
7
+ stub_request(:head, 'example.com')
8
+ .to_return(headers: { 'Content-Length' => 321 })
9
+ stub_request(:get, 'example.com')
10
+ .with(headers: {'Range' => 'bytes=0-321'})
11
+ .to_return(headers: { 'Content-Length' => 321 })
12
+ Net::HTTP::FollowTail.should_receive(:sleep).with(60)
13
+ end
14
+
15
+ describe '#follow' do
16
+ it 'calls a block with a result and a tailer', simple_stub_request: true do
17
+ Net::HTTP::FollowTail.follow(uri: 'http://example.com/') do |result, tailer|
18
+ expect(result).to be_an_instance_of(Net::HTTP::FollowTail::Result)
19
+ expect(result.is_success?).to be_true
20
+ tailer.still_following = false
21
+ end
22
+ end
23
+
24
+ it 'should accept an Array of options', simple_stub_request: true do
25
+ # Would use multiple items but failing to stub sleep correctly :/
26
+ opts = [{uri: 'http://example.com/'}]
27
+ Net::HTTP::FollowTail.follow(opts) do |result, tailer|
28
+ expect(result).to be_an_instance_of(Net::HTTP::FollowTail::Result)
29
+ expect(result.is_success?).to be_true
30
+ tailer.still_following = false
31
+ end
32
+ end
33
+
34
+ it 'raises an error for weird input' do
35
+ expect {
36
+ Net::HTTP::FollowTail.follow(:boom)
37
+ }.to raise_error(ArgumentError)
38
+ end
39
+ end
40
+
41
+ describe '#get_tail' do
42
+ it 'should make a request and call a block', simple_stub_request: true do
43
+ a_tailer = Net::HTTP::FollowTail::Tailer.new(uri: 'http://example.com')
44
+ Net::HTTP::FollowTail.get_tail(a_tailer, false, Proc.new{ |result, tailer|
45
+ expect(tailer).to eql(a_tailer)
46
+ expect(result.is_success?).to be_true
47
+ })
48
+ end
49
+
50
+ # Incidentally tests always_callback which was introduced to allow
51
+ # this testing to work.
52
+ it 'should retry when an error is received' do
53
+ stub_request(:head, 'example.com').to_timeout
54
+ Net::HTTP::FollowTail.stub(:sleep) { }
55
+
56
+ # A bit gross but effective.
57
+ call_count = 0
58
+
59
+ a_tailer = Net::HTTP::FollowTail::Tailer.new(uri: 'http://example.com')
60
+ Net::HTTP::FollowTail.get_tail(a_tailer, true, Proc.new{ |result, tailer|
61
+ if call_count == 1
62
+ expect(result.is_success?).to be_true
63
+ call_count += 1
64
+ else
65
+ expect(result.is_success?).to be_false
66
+ call_count += 1
67
+
68
+ stub_request(:head, 'example.com')
69
+ .to_return(headers: { 'Content-Length' => 321 })
70
+ stub_request(:get, 'example.com')
71
+ .with(headers: {'Range' => 'bytes=0-321'})
72
+ .to_return(headers: { 'Content-Length' => 321 })
73
+ end
74
+ })
75
+ expect(call_count).to eq(2)
76
+ end
77
+
78
+ it 'exit loop when max_retries is hit' do
79
+ stub_request(:head, 'example.com').to_timeout
80
+ Net::HTTP::FollowTail.stub(:sleep) { }
81
+
82
+ a_tailer = Net::HTTP::FollowTail::Tailer.new(
83
+ uri: 'http://example.com',
84
+ max_retries: 2
85
+ )
86
+
87
+ Net::HTTP::FollowTail.get_tail(a_tailer, true, Proc.new{ |result, tailer|
88
+ expect(result.is_success?).to be_false
89
+ })
90
+
91
+ expect(a_tailer.still_following?).to be_false
92
+ expect(a_tailer.retries_so_far).to eq(2)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,8 @@
1
+ require 'webmock/rspec'
2
+
3
+ RSpec.configure do |config|
4
+ # No monkey patching please.
5
+ config.expect_with :rspec do |c|
6
+ c.syntax = :expect
7
+ end
8
+ end
@@ -0,0 +1,182 @@
1
+ require 'net/http/follow_tail'
2
+ require 'spec_helper'
3
+
4
+ describe Net::HTTP::FollowTail::Tailer do
5
+ let(:example_uri) { 'http://example.com/' }
6
+ let(:example_host) { 'example.com' }
7
+ let(:simple_tailer) { Net::HTTP::FollowTail::Tailer.new(uri: example_uri) }
8
+ let(:default_wait) { 60 }
9
+
10
+ describe '#new' do
11
+ it 'has sensible defaults' do
12
+ tailer = simple_tailer
13
+ expect(tailer.uri).to be_an_instance_of(URI::HTTP)
14
+ expect(tailer.offset).to eq(0)
15
+ expect(tailer.wait_in_seconds).to eq(default_wait)
16
+ expect(tailer.max_retries).to eq(5)
17
+ expect(tailer.retries_so_far).to eq(0)
18
+ end
19
+
20
+ it 'requires a uri to be specified' do
21
+ expect {
22
+ Net::HTTP::FollowTail::Tailer.new
23
+ }.to raise_error(Reindeer::Meta::Attribute::AttributeError)
24
+ end
25
+
26
+ it 'accepts a URI instance' do
27
+ uri = URI.parse(example_uri)
28
+ tailer = Net::HTTP::FollowTail::Tailer.new(uri: uri)
29
+ expect(tailer.uri).to eql(uri)
30
+ end
31
+
32
+ it 'accepts offset, wait & max_retries options' do
33
+ tailer = Net::HTTP::FollowTail::Tailer.new(
34
+ uri: example_uri,
35
+ offset: 1234,
36
+ wait_in_seconds: 20,
37
+ max_retries: 2,
38
+ )
39
+ expect(tailer.uri).to be_an_instance_of(URI::HTTP)
40
+ expect(tailer.offset).to eq(1234)
41
+ expect(tailer.wait_in_seconds).to eq(20)
42
+ expect(tailer.max_retries).to eq(2)
43
+ end
44
+ end
45
+
46
+ describe '.regular_wait' do
47
+ it 'always returns wait_in_seconds' do
48
+ tailer = Net::HTTP::FollowTail::Tailer.new(
49
+ uri: example_uri,
50
+ wait_in_seconds: 66,
51
+ )
52
+
53
+ expect(tailer.wait_in_seconds).to eq(66)
54
+ end
55
+ end
56
+
57
+ describe '.error_wait' do
58
+ it 'munges state appropriately' do
59
+ tailer = simple_tailer
60
+
61
+ expect(tailer.wait_in_seconds).to eq(default_wait)
62
+ expect(tailer.error_wait).to eq(default_wait)
63
+ expect(tailer.error_wait).to be > default_wait
64
+ end
65
+ end
66
+
67
+ describe '.head_request' do
68
+ it 'provides a response' do
69
+ stub_request :head, "example.com"
70
+ expect(simple_tailer.head_request).to be_an_instance_of(Net::HTTPOK)
71
+ end
72
+ end
73
+
74
+ describe '.get_request' do
75
+ it 'should make ranged requests' do
76
+ stub_request(:get, example_host).with(headers: {'Range' => 'bytes=0-200'})
77
+ expect(simple_tailer.get_request(200)).to be_an_instance_of(Net::HTTPOK)
78
+ end
79
+
80
+ it 'makes range requests against an offset' do
81
+ tailer = simple_tailer
82
+ tailer.update_offset 200
83
+ stub_request(:get, example_host).with(headers: {'Range' => 'bytes=200-400'})
84
+ expect(tailer.get_request(400)).to be_an_instance_of(Net::HTTPOK)
85
+ end
86
+ end
87
+
88
+ describe '.tail' do
89
+ it 'returns a result object' do
90
+ stub_request(:head, example_host)
91
+ .to_return(headers: { 'Content-Length' => 321 })
92
+ stub_request(:get, example_host)
93
+ .with(headers: {'Range' => 'bytes=0-321'})
94
+ .to_return(headers: { 'Content-Length' => 321 })
95
+
96
+ result = simple_tailer.tail
97
+ expect(result).to be_an_instance_of(Net::HTTP::FollowTail::Result)
98
+ expect(result.is_success?).to be_true
99
+ end
100
+
101
+ it 'updates offset state with multiple tail calls' do
102
+ stub_request(:head, example_host)
103
+ .to_return(headers: { 'Content-Length' => 5 })
104
+ stub_request(:get, example_host)
105
+ .with(headers: {'Range' => 'bytes=0-5'})
106
+ .to_return(headers: { 'Content-Length' => 5 })
107
+
108
+ tailer = simple_tailer
109
+ tailer.tail
110
+
111
+ stub_request(:head, example_host)
112
+ .to_return(headers: { 'Content-Length' => 10 })
113
+ stub_request(:get, example_host)
114
+ .with(headers: {'Range' => 'bytes=5-10'})
115
+ .to_return(headers: { 'Content-Length' => 5 })
116
+
117
+ result = tailer.tail
118
+ expect(result).to be_an_instance_of(Net::HTTP::FollowTail::Result)
119
+ expect(result.is_success?).to be_true
120
+ expect(result.method).to be(:get)
121
+ expect(tailer.offset).to eq(10)
122
+
123
+ stub_request(:head, example_host)
124
+ .to_return(headers: { 'Content-Length' => 15 })
125
+ stub_request(:get, example_host)
126
+ .with(headers: {'Range' => 'bytes=10-15'})
127
+ .to_return(headers: { 'Content-Length' => 5 })
128
+
129
+ result = tailer.tail
130
+ expect(tailer.offset).to eq(15)
131
+ end
132
+
133
+ it 'correctly updates against existing offset' do
134
+ stub_request(:head, example_host)
135
+ .to_return(headers: { 'Content-Length' => 66 })
136
+ stub_request(:get, example_host)
137
+ .with(headers: {'Range' => 'bytes=50-66'})
138
+ .to_return(headers: { 'Content-Length' => 16 })
139
+
140
+ tailer = Net::HTTP::FollowTail::Tailer.new(uri: example_uri, offset: 50)
141
+ tailer.tail
142
+ expect(tailer.offset).to eq(66)
143
+ end
144
+
145
+ it 'to handle HEAD errors' do
146
+ stub_request(:head, example_host).to_timeout
147
+
148
+ result = simple_tailer.tail
149
+ expect(result.is_error?).to be_true
150
+ expect(result.method).to be(:head)
151
+ expect(result.has_response?).to be_false
152
+ end
153
+
154
+ it 'to handle GET errors' do
155
+ stub_request(:head, example_host)
156
+ .to_return(headers: { 'Content-Length' => 10 })
157
+ stub_request(:get, example_host).to_timeout
158
+
159
+ result = simple_tailer.tail
160
+ expect(result.is_error?).to be_true
161
+ expect(result.method).to be(:get)
162
+ expect(result.has_response?).to be_false
163
+ end
164
+
165
+ it 'returns a no change Result when appropriate' do
166
+ stub_request(:head, example_host)
167
+ .to_return(headers: { 'Content-Length' => 25 })
168
+
169
+ tailer = Net::HTTP::FollowTail::Tailer.new(
170
+ uri: example_uri,
171
+ offset: 25
172
+ )
173
+
174
+ result = tailer.tail
175
+ expect(result.state).to eq(:no_change)
176
+ expect(result.method).to eq(:head)
177
+ expect(result.has_response?).to be_true
178
+ expect(result.is_success?).to be_false
179
+ expect(result.is_error?).to be_false
180
+ end
181
+ end
182
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: net-http-follow_tail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dan Brook
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: exponential-backoff
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.0.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.0.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.9.2
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.9.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ description: Watch multiple URIs for appended content e.g log files
63
+ email: dan@broquaint.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - .rspec
69
+ - Gemfile
70
+ - Gemfile.lock
71
+ - README.md
72
+ - Rakefile
73
+ - lib/net/http/follow_tail.rb
74
+ - spec/follow_tail_spec.rb
75
+ - spec/spec_helper.rb
76
+ - spec/tailer_spec.rb
77
+ homepage: http://github.com/broquaint/net-http-follow_tail
78
+ licenses: []
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '1.9'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 1.8.24
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: Like tail -f for the web
101
+ test_files:
102
+ - spec/follow_tail_spec.rb
103
+ - spec/spec_helper.rb
104
+ - spec/tailer_spec.rb