Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

Commit

Permalink
Ensure Nimbus starts up predictably
Browse files Browse the repository at this point in the history
  • Loading branch information
jhugman authored and mergify[bot] committed Nov 22, 2022
1 parent bfef7c2 commit d610675
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 71 deletions.
56 changes: 46 additions & 10 deletions app/src/main/java/org/mozilla/focus/FocusApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.focus.biometrics.LockObserver
import org.mozilla.focus.experiments.finishNimbusInitialization
import org.mozilla.focus.ext.settings
import org.mozilla.focus.navigation.StoreLink
import org.mozilla.focus.nimbus.FocusNimbus
Expand All @@ -40,6 +41,7 @@ import org.mozilla.focus.utils.AdjustHelper
import org.mozilla.focus.utils.AppConstants
import kotlin.coroutines.CoroutineContext

@Suppress("TooManyFunctions")
open class FocusApplication : LocaleAwareApplication(), Provider, CoroutineScope {
private var job = Job()
override val coroutineContext: CoroutineContext
Expand All @@ -61,7 +63,7 @@ open class FocusApplication : LocaleAwareApplication(), Provider, CoroutineScope
components.crashReporter.install(this)

if (isMainProcess()) {
initializeNativeComponents()
initializeNimbus()

PreferenceManager.setDefaultValues(this, R.xml.settings, false)

Expand All @@ -71,6 +73,8 @@ open class FocusApplication : LocaleAwareApplication(), Provider, CoroutineScope
TelemetryWrapper.init(this)
components.metrics.initialize(this)
FactsProcessor.initialize()
finishSetupMegazord()

ProfilerMarkerFactProcessor.create { components.engine.profiler }.register()

enableStrictMode()
Expand Down Expand Up @@ -102,21 +106,53 @@ open class FocusApplication : LocaleAwareApplication(), Provider, CoroutineScope
// no-op, LeakCanary is disabled by default
}

protected open fun initializeNimbus() {
beginSetupMegazord()

// This lazily constructs the Nimbus object…
val nimbus = components.experiments
// … which we then can populate the feature configuration.
FocusNimbus.initialize { nimbus }
}

/**
* Initiate Megazord sequence! Megazord Battle Mode!
*
* The application-services combined libraries are known as the "megazord". We use the default `full`
* megazord - it contains everything that fenix needs, and (currently) nothing more.
*
* Documentation on what megazords are, and why they're needed:
* - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
* - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
*
* This is the initialization of the megazord without setting up networking, i.e. needing the
* engine for networking. This should do the minimum work necessary as it is done on the main
* thread, early in the app startup sequence.
*/
private fun beginSetupMegazord() {
// Note: Megazord.init() must be called as soon as possible ...
// Megazord.init()

// ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.

// Once application-services has switched to using the new
// error reporting system, RustLog shouldn't input a CrashReporter
// anymore.
// (https://github.com/mozilla/application-services/issues/4981).
RustLog.enable(components.crashReporter)
}

@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
private fun initializeNativeComponents() {
private fun finishSetupMegazord() {
GlobalScope.launch(Dispatchers.IO) {
// We need to use an unwrapped client because native components do not support private
// requests.
@Suppress("Deprecation")
RustHttpConfig.setClient(lazy { components.client.unwrap() })
RustLog.enable(components.crashReporter)
// We want to ensure Nimbus is initialized as early as possible so we can
// experiment on features close to startup.
// But we need viaduct (the RustHttp client) to be ready before we do.
components.experiments.initialize()
// This is the recommended way(Nimbus.api will be deprecated) to establish
// the connection between the Nimbus SDK (and thus the Nimbus server) and the generated code
FocusNimbus.initialize { components.experiments }

// Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch
// experiments recipes from the server.
finishNimbusInitialization(components.experiments)
}
}

Expand Down
115 changes: 54 additions & 61 deletions app/src/main/java/org/mozilla/focus/experiments/NimbusSetup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package org.mozilla.focus.experiments

import android.content.Context
import androidx.core.net.toUri
import kotlinx.coroutines.runBlocking
import mozilla.components.service.nimbus.Nimbus
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.NimbusAppInfo
Expand All @@ -15,13 +16,16 @@ import mozilla.components.support.base.log.logger.Logger
import org.mozilla.experiments.nimbus.NimbusInterface
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.experiments.nimbus.joinOrTimeout
import org.mozilla.focus.BuildConfig
import org.mozilla.focus.GleanMetrics.NimbusExperiments
import org.mozilla.focus.R
import org.mozilla.focus.ext.components
import org.mozilla.focus.ext.settings
import org.mozilla.focus.nimbus.FocusNimbus

private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L

@Suppress("TooGenericExceptionCaught")
fun createNimbus(context: Context, url: String?): NimbusApi {
val errorReporter: ((String, Throwable) -> Unit) = reporter@{ message, e ->
Expand All @@ -34,87 +38,76 @@ fun createNimbus(context: Context, url: String?): NimbusApi {
context.components.crashReporter.submitCaughtException(e)
}

return try {
// 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 = if (!url.isNullOrBlank()) {
if (context.settings.shouldUseNimbusPreview) {
NimbusServerSettings(url = url.toUri(), collection = "nimbus-preview")
} else {
NimbusServerSettings(url = url.toUri())
}
// 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 = if (!url.isNullOrBlank()) {
if (context.settings.shouldUseNimbusPreview) {
NimbusServerSettings(url = url.toUri(), collection = "nimbus-preview")
} else {
null
NimbusServerSettings(url = url.toUri())
}
} else {
null
}

// Global opt out state is stored in Nimbus, and shouldn't be toggled to `true`
// from the app unless the user does so from a UI control.
// However, the user may have opt-ed out of making experiments already, so
// we should respect that setting here.
val enabled = context.settings.isExperimentationEnabled

// The name "focus-android" or "klar-android" here corresponds to the app_name defined
// for the family of apps that encompasses all of the channels for the Focus app.
// This is defined upstream in the telemetry system. For more context on where the
// app_name come from see:
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
// and
// https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
val appInfo = NimbusAppInfo(
appName = getNimbusAppName(),
// Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
// passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
// and would mostly produce the value `Beta` and rarely would produce `beta`.
channel = BuildConfig.BUILD_TYPE,
)
val isTheFirstLaunch = context.settings.getAppLaunchCount() == 0

// The name "focus-android" or "klar-android" here corresponds to the app_name defined
// for the family of apps that encompasses all of the channels for the Focus app.
// This is defined upstream in the telemetry system. For more context on where the
// app_name come from see:
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
// and
// https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
val appInfo = NimbusAppInfo(
appName = getNimbusAppName(),
// Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
// passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
// and would mostly produce the value `Beta` and rarely would produce `beta`.
channel = BuildConfig.BUILD_TYPE,
)
return try {
Nimbus(context, appInfo, serverSettings, errorReporter).apply {
val isTheFirstLaunch = context.settings.getAppLaunchCount() == 0
if (isTheFirstLaunch) {
register(EventsObserver)
}
register(EventsObserver)

// This performs the minimal amount of work required to load branch and enrolment data
// into memory. If `getExperimentBranch` is called from another thread between here
// and the next nimbus disk write (setting `globalUserParticipation` or
// `applyPendingExperiments()`) then this has you covered.
// This call does its work on the db thread.
initialize()

if (!enabled) {
// This opts out of nimbus experiments. It involves writing to disk, so does its
// work on the db thread.
globalUserParticipation = enabled
}

if (url.isNullOrBlank()) {
setExperimentsLocally(R.raw.initial_experiments)
val job = if (isTheFirstLaunch || url.isNullOrBlank()) {
applyLocalExperiments(R.raw.initial_experiments)
} else {
applyPendingExperiments()
}

if (isTheFirstLaunch) {
NimbusExperiments.nimbusInitialFetch.start()
runBlocking {
job.joinOrTimeout(TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS)
}
fetchExperiments()
}
} catch (e: Throwable) {
// Something went wrong. We'd like not to, but stability of the app is more important than
// failing fast here.
errorReporter("Failed to initialize Nimbus", e)
NimbusDisabled(context = context)
}
}

internal fun finishNimbusInitialization(experiments: NimbusApi) =
experiments.run {
// We fetch experiments in all cases,
if (context.settings.getAppLaunchCount() == 0) {
// … however on first run, we immediately apply pending experiments.
// We also want to measure how long this will take, with Glean.
register(
object : NimbusInterface.Observer {
override fun onExperimentsFetched() {
NimbusExperiments.nimbusInitialFetch.stop()
applyPendingExperiments()
if (isTheFirstLaunch) {
NimbusExperiments.nimbusInitialFetch.stop()
}
// Remove lingering observer when we're done fetching experiments on startup.
unregister(this)
}
},
)
NimbusExperiments.nimbusInitialFetch.start()
}
} catch (e: Throwable) {
// Something went wrong. We'd like not to, but stability of the app is more important than
// failing fast here.
errorReporter("Failed to initialize Nimbus", e)
NimbusDisabled(context = context)
fetchExperiments()
}
}

fun getNimbusAppName(): String {
return if (BuildConfig.FLAVOR.contains("focus")) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/test/java/org/mozilla/focus/TestFocusApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class TestFocusApplication : FocusApplication() {
override val components: Components by lazy {
Components(this, engineOverride = FakeEngine(), clientOverride = FakeClient())
}

override fun initializeNimbus() = Unit
}

// Borrowed this from AC unit tests. This is something we should consider moving to support-test, so
Expand Down

0 comments on commit d610675

Please sign in to comment.