@@ -9,7 +9,10 @@ import android.content.pm.PackageInfo
9
9
import android.content.pm.PackageManager
10
10
import android.net.Uri
11
11
import android.os.Build
12
+ import androidx.annotation.AnyThread
13
+ import androidx.annotation.RawRes
12
14
import androidx.annotation.VisibleForTesting
15
+ import androidx.annotation.WorkerThread
13
16
import androidx.core.content.pm.PackageInfoCompat
14
17
import kotlinx.coroutines.CoroutineScope
15
18
import kotlinx.coroutines.asCoroutineDispatcher
@@ -53,14 +56,68 @@ interface NimbusApi : Observable<NimbusApi.Observer> {
53
56
*
54
57
* @return A String representing the branch-id or "slug"
55
58
*/
59
+ @AnyThread
56
60
fun getExperimentBranch (experimentId : String ): String? = null
57
61
58
62
/* *
59
63
* Refreshes the experiments from the endpoint. Should be called at least once after
60
64
* initialization
61
65
*/
66
+ @Deprecated(" Use fetchExperiments() and applyPendingExperiments()" )
62
67
fun updateExperiments () = Unit
63
68
69
+ /* *
70
+ * Open the database and populate the SDK so as make it usable by feature developers.
71
+ *
72
+ * This performs the minimum amount of I/O needed to ensure `getExperimentBranch()` is usable.
73
+ *
74
+ * It will not take in to consideration previously fetched experiments: `applyPendingExperiments()`
75
+ * is more suitable for that use case.
76
+ *
77
+ * This method uses the single threaded worker scope, so callers can safely sequence calls to
78
+ * `initialize` and `setExperimentsLocally`, `applyPendingExperiments`.
79
+ */
80
+ fun initialize () = Unit
81
+
82
+ /* *
83
+ * Fetches experiments from the RemoteSettings server.
84
+ *
85
+ * This is performed on a background thread.
86
+ *
87
+ * Notifies `onExperimentsFetched` to observers once the experiments has been fetched from the
88
+ * server.
89
+ *
90
+ * Notes:
91
+ * * this does not affect experiment enrolment, until `applyPendingExperiments` is called.
92
+ * * this will overwrite pending experiments previously fetched with this method, or set with
93
+ * `setExperimentsLocally`.
94
+ */
95
+ fun fetchExperiments () = Unit
96
+
97
+ /* *
98
+ * Calculates the experiment enrolment from experiments from the last `fetchExperiments` or
99
+ * `setExperimentsLocally`, and then informs Glean of new experiment enrolment.
100
+ *
101
+ * Notifies `onUpdatesApplied` once enrolments are recalculated.
102
+ */
103
+ fun applyPendingExperiments () = Unit
104
+
105
+ /* *
106
+ * Set the experiments as the passed string, just as `fetchExperiments` gets the string from
107
+ * the server. Like `fetchExperiments`, this requires `applyPendingExperiments` to be called
108
+ * before enrolments are affected.
109
+ *
110
+ * The string should be in the same JSON format that is delivered from the server.
111
+ *
112
+ * This is performed on a background thread.
113
+ */
114
+ fun setExperimentsLocally (payload : String ) = Unit
115
+
116
+ /* *
117
+ * A utility method to load a file from resources and pass it to `setExperimentsLocally(String)`.
118
+ */
119
+ fun setExperimentsLocally (@RawRes file : Int ) = Unit
120
+
64
121
/* *
65
122
* Opt out of a specific experiment
66
123
*
@@ -82,14 +139,14 @@ interface NimbusApi : Observable<NimbusApi.Observer> {
82
139
/* *
83
140
* Event to indicate that the experiments have been fetched from the endpoint
84
141
*/
85
- fun onExperimentsFetched ()
142
+ fun onExperimentsFetched () = Unit
86
143
87
144
/* *
88
145
* Event to indicate that the experiment enrollments have been applied. Multiple calls to
89
146
* get the active experiments will return the same value so this has limited usefulness for
90
147
* most feature developers
91
148
*/
92
- fun onUpdatesApplied (updated : List <EnrolledExperiment >)
149
+ fun onUpdatesApplied (updated : List <EnrolledExperiment >) = Unit
93
150
}
94
151
}
95
152
@@ -105,14 +162,20 @@ data class NimbusServerSettings(
105
162
/* *
106
163
* A implementation of the [NimbusApi] interface backed by the Nimbus SDK.
107
164
*/
165
+ @Suppress(" TooManyFunctions" )
108
166
class Nimbus (
109
- context : Context ,
167
+ private val context : Context ,
110
168
server : NimbusServerSettings ? ,
111
169
private val delegate : Observable <NimbusApi .Observer > = ObserverRegistry ()
112
170
) : NimbusApi, Observable<NimbusApi.Observer> by delegate {
113
171
114
- // Using a single threaded executor here to enforce synchronization where needed.
115
- private val scope: CoroutineScope =
172
+ // Using two single threaded executors here to enforce synchronization where needed:
173
+ // An I/O scope is used for reading or writing from the Nimbus's RKV database.
174
+ private val dbScope: CoroutineScope =
175
+ CoroutineScope (Executors .newSingleThreadExecutor().asCoroutineDispatcher())
176
+
177
+ // An I/O scope is used for getting experiments from the network.
178
+ private val fetchScope: CoroutineScope =
116
179
CoroutineScope (Executors .newSingleThreadExecutor().asCoroutineDispatcher())
117
180
118
181
private var nimbus: NimbusClientInterface
@@ -121,7 +184,14 @@ class Nimbus(
121
184
122
185
override var globalUserParticipation: Boolean
123
186
get() = nimbus.getGlobalUserParticipation()
124
- set(active) = nimbus.setGlobalUserParticipation(active)
187
+ set(active) {
188
+ dbScope.launch {
189
+ val enrolmentChanges = nimbus.setGlobalUserParticipation(active)
190
+ if (enrolmentChanges.isNotEmpty()) {
191
+ postEnrolmentCalculation()
192
+ }
193
+ }
194
+ }
125
195
126
196
init {
127
197
// Set the name of the native library so that we use
@@ -147,14 +217,6 @@ class Nimbus(
147
217
collectionName = EXPERIMENT_COLLECTION_NAME
148
218
)
149
219
}
150
- // We'll temporarily use this until the Nimbus SDK supports null servers
151
- // https://github.com/mozilla/nimbus-sdk/pull/66 is landed, so this is the next release of
152
- // Nimbus SDK.
153
- ? : RemoteSettingsConfig (
154
- serverUrl = " https://settings.stage.mozaws.net" ,
155
- bucketName = EXPERIMENT_BUCKET_NAME ,
156
- collectionName = EXPERIMENT_COLLECTION_NAME
157
- )
158
220
159
221
nimbus = NimbusClient (
160
222
experimentContext,
@@ -173,38 +235,108 @@ class Nimbus(
173
235
nimbus.getExperimentBranch(experimentId)
174
236
175
237
override fun updateExperiments () {
176
- scope.launch {
177
- try {
178
- nimbus.updateExperiments()
179
-
180
- // Get the experiments to record in telemetry
181
- nimbus.getActiveExperiments().let {
182
- if (it.any()) {
183
- recordExperimentTelemetry(it)
184
- // The current plan is to have the nimbus-sdk updateExperiments() function
185
- // return a diff of the experiments that have been received, at which point we
186
- // can emit the appropriate telemetry events and notify observers of just the
187
- // diff
188
- notifyObservers { onUpdatesApplied(it) }
189
- }
190
- }
191
- } catch (e: ErrorException .RequestError ) {
192
- logger.info(" Error fetching experiments from endpoint: $e " )
193
- } catch (e: ErrorException .InvalidExperimentResponse ) {
194
- logger.info(" Invalid experiment response: $e " )
238
+ fetchScope.launch {
239
+ fetchExperimentsOnThisThread()
240
+ applyPendingExperimentsOnThisThread()
241
+ }
242
+ }
243
+
244
+ override fun initialize () {
245
+ dbScope.launch {
246
+ initializeOnThisThread()
247
+ }
248
+ }
249
+
250
+ @WorkerThread
251
+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
252
+ private fun initializeOnThisThread () {
253
+ nimbus.initialize()
254
+ }
255
+
256
+ override fun fetchExperiments () {
257
+ fetchScope.launch {
258
+ fetchExperimentsOnThisThread()
259
+ }
260
+ }
261
+
262
+ @WorkerThread
263
+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
264
+ private fun fetchExperimentsOnThisThread () {
265
+ try {
266
+ nimbus.fetchExperiments()
267
+ notifyObservers { onExperimentsFetched() }
268
+ } catch (e: ErrorException .RequestError ) {
269
+ logger.info(" Error fetching experiments from endpoint: $e " )
270
+ }
271
+ }
272
+
273
+ override fun applyPendingExperiments () {
274
+ dbScope.launch {
275
+ applyPendingExperimentsOnThisThread()
276
+ }
277
+ }
278
+
279
+ @WorkerThread
280
+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
281
+ private fun applyPendingExperimentsOnThisThread () {
282
+ try {
283
+ nimbus.applyPendingExperiments()
284
+ // Get the experiments to record in telemetry
285
+ postEnrolmentCalculation()
286
+ } catch (e: ErrorException .InvalidExperimentFormat ) {
287
+ logger.info(" Invalid experiment format: $e " )
288
+ }
289
+ }
290
+
291
+ private fun postEnrolmentCalculation () {
292
+ nimbus.getActiveExperiments().let {
293
+ if (it.any()) {
294
+ recordExperimentTelemetry(it)
295
+ // The current plan is to have the nimbus-sdk updateExperiments() function
296
+ // return a diff of the experiments that have been received, at which point we
297
+ // can emit the appropriate telemetry events and notify observers of just the
298
+ // diff. See also:
299
+ // https://github.com/mozilla/experimenter/issues/3588 and
300
+ // https://jira.mozilla.com/browse/SDK-6
301
+ notifyObservers { onUpdatesApplied(it) }
302
+ }
303
+ }
304
+ }
305
+
306
+ override fun setExperimentsLocally (@RawRes file : Int ) {
307
+ dbScope.launch {
308
+ val payload = context.resources.openRawResource(file).use {
309
+ it.bufferedReader().readText()
195
310
}
311
+ setExperimentsLocallyOnThisThread(payload)
196
312
}
197
313
}
198
314
315
+ override fun setExperimentsLocally (payload : String ) {
316
+ dbScope.launch {
317
+ setExperimentsLocallyOnThisThread(payload)
318
+ }
319
+ }
320
+
321
+ @WorkerThread
322
+ @VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
323
+ private fun setExperimentsLocallyOnThisThread (payload : String ) {
324
+ nimbus.setExperimentsLocally(payload)
325
+ }
326
+
199
327
override fun optOut (experimentId : String ) {
200
- nimbus.optOut(experimentId)
328
+ dbScope.launch {
329
+ nimbus.optOut(experimentId)
330
+ }
201
331
}
202
332
203
333
// This function shouldn't be exposed to the public API, but is meant for testing purposes to
204
334
// force an experiment/branch enrollment.
205
335
@VisibleForTesting(otherwise = VisibleForTesting .NONE )
206
336
internal fun optInWithBranch (experiment : String , branch : String ) {
207
- nimbus.optInWithBranch(experiment, branch)
337
+ dbScope.launch {
338
+ nimbus.optInWithBranch(experiment, branch)
339
+ }
208
340
}
209
341
210
342
@VisibleForTesting(otherwise = VisibleForTesting .PRIVATE )
0 commit comments