ld-eventsource 1.0.2 → 2.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.
@@ -38,10 +38,6 @@ module SSE
38
38
  good_duration = Time.now.to_f - @last_good_time
39
39
  @attempts = 0 if good_duration >= @reconnect_reset_interval
40
40
  end
41
- if @attempts == 0
42
- @attempts += 1
43
- return 0
44
- end
45
41
  @last_good_time = nil
46
42
  target = ([@base_interval * (2 ** @attempts), @max_interval].min).to_f
47
43
  @attempts += 1
@@ -1,3 +1,3 @@
1
1
  module SSE
2
- VERSION = "1.0.2"
2
+ VERSION = "2.0.1"
3
3
  end
@@ -0,0 +1,52 @@
1
+ require "ld-eventsource"
2
+
3
+ require "http_stub"
4
+
5
+ module SSE
6
+ module Impl
7
+ describe Backoff do
8
+ it "increases exponentially with jitter" do
9
+ initial = 1.5
10
+ max = 60
11
+ b = Backoff.new(initial, max)
12
+ previous = 0
13
+
14
+ for i in 1..6 do
15
+ interval = b.next_interval
16
+ expect(interval).to be > previous
17
+ target = initial * (2 ** (i - 1))
18
+ expect(interval).to be <= target
19
+ expect(interval).to be >= target / 2
20
+ previous = i
21
+ end
22
+
23
+ interval = b.next_interval
24
+ expect(interval).to be >= previous
25
+ expect(interval).to be <= max
26
+ end
27
+
28
+ it "resets to initial delay if reset threshold has elapsed" do
29
+ initial = 1.5
30
+ max = 60
31
+ threshold = 2
32
+ b = Backoff.new(initial, max, reconnect_reset_interval: threshold)
33
+
34
+ for i in 1..6 do
35
+ # just cause the backoff to increase quickly, don't actually do these delays
36
+ b.next_interval
37
+ end
38
+
39
+ b.mark_success
40
+ sleep(threshold + 0.001)
41
+
42
+ interval = b.next_interval
43
+ expect(interval).to be <= initial
44
+ expect(interval).to be >= initial / 2
45
+
46
+ interval = b.next_interval # make sure it continues increasing after that
47
+ expect(interval).to be <= (initial * 2)
48
+ expect(interval).to be >= initial
49
+ end
50
+ end
51
+ end
52
+ end
data/spec/client_spec.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  require "ld-eventsource"
2
- require "socketry"
3
- require "http_stub"
4
2
 
5
3
  #
6
4
  # End-to-end tests of the SSE client against a real server
@@ -62,8 +60,11 @@ EOT
62
60
  expect(received_req.header).to eq({
63
61
  "accept" => ["text/event-stream"],
64
62
  "cache-control" => ["no-cache"],
65
- "host" => ["127.0.0.1"],
66
- "authorization" => ["secret"]
63
+ "host" => ["127.0.0.1:" + server.port.to_s],
64
+ "authorization" => ["secret"],
65
+ "user-agent" => ["ruby-eventsource"],
66
+ "content-length" => ["0"],
67
+ "connection" => ["close"]
67
68
  })
68
69
  end
69
70
  end
@@ -85,9 +86,12 @@ EOT
85
86
  expect(received_req.header).to eq({
86
87
  "accept" => ["text/event-stream"],
87
88
  "cache-control" => ["no-cache"],
88
- "host" => ["127.0.0.1"],
89
+ "host" => ["127.0.0.1:" + server.port.to_s],
89
90
  "authorization" => ["secret"],
90
- "last-event-id" => [id]
91
+ "last-event-id" => [id],
92
+ "user-agent" => ["ruby-eventsource"],
93
+ "content-length" => ["0"],
94
+ "connection" => ["close"]
91
95
  })
92
96
  end
93
97
  end
@@ -330,7 +334,7 @@ EOT
330
334
  expect(event_sink.pop).to eq(simple_event_1)
331
335
  if i > 0
332
336
  interval = request_times[i] - request_end_times[i - 1]
333
- expect(interval).to be <= initial_interval
337
+ expect(interval).to be <= (initial_interval + 0.1)
334
338
  end
335
339
  end
336
340
  end
@@ -362,7 +366,63 @@ EOT
362
366
  with_client(client) do |client|
363
367
  expect(event_sink.pop).to eq(simple_event_1)
364
368
  interval = request_times[1] - request_times[0]
365
- expect(interval).to be < ((retry_ms.to_f / 1000) + 0.1)
369
+ expect(interval).to be < 0.5
370
+ end
371
+ end
372
+ end
373
+
374
+ it "connects to HTTP server through proxy" do
375
+ events_body = simple_event_1_text
376
+ with_server do |server|
377
+ server.setup_response("/") do |req,res|
378
+ send_stream_content(res, events_body, keep_open: false)
379
+ end
380
+ with_server(StubProxyServer.new) do |proxy|
381
+ event_sink = Queue.new
382
+ client = subject.new(server.base_uri, proxy: proxy.base_uri) do |c|
383
+ c.on_event { |event| event_sink << event }
384
+ end
385
+
386
+ with_client(client) do |client|
387
+ expect(event_sink.pop).to eq(simple_event_1)
388
+ expect(proxy.request_count).to eq(1)
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ it "resets read timeout between events" do
395
+ event_body = simple_event_1_text
396
+ with_server do |server|
397
+ attempt = 0
398
+ server.setup_response("/") do |req,res|
399
+ attempt += 1
400
+ if attempt == 1
401
+ stream = send_stream_content(res, event_body, keep_open: true)
402
+ Thread.new do
403
+ 2.times {
404
+ # write within timeout interval
405
+ sleep(0.75)
406
+ stream.write(event_body)
407
+ }
408
+ # cause timeout
409
+ sleep(1.25)
410
+ end
411
+ elsif attempt == 2
412
+ send_stream_content(res, event_body, keep_open: false)
413
+ end
414
+ end
415
+
416
+ event_sink = Queue.new
417
+ client = subject.new(server.base_uri, reconnect_time: reconnect_asap, read_timeout: 1) do |c|
418
+ c.on_event { |event| event_sink << event }
419
+ end
420
+
421
+ with_client(client) do |client|
422
+ 4.times {
423
+ expect(event_sink.pop).to eq(simple_event_1)
424
+ }
425
+ expect(attempt).to eq 2
366
426
  end
367
427
  end
368
428
  end
data/spec/http_stub.rb CHANGED
@@ -3,6 +3,8 @@ require "webrick/httpproxy"
3
3
  require "webrick/https"
4
4
 
5
5
  class StubHTTPServer
6
+ attr_reader :port
7
+
6
8
  def initialize
7
9
  @port = 50000
8
10
  begin
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ld-eventsource
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - LaunchDarkly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-10 00:00:00.000000000 Z
11
+ date: 2021-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: '1.7'
19
+ version: 2.2.10
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: '1.7'
26
+ version: 2.2.10
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rspec
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -53,47 +53,53 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.3.0
55
55
  - !ruby/object:Gem::Dependency
56
- name: concurrent-ruby
56
+ name: webrick
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.0'
62
- type: :runtime
61
+ version: '1.7'
62
+ type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.0'
68
+ version: '1.7'
69
69
  - !ruby/object:Gem::Dependency
70
- name: http_tools
70
+ name: concurrent-ruby
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 0.4.5
75
+ version: '1.0'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 0.4.5
82
+ version: '1.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: socketry
84
+ name: http
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 4.4.1
90
+ - - "<"
88
91
  - !ruby/object:Gem::Version
89
- version: 0.5.1
92
+ version: 6.0.0
90
93
  type: :runtime
91
94
  prerelease: false
92
95
  version_requirements: !ruby/object:Gem::Requirement
93
96
  requirements:
94
- - - "~>"
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 4.4.1
100
+ - - "<"
95
101
  - !ruby/object:Gem::Version
96
- version: 0.5.1
102
+ version: 6.0.0
97
103
  description: LaunchDarkly SSE client for Ruby
98
104
  email:
99
105
  - team@launchdarkly.com
@@ -103,10 +109,18 @@ extra_rdoc_files: []
103
109
  files:
104
110
  - ".circleci/config.yml"
105
111
  - ".gitignore"
112
+ - ".ldrelease/circleci/linux/execute.sh"
113
+ - ".ldrelease/circleci/mac/execute.sh"
114
+ - ".ldrelease/circleci/template/build.sh"
115
+ - ".ldrelease/circleci/template/gems-setup.sh"
116
+ - ".ldrelease/circleci/template/prepare.sh"
117
+ - ".ldrelease/circleci/template/publish.sh"
118
+ - ".ldrelease/circleci/template/test.sh"
119
+ - ".ldrelease/circleci/template/update-version.sh"
120
+ - ".ldrelease/circleci/windows/execute.ps1"
106
121
  - ".ldrelease/config.yml"
107
122
  - CHANGELOG.md
108
123
  - Gemfile
109
- - Gemfile.lock
110
124
  - LICENSE
111
125
  - README.md
112
126
  - ld-eventsource.gemspec
@@ -116,14 +130,13 @@ files:
116
130
  - lib/ld-eventsource/events.rb
117
131
  - lib/ld-eventsource/impl/backoff.rb
118
132
  - lib/ld-eventsource/impl/event_parser.rb
119
- - lib/ld-eventsource/impl/streaming_http.rb
120
133
  - lib/ld-eventsource/version.rb
121
134
  - scripts/gendocs.sh
122
135
  - scripts/release.sh
136
+ - spec/backoff_spec.rb
123
137
  - spec/client_spec.rb
124
138
  - spec/event_parser_spec.rb
125
139
  - spec/http_stub.rb
126
- - spec/streaming_http_spec.rb
127
140
  homepage: https://github.com/launchdarkly/ruby-eventsource
128
141
  licenses:
129
142
  - Apache-2.0
@@ -143,13 +156,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
156
  - !ruby/object:Gem::Version
144
157
  version: '0'
145
158
  requirements: []
146
- rubyforge_project:
147
- rubygems_version: 2.5.2.3
159
+ rubygems_version: 3.2.15
148
160
  signing_key:
149
161
  specification_version: 4
150
162
  summary: LaunchDarkly SSE client
151
163
  test_files:
164
+ - spec/backoff_spec.rb
152
165
  - spec/client_spec.rb
153
166
  - spec/event_parser_spec.rb
154
167
  - spec/http_stub.rb
155
- - spec/streaming_http_spec.rb
data/Gemfile.lock DELETED
@@ -1,46 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- ld-eventsource (1.0.2)
5
- concurrent-ruby (~> 1.0)
6
- http_tools (~> 0.4.5)
7
- socketry (~> 0.5.1)
8
-
9
- GEM
10
- remote: https://rubygems.org/
11
- specs:
12
- concurrent-ruby (1.1.6)
13
- diff-lcs (1.3)
14
- hitimes (1.3.1)
15
- hitimes (1.3.1-java)
16
- http_tools (0.4.5)
17
- rspec (3.7.0)
18
- rspec-core (~> 3.7.0)
19
- rspec-expectations (~> 3.7.0)
20
- rspec-mocks (~> 3.7.0)
21
- rspec-core (3.7.1)
22
- rspec-support (~> 3.7.0)
23
- rspec-expectations (3.7.0)
24
- diff-lcs (>= 1.2.0, < 2.0)
25
- rspec-support (~> 3.7.0)
26
- rspec-mocks (3.7.0)
27
- diff-lcs (>= 1.2.0, < 2.0)
28
- rspec-support (~> 3.7.0)
29
- rspec-support (3.7.0)
30
- rspec_junit_formatter (0.3.0)
31
- rspec-core (>= 2, < 4, != 2.12.0)
32
- socketry (0.5.1)
33
- hitimes (~> 1.2)
34
-
35
- PLATFORMS
36
- java
37
- ruby
38
-
39
- DEPENDENCIES
40
- bundler (~> 1.7)
41
- ld-eventsource!
42
- rspec (~> 3.2)
43
- rspec_junit_formatter (~> 0.3.0)
44
-
45
- BUNDLED WITH
46
- 1.17.3
@@ -1,222 +0,0 @@
1
- require "ld-eventsource/errors"
2
-
3
- require "concurrent/atomics"
4
- require "http_tools"
5
- require "socketry"
6
-
7
- module SSE
8
- module Impl
9
- #
10
- # Wrapper around a socket providing a simplified HTTP request-response cycle including streaming.
11
- # The socket is created and managed by Socketry, which we use so that we can have a read timeout.
12
- #
13
- class StreamingHTTPConnection
14
- attr_reader :status, :headers
15
-
16
- #
17
- # Opens a new connection.
18
- #
19
- # @param [String] uri the URI to connect o
20
- # @param [String] proxy the proxy server URI, if any
21
- # @param [Hash] headers request headers
22
- # @param [Float] connect_timeout connection timeout
23
- # @param [Float] read_timeout read timeout
24
- #
25
- def initialize(uri, proxy: nil, headers: {}, connect_timeout: nil, read_timeout: nil)
26
- @socket = HTTPConnectionFactory.connect(uri, proxy, connect_timeout, read_timeout)
27
- @socket.write(build_request(uri, headers))
28
- @reader = HTTPResponseReader.new(@socket, read_timeout)
29
- @status = @reader.status
30
- @headers = @reader.headers
31
- @closed = Concurrent::AtomicBoolean.new(false)
32
- end
33
-
34
- #
35
- # Closes the connection.
36
- #
37
- def close
38
- if @closed.make_true
39
- @socket.close if @socket
40
- @socket = nil
41
- end
42
- end
43
-
44
- #
45
- # Generator that returns one line of the response body at a time (delimited by \r, \n,
46
- # or \r\n) until the response is fully consumed or the socket is closed.
47
- #
48
- def read_lines
49
- @reader.read_lines
50
- end
51
-
52
- #
53
- # Consumes the entire response body and returns it.
54
- #
55
- # @return [String] the response body
56
- #
57
- def read_all
58
- @reader.read_all
59
- end
60
-
61
- private
62
-
63
- # Build an HTTP request line and headers.
64
- def build_request(uri, headers)
65
- ret = "GET #{uri.request_uri} HTTP/1.1\r\n"
66
- ret << "Host: #{uri.host}\r\n"
67
- headers.each { |k, v|
68
- ret << "#{k}: #{v}\r\n"
69
- }
70
- ret + "\r\n"
71
- end
72
- end
73
-
74
- #
75
- # Used internally to send the HTTP request, including the proxy dialogue if necessary.
76
- # @private
77
- #
78
- class HTTPConnectionFactory
79
- def self.connect(uri, proxy, connect_timeout, read_timeout)
80
- if !proxy
81
- return open_socket(uri, connect_timeout)
82
- end
83
-
84
- socket = open_socket(proxy, connect_timeout)
85
- socket.write(build_proxy_request(uri, proxy))
86
-
87
- # temporarily create a reader just for the proxy connect response
88
- proxy_reader = HTTPResponseReader.new(socket, read_timeout)
89
- if proxy_reader.status != 200
90
- raise Errors::HTTPProxyError.new(proxy_reader.status)
91
- end
92
-
93
- # start using TLS at this point if appropriate
94
- if uri.scheme.downcase == 'https'
95
- wrap_socket_in_ssl_socket(socket)
96
- else
97
- socket
98
- end
99
- end
100
-
101
- private
102
-
103
- def self.open_socket(uri, connect_timeout)
104
- if uri.scheme.downcase == 'https'
105
- Socketry::SSL::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
106
- else
107
- Socketry::TCP::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
108
- end
109
- end
110
-
111
- # Build a proxy connection header.
112
- def self.build_proxy_request(uri, proxy)
113
- ret = "CONNECT #{uri.host}:#{uri.port} HTTP/1.1\r\n"
114
- ret << "Host: #{uri.host}:#{uri.port}\r\n"
115
- if proxy.user || proxy.password
116
- encoded_credentials = Base64.strict_encode64([proxy.user || '', proxy.password || ''].join(":"))
117
- ret << "Proxy-Authorization: Basic #{encoded_credentials}\r\n"
118
- end
119
- ret << "\r\n"
120
- ret
121
- end
122
-
123
- def self.wrap_socket_in_ssl_socket(socket)
124
- io = IO.try_convert(socket)
125
- ssl_sock = OpenSSL::SSL::SSLSocket.new(io, OpenSSL::SSL::SSLContext.new)
126
- ssl_sock.connect
127
- Socketry::SSL::Socket.new.from_socket(ssl_sock)
128
- end
129
- end
130
-
131
- #
132
- # Used internally to read the HTTP response, either all at once or as a stream of text lines.
133
- # Incoming data is fed into an instance of HTTPTools::Parser, which gives us the header and
134
- # chunks of the body via callbacks.
135
- # @private
136
- #
137
- class HTTPResponseReader
138
- DEFAULT_CHUNK_SIZE = 10000
139
-
140
- attr_reader :status, :headers
141
-
142
- def initialize(socket, read_timeout)
143
- @socket = socket
144
- @read_timeout = read_timeout
145
- @parser = HTTPTools::Parser.new
146
- @buffer = ""
147
- @done = false
148
- @lock = Mutex.new
149
-
150
- # Provide callbacks for the Parser to give us the headers and body. This has to be done
151
- # before we start piping any data into the parser.
152
- have_headers = false
153
- @parser.on(:header) do
154
- have_headers = true
155
- end
156
- @parser.on(:stream) do |data|
157
- @lock.synchronize { @buffer << data } # synchronize because we're called from another thread in Socketry
158
- end
159
- @parser.on(:finish) do
160
- @lock.synchronize { @done = true }
161
- end
162
-
163
- # Block until the status code and headers have been successfully read.
164
- while !have_headers
165
- raise EOFError if !read_chunk_into_buffer
166
- end
167
- @headers = Hash[@parser.header.map { |k,v| [k.downcase, v] }]
168
- @status = @parser.status_code
169
- end
170
-
171
- def read_lines
172
- Enumerator.new do |gen|
173
- loop do
174
- line = read_line
175
- break if line.nil?
176
- gen.yield line
177
- end
178
- end
179
- end
180
-
181
- def read_all
182
- while read_chunk_into_buffer
183
- end
184
- @buffer
185
- end
186
-
187
- private
188
-
189
- # Attempt to read some more data from the socket. Return true if successful, false if EOF.
190
- # A read timeout will result in an exception from Socketry's readpartial method.
191
- def read_chunk_into_buffer
192
- # If @done is set, it means the Parser has signaled end of response body
193
- @lock.synchronize { return false if @done }
194
- begin
195
- data = @socket.readpartial(DEFAULT_CHUNK_SIZE, timeout: @read_timeout)
196
- rescue Socketry::TimeoutError
197
- # We rethrow this as our own type so the caller doesn't have to know the Socketry API
198
- raise Errors::ReadTimeoutError.new(@read_timeout)
199
- end
200
- return false if data == :eof
201
- @parser << data
202
- # We are piping the content through the parser so that it can handle things like chunked
203
- # encoding for us. The content ends up being appended to @buffer via our callback.
204
- true
205
- end
206
-
207
- # Extract the next line of text from the read buffer, refilling the buffer as needed.
208
- def read_line
209
- loop do
210
- @lock.synchronize do
211
- i = @buffer.index(/[\r\n]/)
212
- if !i.nil?
213
- i += 1 if (@buffer[i] == "\r" && i < @buffer.length - 1 && @buffer[i + 1] == "\n")
214
- return @buffer.slice!(0, i + 1).force_encoding(Encoding::UTF_8)
215
- end
216
- end
217
- return nil if !read_chunk_into_buffer
218
- end
219
- end
220
- end
221
- end
222
- end