eppo-server-sdk 3.1.2 → 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 +4 -4
- data/Cargo.lock +4 -3
- data/ext/eppo_client/Cargo.toml +3 -2
- data/ext/eppo_client/src/client.rs +50 -20
- data/ext/eppo_client/src/configuration.rs +113 -0
- data/ext/eppo_client/src/gc_lock.rs +25 -0
- data/ext/eppo_client/src/lib.rs +21 -1
- data/lib/eppo_client/client.rb +8 -0
- data/lib/eppo_client/config.rb +4 -2
- data/lib/eppo_client/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9cd858b9082e0b7d776c90f73c086271ac4e7983c7c0d981f060cc44ee2953b1
|
4
|
+
data.tar.gz: 9fa7706a5fc234775980808cafed2924322b8d4db797fe878346b78da8b34d8c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
1299
|
+
version = "1.0.128"
|
1299
1300
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
1300
|
-
checksum = "
|
1301
|
+
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
1301
1302
|
dependencies = [
|
1302
1303
|
"itoa",
|
1303
1304
|
"memchr",
|
data/ext/eppo_client/Cargo.toml
CHANGED
@@ -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.
|
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.
|
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,
|
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
|
-
|
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
|
45
|
-
|
46
|
-
|
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(
|
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
|
+
}
|
data/ext/eppo_client/src/lib.rs
CHANGED
@@ -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
|
}
|
data/lib/eppo_client/client.rb
CHANGED
@@ -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
|
data/lib/eppo_client/config.rb
CHANGED
@@ -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
|
data/lib/eppo_client/version.rb
CHANGED
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.
|
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-
|
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
|