1use crate::enrollment::Participation;
5use crate::stateful::persistence::{
6 DB_KEY_EXPERIMENT_PARTICIPATION, DB_KEY_ROLLOUT_PARTICIPATION,
7 DEFAULT_EXPERIMENT_PARTICIPATION, DEFAULT_ROLLOUT_PARTICIPATION,
8};
9use crate::{
10 enrollment::{
11 map_enrollments, EnrollmentChangeEvent, EnrollmentChangeEventType, EnrollmentsEvolver,
12 ExperimentEnrollment,
13 },
14 error::{debug, warn, Result},
15 stateful::{
16 gecko_prefs::PrefUnenrollReason,
17 persistence::{Database, Readable, StoreId, Writer},
18 },
19 EnrolledExperiment, EnrollmentStatus, Experiment,
20};
21
22impl EnrollmentsEvolver<'_> {
23 pub(crate) fn evolve_enrollments_in_db(
26 &mut self,
27 db: &Database,
28 writer: &mut Writer,
29 next_experiments: &[Experiment],
30 ) -> Result<Vec<EnrollmentChangeEvent>> {
31 let is_participating_in_experiments = get_experiment_participation(db, writer)?;
33 let is_participating_in_rollouts = get_rollout_participation(db, writer)?;
34
35 let participation = Participation {
36 in_experiments: is_participating_in_experiments,
37 in_rollouts: is_participating_in_rollouts,
38 };
39
40 let experiments_store = db.get_store(StoreId::Experiments);
41 let enrollments_store = db.get_store(StoreId::Enrollments);
42 let prev_experiments: Vec<Experiment> = experiments_store.collect_all(writer)?;
43 let prev_enrollments: Vec<ExperimentEnrollment> = enrollments_store.collect_all(writer)?;
44 let (next_enrollments, enrollments_change_events) = self.evolve_enrollments(
46 participation,
47 &prev_experiments,
48 next_experiments,
49 &prev_enrollments,
50 )?;
51 let next_enrollments = map_enrollments(&next_enrollments);
52 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 if !next_enrollments.contains_key(&experiment.slug) {
61 error_support::report_error!("nimbus-evolve-enrollments", "evolve_enrollments_in_db: experiment '{}' has no enrollment, dropping to keep database consistent", &experiment.slug);
62 continue;
63 }
64 experiments_store.put(writer, &experiment.slug, experiment)?;
65 }
66 Ok(enrollments_change_events)
67 }
68}
69
70pub fn get_enrollments<'r>(
73 db: &Database,
74 reader: &'r impl Readable<'r>,
75) -> Result<Vec<EnrolledExperiment>> {
76 let enrollments: Vec<ExperimentEnrollment> =
77 db.get_store(StoreId::Enrollments).collect_all(reader)?;
78 let mut result = Vec::with_capacity(enrollments.len());
79 for enrollment in enrollments {
80 debug!("Have enrollment: {:?}", enrollment);
81 if let EnrollmentStatus::Enrolled { branch, .. } = &enrollment.status {
82 match db
83 .get_store(StoreId::Experiments)
84 .get::<Experiment, _>(reader, &enrollment.slug)?
85 {
86 Some(experiment) => {
87 result.push(EnrolledExperiment {
88 feature_ids: experiment.get_feature_ids(),
89 slug: experiment.slug,
90 user_facing_name: experiment.user_facing_name,
91 user_facing_description: experiment.user_facing_description,
92 branch_slug: branch.to_string(),
93 });
94 }
95 _ => {
96 warn!(
97 "Have enrollment {:?} but no matching experiment!",
98 enrollment
99 );
100 }
101 };
102 }
103 }
104 Ok(result)
105}
106
107pub fn opt_in_with_branch(
108 db: &Database,
109 writer: &mut Writer,
110 experiment_slug: &str,
111 branch: &str,
112) -> Result<Vec<EnrollmentChangeEvent>> {
113 let mut events = vec![];
114 if let Ok(Some(exp)) = db
115 .get_store(StoreId::Experiments)
116 .get::<Experiment, Writer>(writer, experiment_slug)
117 {
118 let enrollment = ExperimentEnrollment::from_explicit_opt_in(&exp, branch, &mut events);
119 db.get_store(StoreId::Enrollments)
120 .put(writer, experiment_slug, &enrollment.unwrap())?;
121 } else {
122 events.push(EnrollmentChangeEvent {
123 experiment_slug: experiment_slug.to_string(),
124 branch_slug: branch.to_string(),
125 reason: Some("does-not-exist".to_string()),
126 change: EnrollmentChangeEventType::EnrollFailed,
127 });
128 }
129
130 Ok(events)
131}
132
133pub fn opt_out(
134 db: &Database,
135 writer: &mut Writer,
136 experiment_slug: &str,
137) -> Result<Vec<EnrollmentChangeEvent>> {
138 let mut events = vec![];
139 let enr_store = db.get_store(StoreId::Enrollments);
140 if let Ok(Some(existing_enrollment)) =
141 enr_store.get::<ExperimentEnrollment, Writer>(writer, experiment_slug)
142 {
143 let updated_enrollment = &existing_enrollment.on_explicit_opt_out(&mut events);
144 enr_store.put(writer, experiment_slug, updated_enrollment)?;
145 } else {
146 events.push(EnrollmentChangeEvent {
147 experiment_slug: experiment_slug.to_string(),
148 branch_slug: "N/A".to_string(),
149 reason: Some("does-not-exist".to_string()),
150 change: EnrollmentChangeEventType::UnenrollFailed,
151 });
152 }
153
154 Ok(events)
155}
156
157pub fn unenroll_for_pref(
158 db: &Database,
159 writer: &mut Writer,
160 experiment_slug: &str,
161 unenroll_reason: PrefUnenrollReason,
162) -> Result<Vec<EnrollmentChangeEvent>> {
163 let mut events = vec![];
164 let enr_store = db.get_store(StoreId::Enrollments);
165 if let Ok(Some(existing_enrollment)) =
166 enr_store.get::<ExperimentEnrollment, Writer>(writer, experiment_slug)
167 {
168 let updated_enrollment =
169 &existing_enrollment.on_pref_unenroll(unenroll_reason, &mut events);
170 enr_store.put(writer, experiment_slug, updated_enrollment)?;
171 } else {
172 events.push(EnrollmentChangeEvent {
173 experiment_slug: experiment_slug.to_string(),
174 branch_slug: "N/A".to_string(),
175 reason: Some("does-not-exist".to_string()),
176 change: EnrollmentChangeEventType::UnenrollFailed,
177 });
178 }
179
180 Ok(events)
181}
182
183pub fn get_experiment_participation<'r>(
184 db: &Database,
185 reader: &'r impl Readable<'r>,
186) -> Result<bool> {
187 let store = db.get_store(StoreId::Meta);
188 let opted_in = store.get::<bool, _>(reader, DB_KEY_EXPERIMENT_PARTICIPATION)?;
189 if let Some(opted_in) = opted_in {
190 Ok(opted_in)
191 } else {
192 Ok(DEFAULT_EXPERIMENT_PARTICIPATION)
193 }
194}
195
196pub fn get_rollout_participation<'r>(db: &Database, reader: &'r impl Readable<'r>) -> Result<bool> {
197 let store = db.get_store(StoreId::Meta);
198 let opted_in = store.get::<bool, _>(reader, DB_KEY_ROLLOUT_PARTICIPATION)?;
199 if let Some(opted_in) = opted_in {
200 Ok(opted_in)
201 } else {
202 Ok(DEFAULT_ROLLOUT_PARTICIPATION)
203 }
204}
205
206pub fn set_experiment_participation(
207 db: &Database,
208 writer: &mut Writer,
209 opt_in: bool,
210) -> Result<()> {
211 let store = db.get_store(StoreId::Meta);
212 store.put(writer, DB_KEY_EXPERIMENT_PARTICIPATION, &opt_in)
213}
214
215pub fn set_rollout_participation(db: &Database, writer: &mut Writer, opt_in: bool) -> Result<()> {
216 let store = db.get_store(StoreId::Meta);
217 store.put(writer, DB_KEY_ROLLOUT_PARTICIPATION, &opt_in)
218}
219
220pub fn reset_telemetry_identifiers(
223 db: &Database,
224 writer: &mut Writer,
225) -> Result<Vec<EnrollmentChangeEvent>> {
226 let mut events = vec![];
227 let store = db.get_store(StoreId::Enrollments);
228 let enrollments: Vec<ExperimentEnrollment> = store.collect_all(writer)?;
229 let updated_enrollments = enrollments
230 .iter()
231 .map(|enrollment| enrollment.reset_telemetry_identifiers(&mut events));
232 store.clear(writer)?;
233 for enrollment in updated_enrollments {
234 store.put(writer, &enrollment.slug, &enrollment)?;
235 }
236 Ok(events)
237}