Skip to content

Commit

Permalink
feat(core): precomputed bandits (#128)
Browse files Browse the repository at this point in the history
* feat(core): evaluate bandits for precomputed configuration

* feat(core): precomputed bandits obfuscation

* feat(core): update precomputed configuration hashing to new spec

* chore: remove edge assignments service
  • Loading branch information
rasendubi authored Dec 17, 2024
1 parent d49dea1 commit 6a471da
Show file tree
Hide file tree
Showing 40 changed files with 549 additions and 3,008 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,3 @@ jobs:

- run: cargo test --verbose --workspace
- run: cargo doc --verbose

# Add WASM target
- run: rustup target add wasm32-wasi
# Build WASM target separately
- run: cargo build --verbose --target wasm32-wasi
working-directory: fastly-edge-assignments
64 changes: 0 additions & 64 deletions .github/workflows/fastly-edge-assignments.yml

This file was deleted.

49 changes: 0 additions & 49 deletions Makefile

This file was deleted.

1 change: 1 addition & 0 deletions eppo_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ magnus = ["dep:magnus", "dep:serde_magnus"]
vendored = ["reqwest/native-tls-vendored"]

[dependencies]
base64 = "0.22.1"
chrono = { version = "0.4.38", features = ["serde"] }
derive_more = { version = "1.0.0", features = ["from", "into"] }
faststr = { version = "0.2.23", features = ["serde"] }
Expand Down
4 changes: 2 additions & 2 deletions eppo_core/benches/evaluation_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ fn criterion_benchmark(c: &mut Criterion) {
{
let mut group = c.benchmark_group("rollout");
group.throughput(Throughput::Elements(1));
let attributes = Arc::new([("country".to_owned(), "US".into())].into());
let attributes = Arc::new([("country".into(), "US".into())].into());
group.bench_function("get_assignment", |b| {
b.iter(|| {
get_assignment(
Expand Down Expand Up @@ -117,7 +117,7 @@ fn criterion_benchmark(c: &mut Criterion) {
{
let mut group = c.benchmark_group("numeric-one-of");
group.throughput(Throughput::Elements(1));
let attributes = Arc::new([("number".to_owned(), 2.0.into())].into());
let attributes = Arc::new([("number".into(), 2.0.into())].into());
group.bench_function("get_assignment", |b| {
b.iter(|| {
get_assignment(
Expand Down
14 changes: 10 additions & 4 deletions eppo_core/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ pub use context_attributes::ContextAttributes;
/// ```
/// # use eppo_core::Attributes;
/// let attributes = [
/// ("age".to_owned(), 30.0.into()),
/// ("is_premium_member".to_owned(), true.into()),
/// ("username".to_owned(), "john_doe".into()),
/// ("age".into(), 30.0.into()),
/// ("is_premium_member".into(), true.into()),
/// ("username".into(), "john_doe".into()),
/// ].into_iter().collect::<Attributes>();
/// ```
pub type Attributes = HashMap<String, AttributeValue>;
pub type Attributes = HashMap<Str, AttributeValue>;

/// Attribute of a subject or action.
///
Expand Down Expand Up @@ -121,6 +121,12 @@ impl AttributeValue {
)]
pub struct NumericAttribute(f64);

impl NumericAttribute {
pub(crate) fn to_f64(&self) -> f64 {
self.0
}
}

/// Categorical attributes are attributes that have a finite set of values that are not directly
/// comparable (i.e., enumeration).
#[derive(Debug, Clone, PartialEq, PartialOrd, derive_more::From, Serialize, Deserialize)]
Expand Down
48 changes: 30 additions & 18 deletions eppo_core/src/attributes/context_attributes.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};

use serde::{Deserialize, Serialize};

use crate::Str;

use super::{
AttributeValue, AttributeValueImpl, Attributes, CategoricalAttribute, NumericAttribute,
};

/// `ContextAttributes` are subject or action attributes split by their semantics.
// TODO(oleksii): I think we should hide fields of this type and maybe the whole type itself. Now
// with `Attributes` being able to faithfully represent numeric and categorical attributes, there's
// little reason for users of eppo_core to know about `ContextAttributes`, so it makes sense to hide
// it and make it an internal type.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "pyo3", pyo3::pyclass(module = "eppo_client"))]
Expand All @@ -16,11 +22,11 @@ pub struct ContextAttributes {
/// Not all numbers are numeric attributes. If a number is used to represent an enumeration or
/// on/off values, it is a categorical attribute.
#[serde(alias = "numericAttributes")]
pub numeric: HashMap<String, NumericAttribute>,
pub numeric: Arc<HashMap<Str, NumericAttribute>>,
/// Categorical attributes are attributes that have a finite set of values that are not directly
/// comparable (i.e., enumeration).
#[serde(alias = "categoricalAttributes")]
pub categorical: HashMap<String, CategoricalAttribute>,
pub categorical: Arc<HashMap<Str, CategoricalAttribute>>,
}

impl From<Attributes> for ContextAttributes {
Expand All @@ -31,25 +37,31 @@ impl From<Attributes> for ContextAttributes {

impl<K, V> FromIterator<(K, V)> for ContextAttributes
where
K: ToOwned<Owned = String>,
V: ToOwned<Owned = AttributeValue>,
K: Into<Str>,
V: Into<AttributeValue>,
{
fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
iter.into_iter()
.fold(ContextAttributes::default(), |mut acc, (key, value)| {
match value.to_owned() {
let (categorical, numeric) = iter.into_iter().fold(
(HashMap::new(), HashMap::new()),
|(mut categorical, mut numeric), (key, value)| {
match value.into() {
AttributeValue(AttributeValueImpl::Categorical(value)) => {
acc.categorical.insert(key.to_owned(), value);
categorical.insert(key.into(), value);
}
AttributeValue(AttributeValueImpl::Numeric(value)) => {
acc.numeric.insert(key.to_owned(), value);
numeric.insert(key.into(), value);
}
AttributeValue(AttributeValueImpl::Null) => {
// Nulls are missing values and are ignored.
}
}
acc
})
(categorical, numeric)
},
);
ContextAttributes {
numeric: Arc::new(numeric),
categorical: Arc::new(categorical),
}
}
}

Expand All @@ -69,24 +81,24 @@ impl ContextAttributes {

#[cfg(feature = "pyo3")]
mod pyo3_impl {
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};

use pyo3::prelude::*;

use crate::{Attributes, CategoricalAttribute, NumericAttribute};
use crate::{Attributes, CategoricalAttribute, NumericAttribute, Str};

use super::ContextAttributes;

#[pymethods]
impl ContextAttributes {
#[new]
fn new(
numeric_attributes: HashMap<String, NumericAttribute>,
categorical_attributes: HashMap<String, CategoricalAttribute>,
numeric_attributes: HashMap<Str, NumericAttribute>,
categorical_attributes: HashMap<Str, CategoricalAttribute>,
) -> ContextAttributes {
ContextAttributes {
numeric: numeric_attributes,
categorical: categorical_attributes,
numeric: Arc::new(numeric_attributes),
categorical: Arc::new(categorical_attributes),
}
}

Expand Down
6 changes: 3 additions & 3 deletions eppo_core/src/bandits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::timestamp::Timestamp;
use crate::{timestamp::Timestamp, Str};

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct BanditResponse {
pub bandits: HashMap<String, BanditConfiguration>,
pub bandits: HashMap<Str, BanditConfiguration>,
pub updated_at: Timestamp,
}

Expand All @@ -18,7 +18,7 @@ pub struct BanditResponse {
pub struct BanditConfiguration {
pub bandit_key: String,
pub model_name: String,
pub model_version: String,
pub model_version: Str,
pub model_data: BanditModelData,
pub updated_at: Timestamp,
}
Expand Down
7 changes: 4 additions & 3 deletions eppo_core/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc};
use crate::{
bandits::{BanditConfiguration, BanditResponse},
ufc::UniversalFlagConfig,
Str,
};

/// Remote configuration for the eppo client. It's a central piece that defines client behavior.
Expand Down Expand Up @@ -34,13 +35,13 @@ impl Configuration {
}

/// Return a bandit variant for the specified flag key and string flag variation.
pub(crate) fn get_bandit_key<'a>(&'a self, flag_key: &str, variation: &str) -> Option<&'a str> {
pub(crate) fn get_bandit_key<'a>(&'a self, flag_key: &str, variation: &str) -> Option<&'a Str> {
self.flags
.compiled
.flag_to_bandit_associations
.get(flag_key)
.and_then(|x| x.get(variation))
.map(|variation| variation.key.as_str())
.map(|variation| &variation.key)
}

/// Return bandit configuration for the given key.
Expand All @@ -52,7 +53,7 @@ impl Configuration {

/// Get a set of all available flags. Note that this may return both disabled flags and flags
/// with bad configuration.
pub fn flag_keys(&self) -> HashSet<String> {
pub fn flag_keys(&self) -> HashSet<Str> {
self.flags.compiled.flags.keys().cloned().collect()
}
}
4 changes: 2 additions & 2 deletions eppo_core/src/eval/eval_assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,8 @@ mod tests {
/// value is absent in configuration (configuration error).
variation_value: serde_json::Value,

bandit_key: Option<String>,
bandit_action: Option<String>,
bandit_key: Option<Str>,
bandit_action: Option<Str>,

matched_rule: Option<RuleWire>,
matched_allocation: Option<TruncatedAllocationEvaluationDetails>,
Expand Down
Loading

0 comments on commit 6a471da

Please sign in to comment.