ld-eventsource 1.0.0
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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +90 -0
- data/.gitignore +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +47 -0
- data/LICENSE +13 -0
- data/README.md +45 -0
- data/Rakefile +5 -0
- data/ld-eventsource.gemspec +31 -0
- data/lib/ld-eventsource.rb +14 -0
- data/lib/ld-eventsource/client.rb +296 -0
- data/lib/ld-eventsource/errors.rb +67 -0
- data/lib/ld-eventsource/events.rb +16 -0
- data/lib/ld-eventsource/impl/backoff.rb +60 -0
- data/lib/ld-eventsource/impl/event_parser.rb +81 -0
- data/lib/ld-eventsource/impl/streaming_http.rb +222 -0
- data/lib/ld-eventsource/version.rb +3 -0
- data/scripts/gendocs.sh +12 -0
- data/scripts/release.sh +30 -0
- data/spec/client_spec.rb +346 -0
- data/spec/event_parser_spec.rb +100 -0
- data/spec/http_stub.rb +81 -0
- data/spec/streaming_http_spec.rb +263 -0
- metadata +169 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 294a32bc78de81b8f7c5ffa0a8b9ffc25847b0ecd20eee8f27caea576ae2b6fc
|
4
|
+
data.tar.gz: 5c0bb7736f090364098525477ec192b89ee02b96e0d12542bf72f08112180ba2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b6466b0b4c060b681640e2b47cc1844bb93a9588076fbcc901a5922be303860538d6e064d1f9ea897062ffeafcb86690e68e26e0c28d6f795ea68bb41dc7d364
|
7
|
+
data.tar.gz: 67a62d448f673893e9fc397cb536ad6ba4194292e3f8c3058d96bcc39b0f8bf3ce6bf12b1bdeb8e843ff962e3324505b9cddc3e42486cdb915456dbe1dba4dcc
|
@@ -0,0 +1,90 @@
|
|
1
|
+
version: 2
|
2
|
+
|
3
|
+
workflows:
|
4
|
+
version: 2
|
5
|
+
test:
|
6
|
+
jobs:
|
7
|
+
- test-misc-rubies
|
8
|
+
- test-2.2
|
9
|
+
- test-2.3
|
10
|
+
- test-2.4
|
11
|
+
- test-2.5
|
12
|
+
- test-jruby-9.2
|
13
|
+
|
14
|
+
ruby-docker-template: &ruby-docker-template
|
15
|
+
steps:
|
16
|
+
- checkout
|
17
|
+
- run: |
|
18
|
+
if [[ $CIRCLE_JOB == test-jruby* ]]; then
|
19
|
+
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
|
20
|
+
fi
|
21
|
+
- run: ruby -v
|
22
|
+
- run: gem install bundler -v "~> 1.17"
|
23
|
+
- run: bundle install
|
24
|
+
- run: mkdir ./rspec
|
25
|
+
- run: bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
|
26
|
+
- store_test_results:
|
27
|
+
path: ./rspec
|
28
|
+
- store_artifacts:
|
29
|
+
path: ./rspec
|
30
|
+
|
31
|
+
jobs:
|
32
|
+
test-2.2:
|
33
|
+
<<: *ruby-docker-template
|
34
|
+
docker:
|
35
|
+
- image: circleci/ruby:2.2.10-jessie
|
36
|
+
test-2.3:
|
37
|
+
<<: *ruby-docker-template
|
38
|
+
docker:
|
39
|
+
- image: circleci/ruby:2.3.7-jessie
|
40
|
+
test-2.4:
|
41
|
+
<<: *ruby-docker-template
|
42
|
+
docker:
|
43
|
+
- image: circleci/ruby:2.4.5-stretch
|
44
|
+
test-2.5:
|
45
|
+
<<: *ruby-docker-template
|
46
|
+
docker:
|
47
|
+
- image: circleci/ruby:2.5.3-stretch
|
48
|
+
test-jruby-9.2:
|
49
|
+
<<: *ruby-docker-template
|
50
|
+
docker:
|
51
|
+
- image: circleci/jruby:9-jdk
|
52
|
+
|
53
|
+
# The following very slow job uses an Ubuntu container to run the Ruby versions that
|
54
|
+
# CircleCI doesn't provide Docker images for.
|
55
|
+
test-misc-rubies:
|
56
|
+
machine:
|
57
|
+
image: circleci/classic:latest
|
58
|
+
environment:
|
59
|
+
- RUBIES: "jruby-9.1.17.0"
|
60
|
+
steps:
|
61
|
+
- checkout
|
62
|
+
- run:
|
63
|
+
name: install all Ruby versions
|
64
|
+
command: "parallel rvm install ::: $RUBIES"
|
65
|
+
- run:
|
66
|
+
name: bundle install for all versions
|
67
|
+
shell: /bin/bash -leo pipefail # need -l in order for "rvm use" to work
|
68
|
+
command: |
|
69
|
+
set -e;
|
70
|
+
for i in $RUBIES;
|
71
|
+
do
|
72
|
+
rvm use $i;
|
73
|
+
if [[ $i == jruby* ]]; then
|
74
|
+
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
|
75
|
+
fi
|
76
|
+
gem install bundler -v "~> 1.17";
|
77
|
+
bundle install;
|
78
|
+
mv Gemfile.lock "Gemfile.lock.$i"
|
79
|
+
done
|
80
|
+
- run:
|
81
|
+
name: run tests for all versions
|
82
|
+
shell: /bin/bash -leo pipefail
|
83
|
+
command: |
|
84
|
+
set -e;
|
85
|
+
for i in $RUBIES;
|
86
|
+
do
|
87
|
+
rvm use $i;
|
88
|
+
cp "Gemfile.lock.$i" Gemfile.lock;
|
89
|
+
bundle exec rspec spec;
|
90
|
+
done
|
data/.gitignore
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
ld-eventsource (1.0.0)
|
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.0.5)
|
13
|
+
diff-lcs (1.3)
|
14
|
+
hitimes (1.3.0)
|
15
|
+
http_tools (0.4.5)
|
16
|
+
rake (10.5.0)
|
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
|
+
rake (~> 10.0)
|
43
|
+
rspec (~> 3.2)
|
44
|
+
rspec_junit_formatter (~> 0.3.0)
|
45
|
+
|
46
|
+
BUNDLED WITH
|
47
|
+
1.17.3
|
data/LICENSE
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright 2018 Catamorphic, Co.
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
LaunchDarkly SSE Client for Ruby
|
2
|
+
================================
|
3
|
+
|
4
|
+
[](http://badge.fury.io/rb/ld-eventsource) [](https://circleci.com/gh/launchdarkly/ruby-eventsource/tree/master)
|
5
|
+
|
6
|
+
A client for the [Server-Sent Events](https://www.w3.org/TR/eventsource/) protocol. This implementation runs on a worker thread, and uses the [`socketry`](https://rubygems.org/gems/socketry) gem to manage a persistent connection. Its primary purpose is to support the [LaunchDarkly SDK for Ruby](https://github.com/launchdarkly/ruby-client), but it can be used independently.
|
7
|
+
|
8
|
+
Parts of this code are based on https://github.com/Tonkpils/celluloid-eventsource, but it does not use Celluloid.
|
9
|
+
|
10
|
+
Supported Ruby versions
|
11
|
+
-----------------------
|
12
|
+
|
13
|
+
This gem has a minimum Ruby version of 2.2.6, or 9.1.6 for JRuby.
|
14
|
+
|
15
|
+
Quick setup
|
16
|
+
-----------
|
17
|
+
|
18
|
+
1. Install the Ruby SDK with `gem`:
|
19
|
+
|
20
|
+
```shell
|
21
|
+
gem install ld-eventsource
|
22
|
+
```
|
23
|
+
|
24
|
+
2. Import the code:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
require 'ld-eventsource'
|
28
|
+
```
|
29
|
+
|
30
|
+
3. Create a new SSE client instance and register your event handler:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
sse_client = SSE::Client.new("http://hostname/resource/path") do |client|
|
34
|
+
client.on_event do |event|
|
35
|
+
puts "I received an event: #{event.type}, #{event.data}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
For other options available with the `Client` constructor, see the [API documentation](https://www.rubydoc.info/gems/ld-eventsource).
|
41
|
+
|
42
|
+
Contributing
|
43
|
+
------------
|
44
|
+
|
45
|
+
We welcome questions, suggestions, and pull requests at our [Github repository](https://github.com/launchdarkly/ruby-eventsource). Pull requests should be done from a fork.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path("../lib", __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "ld-eventsource/version"
|
6
|
+
|
7
|
+
# rubocop:disable Metrics/BlockLength
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "ld-eventsource"
|
10
|
+
spec.version = SSE::VERSION
|
11
|
+
spec.authors = ["LaunchDarkly"]
|
12
|
+
spec.email = ["team@launchdarkly.com"]
|
13
|
+
spec.summary = "LaunchDarkly SSE client"
|
14
|
+
spec.description = "LaunchDarkly SSE client for Ruby"
|
15
|
+
spec.homepage = "https://github.com/launchdarkly/ruby-eventsource"
|
16
|
+
spec.license = "Apache-2.0"
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.2"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
|
27
|
+
|
28
|
+
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
29
|
+
spec.add_runtime_dependency "http_tools", '~> 0.4.5'
|
30
|
+
spec.add_runtime_dependency "socketry", "~> 0.5.1"
|
31
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require "ld-eventsource/client"
|
2
|
+
require "ld-eventsource/version"
|
3
|
+
|
4
|
+
#
|
5
|
+
# A client for the Server-Sent Events protocol.
|
6
|
+
#
|
7
|
+
module SSE
|
8
|
+
#
|
9
|
+
# Internal components of the SSE implementation. All classes in this module should be
|
10
|
+
# considered unsupported and subject to change.
|
11
|
+
#
|
12
|
+
module Impl
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
require "ld-eventsource/impl/backoff"
|
2
|
+
require "ld-eventsource/impl/event_parser"
|
3
|
+
require "ld-eventsource/impl/streaming_http"
|
4
|
+
require "ld-eventsource/events"
|
5
|
+
require "ld-eventsource/errors"
|
6
|
+
|
7
|
+
require "concurrent/atomics"
|
8
|
+
require "logger"
|
9
|
+
require "thread"
|
10
|
+
require "uri"
|
11
|
+
|
12
|
+
module SSE
|
13
|
+
#
|
14
|
+
# A lightweight SSE client implementation. The client uses a worker thread to read from the
|
15
|
+
# streaming HTTP connection. Events are dispatched from the same worker thread.
|
16
|
+
#
|
17
|
+
# The client will attempt to recover from connection failures as follows:
|
18
|
+
#
|
19
|
+
# * The first time the connection is dropped, it will wait about one second (or whatever value is
|
20
|
+
# specified for `reconnect_time`) before attempting to reconnect. The actual delay has a
|
21
|
+
# pseudo-random jitter value added.
|
22
|
+
# * If the connection fails again within the time range specified by `reconnect_reset_interval`,
|
23
|
+
# it will exponentially increase the delay between attempts (and also apply a random jitter).
|
24
|
+
# However, if the connection stays up for at least that amount of time, the delay will be reset
|
25
|
+
# to the minimum.
|
26
|
+
# * Each time a new connection is made, the client will send a `Last-Event-Id` header so the server
|
27
|
+
# can pick up where it left off (if the server has been sending ID values for events).
|
28
|
+
#
|
29
|
+
# It is also possible to force the connection to be restarted if the server sends no data within an
|
30
|
+
# interval specified by `read_timeout`. Using a read timeout is advisable because otherwise it is
|
31
|
+
# possible in some circumstances for a connection failure to go undetected. To keep the connection
|
32
|
+
# from timing out if there are no events to send, the server could send a comment line (`":"`) at
|
33
|
+
# regular intervals as a heartbeat.
|
34
|
+
#
|
35
|
+
class Client
|
36
|
+
# The default value for `connect_timeout` in {#initialize}.
|
37
|
+
DEFAULT_CONNECT_TIMEOUT = 10
|
38
|
+
|
39
|
+
# The default value for `read_timeout` in {#initialize}.
|
40
|
+
DEFAULT_READ_TIMEOUT = 300
|
41
|
+
|
42
|
+
# The default value for `reconnect_time` in {#initialize}.
|
43
|
+
DEFAULT_RECONNECT_TIME = 1
|
44
|
+
|
45
|
+
# The maximum number of seconds that the client will wait before reconnecting.
|
46
|
+
MAX_RECONNECT_TIME = 30
|
47
|
+
|
48
|
+
# The default value for `reconnect_reset_interval` in {#initialize}.
|
49
|
+
DEFAULT_RECONNECT_RESET_INTERVAL = 60
|
50
|
+
|
51
|
+
#
|
52
|
+
# Creates a new SSE client.
|
53
|
+
#
|
54
|
+
# Once the client is created, it immediately attempts to open the SSE connection. You will
|
55
|
+
# normally want to register your event handler before this happens, so that no events are missed.
|
56
|
+
# To do this, provide a block after the constructor; the block will be executed before opening
|
57
|
+
# the connection.
|
58
|
+
#
|
59
|
+
# @example Specifying an event handler at initialization time
|
60
|
+
# client = SSE::Client.new(uri) do |c|
|
61
|
+
# c.on_event do |event|
|
62
|
+
# puts "I got an event: #{event.type}, #{event.data}"
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# @param uri [String] the URI to connect to
|
67
|
+
# @param headers [Hash] ({}) custom headers to send with each HTTP request
|
68
|
+
# @param connect_timeout [Float] (DEFAULT_CONNECT_TIMEOUT) maximum time to wait for a
|
69
|
+
# connection, in seconds
|
70
|
+
# @param read_timeout [Float] (DEFAULT_READ_TIMEOUT) the connection will be dropped and
|
71
|
+
# restarted if this number of seconds elapse with no data; nil for no timeout
|
72
|
+
# @param reconnect_time [Float] (DEFAULT_RECONNECT_TIME) the initial delay before reconnecting
|
73
|
+
# after a failure, in seconds; this can increase as described in {Client}
|
74
|
+
# @param reconnect_reset_interval [Float] (DEFAULT_RECONNECT_RESET_INTERVAL) if a connection
|
75
|
+
# stays alive for at least this number of seconds, the reconnect interval will return to the
|
76
|
+
# initial value
|
77
|
+
# @param last_event_id [String] (nil) the initial value that the client should send in the
|
78
|
+
# `Last-Event-Id` header, if any
|
79
|
+
# @param proxy [String] (nil) optional URI of a proxy server to use (you can also specify a
|
80
|
+
# proxy with the `HTTP_PROXY` or `HTTPS_PROXY` environment variable)
|
81
|
+
# @param logger [Logger] a Logger instance for the client to use for diagnostic output;
|
82
|
+
# defaults to a logger with WARN level that goes to standard output
|
83
|
+
# @yieldparam [Client] client the new client instance, before opening the connection
|
84
|
+
#
|
85
|
+
def initialize(uri,
|
86
|
+
headers: {},
|
87
|
+
connect_timeout: DEFAULT_CONNECT_TIMEOUT,
|
88
|
+
read_timeout: DEFAULT_READ_TIMEOUT,
|
89
|
+
reconnect_time: DEFAULT_RECONNECT_TIME,
|
90
|
+
reconnect_reset_interval: DEFAULT_RECONNECT_RESET_INTERVAL,
|
91
|
+
last_event_id: nil,
|
92
|
+
proxy: nil,
|
93
|
+
logger: nil)
|
94
|
+
@uri = URI(uri)
|
95
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
96
|
+
|
97
|
+
@headers = headers.clone
|
98
|
+
@connect_timeout = connect_timeout
|
99
|
+
@read_timeout = read_timeout
|
100
|
+
@logger = logger || default_logger
|
101
|
+
|
102
|
+
if proxy
|
103
|
+
@proxy = proxy
|
104
|
+
else
|
105
|
+
proxy_uri = @uri.find_proxy
|
106
|
+
if !proxy_uri.nil? && (proxy_uri.scheme == 'http' || proxy_uri.scheme == 'https')
|
107
|
+
@proxy = proxy_uri
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
@backoff = Impl::Backoff.new(reconnect_time || DEFAULT_RECONNECT_TIME, MAX_RECONNECT_TIME,
|
112
|
+
reconnect_reset_interval: reconnect_reset_interval)
|
113
|
+
|
114
|
+
@on = { event: ->(_) {}, error: ->(_) {} }
|
115
|
+
@last_id = last_event_id
|
116
|
+
|
117
|
+
yield self if block_given?
|
118
|
+
|
119
|
+
Thread.new do
|
120
|
+
run_stream
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
#
|
125
|
+
# Specifies a block or Proc to receive events from the stream. This will be called once for every
|
126
|
+
# valid event received, with a single parameter of type {StreamEvent}. It is called from the same
|
127
|
+
# worker thread that reads the stream, so no more events will be dispatched until it returns.
|
128
|
+
#
|
129
|
+
# Any exception that propagates out of the handler will cause the stream to disconnect and
|
130
|
+
# reconnect, on the assumption that data may have been lost and that restarting the stream will
|
131
|
+
# cause it to be resent.
|
132
|
+
#
|
133
|
+
# Any previously specified event handler will be replaced.
|
134
|
+
#
|
135
|
+
# @yieldparam event [StreamEvent]
|
136
|
+
#
|
137
|
+
def on_event(&action)
|
138
|
+
@on[:event] = action
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Specifies a block or Proc to receive connection errors. This will be called with a single
|
143
|
+
# parameter that is an instance of some exception class-- normally, either some I/O exception or
|
144
|
+
# one of the classes in {SSE::Errors}. It is called from the same worker thread that
|
145
|
+
# reads the stream, so no more events or errors will be dispatched until it returns.
|
146
|
+
#
|
147
|
+
# If the error handler decides that this type of error is not recoverable, it has the ability
|
148
|
+
# to prevent any further reconnect attempts by calling {Client#close} on the Client. For instance,
|
149
|
+
# you might want to do this if the server returned a `401 Unauthorized` error and no other authorization
|
150
|
+
# credentials are available, since any further requests would presumably also receive a 401.
|
151
|
+
#
|
152
|
+
# Any previously specified error handler will be replaced.
|
153
|
+
#
|
154
|
+
# @yieldparam error [StandardError]
|
155
|
+
#
|
156
|
+
def on_error(&action)
|
157
|
+
@on[:error] = action
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# Permanently shuts down the client and its connection. No further events will be dispatched. This
|
162
|
+
# has no effect if called a second time.
|
163
|
+
#
|
164
|
+
def close
|
165
|
+
if @stopped.make_true
|
166
|
+
@cxn.close if !@cxn.nil?
|
167
|
+
@cxn = nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def default_logger
|
174
|
+
log = ::Logger.new($stdout)
|
175
|
+
log.level = ::Logger::WARN
|
176
|
+
log.progname = 'ld-eventsource'
|
177
|
+
log
|
178
|
+
end
|
179
|
+
|
180
|
+
def run_stream
|
181
|
+
while !@stopped.value
|
182
|
+
@cxn = nil
|
183
|
+
begin
|
184
|
+
@cxn = connect
|
185
|
+
# There's a potential race if close was called in the middle of the previous line, i.e. after we
|
186
|
+
# connected but before @cxn was set. Checking the variable again is a bit clunky but avoids that.
|
187
|
+
return if @stopped.value
|
188
|
+
read_stream(@cxn) if !@cxn.nil?
|
189
|
+
rescue Errno::EBADF
|
190
|
+
# Don't log this as an error - it probably means we closed our own connection deliberately
|
191
|
+
@logger.info { "Stream connection closed" }
|
192
|
+
rescue StandardError => e
|
193
|
+
# This should not be possible because connect catches all StandardErrors
|
194
|
+
log_and_dispatch_error(e, "Unexpected error from event source")
|
195
|
+
end
|
196
|
+
begin
|
197
|
+
@cxn.close if !@cxn.nil?
|
198
|
+
rescue StandardError => e
|
199
|
+
log_and_dispatch_error(e, "Unexpected error while closing stream")
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Try to establish a streaming connection. Returns the StreamingHTTPConnection object if successful.
|
205
|
+
def connect
|
206
|
+
loop do
|
207
|
+
return if @stopped.value
|
208
|
+
interval = @backoff.next_interval
|
209
|
+
if interval > 0
|
210
|
+
@logger.info { "Will retry connection after #{'%.3f' % interval} seconds" }
|
211
|
+
sleep(interval)
|
212
|
+
end
|
213
|
+
begin
|
214
|
+
@logger.info { "Connecting to event stream at #{@uri}" }
|
215
|
+
cxn = Impl::StreamingHTTPConnection.new(@uri,
|
216
|
+
proxy: @proxy,
|
217
|
+
headers: build_headers,
|
218
|
+
connect_timeout: @connect_timeout,
|
219
|
+
read_timeout: @read_timeout
|
220
|
+
)
|
221
|
+
if cxn.status == 200
|
222
|
+
content_type = cxn.headers["content-type"]
|
223
|
+
if content_type && content_type.start_with?("text/event-stream")
|
224
|
+
return cxn # we're good to proceed
|
225
|
+
else
|
226
|
+
cxn.close
|
227
|
+
err = Errors::HTTPContentTypeError.new(cxn.headers["content-type"])
|
228
|
+
@on[:error].call(err)
|
229
|
+
@logger.warn { "Event source returned unexpected content type '#{cxn.headers["content-type"]}'" }
|
230
|
+
end
|
231
|
+
else
|
232
|
+
body = cxn.read_all # grab the whole response body in case it has error details
|
233
|
+
cxn.close
|
234
|
+
@logger.info { "Server returned error status #{cxn.status}" }
|
235
|
+
err = Errors::HTTPStatusError.new(cxn.status, body)
|
236
|
+
@on[:error].call(err)
|
237
|
+
end
|
238
|
+
rescue Errno::EBADF
|
239
|
+
raise # See EBADF comment in run_stream
|
240
|
+
rescue StandardError => e
|
241
|
+
cxn.close if !cxn.nil?
|
242
|
+
log_and_dispatch_error(e, "Unexpected error from event source")
|
243
|
+
end
|
244
|
+
# if unsuccessful, continue the loop to connect again
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Pipe the output of the StreamingHTTPConnection into the EventParser, and dispatch events as
|
249
|
+
# they arrive.
|
250
|
+
def read_stream(cxn)
|
251
|
+
# Tell the Backoff object that the connection is now in a valid state. It uses that information so
|
252
|
+
# it can automatically reset itself if enough time passes between failures.
|
253
|
+
@backoff.mark_success
|
254
|
+
|
255
|
+
event_parser = Impl::EventParser.new(cxn.read_lines)
|
256
|
+
event_parser.items.each do |item|
|
257
|
+
return if @stopped.value
|
258
|
+
case item
|
259
|
+
when StreamEvent
|
260
|
+
dispatch_event(item)
|
261
|
+
when Impl::SetRetryInterval
|
262
|
+
@logger.debug { "Received 'retry:' directive, setting interval to #{item.milliseconds}ms" }
|
263
|
+
@backoff.base_interval = item.milliseconds.to_f / 1000
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def dispatch_event(event)
|
269
|
+
@logger.debug { "Received event: #{event}" }
|
270
|
+
@last_id = event.id
|
271
|
+
|
272
|
+
# Pass the event to the caller
|
273
|
+
@on[:event].call(event)
|
274
|
+
end
|
275
|
+
|
276
|
+
def log_and_dispatch_error(e, message)
|
277
|
+
@logger.warn { "#{message}: #{e.inspect}"}
|
278
|
+
@logger.debug { "Exception trace: #{e.backtrace}" }
|
279
|
+
begin
|
280
|
+
@on[:error].call(e)
|
281
|
+
rescue StandardError => ee
|
282
|
+
@logger.warn { "Error handler threw an exception: #{ee.inspect}"}
|
283
|
+
@logger.debug { "Exception trace: #{ee.backtrace}" }
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def build_headers
|
288
|
+
h = {
|
289
|
+
'Accept' => 'text/event-stream',
|
290
|
+
'Cache-Control' => 'no-cache'
|
291
|
+
}
|
292
|
+
h['Last-Event-Id'] = @last_id if !@last_id.nil?
|
293
|
+
h.merge(@headers)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|