Skip to content
academia.sh

Lesson 13 / 14

Remote Configuration

In remote configuration, the app fetches values from the server and activates them in a separate step. The lesson measures the last value winning for a key fetched twice before activation, the first value being superseded, and a value that is fetched but not activated being lost.

Published Updated

In this lesson

  • Separates the fetch and activation steps
  • Measures that the last value wins for the same key fetched twice before activation
  • Shows that a value that is fetched but not activated is lost
  • Explains that an in-app default does not count as delivery
Contents

In the previous lesson, events went to the server. In this lesson the direction reverses: the server sends the app a value, for example whether a feature is on or off, or an upper limit. That way behavior changes without releasing a new version. The value enters the app in two steps: first it is fetched, then it is activated. The question is the course’s question: how many copies of this sent event reached the receiver? Here the event is a configuration value, and the receiver is the running app.

// Arrival.kt — classifies how many copies of a sent event reached the receiver
class Signal(val key: String, val seq: Int)

class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
    val copies: Int = received.count { it.seq == sent.seq }
    val verdict: String = when {
        copies > 1 -> "duplicated"
        copies == 1 -> "once"
        received.any { it.key == sent.key && it.seq > sent.seq } -> "superseded"
        else -> "lost"
    }

    fun line() {
        println("$label: key=${sent.key} | copies=$copies -> $verdict")
    }
}

fun countArrivals(arrivals: List<Arrival>) {
    for (name in listOf("once", "duplicated", "superseded", "lost")) {
        println("$name: ${arrivals.count { it.verdict == name }}")
    }
}

Fetch and Activation

Fetching is taking the value from the server and holding it on the device; the app does not read it yet. Activation is the pending value becoming the value the app reads. The reason the two steps are separate is so that the value of a feature running on screen does not change in the middle. Activation is done at a suitable moment, for example at the next launch.

On the platform, the plainest counterpart of this separation is holding the value somewhere and then writing it to the store the app reads.

// Platform code: SharedPreferences
val pending = context.getSharedPreferences("pending", Context.MODE_PRIVATE)
val active = context.getSharedPreferences("active", Context.MODE_PRIVATE)
pending.edit().putString("setting.theme", "dark").apply()
active.edit().putString("setting.theme", pending.getString("setting.theme", null)).apply()

This piece is platform code, and we make no claim here about output, duration, or counts. The model builds the platform’s rule; the model is not the platform itself. The fetch frequency and the moment of activation are determined by the platform or the framework in use; in the model, both are a list.

In the model, the fetched values are an ordered list. Activation picks the last value in the list for each key. An older value of the same key does not affect a separate key.

// Config.kt — activation picks the last fetched value for each key
fun activateLatest(fetched: List<Signal>): List<Signal> =
    fetched.groupBy { it.key }.map { (_, values) -> values.maxBy { it.seq } }

fun activateUpTo(fetched: List<Signal>, count: Int): List<Signal> =
    activateLatest(fetched.take(count))
// Probe.kt — the receiver counts the list the app reads for the sent values
fun probe(sent: List<Signal>, received: List<Signal>): List<Arrival> =
    sent.mapIndexed { i, s -> Arrival("event-${i + 1}", s, received) }

If Each Key Is Fetched Once

Three separate keys are fetched once and activated: a theme, an upper limit, and a feature flag.

// Main.kt — if each key is fetched once, all are activated
fun main() {
    val theme = Signal("setting.theme", 1)
    val limit = Signal("setting.limit", 2)
    val flag = Signal("setting.flag", 3)
    val sent = listOf(theme, limit, flag)
    val arrivals = probe(sent, activateLatest(sent))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
event-1: key=setting.theme | copies=1 -> once
event-2: key=setting.limit | copies=1 -> once
event-3: key=setting.flag | copies=1 -> once
once: 3
duplicated: 0
superseded: 0
lost: 0

All three values reached the app as one copy each. No key competed.

If It Is Fetched Twice Before Activation

Now the theme value is fetched twice before activation arrives: the light theme the first time, the dark theme the second. The server changed the value in between.

// Main.kt — if the same key is fetched twice, the last value wins
fun main() {
    val first = Signal("setting.theme", 1)
    val second = Signal("setting.theme", 2)
    val limit = Signal("setting.limit", 3)
    val sent = listOf(first, second, limit)
    val arrivals = probe(sent, activateLatest(sent))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
event-1: key=setting.theme | copies=0 -> superseded
event-2: key=setting.theme | copies=1 -> once
event-3: key=setting.limit | copies=1 -> once
once: 2
duplicated: 0
superseded: 1
lost: 0

This lesson’s delivery: superseded. The first theme value never reached the app; the second value of the same key, with the greater sequence, took its place. The rule does not see the reason: it does not know whether the first value was wrong or the second was a correction; it sees only that a newer value with the same key arrived. There is a difference from the duplicated of the previous lessons: there, multiple copies of the same event arrived; here, no copy of the first value arrived.

This behavior is mostly what is wanted for configuration. What the app needs is the most current value; seeing the old value is not counted as a gain. The cost is that the first value passes with zero copies; a party that ties the first value to a counter or an experiment must know that it was never applied.

SE1. The model assumes the sequence numbers are given correctly by fetch time. On the platform, the order of two fetches can get mixed up by network latency, and the version information from the server is used for this; in the model, this information is directly the sequence number.

SE2. The model does the activation at a single moment and activates all pending values together. On the platform, activation can be left half done, or some keys can be postponed to later.

The Fetch Loop

Remote configuration does not end with a single request; it is a loop. The app asks the server at certain intervals, the server returns the values of that moment, and the app writes the values to the pending store. Each turn of the loop is a fetch, and each turn is independent of the one before: it takes whatever value the server returns this turn, and the app does not have to tell the server what it received in the previous turn.

This independence has a consequence. If the value on the server changed between two turns, two values for the same key can accumulate in the pending store. Because activation has not yet been done, neither is the value the app reads. As long as the loop keeps fetching, the pending list grows, and the only thing the app sees is the value that activation left at that moment.

The reason the fetch frequency is limited also follows from here. Every turn is a network request; asking too often strains the battery and the network, and asking too rarely makes a change arrive late. The model does not have this balance, because the model carries no time: the fetched values are an ordered list, and the wait between two turns is not visible.

The Line Between Superseded and Lost

The line between the two outcomes is thin but exact. Superseded says that the value’s copy did not arrive and a later value with the same key arrived and took its place; it does not say that the app is on the latest value. Lost says that the app could not reach the server’s latest decision; the app is running with an older value or with the default. For a publisher, the first is often an expected outcome, and the second is a situation that needs to be monitored.

That is why the two numbers must be reported separately. A high superseded count shows that the server changes quickly, and a high lost count shows that activation is falling behind. If they are reduced to a single “value that did not arrive” count, two different problems hide in the same figure.

What We Do Not Count in the Measurement

What the rule does not see is as important for configuration as what it does. The rule says whether a value reached the app; it does not say that the app used that value correctly. An activated flag still turns nothing on if the code that reads the flag is faulty. The rule marks a value being superseded or lost, but it does not know why the value was changed on the server.

These limits do not weaken the measurement; they show its scope. The lesson answers a single question: did the value the server sent become the value the app reads? Whether the value is correct, whether it was activated at the right time, and what it showed the user are other questions.

When a Value Is Activated

The timing of activation determines which value reaches the app. If activation is done right after the previous turn, every fetched value is activated and no superseded arises; there is a single value at the end of each turn. If activation is done after several turns, only the last of the values accumulated for the same key is activated, and the earlier ones are superseded. If activation is never done, all of them stay lost.

So superseded is not an error in configuration; it is the natural result of delaying activation. When activation is left to the app’s next launch, the last of the values fetched up to the launch wins. The same activation point changes, and the result changes: the model imitates this with the count parameter of the activateUpTo function. How many values activation covers produces superseded or lost depending on where it stands in the pending list.

A key is fetched three times, then another key comes, and activation covers only the first two.

// Main.kt — the activation point determines superseded and lost
fun main() {
    val a = Signal("setting.theme", 1)
    val b = Signal("setting.theme", 2)
    val c = Signal("setting.theme", 3)
    val limit = Signal("setting.limit", 4)
    val sent = listOf(a, b, c, limit)
    val arrivals = probe(sent, activateUpTo(sent, 2))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
event-1: key=setting.theme | copies=0 -> superseded
event-2: key=setting.theme | copies=1 -> once
event-3: key=setting.theme | copies=0 -> lost
event-4: key=setting.limit | copies=0 -> lost
once: 1
duplicated: 0
superseded: 1
lost: 2

The second of the same key’s three values was activated. The first was superseded by the second; the third did not arrive because it stayed outside the scope of activation, and since no greater sequence arrived, it was written as lost, not superseded. The rule looks only at what arrived: no sibling of the third value greater than itself came down to the app.

There is a trap here. The last fetched value is the server’s most current decision; but if activation did not cover it, the app runs with the previous value. The value the app runs with may not be the value the server wants right now. In the measurement, the active value for the theme is the second, but the last value the server sent is the third.

The Bounding Measurement

Fetching alone is not delivery. If a value is fetched but not activated, the app cannot read it. Three values are fetched, activation lasts until it covers only the first, and the app closes.

// Main.kt — a value that is fetched but not activated does not reach the app
fun main() {
    val theme = Signal("setting.theme", 1)
    val limit = Signal("setting.limit", 2)
    val flag = Signal("setting.flag", 3)
    val sent = listOf(theme, limit, flag)
    val arrivals = probe(sent, activateUpTo(sent, 1))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
event-1: key=setting.theme | copies=1 -> once
event-2: key=setting.limit | copies=0 -> lost
event-3: key=setting.flag | copies=0 -> lost
once: 1
duplicated: 0
superseded: 0
lost: 2

The limit and the flag were fetched but not activated; the rule counts them as lost. The difference from superseded is this: there is no newer value with the same key that displaced them. The value has only stayed in the pending store.

What does the app read in this case? A fixed fallback, that is, the in-app default. The default is not a value that comes from the server; it is a value the app carries within itself. That is why it does not count as delivery. The same happens when nothing is fetched at all.

// Main.kt — a value that is never fetched is lost, and the default does not count as delivery
fun main() {
    val theme = Signal("setting.theme", 1)
    val limit = Signal("setting.limit", 2)
    val sent = listOf(theme, limit)
    val arrivals = probe(sent, activateUpTo(sent, 0))
    arrivals.forEach { it.line() }
    countArrivals(arrivals)
}
event-1: key=setting.theme | copies=0 -> lost
event-2: key=setting.limit | copies=0 -> lost
once: 0
duplicated: 0
superseded: 0
lost: 2

Both values are lost. The app still runs, because it has default values; but these values are not what the server sent. The measurement does not say whether the running app is compatible with the server; it says only whether the value the server sent arrived.

SE3. The model assumes the value is not lost between fetch and activation. On the platform, a pending value goes away if the store is cleared or the app data is deleted.

The Server’s Role

The receiver is the app, but the source of the value is the server, and the choices the server makes affect the count at the receiver. If the server changes a key often, multiple values accumulate in a time as short as the fetch interval, and the superseded count rises. If the server changes the key rarely, a single value remains in the pending list for the same key, and no superseded arises.

That is why a high superseded is often a sign that the server is changing values too fast, not a problem of the app. A party wondering which value is active in which app should put the version history on the server next to the active value the app recorded. Without one, the other misleads: the server says it sent the value, and the app may never have seen it.

What the App Has to Do

The receiver of this lesson is the running app; for the configuration to reach the app correctly, four jobs are done on the app side.

  • Define a default for every key. The default does not count as delivery, but it keeps the app on its feet when a fetch fails. Setting the default equal to the safe value the server is expected to send makes behavior predictable in the cases where the configuration does not arrive.
  • Choose the moment of activation deliberately. If activation is done immediately, a feature on screen can change midway; if it is left to the next launch, the last value arrives a launch late. Both have a cost; which is acceptable depends on the feature.
  • Record which value is active. Assuming that the value used in an experiment or a counter is the same as the value on the server is wrong. An app that records the active value separately can later tell superseded and lost values apart.
  • Do not leave fetch failure silent. If a fetch could not be done at all, the app runs with the default and no one notices. Counting failed fetches makes the number of losses indirectly visible.

Common Misconceptions

A fetched value is valid in the app. It is not. In the measurement, two values were fetched but not activated, and were written as lost. Fetching only leaves the value on the device.

Superseded is an error. It is not. The last value of the same key won and the first value’s copy stayed at zero; this is the behavior wanted for configuration. The error is tying a superseded value to a counter or an experiment without noticing it.

A default value counts as delivered. It does not. In the measurement with no fetch at all, two values were lost, and the app still ran. Its running does not mean the server’s value arrived.

Activation always applies the most current value. It does not. The scope of activation can lag behind the fetch; in the measurement, the third theme value was fetched but not activated, and the app ran with the second.

Summary

  • This lesson’s delivery: superseded. For the same key fetched twice before activation, the last value wins and no copy of the first value arrives.
  • Fetching holds the value on the device, and activation makes it the value the app reads; the two are separate steps.
  • When three separate keys are fetched once, all three arrive once. For the theme fetched twice, the first value is superseded 1 and the second is once.
  • A value that is fetched but not activated is lost: once 1, lost 2. A value that is never fetched is also lost.
  • The activation point separates superseded and lost: when the second of three theme values is activated, the first is superseded and the third is lost.
  • An in-app default does not count as delivery, because it does not come from the server.

Next Step

In this lesson the last value won and the old value was superseded, because in configuration only the current one is useful. Not every external service is like this. In the next lesson come in-app purchases and ads: a purchase that is redelivered until it is acknowledged does not resemble the result in this lesson. There we will again measure what arrived and what did not.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close