Skip to content
academia.sh

Lesson 12 / 21

Two-Environment Deployment

Rollback's round cost and the standing bill of keeping two full environments up are counted together: the rollback window is scanned, sessions and long-running jobs cut off at the moment of cutover are measured, and the rows that rollback fails to undo in the shared data layer are shown.

Published Updated

In this lesson

  • Compares the two-environment scheme against the staged scheme on the same release sequence: replica-minute cost and average rollback round
  • Scans the rollback window to price, in replica-minutes, the share of releases that can be rolled back with a single switch
  • Counts the state-carrying work cut off at the moment of cutover, and the rows the shared data layer's rollback does not cover
Contents

The previous measurement leaned on one assumption: rollback is instant and free. The moment the gate said “roll back,” nothing happened in the model. This lesson lifts that assumption. Rollback has a round, a price, and a limit — and most of that price gets paid even when rollback never turns out to be needed.

Two-environment deployment’s promise fits one sentence: the new version stands up beside the old one, traffic switches over with a single flip, and if something goes wrong, the switch flips back. What that promise costs is that two full environments doing the same work stand together for a while. In this lesson the gate is the rollback decision: how many rounds the decision takes, under what condition it drops to a single round, and what that single round fails to cover are measured.

  • RS19. The fictional regional measurement network’s work-order service runs with sixteen copies; sixty releases are measured, and one deployment round is three minutes. Both the network and the figures are fictional.
  • RS20. Thirty percent of releases are defective. The defect’s time-to-appear is exponentially distributed, with a mean of twenty-five minutes. Which metric surfaces the defect is a separate measure’s subject; only the moment of appearance is modeled here.
  • RS21. The second environment stands up twelve minutes before the switch flips, and stays up for the rollback window. Once the window closes, rollback means a fresh release: five rounds.
  • RS22. The staged scheme climbs five steps in fifty minutes with four extra copies; rollback takes as many rounds as the steps climbed by then.
  • RS23. At the moment of cutover there are three classes of work unit, and their durations are exponentially distributed. Session affinity carrying over to the new environment is not modeled; what gets measured is how many are cut off when it does not carry over.
  • RS24. The data layer is shared between the two environments. The write rate is 140 rows per minute; 35% of rows are updated after rollback. Migration ordering and backward-compatible writes are not this lesson’s subject.

Measurement Setup

What is measured is not a real cluster but a model set up with node. The model carries three separate things: a release sequence’s resource cost and rollback rounds, the work units in flight at the moment the switch flips, and the rows written to the shared data layer. The random generator is written by hand, and its seed is visible.

// twoenv.mjs — cost and cutover model of two-environment deployment; not a real cluster, a model
export const SEED = 20260904;
export const COPIES = 16, RELEASES = 60, ROUND_MIN = 3, PREP_MIN = 12, REDEPLOY_ROUNDS = 5;
export const STAGED_EXTRA = 4, STAGED_DURATION = 50, STAGED_STEPS = 5;
export const DEFECT_MEAN = 25, WINDOW_MIN = 30;    // the defect's time-to-appear is exponentially distributed

export function generator(seed) {                  // linear congruential generator, seed visible
  let x = seed >>> 0;
  return () => { x = (Math.imul(x, 1664525) + 1013904223) >>> 0; return x / 4294967296; };
}
export const exponential = (rnd, mean) => -mean * Math.log(1 - rnd());

// Release sequence: whether each release is defective, and in which minute the defect appears.
export function releases(rnd, brokenRate = 0.30) {
  return Array.from({ length: RELEASES }, (_, i) => ({
    no: i + 1, broken: rnd() < brokenRate, appear: exponential(rnd, DEFECT_MEAN),
  }));
}

// Two-environment scheme: the old environment is kept up for the window after the switch is flipped.
export function twoEnv(list, window) {
  let replicaMin = 0, round = 0, inWindow = 0, outOfWindow = 0;
  for (const y of list) {
    replicaMin += COPIES * (PREP_MIN + window);    // how long the second environment stays up
    if (!y.broken) continue;
    if (y.appear <= window) { round += 1; inWindow += 1; }
    else { round += REDEPLOY_ROUNDS; outOfWindow += 1; }
  }
  const broken = list.filter((y) => y.broken).length;
  return { name: `two-environment (window ${window} min)`, replicaMin, inWindow, outOfWindow,
    avgRound: broken ? round / broken : 0, rollbackMin: broken ? round * ROUND_MIN / broken : 0 };
}

// Staged scheme: the extra copies stay up for the rollout's duration; rollback takes as many rounds as were climbed by then.
export function staged(list) {
  let replicaMin = 0, round = 0;
  for (const y of list) {
    replicaMin += STAGED_EXTRA * STAGED_DURATION;
    if (!y.broken) continue;
    const step = Math.min(STAGED_STEPS, 1 + Math.floor(y.appear / (STAGED_DURATION / STAGED_STEPS)));
    round += step;
  }
  const broken = list.filter((y) => y.broken).length;
  return { name: 'staged (single environment)', replicaMin, inWindow: 0, outOfWindow: broken,
    avgRound: broken ? round / broken : 0, rollbackMin: broken ? round * ROUND_MIN / broken : 0 };
}

// The moment of cutover: units in flight when the switch is flipped. Three classes, exponential duration.
export const CLASSES = [
  { name: 'short request', rate: 300, mean: 0.04 },  // rate: units starting per minute
  { name: 'session', rate: 8, mean: 12 },
  { name: 'long-running job', rate: 0.5, mean: 25 },
];

export function inFlight(rnd, cls, T = 240) {       // T minutes of warm-up, switch flips at T
  const open = [];
  for (let min = 0; min < T; min += 1) {
    for (let k = 0; k < cls.rate; k += 1) {
      const start = min + rnd(), duration = exponential(rnd, cls.mean);
      if (start + duration > T) open.push({ start, end: start + duration });
    }
  }
  return open;
}

// Shared data layer: the new version writes in the new format, the rollback code reverts, it does not revert rows.
export function dataLayer(rnd, openMin, writeRate = 140, updateRate = 0.35) {
  let newFormat = 0, laterUpdated = 0;
  for (let min = 0; min < openMin; min += 1) {
    for (let k = 0; k < writeRate; k += 1) {
      newFormat += 1;
      if (rnd() < updateRate) laterUpdated += 1;    // updated by the old version after rollback
    }
  }
  return { newFormat, lost: laterUpdated, silent: newFormat - laterUpdated };
}

The model’s setting is window: how many minutes the old environment stays up after the switch flips. This single number sets both rollback’s round cost and the scheme’s standing bill — and it sets them in opposite directions.

Resource Cost and Rollback Round

// run1.mjs — the resource cost and rollback round cost of two schemes; the rollback window is scanned
import { generator, releases, twoEnv, staged, SEED, COPIES, RELEASES, ROUND_MIN, WINDOW_MIN, DEFECT_MEAN } from './twoenv.mjs';

const l = releases(generator(SEED));
const broken = l.filter((y) => y.broken).length;
console.log(`seed ${SEED} | ${RELEASES} releases, ${COPIES} copies, ${broken} releases defective`);
console.log(`the defect's time-to-appear is exponential, mean ${DEFECT_MEAN} min | one round ${ROUND_MIN} min\n`);
console.log('scheme                             replica-min  in window  out of window  avg round  avg rollback');
for (const s of [twoEnv(l, WINDOW_MIN), staged(l)]) {
  console.log(`${s.name.padEnd(34)} ${String(s.replicaMin).padEnd(12)} ${String(s.inWindow).padEnd(10)} ` +
    `${String(s.outOfWindow).padEnd(14)} ${s.avgRound.toFixed(2).padEnd(10)} ${s.rollbackMin.toFixed(1)} min`);
}
console.log('\nthe rollback window is scanned (two-environment scheme):');
console.log('window   replica-min  single switch  redeploy  avg round  avg rollback   extra replica-min');
for (const p of [0, 5, 15, 30, 60, 120, 240]) {
  const s = twoEnv(l, p), a = staged(l);
  console.log(`${`${p} min`.padEnd(8)} ${String(s.replicaMin).padEnd(12)} ${String(s.inWindow).padEnd(14)} ` +
    `${String(s.outOfWindow).padEnd(9)} ${s.avgRound.toFixed(2).padEnd(10)} ${`${s.rollbackMin.toFixed(1)} min`.padEnd(14)} ${s.replicaMin - a.replicaMin}`);
}
seed 20260904 | 60 releases, 16 copies, 18 releases defective
the defect's time-to-appear is exponential, mean 25 min | one round 3 min

scheme                             replica-min  in window  out of window  avg round  avg rollback
two-environment (window 30 min)    40320        11         7              2.56       7.7 min
staged (single environment)        12000        0          18             2.78       8.3 min

the rollback window is scanned (two-environment scheme):
window   replica-min  single switch  redeploy  avg round  avg rollback   extra replica-min
0 min    11520        0              18        5.00       15.0 min       -480
5 min    16320        3              15        4.33       13.0 min       4320
15 min   25920        8              10        3.22       9.7 min        13920
30 min   40320        11             7         2.56       7.7 min        28320
60 min   69120        17             1         1.22       3.7 min        57120
120 min  126720       18             0         1.00       3.0 min        114720
240 min  241920       18             0         1.00       3.0 min        229920

The top table puts a limit on the promise. At a thirty-minute window, only eleven of the eighteen defective releases were rolled back with a single switch; seven were noticed after the window closed and needed a five-round redeploy. The average rollback round is 2.56, and 2.78 in the staged scheme. The difference between the two schemes at this setting is eight percent. In exchange, the two-environment scheme burned three times the resources: 40,320 replica-minutes against 12,000.

The phrase “instant rollback” becomes a number once the window is kept longer than the defect’s time-to-appear. The scan shows the price. At a sixty-minute window, seventeen of the eighteen defects were rolled back in a single round, and the average round dropped to 1.22 — an extra 57,120 replica-minutes. At one hundred twenty minutes, every one of them dropped to a single round, at an extra cost of 114,720 replica-minutes. Every round gained carries a price: at the thirty-minute window, forty-four rounds were shaved off, and that cost 655 replica-minutes per round.

The first row says the same thing from the other direction. At a zero-minute window, the two-environment scheme is cheaper than the staged scheme (11,520 against 12,000), because the second environment stands up only for the twelve-minute prep. But in that scheme, not one release could be rolled back with a single switch; the average round climbed to five. Shutting down the old environment shuts down two-environment deployment’s one reason to exist.

Work Cut Off at the Moment of Cutover

// run2.mjs — state-carrying requests cut off at the moment of cutover, and the shared data layer
import { generator, inFlight, dataLayer, CLASSES, SEED, COPIES } from './twoenv.mjs';

const open = CLASSES.map((s) => ({ s, list: inFlight(generator(SEED + s.rate), s) }));
console.log('units in flight at the moment the switch is flipped:');
for (const { s, list } of open) console.log(`  ${s.name.padEnd(18)} ${String(list.length).padStart(5)} units (average duration ${s.mean} min)`);

console.log('\nthe drain duration is scanned: a unit still running at the end of the duration gets cut');
console.log('drain     short request  session  long-running job  total cut  two environments together');
for (const d of [0, 0.5, 1, 5, 15, 30]) {
  const k = open.map(({ list }) => list.filter((u) => u.end > 240 + d).length);
  console.log(`${`${d} min`.padEnd(9)} ${String(k[0]).padEnd(15)} ${String(k[1]).padEnd(8)} ${String(k[2]).padEnd(17)} ` +
    `${String(k[0] + k[1] + k[2]).padEnd(10)} ${2 * COPIES * d} replica-min`);
}

const v = dataLayer(generator(SEED + 11), 11);
console.log('\nshared data layer: the switch was flipped, rollback happened at minute 11');
console.log(`  rows written in the new format: ${v.newFormat}`);
console.log(`  rollback round: 1 (code) | rows reverted for data: 0`);
console.log(`  rows updated by the old version after rollback, dropping the new field: ${v.lost}`);
console.log(`  rows still carrying the new field, invisible to the old version: ${v.silent}`);
units in flight at the moment the switch is flipped:
  short request         10 units (average duration 0.04 min)
  session               89 units (average duration 12 min)
  long-running job      24 units (average duration 25 min)

the drain duration is scanned: a unit still running at the end of the duration gets cut
drain     short request  session  long-running job  total cut  two environments together
0 min     10              89       24                123        0 replica-min
0.5 min   0               88       24                112        16 replica-min
1 min     0               86       24                110        32 replica-min
5 min     0               68       20                88         160 replica-min
15 min    0               31       14                45         480 replica-min
30 min    0               7        5                 12         960 replica-min

shared data layer: the switch was flipped, rollback happened at minute 11
  rows written in the new format: 1540
  rollback round: 1 (code) | rows reverted for data: 0
  rows updated by the old version after rollback, dropping the new field: 491
  rows still carrying the new field, invisible to the old version: 1049

At the moment the switch flipped, 123 work units were in flight. Ten of them were short requests, eighty-nine sessions, twenty-four long-running jobs. A half-minute drain saved every short request — and only one session. A one-minute drain brought the eighty-nine down to eighty-six, and touched none of the long-running jobs.

The reason is a difference in scale. The drain duration is tuned to the short request’s lifetime; a session lives twelve minutes, a long-running job twenty-five — two to three orders of magnitude longer. Bringing 123 units down to twelve needs a thirty-minute drain, and for those thirty minutes, two versions write to the same data layer together. The cost of a seamless cutover is not just 960 replica-minutes; it is a half hour during which two versions write at once.

One more calculation is needed. If every cutover cuts off eighty-nine sessions, then wrongfully rolling back a clean version means two cutovers: 178 sessions and forty-eight long-running jobs. The cost of a wrongful rollback can outweigh the cost of the defect itself — and how often a wrongful rollback happens depends not on the rollback decision, but on the setting of the gate that came before it.

What Rollback Does Not Undo

The last block shows the single switch’s limit. The switch flipped, the defect was seen at minute eleven, rollback completed in one round. The code returned to the old version. In those eleven minutes, 1,540 rows had been written to the shared data layer in the new format; rollback undid zero of them.

There are two separate kinds of damage. 491 rows were updated by the old version after rollback; because the old version does not recognize the new field, that field dropped during the write and the information was lost for good. The remaining 1,049 rows keep carrying their new field; the old version does not see them, so they stay silent — but once the new version comes back, there will be a format split between these two sets.

Two-environment deployment reduces rolling back the code to a single round. When the data layer is shared, rollback’s scope is limited to code, and that limit never shows up in the round cost: a one-round rollback can leave behind a 1,540-row residue.

The Gate’s Three Numbers

What it catches: all eighteen defective releases were eventually rolled back. What only it catches: eleven releases rolled back in a single round at the thirty-minute window — possible only while the old environment is still standing — for a total of forty-four rounds saved, or 132 minutes. The marginal value’s price is 655 replica-minutes per round.

What it misses: the seven releases noticed after the window closed, and the rows written to the data layer during every rollback — 1,540 rows in the example release, 491 of them a permanent loss. What it wrongly holds: rolling back a clean version means two cutovers, 178 sessions, and forty-eight long-running jobs. The time it adds: twelve minutes of prep per release plus the window duration; up to thirty minutes of drain for a seamless cutover; 40,320 replica-minutes in total.

Summary

  • At a thirty-minute window, eleven of the eighteen defective releases were rolled back with a single switch, seven with a five-round redeploy; the average round is 2.56, and 2.78 in the staged scheme.
  • The resource cost is threefold: 40,320 replica-minutes against 12,000. Raising the window to one hundred twenty minutes drops every release to a single round, at an extra cost of 114,720 replica-minutes.
  • At a zero-minute window, the two-environment scheme comes out cheaper than the staged one, but no release remains rollback-able with a single switch: shutting down the old environment shuts down the pattern’s reason to exist.
  • At the moment of cutover, 123 work units were in flight. A half-minute drain saved every short request, and only one of eighty-nine sessions; getting down to twelve means a thirty-minute drain and two versions writing together for half an hour.
  • In the shared data layer, a one-round rollback undid zero of 1,540 rows; 491 of them were updated by the old version and lost for good.
  • Wrongfully rolling back a clean version means two cutovers, 178 sessions, and forty-eight interrupted long-running jobs; this number depends not on the rollback gate, but on the setting of the gate before it.

Next Step

Every measurement in this lesson rested on a single necessity: turning on new behavior requires putting the new version into traffic, and turning it off requires pulling it out. As long as releasing and enabling behavior are the same operation, rollback is always a deployment operation — it has a round, a cutover moment, and a data residue. These two can be separated. If behavior is carried in the code and kept off with a record, turning it on and off is no longer a deployment but a setting change; rollback’s round cost approaches zero. The next lesson measures this separation: the rounds it saves, the enabling decision’s own gate, and the debt every record left off accumulates over time.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close