Skip to content
academia.sh

Lesson 09 / 09

Deployment

What is inside a release that goes to production is asked with three separate questions. The lesson measures the asset name, the release directory, and the release manifest one by one, tests an atomic switch and a rollback with a symbolic link, and gathers the course's nine measurements in one table.

Published Updated

In this lesson

  • Measures that the asset name comes from the content digest and that its determinant is the code
  • Shows that the release directory name comes from the clock and that its determinant is the environment
  • Tests an atomic switch and a rollback with a symbolic link in a temporary directory
  • Explains that the three parts of the same release have three separate determinants
Contents

The previous lesson showed that the hot path comes from the call count and that time comes from the environment. In hand there is a measured, tested, packaged application; what remains is taking it to production. This ties back to the course’s measuring axis too: what determines this result? What goes to production is not a single object. A release has three parts, and each has a different determinant.

Deployment is moving a release to the target machine and putting it into effect. M12/K05 covered the stages and tools of deployment; this lesson does not repeat the stages, it measures what the three values inside a release are produced by. The core’s determinant.rb file and this lesson’s excerpt deployment.rb are given below as they are; both are source and have no output. The core is a model: the clock, the core count, and the contents of the lock file come from the definition.

# determinant.rb — a run model and a helper that classifies what determines a result

# Run: the three inputs a result may depend on. Everything outside these three is code.
Run = Data.define(:lock, :seed, :environment)

BASE = Run.new(
  lock: { "formatter" => "1.4.2", "parser" => "2.0.1" },
  seed: 1,
  environment: { clock: Time.utc(2026, 10, 16, 9, 0), cores: 4, variables: {} }
)

# Each variant changes a single input; the others stay as in the base.
VARIANTS = {
  dependency: BASE.with(lock: { "formatter" => "1.5.0", "parser" => "2.1.0" }),
  order: BASE.with(seed: 2),
  environment: BASE.with(environment: { clock: Time.utc(2027, 3, 1, 9, 0), cores: 8,
                                        variables: { "WORKER_COUNT" => "3" } })
}

# Rule: the computation runs first in the base, then once in each variant. The first variant
# that changes the result (dependency, order, environment, in that order) is the determinant;
# if none changes it, code.
def determinant(computation)
  base = computation.call(BASE)
  changed = VARIANTS.to_h { |name, run| [name, computation.call(run) != base] }
  kind = changed.key(true)&.to_s || "code"
  evidence = changed.map { |name, c| "#{name[0].upcase}#{c ? '+' : '-'}" }.join(" ")
  [kind, evidence, base]
end
# deployment.rb — the asset name carries the content digest; the release installs the versions in the lock
require "digest"

# Deployment: the asset name carries a digest of its content; the release installs the versions in the lock.
def asset_name(name, content) = name.sub(/(\.\w+)\z/) { "-#{Digest::SHA256.hexdigest(content)[0, 8]}#{Regexp.last_match(1)}" }

def release_manifest(run)
  [asset_name("app.css", "body { margin: 0 }"), *run.lock.map { |name, v| "#{name}-#{v}" }]
end

The Asset Name Comes From the Content Digest

In production, a stylesheet or a script is published with the digest of its content in its name. When the content changes, the name changes, and the old copy in the browser becomes invalid on its own. asset_name inserts the first eight characters of the content’s SHA-256 digest before the file name’s extension. The block below calls the same file name with two different contents and runs the rule.

# asset.rb — the asset name depends on the content
require_relative "determinant"
require_relative "deployment"

puts asset_name("app.css", "body { margin: 0 }")
puts asset_name("app.css", "body { margin: 1px }")
p determinant(->(_r) { asset_name("app.css", "body { margin: 0 }") })
app-997facec.css
app-42043fc8.css
["code", "D- O- E-", "app-997facec.css"]

When 0 in the content became 1px, the name changed. The rule saw a different name in none of the three variants: the versions in the lock file, the seed, and the environment did not affect the name. The determinant is code; the name comes from the content’s digest, and the content is part of the code. RU17. The first eight characters of the digest do not reduce the chance of a collision to zero; this measurement shows separate names for two contents and gives no guarantee of separate names for any two contents.

The Release Directory Comes From the Clock

Each release is placed in its own directory on the target machine and carries the release time as its name. That way the old release stays next to it without being deleted. The directory name is built from the clock, and in the model the clock is an input of the environment.

# release_dir.rb — the release directory is built from the clock
require_relative "determinant"

release_dir = ->(clock) { "releases/#{clock.strftime('%Y%m%d%H%M%S')}" }
p determinant(->(r) { release_dir.call(r.environment[:clock]) })
["environment", "D- O- E+", "releases/20261016090000"]

The base clock is 16 October 2026, nine zero zero, and the directory name carries this time. The rule ran this computation in the three variants: only the environment’s clock was different, and the result changed. The evidence is D- O- E+ and the determinant is environment. The computation is deterministic, but its input is the clock, and the clock comes from outside. RU18. The real clock was not used in this measurement; the core’s fixed clock was used, and in a real deployment the name is different for every release.

The Manifest Carries the Versions in the Lock

The release manifest says which asset name and which package versions the release contains. The asset name comes from the code; the package versions come from the lock file.

# manifest.rb — the release manifest carries the versions in the lock
require_relative "determinant"
require_relative "deployment"

p release_manifest(VARIANTS[:dependency])
p determinant(->(r) { release_manifest(r) })
["app-997facec.css", "formatter-1.5.0", "parser-2.1.0"]
["dependency", "D+ O- E-", ["app-997facec.css", "formatter-1.4.2", "parser-2.0.1"]]

The first line is the manifest of the variant whose lock is 1.5.0 and 2.1.0: the asset name stays the same and the package versions change. The second line runs the rule. The rule sees the difference in the lock variant, and the determinant is dependency. In the same manifest the asset name comes from the code, but the rule gives the first determinant that changes the result, and when what changes it is the dependency, the code stays in the background. The evidence is D+ O- E-. RU19. The rule gives a single determinant; if several inputs change at once, the first one that changes is chosen and the others show up in the evidence column.

When a new release is ready, the release in effect is pointed to by a symbolic link (symlink); the switch is changing the link in one step. We test this in a temporary directory: two release directories, one link, an atomic switch, and a rollback. The link is first created under a temporary name, then moved to the real name with File.rename; renaming is a single step, so at no moment in between is the link missing.

# switch.rb — atomic switch and rollback with a symbolic link
require "tmpdir"

def link(target)
  File.symlink(target, "current.tmp")
  File.rename("current.tmp", "current")
end

Dir.mktmpdir do |dir|
  Dir.chdir(dir) do
    Dir.mkdir("releases")
    Dir.mkdir("releases/20261016090000")
    Dir.mkdir("releases/20261016100000")
    link("releases/20261016090000")
    puts File.readlink("current").split("/").last
    link("releases/20261016100000")
    puts File.readlink("current").split("/").last
    link("releases/20261016090000")
    puts File.readlink("current").split("/").last
  end
end
20261016090000
20261016100000
20261016090000

The link first pointed to the nine zero zero release, then to the ten zero zero release; the rollback created a new link and went back to nine zero zero. Rolling back is not recreating the old directory; since the old directory was never deleted, only the link is changed. Before the switch, a health check looks at whether the new release responds and makes the decision: if both requests succeed, switch; if one fails, roll back.

# health.rb — the health check decides on a switch or a rollback
decide = ->(codes) { codes.all?(200) ? "switch" : "roll back" }
puts decide.call([200, 200])
puts decide.call([200, 503])
switch
roll back

When both requests return two hundred the decision is to switch, and when one returns five hundred three it is to roll back. RU20. The directories and the link were tested in a temporary directory and in a single process; requests arriving over a network and processes reading at the same time are not in this measurement. The extra machinery needed for zero-downtime deployment was not measured in this lesson.

The Bounding Measurement

The three parts of the same release gave three separate answers to three separate questions. The block below asks the three at the same time.

# release.rb — three parts of the same release, three determinants
require_relative "determinant"
require_relative "deployment"

names = {
  "asset name" => ->(_r) { asset_name("app.css", "body { margin: 0 }") },
  "release directory" => ->(r) { "releases/#{r.environment[:clock].strftime('%Y%m%d%H%M%S')}" },
  "manifest" => ->(r) { release_manifest(r) }
}
names.each { |name, computation| puts "#{name}: #{determinant(computation).first}" }
asset name: code
release directory: environment
manifest: dependency

The code determines the asset name, the environment determines the release directory, and the dependency determines the manifest. The three belong to the same release; tying a release to a single determinant is wrong. In the manifest the rule gave the dependency, not the code: although the manifest is the product of both the code and the lock, determinant picks the first input that changes, and the lock comes before the code in the rule. This lesson’s observation is the release manifest row, and its determinant is dependency.

Summary

  • The content’s digest determines the asset name; since the content comes from the code, the determinant is code.
  • The name of the release directory comes from the clock; the determinant is environment.
  • The release manifest carries the versions in the lock; the determinant is dependency and its evidence is D+ O- E-.
  • A symbolic link and File.rename make the switch atomic; rolling back is creating a new link without deleting the old directory.
  • The three parts of the same release have three separate determinants.

Course Wrap-Up

Every lesson of the course asked the same question: what determines this result. The table puts side by side the value each of the nine lessons measured, that lesson’s determinant, and what its bounding measurement said. The determinant column is the one written in each lesson’s ## Summary section.

Lesson Measured value Determinant Bounding measurement
Gem Structure and Publishing package file name gauge-0.3.0.gem code in the definition that gathers the file list from the working directory, both [] and ["lib/gauge.rb"] give code
Dependency Management locked version 1.4.2 dependency ~> 1.4.0 with a 1.5.0 lock raises ArgumentError; the rule does not classify a computation that raises
Testing Frameworks test result [] order when every test uses settings.dup, the result is ["code", "D- O- E-", []]
Mocking and Test Data clock-reading test true environment seeded data is ["order", "D- O+ E-", [37, 12, 72]]
The Concurrency Model unlocked counter 6 order a Ractor removes shared state; freezing stays shallow
Process-Based Scaling worker count 4 environment the slice total in a child process is ["code", "D- O- E-", 155]
Memory Management allocated objects 1001 code the first run carries a warm-up share
Profiling hot path [:format_money, 200] code time is environment, the call count is code
Deployment release manifest dependency three parts of the same release, three determinants

The distribution of the nine lessons’ determinants is 3 code · 2 dependency · 2 order · 2 environment. This is the same as the distribution of the core’s nine observations, because the core carries one observation for each lesson. Two results can be read from the table. First, even two lessons that look at the same code can give different determinants: the unlocked counter depends on order, the worker count on the environment. Second, most of the bounding measurements showed that another determinant was working next to the lesson’s determinant; a release, a test, or a measurement is not tied to a single source. RU21. The table gives the distribution of this course’s nine observations; it carries no claim about the distribution for Ruby applications as a whole.

The course closes here, and M13 closes with it. The curriculum asked four questions. The first: who answers this message. The second: who wrote this method. The third: how many requests does this value stay for. The fourth: what determines this result. Each question entered the gap the previous one left: finding who answers required asking who wrote, finding who wrote required asking how long it stays, and finding how long it stays required asking where the result comes from. This course answered the last one only by measuring; the question of why a result is tied to a particular source remained beyond measurement.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close