Skip to content

Experiment init #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# will have compiled files and executables
/target/

/ffi/target

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
Expand Down
34 changes: 34 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "nimbus_experiments"
version = "0.1.0"
authors = ["Tarik Eshaq <[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
url = "2.1"
serde = { version = "1", features = ["rc"] }
serde_derive = "1"
serde_json = "1"
anyhow = "1.0"
rand = "0.7"
log = "0.4"
viaduct = { git = "https://github.com/mozilla/application-services" }
ffi-support = "0.4"
thiserror = "1"
rkv = "0.10"
lazy_static = "1.4"
uuid = { version = "0.8", features = ["serde", "v4"]}
prost = "0.6"

[build-dependencies]
prost-build = { version = "0.6" }

[lib]
name = "nimbus_experiments"
crate-type = ["lib"]

[dev-dependencies]
viaduct-reqwest = { git = "https://github.com/mozilla/application-services" }

7 changes: 7 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

fn main() {
prost_build::compile_protos(&["src/experiments_msg_types.proto"], &["src/"]).unwrap();
}
24 changes: 24 additions & 0 deletions examples/experiment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use anyhow::Result;
use nimbus_experiments::{AppContext, Experiments};
fn main() -> Result<()> {
viaduct_reqwest::use_reqwest_backend();
let exp = Experiments::new(AppContext::default(), "./mydb");
let enrolled_exp = exp.get_enrolled_experiments();
exp.get_experiments().iter().for_each(|e| {
print!(
"Experiment: \"{}\", Buckets: {} to {}, Branches: ",
e.id, e.buckets.count, e.buckets.start
);
e.branches.iter().for_each(|b| print!(" \"{}\", ", b.name));
println!()
});
println!("You are in bucket: {}", exp.get_bucket());
enrolled_exp.iter().for_each(|ee| {
println!(
"Enrolled in experiment \"{}\" in branch \"{}\"",
ee.get_id(),
ee.get_branch()
)
});
Ok(())
}
9 changes: 9 additions & 0 deletions src/buckets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! This might be where the bucketing logic can go
//! It would be different from current experimentation tools
//! There is a namespacing concept to allow users to be in multiple
//! unrelated experiments at the same time.
//! Not implemented yet
29 changes: 29 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! Not implemented yet!!!
//! This is purely boilerplate to communicate over the ffi
//! We should define real variants for our error and use proper
//! error propegation (we can use the `thiserror` crate for that)
use ffi_support::{ErrorCode, ExternError};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid")]
Invalid,
}

pub type Result<T, E = Error> = std::result::Result<T, E>;

impl From<Error> for ExternError {
fn from(_: Error) -> ExternError {
let code = ErrorCode::new(1);
ExternError::new_error(code, "UNEXPECTED")
}
}

impl Into<Error> for anyhow::Error {
fn into(self) -> Error {
Error::Invalid
}
}
9 changes: 9 additions & 0 deletions src/experiments.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# This is a test file for defining WebIDL for uniffi
# For the time being, it is not used for anything!
# However, if we use uniffi in the future, we could define
# The api here. (Unless uniffi changes to a non WebIDL way (looking at you proc-macros))
namespace experiments {};
interface Experiments {
constructor();
void get_experiment_branch();
};
24 changes: 24 additions & 0 deletions src/experiments_msg_types.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
syntax = "proto2";

// This kinda beats the purpose of using protobufs since we have one file here/
// And a duplicate in the glean PR, but bear with me :)
// Eventually once we figure out the details of where each part lives, we'll merge the proto files
// into one

package mozilla.telemetry.glean.protobuf;

option java_package = "mozilla.telemtery.glean";
option java_outer_classname = "MsgTypes";
option swift_prefix = "MsgTypes_";
option optimize_for = LITE_RUNTIME;

message AppContext {
optional string app_id = 1;
optional string app_version = 2;
optional string locale_language = 3;
optional string locale_country = 4;
optional string device_manufacturer = 5;
optional string device_model = 6;
optional string region = 7;
optional string debug_tag = 8;
}
69 changes: 69 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::os::raw::c_char;

use super::{error::Result, msg_types, AppContext, Experiments};
use ffi_support::{define_handle_map_deleter, ConcurrentHandleMap, ExternError, FfiStr};

lazy_static::lazy_static! {
static ref EXPERIMENTS: ConcurrentHandleMap<Experiments> = ConcurrentHandleMap::new();
}

#[no_mangle]
pub extern "C" fn experiments_new(
app_ctx: *const u8,
app_ctx_len: i32,
db_path: FfiStr<'_>,
error: &mut ExternError,
) -> u64 {
EXPERIMENTS.insert_with_result(error, || -> Result<Experiments> {
let app_ctx = unsafe {
from_protobuf_ptr::<AppContext, msg_types::AppContext>(app_ctx, app_ctx_len).unwrap()
}; // Todo: make the whole function unsafe and implement proper error handling in error.rs
log::info!("=================== Initializing experiments ========================");
Ok(Experiments::new(app_ctx, db_path.as_str()))
})
}

#[no_mangle]
pub extern "C" fn experiments_get_branch(
handle: u64,
branch: FfiStr<'_>,
error: &mut ExternError,
) -> *mut c_char {
EXPERIMENTS.call_with_result(error, handle, |experiment| -> Result<String> {
log::info!("==================== Getting branch ========================");
let branch_name = experiment.get_experiment_branch(branch.as_str())?;
Ok(branch_name)
})
}

define_handle_map_deleter!(EXPERIMENTS, experiements_destroy);

/// # Safety
/// data is a raw pointer to the protobuf data
/// get_buffer will return an error if the length is invalid,
/// or if the pointer is a null pointer
pub unsafe fn from_protobuf_ptr<T, F: prost::Message + Default + Into<T>>(
data: *const u8,
len: i32,
) -> anyhow::Result<T> {
let buffer = get_buffer(data, len)?;
let item: Result<F, _> = prost::Message::decode(buffer);
item.map(|inner| inner.into()).map_err(|e| e.into())
}

unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> anyhow::Result<&'a [u8]> {
match len {
len if len < 0 => anyhow::bail!("Invalid length"),
0 => Ok(&[]),
_ => {
if data.is_null() {
anyhow::bail!("Null pointer")
}
Ok(std::slice::from_raw_parts(data, len as usize))
}
}
}
81 changes: 81 additions & 0 deletions src/http_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! This is a simple Http client that uses viaduct to retrieve experiment data from the server
//! Currently configured to use Kinto and the old schema, although that would change once we start
//! Working on the real Nimbus schema.

use super::Experiment;
use anyhow::Result;
use serde_derive::*;
use url::Url;
use viaduct::{status_codes, Request, Response};

// Making this a trait so that we can mock those later.
pub(crate) trait SettingsClient {
fn get_experiements_metadata(&self) -> Result<String>;
fn get_experiments(&self) -> Result<Vec<Experiment>>;
}

#[derive(Deserialize)]
struct RecordsResponse {
data: Vec<Experiment>,
}

pub struct Client {
base_url: Url,
collection_name: String,
bucket_name: String,
}

impl Client {
pub fn new(base_url: Url, collection_name: String, bucket_name: String) -> Self {
Self {
base_url,
collection_name,
bucket_name,
}
}

fn make_request(&self, request: Request) -> Result<Response> {
let resp = request.send()?;
if resp.is_success() || resp.status == status_codes::NOT_MODIFIED {
Ok(resp)
} else {
anyhow::bail!("Error in request: {}", resp.text())
}
}
}

impl SettingsClient for Client {
fn get_experiements_metadata(&self) -> Result<String> {
let path = format!(
"buckets/{}/collections/{}",
&self.bucket_name, &self.collection_name
);
let url = self.base_url.join(&path)?;
let req = Request::get(url).header(
"User-Agent",
"Experiments Rust Component <[email protected]>",
)?;
let resp = self.make_request(req)?;
let res = serde_json::to_string(&resp.body)?;
Ok(res)
}

fn get_experiments(&self) -> Result<Vec<Experiment>> {
let path = format!(
"buckets/{}/collections/{}/records",
&self.bucket_name, &self.collection_name
);
let url = self.base_url.join(&path)?;
let req = Request::get(url).header(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:77.0) Gecko/20100101 Firefox/77.0",
)?;
// TODO: Add authentication based on server requirements
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tarikeshaq I am not sure if they have authentication !

let resp = self.make_request(req)?.json::<RecordsResponse>()?;
Ok(resp.data)
}
}
Loading