Skip to content

Move nimbus to new RS api #6662

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.remotesettings.RemoteSettingsConfig
import mozilla.appservices.remotesettings.RemoteSettingsServer
import mozilla.appservices.remotesettings.RemoteSettingsService
import mozilla.telemetry.glean.Glean
import org.json.JSONObject
import org.mozilla.experiments.nimbus.GleanMetrics.NimbusEvents
Expand Down Expand Up @@ -68,11 +67,12 @@ open class Nimbus(
override val prefs: SharedPreferences? = null,
appInfo: NimbusAppInfo,
coenrollingFeatureIds: List<String>,
server: NimbusServerSettings?,
deviceInfo: NimbusDeviceInfo,
private val observer: NimbusInterface.Observer? = null,
delegate: NimbusDelegate,
private val recordedContext: RecordedContext? = null,
collectionName: String? = null,
remoteSettingsService: RemoteSettingsService? = null,
) : NimbusInterface {
// An I/O scope is used for reading or writing from the Nimbus's RKV database.
private val dbScope: CoroutineScope = delegate.dbScope
Expand Down Expand Up @@ -159,21 +159,14 @@ open class Nimbus(
// Build Nimbus AppContext object to pass into initialize
val experimentContext = buildExperimentContext(context, appInfo, deviceInfo)

// Initialize Nimbus
val remoteSettingsConfig = server?.let {
RemoteSettingsConfig(
server = RemoteSettingsServer.Custom(it.url.toString()),
collectionName = it.collection,
)
}

nimbusClient = NimbusClient(
experimentContext,
recordedContext,
coenrollingFeatureIds,
dataDir.path,
remoteSettingsConfig,
metricsHandler,
collectionName,
remoteSettingsService,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ package org.mozilla.experiments.nimbus

import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import androidx.annotation.RawRes
import kotlinx.coroutines.runBlocking
import mozilla.appservices.remotesettings.RemoteSettingsService
import org.mozilla.experiments.nimbus.internal.FeatureManifestInterface
import org.mozilla.experiments.nimbus.internal.RecordedContext

Expand Down Expand Up @@ -103,26 +103,18 @@ abstract class AbstractNimbusBuilder<T : NimbusInterface>(val context: Context)
* network. This is to allow the networking stack to be initialized after this method is called
* and the networking stack to be involved in experiments.
*/
fun build(appInfo: NimbusAppInfo): T {
// Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT.
// but we keep this here to not mix feature flags and how we configure Nimbus.
val serverSettings: NimbusServerSettings? = if (!url.isNullOrBlank()) {
if (usePreviewCollection) {
NimbusServerSettings(url = Uri.parse(url), collection = "nimbus-preview")
} else {
NimbusServerSettings(url = Uri.parse(url))
}
} else {
null
}
fun build(appInfo: NimbusAppInfo, remoteSettingsService: RemoteSettingsService?): T {
val collectionName: String? = if (usePreviewCollection) {
"nimbus-preview"
} else { null }

// Is the app being built locally, and the nimbus-cli
// hasn't been used before this run.
fun NimbusInterface.isLocalBuild() = url.isNullOrBlank() && isFetchEnabled()

@Suppress("TooGenericExceptionCaught")
return try {
newNimbus(appInfo, serverSettings).apply {
newNimbus(appInfo, collectionName, remoteSettingsService).apply {
// Apply any experiment recipes we downloaded last time, or
// if this is the first time, we load the ones bundled in the res/raw
// directory.
Expand Down Expand Up @@ -163,7 +155,8 @@ abstract class AbstractNimbusBuilder<T : NimbusInterface>(val context: Context)
*/
protected abstract fun newNimbus(
appInfo: NimbusAppInfo,
serverSettings: NimbusServerSettings?,
collectionName: String?,
remoteSettingsService: RemoteSettingsService?,
): T

/**
Expand Down Expand Up @@ -218,17 +211,18 @@ private class Observer(
}

class DefaultNimbusBuilder(context: Context) : AbstractNimbusBuilder<NimbusInterface>(context) {
override fun newNimbus(appInfo: NimbusAppInfo, serverSettings: NimbusServerSettings?) =
override fun newNimbus(appInfo: NimbusAppInfo, collectionName: String?, remoteSettingsService: RemoteSettingsService?) =
Nimbus(
context,
appInfo = appInfo,
prefs = sharedPreferences,
coenrollingFeatureIds = getCoenrollingFeatureIds(),
server = serverSettings,
deviceInfo = createDeviceInfo(),
delegate = createDelegate(),
observer = createObserver(),
recordedContext = recordedContext,
collectionName = collectionName,
remoteSettingsService = remoteSettingsService,
)

override fun newNimbusDisabled() = NullNimbus(context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ class EventStoreTest {
channel = "test",
)

private val nimbus = TestNimbusBuilder(context).build(appInfo)
val collectionName = "test-collection-name"

private val nimbus = TestNimbusBuilder(context).build(appInfo, null)

val events: NimbusEventStore
get() = nimbus.events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ class MessagingHelperTests {
context = context,
appInfo = developmentAppInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)
nimbus.initializeOnThisThread()

Expand All @@ -74,9 +75,10 @@ class MessagingHelperTests {
context = context,
appInfo = developmentAppInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)
nimbus.initializeOnThisThread()

Expand Down Expand Up @@ -110,9 +112,10 @@ class MessagingHelperTests {
context = context,
appInfo = developmentAppInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)
nimbus.initializeOnThisThread()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package org.mozilla.experiments.nimbus
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.Job
import mozilla.appservices.remotesettings.RemoteSettingsService
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
Expand All @@ -31,64 +32,64 @@ class NimbusBuilderTest {
val n1 = NimbusBuilder(context).apply {
url = "https://example.com"
usePreviewCollection = true
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertTrue(n1.usePreviewCollection)

val n2 = NimbusBuilder(context).apply {
url = "https://example.com"
usePreviewCollection = false
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertFalse(n2.usePreviewCollection)

// Without a URL, there is no preview collection
val n3 = NimbusBuilder(context).apply {
usePreviewCollection = true
}.build(appInfo) as DummyNimbus
usePreviewCollection = false
}.build(appInfo, null) as DummyNimbus
assertFalse(n3.usePreviewCollection)
}

@Test
fun `test use bundled experiments on first run only`() {
val bundledExperiments = Random.nextInt()

val n0 = NimbusBuilder(context).build(appInfo) as DummyNimbus
val n0 = NimbusBuilder(context).build(appInfo, null) as DummyNimbus
assertNull(n0.initialExperiments)

// Normal operation, first run.
val normalFirstRun = NimbusBuilder(context).apply {
url = "https://example.com"
isFirstRun = true
initialExperiments = bundledExperiments
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertEquals(bundledExperiments, normalFirstRun.initialExperiments)

// Normal operation, subsequent runs
val normalNonFirstRun = NimbusBuilder(context).apply {
url = "https://example.com"
isFirstRun = false
initialExperiments = bundledExperiments
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertNull(normalNonFirstRun.initialExperiments)

// Normal operation, without bundling
val fetchOnFirstRun = NimbusBuilder(context).apply {
url = "https://example.com"
isFirstRun = false
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertNull(fetchOnFirstRun.initialExperiments)

// Local development operation, first run
val devBuild1 = NimbusBuilder(context).apply {
isFirstRun = true
initialExperiments = bundledExperiments
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertEquals(bundledExperiments, devBuild1.initialExperiments)

// Local development operation, subsequent
val devBuild2 = NimbusBuilder(context).apply {
isFirstRun = false
initialExperiments = bundledExperiments
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertEquals(bundledExperiments, devBuild2.initialExperiments)
}

Expand All @@ -100,7 +101,7 @@ class NimbusBuilderTest {
url = null
isFirstRun = true
initialExperiments = bundledExperiments
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertEquals(bundledExperiments, devBuild1.initialExperiments)

// Local development operation, subsequent runs, but with isFetchEnabled = false
Expand All @@ -110,7 +111,7 @@ class NimbusBuilderTest {
url = null
isFirstRun = false
initialExperiments = bundledExperiments
}.build(appInfo) as DummyNimbus
}.build(appInfo, null) as DummyNimbus
assertNull(devBuild2.initialExperiments)
}
}
Expand All @@ -121,25 +122,26 @@ class NimbusBuilder(
) : AbstractNimbusBuilder<NimbusInterface>(context) {
override fun newNimbus(
appInfo: NimbusAppInfo,
serverSettings: NimbusServerSettings?,
collectionName: String?,
remoteSettingsService: RemoteSettingsService?,
): NimbusInterface =
DummyNimbus(context, appInfo = appInfo, serverSettings = serverSettings, isFetchEnabled = isFetchEnabled)
DummyNimbus(context, appInfo = appInfo, collectionName = collectionName, isFetchEnabled = isFetchEnabled)

override fun newNimbusDisabled(): NimbusInterface =
NullNimbus(context)
}

class DummyNimbus(
override val context: Context,
val serverSettings: NimbusServerSettings?,
val appInfo: NimbusAppInfo,
val collectionName: String? = null,
private val isFetchEnabled: Boolean,
) : NimbusInterface {

var initialExperiments: Int? = null

val usePreviewCollection: Boolean
get() = serverSettings?.collection == "nimbus-preview"
get() = collectionName == "nimbus-preview"

override fun applyLocalExperiments(file: Int): Job {
initialExperiments = file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ class NimbusTests {
context = context,
appInfo = appInfo,
coenrollingFeatureIds = coenrollingFeatureIds,
server = null,
deviceInfo = deviceInfo,
observer = null,
delegate = nimbusDelegate,
recordedContext = recordedContext,
collectionName = null,
remoteSettingsService = null,
).also(block)

@get:Rule
Expand Down Expand Up @@ -544,9 +545,10 @@ class NimbusTests {
context = context,
appInfo = developmentAppInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)

nimbus.setUpTestExperiments("$packageName.nightly", targetedAppInfo)
Expand All @@ -565,9 +567,10 @@ class NimbusTests {
context = context,
appInfo = developmentAppInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)

nimbus.setUpTestExperiments(packageName, targetedAppInfo)
Expand Down Expand Up @@ -656,10 +659,11 @@ class NimbusTests {
context = context,
appInfo = appInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
observer = observer,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)

suspend fun getString() = testExperimentsJsonString(appInfo, packageName)
Expand Down Expand Up @@ -687,10 +691,11 @@ class NimbusTests {
context = context,
appInfo = appInfo,
coenrollingFeatureIds = listOf(),
server = null,
deviceInfo = deviceInfo,
observer = observer,
delegate = nimbusDelegate,
collectionName = null,
remoteSettingsService = null,
)

suspend fun getString(): String = throw CancellationException()
Expand Down
Loading