eppo-server-sdk 3.1.1 → 3.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6fb69972c523dd9d2c7c683c751c012608c63bb36fd3a6fe9463fb27d77d3d6
4
- data.tar.gz: dfb6b27af63f652094f2141791aaf31be01ce8277801ce6537b30f0cbfa64820
3
+ metadata.gz: 9cd858b9082e0b7d776c90f73c086271ac4e7983c7c0d981f060cc44ee2953b1
4
+ data.tar.gz: 9fa7706a5fc234775980808cafed2924322b8d4db797fe878346b78da8b34d8c
5
5
  SHA512:
6
- metadata.gz: 1d191dfefc76fb61a1b873c61da265066e1d674e628864d326d3d51bb3a5c14933fb69aa48e72a1325bd2753a4acc2083bde48d1daf1a408c29b90cea4f858b5
7
- data.tar.gz: b79006cc38615b15e44d12362fcc61520ae34f852272fe5a42d4761d7e36e5c407aa1a63df0226109c2198a4798fbb0ded3ce3c0b22fa27e004a69165a2a1696
6
+ metadata.gz: cb6fa1371ed76e2c0f041625ac1aa336ae13c2fe303e722749efb4ddf9b9e7d0ba1536978b8d889e78f1e573a8021be0f9396f51a0f623dcaac1f7e66c4e9c75
7
+ data.tar.gz: bb022578cba95db2cdfae89cc1431abaf30cb7c83875f18ee8ca1d9feecfb93e735aa72d3e9990d59a4e718d401ec95d65063ccc2d3aef57d8f0a1ae5f2d6410
data/Cargo.lock CHANGED
@@ -304,7 +304,7 @@ dependencies = [
304
304
 
305
305
  [[package]]
306
306
  name = "eppo_client"
307
- version = "3.1.1"
307
+ version = "3.2.0"
308
308
  dependencies = [
309
309
  "env_logger",
310
310
  "eppo_core",
@@ -312,17 +312,19 @@ dependencies = [
312
312
  "magnus",
313
313
  "rb-sys",
314
314
  "serde",
315
+ "serde_json",
315
316
  "serde_magnus",
316
317
  ]
317
318
 
318
319
  [[package]]
319
320
  name = "eppo_core"
320
- version = "3.0.0"
321
+ version = "4.0.0"
321
322
  source = "registry+https://github.com/rust-lang/crates.io-index"
322
- checksum = "34f3fc5a7f54cc47a5ebf063025176726db7eb5e51661185b5f4d20aaacea611"
323
+ checksum = "b071fed21065318dcd6a91a443bd9f6b39796d727297b03d092e2e8dc9e02414"
323
324
  dependencies = [
324
325
  "chrono",
325
326
  "derive_more",
327
+ "faststr",
326
328
  "log",
327
329
  "md5",
328
330
  "rand",
@@ -367,6 +369,17 @@ version = "2.1.0"
367
369
  source = "registry+https://github.com/rust-lang/crates.io-index"
368
370
  checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
369
371
 
372
+ [[package]]
373
+ name = "faststr"
374
+ version = "0.2.23"
375
+ source = "registry+https://github.com/rust-lang/crates.io-index"
376
+ checksum = "4dc21a7d5a45182c2bb5ae9471b93f10919c0744b54403e54a9e2329c26ed5a3"
377
+ dependencies = [
378
+ "bytes",
379
+ "serde",
380
+ "simdutf8",
381
+ ]
382
+
370
383
  [[package]]
371
384
  name = "fnv"
372
385
  version = "1.0.7"
@@ -1283,9 +1296,9 @@ dependencies = [
1283
1296
 
1284
1297
  [[package]]
1285
1298
  name = "serde_json"
1286
- version = "1.0.124"
1299
+ version = "1.0.128"
1287
1300
  source = "registry+https://github.com/rust-lang/crates.io-index"
1288
- checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
1301
+ checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
1289
1302
  dependencies = [
1290
1303
  "itoa",
1291
1304
  "memchr",
@@ -1328,6 +1341,12 @@ version = "1.3.0"
1328
1341
  source = "registry+https://github.com/rust-lang/crates.io-index"
1329
1342
  checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1330
1343
 
1344
+ [[package]]
1345
+ name = "simdutf8"
1346
+ version = "0.1.5"
1347
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1348
+ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
1349
+
1331
1350
  [[package]]
1332
1351
  name = "slab"
1333
1352
  version = "0.4.9"
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "eppo_client"
3
3
  # TODO: this version and lib/eppo_client/version.rb should be in sync
4
- version = "3.1.1"
4
+ version = "3.2.0"
5
5
  edition = "2021"
6
6
  license = "MIT"
7
7
  publish = false
@@ -12,9 +12,10 @@ crate-type = ["cdylib"]
12
12
 
13
13
  [dependencies]
14
14
  env_logger = { version = "0.11.3", features = ["unstable-kv"] }
15
- eppo_core = { version = "3.0.0" }
15
+ eppo_core = { version = "4.0.0" }
16
16
  log = { version = "0.4.21", features = ["kv_serde"] }
17
- magnus = { version = "0.6.2" }
17
+ magnus = { version = "0.6.4" }
18
18
  serde = { version = "1.0.203", features = ["derive"] }
19
19
  serde_magnus = "0.8.1"
20
20
  rb-sys = "0.9"
21
+ serde_json = "1.0.128"
@@ -1,20 +1,24 @@
1
- use std::{cell::RefCell, sync::Arc};
1
+ use std::{cell::RefCell, sync::Arc, time::Duration};
2
2
 
3
3
  use eppo_core::{
4
4
  configuration_fetcher::{ConfigurationFetcher, ConfigurationFetcherConfig},
5
5
  configuration_store::ConfigurationStore,
6
6
  eval::{Evaluator, EvaluatorConfig},
7
- poller_thread::PollerThread,
7
+ poller_thread::{PollerThread, PollerThreadConfig},
8
8
  ufc::VariationType,
9
- Attributes, ContextAttributes, SdkMetadata,
9
+ Attributes, ContextAttributes,
10
10
  };
11
11
  use magnus::{error::Result, exception, prelude::*, Error, TryConvert, Value};
12
12
 
13
+ use crate::{configuration::Configuration, SDK_METADATA};
14
+
13
15
  #[derive(Debug)]
14
16
  #[magnus::wrap(class = "EppoClient::Core::Config", size, free_immediately)]
15
17
  pub struct Config {
16
18
  api_key: String,
17
19
  base_url: String,
20
+ poll_interval: Option<Duration>,
21
+ poll_jitter: Duration,
18
22
  }
19
23
 
20
24
  impl TryConvert for Config {
@@ -22,12 +26,21 @@ impl TryConvert for Config {
22
26
  fn try_convert(val: magnus::Value) -> Result<Self> {
23
27
  let api_key = String::try_convert(val.funcall("api_key", ())?)?;
24
28
  let base_url = String::try_convert(val.funcall("base_url", ())?)?;
25
- Ok(Config { api_key, base_url })
29
+ let poll_interval_seconds =
30
+ Option::<u64>::try_convert(val.funcall("poll_interval_seconds", ())?)?;
31
+ let poll_jitter_seconds = u64::try_convert(val.funcall("poll_jitter_seconds", ())?)?;
32
+ Ok(Config {
33
+ api_key,
34
+ base_url,
35
+ poll_interval: poll_interval_seconds.map(Duration::from_secs),
36
+ poll_jitter: Duration::from_secs(poll_jitter_seconds),
37
+ })
26
38
  }
27
39
  }
28
40
 
29
41
  #[magnus::wrap(class = "EppoClient::Core::Client")]
30
42
  pub struct Client {
43
+ configuration_store: Arc<ConfigurationStore>,
31
44
  evaluator: Evaluator,
32
45
  // Magnus only allows sharing aliased references (&T) through the API, so we need to use RefCell
33
46
  // to get interior mutability.
@@ -41,29 +54,35 @@ impl Client {
41
54
  pub fn new(config: Config) -> Client {
42
55
  let configuration_store = Arc::new(ConfigurationStore::new());
43
56
 
44
- let sdk_metadata = SdkMetadata {
45
- name: "ruby",
46
- version: env!("CARGO_PKG_VERSION"),
57
+ let poller_thread = if let Some(poll_interval) = config.poll_interval {
58
+ Some(
59
+ PollerThread::start_with_config(
60
+ ConfigurationFetcher::new(ConfigurationFetcherConfig {
61
+ base_url: config.base_url,
62
+ api_key: config.api_key,
63
+ sdk_metadata: SDK_METADATA,
64
+ }),
65
+ configuration_store.clone(),
66
+ PollerThreadConfig {
67
+ interval: poll_interval,
68
+ jitter: config.poll_jitter,
69
+ },
70
+ )
71
+ .expect("should be able to start poller thread"),
72
+ )
73
+ } else {
74
+ None
47
75
  };
48
76
 
49
- let poller_thread = PollerThread::start(
50
- ConfigurationFetcher::new(ConfigurationFetcherConfig {
51
- base_url: config.base_url,
52
- api_key: config.api_key,
53
- sdk_metadata: sdk_metadata.clone(),
54
- }),
55
- configuration_store.clone(),
56
- )
57
- .expect("should be able to start poller thread");
58
-
59
77
  let evaluator = Evaluator::new(EvaluatorConfig {
60
- configuration_store,
61
- sdk_metadata,
78
+ configuration_store: configuration_store.clone(),
79
+ sdk_metadata: SDK_METADATA,
62
80
  });
63
81
 
64
82
  Client {
83
+ configuration_store,
65
84
  evaluator,
66
- poller_thread: RefCell::new(Some(poller_thread)),
85
+ poller_thread: RefCell::new(poller_thread),
67
86
  }
68
87
  }
69
88
 
@@ -81,8 +100,8 @@ impl Client {
81
100
  .evaluator
82
101
  .get_assignment(
83
102
  &flag_key,
84
- &subject_key,
85
- &subject_attributes,
103
+ &subject_key.into(),
104
+ &Arc::new(subject_attributes),
86
105
  Some(expected_type),
87
106
  )
88
107
  // TODO: maybe expose possible errors individually.
@@ -103,8 +122,8 @@ impl Client {
103
122
 
104
123
  let result = self.evaluator.get_assignment_details(
105
124
  &flag_key,
106
- &subject_key,
107
- &subject_attributes,
125
+ &subject_key.into(),
126
+ &Arc::new(subject_attributes),
108
127
  Some(expected_type),
109
128
  );
110
129
 
@@ -132,10 +151,10 @@ impl Client {
132
151
 
133
152
  let result = self.evaluator.get_bandit_action(
134
153
  &flag_key,
135
- &subject_key,
154
+ &subject_key.into(),
136
155
  &subject_attributes,
137
156
  &actions,
138
- &default_variation,
157
+ &default_variation.into(),
139
158
  );
140
159
 
141
160
  serde_magnus::serialize(&result)
@@ -162,15 +181,26 @@ impl Client {
162
181
 
163
182
  let result = self.evaluator.get_bandit_action_details(
164
183
  &flag_key,
165
- &subject_key,
184
+ &subject_key.into(),
166
185
  &subject_attributes,
167
186
  &actions,
168
- &default_variation,
187
+ &default_variation.into(),
169
188
  );
170
189
 
171
190
  serde_magnus::serialize(&result)
172
191
  }
173
192
 
193
+ pub fn get_configuration(&self) -> Option<Configuration> {
194
+ self.configuration_store
195
+ .get_configuration()
196
+ .map(|it| it.into())
197
+ }
198
+
199
+ pub fn set_configuration(&self, configuration: &Configuration) {
200
+ self.configuration_store
201
+ .set_configuration(configuration.clone().into())
202
+ }
203
+
174
204
  pub fn shutdown(&self) {
175
205
  if let Some(t) = self.poller_thread.take() {
176
206
  let _ = t.shutdown();
@@ -0,0 +1,113 @@
1
+ use std::sync::Arc;
2
+
3
+ use magnus::{function, method, prelude::*, scan_args::get_kwargs, Error, RHash, RString, Ruby};
4
+
5
+ use eppo_core::{ufc::UniversalFlagConfig, Configuration as CoreConfiguration};
6
+
7
+ use crate::{gc_lock::GcLock, SDK_METADATA};
8
+
9
+ pub(crate) fn init(ruby: &Ruby) -> Result<(), Error> {
10
+ let eppo_client = ruby.define_module("EppoClient")?;
11
+
12
+ let configuration = eppo_client.define_class("Configuration", magnus::class::object())?;
13
+ configuration.define_singleton_method("new", function!(Configuration::new, 1))?;
14
+ configuration.define_method(
15
+ "flags_configuration",
16
+ method!(Configuration::flags_configuration, 0),
17
+ )?;
18
+ configuration.define_method(
19
+ "bandits_configuration",
20
+ method!(Configuration::bandits_configuration, 0),
21
+ )?;
22
+
23
+ Ok(())
24
+ }
25
+
26
+ #[derive(Debug, Clone)]
27
+ #[magnus::wrap(class = "EppoClient::Configuration", free_immediately)]
28
+ pub struct Configuration {
29
+ inner: Arc<CoreConfiguration>,
30
+ }
31
+
32
+ impl Configuration {
33
+ fn new(ruby: &Ruby, kw: RHash) -> Result<Configuration, Error> {
34
+ let args = get_kwargs(kw, &["flags_configuration"], &["bandits_configuration"])?;
35
+ let (flags_configuration,): (RString,) = args.required;
36
+ let (bandits_configuration,): (Option<Option<RString>>,) = args.optional;
37
+ let rest: RHash = args.splat;
38
+ if !rest.is_empty() {
39
+ return Err(Error::new(
40
+ ruby.exception_arg_error(),
41
+ format!("unexpected keyword arguments: {:?}", rest),
42
+ ));
43
+ }
44
+
45
+ let inner = {
46
+ let _gc_lock = GcLock::new(ruby);
47
+
48
+ Arc::new(CoreConfiguration::from_server_response(
49
+ UniversalFlagConfig::from_json(
50
+ SDK_METADATA,
51
+ unsafe {
52
+ // SAFETY: we have disabled GC, so the memory can't be modified concurrently.
53
+ flags_configuration.as_slice()
54
+ }
55
+ .to_vec(),
56
+ )
57
+ .map_err(|err| {
58
+ Error::new(
59
+ ruby.exception_arg_error(),
60
+ format!("failed to parse flags_configuration: {err:?}"),
61
+ )
62
+ })?,
63
+ bandits_configuration
64
+ .flatten()
65
+ .map(|bandits| {
66
+ serde_json::from_slice(unsafe {
67
+ // SAFETY: we have disabled GC, so the memory can't be modified concurrently.
68
+ bandits.as_slice()
69
+ })
70
+ })
71
+ .transpose()
72
+ .map_err(|err| {
73
+ Error::new(
74
+ ruby.exception_arg_error(),
75
+ format!("failed to parse bandits_configuration: {err:?}"),
76
+ )
77
+ })?,
78
+ ))
79
+ };
80
+
81
+ Ok(Configuration { inner })
82
+ }
83
+
84
+ fn flags_configuration(ruby: &Ruby, rb_self: &Self) -> Result<RString, Error> {
85
+ Ok(ruby.str_from_slice(rb_self.inner.flags.to_json()))
86
+ }
87
+
88
+ fn bandits_configuration(ruby: &Ruby, rb_self: &Self) -> Result<Option<RString>, Error> {
89
+ let Some(bandits) = &rb_self.inner.bandits else {
90
+ return Ok(None)
91
+ };
92
+ let vec = serde_json::to_vec(bandits).map_err(|err| {
93
+ // this should never happen
94
+ Error::new(
95
+ ruby.exception_runtime_error(),
96
+ format!("failed to serialize bandits configuration: {err:?}"),
97
+ )
98
+ })?;
99
+ Ok(Some(ruby.str_from_slice(&vec)))
100
+ }
101
+ }
102
+
103
+ impl From<Arc<CoreConfiguration>> for Configuration {
104
+ fn from(inner: Arc<CoreConfiguration>) -> Configuration {
105
+ Configuration { inner }
106
+ }
107
+ }
108
+
109
+ impl From<Configuration> for Arc<CoreConfiguration> {
110
+ fn from(value: Configuration) -> Arc<CoreConfiguration> {
111
+ value.inner
112
+ }
113
+ }
@@ -0,0 +1,25 @@
1
+ use magnus::Ruby;
2
+
3
+ pub struct GcLock<'a> {
4
+ ruby: &'a Ruby,
5
+ /// Holds `true` if GC was already disabled before acquiring the lock (so it doesn't need to be
6
+ /// re-enabled).
7
+ gc_was_disabled: bool,
8
+ }
9
+
10
+ impl<'a> GcLock<'a> {
11
+ pub fn new(ruby: &'a Ruby) -> GcLock<'a> {
12
+ GcLock {
13
+ ruby,
14
+ gc_was_disabled: ruby.gc_disable(),
15
+ }
16
+ }
17
+ }
18
+
19
+ impl<'a> Drop for GcLock<'a> {
20
+ fn drop(&mut self) {
21
+ if !self.gc_was_disabled {
22
+ self.ruby.gc_enable();
23
+ }
24
+ }
25
+ }
@@ -1,12 +1,20 @@
1
1
  mod client;
2
+ mod configuration;
3
+ mod gc_lock;
2
4
 
5
+ use eppo_core::SdkMetadata;
3
6
  use magnus::{function, method, prelude::*, Error, Object, Ruby};
4
7
 
5
8
  use crate::client::Client;
6
9
 
10
+ pub(crate) const SDK_METADATA: SdkMetadata = SdkMetadata {
11
+ name: "ruby",
12
+ version: env!("CARGO_PKG_VERSION"),
13
+ };
14
+
7
15
  #[magnus::init]
8
16
  fn init(ruby: &Ruby) -> Result<(), Error> {
9
- env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("eppo")).init();
17
+ env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("eppo=debug")).init();
10
18
 
11
19
  let eppo_client = ruby.define_module("EppoClient")?;
12
20
  let core = eppo_client.define_module("Core")?;
@@ -23,12 +31,24 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
23
31
  "get_bandit_action_details",
24
32
  method!(Client::get_bandit_action_details, 5),
25
33
  )?;
34
+ core_client.define_method("configuration", method!(Client::get_configuration, 0))?;
35
+ core_client.define_method("configuration=", method!(Client::set_configuration, 1))?;
26
36
  core_client.define_method("shutdown", method!(Client::shutdown, 0))?;
27
37
 
28
38
  core.const_set(
29
39
  "DEFAULT_BASE_URL",
30
40
  eppo_core::configuration_fetcher::DEFAULT_BASE_URL,
31
41
  )?;
42
+ core.const_set(
43
+ "DEFAULT_POLL_INTERVAL_SECONDS",
44
+ eppo_core::poller_thread::PollerThreadConfig::DEFAULT_POLL_INTERVAL.as_secs(),
45
+ )?;
46
+ core.const_set(
47
+ "DEFAULT_POLL_JITTER_SECONDS",
48
+ eppo_core::poller_thread::PollerThreadConfig::DEFAULT_POLL_JITTER.as_secs(),
49
+ )?;
50
+
51
+ configuration::init(ruby)?;
32
52
 
33
53
  Ok(())
34
54
  }
@@ -24,6 +24,14 @@ module EppoClient
24
24
  @core = EppoClient::Core::Client.new(config)
25
25
  end
26
26
 
27
+ def configuration
28
+ @core.configuration
29
+ end
30
+
31
+ def configuration=(configuration)
32
+ @core.configuration = configuration
33
+ end
34
+
27
35
  def shutdown
28
36
  @core.shutdown
29
37
  end
@@ -6,12 +6,14 @@ require_relative "assignment_logger"
6
6
  module EppoClient
7
7
  # The class for configuring the Eppo client singleton
8
8
  class Config
9
- attr_reader :api_key, :assignment_logger, :base_url
9
+ attr_reader :api_key, :assignment_logger, :base_url, :poll_interval_seconds, :poll_jitter_seconds
10
10
 
11
- def initialize(api_key, assignment_logger: AssignmentLogger.new, base_url: EppoClient::Core::DEFAULT_BASE_URL)
11
+ def initialize(api_key, assignment_logger: AssignmentLogger.new, base_url: EppoClient::Core::DEFAULT_BASE_URL, poll_interval_seconds: EppoClient::Core::DEFAULT_POLL_INTERVAL_SECONDS, poll_jitter_seconds: EppoClient::Core::DEFAULT_POLL_JITTER_SECONDS, initial_configuration: nil)
12
12
  @api_key = api_key
13
13
  @assignment_logger = assignment_logger
14
14
  @base_url = base_url
15
+ @poll_interval_seconds = poll_interval_seconds
16
+ @poll_jitter_seconds = poll_jitter_seconds
15
17
  end
16
18
 
17
19
  def validate
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: this version and ext/eppo_rb/Cargo.toml should be in sync
3
+ # TODO: this version and ext/eppo_client/Cargo.toml should be in sync
4
4
  module EppoClient
5
- VERSION = "3.1.1"
5
+ VERSION = "3.2.0"
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eppo-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eppo
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-19 00:00:00.000000000 Z
11
+ date: 2024-10-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -31,6 +31,8 @@ files:
31
31
  - ext/eppo_client/build.rs
32
32
  - ext/eppo_client/extconf.rb
33
33
  - ext/eppo_client/src/client.rs
34
+ - ext/eppo_client/src/configuration.rs
35
+ - ext/eppo_client/src/gc_lock.rs
34
36
  - ext/eppo_client/src/lib.rs
35
37
  - lib/eppo_client.rb
36
38
  - lib/eppo_client/assignment_logger.rb