eppo-server-sdk 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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