Skip to content
academia.sh

Lesson 08 / 14

Connectivity State Management

Connectivity state management covers monitoring the connection, adapting to a weak connection, retrying, and showing the state to the user. The lesson measures that an image gallery is degraded and messaging stays deferred on a weak connection.

Published Updated

In this lesson

  • Explains that connectivity is more than present or absent, and what the monitoring mechanisms report
  • Measures a feature switching to a low-quality fallback on a weak connection
  • Compares fixed-interval retry with exponential backoff
  • Derives the per-feature display of connection state to the user from operability records
Contents

In the previous lesson, background capability was either there or not; the connection was always there. In reality the connection also drops, slows down, and comes back. In a subway the signal comes and goes, in a crowded hall the connection looks present but no data flows, and coming out of a tunnel the network returns all at once. This lesson covers monitoring the connection, adapting to a weak connection, retrying a failed request, and showing the state to the user.

The course question stays the same: how well does this feature work under this condition? This lesson asks it for three features under three connection states.

Monitoring the Connection

Both platforms report changes in the connection to the app. The app registers a monitor, and the system calls it when the connection is established, lost, or changes quality. The iOS snippet below shows this. The snippet is not measured.

// Platform code: iOS connection monitoring
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
    let isOnline = path.status == .satisfied
    let isWeak = path.isConstrained || path.isExpensive
    connectionState.update(online: isOnline, weak: isWeak)
}
monitor.start(queue: DispatchQueue(label: "connectivity"))

On Android the same job is done by registering a callback on a manager obtained from a system service. The snippet is not measured.

// Platform code: Android connection monitoring
val manager = context.getSystemService(ConnectivityManager::class.java)
manager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) = connectionState.update(online = true)
    override fun onLost(network: Network) = connectionState.update(online = false)
})

What the monitor says is limited. The system says “a network path exists”; it does not say “a request will reach the server”. A connection that is joined to a network but cannot reach the internet, that is waiting for a sign-in page, or that is very slow can look “connected” to the monitor. So the monitor is a hint, not a certainty: the success or failure of a real request has the last word.

Weak Connection

The two ends of connectivity, fully connected and not connected at all, are easy. The hard part is the middle: the connection exists but data flows slowly, drops often, or the user has turned on data saving. In this situation the app has two options: keep asking for large content and wait, or settle for smaller content. A good app picks the second: an image arrives at low resolution instead of high, a large list shrinks to a short one, and refresh on the user’s request replaces live updates.

The model builds a weak connection as a missing capability: the network exists but bandwidth does not. bandwidth means enough data rate to receive large content in a reasonable time. When it is missing, the feature moves to its defined fallback.

Building the Features

Measuring needs the course core. The core is familiar from the first lesson and is given again in the same form in every lesson; this lesson does not touch it.

// operability.mjs — classifies how fully a feature works under a condition
export const LEVELS = ["full", "degraded", "deferred", "disabled"];
const FALLBACKS = ["degraded", "deferred"];

export function feature(name, needs, fallback = {}) {
  return { name, needs, fallback };
}

export function condition(name, has) {
  return { name, has };
}

function judge(item, cond) {
  const missing = item.needs.filter((need) => !cond.has.includes(need)).sort();
  let level = "full";
  for (const need of missing) {
    const given = FALLBACKS.includes(item.fallback[need]) ? item.fallback[need] : "disabled";
    if (LEVELS.indexOf(given) > LEVELS.indexOf(level)) level = given;
  }
  return { level, feature: item.name, condition: cond.name, missing };
}

export function assess(features, conditions) {
  const records = [];
  for (const cond of conditions) {
    for (const item of features) records.push(judge(item, cond));
  }
  return records;
}

export function printLevels(records) {
  for (const r of records) {
    const missing = r.missing.length === 0 ? "-" : r.missing.join(",");
    console.log(`${r.level}: ${r.feature} @ ${r.condition} missing=${missing}`);
  }
}

export function countLevels(records) {
  for (const name of LEVELS) {
    console.log(`${name}: ${records.filter((r) => r.level === name).length}`);
  }
  console.log(`affected: ${records.filter((r) => r.level !== "full").length}`);
}

The lesson’s helper file builds three features. Gallery is an image gallery; it needs network and bandwidth, and when bandwidth is missing it falls to a low-resolution preview, so its fallback is degraded. It has no fallback for network. Chat is messaging; it needs the same two capabilities and queues the message when either is missing, so both of its fallbacks are deferred. Settings is the settings screen and needs only storage. conditionsFor(link) turns a connection state into a condition: online for a good connection, weak-network for a weak one, and offline when there is no connection.

// connectivity.mjs — builds the connectivity features and conditions
import { feature, condition } from "./operability.mjs";

export const gallery = feature("Gallery", ["network", "bandwidth"], { bandwidth: "degraded" });
export const chat = feature("Chat", ["network", "bandwidth"], { network: "deferred", bandwidth: "deferred" });
export const settings = feature("Settings", ["storage"]);

export function conditionsFor(link) {
  if (link.quality === "good") return condition("online", ["network", "bandwidth", "storage"]);
  if (link.quality === "weak") return condition("weak-network", ["network", "storage"]);
  return condition("offline", ["storage"]);
}

The first measurement turns three connection states into conditions, prints the capabilities of each condition, and classifies two features under three conditions.

// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { gallery, settings, conditionsFor } from "./connectivity.mjs";

const links = [{ quality: "good" }, { quality: "weak" }, { quality: "none" }];
const conds = links.map(conditionsFor);
for (const c of conds) console.log(`${c.name}: ${c.has.join(",")}`);
printLevels(assess([gallery, settings], conds));
online: network,bandwidth,storage
weak-network: network,storage
offline: storage
full: Gallery @ online missing=-
full: Settings @ online missing=-
degraded: Gallery @ weak-network missing=bandwidth
full: Settings @ weak-network missing=-
disabled: Gallery @ offline missing=bandwidth,network
full: Settings @ offline missing=-

On a weak connection Gallery is degraded: the network exists but bandwidth does not, and the gallery falls to a low-resolution preview. With no connection the same gallery is disabled, because the missing network has no fallback and the gallery keeps no cache. Settings depends only on the local store, so it is full under all three conditions.

OS4. The model reduces the connection to three exact states: good, weak, none. A real connection slides constantly between these three, and the monitor reports the slide with a delay. Where weakness begins is built in the model not as a threshold but as the absence of the bandwidth capability. The rule sees the capability and the fallback; it does not see the data rate, the lost packets, or the user’s patience. The model builds this operability’s rule; the model is not the framework itself.

Retry and Exponential Backoff

A message sent while there is no connection or while it is weak can fail. The app retries the request. The question is when to try. In fixed-interval retry, each attempt comes the same interval after the previous one. If the connection will return soon, it catches it at the first opportunity, but if it will not return, it wastes battery and network for nothing; and if thousands of clients load the same server at the same moment, it tires the server too.

In exponential backoff the wait doubles after each failed attempt and stops at an upper limit. The first attempts are fast and later ones are sparse. In real apps a small random share is also added to the wait so that thousands of clients do not try at the same moment; the model has no randomness, so it does not build this share. Being able to retry safely depends on the idempotent submission of the previous lesson: the same message arriving twice does not corrupt the result.

In the model, time is expressed not in real duration but in turns. A turn is a unit of order; it does not stand for a second or a minute. The function below computes, in a scenario where the connection returns at a given turn, how many attempts each strategy makes and at which turn the work gets through.

// retry.mjs — builds retry strategies by turn count
export function attemptsUntil(strategy, returnTurn) {
  let turn = 0;
  let wait = 1;
  let attempts = 0;
  while (true) {
    attempts += 1;
    if (turn >= returnTurn) return { attempts, turn };
    turn += wait;
    if (strategy === "exponential") wait = Math.min(wait * 2, 8);
  }
}

The second measurement prints the record of Chat with no connection and the result of the two strategies. The connection returns at turn ten.

// main.mjs
import { assess, printLevels } from "./operability.mjs";
import { chat, settings, conditionsFor } from "./connectivity.mjs";
import { attemptsUntil } from "./retry.mjs";

const conds = [{ quality: "none" }, { quality: "good" }].map(conditionsFor);
printLevels(assess([chat, settings], conds).filter((r) => r.feature === "Chat"));
for (const strategy of ["fixed", "exponential"]) {
  const result = attemptsUntil(strategy, 10);
  console.log(`${strategy}: ${result.attempts} attempts, got through at turn ${result.turn}`);
}
deferred: Chat @ offline missing=bandwidth,network
full: Chat @ online missing=-
fixed: 11 attempts, got through at turn 10
exponential: 5 attempts, got through at turn 15

The fixed-interval strategy got the work through at turn ten, as soon as the connection returned, but it made eleven attempts. The exponential strategy made five attempts and got the work through at turn fifteen: the connection had returned, but the next attempt had not yet come. The cost of sparse attempts is delay; the cost of tight attempts is wasted attempts. Which one to choose depends on what the product cares about. In both, Chat @ offline is deferred, because the message is not lost; it is queued and sent when the connection returns.

Showing the State to the User

When the connection is absent or weak, showing the user a single “you are offline” notice is easy and misleading. At the same moment the gallery has fallen to low resolution, messages have entered the queue, and settings work normally. What is meaningful to the user is the state of each feature. The operability record carries this message directly: a degraded feature says “limited”, a deferred feature says “queued”, a disabled feature says “unavailable”; a full feature says nothing.

// banner.mjs — derives the state to show the user from a record
const LABELS = { full: "-", degraded: "limited", deferred: "queued", disabled: "unavailable" };

export function labelFor(record) {
  return LABELS[record.level];
}

export function printBanners(records) {
  for (const r of records) console.log(`${r.feature} @ ${r.condition}: ${labelFor(r)}`);
}
// main.mjs
import { assess } from "./operability.mjs";
import { gallery, chat, settings, conditionsFor } from "./connectivity.mjs";
import { printBanners } from "./banner.mjs";

const conds = [{ quality: "weak" }, { quality: "none" }].map(conditionsFor);
printBanners(assess([gallery, chat, settings], conds));
Gallery @ weak-network: limited
Chat @ weak-network: queued
Settings @ weak-network: -
Gallery @ offline: unavailable
Chat @ offline: queued
Settings @ offline: -

On a weak connection and with no connection, Settings carries no notice. Chat says “queued” under both conditions: the message was not sent, but it was not lost. Gallery says “limited” on a weak connection and “unavailable” with no connection. The same connection state produced three different messages on three features.

The Bounding Measurement

This lesson’s focus record is Gallery @ weak-network. The last measurement runs three features under three connection states and prints the weak-connection records and the count.

// main.mjs
import { assess, printLevels, countLevels } from "./operability.mjs";
import { gallery, chat, settings, conditionsFor } from "./connectivity.mjs";

const features = [gallery, chat, settings];
const links = [{ quality: "good" }, { quality: "weak" }, { quality: "none" }];
const records = assess(features, links.map(conditionsFor));
printLevels(records.filter((r) => r.condition === "weak-network"));
countLevels(records);

const withCopies = (copies) =>
  assess(features, [{ quality: "weak", copies }].map(conditionsFor));
const same = JSON.stringify(withCopies(1)) === JSON.stringify(withCopies(3));
console.log(`copy count is not recorded: ${same ? "yes" : "no"}`);
degraded: Gallery @ weak-network missing=bandwidth
deferred: Chat @ weak-network missing=bandwidth
full: Settings @ weak-network missing=-
full: 5
degraded: 1
deferred: 2
disabled: 1
affected: 4
copy count is not recorded: yes

The first line carries the lesson’s operability: on a weak connection the gallery came out degraded. Under the same condition Chat is deferred and Settings is full. Of the nine records, five are full, one is degraded, two are deferred, and one is disabled; four are affected.

The last line shows how this differs from the question of M31/K05. That lesson asked how many copies a sent event reaches the receiver as. When the connection returns, the queued messages are delivered, and because of retries the same message can arrive more than once. Operability does not classify this: one copy and three copies produce the same record. This is not an observation, it is the definition of the model: the conditionsFor function that builds the condition never reads the copies field. Operability classifies not how many copies a delivery was, but what the feature does while the connection is weak. The message enters a queue, and that is deferred regardless of how many times it is sent.

Summary

  • This lesson’s operability: degraded. Gallery @ weak-network came out degraded because bandwidth is missing and a degraded fallback is defined; under the same condition Chat stayed deferred and Settings stayed full.
  • Platform monitors report that a network path exists, not that a request will reach the server; the result of a real request has the last word.
  • On a weak connection the network exists but bandwidth does not; the feature moves to its defined fallback, a low-resolution preview or a queue.
  • In the same scenario the fixed-interval strategy made eleven attempts and got through at turn ten, and the exponential strategy made five attempts and got through at turn fifteen; the cost of sparse attempts is delay.
  • The state is shown to the user per feature: limited, queued, unavailable.

Next Step

In this topic, the reason a feature stayed disabled or degraded was always a condition: network, storage, background, data rate. In production, a feature does not need a broken condition to stay disabled; the configuration of the stage can keep the feature disabled. The next lesson, Mobile Continuous Delivery, covers the pipeline in which the app is built, signed, and reaches the stores and test users. It then measures the operability of a feature that is enabled at the staging stage and disabled at the production stage.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close