net-http-follow_tail 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/.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