Skip to content

Commit

Permalink
Wire up persistence of enrolled experiments. (mozilla#39)
Browse files Browse the repository at this point in the history
* experiments and enrollment status are all stored in the DB. Only
  fetches experiments in the constructor if there are none in the DB,
  which doesn't smell like the right thing long term - otherwise though,
  you must explicitly call `update_experiments()`. We should work out
  what behaviour makes sense for apps here.

* Fully implements the `opt_out` etc methods. Any experiment can be opted in
  to and opted out of, with this stored in the DB. Calling
  `update_experiments()` will not change the opted-on/out status - there's
  a new 'reset_enrollment` function that does though. Note that opting
  out is fully supported, but the expectation is that this must only be
  used in a dev or test context.

* When `update_experiments()` is called, enrollments for experiments
  which no longer exist are removed, and new experiments are evaluated
  for enrollment. We don't emit any events yet, but we could. We do log
  so you can see things happening. We don't support experiments being paused,
  nor start and end dates for experiments, etc.

* It allows you to opt in/out/etc to non-existing experiments - but they will
  be removed when `update_experiments()` is called (ie, it's treated as
  though the experiment was removed. We could fix this, but meh.

* No longer hits the server as the SDK interface is constructed.

* Our command-line utility `experiment.rs` supports all of this - use --help.
  • Loading branch information
mhammond authored Oct 19, 2020
1 parent ba43e00 commit 73f3435
Show file tree
Hide file tree
Showing 10 changed files with 824 additions and 180 deletions.
66 changes: 60 additions & 6 deletions nimbus/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nimbus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ viaduct-reqwest = { git = "https://github.com/mozilla/application-services", re
mockito = "0.27"
env_logger = "0.7"
clap = "2.33.3"
tempdir = "0.3"
5 changes: 2 additions & 3 deletions nimbus/examples/config/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"context": {"app_id": "101010", "locale": "en-US"},
"collection_name": "messaging-experiments",
"bucket_name": "main",
"uuid": "d7bb442c-81ef-4d0d-bd14-fedb3b67ccd4"
"collection_name": "nimbus-mobile-experiments",
"bucket_name": "main"
}
166 changes: 129 additions & 37 deletions nimbus/examples/experiment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

use clap::{App, Arg, SubCommand};
use env_logger::Env;
use nimbus::{AppContext, AvailableRandomizationUnits, NimbusClient, RemoteSettingsConfig};
use nimbus::{
error::Result, AppContext, AvailableRandomizationUnits, NimbusClient, RemoteSettingsConfig,
};
use std::io::prelude::*;

const DEFAULT_BASE_URL: &str = "https://settings.stage.mozaws.net"; // TODO: Replace this with prod
const DEFAULT_BUCKET_NAME: &str = "main";
const DEFAULT_COLLECTION_NAME: &str = "messaging-experiments";
fn main() {
fn main() -> Result<()> {
// We set the logging level to be `warn` here, meaning that only
// logs of `warn` or higher will be actually be shown, any other
// error will be omitted
Expand All @@ -34,11 +36,63 @@ fn main() {
.takes_value(true),
)
.subcommand(
SubCommand::with_name("show_experiments")
SubCommand::with_name("show-experiments")
.about("Show all experiments, followed by the enrolled experiments"),
)
.subcommand(
SubCommand::with_name("gen_uuid")
SubCommand::with_name("update-experiments")
.about("Updates experiments and enrollments from the server"),
)
.subcommand(
SubCommand::with_name("opt-in")
.about("Opts in to an experiment and branch")
.arg(
Arg::with_name("experiment")
.long("experiment")
.value_name("EXPERIMENT_ID")
.help("The ID of the experiment to opt in to")
.required(true)
.takes_value(true)
)
.arg(
Arg::with_name("branch")
.long("branch")
.value_name("BRANCH_ID")
.help("The ID of the branch to opt in to")
.required(true)
.takes_value(true)
)
)
.subcommand(
SubCommand::with_name("opt-out")
.about("Opts out of an experiment")
.arg(
Arg::with_name("experiment")
.long("experiment")
.value_name("EXPERIMENT_ID")
.help("The ID of the experiment to opt out of")
.required(true)
.takes_value(true)
)
)
.subcommand(
SubCommand::with_name("opt-out-all")
.about("Opts out of all experiments")
)
.subcommand(
SubCommand::with_name("reset-enrollment")
.about("Resets enrollment information for the specified experiment")
.arg(
Arg::with_name("experiment")
.long("experiment")
.value_name("EXPERIMENT_ID")
.help("The ID of the experiment to reset")
.required(true)
.takes_value(true)
)
)
.subcommand(
SubCommand::with_name("gen-uuid")
.about("Generate a uuid that can get enrolled in experiments")
.arg(
Arg::with_name("number")
Expand Down Expand Up @@ -89,52 +143,90 @@ fn main() {
"",
Some(config),
available_randomization_units,
)
.unwrap();
)?;

// We match against the subcommands
match matches.subcommand() {
// show_enrolled shows only the enrolled experiments and the chosen branches
("show_experiments", _) => {
("show-experiments", _) => {
println!("======================================");
println!("Printing all experiments (regardless of enrollment)");
nimbus_client
.get_all_experiments()
.get_all_experiments()?
.iter()
.for_each(|e| println!("Experiment: {}", e.slug));
println!("======================================");
println!("Printing only enrolled experiments");
nimbus_client.get_active_experiments().iter().for_each(|e| {
println!(
"Enrolled in experiment: {}, in branch: {}",
e.slug, e.branch_slug
)
});
nimbus_client
.get_active_experiments()?
.iter()
.for_each(|e| {
println!(
"Enrolled in experiment: {}, in branch: {}",
e.slug, e.branch_slug
)
});
}
// gen_uuid will generate a UUID that gets enrolled in a given number of
// experiments
("gen_uuid", Some(matches)) => {
let num = matches
.value_of("number")
.unwrap_or("0")
.parse::<usize>()
.expect("the number parameter should be a number");
let all_experiments = nimbus_client.get_all_experiments();
let mut num_of_experiments_enrolled = 0;
let mut uuid = uuid::Uuid::new_v4();
let available_randomization_units = AvailableRandomizationUnits {
client_id: Some("bobo".to_string()),
};
while num_of_experiments_enrolled != num {
uuid = uuid::Uuid::new_v4();
num_of_experiments_enrolled =
nimbus::filter_enrolled(&uuid, &available_randomization_units, &all_experiments)
.unwrap()
.len()
}
("update-experiments", _) => {
println!("======================================");
println!("Updating experiments");
nimbus_client.update_experiments()?;
}
("opt-in", Some(matches)) => {
println!("======================================");
println!("Generated UUID is: {}", uuid);
let experiment = matches.value_of("experiment").unwrap();
let branch = matches.value_of("branch").unwrap();
println!(
"Opting in to experiment '{}', branch '{}'",
experiment, branch
);
nimbus_client.opt_in_with_branch(experiment.to_string(), branch.to_string())?;
}
("opt-out", Some(matches)) => {
println!("======================================");
let experiment = matches.value_of("experiment").unwrap();
println!("Opting out of experiment '{}'", experiment);
nimbus_client.opt_out(experiment.to_string())?;
}
("reset-enrollment", Some(matches)) => {
println!("======================================");
let experiment = matches.value_of("experiment").unwrap();
println!("Resetting enrollment of experiment '{}'", experiment);
nimbus_client.reset_enrollment(experiment.to_string())?;
}
// gen_uuid will generate a UUID that gets enrolled in a given number of
// experiments
// Should we get back to this? Or is the ability to explicitly opt-in
// good enough?
// Another idea: command to "brute-force" an experiment - ie, run a loop
// where each iteration generates a new uuid and attempts enrollment,
// keeping track of how often we were enrolled and in which branch, then
// print those stats.
/*
("gen_uuid", Some(matches)) => {
let num = matches
.value_of("number")
.unwrap_or("0")
.parse::<usize>()
.expect("the number parameter should be a number");
let all_experiments = nimbus_client.get_all_experiments()?;
let mut num_of_experiments_enrolled = 0;
let mut uuid = uuid::Uuid::new_v4();
let available_randomization_units = AvailableRandomizationUnits {
client_id: Some("bobo".to_string()),
};
while num_of_experiments_enrolled != num {
uuid = uuid::Uuid::new_v4();
num_of_experiments_enrolled =
nimbus::filter_enrolled(&uuid, &available_randomization_units, &all_experiments)
.unwrap()
.len()
}
println!("======================================");
println!("Generated UUID is: {}", uuid);
}
*/
(&_, _) => println!("Invalid subcommand"),
}
};
Ok(())
}
Loading

0 comments on commit 73f3435

Please sign in to comment.