eppo-server-sdk 0.3.0 → 3.1.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 +4 -4
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Cargo.lock +2015 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +22 -0
- data/Rakefile +53 -0
- data/Steepfile +27 -0
- data/ext/eppo_client/Cargo.toml +19 -0
- data/ext/eppo_client/build.rs +5 -0
- data/ext/eppo_client/extconf.rb +6 -0
- data/ext/eppo_client/src/client.rs +178 -0
- data/ext/eppo_client/src/lib.rs +34 -0
- data/lib/eppo_client/assignment_logger.rb +7 -3
- data/lib/eppo_client/client.rb +159 -205
- data/lib/eppo_client/config.rb +4 -4
- data/lib/eppo_client/custom_errors.rb +0 -17
- data/lib/eppo_client/validation.rb +2 -2
- data/lib/eppo_client/version.rb +2 -1
- data/lib/eppo_client.rb +7 -45
- data/sig/eppo_server_sdk.rbs +96 -0
- metadata +26 -172
- data/lib/eppo_client/configuration_requestor.rb +0 -108
- data/lib/eppo_client/configuration_store.rb +0 -35
- data/lib/eppo_client/constants.rb +0 -20
- data/lib/eppo_client/http_client.rb +0 -75
- data/lib/eppo_client/lru_cache.rb +0 -28
- data/lib/eppo_client/poller.rb +0 -48
- data/lib/eppo_client/rules.rb +0 -119
- data/lib/eppo_client/shard.rb +0 -30
- data/lib/eppo_client/variation_type.rb +0 -39
metadata
CHANGED
@@ -1,191 +1,45 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: eppo-server-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eppo
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: concurrent-ruby
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.1'
|
20
|
-
- - ">="
|
21
|
-
- !ruby/object:Gem::Version
|
22
|
-
version: 1.1.9
|
23
|
-
type: :runtime
|
24
|
-
prerelease: false
|
25
|
-
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
requirements:
|
27
|
-
- - "~>"
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '1.1'
|
30
|
-
- - ">="
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: 1.1.9
|
33
|
-
- !ruby/object:Gem::Dependency
|
34
|
-
name: faraday
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
36
|
-
requirements:
|
37
|
-
- - "~>"
|
38
|
-
- !ruby/object:Gem::Version
|
39
|
-
version: '2.7'
|
40
|
-
- - ">="
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
version: 2.7.1
|
43
|
-
type: :runtime
|
44
|
-
prerelease: false
|
45
|
-
version_requirements: !ruby/object:Gem::Requirement
|
46
|
-
requirements:
|
47
|
-
- - "~>"
|
48
|
-
- !ruby/object:Gem::Version
|
49
|
-
version: '2.7'
|
50
|
-
- - ">="
|
51
|
-
- !ruby/object:Gem::Version
|
52
|
-
version: 2.7.1
|
53
|
-
- !ruby/object:Gem::Dependency
|
54
|
-
name: faraday-retry
|
55
|
-
requirement: !ruby/object:Gem::Requirement
|
56
|
-
requirements:
|
57
|
-
- - "~>"
|
58
|
-
- !ruby/object:Gem::Version
|
59
|
-
version: '2.0'
|
60
|
-
- - ">="
|
61
|
-
- !ruby/object:Gem::Version
|
62
|
-
version: 2.0.0
|
63
|
-
type: :runtime
|
64
|
-
prerelease: false
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
66
|
-
requirements:
|
67
|
-
- - "~>"
|
68
|
-
- !ruby/object:Gem::Version
|
69
|
-
version: '2.0'
|
70
|
-
- - ">="
|
71
|
-
- !ruby/object:Gem::Version
|
72
|
-
version: 2.0.0
|
73
|
-
- !ruby/object:Gem::Dependency
|
74
|
-
name: semver2
|
75
|
-
requirement: !ruby/object:Gem::Requirement
|
76
|
-
requirements:
|
77
|
-
- - "~>"
|
78
|
-
- !ruby/object:Gem::Version
|
79
|
-
version: '3.4'
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: 3.4.2
|
83
|
-
type: :runtime
|
84
|
-
prerelease: false
|
85
|
-
version_requirements: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - "~>"
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '3.4'
|
90
|
-
- - ">="
|
91
|
-
- !ruby/object:Gem::Version
|
92
|
-
version: 3.4.2
|
93
|
-
- !ruby/object:Gem::Dependency
|
94
|
-
name: rake
|
95
|
-
requirement: !ruby/object:Gem::Requirement
|
96
|
-
requirements:
|
97
|
-
- - "~>"
|
98
|
-
- !ruby/object:Gem::Version
|
99
|
-
version: '13.0'
|
100
|
-
- - ">="
|
101
|
-
- !ruby/object:Gem::Version
|
102
|
-
version: 13.0.6
|
103
|
-
type: :development
|
104
|
-
prerelease: false
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
106
|
-
requirements:
|
107
|
-
- - "~>"
|
108
|
-
- !ruby/object:Gem::Version
|
109
|
-
version: '13.0'
|
110
|
-
- - ">="
|
111
|
-
- !ruby/object:Gem::Version
|
112
|
-
version: 13.0.6
|
113
|
-
- !ruby/object:Gem::Dependency
|
114
|
-
name: rspec
|
115
|
-
requirement: !ruby/object:Gem::Requirement
|
116
|
-
requirements:
|
117
|
-
- - "~>"
|
118
|
-
- !ruby/object:Gem::Version
|
119
|
-
version: '3.12'
|
120
|
-
- - ">="
|
121
|
-
- !ruby/object:Gem::Version
|
122
|
-
version: 3.12.0
|
123
|
-
type: :development
|
124
|
-
prerelease: false
|
125
|
-
version_requirements: !ruby/object:Gem::Requirement
|
126
|
-
requirements:
|
127
|
-
- - "~>"
|
128
|
-
- !ruby/object:Gem::Version
|
129
|
-
version: '3.12'
|
130
|
-
- - ">="
|
131
|
-
- !ruby/object:Gem::Version
|
132
|
-
version: 3.12.0
|
133
|
-
- !ruby/object:Gem::Dependency
|
134
|
-
name: rubocop
|
135
|
-
requirement: !ruby/object:Gem::Requirement
|
136
|
-
requirements:
|
137
|
-
- - "~>"
|
138
|
-
- !ruby/object:Gem::Version
|
139
|
-
version: 0.82.0
|
140
|
-
type: :development
|
141
|
-
prerelease: false
|
142
|
-
version_requirements: !ruby/object:Gem::Requirement
|
143
|
-
requirements:
|
144
|
-
- - "~>"
|
145
|
-
- !ruby/object:Gem::Version
|
146
|
-
version: 0.82.0
|
147
|
-
- !ruby/object:Gem::Dependency
|
148
|
-
name: webmock
|
149
|
-
requirement: !ruby/object:Gem::Requirement
|
150
|
-
requirements:
|
151
|
-
- - "~>"
|
152
|
-
- !ruby/object:Gem::Version
|
153
|
-
version: '3.18'
|
154
|
-
- - ">="
|
155
|
-
- !ruby/object:Gem::Version
|
156
|
-
version: 3.18.1
|
157
|
-
type: :development
|
158
|
-
prerelease: false
|
159
|
-
version_requirements: !ruby/object:Gem::Requirement
|
160
|
-
requirements:
|
161
|
-
- - "~>"
|
162
|
-
- !ruby/object:Gem::Version
|
163
|
-
version: '3.18'
|
164
|
-
- - ">="
|
165
|
-
- !ruby/object:Gem::Version
|
166
|
-
version: 3.18.1
|
11
|
+
date: 2024-08-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
167
13
|
description:
|
168
|
-
email:
|
14
|
+
email:
|
15
|
+
- eppo-team@geteppo.com
|
169
16
|
executables: []
|
170
|
-
extensions:
|
17
|
+
extensions:
|
18
|
+
- ext/eppo_client/Cargo.toml
|
171
19
|
extra_rdoc_files: []
|
172
20
|
files:
|
21
|
+
- ".gitignore"
|
22
|
+
- ".rspec"
|
23
|
+
- ".rubocop.yml"
|
24
|
+
- Cargo.lock
|
25
|
+
- Cargo.toml
|
26
|
+
- LICENSE.txt
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- Steepfile
|
30
|
+
- ext/eppo_client/Cargo.toml
|
31
|
+
- ext/eppo_client/build.rs
|
32
|
+
- ext/eppo_client/extconf.rb
|
33
|
+
- ext/eppo_client/src/client.rs
|
34
|
+
- ext/eppo_client/src/lib.rs
|
173
35
|
- lib/eppo_client.rb
|
174
36
|
- lib/eppo_client/assignment_logger.rb
|
175
37
|
- lib/eppo_client/client.rb
|
176
38
|
- lib/eppo_client/config.rb
|
177
|
-
- lib/eppo_client/configuration_requestor.rb
|
178
|
-
- lib/eppo_client/configuration_store.rb
|
179
|
-
- lib/eppo_client/constants.rb
|
180
39
|
- lib/eppo_client/custom_errors.rb
|
181
|
-
- lib/eppo_client/http_client.rb
|
182
|
-
- lib/eppo_client/lru_cache.rb
|
183
|
-
- lib/eppo_client/poller.rb
|
184
|
-
- lib/eppo_client/rules.rb
|
185
|
-
- lib/eppo_client/shard.rb
|
186
40
|
- lib/eppo_client/validation.rb
|
187
|
-
- lib/eppo_client/variation_type.rb
|
188
41
|
- lib/eppo_client/version.rb
|
42
|
+
- sig/eppo_server_sdk.rbs
|
189
43
|
homepage: https://github.com/Eppo-exp/ruby-sdk
|
190
44
|
licenses:
|
191
45
|
- MIT
|
@@ -203,14 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
203
57
|
requirements:
|
204
58
|
- - ">="
|
205
59
|
- !ruby/object:Gem::Version
|
206
|
-
version: 3.0.
|
60
|
+
version: 3.0.0
|
207
61
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
208
62
|
requirements:
|
209
63
|
- - ">="
|
210
64
|
- !ruby/object:Gem::Version
|
211
|
-
version:
|
65
|
+
version: 3.3.11
|
212
66
|
requirements: []
|
213
|
-
rubygems_version: 3.
|
67
|
+
rubygems_version: 3.5.11
|
214
68
|
signing_key:
|
215
69
|
specification_version: 4
|
216
70
|
summary: Eppo SDK for Ruby
|
@@ -1,108 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'custom_errors'
|
4
|
-
require_relative 'constants'
|
5
|
-
|
6
|
-
module EppoClient
|
7
|
-
# A class for the variation object
|
8
|
-
class VariationDto
|
9
|
-
attr_reader :name, :value, :typed_value, :shard_range
|
10
|
-
|
11
|
-
def initialize(name, value, typed_value, shard_range)
|
12
|
-
@name = name
|
13
|
-
@value = value
|
14
|
-
@typed_value = typed_value
|
15
|
-
@shard_range = shard_range
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
# A class for the allocation object
|
20
|
-
class AllocationDto
|
21
|
-
attr_reader :percent_exposure, :variations
|
22
|
-
|
23
|
-
def initialize(percent_exposure, variations)
|
24
|
-
@percent_exposure = percent_exposure
|
25
|
-
@variations = variations
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# A class for the experiment configuration object
|
30
|
-
class ExperimentConfigurationDto
|
31
|
-
attr_reader :subject_shards, :enabled, :name, :overrides,
|
32
|
-
:typed_overrides, :rules, :allocations
|
33
|
-
|
34
|
-
def initialize(exp_config)
|
35
|
-
@subject_shards = exp_config['subjectShards']
|
36
|
-
@enabled = exp_config['enabled']
|
37
|
-
@name = exp_config['name'] || nil
|
38
|
-
@overrides = exp_config['overrides'] || {}
|
39
|
-
@typed_overrides = exp_config['typedOverrides'] || {}
|
40
|
-
@rules = exp_config['rules'] || []
|
41
|
-
@allocations = exp_config['allocations']
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
# A class for getting exp configs from the local cache or API
|
46
|
-
class ExperimentConfigurationRequestor
|
47
|
-
attr_reader :config_store
|
48
|
-
|
49
|
-
def initialize(http_client, config_store)
|
50
|
-
@http_client = http_client
|
51
|
-
@config_store = config_store
|
52
|
-
end
|
53
|
-
|
54
|
-
def get_configuration(experiment_key)
|
55
|
-
@http_client.is_unauthorized && raise(EppoClient::UnauthorizedError,
|
56
|
-
'please check your API key')
|
57
|
-
@config_store.retrieve_configuration(experiment_key)
|
58
|
-
end
|
59
|
-
|
60
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
61
|
-
def fetch_and_store_configurations
|
62
|
-
configs = {}
|
63
|
-
begin
|
64
|
-
exp_configs = @http_client.get(EppoClient::RAC_ENDPOINT).fetch(
|
65
|
-
'flags', {}
|
66
|
-
)
|
67
|
-
# rubocop: disable Metrics/BlockLength
|
68
|
-
exp_configs.each do |exp_key, exp_config|
|
69
|
-
exp_config['allocations'].each do |k, v|
|
70
|
-
exp_config['allocations'][k] = EppoClient::AllocationDto.new(
|
71
|
-
v['percentExposure'],
|
72
|
-
v['variations'].map do |var|
|
73
|
-
EppoClient::VariationDto.new(
|
74
|
-
var['name'], var['value'], var['typedValue'],
|
75
|
-
EppoClient::ShardRange.new(var['shardRange']['start'],
|
76
|
-
var['shardRange']['end'])
|
77
|
-
)
|
78
|
-
end
|
79
|
-
)
|
80
|
-
end
|
81
|
-
exp_config['rules'] = exp_config['rules'].map do |rule|
|
82
|
-
EppoClient::Rule.new(
|
83
|
-
conditions: rule['conditions'].map do |condition|
|
84
|
-
EppoClient::Condition.new(
|
85
|
-
value: condition['value'],
|
86
|
-
operator: condition['operator'],
|
87
|
-
attribute: condition['attribute']
|
88
|
-
)
|
89
|
-
end,
|
90
|
-
allocation_key: rule['allocationKey']
|
91
|
-
)
|
92
|
-
end
|
93
|
-
configs[exp_key] = EppoClient::ExperimentConfigurationDto.new(
|
94
|
-
exp_config
|
95
|
-
)
|
96
|
-
end
|
97
|
-
# rubocop: enable Metrics/BlockLength
|
98
|
-
@config_store.assign_configurations(configs)
|
99
|
-
rescue EppoClient::HttpRequestError => e
|
100
|
-
Logger.new($stdout).error(
|
101
|
-
"Error retrieving assignment configurations: #{e}"
|
102
|
-
)
|
103
|
-
end
|
104
|
-
configs
|
105
|
-
end
|
106
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
107
|
-
end
|
108
|
-
end
|
@@ -1,35 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent/atomic/read_write_lock'
|
4
|
-
|
5
|
-
require_relative 'lru_cache'
|
6
|
-
|
7
|
-
module EppoClient
|
8
|
-
# A thread safe store for the configurations to ensure that retrievals pull from a single source of truth
|
9
|
-
class ConfigurationStore
|
10
|
-
attr_reader :lock, :cache
|
11
|
-
|
12
|
-
def initialize(max_size)
|
13
|
-
@cache = EppoClient::LRUCache.new(max_size)
|
14
|
-
@lock = Concurrent::ReadWriteLock.new
|
15
|
-
end
|
16
|
-
|
17
|
-
def retrieve_configuration(key)
|
18
|
-
@lock.with_read_lock { @cache[key] }
|
19
|
-
end
|
20
|
-
|
21
|
-
def assign_configurations(configs)
|
22
|
-
@lock.with_write_lock do
|
23
|
-
# Create a temporary new cache and populate it.
|
24
|
-
new_cache = EppoClient::LRUCache.new(@cache.size)
|
25
|
-
configs.each do |key, config|
|
26
|
-
new_cache[key] = config
|
27
|
-
end
|
28
|
-
|
29
|
-
# Replace the old cache with the new one.
|
30
|
-
# Performs an atomic swap.
|
31
|
-
@cache = new_cache
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
@@ -1,20 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'logger'
|
4
|
-
|
5
|
-
module EppoClient
|
6
|
-
# default level for logging
|
7
|
-
DEFAULT_LOGGER_LEVEL = Logger::INFO
|
8
|
-
|
9
|
-
# configuration cache constants
|
10
|
-
MAX_CACHE_ENTRIES = 1000 # arbitrary; the caching library requires a max limit
|
11
|
-
|
12
|
-
# poller constants
|
13
|
-
SECOND_MILLIS = 1000
|
14
|
-
MINUTE_MILLIS = 60 * SECOND_MILLIS
|
15
|
-
POLL_JITTER_MILLIS = 30 * SECOND_MILLIS
|
16
|
-
POLL_INTERVAL_MILLIS = 5 * MINUTE_MILLIS
|
17
|
-
|
18
|
-
# the configs endpoint
|
19
|
-
RAC_ENDPOINT = 'randomized_assignment/v3/config'
|
20
|
-
end
|
@@ -1,75 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'faraday'
|
4
|
-
require 'faraday/retry'
|
5
|
-
|
6
|
-
require_relative 'custom_errors'
|
7
|
-
|
8
|
-
REQUEST_TIMEOUT_SECONDS = 2
|
9
|
-
# This applies only to failed DNS lookups and connection timeouts,
|
10
|
-
# never to requests where data has made it to the server.
|
11
|
-
MAX_RETRIES = 3
|
12
|
-
|
13
|
-
module EppoClient
|
14
|
-
# The SDK params object
|
15
|
-
class SdkParams
|
16
|
-
attr_reader :api_key, :sdk_name, :sdk_version
|
17
|
-
|
18
|
-
def initialize(api_key, sdk_name, sdk_version)
|
19
|
-
@api_key = api_key
|
20
|
-
@sdk_name = sdk_name
|
21
|
-
@sdk_version = sdk_version
|
22
|
-
end
|
23
|
-
|
24
|
-
# attributes are camelCase because that's what the backend endpoint expects
|
25
|
-
def formatted
|
26
|
-
{
|
27
|
-
'apiKey' => api_key,
|
28
|
-
'sdkName' => sdk_name,
|
29
|
-
'sdkVersion' => sdk_version
|
30
|
-
}
|
31
|
-
end
|
32
|
-
|
33
|
-
# Hide instance variables (specifically api_key) from logs
|
34
|
-
def inspect
|
35
|
-
"#<EppoClient::SdkParams:#{object_id}>"
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# The http request client with retry/timeout behavior
|
40
|
-
class HttpClient
|
41
|
-
attr_reader :is_unauthorized
|
42
|
-
|
43
|
-
@retry_options = {
|
44
|
-
max: MAX_RETRIES,
|
45
|
-
interval: 0.05,
|
46
|
-
interval_randomness: 0.5,
|
47
|
-
backoff_factor: 2,
|
48
|
-
exceptions: ['Timeout::Error']
|
49
|
-
}
|
50
|
-
|
51
|
-
def initialize(base_url, sdk_params)
|
52
|
-
@base_url = base_url
|
53
|
-
@sdk_params = sdk_params
|
54
|
-
@is_unauthorized = false
|
55
|
-
end
|
56
|
-
|
57
|
-
def get(resource)
|
58
|
-
conn = Faraday::Connection.new(@base_url, params: @sdk_params) do |f|
|
59
|
-
f.request :retry, @retry_options
|
60
|
-
end
|
61
|
-
conn.options.timeout = REQUEST_TIMEOUT_SECONDS
|
62
|
-
response = conn.get(resource)
|
63
|
-
@is_unauthorized = response.status == 401
|
64
|
-
raise get_http_error(response.status, resource) if response.status != 200
|
65
|
-
|
66
|
-
JSON.parse(response.body)
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def get_http_error(status_code, resource)
|
72
|
-
EppoClient::HttpRequestError.new("HTTP #{status_code} error while requesting resource #{resource}", status_code)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module EppoClient
|
4
|
-
# The LRU cache relies on the fact that Ruby's Hash class maintains insertion order. So deleting
|
5
|
-
# and re-inserting a key-value pair on access moves the key to the last position. When an
|
6
|
-
# entry is added and the cache is full, the first entry is removed.
|
7
|
-
class LRUCache
|
8
|
-
attr_reader :cache, :size
|
9
|
-
|
10
|
-
# Creates a new LRUCache that can hold +size+ entries.
|
11
|
-
def initialize(size)
|
12
|
-
@size = size
|
13
|
-
@cache = {}
|
14
|
-
end
|
15
|
-
|
16
|
-
# Returns the stored value for +key+ or +nil+ if no value was stored under the key.
|
17
|
-
def [](key)
|
18
|
-
(val = @cache.delete(key)).nil? ? nil : @cache[key] = val
|
19
|
-
end
|
20
|
-
|
21
|
-
# Stores the +value+ under the +key+.
|
22
|
-
def []=(key, value)
|
23
|
-
@cache.delete(key)
|
24
|
-
@cache[key] = value
|
25
|
-
@cache.shift if @cache.length > @size
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/eppo_client/poller.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'concurrent/atom'
|
4
|
-
|
5
|
-
# The poller
|
6
|
-
module EppoClient
|
7
|
-
# The poller class invokes a callback and waits on repeat on a separate thread
|
8
|
-
class Poller
|
9
|
-
def initialize(interval_millis, jitter_millis, callback)
|
10
|
-
@jitter_millis = jitter_millis
|
11
|
-
@interval = interval_millis
|
12
|
-
@stopped = Concurrent::Atom.new(false)
|
13
|
-
@callback = callback
|
14
|
-
@thread = nil
|
15
|
-
end
|
16
|
-
|
17
|
-
def start
|
18
|
-
@stopped.reset(false)
|
19
|
-
@thread = Thread.new { poll }
|
20
|
-
end
|
21
|
-
|
22
|
-
def stop
|
23
|
-
@stopped.reset(true)
|
24
|
-
Thread.kill(@thread)
|
25
|
-
end
|
26
|
-
|
27
|
-
def stopped?
|
28
|
-
@stopped.value
|
29
|
-
end
|
30
|
-
|
31
|
-
def poll
|
32
|
-
until stopped?
|
33
|
-
begin
|
34
|
-
@callback.call
|
35
|
-
rescue StandardError => e
|
36
|
-
Logger.new($stdout).error("Unexpected error running poll task: #{e}")
|
37
|
-
break
|
38
|
-
end
|
39
|
-
_wait_for_interval
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def _wait_for_interval
|
44
|
-
interval_with_jitter = @interval - rand(@jitter_millis)
|
45
|
-
sleep interval_with_jitter / 1000
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
data/lib/eppo_client/rules.rb
DELETED
@@ -1,119 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'semver'
|
4
|
-
|
5
|
-
# The helper module for rules
|
6
|
-
module EppoClient
|
7
|
-
module OperatorType
|
8
|
-
MATCHES = 'MATCHES'
|
9
|
-
GTE = 'GTE'
|
10
|
-
GT = 'GT'
|
11
|
-
LTE = 'LTE'
|
12
|
-
LT = 'LT'
|
13
|
-
ONE_OF = 'ONE_OF'
|
14
|
-
NOT_ONE_OF = 'NOT_ONE_OF'
|
15
|
-
end
|
16
|
-
|
17
|
-
# A class for the Condition object
|
18
|
-
class Condition
|
19
|
-
attr_accessor :operator, :attribute, :value
|
20
|
-
|
21
|
-
def initialize(operator:, attribute:, value:)
|
22
|
-
@operator = operator
|
23
|
-
@attribute = attribute
|
24
|
-
@value = value
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
# A class for the Rule object
|
29
|
-
class Rule
|
30
|
-
attr_accessor :allocation_key, :conditions
|
31
|
-
|
32
|
-
def initialize(allocation_key:, conditions:)
|
33
|
-
@allocation_key = allocation_key
|
34
|
-
@conditions = conditions
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def find_matching_rule(subject_attributes, rules)
|
39
|
-
rules.each do |rule|
|
40
|
-
return rule if matches_rule(subject_attributes, rule)
|
41
|
-
end
|
42
|
-
nil
|
43
|
-
end
|
44
|
-
|
45
|
-
def matches_rule(subject_attributes, rule)
|
46
|
-
rule.conditions.each do |condition|
|
47
|
-
return false unless evaluate_condition(subject_attributes, condition)
|
48
|
-
end
|
49
|
-
true
|
50
|
-
end
|
51
|
-
|
52
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
53
|
-
def evaluate_condition(subject_attributes, condition)
|
54
|
-
subject_value = subject_attributes[condition.attribute]
|
55
|
-
return false if subject_value.nil?
|
56
|
-
|
57
|
-
case condition.operator
|
58
|
-
when OperatorType::MATCHES
|
59
|
-
!!(Regexp.new(condition.value) =~ subject_value.to_s)
|
60
|
-
when OperatorType::ONE_OF
|
61
|
-
condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
|
62
|
-
when OperatorType::NOT_ONE_OF
|
63
|
-
!condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
|
64
|
-
else
|
65
|
-
# Numeric operator: value could be numeric or semver.
|
66
|
-
if subject_value.is_a?(Numeric)
|
67
|
-
evaluate_numeric_condition(subject_value, condition)
|
68
|
-
elsif valid_semver?(subject_value)
|
69
|
-
compare_semver(subject_value, condition.value, condition.operator)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
74
|
-
|
75
|
-
# rubocop:disable Metrics/MethodLength
|
76
|
-
def evaluate_numeric_condition(subject_value, condition)
|
77
|
-
case condition.operator
|
78
|
-
when OperatorType::GT
|
79
|
-
subject_value > condition.value
|
80
|
-
when OperatorType::GTE
|
81
|
-
subject_value >= condition.value
|
82
|
-
when OperatorType::LT
|
83
|
-
subject_value < condition.value
|
84
|
-
when OperatorType::LTE
|
85
|
-
subject_value <= condition.value
|
86
|
-
else
|
87
|
-
false
|
88
|
-
end
|
89
|
-
end
|
90
|
-
# rubocop:enable Metrics/MethodLength
|
91
|
-
|
92
|
-
# rubocop:disable Metrics/MethodLength
|
93
|
-
def compare_semver(attribute_value, condition_value, operator)
|
94
|
-
unless valid_semver?(attribute_value) && valid_semver?(condition_value)
|
95
|
-
return false
|
96
|
-
end
|
97
|
-
|
98
|
-
case operator
|
99
|
-
when OperatorType::GT
|
100
|
-
SemVer.parse(attribute_value) > SemVer.parse(condition_value)
|
101
|
-
when OperatorType::GTE
|
102
|
-
SemVer.parse(attribute_value) >= SemVer.parse(condition_value)
|
103
|
-
when OperatorType::LT
|
104
|
-
SemVer.parse(attribute_value) < SemVer.parse(condition_value)
|
105
|
-
when OperatorType::LTE
|
106
|
-
SemVer.parse(attribute_value) <= SemVer.parse(condition_value)
|
107
|
-
else
|
108
|
-
false
|
109
|
-
end
|
110
|
-
end
|
111
|
-
# rubocop:enable Metrics/MethodLength
|
112
|
-
|
113
|
-
def valid_semver?(string)
|
114
|
-
!SemVer.parse(string).nil?
|
115
|
-
end
|
116
|
-
|
117
|
-
module_function :find_matching_rule, :matches_rule, :evaluate_condition,
|
118
|
-
:evaluate_numeric_condition, :valid_semver?, :compare_semver
|
119
|
-
end
|
data/lib/eppo_client/shard.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'digest'
|
4
|
-
|
5
|
-
# The helper module for shard logic
|
6
|
-
module EppoClient
|
7
|
-
# A class for checking if a shard is in a range
|
8
|
-
class ShardRange
|
9
|
-
attr_reader :start, :end
|
10
|
-
|
11
|
-
def initialize(range_start, range_end)
|
12
|
-
@start = range_start
|
13
|
-
@end = range_end
|
14
|
-
end
|
15
|
-
|
16
|
-
def shard_in_range?(shard)
|
17
|
-
shard >= @start && shard < @end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
module_function
|
22
|
-
|
23
|
-
def get_shard(input, subject_shards)
|
24
|
-
hash_output = Digest::MD5.hexdigest(input)
|
25
|
-
# get the first 4 bytes of the md5 hex string and parse it using base 16
|
26
|
-
# (8 hex characters represent 4 bytes, e.g. 0xffffffff represents the max 4-byte integer)
|
27
|
-
int_from_hash = hash_output[0...8].to_i(16)
|
28
|
-
int_from_hash % subject_shards
|
29
|
-
end
|
30
|
-
end
|