nimbus/stateful/
enrollment.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::enrollment::Participation;
6use crate::enrollment::{
7    EnrollmentChangeEvent, EnrollmentChangeEventType, EnrollmentsEvolver, ExperimentEnrollment,
8    map_enrollments,
9};
10use crate::error::{Result, debug, warn};
11use crate::stateful::gecko_prefs::GeckoPrefStore;
12use crate::stateful::gecko_prefs::PrefUnenrollReason;
13use crate::stateful::persistence::{
14    DB_KEY_EXPERIMENT_PARTICIPATION, DB_KEY_ROLLOUT_PARTICIPATION,
15    DEFAULT_EXPERIMENT_PARTICIPATION, DEFAULT_ROLLOUT_PARTICIPATION,
16};
17use crate::stateful::persistence::{Database, Readable, StoreId, Writer};
18use crate::{EnrolledExperiment, EnrollmentStatus, Experiment};
19
20impl EnrollmentsEvolver<'_> {
21    /// Convenient wrapper around `evolve_enrollments` that fetches the current state of experiments,
22    /// enrollments and user participation from the database.
23    pub(crate) fn evolve_enrollments_in_db(
24        &mut self,
25        db: &Database,
26        writer: &mut Writer,
27        next_experiments: &[Experiment],
28        gecko_pref_store: Option<&GeckoPrefStore>,
29    ) -> Result<Vec<EnrollmentChangeEvent>> {
30        // Get separate participation states from the db
31        let is_participating_in_experiments = get_experiment_participation(db, writer)?;
32        let is_participating_in_rollouts = get_rollout_participation(db, writer)?;
33
34        let participation = Participation {
35            in_experiments: is_participating_in_experiments,
36            in_rollouts: is_participating_in_rollouts,
37        };
38
39        let experiments_store = db.get_store(StoreId::Experiments);
40        let enrollments_store = db.get_store(StoreId::Enrollments);
41        let prev_experiments: Vec<Experiment> = experiments_store.collect_all(writer)?;
42        let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;
43        // Calculate the changes.
44        let (next_enrollments, enrollments_change_events) = self.evolve_enrollments(
45            participation,
46            &prev_experiments,
47            next_experiments,
48            &prev_enrollments,
49            gecko_pref_store,
50        )?;
51        let next_enrollments = map_enrollments(&next_enrollments);
52        // Write the changes to the Database.
53        enrollments_store.clear(writer)?;
54        for enrollment in next_enrollments.values() {
55            enrollments_store.put(writer, &enrollment.slug, *enrollment)?;
56        }
57        experiments_store.clear(writer)?;
58        for experiment in next_experiments {
59            // Sanity check.
60            if !next_enrollments.contains_key(&experiment.slug) {
61                error_support::report_error!(
62                    "nimbus-evolve-enrollments",
63                    "evolve_enrollments_in_db: experiment '{}' has no enrollment, dropping to keep database consistent",
64                    &experiment.slug
65                );
66                continue;
67            }
68            experiments_store.put(writer, &experiment.slug, experiment)?;
69        }
70        Ok(enrollments_change_events)
71    }
72}
73
74/// Return information about all enrolled experiments.
75/// Note this does not include rollouts
76pub fn get_enrollments<'r>(
77    db: &Database,
78    reader: &'r impl Readable<'r>,
79) -> Result<Vec<EnrolledExperiment>> {
80    let enrollments: Vec<ExperimentEnrollment> =
81        db.get_store(StoreId::Enrollments).collect_all(reader)?;
82    let mut result = Vec::with_capacity(enrollments.len());
83    for enrollment in enrollments {
84        debug!("Have enrollment: {:?}", enrollment);
85        if let EnrollmentStatus::Enrolled { branch, .. } = &enrollment.status {
86            match db
87                .get_store(StoreId::Experiments)
88                .get::<Experiment, _>(reader, &enrollment.slug)?
89            {
90                Some(experiment) => {
91                    result.push(EnrolledExperiment {
92                        feature_ids: experiment.get_feature_ids(),
93                        slug: experiment.slug,
94                        user_facing_name: experiment.user_facing_name,
95                        user_facing_description: experiment.user_facing_description,
96                        branch_slug: branch.to_string(),
97                    });
98                }
99                _ => {
100                    warn!(
101                        "Have enrollment {:?} but no matching experiment!",
102                        enrollment
103                    );
104                }
105            };
106        }
107    }
108    Ok(result)
109}
110
111pub fn opt_in_with_branch(
112    db: &Database,
113    writer: &mut Writer,
114    experiment_slug: &str,
115    branch: &str,
116) -> Result<Vec<EnrollmentChangeEvent>> {
117    let mut events = vec![];
118    if let Ok(Some(exp)) = db
119        .get_store(StoreId::Experiments)
120        .get::<Experiment, Writer>(writer, experiment_slug)
121    {
122        let enrollment = ExperimentEnrollment::from_explicit_opt_in(&exp, branch, &mut events);
123        db.get_store(StoreId::Enrollments)
124            .put(writer, experiment_slug, &enrollment.unwrap())?;
125    } else {
126        events.push(EnrollmentChangeEvent {
127            experiment_slug: experiment_slug.to_string(),
128            branch_slug: branch.to_string(),
129            reason: Some("does-not-exist".to_string()),
130            change: EnrollmentChangeEventType::EnrollFailed,
131        });
132    }
133
134    Ok(events)
135}
136
137pub fn opt_out(
138    db: &Database,
139    writer: &mut Writer,
140    experiment_slug: &str,
141    gecko_prefs: Option<&GeckoPrefStore>,
142) -> Result<Vec<EnrollmentChangeEvent>> {
143    let mut events = vec![];
144    let enr_store = db.get_store(StoreId::Enrollments);
145    if let Ok(Some(existing_enrollment)) =
146        enr_store.get::<ExperimentEnrollment, Writer>(writer, experiment_slug)
147    {
148        let updated_enrollment = &existing_enrollment.on_explicit_opt_out(&mut events, gecko_prefs);
149        enr_store.put(writer, experiment_slug, updated_enrollment)?;
150    } else {
151        events.push(EnrollmentChangeEvent {
152            experiment_slug: experiment_slug.to_string(),
153            branch_slug: "N/A".to_string(),
154            reason: Some("does-not-exist".to_string()),
155            change: EnrollmentChangeEventType::UnenrollFailed,
156        });
157    }
158
159    Ok(events)
160}
161
162#[cfg(feature = "stateful")]
163pub fn unenroll_for_pref(
164    db: &Database,
165    writer: &mut Writer,
166    experiment_slug: &str,
167    unenroll_reason: PrefUnenrollReason,
168    triggering_pref_name: &str,
169    gecko_pref_store: Option<&GeckoPrefStore>,
170) -> Result<Vec<EnrollmentChangeEvent>> {
171    let mut events = vec![];
172    let enr_store = db.get_store(StoreId::Enrollments);
173    if let Ok(Some(existing_enrollment)) =
174        enr_store.get::<ExperimentEnrollment, Writer>(writer, experiment_slug)
175    {
176        #[cfg(feature = "stateful")]
177        existing_enrollment
178            .maybe_revert_unchanged_gecko_pref_states(triggering_pref_name, gecko_pref_store);
179
180        let updated_enrollment =
181            &existing_enrollment.on_pref_unenroll(unenroll_reason, &mut events);
182        enr_store.put(writer, experiment_slug, updated_enrollment)?;
183    } else {
184        events.push(EnrollmentChangeEvent {
185            experiment_slug: experiment_slug.to_string(),
186            branch_slug: "N/A".to_string(),
187            reason: Some("does-not-exist".to_string()),
188            change: EnrollmentChangeEventType::UnenrollFailed,
189        });
190    }
191
192    Ok(events)
193}
194
195pub fn get_experiment_participation<'r>(
196    db: &Database,
197    reader: &'r impl Readable<'r>,
198) -> Result<bool> {
199    let store = db.get_store(StoreId::Meta);
200    let opted_in = store.get::<bool, _>(reader, DB_KEY_EXPERIMENT_PARTICIPATION)?;
201    if let Some(opted_in) = opted_in {
202        Ok(opted_in)
203    } else {
204        Ok(DEFAULT_EXPERIMENT_PARTICIPATION)
205    }
206}
207
208pub fn get_rollout_participation<'r>(db: &Database, reader: &'r impl Readable<'r>) -> Result<bool> {
209    let store = db.get_store(StoreId::Meta);
210    let opted_in = store.get::<bool, _>(reader, DB_KEY_ROLLOUT_PARTICIPATION)?;
211    if let Some(opted_in) = opted_in {
212        Ok(opted_in)
213    } else {
214        Ok(DEFAULT_ROLLOUT_PARTICIPATION)
215    }
216}
217
218pub fn set_experiment_participation(
219    db: &Database,
220    writer: &mut Writer,
221    opt_in: bool,
222) -> Result<()> {
223    let store = db.get_store(StoreId::Meta);
224    store.put(writer, DB_KEY_EXPERIMENT_PARTICIPATION, &opt_in)
225}
226
227pub fn set_rollout_participation(db: &Database, writer: &mut Writer, opt_in: bool) -> Result<()> {
228    let store = db.get_store(StoreId::Meta);
229    store.put(writer, DB_KEY_ROLLOUT_PARTICIPATION, &opt_in)
230}
231
232/// Reset unique identifiers in response to application-level telemetry reset.
233///
234pub fn reset_telemetry_identifiers(
235    db: &Database,
236    writer: &mut Writer,
237) -> Result<Vec<EnrollmentChangeEvent>> {
238    let mut events = vec![];
239    let store = db.get_store(StoreId::Enrollments);
240    let enrollments: Vec<ExperimentEnrollment> = store.collect_all(writer)?;
241    let updated_enrollments = enrollments
242        .iter()
243        .map(|enrollment| enrollment.reset_telemetry_identifiers(&mut events));
244    store.clear(writer)?;
245    for enrollment in updated_enrollments {
246        store.put(writer, &enrollment.slug, &enrollment)?;
247    }
248    Ok(events)
249}