Skip to content

Commit 760d58d

Browse files
feat: fastly compute service to perform assignments (FF-3552) (#69)
* feat: fastly compute service to perform assignments (FF-3552) * convert from reqwest blocking to non-blocking to conform to wasm target * remove vscode settings * makefile for local development * split into multiple handlers * its a thing of beauty * coming along nicely * good * local test kv-store! * feat: add `get_subject_assignments` method to eppo_core to compute all assignments for a given subject against the cached flags (FF-3571) * simplify unnecessary ownership transfer * e2e evaluation * remove space end of filename * Revert "feat: add `get_subject_assignments` method to eppo_core to compute all assignments for a given subject against the cached flags (FF-3571)" This reverts commit a159065. * tidy up error logging * edge function dep on 4.1.1 * generate token hash * fastly local * conform to desired API * add CORS support * version from cargo * use Datetime * String * no copy * use VariationType * move structs to core * extra logging * refactor FlagAssignment to model * use Str * build responses in core * tidy * add format * str * format * one more format
1 parent 7b6e145 commit 760d58d

File tree

20 files changed

+457
-25
lines changed

20 files changed

+457
-25
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ jobs:
2323
submodules: true
2424
- run: npm ci
2525
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
26-
- run: cargo build --verbose --all-targets
27-
- run: cargo test --verbose
26+
# Add WASM target
27+
- run: rustup target add wasm32-wasi
28+
# Build non-WASM targets
29+
- run: cargo build --verbose --all-targets --workspace --exclude fastly-edge-assignments
30+
# Build WASM target separately
31+
- run: cargo build --verbose -p fastly-edge-assignments --target wasm32-wasi
32+
# Run tests (excluding WASM package)
33+
- run: cargo test --verbose --workspace --exclude fastly-edge-assignments
2834
- run: cargo doc --verbose

Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
[workspace]
22
resolver = "2"
33
members = [
4-
"eppo_core",
5-
"rust-sdk",
6-
"python-sdk",
7-
"ruby-sdk/ext/eppo_client",
4+
"eppo_core",
5+
"rust-sdk",
6+
"python-sdk",
7+
"ruby-sdk/ext/eppo_client",
8+
"fastly-edge-assignments",
89
]
910

1011
[patch.crates-io]

Makefile

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
1-
# Make settings - @see https://tech.davis-hansson.com/p/make/
21
SHELL := bash
32
.ONESHELL:
43
.SHELLFLAGS := -eu -o pipefail -c
54
.DELETE_ON_ERROR:
65
MAKEFLAGS += --warn-undefined-variables
76
MAKEFLAGS += --no-builtin-rules
87

9-
# Log levels
10-
DEBUG := $(shell printf "\e[2D\e[35m")
11-
INFO := $(shell printf "\e[2D\e[36m🔵 ")
12-
OK := $(shell printf "\e[2D\e[32m🟢 ")
13-
WARN := $(shell printf "\e[2D\e[33m🟡 ")
14-
ERROR := $(shell printf "\e[2D\e[31m🔴 ")
15-
END := $(shell printf "\e[0m")
8+
WASM_TARGET=wasm32-wasi
9+
FASTLY_PACKAGE=fastly-edge-assignments
10+
BUILD_DIR=target/$(WASM_TARGET)/release
11+
WASM_FILE=$(BUILD_DIR)/$(FASTLY_PACKAGE).wasm
1612

17-
.PHONY: default
18-
default: help
19-
20-
## help - Print help message.
13+
# Help target for easy documentation
2114
.PHONY: help
22-
help: Makefile
23-
@echo "usage: make <target>"
24-
@sed -n 's/^##//p' $<
15+
help:
16+
@echo "Available targets:"
17+
@echo " all - Default target (build workspace)"
18+
@echo " workspace-build - Build the entire workspace excluding the Fastly package"
19+
@echo " workspace-test - Test the entire workspace excluding the Fastly package"
20+
@echo " fastly-edge-assignments-build - Build only the Fastly package for WASM"
21+
@echo " fastly-edge-assignments-test - Test only the Fastly package"
22+
@echo " clean - Clean all build artifacts"
2523

2624
.PHONY: test
2725
test: ${testDataDir}
2826
npm test
27+
28+
# Build the entire workspace excluding the `fastly-edge-assignments` package
29+
.PHONY: workspace-build
30+
workspace-build:
31+
cargo build --workspace --exclude $(FASTLY_PACKAGE)
32+
33+
# Run tests for the entire workspace excluding the `fastly-edge-assignments` package
34+
.PHONY: workspace-test
35+
workspace-test:
36+
cargo test --workspace --exclude $(FASTLY_PACKAGE)
37+
38+
# Build only the `fastly-edge-assignments` package for WASM
39+
.PHONY: fastly-edge-assignments-build
40+
fastly-edge-assignments-build:
41+
rustup target add $(WASM_TARGET)
42+
cargo build --release --target $(WASM_TARGET) --package $(FASTLY_PACKAGE)
43+
44+
# Test only the `fastly-edge-assignments` package
45+
.PHONY: fastly-edge-assignments-test
46+
fastly-edge-assignments-test:
47+
cargo test --target $(WASM_TARGET) --package $(FASTLY_PACKAGE)

eppo_core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub mod configuration_store;
5555
pub mod eval;
5656
pub mod events;
5757
pub mod poller_thread;
58+
pub mod precomputed_assignments;
5859
#[cfg(feature = "pyo3")]
5960
pub mod pyo3;
6061
pub mod sharder;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
use crate::ufc::{Assignment, AssignmentFormat, Environment, VariationType};
2+
use crate::{Attributes, Configuration, Str};
3+
use serde::{Deserialize, Serialize};
4+
use std::collections::HashMap;
5+
use std::sync::Arc;
6+
7+
// Request
8+
#[derive(Debug, Deserialize)]
9+
pub struct PrecomputedAssignmentsServiceRequestBody {
10+
pub subject_key: Str,
11+
pub subject_attributes: Arc<Attributes>,
12+
// TODO: Add bandit actions
13+
// #[serde(rename = "banditActions")]
14+
// #[serde(skip_serializing_if = "Option::is_none")]
15+
// bandit_actions: Option<HashMap<String, serde_json::Value>>,
16+
}
17+
18+
// Response
19+
#[derive(Debug, Serialize)]
20+
#[serde(rename_all = "camelCase")]
21+
pub struct FlagAssignment {
22+
pub allocation_key: Str,
23+
pub variation_key: Str,
24+
pub variation_type: VariationType,
25+
pub variation_value: serde_json::Value,
26+
/// Additional user-defined logging fields for capturing extra information related to the
27+
/// assignment.
28+
#[serde(flatten)]
29+
pub extra_logging: HashMap<String, String>,
30+
pub do_log: bool,
31+
}
32+
33+
impl FlagAssignment {
34+
pub fn try_from_assignment(assignment: Assignment) -> Option<Self> {
35+
// WARNING! There is a problem here. The event is only populated for splits
36+
// that have `do_log` set to true in the wire format. This means that
37+
// all the ones present here are logged, but any splits that are not
38+
// logged are not present here.
39+
//
40+
// This is a problem for us because we want to be able to return
41+
// precomputed assignments for any split, logged or not, since we
42+
// want to be able to return them for all flags.
43+
//
44+
// We need to fix this.
45+
assignment.event.as_ref().map(|event| Self {
46+
allocation_key: event.base.allocation.clone(),
47+
variation_key: event.base.variation.clone(),
48+
variation_type: assignment.value.variation_type(),
49+
variation_value: assignment.value.variation_value(),
50+
extra_logging: event
51+
.base
52+
.extra_logging
53+
.iter()
54+
.map(|(k, v)| (k.clone(), v.clone()))
55+
.collect(),
56+
do_log: true,
57+
})
58+
}
59+
}
60+
61+
#[derive(Debug, Serialize)]
62+
#[serde(rename_all = "camelCase")]
63+
pub struct PrecomputedAssignmentsServiceResponse {
64+
created_at: chrono::DateTime<chrono::Utc>,
65+
format: AssignmentFormat,
66+
environment: Environment,
67+
flags: HashMap<String, FlagAssignment>,
68+
}
69+
70+
impl PrecomputedAssignmentsServiceResponse {
71+
pub fn from_configuration(
72+
configuration: Arc<Configuration>,
73+
flags: HashMap<String, FlagAssignment>,
74+
) -> Self {
75+
Self {
76+
created_at: chrono::Utc::now(),
77+
format: AssignmentFormat::Precomputed,
78+
environment: {
79+
Environment {
80+
name: configuration.flags.compiled.environment.name.clone(),
81+
}
82+
},
83+
flags,
84+
}
85+
}
86+
}

eppo_core/src/ufc/assignment.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
44

55
use crate::{events::AssignmentEvent, Str};
66

7+
use crate::ufc::VariationType;
8+
79
/// Result of assignment evaluation.
810
#[derive(Debug, Serialize, Clone)]
911
#[serde(rename_all = "camelCase")]
@@ -231,6 +233,52 @@ impl AssignmentValue {
231233
_ => None,
232234
}
233235
}
236+
237+
/// Returns the type of the variation as a string.
238+
///
239+
/// # Returns
240+
/// - A string representing the type of the variation ("STRING", "INTEGER", "NUMERIC", "BOOLEAN", or "JSON").
241+
///
242+
/// # Examples
243+
/// ```
244+
/// # use eppo_core::ufc::AssignmentValue;
245+
/// # use eppo_core::ufc::VariationType;
246+
/// let value = AssignmentValue::String("example".into());
247+
/// assert_eq!(value.variation_type(), VariationType::String);
248+
/// ```
249+
pub fn variation_type(&self) -> VariationType {
250+
match self {
251+
AssignmentValue::String(_) => VariationType::String,
252+
AssignmentValue::Integer(_) => VariationType::Integer,
253+
AssignmentValue::Numeric(_) => VariationType::Numeric,
254+
AssignmentValue::Boolean(_) => VariationType::Boolean,
255+
AssignmentValue::Json(_) => VariationType::Json,
256+
}
257+
}
258+
259+
/// Returns the raw value of the variation.
260+
///
261+
/// # Returns
262+
/// - A JSON Value containing the variation value.
263+
///
264+
/// # Examples
265+
/// ```
266+
/// # use eppo_core::ufc::AssignmentValue;
267+
/// # use serde_json::json;
268+
/// let value = AssignmentValue::String("example".into());
269+
/// assert_eq!(value.variation_value(), json!("example"));
270+
/// ```
271+
pub fn variation_value(&self) -> serde_json::Value {
272+
match self {
273+
AssignmentValue::String(s) => serde_json::Value::String(s.to_string()),
274+
AssignmentValue::Integer(i) => serde_json::Value::Number((*i).into()),
275+
AssignmentValue::Numeric(n) => serde_json::Value::Number(
276+
serde_json::Number::from_f64(*n).unwrap_or_else(|| serde_json::Number::from(0)),
277+
),
278+
AssignmentValue::Boolean(b) => serde_json::Value::Bool(*b),
279+
AssignmentValue::Json(j) => j.as_ref().clone(),
280+
}
281+
}
234282
}
235283

236284
#[cfg(feature = "pyo3")]

eppo_core/src/ufc/models.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub type Timestamp = chrono::DateTime<chrono::Utc>;
1818
pub(crate) struct UniversalFlagConfigWire {
1919
/// When configuration was last updated.
2020
pub created_at: Timestamp,
21+
pub format: AssignmentFormat,
2122
/// Environment this configuration belongs to.
2223
pub environment: Environment,
2324
/// Flags configuration.
@@ -31,6 +32,14 @@ pub(crate) struct UniversalFlagConfigWire {
3132
pub bandits: HashMap<String, Vec<BanditVariationWire>>,
3233
}
3334

35+
#[derive(Debug, Serialize, Deserialize, Clone)]
36+
#[serde(rename_all = "UPPERCASE")]
37+
pub(crate) enum AssignmentFormat {
38+
Client,
39+
Precomputed,
40+
Server,
41+
}
42+
3443
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3544
#[serde(rename_all = "camelCase")]
3645
pub(crate) struct Environment {
@@ -540,6 +549,7 @@ mod tests {
540549
&r#"
541550
{
542551
"createdAt": "2024-07-18T00:00:00Z",
552+
"format": "SERVER",
543553
"environment": {"name": "test"},
544554
"flags": {
545555
"success": {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasi"

fastly-edge-assignments/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pkg/
2+
bin/

fastly-edge-assignments/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "fastly-edge-assignments"
3+
version = "0.1.0"
4+
edition = "2021"
5+
# Remove this line if you want to be able to publish this crate as open source on crates.io.
6+
# Otherwise, `publish = false` prevents an accidental `cargo publish` from revealing private source.
7+
publish = false
8+
9+
[dependencies]
10+
base64-url = "2.0.0"
11+
chrono = "0.4.19"
12+
eppo_core = { version = "=4.1.1", path = "../eppo_core" }
13+
fastly = "0.11.0"
14+
serde_json = "1.0.132"
15+
serde = "1.0.192"
16+
sha2 = "0.10.0"

0 commit comments

Comments
 (0)