eppo-server-sdk 3.1.2 → 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: e157a64d8d72757a3bd049d7e40db5589fb7b16d6ff466ea4a1015dbb54fa066
4
- data.tar.gz: 9a4d5cd6edec77d1b8bf8103debe2b096816b6e9957a1c2308747a5182b5a703
3
+ metadata.gz: 9cd858b9082e0b7d776c90f73c086271ac4e7983c7c0d981f060cc44ee2953b1
4
+ data.tar.gz: 9fa7706a5fc234775980808cafed2924322b8d4db797fe878346b78da8b34d8c
5
5
  SHA512:
6
- metadata.gz: 1b98a6ee8aefd1b833f83ddae8141f42798f825a30f0a54d257f390aa24efc9d8e8a2f1c5b9ff1542db9c11700b7d5fac513c0fa1632aca2f5d22b1eb1a4a9b2
7
- data.tar.gz: abc686a5a7e46bf85ba655157de3b7ae9b9367e53603d962741d52edac3f42da1e24301a67145b6719663583a89083dbe0e5df757ebf0a6decc8a570f8f64edf
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.2"
307
+ version = "3.2.0"
308
308
  dependencies = [
309
309
  "env_logger",
310
310
  "eppo_core",
@@ -312,6 +312,7 @@ dependencies = [
312
312
  "magnus",
313
313
  "rb-sys",
314
314
  "serde",
315
+ "serde_json",
315
316
  "serde_magnus",
316
317
  ]
317
318
 
@@ -1295,9 +1296,9 @@ dependencies = [
1295
1296
 
1296
1297
  [[package]]
1297
1298
  name = "serde_json"
1298
- version = "1.0.124"
1299
+ version = "1.0.128"
1299
1300
  source = "registry+https://github.com/rust-lang/crates.io-index"
1300
- checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
1301
+ checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
1301
1302
  dependencies = [
1302
1303
  "itoa",
1303
1304
  "memchr",
@@ -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.2"
4
+ version = "3.2.0"
5
5
  edition = "2021"
6
6
  license = "MIT"
7
7
  publish = false
@@ -14,7 +14,8 @@ crate-type = ["cdylib"]
14
14
  env_logger = { version = "0.11.3", features = ["unstable-kv"] }
15
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
 
@@ -171,6 +190,17 @@ impl Client {
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # TODO: this version and ext/eppo_client/Cargo.toml should be in sync
4
4
  module EppoClient
5
- VERSION = "3.1.2"
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.2
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-10-08 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