Lesson 08 / 18
Peer-to-Peer
Measuring the decentralized arrangement where the initiator and waiter roles merge into a single unit: the neighbor count six transfer hubs must know across three neighborhood arrangements, the round and message count required for a tariff update to reach everyone, the learner count when a unit is withdrawn, and the count of neighbor lists edited when a new unit is added.
Contents
In the previous arrangement, asymmetry was deliberate: one side always initiated, the other always waited, and the waiting side was singular. This singularity has a counterpart in the delivery operations context — all fee questions going to a single place means that place is known by everyone. In an arrangement where multiple transfer hubs carry out the same work, the question reverses. If every hub is both asker and answerer, the obligation to know spreads out instead of piling up in one direction.
Peer-to-peer is the software-architecture-level signature of this style: role separation falls away, and the two roles merge into the same module. The Client–Server Model lesson in the Computer Networks curriculum said that this arrangement gains capacity as demand grows, but that the client–server model was chosen for the web because it requires a stable address and centralized control. Address, connection, and the network layer belong there. The question here is three counts: the number of neighbors a unit must know, the number of messages required for a tariff update to reach every unit, the number of units that keep working when one unit is withdrawn.
The quality attribute it connects to is reliability, and the quality question is: when one transfer hub goes out of service, how many hubs fail to learn the new tariff.
The Module That Carries Two Roles
There are six transfer hubs, and the same file runs for all of them. Two things sit inside the
module at once: handleRequest, which serves incoming requests, and broadcast, which sends
requests to neighbors. The previous lesson’s two files — client and server — are merged into a
single file here.
mkdir -p es
// es/es.mjs — the single module running for each transfer hub: both receiving and initiating export function peer(name) { const neighbors = []; let version = "V1", received = 0, sent = 0; return { name, get version() { return version; }, introduce: (neighbor) => neighbors.push(neighbor), counters: () => ({ received, sent, neighbor: neighbors.length }), handleRequest(request) { received += 1; if (request.operation !== "tariff-announce") return { state: "unknown-operation" }; if (request.version === version) return { state: "known" }; version = request.version; return { state: "new" }; }, broadcast() { for (const neighbor of neighbors) { sent += 1; neighbor.handleRequest({ operation: "tariff-announce", version }); } }, }; }
A peer’s only knowledge is its neighbors. How many neighbors it knows is supplied from the outside; the measurable face of the style is right there.
Three Neighborhood Arrangements
The same six units are connected in three separate ways. In the star arrangement, one hub knows everyone and everyone knows only the hub; this is the previous lesson’s arrangement written out as a neighborhood. In the ring arrangement, each unit knows two neighbors. In the full mesh, each unit knows the other five.
The update enters from the same place in all three arrangements: the new tariff version is announced to the ANK hub. Then it propagates round by round — in each round, every unit that knows the version announces it to its neighbors.
// es/ag.mjs — the same units in three neighborhood arrangements, and one update's propagation run import { peer } from "./es.mjs"; export const NAMES = ["IST", "ANK", "IZM", "BUR", "ADA", "TRA"]; export const ENTRY = "ANK"; export const star = (names) => new Map(names.map((a, i) => [a, i === 0 ? names.slice(1) : [names[0]]])); export const ring = (names) => new Map(names.map((a, i) => [a, [names[(i + 1) % names.length], names[(i - 1 + names.length) % names.length]]])); export const fullMesh = (names) => new Map(names.map((a) => [a, names.filter((b) => b !== a)])); export const ARRANGEMENTS = [["star", star], ["ring", ring], ["full mesh", fullMesh]]; export function propagate(map) { const peers = new Map([...map.keys()].map((a) => [a, peer(a)])); for (const [a, neighbors] of map) for (const n of neighbors) peers.get(a).introduce(peers.get(n)); peers.get(ENTRY).handleRequest({ operation: "tariff-announce", version: "V2" }); const all = [...peers.values()]; let round = 0; while (all.some((e) => e.version !== "V2") && round < 20) { round += 1; for (const e of all.filter((e) => e.version === "V2")) e.broadcast(); } return { round, message: all.reduce((t, e) => t + e.counters().sent, 0), neighbor: all.reduce((t, e) => t + e.counters().neighbor, 0), learner: all.filter((e) => e.version === "V2").length, unit: all.length, }; }
// propagation.mjs — known neighbor count, propagation round, and message count in three arrangements import { NAMES, ARRANGEMENTS, propagate } from "./es/ag.mjs"; console.log("arrangement units known neighbor round message learner"); for (const [label, build] of ARRANGEMENTS) { const s = propagate(build(NAMES)); console.log( `${label.padEnd(10)}${String(s.unit).padStart(4)}${String(s.neighbor).padStart(13)}` + `${String(s.round).padStart(6)}${String(s.message).padStart(7)}${`${s.learner}/${s.unit}`.padStart(9)}`, ); }
node propagation.mjs
arrangement units known neighbor round message learner star 6 10 2 7 6/6 ring 6 12 3 18 6/6 full mesh 6 30 1 5 6/6
Propagation here is a model, not a network measurement: what is counted is the message and round count, no duration is measured.
Reading the Numbers
All three arrangements reached six out of six units, so they do the same job. The differences collect in three columns.
The known-neighbor count is the total of the obligation to know. In the star it is 10: the hub knows five, and five units know one hub each. In the ring it is 12, in the full mesh 30. The full mesh is the most expensive arrangement to know, and this grows with the square of the unit count.
The message and round counts move in opposite directions. The full mesh finished with the fewest messages (5) and in a single round, because the entry unit knew everyone. The ring spent the most messages (18) and took three rounds, because the news has to hop from neighbor to neighbor; in every round, every unit that knows the news announces it again, and most of those announcements carry an already-known version. The star landed between the two: 7 messages, 2 rounds — the news had to reach the hub in the first round and spread out from the hub in the second.
These three columns form a trade-off table: reducing the obligation to know increases the message count.
When a Unit Is Withdrawn
The measurement for the reliability question is this experiment: every unit except the entry unit is removed from the network in turn, propagation is run from the start, and the count of how many of the remaining units learned the new version is taken. The same script also calculates how many neighbor lists must be edited when a seventh hub is added to the network.
// resilience.mjs — learner count when a unit is withdrawn, the cost of adding a unit, and the two roles in one file import { readFileSync } from "node:fs"; import { NAMES, ENTRY, ARRANGEMENTS, propagate } from "./es/ag.mjs"; const withdraw = (map, name) => new Map([...map].filter(([a]) => a !== name).map(([a, n]) => [a, n.filter((x) => x !== name)])); console.log("arrangement worst learner critical unit lists edited for new unit"); for (const [label, build] of ARRANGEMENTS) { const full = build(NAMES); let critical = 0, worst = [Infinity, 0]; for (const name of NAMES.filter((a) => a !== ENTRY)) { const s = propagate(withdraw(full, name)); if (s.learner < s.unit) critical += 1; if (s.learner < worst[0]) worst = [s.learner, s.unit]; } const bigger = build([...NAMES, "OLD"]); const changed = NAMES.filter((a) => full.get(a).join(",") !== bigger.get(a).join(",")).length; console.log( `${label.padEnd(9)}${`${worst[0]}/${worst[1]}`.padStart(16)}${String(critical).padStart(14)}` + `${String(changed).padStart(34)}`, ); } const source = readFileSync("es/es.mjs", "utf8"); const receiver = (source.match(/handleRequest\(request\)/g) ?? []).length; const initiator = (source.match(/\.handleRequest\(\{/g) ?? []).length; console.log(`es/es.mjs: receiving definitions = ${receiver}, initiating calls = ${initiator}`);
node resilience.mjs
arrangement worst learner critical unit lists edited for new unit star 1/5 1 1 ring 5/5 0 2 full mesh 5/5 0 6 es/es.mjs: receiving definitions = 1, initiating calls = 1
In the star, the critical-unit count is 1, and that unit is the hub: when it is withdrawn, only one of the remaining five units — the one the news enters through — knows the version, and four fail to learn it. In the ring and the full mesh, the critical-unit count is 0; whichever unit is withdrawn, all five of the remaining five learn it. This is the count of decentralization. The word is not a description of structure but a measurement result: the number of units that stop others’ work when withdrawn is zero.
The last column is the other half of the cost. When the seventh hub is added, one list — the hub’s list — is edited in the star. In the ring, two lists are edited; in the full mesh, six. The full mesh’s gain of finishing in a single round is paid for by updating every existing unit’s knowledge with each new unit.
The last line gives the style’s code signature. There is one receiving definition and one initiating call in a single file. The previous lesson’s two separate files collapsed into one file here, and this is the reason for the measurement: because the direction of initiation does not pile up in a single direction, removing one unit from the network does not stop the rest from talking to each other.
Summary
- The code signature of the peer-to-peer style is the removal of role separation: the receiving definition and the initiating call sit in the same module, and a single file runs for six units.
- The known-neighbor count came out to 10 in the star, 12 in the ring, and 30 in the full mesh; the obligation to know grows with the square of the unit count in the full mesh.
- The message and round counts moved in opposite directions: the full mesh spent 5 messages and 1 round, the star 7 messages and 2 rounds, the ring 18 messages and 3 rounds; all three reached six out of six units.
- The critical-unit count is 1 in the star, 0 in the ring and the full mesh; when the hub was withdrawn, four of the remaining five units failed to learn the new tariff.
- The cost of decentralization is the price of adding a new unit: for the seventh hub, 1 neighbor list was edited in the star, 2 in the ring, and 6 in the full mesh.
Next Step
The two styles covered so far both addressed interaction between units of the same kind: both sides of the conversation were units that calculated a fee or announced a tariff, and the only difference was who initiated. The library also has a boundary of a different kind. The clerk at the operations desk does not see how the fee was calculated, only what appears on the screen: the zone name, the tier, the discount rationale, and the amount due must be arranged in a specific form. The two sides of this boundary are not equal — one carries the rule and the data, the other displays it. The next lesson compares three separate distributions of this responsibility: how many of the field names the displaying file recognizes, how many separate places formatting is done in, and how many files a display change touches.
To keep your progress and take notes, Log in
My notes
Log in to take notes.