Skip to content

Commit 4f37494

Browse files
committed
fix: exp and config partial apply
1 parent 2e3bad1 commit 4f37494

File tree

7 files changed

+128
-22
lines changed

7 files changed

+128
-22
lines changed

clients/haskell/hs-exp-client/src/Client.hs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ foreign import ccall unsafe "expt_get_applicable_variant"
5454
c_get_applicable_variants :: Ptr ExpClient -> CString -> CString -> CString -> CString -> IO CString
5555

5656
foreign import ccall unsafe "expt_get_satisfied_experiments"
57-
c_get_satisfied_experiments :: Ptr ExpClient -> CString -> CString -> IO CString
57+
c_get_satisfied_experiments :: Ptr ExpClient -> CString -> CString -> CString -> IO CString
5858

5959
foreign import ccall unsafe "expt_get_filtered_satisfied_experiments"
60-
c_get_filtered_satisfied_experiments :: Ptr ExpClient -> CString -> CString -> IO CString
60+
c_get_filtered_satisfied_experiments :: Ptr ExpClient -> CString -> CString -> CString -> IO CString
6161

6262
foreign import ccall unsafe "expt_get_running_experiments"
6363
c_get_running_experiments :: Ptr ExpClient -> IO CString
@@ -116,29 +116,31 @@ getApplicableVariants client dimensions query identifier mbPrefix = do
116116
-- Error s -> Left s
117117
-- Success vec -> Right vec
118118

119-
getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> Maybe String -> IO (Either Error Value)
120-
getSatisfiedExperiments client query mbPrefix = do
119+
getSatisfiedExperiments :: ForeignPtr ExpClient -> String -> String -> Maybe String -> IO (Either Error Value)
120+
getSatisfiedExperiments client dimensions query mbPrefix = do
121121
context <- newCString query
122+
dimensions <- newCString dimensions
122123
prefix <- case mbPrefix of
123124
Just prefix -> newCString prefix
124125
Nothing -> return nullPtr
125-
experiments <- withForeignPtr client $ \client -> c_get_satisfied_experiments client context prefix
126+
experiments <- withForeignPtr client $ \client -> c_get_satisfied_experiments dimensions client context prefix
126127
_ <- cleanup [context]
127128
if experiments == nullPtr
128129
then Left <$> getError
129130
else do
130131
fptrExperiments <- newForeignPtr c_free_string experiments
131132
Right . toJSON <$> withForeignPtr fptrExperiments peekCString
132133

133-
getFilteredSatisfiedExperiments :: ForeignPtr ExpClient -> Maybe String -> Maybe String -> IO (Either Error Value)
134-
getFilteredSatisfiedExperiments client mbFilters mbPrefix = do
134+
getFilteredSatisfiedExperiments :: ForeignPtr ExpClient -> String -> Maybe String -> Maybe String -> IO (Either Error Value)
135+
getFilteredSatisfiedExperiments client dimensions mbFilters mbPrefix = do
136+
dimensions <- newCString dimensions
135137
filters <- case mbFilters of
136138
Just filters' -> newCString filters'
137139
Nothing -> return nullPtr
138140
prefix <- case mbPrefix of
139141
Just prefix' -> newCString prefix'
140142
Nothing -> return nullPtr
141-
experiments <- withForeignPtr client $ \client -> c_get_filtered_satisfied_experiments client filters prefix
143+
experiments <- withForeignPtr client $ \client -> c_get_filtered_satisfied_experiments dimensions client filters prefix
142144
_ <- cleanup [filters]
143145
if experiments == nullPtr
144146
then Left <$> getError

clients/haskell/hs-exp-client/src/Main.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ main = do
2424
loopNTimes 0 _ = return ()
2525
loopNTimes n client = do
2626
runningExperiments <- getRunningExperiments client
27-
satisfiedExperiments <- getSatisfiedExperiments client "{\"os\": \"android\", \"client\": \"1mg\"}" Nothing
28-
filteredExperiments <- getFilteredSatisfiedExperiments client (Just "{\"os\": \"android\"}") (Just "hyperpay")
27+
satisfiedExperiments <- getSatisfiedExperiments client "{}" "{\"os\": \"android\", \"client\": \"1mg\"}" Nothing
28+
filteredExperiments <- getFilteredSatisfiedExperiments client "{}" (Just "{\"os\": \"android\"}") (Just "hyperpay")
2929
variants <- getApplicableVariants client "{}" "{\"os\": \"android\", \"client\": \"1mg\"}" "1mg-android" Nothing
3030
print "Running experiments"
3131
print runningExperiments

crates/experimentation_client/src/interface.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use std::{
1111
cell::RefCell,
1212
ffi::{c_int, CString},
1313
};
14-
use superposition_types::DimensionInfo;
14+
use superposition_types::{
15+
logic::{evaluate_local_cohorts, evaluate_local_cohorts_skip_unresolved},
16+
DimensionInfo,
17+
};
1518
use tokio::{runtime::Runtime, task};
1619

1720
thread_local! {
@@ -216,6 +219,7 @@ pub extern "C" fn expt_get_applicable_variant(
216219
#[no_mangle]
217220
pub extern "C" fn expt_get_satisfied_experiments(
218221
client: *mut Arc<Client>,
222+
c_dimensions: *const c_char,
219223
c_context: *const c_char,
220224
filter_prefix: *const c_char,
221225
) -> *mut c_char {
@@ -227,6 +231,16 @@ pub extern "C" fn expt_get_satisfied_experiments(
227231
return std::ptr::null_mut()
228232
);
229233

234+
let dimensions = unwrap_safe!(
235+
cstring_to_rstring(c_dimensions),
236+
return std::ptr::null_mut()
237+
);
238+
239+
let dimensions = unwrap_safe!(
240+
serde_json::from_str::<HashMap<String, DimensionInfo>>(dimensions.as_str()),
241+
return std::ptr::null_mut()
242+
);
243+
230244
let prefix_list = if filter_prefix.is_null() {
231245
None
232246
} else {
@@ -238,6 +252,11 @@ pub extern "C" fn expt_get_satisfied_experiments(
238252
Some(prefix_list)
239253
};
240254

255+
let context = Value::Object(evaluate_local_cohorts(
256+
&dimensions,
257+
&context.as_object().cloned().unwrap_or_default(),
258+
));
259+
241260
let local = task::LocalSet::new();
242261
local.block_on(&Runtime::new().unwrap(), async move {
243262
unsafe {
@@ -258,6 +277,7 @@ pub extern "C" fn expt_get_satisfied_experiments(
258277
#[no_mangle]
259278
pub extern "C" fn expt_get_filtered_satisfied_experiments(
260279
client: *mut Arc<Client>,
280+
c_dimensions: *const c_char,
261281
c_context: *const c_char,
262282
filter_prefix: *const c_char,
263283
) -> *mut c_char {
@@ -268,6 +288,17 @@ pub extern "C" fn expt_get_filtered_satisfied_experiments(
268288
serde_json::from_str::<Value>(context.as_str()),
269289
return std::ptr::null_mut()
270290
);
291+
292+
let dimensions = unwrap_safe!(
293+
cstring_to_rstring(c_dimensions),
294+
return std::ptr::null_mut()
295+
);
296+
297+
let dimensions = unwrap_safe!(
298+
serde_json::from_str::<HashMap<String, DimensionInfo>>(dimensions.as_str()),
299+
return std::ptr::null_mut()
300+
);
301+
271302
let prefix_list = if filter_prefix.is_null() {
272303
None
273304
} else {
@@ -280,6 +311,12 @@ pub extern "C" fn expt_get_filtered_satisfied_experiments(
280311

281312
Some(prefix_list).filter(|list| !list.is_empty())
282313
};
314+
315+
let context = Value::Object(evaluate_local_cohorts_skip_unresolved(
316+
&dimensions,
317+
&context.as_object().cloned().unwrap_or_default(),
318+
));
319+
283320
let local = task::LocalSet::new();
284321
local.block_on(&Runtime::new().unwrap(), async move {
285322
unsafe {

crates/superposition_core/src/experiment.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ pub fn get_satisfied_experiments(
230230
&Value::Object(context.clone()),
231231
) == Ok(Value::Bool(true))
232232
} else {
233-
superposition_types::partial_apply(&exp.context, context)
233+
superposition_types::apply(&exp.context, context)
234234
}
235235
}
236236
})
@@ -247,6 +247,50 @@ pub fn get_satisfied_experiments(
247247
Ok(running_experiments)
248248
}
249249

250+
pub fn get_filtered_satisfied_experiments(
251+
experiments: &Experiments,
252+
context: &Map<String, Value>,
253+
filter_prefixes: Option<Vec<String>>,
254+
) -> Result<Experiments, String> {
255+
let running_experiments = experiments
256+
.iter()
257+
.filter_map(|exp| {
258+
if exp.context.is_empty() {
259+
Some(exp.clone())
260+
} else {
261+
cfg_if::cfg_if! {
262+
if #[cfg(feature = "jsonlogic")] {
263+
match jsonlogic::partial_apply(
264+
&Value::Object(exp.context.clone().into()),
265+
context,
266+
) {
267+
Ok(jsonlogic::PartialApplyOutcome::Resolved(Value::Bool(
268+
true,
269+
)))
270+
| Ok(jsonlogic::PartialApplyOutcome::Ambiguous) => {
271+
Some(exp.clone())
272+
}
273+
_ => None,
274+
}
275+
} else {
276+
superposition_types::partial_apply(&exp.context, &context)
277+
.then(|| exp.clone())
278+
}
279+
}
280+
}
281+
})
282+
.collect();
283+
284+
if let Some(prefix_list) = filter_prefixes {
285+
return Ok(filter_experiments_by_prefix(
286+
running_experiments,
287+
prefix_list,
288+
));
289+
}
290+
291+
Ok(running_experiments)
292+
}
293+
250294
fn filter_experiments_by_prefix(
251295
experiments: Experiments,
252296
filter_prefixes: Vec<String>,

crates/superposition_types/src/config.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use uniffi::deps::anyhow;
1616

1717
use crate::{
1818
database::models::cac::{DependencyGraph, DimensionType},
19-
logic::evaluate_local_cohorts,
19+
logic::evaluate_local_cohorts_skip_unresolved,
2020
overridden::filter_config_keys_by_prefix,
2121
Cac, Contextual, Exp, ExtendedMap,
2222
};
@@ -360,7 +360,8 @@ pub struct Config {
360360

361361
impl Config {
362362
pub fn filter_by_dimensions(&self, dimension_data: &Map<String, Value>) -> Self {
363-
let modified_context = evaluate_local_cohorts(&self.dimensions, dimension_data);
363+
let modified_context =
364+
evaluate_local_cohorts_skip_unresolved(&self.dimensions, dimension_data);
364365

365366
let filtered_context =
366367
Context::filter_by_eval(self.contexts.clone(), &modified_context);

crates/superposition_types/src/logic.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,10 @@ fn evaluate_local_cohorts_dependency(
141141
}
142142
}
143143

144-
/// Evaluates all local cohort dimensions based on the provided query data and dimension definitions
145-
/// First all local cohorts which are computable from the query data are evaluated, then any remaining local cohorts are set to "otherwise"
146-
/// Computation starts from such a point, such that dependencies can be resolved in a depth-first manner
147-
///
148-
/// Values of regular and remote cohort dimensions in query_data are retained as is.
149-
/// Returned value, might have a different value for local cohort dimensions based on its based on dimensions,
150-
/// if the value provided for the local cohort was incorrect in the query data.
151-
pub fn evaluate_local_cohorts(
144+
fn _evaluate_local_cohorts(
152145
dimensions: &HashMap<String, DimensionInfo>,
153146
query_data: &Map<String, Value>,
147+
skip_unresolved: bool,
154148
) -> Map<String, Value> {
155149
if dimensions.is_empty() {
156150
return query_data.clone();
@@ -175,6 +169,10 @@ pub fn evaluate_local_cohorts(
175169
}
176170
}
177171

172+
if skip_unresolved {
173+
return modified_context;
174+
}
175+
178176
// For any local cohort dimension not yet set, set it to "otherwise"
179177
for dimension_key in dimensions.keys() {
180178
if let Some(dimension_info) = dimensions.get(dimension_key) {
@@ -192,6 +190,28 @@ pub fn evaluate_local_cohorts(
192190
modified_context
193191
}
194192

193+
/// Evaluates all local cohort dimensions based on the provided query data and dimension definitions
194+
/// First all local cohorts which are computable from the query data are evaluated, then any remaining local cohorts are set to "otherwise"
195+
/// Computation starts from such a point, such that dependencies can be resolved in a depth-first manner
196+
///
197+
/// Values of regular and remote cohort dimensions in query_data are retained as is.
198+
/// Returned value, might have a different value for local cohort dimensions based on its based on dimensions,
199+
/// if the value provided for the local cohort was incorrect in the query data.
200+
pub fn evaluate_local_cohorts(
201+
dimensions: &HashMap<String, DimensionInfo>,
202+
query_data: &Map<String, Value>,
203+
) -> Map<String, Value> {
204+
_evaluate_local_cohorts(dimensions, query_data, false)
205+
}
206+
207+
/// Same as evaluate_local_cohorts but does not set unresolved local cohorts to "otherwise"
208+
pub fn evaluate_local_cohorts_skip_unresolved(
209+
dimensions: &HashMap<String, DimensionInfo>,
210+
query_data: &Map<String, Value>,
211+
) -> Map<String, Value> {
212+
_evaluate_local_cohorts(dimensions, query_data, true)
213+
}
214+
195215
/// Identifies starting dimensions for evaluation based on query data and dimension definitions
196216
/// For each tree in the dependency graph, picks the node closest to root from query_data for each branch of the tree.
197217
/// If nothing is found and a local cohort is encountered, picks that local cohort as start point from that branch.

headers/libexperimentation_client.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ char *expt_get_applicable_variant(struct Arc_Client *client,
2626
const char *filter_prefix);
2727

2828
char *expt_get_satisfied_experiments(struct Arc_Client *client,
29+
const char *c_dimensions,
2930
const char *c_context,
3031
const char *filter_prefix);
3132

3233
char *expt_get_filtered_satisfied_experiments(struct Arc_Client *client,
34+
const char *c_dimensions,
3335
const char *c_context,
3436
const char *filter_prefix);
3537

0 commit comments

Comments
 (0)