feat-sdk 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/lib/feat/client.rb +75 -3
- data/lib/feat/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50c482a000c1e2ef525db051f580656d302cfbc0cb983c0241507615cd3d6b87
|
|
4
|
+
data.tar.gz: 1e52811533d9ba5d42805e2a9b665b626b984a1d30df61dfaa97fdacc90fe2f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40b5b6c2ab9fa985ac32c17bf39b4606c8e38a271e0e526383d2c7e1824348107f526d67350cc06dd60e5c4fd186a5df0ff41bad20f12353d1ddbf5cb3de583b
|
|
7
|
+
data.tar.gz: c41a41b359e9cd68664189fa9271b99346f67fbd4fa6286d56ec116d9cfeabf3788b16d68ad9c20bb5eb4e65f14a0760c05bacb06fc2fcd607435bb751fae213
|
data/README.md
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://feat.so">
|
|
3
|
+
<img src="https://feat.so/logo/wordmark.png" alt="feat.so" width="320" />
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# feat Ruby SDK
|
|
2
10
|
|
|
3
11
|
Server-side Ruby SDK for [feat](https://feat.so) feature flags. Local flag evaluation against a polled datafile. Standard library only - no gem dependencies.
|
|
4
12
|
|
data/lib/feat/client.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "net/http"
|
|
3
|
+
require "socket"
|
|
3
4
|
require "uri"
|
|
5
|
+
require_relative "version"
|
|
4
6
|
|
|
5
7
|
module Feat
|
|
6
8
|
# Polling HTTP client. Uses stdlib only - zero gem dependencies.
|
|
@@ -8,6 +10,15 @@ module Feat
|
|
|
8
10
|
DEFAULT_POLL_INTERVAL = 30.0
|
|
9
11
|
MIN_POLL_INTERVAL = 5.0
|
|
10
12
|
MAX_DATAFILE_BYTES = 10 * 1024 * 1024
|
|
13
|
+
OPEN_TIMEOUT_SECONDS = 3
|
|
14
|
+
READ_TIMEOUT_SECONDS = 10
|
|
15
|
+
RETRYABLE_CONNECT_ERRORS = [
|
|
16
|
+
Net::OpenTimeout,
|
|
17
|
+
Errno::ETIMEDOUT,
|
|
18
|
+
Errno::ECONNREFUSED,
|
|
19
|
+
Errno::EHOSTUNREACH,
|
|
20
|
+
Errno::ENETUNREACH,
|
|
21
|
+
].freeze
|
|
11
22
|
|
|
12
23
|
def initialize(api_key:, data_plane_url:, poll_interval: DEFAULT_POLL_INTERVAL, http_client: nil)
|
|
13
24
|
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
|
@@ -24,6 +35,7 @@ module Feat
|
|
|
24
35
|
@mutex = Mutex.new
|
|
25
36
|
@stop = false
|
|
26
37
|
@thread = nil
|
|
38
|
+
@sticky_ip = nil
|
|
27
39
|
end
|
|
28
40
|
|
|
29
41
|
# Blocking initial fetch; spawns a background poller thread.
|
|
@@ -99,11 +111,10 @@ module Feat
|
|
|
99
111
|
uri = URI.parse("#{@data_plane_url}/sdk/v1/datafile")
|
|
100
112
|
req = Net::HTTP::Get.new(uri)
|
|
101
113
|
req["Authorization"] = "Bearer #{@api_key}"
|
|
114
|
+
req["User-Agent"] = "feat-sdk-ruby/#{Feat::VERSION}"
|
|
102
115
|
@mutex.synchronize { req["If-None-Match"] = @etag if @etag }
|
|
103
116
|
|
|
104
|
-
res = (
|
|
105
|
-
http.request(req)
|
|
106
|
-
end
|
|
117
|
+
res = with_http_connection(uri) { |http| http.request(req) }
|
|
107
118
|
|
|
108
119
|
case res.code.to_i
|
|
109
120
|
when 304, 404
|
|
@@ -124,5 +135,66 @@ module Feat
|
|
|
124
135
|
raise "feat: fetch datafile failed: #{res.code}"
|
|
125
136
|
end
|
|
126
137
|
end
|
|
138
|
+
|
|
139
|
+
# Net::HTTP doesn't iterate getaddrinfo results on connect failure
|
|
140
|
+
# (Ruby 3.3 has no Happy Eyeballs); ipaddr= lets us pin each attempt
|
|
141
|
+
# to a specific IP while keeping the hostname for SNI.
|
|
142
|
+
def with_http_connection(uri, &block)
|
|
143
|
+
if @http_client
|
|
144
|
+
return @http_client.start(
|
|
145
|
+
uri.host, uri.port,
|
|
146
|
+
use_ssl: uri.scheme == "https",
|
|
147
|
+
open_timeout: OPEN_TIMEOUT_SECONDS,
|
|
148
|
+
read_timeout: READ_TIMEOUT_SECONDS,
|
|
149
|
+
) { |h| block.call(h) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
sticky = @mutex.synchronize { @sticky_ip }
|
|
153
|
+
if sticky
|
|
154
|
+
begin
|
|
155
|
+
return attempt_request(uri, sticky, &block)
|
|
156
|
+
rescue *RETRYABLE_CONNECT_ERRORS
|
|
157
|
+
@mutex.synchronize { @sticky_ip = nil if @sticky_ip == sticky }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
addresses = resolve_addresses(uri.host)
|
|
162
|
+
if addresses.empty?
|
|
163
|
+
return Net::HTTP.start(
|
|
164
|
+
uri.host, uri.port,
|
|
165
|
+
use_ssl: uri.scheme == "https",
|
|
166
|
+
open_timeout: OPEN_TIMEOUT_SECONDS,
|
|
167
|
+
read_timeout: READ_TIMEOUT_SECONDS,
|
|
168
|
+
) { |h| block.call(h) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
last_error = nil
|
|
172
|
+
addresses.each do |ip|
|
|
173
|
+
next if ip == sticky
|
|
174
|
+
begin
|
|
175
|
+
result = attempt_request(uri, ip, &block)
|
|
176
|
+
@mutex.synchronize { @sticky_ip = ip }
|
|
177
|
+
return result
|
|
178
|
+
rescue *RETRYABLE_CONNECT_ERRORS => e
|
|
179
|
+
last_error = e
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
raise last_error
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def attempt_request(uri, ip, &block)
|
|
186
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
187
|
+
http.ipaddr = ip
|
|
188
|
+
http.use_ssl = (uri.scheme == "https")
|
|
189
|
+
http.open_timeout = OPEN_TIMEOUT_SECONDS
|
|
190
|
+
http.read_timeout = READ_TIMEOUT_SECONDS
|
|
191
|
+
http.start { |h| block.call(h) }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_addresses(host)
|
|
195
|
+
Addrinfo.getaddrinfo(host, nil, nil, :STREAM).map(&:ip_address).uniq
|
|
196
|
+
rescue StandardError
|
|
197
|
+
[]
|
|
198
|
+
end
|
|
127
199
|
end
|
|
128
200
|
end
|
data/lib/feat/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: feat-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- feat HQ
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-06-04 00:00:00.000000000 Z
|
|
11
12
|
dependencies: []
|
|
12
13
|
description: Server-side Ruby SDK for feat. Polls a per-environment datafile and evaluates
|
|
13
14
|
flags locally with no per-flag network call. Stdlib only.
|
|
14
15
|
email:
|
|
15
|
-
-
|
|
16
|
+
- support@feat.so
|
|
16
17
|
executables: []
|
|
17
18
|
extensions: []
|
|
18
19
|
extra_rdoc_files: []
|
|
@@ -36,6 +37,7 @@ metadata:
|
|
|
36
37
|
source_code_uri: https://github.com/feathq/ruby-sdk
|
|
37
38
|
bug_tracker_uri: https://github.com/feathq/ruby-sdk/issues
|
|
38
39
|
changelog_uri: https://github.com/feathq/ruby-sdk/releases
|
|
40
|
+
post_install_message:
|
|
39
41
|
rdoc_options: []
|
|
40
42
|
require_paths:
|
|
41
43
|
- lib
|
|
@@ -50,7 +52,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
50
52
|
- !ruby/object:Gem::Version
|
|
51
53
|
version: '0'
|
|
52
54
|
requirements: []
|
|
53
|
-
rubygems_version: 3.
|
|
55
|
+
rubygems_version: 3.5.22
|
|
56
|
+
signing_key:
|
|
54
57
|
specification_version: 4
|
|
55
58
|
summary: feat feature-flag SDK for Ruby (server-side, local evaluation)
|
|
56
59
|
test_files: []
|