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 +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
|