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
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user