ldclient-rb 5.4.3 → 5.5.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.
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