---
title: 'The Ruby Ecosystem and Production'
source: 'https://academia.sh/en/courses/ruby-production'
language: en
updated: '2026-10-21T05:03:43+00:00'
license: 'CC BY-SA 4.0'
lessons: 9
outcomes:
    - 'Builds a package with its specification, version and dependencies, and measures which versions a version constraint accepts'
    - 'Explains how the lock file pins the installed versions and what an unlocked resolution depends on'
    - 'Measures how tests depend on order, the clock and shared state, and fixes it with isolation and mocking'
    - 'Distinguishes the limits of the concurrency model across threads, processes and Ractors'
    - 'Diagnoses memory and performance problems in production with object counts and call counts, and deploys a release so it can be rolled back'
    - 'Distinguishes by measurement whether a result is determined by the code, a dependency, the order or the environment'
---

# The Ruby Ecosystem and Production

A program that runs correctly on the developer's machine can give a different result in production: the code is the same, what changes is the installed package version, the order in which tests or threads run, the server's clock or an environment variable. The course measures every mechanism with a single question — what determines this result? Package structure, dependency management, testing frameworks, mocking, the concurrency model, process-based scaling, memory management, profiling and deployment all pass through the same frame.

## Packages and Testing

A package's specification, version and file name, version constraints and the lock file, test order shuffled by a seed and shared state, tests that take the clock and collaborators from outside, fake objects and repeatable test data.

### Gem Structure and Publishing

Source: https://academia.sh/en/courses/ruby-production/gem-structure-and-publishing

This course asks what determines a result. The lesson introduces the nine-observation measurement core, then measures the package specification, the origin of the file name, the dependency kinds and version comparison.

The previous course measured how long a value stays, and test isolation appeared at its close: two tests that shared the same application leaked through the persistent store between them. Once the application leaves the developer's machine and goes to production, a new question arises: why does the result change when the code is the same? The question of this course is **what determines this result?** The rule reads how the result behaves, not how the code is written.

M08/K06 also searched for the cause of a result, but there the cause was a call in the source, a comment or a default value, all of it inside the code. Here the question concerns an input **outside** the code. The package specification is written in the source and is code; the installed version is not written in the source.

The rule places a result in one of four classes. **Code** is the case where the computation looks only at its own input. **Dependency** is what the versions in the lock file pin. **Order** is the sequence in which tests or threads run. **Environment** is what the machine reports: the clock, the core count, the environment variable. These four classes are called the **determinant**. The measurement model is built from three inputs: `lock`, `seed` and `environment`. The `lock` input is the lock file's contents, the versions it pins. Everything outside these three is code.

#### The Measurement Core

The core is two files, and it is not rewritten during the course; excerpts are taken from it. `determinant.rb` carries the run model and the classifying helper; the block is source and has no output.

```ruby
# 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
```

`Run` holds the three inputs. `BASE` is a starting run; `VARIANTS` changes only one input at a time. `determinant(computation)` runs the computation in the base and in the three variants and returns the name of the first variant that changes the result; if none changes it, the result is `"code"`. The evidence column has the form `D± O± E±`: whether the result changed in the dependency, order and environment variants. The second file carries the small models of the ecosystem and the nine observations.

```ruby
# observations.rb — nine observations: what determines this result
require "digest"
require_relative "determinant"

# Package specification: the name and version give the package its file name.
def package_spec(files = ["lib/gauge.rb", "lib/gauge/counter.rb"])
  Gem::Specification.new do |s|
    s.name = "gauge"
    s.version = "0.3.0"
    s.summary = "a small measurement package"
    s.authors = ["sample author"]
    s.files = files
    s.add_dependency "formatter", "~> 1.4"
  end
end

# Dependency resolution: if the version in the lock satisfies the constraint, that version is installed.
def installed_version(run, name, constraint)
  version = Gem::Version.new(run.lock.fetch(name))
  raise ArgumentError, "lock does not satisfy constraint" unless Gem::Requirement.new(constraint).satisfied_by?(version)

  version.to_s
end

# Test runner: tests run in an order shuffled by the seed and share the same settings.
TESTS = {
  "price_format" => ->(settings) { "12 #{settings[:currency]}" == "12 TRY" },
  "currency_conversion" => ->(settings) { settings[:currency] = "USD"; true },
  "invoice_total" => ->(settings) { settings[:currency] == "TRY" }
}

def run_tests(run, tests = TESTS)
  settings = { currency: "TRY" }
  order = tests.keys.shuffle(random: Random.new(run.seed))
  order.reject { |name| tests[name].call(settings) }
end

# Mocking: the trial period is 30 days; the clock is passed in from outside.
def in_trial?(start, clock) = clock - start < 30 * 86_400

# Scheduler model: two threads read the counter and write one more; a switch can happen
# between the read and the write. The seed picks which thread takes the next step.
def unlocked_counter(run, increments: 3)
  random = Random.new(run.seed)
  counter = 0
  jobs = Array.new(2) { { left: increments, read: nil } }
  loop do
    waiting = jobs.select { |j| j[:left].positive? }
    break if waiting.empty?

    job = waiting.sample(random: random)
    if job[:read].nil?
      job[:read] = counter
    else
      counter = job[:read] + 1
      job[:read] = nil
      job[:left] -= 1
    end
  end
  counter
end

# Process-based scaling: the worker count comes from an environment variable, or else from the core count.
def worker_count(environment) = Integer(environment[:variables].fetch("WORKER_COUNT", environment[:cores]))

# Memory: the block runs three times; the first runs are warm-up, the last run's allocations are counted.
def allocated_objects
  result = nil
  3.times do
    before = GC.stat(:total_allocated_objects)
    yield
    result = GC.stat(:total_allocated_objects) - before
  end
  result
end

# Profiling: calls to the methods the program defines are counted; the most called is the hot path.
def hot_path
  counts = Hash.new(0)
  trace = TracePoint.new(:call) { |t| counts[t.method_id] += 1 if t.defined_class == Object }
  trace.enable { yield }
  counts.max_by { |_, n| n }
end

def format_money(cents) = format("%d.%02d", cents / 100, cents % 100)
def format_line(line) = "#{line[:name]} #{format_money(line[:unit])} #{format_money(line[:total])}"
def report(lines) = lines.map { |l| format_line(l) }.join("\n")

# 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

lines = Array.new(100) { |i| { name: "product#{i}", unit: 1250, total: 1250 * (i + 1) } }
observations = {
  "package file name" => ->(_r) { package_spec.file_name },
  "locked version" => ->(r) { installed_version(r, "formatter", "~> 1.4") },
  "test result" => ->(r) { run_tests(r) },
  "clock-reading test" => ->(r) { in_trial?(Time.utc(2026, 10, 1), r.environment[:clock]) },
  "unlocked counter" => ->(r) { unlocked_counter(r) },
  "worker count" => ->(r) { worker_count(r.environment) },
  "allocated objects" => ->(_r) { allocated_objects { Array.new(1000) { |i| i.to_s } } },
  "hot path" => ->(_r) { hot_path { report(lines) } },
  "release manifest" => ->(r) { release_manifest(r) }
}

tally = { "code" => 0, "dependency" => 0, "order" => 0, "environment" => 0 }

printf("%-24s%-13s%-10s%s\n", "observation", "determinant", "evidence", "base")
observations.each do |name, computation|
  kind, evidence, base = determinant(computation)
  tally[kind] += 1
  printf("%-24s%-13s%-10s%s\n", name, kind, evidence, base.inspect)
end

puts
tally.each { |kind, n| printf("%-13s%d\n", kind, n) }
puts "total observations: #{tally.values.sum}"
```

```
observation             determinant  evidence  base
package file name       code         D- O- E-  "gauge-0.3.0.gem"
locked version          dependency   D+ O- E-  "1.4.2"
test result             order        D- O+ E-  []
clock-reading test      environment  D- O- E+  true
unlocked counter        order        D- O+ E-  6
worker count            environment  D- O- E+  4
allocated objects       code         D- O- E-  1001
hot path                code         D- O- E-  [:format_money, 200]
release manifest        dependency   D+ O- E-  ["app-997facec.css", "formatter-1.4.2", "parser-2.0.1"]

code         3
dependency   2
order        2
environment  2
total observations: 9
```

The 3/2/2/2 distribution in the output is the map of this course. Three observations are code, two are dependency, two are order and two are environment. Each lesson measures one observation in detail; when the course ends, all nine have been opened. `installed_version` is not a dependency resolver but a model that reads the version in the lock file. Among the nine rows, the observation of this lesson is the first: `package file name`, code. **PG1.** The core is not a real package ecosystem but a model of it; every block measured in this course is plain Ruby and the standard library.

#### Package Specification and File Name

A **package** carries its code, name, version and dependencies as one unit. What defines them is the **package specification**; in Ruby this is the `Gem::Specification` class. The `package_spec` helper in the core builds a small specification. The next file takes the same specification exactly; since `determinant` is not needed, it only calls the specification.

```ruby
# package.rb — package specification: the name and version give the package its file name
# Package specification: the name and version give the package its file name.
def package_spec(files = ["lib/gauge.rb", "lib/gauge/counter.rb"])
  Gem::Specification.new do |s|
    s.name = "gauge"
    s.version = "0.3.0"
    s.summary = "a small measurement package"
    s.authors = ["sample author"]
    s.files = files
    s.add_dependency "formatter", "~> 1.4"
  end
end
```

Now the file name and the full name of the same specification are examined. To make the version `0.3.0.pre1`, the `version` field is changed; a pre-release is a version that comes before the stable version.

```ruby
# file_name.rb — the package's file name comes from the name and version
require_relative "package"

s = package_spec
p s.file_name
p s.full_name

s.version = "0.3.0.pre1"
p s.file_name
p s.full_name
```
```
"gauge-0.3.0.gem"
"gauge-0.3.0"
"gauge-0.3.0.pre1.gem"
"gauge-0.3.0.pre1"
```

The file name has the form `name-version.gem`. `file_name` carries the extension and `full_name` does not. With a pre-release, the suffix becomes part of the version string and enters the file name. This explains why the rule sees code: the name and version are written in the source and do not come from the lock file, the seed or the clock. **PG2.** A single package specification was measured here; that a real specification with many fields follows the same rule was not tested.

#### Runtime and Development Dependency

A package carries two kinds of dependency. A **runtime dependency** is what arrives together when the package is installed. A **development dependency** is what only the person developing the package needs, such as a checking tool used for tests. In the package specification the two are written with separate methods.

```ruby
# dependencies.rb — runtime and development dependency
require_relative "package"

s = package_spec
s.add_development_dependency "checker"
p s.dependencies.map { |d| [d.name, d.requirement.to_s, d.type] }
p s.runtime_dependencies.map(&:name)
```
```
[["formatter", "~> 1.4", :runtime], ["checker", ">= 0", :development]]
["formatter"]
```

The first line lists the two dependencies with their version constraints and kinds. The constraint of the development dependency is `>= 0`: no version limit was written. The second line gives those that will be installed with the package, and only `formatter` remains. Whoever installs the package does not receive `checker`. **PG3.** The lock file pins the version of a dependency and the constraint only bounds it; measuring this is the subject of the next lesson.

#### A Version Is Not a String

A version number consists of numbers separated by dots, but its comparison does not work like a string comparison. Version `0.10.0` comes after `0.3.0`; compared as strings, `1` meets `3` at the first differing character and the order reverses.

```ruby
# version.rb — a version is not compared like a string
p "0.10.0" > "0.3.0"
p Gem::Version.new("0.10.0") > Gem::Version.new("0.3.0")

pre_release = Gem::Version.new("1.0.0.pre1")
p pre_release < Gem::Version.new("1.0.0")
p pre_release.prerelease?

begin
  Gem::Version.new("one.two")
rescue => e
  p e.class
end
```
```
false
true
true
true
ArgumentError
```

The string comparison gives `false` and the version comparison gives `true`. A pre-release is smaller than the stable version with the same number, and `prerelease?` reports that it is a pre-release. A string that does not start with a number (`"one.two"`) is not accepted as a version, and `ArgumentError` is raised; a part with letters such as `pre1` raises no error when it comes after the numbers and is read as a pre-release label. **Semantic versioning** splits a version into three numbers, major, minor and patch: the major version rises with an incompatible change, the minor version with a compatible new feature and the patch version with a bug fix. This is a written contract; Ruby does not verify it. **PG4.** The measurement shows how the version rule performs the comparison; it does not show that a package actually follows semantic versioning.

#### Publishing

Producing the package and sending it to the package registry are two separate jobs. The first makes the `.gem` file from the package specification, and the second uploads the file to the public registry. The sandbox has neither the package command nor a network, so the two commands below are not run.

```text
gem build gauge.gemspec
gem push gauge-0.3.0.gem
```

`gem build` takes the file name from the package specification; it is the same name as the measured `file_name`. The content of a version published with `gem push` cannot be changed afterward. A faulty version can be removed from the registry index with `gem yank`, but the copy held by those who already downloaded that version stays in place; the corrected content is released under a new version number. If a password or a key entered the package by mistake, removal is not enough and that secret is changed at once. This is why pre-releases exist: trying with `0.3.0.pre1` before publishing protects the stable version.

#### The Bounding Measurement

The rule sees only three inputs: lock, seed and environment. The working directory is not one of them. The measurement below shows this limit. The package's file list is read from the directory with `Dir.glob`; the computation is classified first while the directory is empty and then after a file is written.

```ruby
# boundary.rb — when the file list comes from the working directory
require_relative "determinant"
require_relative "package"

computation = ->(_r) { package_spec(Dir.glob("lib/**/*.rb")).files }
p determinant(computation)

Dir.mkdir("lib")
File.write("lib/gauge.rb", "module Gauge; end\n")
p determinant(computation)
```
```
["code", "D- O- E-", []]
["code", "D- O- E-", ["lib/gauge.rb"]]
```

The result was `code` both times, yet the file list changed: first empty, then `lib/gauge.rb`. The rule saw no difference in any of the three variants, because none of them touches the directory. The result actually depends on the working directory. Since the directory is not an input of the model, the rule names this dependency **code**, and that name is misleading: what changed is not the code itself but the file system the code reads. **PG5.** If this limit is left unmarked, the sentence "this result depends on the code" appears stronger than it is.

#### Summary

- The question of this course is "what determines this result?"; the rule classifies a result as code, dependency, order or environment, and the distribution is 3/2/2/2.
- The determinant of this lesson is **code**: the package's file name `gauge-0.3.0.gem` comes from the name and version, and the `package file name` row shows `D- O- E-`.
- A runtime dependency is installed with the package and a development dependency is not; a pre-release enters the file name.
- A version is not compared as a string; the `Gem::Version` rule says `0.10.0 > 0.3.0`.
- The content of a published version cannot be changed; a package specification that reads its file list from the directory depends on an input the rule does not see.

#### Next Step

What determines the package's file name is now visible. But when a package is installed, which version of the dependency inside it will arrive is not written in the package specification. The next lesson, Dependency Management, measures why the installed version does not change while the lock file stays the same.

### Dependency Management

Source: https://academia.sh/en/courses/ruby-production/dependency-management

Why does the installed version not change while the lock file stays the same? The lesson measures which versions a version constraint accepts, shows that resolution without a lock file depends on the registry, and that the dependency is the determinant.

The previous lesson showed that the package's file name comes from the name and version: that result was code. The package specification also declared a dependency, but the version to be installed was not written there. The question of this lesson is therefore one step further: why does the installed version not change as long as the lock file stays the same, and what happens when the lock file changes?

The question of the course is the same: **what determines this result?** The observation of this lesson is the `locked version` row. In M12/K02 the question was where a change reaches; there it was measured which layers a change spreads to. Here the question is reversed: which input does the installed version depend on? The core's `determinant.rb` file is given again exactly; the block is source and is pulled in with `require_relative`.

```ruby
# 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
```

#### Version Constraint

A **dependency manager** resolves which versions a package will work with through a **version constraint**. A constraint is not a version but an accepted range. Three forms are seen. An exact version constraint accepts a single version. A range constraint accepts what lies between two bounds: `>= 1.4` and `< 3` are written together. The third is the **pessimistic constraint**: the notation `~> 1.4` means "not less than 1.4, but with the major version unchanged". The constraint allows the last written digit to rise and does not allow the digit before it to rise; for `~> 1.4` the upper bound is `2`.

The file below applies the three forms to the same version list. `Gem::Requirement` builds the constraint and `satisfied_by?` says whether a version is accepted.

```ruby
# constraints.rb — which versions a version constraint accepts
def accepted(constraints, versions)
  requirement = Gem::Requirement.new(*constraints)
  versions.select { |v| requirement.satisfied_by?(Gem::Version.new(v)) }
end

list = %w[1.3.9 1.4.0 1.4.2 1.5.0 1.99 2.0.0]
p accepted(["~> 1.4"], list)
p accepted(["~> 1.4.0"], list)
p accepted([">= 1.4", "< 3"], %w[1.3.9 1.4.0 2.9 3.0])

dependency = Gem::Dependency.new("formatter", "~> 1.4")
p dependency.match?("formatter", "1.9.0")
p dependency.match?("formatter", "2.0.0")
```
```
["1.4.0", "1.4.2", "1.5.0", "1.99"]
["1.4.0", "1.4.2"]
["1.4.0", "2.9"]
true
false
```

`~> 1.4` keeps the major version at 1 and accepts everything up to `1.99`: the minor version may rise. `~> 1.4.0` is one digit longer and the accepted range narrows: only `1.4.0` and `1.4.2`. **The last digit determines the width of the constraint.** The range constraint is the intersection of two bounds and leaves `3.0` out. `Gem::Dependency` carries the name and the constraint together; it accepts `1.9.0` and rejects `2.0.0`. If the semantic versioning contract holds, no incompatible change arrives as long as the major version stays the same; but the author of the package gives this contract, and the constraint does not check it. **PG6.** This measurement shows which versions the constraint accepts; it does not show that an accepted version is actually compatible.

#### Resolution Without a Lock

When a constraint accepts more than one version, the manager has to pick one. The simplest rule is to take the highest matching version. The pure Ruby helper below is a small model of this resolution.

```ruby
# resolution.rb — the highest matching version from the registry when there is no lock file
def highest_matching(registry, constraint)
  requirement = Gem::Requirement.new(constraint)
  registry.map { |v| Gem::Version.new(v) }.select { |v| requirement.satisfied_by?(v) }.max.to_s
end
```

The helper is source. The file below applies it first to a registry of three versions and then to the registry after one version is added.

```ruby
# registry.rb — the same constraint, a changed registry
require_relative "resolution"

registry = %w[1.4.2 1.5.0 2.0.0]
p highest_matching(registry, "~> 1.4")

registry << "1.6.0"
p highest_matching(registry, "~> 1.4")
```
```
"1.5.0"
"1.6.0"
```

The constraint stayed the same, the registry changed, and the result moved from `"1.5.0"` to `"1.6.0"`. Resolution without a lock file depends on **the registry's contents at that moment**. A machine installed yesterday and a machine installed today can receive different versions with the same constraint. This is one of the sources of the question "it worked on my machine".

#### The Lock File

The remedy is to write the result of resolution to a **lock file**. The lock file holds the fully resolved versions of the constraints in the manifest. Every later installation does not resolve but reads the version in the lock file. The lock file is committed to the repository, because that is the only way to make all machines receive the same versions.

The core's `installed_version` is not a dependency resolver but a model that reads the version in the lock file. The file below takes its definition exactly.

```ruby
# dependency.rb — a model that reads the version in the lock file
# Dependency resolution: if the version in the lock satisfies the constraint, that version is installed.
def installed_version(run, name, constraint)
  version = Gem::Version.new(run.lock.fetch(name))
  raise ArgumentError, "lock does not satisfy constraint" unless Gem::Requirement.new(constraint).satisfied_by?(version)

  version.to_s
end
```

The model is measured in three places: with an exact version constraint, with a wide constraint and in a run whose lock file has changed. The last line shows how the rule classifies this observation.

```ruby
# lock_file.rb — the lock file pins the installed version
require_relative "determinant"
require_relative "dependency"

p installed_version(BASE, "formatter", "~> 1.4.0")
p installed_version(BASE, "formatter", "~> 1.4")
p installed_version(VARIANTS[:dependency], "formatter", "~> 1.4")
p determinant(->(r) { installed_version(r, "formatter", "~> 1.4") })
```
```
"1.4.2"
"1.4.2"
"1.5.0"
["dependency", "D+ O- E-", "1.4.2"]
```

Although the constraint `~> 1.4` is wide, the lock file says `1.4.2`, so the installed `"1.4.2"` stayed; the constraint accepts `1.5.0`, but the lock file did not choose it. When the lock file became `1.5.0`, the installed version became `"1.5.0"` too. The fourth line gives the classification: `dependency`, with the evidence `D+ O- E-`. The result changed in the dependency variant and did not change in the order and environment variants. **The constraint does not pin the version, it only bounds it; the lock file pins.** The determinant of this lesson is **dependency**, and throughout the lesson this word means the contents of the lock file. **PG7.** The measurement assumes that the version list in the lock file comes from a resolution; that the list was actually produced by a resolver is not part of the model.

#### Updating the Lock

The lock file does not change by itself. A new version arriving in the registry is not enough to renew the lock file; the lock file is written only when the manager resolves again. This is an explicit operation and is represented in the core by `BASE.with`: the lock file's contents are replaced with a new value and the old value stays as it was.

```ruby
# update.rb — the lock file changes only when it is updated explicitly
require_relative "determinant"
require_relative "dependency"
require_relative "resolution"

registry = %w[1.4.2 1.5.0 1.6.0]
latest = highest_matching(registry, "~> 1.4")
updated = BASE.with(lock: BASE.lock.merge("formatter" => latest))

p installed_version(BASE, "formatter", "~> 1.4")
p installed_version(updated, "formatter", "~> 1.4")
p BASE.lock
p updated.lock
```
```
"1.4.2"
"1.6.0"
{"formatter" => "1.4.2", "parser" => "2.0.1"}
{"formatter" => "1.6.0", "parser" => "2.0.1"}
```

Although `1.6.0` was in the registry, the lock file of `BASE` stayed at `1.4.2`; `installed_version` gave `"1.4.2"` in this run. The run with the renewed lock file gave `"1.6.0"`, and only `formatter` changed: `parser` stayed at `2.0.1` in the lock file. An update can move only the requested dependency and does not touch the others. This also explains why the dependency is a determinant: what changes the result is not the code but a write to the lock file. The code and the constraint stay the same; when the lock file changes, the result changes. **PG8.** The update was modeled here for a single dependency and with a pure registry; the joint update of dependencies that rely on each other was not measured.

#### The Constraints of Two Packages

An application installs more than one package, and each package writes its own constraint for the same dependency. The manager looks for a version that satisfies all constraints together: the intersection of the accepted ranges. If the intersection is empty, there is no resolution. The file below calls `accepted` with the constraints of two packages.

```ruby
# intersection.rb — the intersection of the constraints of two packages
def accepted(constraints, versions)
  requirement = Gem::Requirement.new(*constraints)
  versions.select { |v| requirement.satisfied_by?(Gem::Version.new(v)) }
end

registry = %w[1.4.0 1.4.2 1.5.0 1.99 2.0.0]
p accepted(["~> 1.4", ">= 1.5"], registry)
p accepted(["~> 1.4.0", ">= 1.5"], registry)
p accepted(["~> 1.4", "~> 2.0"], registry)

pre_release = Gem::Dependency.new("formatter", "~> 1.4")
p Gem::Requirement.new("~> 1.4").satisfied_by?(Gem::Version.new("1.5.0.pre1"))
p pre_release.match?("formatter", "1.5.0.pre1")
```
```
["1.5.0", "1.99"]
[]
[]
true
false
```

When the first package writes `~> 1.4` and the second writes `>= 1.5`, `1.5.0` and `1.99` remain in the registry. If the second writes `~> 1.4.0`, the intersection empties, because the `1.4.x` range and `1.5` onward meet at no version. In the third line two pessimistic constraints separate at the major version and again no version remains. An empty set is not a resolution: the manager does not install here and reports the conflict. Resolution means looking for a version that stays in the intersection of all constraints. This also shows what the lock file does not store: the lock file stores the result of resolution, but whether a resolution exists depends on the intersection of the constraints.

The last two lines separate the pre-release. The constraint object accepts `1.5.0.pre1` by looking only at the numbers. The dependency object does not count a pre-release as a candidate by default. The same constraint gave two results in two objects, so whether a pre-release will be installed depends on the object that asks the question, not on the constraint. **PG9.** The model of this lesson measures only the objects of the `Gem` module; the rule by which a real manager counts a pre-release as a candidate was not tested.

#### Two Machines, One Lock File

The reason for committing the lock file is measured with two machines. The first machine installs today, the second the next day when the registry has gained one more version. An installation without a lock file resolves again on every machine. An installation with a lock file does not resolve and reads the lock file.

```ruby
# machines.rb — installation without and with a lock file on two machines
require_relative "determinant"
require_relative "dependency"
require_relative "resolution"

today = %w[1.4.2 1.5.0]
tomorrow = today + ["1.6.0"]

p [highest_matching(today, "~> 1.4"), highest_matching(tomorrow, "~> 1.4")]
p [installed_version(BASE, "formatter", "~> 1.4"), installed_version(BASE, "formatter", "~> 1.4")]
```
```
["1.5.0", "1.6.0"]
["1.4.2", "1.4.2"]
```

In the pair without a lock file, the two machines installed two different versions; in the pair with a lock file, both installed the same one. In the second pair the registry was never used: `installed_version` does not look at the registry, so the next day's registry contents do not affect the result. The difference between yesterday and today is that the registry changed; the lock file separates this difference from the installation. The lock file provides the consistency, and this holds only if everyone reads the same lock file. If the lock file exists on only one machine, the version the other machine installs says nothing about this measurement. **PG10.** The model represents the two machines with the same run object; synchronizing the lock file in a real team is a separate job and was not measured here.

#### Dependency Kind and Constraint

The distinction between runtime and development dependencies was built in the previous lesson through the package specification. What is added here is how the kind relates to the constraint: the kind determines not the installed version but where it will be installed, and both kinds carry a constraint in the same form.

```ruby
# kind.rb — dependency kind and constraint
formatter = Gem::Dependency.new("formatter", "~> 1.4", :runtime)
examiner = Gem::Dependency.new("examiner", ">= 2", :development)

p [formatter.type, formatter.runtime?]
p [examiner.type, examiner.runtime?]
p formatter.requirement.to_s
p examiner.requirement.to_s
```
```
[:runtime, true]
[:development, false]
"~> 1.4"
">= 2"
```

The `type` of the development dependency returned `:development` and `runtime?` was false; the runtime dependency gave `:runtime` and true. Both constraint texts were read back as given: `"~> 1.4"` and `">= 2"`. The kind of each dependency is read separately, and `runtime?` is true only for the runtime kind. The text of the constraint is independent of the kind. This distinction explains why the lock file can contain both: the lock file holds the whole resolution, and the kind says which part of that resolution lands on whose machine. **PG11.** `type` and `runtime?` show only what the `Gem::Dependency` object says; a manager skipping the development dependency during installation lies outside this measurement.

#### The Bounding Measurement

The rule runs a computation in three variants. If the computation raises an error in any of them, the rule cannot classify. The error of this lesson itself shows this: with the constraint `~> 1.4.0`, the lock file in the base (`1.4.2`) satisfies the constraint, but the lock file of the dependency variant (`1.5.0`) does not.

```ruby
# boundary.rb — the rule does not classify when the lock file does not satisfy the constraint
require_relative "determinant"
require_relative "dependency"

begin
  determinant(->(r) { installed_version(r, "formatter", "~> 1.4.0") })
rescue => e
  p e.class
  puts e.message
end
```
```
ArgumentError
lock does not satisfy constraint
```

The rule gave no class; `ArgumentError` was raised. This error is not the rule's but the computation's: the lock file contradicts the constraint. This is the problem seen at installation; the rule does not call it "dependency" because the computation could not produce a result. **PG12.** This measurement shows only what happens when the lock file and the constraint contradict each other; a real manager may choose to remove the contradiction by resolving the lock file again, which lies outside this model.

#### Summary

- A version constraint is not a version but an accepted range: `~> 1.4` keeps the major version fixed, `~> 1.4.0` frees only the patch version, and a range constraint is the intersection of two bounds.
- Resolution without a lock file depends on the registry's contents: when `1.6.0` arrives in the registry, the same constraint gives another version.
- The determinant of this lesson is **dependency**: the `locked version` row shows `D+ O- E-`, and when the lock file becomes `1.5.0`, the installed version changes too.
- The constraint bounds and the lock file pins; this is why the lock file is committed to the repository.
- If the lock file does not satisfy the constraint, the rule does not classify and raises an error.

#### Next Step

The lock file now determines the installed version. But a result can change even with the same code and the same lock file. The next lesson measures why the same tests sometimes fail and how the order determines this.

### Testing Frameworks

Source: https://academia.sh/en/courses/ruby-production/testing-frameworks

Why do the same tests sometimes fail? The lesson builds the three parts of a test framework in pure Ruby and measures that the seed chooses the order and that shared state makes a test fail.

The dependency is locked and the installed version is now fixed. Even so, the same code, the same lock file and the same tests can pass in one run and fail in another. The observation of this lesson is the `test result` row: the base is `[]`, so no test fails, but the evidence column shows `D- O+ E-`. The result changes when the order changes.

M10/K05 asked about the limit of an assurance: what a test catches and what it misses. Here the question is different. What determines whether a test passes? There the limit was the coverage of the assurance; here the limit is an input outside the test itself. The core's `determinant.rb` file is given again exactly; the block is source and is pulled in with `require_relative`.

```ruby
# 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
```

#### The Three Parts of a Framework

A **test framework** consists of three parts. A **test definition** is a named block that is a runnable trial. An **assertion** compares the expected value with the actual one and raises an error if they do not match. A **test runner** runs the definitions, catches the assertion error and writes the result to a table. Common test frameworks are built on these three parts too. Here all three are built in pure Ruby, because the observation lies inside the runner and the runner must be visible.

```ruby
# framework.rb — the three parts of a test framework: definition, assertion, runner
class AssertionFailed < StandardError; end

# Assertion: silent if the expected and the actual value are the same, otherwise raises an error.
def assertion(expected, actual)
  raise AssertionFailed, "assertion failed" unless expected == actual
end

# Runner: runs each test, catches the assertion error, writes the result to the table.
def run_suite(tests)
  tests.to_h do |name, test|
    test.call
    [name, :passed]
  rescue AssertionFailed
    [name, :failed]
  end
end
```

The file is source. The file below runs three tests. The second one expects integer division wrongly.

```ruby
# small.rb — the three parts come together
require_relative "framework"

tests = {
  "addition" => -> { assertion(5, 2 + 3) },
  "division" => -> { assertion(3.5, 7 / 2) },
  "decimal" => -> { assertion(3.5, 7 / 2.0) }
}
p run_suite(tests)
```
```
{"addition" => :passed, "division" => :failed, "decimal" => :passed}
```

`7 / 2` is integer division and gives `3`; the second assertion did not hold, the runner caught the error and wrote the `division` test as `:failed`. The other two passed. Note that the runner catches only `AssertionFailed`. Any other error, for example division by zero, is not an assertion result and does not enter the table.

```ruby
# error.rb — an error that is not an assertion error does not enter the table
require_relative "framework"

begin
  run_suite("division by zero" => -> { 1 / 0 })
rescue => e
  p e.class
end
```
```
ZeroDivisionError
```

The runner did not catch `ZeroDivisionError`; the error escaped. This distinction matters: a test **failing** and a test **erroring out** are different events, and this lesson measures only the first. **PG13.** The framework built here shows the three parts; the expectation library, the setup and the cleanup steps of a real framework lie outside this model.

#### Two Writing Styles

Common frameworks offer the same three parts in two different writing styles. In the first, tests are methods of a class whose names start with `test_`, and an assertion is a method call. In the second, tests are named examples inside a definition block, and an assertion is an expectation chain that starts from the actual value. The file below writes the same two tests in both styles and hands both to the same runner.

```ruby
# styles.rb — the same two tests, two writing styles, one runner
require_relative "framework"

# First style: a test class; every method starting with test_ is a test.
class DivisionTests
  def test_integer
    assertion(3, 7 / 2)
  end

  def test_decimal
    assertion(3.5, 7 / 2)
  end
end

class_tests = DivisionTests.public_instance_methods(false).grep(/\Atest_/).sort.to_h do |name|
  [name.to_s, -> { DivisionTests.new.public_send(name) }]
end

# Second style: a definition block and an expectation chain.
Expectation = Struct.new(:actual) do
  def to_be(expected)
    assertion(expected, actual)
  end
end

def expect(actual)
  Expectation.new(actual)
end

class Definition
  attr_reader :tests

  def initialize(subject)
    @subject = subject
    @tests = {}
  end

  def example(name, &block)
    @tests["#{@subject} #{name}"] = block
  end
end

def define(subject, &body)
  definition = Definition.new(subject)
  definition.instance_eval(&body)
  definition.tests
end

block_tests = define("division") do
  example("integer") { expect(7 / 2).to_be(3) }
  example("decimal") { expect(7 / 2).to_be(3.5) }
end

p run_suite(class_tests)
p run_suite(block_tests)
```
```
{"test_decimal" => :failed, "test_integer" => :passed}
{"division integer" => :passed, "division decimal" => :failed}
```

The two tables give the same result: the test that expects `3.5` from integer division failed in both styles. The difference lies in the writing. In the first style the test name is the method name, and the tests are gathered in a class. In the second style the test name is a string built from the subject and the example name. In both cases the runner sees the same name → block table; this is why the order and the seed effect measured in the following sections belong to the runner. **PG13a.** The two styles were compared here in a single example with two tests; the setup, nested grouping and reporting differences of real frameworks lie outside this comparison.

#### The Observation's Runner

The runner in the observation row is smaller but does one more thing: it shuffles the tests with the seed and gives all of them the same settings table. The file below carries the `TESTS` and `run_tests` definitions taken from `observations.rb` exactly.

```ruby
# tests.rb — the tests and the runner of the observation row
# Test runner: tests run in an order shuffled by the seed and share the same settings.
TESTS = {
  "price_format" => ->(settings) { "12 #{settings[:currency]}" == "12 TRY" },
  "currency_conversion" => ->(settings) { settings[:currency] = "USD"; true },
  "invoice_total" => ->(settings) { settings[:currency] == "TRY" }
}

def run_tests(run, tests = TESTS)
  settings = { currency: "TRY" }
  order = tests.keys.shuffle(random: Random.new(run.seed))
  order.reject { |name| tests[name].call(settings) }
end
```

There are three tests. `price_format` looks at the currency in the settings and expects `TRY`. `currency_conversion` sets the currency in the settings to `USD` and returns true. `invoice_total` expects `TRY` again. The runner returns the names of the failing tests. The file below prints the shuffled order and the failing tests side by side for six seeds.

```ruby
# seeds.rb — six seeds, six orders
require_relative "determinant"
require_relative "tests"

(1..6).each do |seed|
  order = TESTS.keys.shuffle(random: Random.new(seed))
  puts "#{seed} #{order.inspect} #{run_tests(BASE.with(seed: seed)).inspect}"
end
```
```
1 ["price_format", "invoice_total", "currency_conversion"] []
2 ["invoice_total", "currency_conversion", "price_format"] ["price_format"]
3 ["currency_conversion", "price_format", "invoice_total"] ["price_format", "invoice_total"]
4 ["currency_conversion", "price_format", "invoice_total"] ["price_format", "invoice_total"]
5 ["price_format", "currency_conversion", "invoice_total"] ["invoice_total"]
6 ["price_format", "currency_conversion", "invoice_total"] ["invoice_total"]
```

The order of seed 1 puts `currency_conversion` last; since no test runs after it, no test fails. In seed 2 `currency_conversion` is in the middle, and `price_format`, which comes after it, fails. Seeds 3 and 4 gave the same order: `currency_conversion` first, and the two tests after it also failed. Seeds 5 and 6 share an order too; `currency_conversion` is in the middle and only `invoice_total`, which comes after it, fails. Four different orders came out of six seeds, and the result of each order is the same as long as the seed stays fixed.

#### Shared State

The flaw is not in the order but in what makes the order visible as a flaw. `currency_conversion` changes the shared settings table. This change is the input of every test that runs after it. That the table is shared among the tests can be shown directly.

```ruby
# shared.rb — the setting a test wrote is the input of the test that comes after it
require_relative "tests"

settings = { currency: "TRY" }
TESTS["currency_conversion"].call(settings)
p settings
p TESTS["price_format"].call(settings)
p TESTS["price_format"].call({ currency: "TRY" })
```
```
{currency: "USD"}
false
true
```

After `currency_conversion` the settings became `USD`. The same `price_format` test returned `false` with these settings and `true` with fresh settings. The test code is the same; what changed is the starting state of the test. This is **shared state**, and it is the most common source of a **flaky test**: a test is fine, but it depends on who ran before it. **PG14.** The measurement holds only for a model that shares a settings table; shared state through the file system, the network or a global variable behaves the same way, but it was not measured here.

#### Which Test Fails on How Many Seeds

The result of a single seed is a diagnosis; the count over six seeds gives a profile. The file below counts the number of tests each seed fails, the number of seeds on which each test fails and the number of seeds on which at least one test fails.

```ruby
# counts.rb — the distribution of failures by seed
require_relative "determinant"
require_relative "tests"

results = (1..6).map { |s| run_tests(BASE.with(seed: s)) }
p results.map(&:size)
p results.flatten.tally
p results.count { |f| !f.empty? }
```
```
[0, 1, 2, 2, 1, 1]
{"price_format" => 3, "invoice_total" => 4}
5
```

In five of the six runs at least one test failed; only seed 1 passed cleanly. `currency_conversion` never failed, but it is the test that makes the other two fail. `invoice_total` failed on more seeds because `currency_conversion` came before it more often; `price_format` failed less. This table does not point at the culprit, it points at the victim. The failing test is not the faulty test itself; the faulty test passes silently and makes someone else fail. **How often a test fails measures not its own soundness but who runs before it.** For this reason, when reading the code of a failing test, the first question should be what the failing test shares. **PG15.** The count is for the six seeds of a single suite; it does not follow from this measurement that the failure frequency would have the same proportions in a real team's suite.

Fixing the order does not solve this problem, it hides it. If seed 1 were used in every run, this suite would never fail and the shared state would stay unnoticed. Shuffling does not produce a defect; it makes an existing dependency visible. And to the extent that it can make it visible, printing the seed of the failing test turns shuffling from a suspicion into a diagnosis.

#### Reproducing with the Seed

A flaky test is a bug report if it can be reproduced at the moment it fails; if it cannot be reproduced, it is a rumor. When the runner fixes the seed, the failing test fails in the same order every time. For this reason a good runner **prints** the seed: when a run fails, the seed is the key to reproduction.

```ruby
# replay.rb — reproduce a failing test with the seed
require_relative "determinant"
require_relative "tests"

results = (1..6).map { |s| run_tests(BASE.with(seed: s)) }
p results.count([])
p run_tests(VARIANTS[:order])
p run_tests(VARIANTS[:order]) == run_tests(BASE.with(seed: 2))
p determinant(->(r) { run_tests(r) })
```
```
1
["price_format"]
true
["order", "D- O+ E-", []]
```

On only one of the six seeds did no test fail: seed 1 passed while seed 2 failed. Since `VARIANTS[:order]` is seed 2, it gave `["price_format"]`, and the second call with the same seed gave the same result as the first. The last line shows the classification: the result comes from `order`, the evidence is `D- O+ E-`, the base is `[]`. The rule wrote the base result as `[]`; the failing test is not visible in the base run, only in the `order` variant. The determinant of this lesson is **order**. **PG16.** Seed 1 showing "passed" does not show that the tests are sound; it shows only that in that order the test that corrupts the settings table fell to the end.

#### The Bounding Measurement

The class is tested from two directions. The first: what happens if each test runs with its own copy of the settings? The second: the rule tries only two seeds, so do these two seeds catch every flakiness?

```ruby
# isolation.rb — each test gets its own copy of the settings
require_relative "determinant"
require_relative "tests"

def isolated_run_tests(run, tests = TESTS)
  settings = { currency: "TRY" }
  order = tests.keys.shuffle(random: Random.new(run.seed))
  order.reject { |name| tests[name].call(settings.dup) }
end

p (1..6).map { |s| isolated_run_tests(BASE.with(seed: s)) }
p determinant(->(r) { isolated_run_tests(r) })
```
```
[[], [], [], [], [], []]
["code", "D- O- E-", []]
```

All six seeds gave `[]` and this time the rule said `code`, with the evidence `D- O- E-`. The same three tests, the same six seeds; the only difference is that each test takes its own copy. The class moved from **order** to **code**: **isolation removes the effect of the order**, because the tests are no longer each other's input. The limit of isolation is the depth of the copy.

```ruby
# shallow.rb — dup is shallow
settings = { list: [] }
copy = settings.dup
copy[:list] << 1
p settings
p settings[:list].equal?(copy[:list])
```
```
{list: [1]}
true
```

`dup` copied the table but not the array inside it: the two tables hold the same array, and the change in the copy passed to the original. Because the settings table carries only simple values, the isolation of this lesson worked. For nested settings `dup` is not enough. **PG17.** The isolation measurement holds only for a flat table.

The second direction is the rule's own blind spot. The rule tries the base and the `order` variant, that is, seed 1 and seed 2. A suite can be built that passes on seed 2 but fails on seed 5. It is enough to replace the `price_format` test with a test that never looks at the settings.

```ruby
# blind.rb — where the rule tries only seeds 1 and 2
require_relative "determinant"
require_relative "tests"

BLIND_TESTS = TESTS.merge("price_format" => ->(_settings) { true })

p determinant(->(r) { run_tests(r, BLIND_TESTS) })
results = (1..6).map { |s| run_tests(BASE.with(seed: s), BLIND_TESTS) }
p results
```
```
["code", "D- O- E-", []]
[[], [], ["invoice_total"], ["invoice_total"], ["invoice_total"], ["invoice_total"]]
```

The rule classified this suite as `code`, because seeds 1 and 2 both gave `[]`. But on four of the six seeds `invoice_total` failed. **The result "not order" was stated only for the two tried seeds.** The suite still depends on shared state; the rule did not see it because it fails in the orders it did not look at. The remedy is not to change the rule but to interpret the measurement: a test suite that the rule calls `code` is not considered isolated until the seed range is widened. The two measurements above should be read together. The isolated runner gave `[]` on all six seeds; the tested suite gave `[]` only on seeds 1 and 2. In the first, `[]` was seen on all six seeds, in the second on only two. The same output is supported by a different number of seeds, and the value of the measurement comes from that number. **PG18.** Six seeds gave only four of the possible orders; as the number of tests grows, the number of possible orders grows quickly and a few seeds cannot exhaust them.

#### Summary

- A test framework has three parts: the test definition, the assertion and the runner. A failing test is an assertion error; an erroring test is another kind of error.
- The two writing styles, a test class with `test_` methods and a definition block with an expectation chain, give the runner the same name → block table; the same two tests gave the same result in both styles.
- The determinant of this lesson is **order**: in the `test result` row the evidence is `D- O+ E-`, the base is `[]`, and with seed 2 it is `["price_format"]`.
- The source of a flaky test is shared state: `currency_conversion` changes the shared settings and makes the test that runs after it fail. If the seed is printed, the failing test is reproduced.
- When each test takes its own copy of the settings, all six seeds give `[]` and the rule says `code`; `dup` is shallow.
- The rule tries only seeds 1 and 2; it classifies as `code` a suite that passes on seed 2 but fails on seed 5.

#### Next Step

The seed chooses the order and isolation removes it. But a test can depend on another input too: today's clock. The next lesson measures why a test that reads the clock will fail tomorrow, and how faking and test data remove this dependency.

### Mocking and Test Data

Source: https://academia.sh/en/courses/ruby-production/mocking-and-test-data

Why does a test that reads the clock fail tomorrow? The lesson measures passing the clock in from outside, fake objects and seeded test data, and separates which determinant mocking removes and which it leaves.

The previous lesson showed that a test suite fails depending on the order, and removed the failure through isolation. Now there is another input: today's date. A test passes today, fails thirty days later, and not a single line of its code has changed. The observation of this lesson is the `clock-reading test` row: the base is `true` and the evidence column shows `D- O- E+`. The result changes when the clock changes; it does not change when the dependency or the order changes.

M13/K03 asked how long a value stays: information held in the session was still there at the next request. Here the same question is applied to tests: after which test is a piece of test data still there? The answer depends on whose hands the data is in, and this lesson measures it. The core's `determinant.rb` file is given again exactly; the block is source and is pulled in with `require_relative`.

```ruby
# 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
```

#### The Clock-Reading Test

The computation of the observation is a small rule: the trial period is 30 days, and the period is measured by the number of seconds elapsed since the start. The computation takes the clock as a parameter; two boundary values are measured.

```ruby
# clock.rb — the trial period is 30 days
def in_trial?(start, clock) = clock - start < 30 * 86_400

start = Time.utc(2026, 10, 1)
p in_trial?(start, Time.utc(2026, 10, 30, 23, 59))
p in_trial?(start, Time.utc(2026, 10, 31, 0, 0))
```
```
true
false
```

The boundary is `30 * 86_400` seconds. October 30 at 23:59 is within the period; October 31 at 00:00 is exactly thirty days later and gives `false`. This computation is pure: the same two inputs always give the same result. So far nothing depends on the environment. The dependency begins when the question is asked **where** the clock comes from.

The file below writes two tests. The first reads the clock from the run model's environment, that is, it takes the clock as the machine reports it. The second runs the same computation with a fixed clock source given up front.

```ruby
# environment.rb — who provides the clock determines the result
require_relative "determinant"

def in_trial?(start, clock) = clock - start < 30 * 86_400

start = Time.utc(2026, 10, 1)
p in_trial?(start, BASE.environment[:clock])
p in_trial?(start, VARIANTS[:environment].environment[:clock])
p determinant(->(r) { in_trial?(start, r.environment[:clock]) })
```
```
true
false
["environment", "D- O- E+", true]
```

With the clock in the base the test passed; with the clock in March 2027 it failed. The rule said `environment`, with the evidence `D- O- E+`: the result stayed the same in the dependency and order variants and changed in the environment variant. Without a single line of code changing, the same test gave two results. This is the test that reads the clock and will fail tomorrow: the clock read with `Time.now` behaves like the base in today's run and like March thirty days later. **PG19.** This measurement reads the clock from the environment model; the real clock's value is read with `Time.now` and is not printed because it changes from sandbox to sandbox.

#### Making the Clock a Parameter

The fix is to take the clock out of the computation and pass it in from outside. When the computation takes the clock source as a parameter, who chooses the clock moves to the caller. In production the source is the real clock; in the test the source is a fixed clock. This fixed clock is a **fake**: an object that stands in for the real one and whose behavior the test controls.

```ruby
# fake_clock.rb — a test that passes the clock in from outside
require_relative "determinant"

def in_trial?(start, clock) = clock - start < 30 * 86_400
def trial_remaining?(start, clock_source) = in_trial?(start, clock_source.call)

start = Time.utc(2026, 10, 1)
fake_clock = -> { Time.utc(2026, 10, 16, 9, 0) }

p determinant(->(r) { trial_remaining?(start, -> { r.environment[:clock] }) })
p determinant(->(_r) { trial_remaining?(start, fake_clock) })
```
```
["environment", "D- O- E+", true]
["code", "D- O- E-", true]
```

The first line classifies the result as `environment` when the clock source comes from the run's environment. The second line replaces the source with a fixed clock and the result becomes `code`, with the evidence `D- O- E-`. The computation is still the same, the test still makes the same assertion; what changed is that the clock is chosen by the test. The clock is no longer something the environment says but the test's own input. **PG20.** The fake clock was built with a lambda; the way a real framework freezes time, for example by temporarily replacing the `Time.now` method, has a different scope and was not measured here.

Making it a parameter has a cost, and the measurement does not leave it out. The computation now expects a clock source; every place that calls it has to pass one, and since the source passed in production is the real clock, the path that works with the real clock never runs in the test. A test written with a fake clock verifies the computation; it does not verify that the clock is read correctly from the real source. For this reason faking the clock protects the clock-reading test from failing tomorrow but leaves the reading line unchecked. A good separation is to gather the reading in a single place and to try that place separately.

A fixed clock has a second benefit: boundary values become testable. With the real clock, catching the last minute of the 30th day requires waiting for that minute; with a fake clock two calls are enough, and the separation of `23:59` and `00:00` in the first measurement comes out the same in every run.

#### Fake Objects

The clock is the simplest dependency to fake. Three kinds of fake objects are seen in tests, and the job of each is different. A **recording fake** does not do the work of the real one; it only records what it was told. A **canned response** returns a ready answer to a given call. An **expectation object** checks afterward whether a call was made.

```ruby
# notifier.rb — a recording fake
class FakeNotifier
  attr_reader :sent

  def initialize
    @sent = []
  end

  def deliver(recipient, text)
    @sent << [recipient, text]
  end
end

def notify_invoice(notifier, recipient) = notifier.deliver(recipient, "invoice ready")

notifier = FakeNotifier.new
notify_invoice(notifier, "a@example.test")
p notifier.sent
```
```
[["a@example.test", "invoice ready"]]
```

The invoice notification did not send a real message; the fake notifier wrote what was sent to an array. The test can build its assertion on this array: `[["a@example.test", "invoice ready"]]`. A recording fake measures not what a call **does** but **whom it told what**. The notifier in production would reach out to the network; the fake notifier does not, so the test is both fast and independent of the state of the network.

```ruby
# canned.rb — a canned response
exchange = Object.new
exchange.define_singleton_method(:rate) { |_currency| 42.5 }

p exchange.rate(:usd)
p exchange.singleton_methods
```
```
42.5
[:rate]
```

The canned response did no computation: whatever was asked, `42.5` was returned. The object has a single call and that call is the one the test wrote. This cuts a test's dependency on an external source, for example an exchange rate service; but it also raises the question of how well the fake represents the real one. The fake notifier took two arguments; the real notifier may ask for a third argument, the channel.

```ruby
# drift.rb — a fake object can really drift apart from the real one
class FakeNotifier
  attr_reader :sent

  def initialize = @sent = []

  def deliver(recipient, text) = @sent << [recipient, text]
end

class RealNotifier
  def deliver(recipient, text, channel) = [recipient, text, channel]
end

fake = FakeNotifier.new
fake.deliver("a@example.test", "invoice ready")
p fake.sent

begin
  RealNotifier.new.deliver("a@example.test", "invoice ready")
rescue ArgumentError => e
  p e.class
end
```
```
[["a@example.test", "invoice ready"]]
ArgumentError
```

The call written with the fake object passed; the same call raised `ArgumentError` on the real object. The test was green and production broke. **PG21.** A fake object imitates the behavior of the real one; it does not verify it. To catch the drift between the fake and the real one, either a separate test run with the real object or an assertion that checks that the fake follows the real one's signature is needed. This measurement shows only that a drift can exist; it does not measure a tool that catches the drift automatically.

The expectation object adds a decision to the recording object. It keeps what was called, but at the end of the test it turns this into an assertion: "exactly one notification should have gone to this recipient".

```ruby
# expectation.rb — an expectation object
class NotificationExpectation
  def initialize(recipient, expected_count)
    @recipient = recipient
    @expected_count = expected_count
    @calls = []
  end

  def deliver(recipient, _text) = @calls << recipient

  def verify = @calls.count(@recipient) == @expected_count
end

expectation = NotificationExpectation.new("a@example.test", 1)
expectation.deliver("a@example.test", "invoice ready")
p expectation.verify
expectation.deliver("a@example.test", "invoice ready")
p expectation.verify
```
```
true
false
```

After the first call the expectation holds; after the second call it does not: two notifications have gone to the same recipient. The distinction is this: the recording object collects the information, and the expectation object ties a decision to that information. If the decision is set up wrongly, the test verifies the wrong thing; the expected count is the test's own assumption.

#### Test Data

Test data is the records produced as the input of a test: a customer, an invoice, a user. Writing the data by hand repeats the same lines in every test; a **test data generator** produces records by a rule. The simplest generator keeps a counter.

```ruby
# generator.rb — a counter-based test data generator
def customer_generator
  counter = 0
  -> { counter += 1; "customer#{counter}" }
end

generate = customer_generator
p generate.call
p generate.call
p customer_generator.call
```
```
"customer1"
"customer2"
"customer1"
```

Two calls from the same generator gave `"customer1"` and `"customer2"`: the counter rises by one on every call. A new generator gave `"customer1"` again. These two observations are the answer to isolation. If one generator is shared among all tests, a record produced by one test advances the counter of the other, and the second test sees `"customer2"` instead of `"customer1"`. When each test builds its own generator, the shared state is divided. This is the same as the copy of the settings table in the previous lesson, for a counter. **PG22.** The measurement shows a counter-based generator; for a generator that produces a persistent record in a database to provide the same isolation, a separate cleanup step is needed, and it was not measured here.

#### The Bounding Measurement

The result of mocking looks as if it solved everything: the clock is fixed, the notifier is fake, the counter is isolated. But removing one determinant does not remove another. If test data is produced with a seed, the seed determines the result.

```ruby
# data.rb — seeded test data
require_relative "determinant"

p Array.new(3) { Random.new(1).rand(100) }.uniq.size
p (r = Random.new(1); Array.new(3) { r.rand(100) })
p (r = Random.new(2); Array.new(3) { r.rand(100) })

data = lambda do |run|
  r = Random.new(run.seed)
  Array.new(3) { r.rand(100) }
end
p determinant(data)

fixed = lambda do |_run|
  r = Random.new(7)
  Array.new(3) { r.rand(100) }
end
p determinant(fixed)
```
```
1
[37, 12, 72]
[40, 15, 72]
["order", "D- O+ E-", [37, 12, 72]]
["code", "D- O- E-", [47, 68, 25]]
```

The first line shows that if a new `Random.new(1)` is built on every call, the three values are always the same: a single value. Seed 1 and seed 2 gave two different triples. When `determinant` takes the seed from the run, the result is `order`: the evidence is `D- O+ E-`, and the base is the three values of seed 1. When the seed is a fixed number, that is `Random.new(7)`, the result is `code`: the computation does not look at the run's seed. **Mocking removes one determinant; it does not remove another by itself.** Fixing the clock removed the environment; a data generator that takes its seed from the run left the order determinant. Which determinant remains is understood not from whether something is fake but from what the result continues to look at. **PG23.** The seeded data here consists of three numbers; why a test sometimes fails when the records of a real data generator depend on the seed is the same reason as in the previous lesson and was not measured separately.

The practical consequence of this distinction is as follows: to make a test stable, one must first measure which determinant moves the result. Faking the clock does not rescue a test whose result depends on the order; fixing the order does not rescue a test that depends on the clock. The seed table of the previous lesson and the clock measurement of this lesson ask the same question on two different variables, and the answer each time depends on the variable itself.

#### Summary

- A test that reads the clock depends on what the environment says: in the `clock-reading test` row the evidence is `D- O- E+`, and the result changes in March 2027.
- The determinant of this lesson is **environment**. When the clock is passed in as a parameter and a fixed clock source is used, the result becomes `code`.
- A recording fake collects what was told, a canned response returns a ready answer and an expectation object ties a decision to the call.
- A counter-based test data generator gives a different value on each call; a generator of its own for each test divides the shared state.
- Mocking removes one determinant and does not remove another by itself: test data produced from the run's seed stays as `order`.

#### Next Step

The first half of this course measured what determines the result for packages and tests: the package specification for the file name, the lock file for the installed version, the order for the failing test and the environment for the test that reads the clock. The second half moves to code running in production. The lesson The Concurrency Model measures that the same code gives a different result by seed when it runs with a counter without a lock, and shows why a mutex removes this difference.

## Runtime and Performance

The global interpreter lock, threads and Ractors, scaling with forked worker processes and environment variables, object allocation and the garbage collector, finding the hot path by counting calls, content-digested assets and a release switch that can be rolled back.

### The Concurrency Model

Source: https://academia.sh/en/courses/ruby-production/the-concurrency-model

The same code with the same inputs giving different numbers is an order problem. The lesson measures how a switch between a read and a write loses a count, how a mutex lets the result come from the code, and how a Ractor removes shared state.

The previous lesson showed that a test that reads the clock can fail tomorrow: the environment determined that result. This lesson turns to order. When four threads increment a counter a thousand times each, four thousand is expected; the counter sometimes returns less, and the code has not changed. The course's measuring axis continues: **what determines this result?** The answer is one of four names, printed in the output as plain ASCII strings: `code`, `dependency`, `order`, `environment`. This lesson's observation is the `unlocked counter` row, and its determinant is **order**.

M08/K04 asked which thread a step lands on. Here the question is not the identity of the thread but whether the order changes the result. The core's `determinant.rb` file is given below as it is; the block is source, it has no output, and it is pulled in with `require_relative`. The `lock` in the core is the contents of the lock file, that is, the installed versions. A mutex shares only the word with it, so this lesson names its mutex variable `mutex`.

```ruby
# 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
```

#### The Gap Between a Read and a Write

The core's counter is not made of real threads; it is a **scheduler model**. Two threads read the counter, write one more than they read, and each makes three increments. A switch can happen between the read and the write; the `seed` picks which thread takes the next step. The correct result is six. The model stands in for real threads because the order of real threads can change from run to run, and the printed number would not come out the same twice. The seed fixes the order, so the broken result comes out the same on every run and can be measured.

```ruby
# counter.rb — scheduler model: unlocked counter
# Scheduler model: two threads read the counter and write one more; a switch can happen
# between the read and the write. The seed picks which thread takes the next step.
def unlocked_counter(run, increments: 3)
  random = Random.new(run.seed)
  counter = 0
  jobs = Array.new(2) { { left: increments, read: nil } }
  loop do
    waiting = jobs.select { |j| j[:left].positive? }
    break if waiting.empty?

    job = waiting.sample(random: random)
    if job[:read].nil?
      job[:read] = counter
    else
      counter = job[:read] + 1
      job[:read] = nil
      job[:left] -= 1
    end
  end
  counter
end
```

```ruby
# orders.rb — unlocked counter: determinant and six seeds
require_relative "determinant"
require_relative "counter"

p determinant(->(r) { unlocked_counter(r) })
results = (1..6).map { |seed| unlocked_counter(BASE.with(seed: seed)) }
p results
```

```
["order", "D- O+ E-", 6]
[6, 3, 5, 4, 4, 4]
```

The first line is the core's result: in the base the counter is six, and the evidence holds only `O+`. Changing the lock file or the environment does not move the result; changing the seed does. The second line shows the results of six seeds: four different values appear, and only one of them gives the correct six. The lost increments come from two threads reading the same value and writing the same value. **RU1.** These results hold only for two threads and three increments each; the loss rate with more threads cannot be read from this measurement.

#### What the Global Interpreter Lock Does Not Protect

Ruby's **global interpreter lock** lets only one thread run Ruby code at a time. This prevents two threads from writing to the same memory location at the same moment, but it does not protect the interval between a thread's read and its write. `counter += 1` looks like a single step, yet it is a read, an addition, and a write. The scheduler can switch to another thread between those three steps, and that is exactly what the model does. For this reason an unlocked counter written with real threads is not printed in this lesson: its result can change from run to run.

A design in which order does not change the result closes the gap. A **mutex** admits only one thread into a region at a time. The block below creates four real threads, each increments the counter a thousand times, and every increment happens inside the `mutex`.

```ruby
# mutex.rb — four real threads, with a mutex
require_relative "determinant"

def mutex_counter(threads: 4, increments: 1000)
  counter = 0
  mutex = Mutex.new
  jobs = Array.new(threads) do
    Thread.new do
      increments.times { mutex.synchronize { counter += 1 } }
    end
  end
  jobs.each(&:join)
  counter
end

p mutex_counter
p determinant(->(_r) { mutex_counter })
```

```
4000
["code", "D- O- E-", 4000]
```

The result is four thousand. The rule ran this computation in all three variants and the result did not change in any of them; the evidence is `D- O- E-` and the determinant is **code**. The unlocked model depended on order, while the real counter written with a mutex depends on its own code. **RU2.** The three variants of this rule do not directly change the order of real threads: the `seed` drives only the scheduler in the model. Four thousand does not prove that the mutex preserves the result when the order changes; it says only that the result is consistent in this run.

#### The Queue and an Exception in a Thread

Locking shared state is not the only way. If a producer and a consumer talk through a **queue**, the value passes from one hand to the other and the two sides never write to the same variable. In the block below the producer puts the numbers from zero to four on the queue and the consumer adds them up.

```ruby
# queue.rb — a producer and a consumer talk through a queue
queue = Thread::Queue.new
producer = Thread.new do
  5.times { |i| queue << i }
  queue << :done
end
total = 0
consumer = Thread.new do
  while (item = queue.pop) != :done
    total += item
  end
end
[producer, consumer].each(&:join)
p total
```

```
10
```

The total is ten. Only the consumer writes the `total` variable, so there is no other writer to slip in between a read and a write. The queue preserves order, but the result is independent of order as well: the order of the additions does not change the sum.

An exception that blows up in a thread does not vanish silently; it is raised again in the main thread at the `join` call. So that nothing is written to standard error, the block first turns reporting off. The exception message is in the lesson's own `raise` call.

```ruby
# exception.rb — an exception in a thread returns to the main thread at join
Thread.report_on_exception = false
job = Thread.new { raise "exploded" }
begin
  job.join
rescue => e
  puts "caught: #{e.class} #{e.message}"
end
p job.status
```

```
caught: RuntimeError exploded
nil
```

The exception was caught at `join` as a `RuntimeError`. The thread's status is `nil`: the thread ended with an exception. Without the `join` call the exception would never have reached the main thread. **RU3.** This measurement covers one thread and one exception; several threads blowing up at the same time was not measured here.

#### The Bounding Measurement

A mutex protects shared state against order. A **Ractor** opens another path: a Ractor is an execution unit that runs with isolated memory and cannot reach local variables outside it. Shared state does not exist from the start, so there is nothing left to protect. The first block measures that a Ractor returns a value, and the second measures that a block touching an outside variable is rejected.

```ruby
# ractor.rb — an execution unit that runs with isolated memory
p Ractor.new { (1..10).sum }.value

outer = [1, 2, 3]
begin
  Ractor.new { outer.size }
rescue ArgumentError => e
  puts e.class
end
```

```
55
ArgumentError
```

The first Ractor returned fifty-five. The second was rejected with `ArgumentError`: the block was reading the `outer` variable outside it. Isolation removes shared state, not order. The order is still undetermined, but since two Ractors cannot write to the same variable, there is no place left where the order could spoil the result. For an object to be shared between Ractors it must be a **shareable object**. Freezing it is not enough.

```ruby
# shareable.rb — freezing is shallow
plain = [1, 2].freeze
inner_mutable = [+"a"].freeze
p Ractor.shareable?(plain)
p Ractor.shareable?(inner_mutable)
p Ractor.shareable?(Ractor.make_shareable([+"a"]))
```

```
true
false
true
```

A frozen array made of numbers is shareable. A frozen array that holds an unfrozen string is not: freezing freezes only the array itself, not what is inside it. `make_shareable` freezes the contents as well, and the result is shareable. **RU4.** These three results were measured on Ruby 4.0.7; the rules for Ractors can change between versions.

The bounding result is this: a Ractor does not destroy the rule's `order` category. The unlocked model depended on order because two threads shared a variable. When the sharing goes away, order no longer determines the result; as long as the sharing stays, order is the determinant.

#### Summary

- **Order** determines the unlocked counter: with the same code, the same environment, and the same lock file, six seeds gave four different results, and only one was correct.
- The global interpreter lock does not protect the gap between a read and a write; `counter += 1` is three steps.
- The mutex fixed the counter at four thousand and the rule returned the category `code`; order no longer determines the result.
- A queue passes a value from one hand to the other; an exception in a thread is raised again at the `join` call.
- A Ractor removes shared state; freezing is shallow, and a frozen array holding an unfrozen string is not counted as shareable.

#### Next Step

The question of why the unlocked counter sometimes counted fewer than six has its answer: order determined the counter. The next question is how many parts the work will be split into. The number of forked worker processes comes from an environment variable or from the core count; the next lesson measures where that number is read from and why the memory of a forked process does not show up in its parent.

### Process-Based Scaling

Source: https://academia.sh/en/courses/ruby-production/process-based-scaling

The worker count comes from the environment, not from the code. The lesson measures that the memory of a forked process does not reach its parent, that work can be sliced and collected through a pipe, and that the worker count is read from an environment variable and the core count.

The previous lesson showed that order determines the counter's result, and it measured a Ractor as one way to remove shared state. Process-based scaling reaches the same result by an older and more common path: each worker is a separate process that runs with its own memory, so there is no shared variable to begin with. One question remains: how many workers? The course's measuring axis applies here as well: **what determines this result?** This lesson's observation is the `worker count` row, and its determinant is **environment**.

In this course the word environment appears in two places. In M12/K03 the environment was the application's configuration layer: development, test, and production. Here the environment is one of the rule's three inputs: the clock, the core count, and the environment variables. The two are different things under the same name. M11/K03 showed how processes are born and how they talk through pipes; this lesson asks who determines how many of them there are. The core's `determinant.rb` file and this lesson's excerpt `worker.rb` are given below as they are. Both are source and have no output. The core is a model: the clock and the core count in `environment` come from the definition, not from the real machine.

```ruby
# 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
```

```ruby
# worker.rb — the worker count comes from an environment variable, or else from the core count
require_relative "determinant"

# Process-based scaling: the worker count comes from an environment variable, or else from the core count.
def worker_count(environment) = Integer(environment[:variables].fetch("WORKER_COUNT", environment[:cores]))
```

#### A Forked Process Writes to Its Own Memory

**Forking** starts a copy of the running process. The copy inherits the parent's memory as it was at that moment, but from then on the two memories are separate. If the child changes a variable, the parent does not see it. The block below measures this separation: the child writes ninety-nine to the `value` variable and exits with code three; the parent reads the variable's value and the child's exit code.

```ruby
# child.rb — the child writes to its own memory, the exit code returns to the parent
value = 1
pid = fork do
  value = 99
  exit!(3)
end
Process.wait(pid)
p value
p $?.exitstatus
```

```
1
3
```

The parent sees one for `value`; the ninety-nine the child wrote did not reach the parent. The only thing that came back to the parent was the exit code. This is the isolation that a Ractor enforces by force, arriving for free from the operating system in processes. **RU5.** This measurement shows that forking separates the memory; it does not show how much space the separated memory takes. That question is in the next lesson.

#### Slicing the Work and Collecting It Through a Pipe

Once the memory is separate, the result has to be carried back to the parent. The way to do that is a **pipe**. The block below splits the numbers from one to thirty into three slices of ten, hands each slice to a child, and the children write the slice number and the slice's sum to the same pipe. Since the order in which the children finish is not fixed, the parent sorts the results by number before printing.

```ruby
# slice.rb — the job is sliced across three workers, results are collected through a pipe
reader, writer = IO.pipe
pids = (0..2).map do |index|
  fork do
    reader.close
    slice = ((index * 10) + 1)..((index * 10) + 10)
    writer.puts "#{index} #{slice.sum}"
    writer.close
    exit!(0)
  end
end
writer.close
pids.each { |pid| Process.wait(pid) }
results = reader.each_line.map { |line| line.split.map(&:to_i) }.sort
p results
p results.sum { |_, total| total }
```

```
[[0, 55], [1, 155], [2, 255]]
465
```

The three slices gave fifty-five, one hundred fifty-five, and two hundred fifty-five; the total is four hundred sixty-five, the same as the sum of the numbers from one to thirty. The sort is part of the measurement: without it the lines would arrive in the order the children finished, and that order could change from run to run. **RU6.** This was measured for three children and a small job; the cost of starting a child can exceed the gain when the job is small, and that cost was not measured here.

#### Where the Worker Count Comes From

The worker count is not hard-coded. A deployment can land on a four-core machine or an eight-core one, and the machine is expected to supply the number. `worker_count` looks first at the `WORKER_COUNT` environment variable and takes the core count if it is missing. The block below runs this definition in the core's base, then in an environment with the variable set, and then tries a value that is not a number.

```ruby
# worker_measure.rb — the worker count is read from the environment
require_relative "worker"

p worker_count(BASE.environment)
p worker_count(BASE.environment.merge(variables: { "WORKER_COUNT" => "3" }))
begin
  worker_count(BASE.environment.merge(variables: { "WORKER_COUNT" => "three" }))
rescue ArgumentError => e
  puts e.class
end
p determinant(->(r) { worker_count(r.environment) })
p determinant(->(r) { r.environment[:variables].fetch("WORKER_COUNT", "none") })
```

```
4
3
ArgumentError
["environment", "D- O- E+", 4]
["environment", "D- O- E+", "none"]
```

In the base the output is four: there is no environment variable, and the core count is four. When the variable is three, the count became three. The string `"three"` cannot be converted to an integer, so `ArgumentError` was raised; a wrongly written variable did not fall silently to the default, it blew up. On the fourth line the rule ran this computation in all three variants. Only the environment moved the result; the evidence is `D- O- E+` and the determinant is **environment**. The fifth line asks the same question without falling back to the core count: `"none"` if the environment variable is missing and `"3"` if it is set, and the determinant is again **environment**.

This output also shows a configuration decision: the worker count coming from the environment is not a flaw, it is a deliberate choice. The flaw would be code that does not know what the environment says. **RU7.** The core count is four in the model; on a real machine this number is read with `Etc.nprocessors` and differs from machine to machine.

```ruby
# cores.rb — the real core count is not printed, only that it exists is checked
require "etc"
p Etc.nprocessors.positive?
```

```
true
```

The real number was not printed because it differs on every machine; what was measured is that the number is greater than zero.

#### The Bounding Measurement

Whether the rule also works in a forked process must be tested. In the block below, the sum of the second slice is computed in the child process, inside a call to `determinant`, and the result passes through the pipe to the parent with `Marshal`. The question is: what determines the slice's sum?

```ruby
# bound.rb — the rule classifies the child process's slice sum
require_relative "determinant"

reader, writer = IO.pipe
pid = fork do
  reader.close
  writer.write Marshal.dump(determinant(->(_r) { (11..20).sum }))
  writer.close
  exit!(0)
end
writer.close
Process.wait(pid)
p Marshal.load(reader.read)
```

```
["code", "D- O- E-", 155]
```

The result is one hundred fifty-five and the evidence is `D- O- E-`; the determinant is **code**. The slice's sum comes from the numbers from eleven to twenty; the order in which the child finished, how many children there were, or how many cores the machine had does not change the sum. Process-based scaling separates two things: the value each worker computes belongs to the code, while how many workers run belongs to the environment. This separation matters in deployment: the same code gives the same total with four workers or with eight, but not in the same time.

A server that forks workers once, after the code is loaded, pays the forking cost once. This is called **pre-forking**, and the children share the parent's memory at first and copy only the pages they write to. **RU8.** The memory gain of pre-forking was not measured here; the assumption that the pages a child writes get copied depends on the memory measurement in the next lesson.

#### Summary

- A forked process inherits the parent's memory but writes to its own copy; the only things returning to the parent are the exit code and whatever is written to the pipe.
- Work can be sliced and collected through a pipe; since the order in which the children finish is not fixed, the results are sorted by number.
- The determinant of the worker count is **environment**: it comes from the environment variable if there is one, otherwise from the core count, and its evidence has the form `D- O- E+`.
- An environment variable that is not a number raises `ArgumentError`; it does not fall silently to a default.
- A child's slice sum belongs to the code; how many workers run belongs to the environment.

#### Next Step

Once the workers run with separate memory, the question arises of how many objects each one allocates. The next lesson measures why the same block allocates a thousand and one objects and why the first run costs more than the later ones.

### Memory Management

Source: https://academia.sh/en/courses/ruby-production/memory-management

While the same job runs with the same code, how many objects a block allocates is also a measurement question. The lesson measures allocation counts, the garbage collector's visible counters, and why the first run differs from the later ones.

The previous lesson showed that the environment determines the worker count. Since every worker has its own memory, the next question is what a worker's memory fills up with. That question ties back to the course's measuring axis: **what determines this result?** The number of objects a block allocates is not the work of the machine or of time; it is the work of the block itself. This lesson's observation is the `allocated objects` row, and its determinant is **code**.

M11/K06 asked which resource (compile · run · memory · wait) a cost **moved** to. Here the question is **what determines** the number of allocated objects; the cost is measured, but the class is the determinant. How the garbage collector ages and collects objects was covered in M08/K04. This lesson does not go inside that mechanism; it measures only the numbers readable from the outside, the **allocation** counts. The core's `determinant.rb` file and this lesson's excerpt `memory.rb` are given below as they are; both are source and have no output. The core is a model, and this lesson's measurement uses only the model's rule: the number of allocated objects is the work of the block, not of the running environment.

```ruby
# 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
```

```ruby
# memory.rb — the block runs three times; the allocations of the last run are counted
# Memory: the block runs three times; the first runs are warm-up, the last run's allocations are counted.
def allocated_objects
  result = nil
  3.times do
    before = GC.stat(:total_allocated_objects)
    yield
    result = GC.stat(:total_allocated_objects) - before
  end
  result
end
```

#### The Same Thousand-Element Array, Different Object Counts

`allocated_objects` runs a block three times and takes the difference of the `GC.stat(:total_allocated_objects)` counter on the last one. The counter is the total of the objects allocated so far, and it does not go down when the garbage collector collects an object; so the difference equals the number of objects the block created. The block below builds the same thousand-element array in different ways.

```ruby
# measure.rb — the same array, different object counts
require_relative "memory"

measurements = {
  "i.to_s" => -> { Array.new(1000) { |i| i.to_s } },
  "i" => -> { Array.new(1000) { |i| i } },
  ":constant" => -> { Array.new(1000) { :constant } },
  "\"constant\"" => -> { Array.new(1000) { "constant" } },
  "\"constant\".freeze" => -> { Array.new(1000) { "constant".freeze } },
  "-\"constant\"" => -> { Array.new(1000) { -"constant" } },
  "map" => -> { (1..1000).map { _1 * 2 } },
  "each_slice" => -> { (1..1000).each_slice(10).to_a },
  "<<" => -> { s = +""; 1000.times { s << "a" }; s },
  "+=" => -> { s = ""; 1000.times { s += "a" }; s }
}
measurements.each { |name, block| puts "#{name}: #{allocated_objects(&block)}" }
```

```
i.to_s: 1001
i: 1
:constant: 1
"constant": 1001
"constant".freeze: 1
-"constant": 1
map: 2
each_slice: 107
<<: 1002
+=: 2001
```

Every line reads by the same logic. An integer and a symbol allocate no object; the array itself is a single object, so `i` and `:constant` give one. `i.to_s` and `"constant"` create a new string on every iteration, so they give a thousand and one. A frozen string is immutable and the same object is reused: `"constant".freeze` and `-"constant"` give one. `map` allocates two, for the array and the result array, while `each_slice` allocates a hundred and seven with its hundred intermediate slices. The last two lines show string building: `<<` appends to the same string, `+=` builds a new string at every step, and the count roughly doubles.

None of these lines measures time. Time depends on the machine, while the number of allocated objects depends on the block; this lesson's measure is the second. **RU9.** These counts were measured on Ruby 4.0.7; if the array and string implementations change between versions, the counts change too.

Now the same blocks go to the determinant rule. The chosen block is an integer array of a thousand elements.

```ruby
# rule.rb — what determines the allocation count
require_relative "determinant"
require_relative "memory"

p determinant(->(_r) { allocated_objects { Array.new(1000) { |i| i } } })
```

```
["code", "D- O- E-", 1]
```

The result is one and the evidence is `D- O- E-`: the versions in the lock file, the seed, and the environment changed, and the count did not. The determinant is **code**. Writing `i` instead of `i.to_s` inside the block takes the count from a thousand and one to one, and no other input does that. **RU10.** The rule moves only the core's three inputs; another variable that could affect memory allocation, for example the garbage collector's settings, is not in the model here.

#### The Garbage Collector's Visible Counters

Allocated objects are the garbage collector's business: an object is collected when it becomes unreachable and lives as long as it stays reachable. The block below puts a thousand strings in an array, forces the garbage collector to run, and counts that the strings are alive. Then it checks that the expected keys exist among the garbage collector's counters and that an object's size is positive. The values of the counters are not printed: they differ on every run and on every machine.

```ruby
# gc.rb — garbage collector counters and object size
require "objspace"

retained = Array.new(1000) { |i| i.to_s }
GC.start
p ObjectSpace.each_object(String).count >= 1000
p %i[major_gc_count minor_gc_count heap_live_slots].all? { |key| GC.stat.key?(key) }
p ObjectSpace.memsize_of("a" * 1000).positive?
p retained.size
```

```
true
true
true
1000
```

Three booleans and one number were printed. The strings are alive after the `GC.start` call too, because the `retained` array reaches them. The counters have names; instead of looking at their values we checked that they exist. `ObjectSpace.memsize_of` gives the number of bytes an object takes in memory, and it is positive for a string of a thousand characters. **RU11.** The values themselves, byte counts included, were not printed in the measurement, because they change, and a changing value weakens the trust in the numbers this course carries.

#### The Bounding Measurement

Why does `allocated_objects` run the block three times? Because the first run can differ from the later ones. On the first call the interpreter's internal caches may not be filled yet, and the objects allocated for that preparation are not repeated in later runs. We do not measure the cause here; we measure only the direction of the difference. The block below runs the same block three times and compares the first count with the last.

```ruby
# warmup.rb — the first run is warm-up, the measurement itself is an input
counts = Array.new(3) do
  before = GC.stat(:total_allocated_objects)
  Array.new(1000) { |i| i.to_s }
  GC.stat(:total_allocated_objects) - before
end
p counts.first > counts.last
```

```
true
```

The first run allocated more objects than the last. The same block, the same code, the same inputs; the difference comes from the measurement itself. This is why `allocated_objects` counts the first two runs as warm-up and returns only the last one's difference. A single-run measurement would carry this warm-up share too, and the number would come out larger than the block's real work. The result: how the measurement is made behaves like one of the inputs that determine the result; when a measurement is shared, how the warm-up was handled must be shared with it.

This also shows the rule's limit. `determinant` moves three inputs; it does not move the measurement method itself. The warm-up share is a property of the measurement, not of the code, and it depends on none of the three variants. That is why it is kept inside `allocated_objects`, outside the rule. **RU12.** The size of the warm-up was measured here only by its direction; whether one or two extra runs are enough does not follow from this measurement.

#### Summary

- The number of objects a block allocates is determined by the **code**: the rule saw the same count in all three variants.
- An integer and a symbol allocate no object; a frozen string reuses the same object; `i.to_s` and `"constant"` create a new string on every iteration.
- With string building, `+=` allocates about twice as many objects as `<<`.
- The garbage collector's counters are read with `GC.stat` and an object's size with `ObjectSpace.memsize_of`; the values are not printed, their existence and sign are checked.
- The first run carries a warm-up share; the measurement method itself is an input that affects the result.

#### Next Step

How many objects a block allocates is known; which method of a program is the hot path is not yet known. The next lesson counts call counts and shows that the slow one cannot be read off the call count.

### Profiling

Source: https://academia.sh/en/courses/ruby-production/profiling

Which method of a program is the hot path is asked with call counts, and the call count belongs to the code. The lesson counts calls with TracePoint, measures how a fix changes the count, and shows why a ranking by time depends on the environment.

The previous lesson asked how many objects a block allocates. This lesson asks in which method a program does how much work, but with call counts, not with time. The course's measuring axis does not change: **what determines this result?** This lesson's observation is the `hot path` row, and its determinant is **code**.

**Profiling** is measuring where a program's time or calls gather. M10/K03 measured **how many times** a piece of code ran. Profiling counts calls too, but here the class is not the count itself; it is **what determines** the count. The principles of comparative measurement and profiling were covered in M10/K05; this lesson measures two things using only Ruby's standard library, with no product and no profiling tool: the number of method calls and the kind of time. The **hot path** is the most called method. The core's `determinant.rb` file and this lesson's excerpt `profile.rb` are given below as they are; both are source and have no output. `hot_path` in the excerpt counts calls with `TracePoint`; only the methods the program defines itself are counted.

```ruby
# 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
```

```ruby
# profile.rb — the calls of the methods the program defines are counted
# Profiling: calls to the methods the program defines are counted; the most called is the hot path.
def hot_path
  counts = Hash.new(0)
  trace = TracePoint.new(:call) { |t| counts[t.method_id] += 1 if t.defined_class == Object }
  trace.enable { yield }
  counts.max_by { |_, n| n }
end

def format_money(cents) = format("%d.%02d", cents / 100, cents % 100)
def format_line(line) = "#{line[:name]} #{format_money(line[:unit])} #{format_money(line[:total])}"
def report(lines) = lines.map { |l| format_line(l) }.join("\n")
```

#### Counting Calls

A report of a hundred lines is built: each line has a name, a unit price, and a total. `report` hands each line to `format_line`, which formats both the unit price and the total with `format_money`. The block below runs this report under `TracePoint` and extracts the call count of each method.

```ruby
# count.rb — the call count of each method
require_relative "determinant"
require_relative "profile"

def call_counts
  counts = Hash.new(0)
  trace = TracePoint.new(:call) { |t| counts[t.method_id] += 1 if t.defined_class == Object }
  trace.enable { yield }
  counts.sort_by { |name, n| [-n, name] }
end

lines = Array.new(100) { |i| { name: "product#{i}", unit: 1250, total: 1250 * (i + 1) } }
p call_counts { report(lines) }
p hot_path { report(lines) }
p determinant(->(_r) { hot_path { report(lines) } })
```

```
[[:format_money, 200], [:format_line, 100], [:report, 1]]
[:format_money, 200]
["code", "D- O- E-", [:format_money, 200]]
```

`format_money` was called two hundred times: each of the hundred lines called it twice. `format_line` was called a hundred times and `report` once. The second line is the `hot_path` result, that is, the most called method and its count. The third line runs the rule: the versions in the lock file, the seed, and the environment changed, and the result did not; the determinant is **code**. The call count comes from the number of lines in the report and from how the methods call one another; both are the work of the code. **RU13.** The `defined_class == Object` filter counts only the methods defined at the top level; methods in other classes do not enter this count.

The filter selects only the program's own methods. Ruby's own built-in methods do not produce the `:call` event, they produce the `:c_call` event. Counting the same report with `:c_call` shows which built-in methods are hot.

```ruby
# builtin.rb — the call counts of the built-in methods
require_relative "profile"

lines = Array.new(100) { |i| { name: "product#{i}", unit: 1250, total: 1250 * (i + 1) } }
counts = Hash.new(0)
trace = TracePoint.new(:c_call) { |t| counts[t.method_id] += 1 }
trace.enable { report(lines) }
p counts.sort_by { |name, n| [-n, name] }.first(5)
```

```
[[:[], 300], [:%, 200], [:/, 200], [:format, 200], [:join, 1]]
```

The most called built-in method was `[]`: each line indexes the name, the unit price, and the total, three times. `%`, `/`, and `format` came two hundred times each, because `format_money` was called two hundred times and every call did one division, one remainder, and one `format`. The counts reflect the structure of the code; if we doubled the number of lines, all the counts that depend on lines would double. **RU14.** The sixth place and beyond are not shown here; the `join` in fifth place was called once, and the rest do not come out of this count.

#### A Fix and a New Count

Once the hot path is found, the first question is whether all these calls are needed. In the report the unit price is the same on every line, and printing it once is enough. The fixed version formats the total once per line.

```ruby
# fix.rb — format_money is called once per line
require_relative "profile"

def format_line_short(line) = "#{line[:name]} #{format_money(line[:total])}"
def report_short(lines) = lines.map { |l| format_line_short(l) }.join("\n")

def call_counts
  counts = Hash.new(0)
  trace = TracePoint.new(:call) { |t| counts[t.method_id] += 1 if t.defined_class == Object }
  trace.enable { yield }
  counts.sort_by { |name, n| [-n, name] }
end

lines = Array.new(100) { |i| { name: "product#{i}", unit: 1250, total: 1250 * (i + 1) } }
p call_counts { report_short(lines) }
```

```
[[:format_line_short, 100], [:format_money, 100], [:report_short, 1]]
```

`format_money` is now called a hundred times and has fallen to the same count as `format_line_short`. The hot path stopped being a single method: two methods are called equally. The fix halved the calls but did not settle the question of which one is slow. **RU15.** This measurement shows that the call count was halved; it does not show that the time was halved too.

#### The Bounding Measurement

The call count says which method works the most; it does not say which method is the slowest. A method can be called a hundred times and take long on each call, while another is called two hundred times and takes little each time. Time is a separate measurement and has a separate determinant. Two tools measure time: `Benchmark.realtime` and `Process.clock_gettime(Process::CLOCK_MONOTONIC)`. Both return a floating-point number; the number itself is not printed, because it differs on every run and on every machine.

```ruby
# timing.rb — two tools that measure time, both return a Float
require "benchmark"
require_relative "profile"

lines = Array.new(100) { |i| { name: "product#{i}", unit: 1250, total: 1250 * (i + 1) } }
p Benchmark.realtime { report(lines) }.class
p Process.clock_gettime(Process::CLOCK_MONOTONIC).class
```

```
Float
Float
```

Both tools returned a `Float`; their values were not printed. If a measurement rests on time and the result is "which is slowest", the determinant is the **environment**: if the machine is loaded, if the core is different, or if the cache has warmed up, the ranking changes. The call count is the work of the code, so it comes out the same everywhere. The two do not answer the same question: the answer to which one works more is in the code; the answer to which one takes more time is in the machine.

This is the lesson's bound: the hot path is determined by the call count and classified as code, but "the slowest" lies outside this classification. **RU16.** The time measurement was shown here only by its type; which method is really the slowest was not measured in this lesson and must not be inferred from the call count.

#### Summary

- The call counts of the program's own methods are counted with `TracePoint` on the `:call` event; built-in methods are counted separately with `:c_call`.
- The call count determines the hot path, and the **code** determines the call count: the rule found the same method in all three variants.
- A new count is taken after a fix; the hot path can spread from a single method to several.
- Time is measured with `Benchmark.realtime` or `Process.clock_gettime` and depends on the environment; the call count and the time do not answer the same question.

#### Next Step

The code, the call counts, and the allocated objects have been measured; in hand there is an application whose release is to be cut. The last lesson queries the three things inside a release, the asset name, the release directory, and the manifest, one by one, and finds the determinant of each.

### Deployment

Source: https://academia.sh/en/courses/ruby-production/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.

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.

```ruby
# 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
```

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

#### Symbolic Link, Atomic Switch, and Rollback

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.

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

