ldclient-rb 5.4.3 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +33 -6
  3. data/CHANGELOG.md +19 -0
  4. data/CONTRIBUTING.md +0 -12
  5. data/Gemfile.lock +22 -3
  6. data/README.md +41 -35
  7. data/ldclient-rb.gemspec +4 -3
  8. data/lib/ldclient-rb.rb +9 -1
  9. data/lib/ldclient-rb/cache_store.rb +1 -0
  10. data/lib/ldclient-rb/config.rb +201 -90
  11. data/lib/ldclient-rb/evaluation.rb +56 -8
  12. data/lib/ldclient-rb/event_summarizer.rb +3 -0
  13. data/lib/ldclient-rb/events.rb +16 -0
  14. data/lib/ldclient-rb/expiring_cache.rb +1 -0
  15. data/lib/ldclient-rb/file_data_source.rb +18 -13
  16. data/lib/ldclient-rb/flags_state.rb +3 -2
  17. data/lib/ldclient-rb/impl.rb +13 -0
  18. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  19. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  20. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  21. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  22. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  23. data/lib/ldclient-rb/in_memory_store.rb +15 -4
  24. data/lib/ldclient-rb/integrations.rb +55 -0
  25. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  26. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  27. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  28. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  29. data/lib/ldclient-rb/interfaces.rb +153 -0
  30. data/lib/ldclient-rb/ldclient.rb +135 -77
  31. data/lib/ldclient-rb/memoized_value.rb +2 -0
  32. data/lib/ldclient-rb/newrelic.rb +1 -0
  33. data/lib/ldclient-rb/non_blocking_thread_pool.rb +3 -3
  34. data/lib/ldclient-rb/polling.rb +1 -0
  35. data/lib/ldclient-rb/redis_store.rb +24 -190
  36. data/lib/ldclient-rb/requestor.rb +3 -2
  37. data/lib/ldclient-rb/simple_lru_cache.rb +1 -0
  38. data/lib/ldclient-rb/stream.rb +22 -10
  39. data/lib/ldclient-rb/user_filter.rb +1 -0
  40. data/lib/ldclient-rb/util.rb +1 -0
  41. data/lib/ldclient-rb/version.rb +1 -1
  42. data/scripts/gendocs.sh +12 -0
  43. data/spec/feature_store_spec_base.rb +173 -72
  44. data/spec/file_data_source_spec.rb +2 -2
  45. data/spec/http_util.rb +103 -0
  46. data/spec/in_memory_feature_store_spec.rb +1 -1
  47. data/spec/integrations/consul_feature_store_spec.rb +41 -0
  48. data/spec/integrations/dynamodb_feature_store_spec.rb +104 -0
  49. data/spec/integrations/store_wrapper_spec.rb +276 -0
  50. data/spec/ldclient_spec.rb +83 -4
  51. data/spec/redis_feature_store_spec.rb +25 -16
  52. data/spec/requestor_spec.rb +44 -38
  53. data/spec/stream_spec.rb +18 -18
  54. metadata +55 -33
  55. data/lib/sse_client.rb +0 -4
  56. data/lib/sse_client/backoff.rb +0 -38
  57. data/lib/sse_client/sse_client.rb +0 -171
  58. data/lib/sse_client/sse_events.rb +0 -67
  59. data/lib/sse_client/streaming_http.rb +0 -199
  60. data/spec/sse_client/sse_client_spec.rb +0 -177
  61. data/spec/sse_client/sse_events_spec.rb +0 -100
  62. data/spec/sse_client/sse_shared.rb +0 -82
  63. data/spec/sse_client/streaming_http_spec.rb +0 -263
@@ -1,177 +0,0 @@
1
- require "spec_helper"
2
- require "socketry"
3
- require "sse_client/sse_shared"
4
-
5
- #
6
- # End-to-end tests of SSEClient against a real server
7
- #
8
- describe SSE::SSEClient do
9
- subject { SSE::SSEClient }
10
-
11
- def with_client(client)
12
- begin
13
- yield client
14
- ensure
15
- client.close
16
- end
17
- end
18
-
19
- it "sends expected headers" do
20
- with_server do |server|
21
- requests = Queue.new
22
- server.setup_response("/") do |req,res|
23
- requests << req
24
- res.content_type = "text/event-stream"
25
- res.status = 200
26
- end
27
-
28
- headers = {
29
- "Authorization" => "secret"
30
- }
31
-
32
- with_client(subject.new(server.base_uri, headers: headers)) do |client|
33
- received_req = requests.pop
34
- expect(received_req.header).to eq({
35
- "accept" => ["text/event-stream"],
36
- "cache-control" => ["no-cache"],
37
- "host" => ["127.0.0.1"],
38
- "authorization" => ["secret"]
39
- })
40
- end
41
- end
42
- end
43
-
44
- it "receives messages" do
45
- events_body = <<-EOT
46
- event: go
47
- data: foo
48
- id: 1
49
-
50
- event: stop
51
- data: bar
52
-
53
- EOT
54
- with_server do |server|
55
- server.setup_response("/") do |req,res|
56
- res.content_type = "text/event-stream"
57
- res.status = 200
58
- res.body = events_body
59
- end
60
-
61
- event_sink = Queue.new
62
- client = subject.new(server.base_uri) do |c|
63
- c.on_event { |event| event_sink << event }
64
- end
65
-
66
- with_client(client) do |client|
67
- expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", "1"))
68
- expect(event_sink.pop).to eq(SSE::SSEEvent.new(:stop, "bar", nil))
69
- end
70
- end
71
- end
72
-
73
- it "reconnects after error response" do
74
- events_body = <<-EOT
75
- event: go
76
- data: foo
77
-
78
- EOT
79
- with_server do |server|
80
- attempt = 0
81
- server.setup_response("/") do |req,res|
82
- attempt += 1
83
- if attempt == 1
84
- res.status = 500
85
- res.body = "sorry"
86
- res.keep_alive = false
87
- else
88
- res.content_type = "text/event-stream"
89
- res.status = 200
90
- res.body = events_body
91
- end
92
- end
93
-
94
- event_sink = Queue.new
95
- error_sink = Queue.new
96
- client = subject.new(server.base_uri, reconnect_time: 0.25) do |c|
97
- c.on_event { |event| event_sink << event }
98
- c.on_error { |error| error_sink << error }
99
- end
100
-
101
- with_client(client) do |client|
102
- expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", nil))
103
- expect(error_sink.pop).to eq({ status_code: 500, body: "sorry" })
104
- expect(attempt).to be >= 2
105
- end
106
- end
107
- end
108
-
109
- it "reconnects after read timeout" do
110
- events_body = <<-EOT
111
- event: go
112
- data: foo
113
-
114
- EOT
115
- with_server do |server|
116
- attempt = 0
117
- server.setup_response("/") do |req,res|
118
- attempt += 1
119
- if attempt == 1
120
- sleep(2)
121
- end
122
- res.content_type = "text/event-stream"
123
- res.status = 200
124
- res.body = events_body
125
- end
126
-
127
- event_sink = Queue.new
128
- client = subject.new(server.base_uri,
129
- reconnect_time: 0.25, read_timeout: 0.25) do |c|
130
- c.on_event { |event| event_sink << event }
131
- end
132
-
133
- with_client(client) do |client|
134
- expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", nil))
135
- expect(attempt).to be >= 2
136
- end
137
- end
138
- end
139
-
140
- it "reconnects if stream returns EOF" do
141
- events_body_1 = <<-EOT
142
- event: go
143
- data: foo
144
-
145
- EOT
146
- events_body_2 = <<-EOT
147
- event: go
148
- data: bar
149
-
150
- EOT
151
- with_server do |server|
152
- attempt = 0
153
- server.setup_response("/") do |req,res|
154
- attempt += 1
155
- if attempt == 1
156
- res.body = events_body_1
157
- else
158
- res.body = events_body_2
159
- end
160
- res.content_type = "text/event-stream"
161
- res.status = 200
162
- end
163
-
164
- event_sink = Queue.new
165
- client = subject.new(server.base_uri,
166
- reconnect_time: 0.25, read_timeout: 0.25) do |c|
167
- c.on_event { |event| event_sink << event }
168
- end
169
-
170
- with_client(client) do |client|
171
- expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", nil))
172
- expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "bar", nil))
173
- expect(attempt).to be >= 2
174
- end
175
- end
176
- end
177
- end
@@ -1,100 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe SSE::EventParser do
4
- subject { SSE::EventParser }
5
-
6
- it "parses an event with all fields" do
7
- lines = [
8
- "event: abc\r\n",
9
- "data: def\r\n",
10
- "id: 1\r\n",
11
- "\r\n"
12
- ]
13
- ep = subject.new(lines)
14
-
15
- expected_event = SSE::SSEEvent.new(:abc, "def", "1")
16
- output = ep.items.to_a
17
- expect(output).to eq([ expected_event ])
18
- end
19
-
20
- it "parses an event with only data" do
21
- lines = [
22
- "data: def\r\n",
23
- "\r\n"
24
- ]
25
- ep = subject.new(lines)
26
-
27
- expected_event = SSE::SSEEvent.new(:message, "def", nil)
28
- output = ep.items.to_a
29
- expect(output).to eq([ expected_event ])
30
- end
31
-
32
- it "parses an event with multi-line data" do
33
- lines = [
34
- "data: def\r\n",
35
- "data: ghi\r\n",
36
- "\r\n"
37
- ]
38
- ep = subject.new(lines)
39
-
40
- expected_event = SSE::SSEEvent.new(:message, "def\nghi", nil)
41
- output = ep.items.to_a
42
- expect(output).to eq([ expected_event ])
43
- end
44
-
45
- it "ignores comments" do
46
- lines = [
47
- ":",
48
- "data: def\r\n",
49
- ":",
50
- "\r\n"
51
- ]
52
- ep = subject.new(lines)
53
-
54
- expected_event = SSE::SSEEvent.new(:message, "def", nil)
55
- output = ep.items.to_a
56
- expect(output).to eq([ expected_event ])
57
- end
58
-
59
- it "parses reconnect interval" do
60
- lines = [
61
- "retry: 2500\r\n",
62
- "\r\n"
63
- ]
64
- ep = subject.new(lines)
65
-
66
- expected_item = SSE::SSESetRetryInterval.new(2500)
67
- output = ep.items.to_a
68
- expect(output).to eq([ expected_item ])
69
- end
70
-
71
- it "parses multiple events" do
72
- lines = [
73
- "event: abc\r\n",
74
- "data: def\r\n",
75
- "id: 1\r\n",
76
- "\r\n",
77
- "data: ghi\r\n",
78
- "\r\n"
79
- ]
80
- ep = subject.new(lines)
81
-
82
- expected_event_1 = SSE::SSEEvent.new(:abc, "def", "1")
83
- expected_event_2 = SSE::SSEEvent.new(:message, "ghi", nil)
84
- output = ep.items.to_a
85
- expect(output).to eq([ expected_event_1, expected_event_2 ])
86
- end
87
-
88
- it "ignores events with no data" do
89
- lines = [
90
- "event: nothing\r\n",
91
- "\r\n",
92
- "event: nada\r\n",
93
- "\r\n"
94
- ]
95
- ep = subject.new(lines)
96
-
97
- output = ep.items.to_a
98
- expect(output).to eq([])
99
- end
100
- end
@@ -1,82 +0,0 @@
1
- require "spec_helper"
2
- require "webrick"
3
- require "webrick/httpproxy"
4
- require "webrick/https"
5
-
6
- class StubHTTPServer
7
- def initialize
8
- @port = 50000
9
- begin
10
- @server = create_server(@port)
11
- rescue Errno::EADDRINUSE
12
- @port += 1
13
- retry
14
- end
15
- end
16
-
17
- def create_server(port)
18
- WEBrick::HTTPServer.new(
19
- BindAddress: '127.0.0.1',
20
- Port: port,
21
- AccessLog: [],
22
- Logger: NullLogger.new
23
- )
24
- end
25
-
26
- def start
27
- Thread.new { @server.start }
28
- end
29
-
30
- def stop
31
- @server.shutdown
32
- end
33
-
34
- def base_uri
35
- URI("http://127.0.0.1:#{@port}")
36
- end
37
-
38
- def setup_response(uri_path, &action)
39
- @server.mount_proc(uri_path, action)
40
- end
41
- end
42
-
43
- class StubProxyServer < StubHTTPServer
44
- attr_reader :request_count
45
- attr_accessor :connect_status
46
-
47
- def initialize
48
- super
49
- @request_count = 0
50
- end
51
-
52
- def create_server(port)
53
- WEBrick::HTTPProxyServer.new(
54
- BindAddress: '127.0.0.1',
55
- Port: port,
56
- AccessLog: [],
57
- Logger: NullLogger.new,
58
- ProxyContentHandler: proc do |req,res|
59
- if !@connect_status.nil?
60
- res.status = @connect_status
61
- end
62
- @request_count += 1
63
- end
64
- )
65
- end
66
- end
67
-
68
- class NullLogger
69
- def method_missing(*)
70
- self
71
- end
72
- end
73
-
74
- def with_server(server = nil)
75
- server = StubHTTPServer.new if server.nil?
76
- begin
77
- server.start
78
- yield server
79
- ensure
80
- server.stop
81
- end
82
- end
@@ -1,263 +0,0 @@
1
- require "spec_helper"
2
- require "socketry"
3
- require "sse_client/sse_shared"
4
-
5
- #
6
- # End-to-end tests of HTTP requests against a real server
7
- #
8
- describe SSE::StreamingHTTPConnection do
9
- subject { SSE::StreamingHTTPConnection }
10
-
11
- def with_connection(cxn)
12
- begin
13
- yield cxn
14
- ensure
15
- cxn.close
16
- end
17
- end
18
-
19
- it "makes HTTP connection and sends request" do
20
- with_server do |server|
21
- requests = Queue.new
22
- server.setup_response("/foo") do |req,res|
23
- requests << req
24
- res.status = 200
25
- end
26
- headers = {
27
- "Accept" => "text/plain"
28
- }
29
- with_connection(subject.new(server.base_uri.merge("/foo?bar"), nil, headers, 30, 30)) do
30
- received_req = requests.pop
31
- expect(received_req.unparsed_uri).to eq("/foo?bar")
32
- expect(received_req.header).to eq({
33
- "accept" => ["text/plain"],
34
- "host" => [server.base_uri.host]
35
- })
36
- end
37
- end
38
- end
39
-
40
- it "receives response status" do
41
- with_server do |server|
42
- server.setup_response("/foo") do |req,res|
43
- res.status = 204
44
- end
45
- with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
46
- expect(cxn.status).to eq(204)
47
- end
48
- end
49
- end
50
-
51
- it "receives response headers" do
52
- with_server do |server|
53
- server.setup_response("/foo") do |req,res|
54
- res["Content-Type"] = "application/json"
55
- end
56
- with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
57
- expect(cxn.headers["content-type"]).to eq("application/json")
58
- end
59
- end
60
- end
61
-
62
- it "can read response as lines" do
63
- body = <<-EOT
64
- This is
65
- a response
66
- EOT
67
- with_server do |server|
68
- server.setup_response("/foo") do |req,res|
69
- res.body = body
70
- end
71
- with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
72
- lines = cxn.read_lines
73
- expect(lines.next).to eq("This is\n")
74
- expect(lines.next).to eq("a response\n")
75
- end
76
- end
77
- end
78
-
79
- it "can read entire response body" do
80
- body = <<-EOT
81
- This is
82
- a response
83
- EOT
84
- with_server do |server|
85
- server.setup_response("/foo") do |req,res|
86
- res.body = body
87
- end
88
- with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
89
- read_body = cxn.read_all
90
- expect(read_body).to eq("This is\na response\n")
91
- end
92
- end
93
- end
94
-
95
- it "enforces read timeout" do
96
- with_server do |server|
97
- server.setup_response("/") do |req,res|
98
- sleep(2)
99
- res.status = 200
100
- end
101
- expect { subject.new(server.base_uri, nil, {}, 30, 0.25) }.to raise_error(Socketry::TimeoutError)
102
- end
103
- end
104
-
105
- it "connects to HTTP server through proxy" do
106
- body = "hi"
107
- with_server do |server|
108
- server.setup_response("/") do |req,res|
109
- res.body = body
110
- end
111
- with_server(StubProxyServer.new) do |proxy|
112
- with_connection(subject.new(server.base_uri, proxy.base_uri, {}, 30, 30)) do |cxn|
113
- read_body = cxn.read_all
114
- expect(read_body).to eq("hi")
115
- expect(proxy.request_count).to eq(1)
116
- end
117
- end
118
- end
119
- end
120
-
121
- it "throws error if proxy responds with error status" do
122
- with_server do |server|
123
- server.setup_response("/") do |req,res|
124
- res.body = body
125
- end
126
- with_server(StubProxyServer.new) do |proxy|
127
- proxy.connect_status = 403
128
- expect { subject.new(server.base_uri, proxy.base_uri, {}, 30, 30) }.to raise_error(SSE::ProxyError)
129
- end
130
- end
131
- end
132
-
133
- # The following 2 tests were originally written to connect to an embedded HTTPS server made with
134
- # WEBrick. Unfortunately, some unknown problem prevents WEBrick's self-signed certificate feature
135
- # from working in JRuby 9.1 (but not in any other Ruby version). Therefore these tests currently
136
- # hit an external URL.
137
-
138
- it "connects to HTTPS server" do
139
- with_connection(subject.new(URI("https://app.launchdarkly.com"), nil, {}, 30, 30)) do |cxn|
140
- expect(cxn.status).to eq 200
141
- end
142
- end
143
-
144
- it "connects to HTTPS server through proxy" do
145
- with_server(StubProxyServer.new) do |proxy|
146
- with_connection(subject.new(URI("https://app.launchdarkly.com"), proxy.base_uri, {}, 30, 30)) do |cxn|
147
- expect(cxn.status).to eq 200
148
- expect(proxy.request_count).to eq(1)
149
- end
150
- end
151
- end
152
- end
153
-
154
- #
155
- # Tests of response parsing functionality without a real HTTP request
156
- #
157
- describe SSE::HTTPResponseReader do
158
- subject { SSE::HTTPResponseReader }
159
-
160
- let(:simple_response) { <<-EOT
161
- HTTP/1.1 200 OK
162
- Cache-Control: no-cache
163
- Content-Type: text/event-stream
164
-
165
- line1\r
166
- line2
167
- \r
168
- EOT
169
- }
170
-
171
- def make_chunks(str)
172
- # arbitrarily split content into 5-character blocks
173
- str.scan(/.{1,5}/m).to_enum
174
- end
175
-
176
- def mock_socket_without_timeout(chunks)
177
- mock_socket(chunks) { :eof }
178
- end
179
-
180
- def mock_socket_with_timeout(chunks)
181
- mock_socket(chunks) { raise Socketry::TimeoutError }
182
- end
183
-
184
- def mock_socket(chunks)
185
- sock = double
186
- allow(sock).to receive(:readpartial) do
187
- begin
188
- chunks.next
189
- rescue StopIteration
190
- yield
191
- end
192
- end
193
- sock
194
- end
195
-
196
- it "parses status code" do
197
- socket = mock_socket_without_timeout(make_chunks(simple_response))
198
- reader = subject.new(socket, 0)
199
- expect(reader.status).to eq(200)
200
- end
201
-
202
- it "parses headers" do
203
- socket = mock_socket_without_timeout(make_chunks(simple_response))
204
- reader = subject.new(socket, 0)
205
- expect(reader.headers).to eq({
206
- 'cache-control' => 'no-cache',
207
- 'content-type' => 'text/event-stream'
208
- })
209
- end
210
-
211
- it "can read entire response body" do
212
- socket = mock_socket_without_timeout(make_chunks(simple_response))
213
- reader = subject.new(socket, 0)
214
- expect(reader.read_all).to eq("line1\r\nline2\n\r\n")
215
- end
216
-
217
- it "can read response body as lines" do
218
- socket = mock_socket_without_timeout(make_chunks(simple_response))
219
- reader = subject.new(socket, 0)
220
- expect(reader.read_lines.to_a).to eq([
221
- "line1\r\n",
222
- "line2\n",
223
- "\r\n"
224
- ])
225
- end
226
-
227
- it "handles chunked encoding" do
228
- chunked_response = <<-EOT
229
- HTTP/1.1 200 OK
230
- Content-Type: text/plain
231
- Transfer-Encoding: chunked
232
-
233
- 6\r
234
- things\r
235
- A\r
236
- and stuff\r
237
- 0\r
238
- \r
239
- EOT
240
- socket = mock_socket_without_timeout(make_chunks(chunked_response))
241
- reader = subject.new(socket, 0)
242
- expect(reader.read_all).to eq("things and stuff")
243
- end
244
-
245
- it "raises error if response ends without complete headers" do
246
- malformed_response = <<-EOT
247
- HTTP/1.1 200 OK
248
- Cache-Control: no-cache
249
- EOT
250
- socket = mock_socket_without_timeout(make_chunks(malformed_response))
251
- expect { subject.new(socket, 0) }.to raise_error(EOFError)
252
- end
253
-
254
- it "throws timeout if thrown by socket read" do
255
- socket = mock_socket_with_timeout(make_chunks(simple_response))
256
- reader = subject.new(socket, 0)
257
- lines = reader.read_lines
258
- lines.next
259
- lines.next
260
- lines.next
261
- expect { lines.next }.to raise_error(Socketry::TimeoutError)
262
- end
263
- end