Skip to content

Commit 799e6f8

Browse files
authored
Add methods to cover new methods in NimbusClient (mozilla-mobile#9394)
* Add methods to cover new methods in NimbusClient * Address reviewer comments * Version bump to Application Services to get Threadsafe version of Nimbus * Expose initialize method * Fixup test build failure * Address reviewer comments
1 parent a8a2833 commit 799e6f8

File tree

8 files changed

+281
-58
lines changed

8 files changed

+281
-58
lines changed

buildSrc/src/main/java/Dependencies.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ object Versions {
3030
const val disklrucache = "2.0.2"
3131
const val leakcanary = "2.4"
3232

33-
const val mozilla_appservices = "67.2.0"
33+
const val mozilla_appservices = "69.0.0"
3434

3535
const val mozilla_glean = "33.1.2"
3636

components/service/nimbus/src/main/java/mozilla/components/service/nimbus/Nimbus.kt

Lines changed: 167 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import android.content.pm.PackageInfo
99
import android.content.pm.PackageManager
1010
import android.net.Uri
1111
import android.os.Build
12+
import androidx.annotation.AnyThread
13+
import androidx.annotation.RawRes
1214
import androidx.annotation.VisibleForTesting
15+
import androidx.annotation.WorkerThread
1316
import androidx.core.content.pm.PackageInfoCompat
1417
import kotlinx.coroutines.CoroutineScope
1518
import kotlinx.coroutines.asCoroutineDispatcher
@@ -53,14 +56,68 @@ interface NimbusApi : Observable<NimbusApi.Observer> {
5356
*
5457
* @return A String representing the branch-id or "slug"
5558
*/
59+
@AnyThread
5660
fun getExperimentBranch(experimentId: String): String? = null
5761

5862
/**
5963
* Refreshes the experiments from the endpoint. Should be called at least once after
6064
* initialization
6165
*/
66+
@Deprecated("Use fetchExperiments() and applyPendingExperiments()")
6267
fun updateExperiments() = Unit
6368

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+
64121
/**
65122
* Opt out of a specific experiment
66123
*
@@ -82,14 +139,14 @@ interface NimbusApi : Observable<NimbusApi.Observer> {
82139
/**
83140
* Event to indicate that the experiments have been fetched from the endpoint
84141
*/
85-
fun onExperimentsFetched()
142+
fun onExperimentsFetched() = Unit
86143

87144
/**
88145
* Event to indicate that the experiment enrollments have been applied. Multiple calls to
89146
* get the active experiments will return the same value so this has limited usefulness for
90147
* most feature developers
91148
*/
92-
fun onUpdatesApplied(updated: List<EnrolledExperiment>)
149+
fun onUpdatesApplied(updated: List<EnrolledExperiment>) = Unit
93150
}
94151
}
95152

@@ -105,14 +162,20 @@ data class NimbusServerSettings(
105162
/**
106163
* A implementation of the [NimbusApi] interface backed by the Nimbus SDK.
107164
*/
165+
@Suppress("TooManyFunctions")
108166
class Nimbus(
109-
context: Context,
167+
private val context: Context,
110168
server: NimbusServerSettings?,
111169
private val delegate: Observable<NimbusApi.Observer> = ObserverRegistry()
112170
) : NimbusApi, Observable<NimbusApi.Observer> by delegate {
113171

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 =
116179
CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher())
117180

118181
private var nimbus: NimbusClientInterface
@@ -121,7 +184,14 @@ class Nimbus(
121184

122185
override var globalUserParticipation: Boolean
123186
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+
}
125195

126196
init {
127197
// Set the name of the native library so that we use
@@ -147,14 +217,6 @@ class Nimbus(
147217
collectionName = EXPERIMENT_COLLECTION_NAME
148218
)
149219
}
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-
)
158220

159221
nimbus = NimbusClient(
160222
experimentContext,
@@ -173,38 +235,108 @@ class Nimbus(
173235
nimbus.getExperimentBranch(experimentId)
174236

175237
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()
195310
}
311+
setExperimentsLocallyOnThisThread(payload)
196312
}
197313
}
198314

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+
199327
override fun optOut(experimentId: String) {
200-
nimbus.optOut(experimentId)
328+
dbScope.launch {
329+
nimbus.optOut(experimentId)
330+
}
201331
}
202332

203333
// This function shouldn't be exposed to the public API, but is meant for testing purposes to
204334
// force an experiment/branch enrollment.
205335
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
206336
internal fun optInWithBranch(experiment: String, branch: String) {
207-
nimbus.optInWithBranch(experiment, branch)
337+
dbScope.launch {
338+
nimbus.optInWithBranch(experiment, branch)
339+
}
208340
}
209341

210342
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)

components/service/nimbus/src/test/java/mozilla/components/service/nimbus/NimbusTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class NimbusTest {
5151

5252
// Create a list of experiments to test the telemetry enrollment recording
5353
val enrolledExperiments = listOf(EnrolledExperiment(
54+
enrollmentId = "enrollment-id",
5455
slug = "test-experiment",
5556
branchSlug = "test-branch",
5657
userFacingDescription = "A test experiment for testing experiments",
@@ -72,7 +73,8 @@ class NimbusTest {
7273
@Test
7374
fun `NimbusDisabled is empty`() {
7475
val nimbus: NimbusApi = NimbusDisabled()
75-
nimbus.updateExperiments()
76+
nimbus.fetchExperiments()
77+
nimbus.applyPendingExperiments()
7678
assertTrue("getActiveExperiments should be empty", nimbus.getActiveExperiments().isEmpty())
7779
assertEquals(null, nimbus.getExperimentBranch("test-experiment"))
7880
}

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ permalink: /changelog/
3939
* **feature-top-sites**
4040
* ⚠️ **This is a breaking change**: Replaces `includeFrecent` with an optional `frecencyConfig` in `TopSitesConfig` and `TopSitesStorage.getTopSites` to specify the frecency threshold for the returned list of top frecent sites see [#8690](https://github.com/mozilla-mobile/android-components/issues/8690).
4141

42+
* **service-nimbus**
43+
* Upgraded nimbus-sdk to enable `getExperimentBranch()` (and friends) to be callable from the main thread.
44+
* Split up `updateExperiments()` in to two methods: `fetchExperiments()` and `applyPendingExperiments()`.
45+
* Exposed `setExperimentsLocally()` for testing and startup.
46+
4247
# 71.0.0
4348

4449
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v70.0.0...v71.0.0)
@@ -151,6 +156,8 @@ permalink: /changelog/
151156
* **browser-awesomebar**:
152157
* Awesomebar can now be customized for bottom toolbar using the [customizeForBottomToolbar] property
153158

159+
* **service-numbus**
160+
* Added a `NimbusDisabled` class to provide implementers who are not able to use Nimbus yet.
154161
# 69.0.0
155162

156163
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v68.0.0...v69.0.0)

0 commit comments

Comments
 (0)