Files
spring-boot-demo/db-migrations-expand-contract/src/test/java/com/ankurm/expandcontract/Transcript.java
T
asmhatreandClaude Sonnet 5 e478eafda3 Add db-migrations-expand-contract: zero-downtime schema migrations proven with a real 4-deploy rolling run
Companion code for Zero-Downtime Database Migrations: Expand-Contract in Practice
with Spring Boot: a full expand/migrate-writes/migrate-reads/contract sequence run
as an actual rolling deploy across two live replicas, with a load generator sending
continuous HTTP traffic through all four deploys (99.98% success, every residual
error traced to a root cause rather than left unexplained). Findings include a real
NOT NULL constraint trap in the expand migration, a backfill-window bug in the read
switch, H2's AUTO_SERVER=TRUE single-point-of-failure behavior under a rolling
restart, the drain-before-SIGTERM fix needed to close a health-check gap during
graceful shutdown, and H2 silently discarding a concurrently committed INSERT during
an ALTER TABLE ADD/DROP COLUMN rebuild - confirmed, by primary source, to be an
H2-specific behavior rather than a property of the technique itself.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019Fb7vW8vLyLKngBc4R3huA
2026-09-16 19:22:48 +00:00

61 lines
1.8 KiB
Java

package com.ankurm.expandcontract;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
/**
* Writes a named transcript under {@code docs/output/} while the test that produced it runs.
* Every console line quoted in the companion article for this module comes from one of these
* files, and the file is only ever written by a test assertion - never hand-typed.
*/
public final class Transcript {
private static final Path OUTPUT_DIR = Paths.get("docs", "output");
private final StringBuilder buffer = new StringBuilder();
private final String name;
private Transcript(String name) {
this.name = name;
}
public static Transcript start(String name, String heading) {
Transcript t = new Transcript(name);
t.line("=".repeat(Math.min(78, heading.length() + 4)));
t.line(heading);
t.line("=".repeat(Math.min(78, heading.length() + 4)));
t.line("captured: " + Instant.now());
t.line("");
return t;
}
public Transcript line(String text) {
buffer.append(text).append(System.lineSeparator());
System.out.println(text);
return this;
}
public Transcript section(String title) {
line("");
line("-- " + title + " --");
return this;
}
public void write() {
try {
Files.createDirectories(OUTPUT_DIR);
Path target = OUTPUT_DIR.resolve(name + ".txt");
Files.writeString(target, buffer.toString(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}