Skip to content
academia.sh

Lesson 03 / 16

Push and Pull Based Distribution

Propagating content to edges with two distinct models: pull-based distribution producing the first miss and a stale-version window, push-based distribution producing wasted copies and a message multiplier, choosing between the two by content class, and the choice's effect on the introductory course's write bandwidth and storage calculations.

Contents

The edge cache in the previous lesson waited for the request: the first time a key was asked for, it missed, went to the origin, and stored the response. The measurement also counted those first requests — 20,046 of the 200,000 requests missed at the edge, each a trip to the origin. This waiting is not mandatory.

Content can be sent to the edge before it is ever asked for. There are two ways, and the choice between them is a propagation model decision. Pull-based distribution pulls content on the first request: the edge misses, goes to the origin, stores the response. Push-based distribution sends content to edges before any request arrives: the edge never misses, but whether the pushed content will ever be requested is unknown. This lesson measures both models on the same content set.

What Marks the Difference

The difference between the two models shows up in three places. First, origin requests: in pull, every edge goes to the origin at least once per object; in push, never. Second, wasted copies: push sends an object even to edges that will never request it. Third, stale versions: when an object changes, pull’s edges keep serving the old version until their TTL expires, while push sends the new version to every edge in one step.

All three tie back to a single quantity: how many edges request an object. If every edge requests it, push wastes nothing; if only one edge does, all but one of push’s copies are wasted. This lesson measures that.

Setup

The model is in-process, and a round is an abstract step, the same kind of abstraction as the introductory course’s round. Content splits into two classes. Common objects are the tracking page’s shell, style, and script files: requested from every edge. Unique objects are per-shipment tracking responses, and their count comes from the previous lesson’s measurement — 20,046 distinct responses in the window.

Code Assumption Value Rationale
T10 edge count 12 four entry points in each of the three zones
T11 common object count 20 shell, style, script, and icon files
T12 edges requesting a unique object 1 a shipment’s queries come from the same user, the same edge
// propagation/model.mjs — in-process model: the same content set is distributed first pull
// based, then push based. A round is an abstract step; TTL and the update round are parameters.
export const EDGES = 12;        // T10: edge count
export const TTL = 300;         // T1: TTL (rounds)
export const ROUNDS = 900, UPDATE = 100;

// T11: common objects are requested from every edge. T12: a shipment's queries come from a
// single edge, so a unique object is requested from only one edge. UNIQUE is the previous lesson's measurement.
export const CLASS = [
  { name: "common", objects: 20, requesting: EDGES },
  { name: "unique", objects: 20_046, requesting: 1 },
];

export function cost(cls) {
  const pull = cls.objects * cls.requesting;        // one trip to the origin on the first miss
  const push = cls.objects * EDGES;                 // the set is pushed to every edge
  return { pull, push, wasted: push - pull, wastedShare: 1 - cls.requesting / EDGES };
}

// mode: "pull", "push", or "hybrid" (common objects pushed, unique objects pulled)
const modeOf = (mode, name) => (mode === "hybrid" ? (name === "common" ? "push" : "pull") : mode);

export function propagate(mode) {
  const s = { origin: 0, pushed: 0, wasted: 0, stale: 0, messages: 0 };
  for (const cls of CLASS) {
    const m = cost(cls);
    if (modeOf(mode, cls.name) === "push") { s.pushed += m.push; s.wasted += m.wasted; s.messages += m.push; }
    else { s.origin += m.pull; s.messages += m.pull; }
  }
  const commonMode = modeOf(mode, "common");
  const version = new Array(EDGES).fill(1);          // the common object's version each edge sees
  const expires = Array.from({ length: EDGES }, (_, k) => 1 + ((k * 27) % TTL));
  for (let round = 1; round <= ROUNDS; round++) {
    if (commonMode === "push" && round === UPDATE) { version.fill(2); s.messages += EDGES; }
    for (let k = 0; k < EDGES; k++) {
      if (commonMode === "pull" && round >= expires[k]) {  // TTL expired, refetched
        version[k] = round >= UPDATE ? 2 : 1;
        expires[k] = round + TTL;
        s.origin += 1; s.messages += 1;
      }
      if (round >= UPDATE && version[k] === 1) s.stale += 1;   // round in which the stale version was served
    }
  }
  return s;
}
// propagation/measure.mjs — comparing two propagation modes on the same content set
import { propagate, cost, CLASS, EDGES, TTL, ROUNDS, UPDATE } from "./model.mjs";

console.log(`${EDGES} edges, TTL ${TTL} rounds, ${ROUNDS} rounds, ` +
  `common object's version changes at round ${UPDATE}`);
console.log();
console.log("class  | objects | requesting edges | pull req | push req | wasted | wasted share");
console.log("-------|---------|-------------------|----------|----------|--------|-------------");
for (const cls of CLASS) {
  const m = cost(cls);
  console.log(`${cls.name.padEnd(6)} | ${String(cls.objects).padStart(7)} | ` +
    `${String(cls.requesting).padStart(17)} | ${String(m.pull).padStart(8)} | ` +
    `${String(m.push).padStart(8)} | ${String(m.wasted).padStart(6)} | ` +
    `${`%${(m.wastedShare * 100).toFixed(1)}`.padStart(11)}`);
}
console.log();
console.log("mode   | origin req | pushed obj | wasted | stale version rounds | messages");
console.log("-------|------------|------------|--------|-----------------------|---------");
const record = {};
for (const mode of ["pull", "push", "hybrid"]) {
  const r = record[mode] = propagate(mode);
  console.log(`${mode.padEnd(6)} | ${String(r.origin).padStart(10)} | ` +
    `${String(r.pushed).padStart(10)} | ${String(r.wasted).padStart(6)} | ` +
    `${String(r.stale).padStart(21)} | ${String(r.messages).padStart(8)}`);
}
const pull = record.pull, push = record.push;
console.log();
console.log(`message ratio push/pull = ${(push.messages / pull.messages).toFixed(2)}`);
console.log(`stale version rounds, average per edge = ${(pull.stale / EDGES).toFixed(1)} rounds ` +
  `(${((pull.stale / EDGES) / TTL).toFixed(2)} times the TTL)`);
const two = cost({ ...CLASS[1], requesting: 2 });
console.log(`T12 sensitivity: if the unique object were requested from two edges, wasted share ` +
  `%${(two.wastedShare * 100).toFixed(1)} (%${(cost(CLASS[1]).wastedShare * 100).toFixed(1)} at one edge)`);
12 edges, TTL 300 rounds, 900 rounds, common object's version changes at round 100

class  | objects | requesting edges | pull req | push req | wasted | wasted share
-------|---------|-------------------|----------|----------|--------|-------------
common |      20 |                12 |      240 |      240 |      0 |        %0.0
unique |   20046 |                 1 |    20046 |   240552 | 220506 |       %91.7

mode   | origin req | pushed obj | wasted | stale version rounds | messages
-------|------------|------------|--------|-----------------------|---------
pull   |      20322 |          0 |      0 |                  1794 |    20322
push   |          0 |     240792 | 220506 |                     0 |   240804
hybrid |      20046 |        240 |      0 |                     0 |    20298

message ratio push/pull = 11.85
stale version rounds, average per edge = 149.5 rounds (0.50 times the TTL)
T12 sensitivity: if the unique object were requested from two edges, wasted share %83.3 (%91.7 at one edge)

These numbers belong to the measurement class; their inputs are the T10–T12 assumptions and the previous lesson’s 20,046 measurement.

The first table confirms the selection criterion. In the common class, pull and push request the same amount (240 to 240) with zero wasted copies, since the object is already requested from all twelve edges. In the unique class, push sends 240,552 copies, 220,506 of which will never be requested: a wasted share of 91.7%. The same criterion shows up as a formula — the wasted share is the complement of the ratio of requesting edges to total edges. Playing T12 drops the share from 91.7% to 83.3%; even doubled, the assumption still leaves push heavily wasted in the unique class.

The second table shows where the cost lands. Pull generates 20,322 requests to the origin and, in return, serves stale versions for 1,794 rounds — an average of 149.5 rounds per edge, half the TTL. Push produces no stale versions but sends 240,804 messages, 11.85 times pull’s count. The two models pay for the same work in different currencies: one in stale responses, the other in wasted messages. This mirrors the tradeoff between the two failover patterns in the introductory course, where the cost split between downtime and lost data.

The third row shows the decision is made at the content-class level, not the system level. The hybrid mode pushes common objects and pulls unique ones: 20,046 origin requests, 0 wasted copies, 0 stale-version rounds, 20,298 messages — better than both other modes on all three counts. A design forced to pick a single model pays unnecessary cost on one of the two classes.

Back to the Numbers

// propagation/compute.mjs — which of K01's computations change when a propagation mode is chosen
import { propagate, cost, CLASS, EDGES } from "./model.mjs";

const PEAK_READ = 416.67, PEAK_WRITE = 97.22, DAILY_EVENTS = 2_800_000;  // K01 calculation
const V5 = 480, ACTIVE = 1_200_000;             // K01 V5; active shipment count computed with T6
const K01_BEHIND_CACHE = 41.67, K01_WRITE = 0.17, K01_READ = 1.60;
const WINDOW = 480;                             // s: the previous lesson's measurement window

const pull = propagate("pull"), hybrid = propagate("hybrid");
const mbit = (rate, bytes) => (rate * bytes * 8) / 1e6;
const pushRate = PEAK_WRITE * EDGES;
const staleResponses = pull.stale * (PEAK_READ / EDGES);
const pushGB = (ACTIVE * V5 * EDGES) / 1e9, pullMB = (CLASS[1].objects * V5) / 1e6;

console.log(`pull: ${pull.origin} requests to the origin / ${WINDOW} s = ${(pull.origin / WINDOW).toFixed(2)} ` +
  `requests/s -- K01 reads behind cache/s = ${K01_BEHIND_CACHE}`);
console.log(`hybrid: ${(hybrid.origin / WINDOW).toFixed(2)} requests/s to the origin, no cold start ` +
  `miss because the common object is pushed`);
console.log();
console.log(`push: every state event goes to ${EDGES} edges`);
console.log(`  peak push rate = ${pushRate.toFixed(2)} pushes/s, ${DAILY_EVENTS * EDGES} pushes/day`);
console.log(`  push bandwidth = ${mbit(pushRate, V5).toFixed(2)} Mbit/s = ` +
  `${(mbit(pushRate, V5) / K01_WRITE).toFixed(1)} times K01's write ingress (${K01_WRITE}), ` +
  `${(mbit(pushRate, V5) / K01_READ).toFixed(2)} times its read egress (${K01_READ.toFixed(2)})`);
console.log(`  unique content held at the edges = ${pushGB.toFixed(2)} GB ` +
  `(${(ACTIVE * V5 / 1e6).toFixed(0)} MB x ${EDGES} edges)`);
console.log(`  same content in pull mode = ${pullMB.toFixed(2)} MB, ratio ` +
  `${((pushGB * 1e3) / pullMB).toFixed(0)}`);
console.log();
console.log(`stale responses from one version change in pull mode = ` +
  `${staleResponses.toFixed(0)} (${pull.stale} rounds x ${(PEAK_READ / EDGES).toFixed(2)} requests/s)`);
console.log(`unique object's wasted push share = ` +
  `%${(cost(CLASS[1]).wastedShare * 100).toFixed(1)}; a push is only not wasted when the object is ` +
  `requested from all ${EDGES} edges`);
pull: 20322 requests to the origin / 480 s = 42.34 requests/s -- K01 reads behind cache/s = 41.67
hybrid: 41.76 requests/s to the origin, no cold start miss because the common object is pushed

push: every state event goes to 12 edges
  peak push rate = 1166.64 pushes/s, 33600000 pushes/day
  push bandwidth = 4.48 Mbit/s = 26.4 times K01's write ingress (0.17), 2.80 times its read egress (1.60)
  unique content held at the edges = 6.91 GB (576 MB x 12 edges)
  same content in pull mode = 9.62 MB, ratio 718

stale responses from one version change in pull mode = 62292 (1794 rounds x 34.72 requests/s)
unique object's wasted push share = %91.7; a push is only not wasted when the object is requested from all 12 edges

The first line shows pull staying consistent with the introductory course: the request rate to the origin is 42.34, against the course’s reads behind cache/s of 41.67. The two numbers come from separate paths — one from the assumption table, the other from this lesson’s model run — and land within two percent of each other. When pull is chosen, the introductory course’s arithmetic stays valid as it is.

Push breaks that same arithmetic. Since every state event changes the corresponding tracking response, the new version must go to all twelve edges: peak push rate is 1,166.64 pushes/s, 33,600,000 pushes a day. That bandwidth is 4.48 Mbit/s — 26.4 times the course’s write ingress Mbit/s (0.17) and 2.80 times its read egress Mbit/s (1.60). Push turns read load into write load and multiplies it by the edge count. A design’s largest bandwidth line item shifts, once push is chosen, to the write side flowing at 97 events per second.

Storage moves the same way. Holding 1,200,000 active shipments’ tracking responses at every edge costs 576 MB per edge, 6.91 GB across twelve edges. In pull mode, the same content’s total footprint at the edges is 9.62 MB — a 718-fold difference. This figure is not added to the course’s stored data GB calculation, because the edge copies do not replace the store — they sit on top of it.

The last line gives pull’s bill. When a common object’s version changes, the stale version is served for 1,794 rounds, and at 34.72 requests per second per edge, that means 62,292 stale responses. The number ties directly to the TTL and repeats the first lesson’s finding: the TTL is not a cache setting but a single decision that determines both the failover duration and the version propagation time.

Summary

  • The propagation model is a decision: pull-based distribution pulls content on the first request, push-based distribution sends it before any request arrives.
  • The selection criterion is how many edges request an object; the wasted push share is the complement of the requesting-edge ratio. The measured share was 0% in the common class, 91.7% in the unique class.
  • Pull generated 20,322 requests to the origin and served stale versions for 1,794 rounds; push produced no stale versions but sent 240,804 messages — 11.85 times pull’s count.
  • Hybrid mode beats both other modes on all three counts (20,046 origin requests, 0 wasted, 0 stale rounds): the decision is made at the content-class level, not the system level.
  • Pull preserves the introductory course’s arithmetic: 42.34 requests/s reach the origin, against the course’s reads behind cache/s of 41.67.
  • Push breaks it: a peak of 1,166.64 pushes/s and 4.48 Mbit/s — 26.4 times write ingress; content held at the edges is 6.91 GB, 718 times pull mode’s footprint.

Next Step

These two lessons kept content at the edge, but the content always came from the same place: the application itself. The twenty common-class objects — shell, style, script, and icon files — touch none of the application’s logic; they sit as fixed byte sequences and are still requested from the application processes’ address. The next lesson separates that path: it measures what happens to the requests reaching the application, the number of stops a request touches, and the introductory course’s peak edge request rate once content is served from a place separate from the application.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close