@@ -18,7 +18,7 @@ use crate::mmds::{MmdsConfig, MmdsError, PgTier};
1818use crate :: pg:: { self , PGDATA } ;
1919
2020const PG_WAL_LINK : & str = "/var/lib/postgresql/18/main/pg_wal" ;
21- const PG_WAL_TARGET : & str = "/var/lib/postgresql/18/wal" ;
21+ const PG_WAL_TARGET : & str = crate :: pg :: PG_WALDIR ;
2222const HOOKS_PRE_START : & str = "/etc/postgresql/18/hooks/pre-start.d" ;
2323
2424#[ derive( Debug , thiserror:: Error ) ]
@@ -216,10 +216,146 @@ async fn maybe_initdb(cfg: &MmdsConfig) -> Result<(), BootError> {
216216
217217 chown_data_tree ( ) ;
218218
219+ // Fast path: a pre-baked PGDATA template shipped in the rootfs (image build
220+ // ran `beyond-pg build-template`). Copy it onto the fresh volume instead of
221+ // running initdb (+ first-boot CREATE EXTENSION) in the guest — both come off
222+ // the cold-boot critical path. The supervisor's `post_start` still resets the
223+ // per-instance superuser password and runs idempotent `CREATE EXTENSION IF NOT
224+ // EXISTS` (no-ops against the baked extensions), so the result is identical to
225+ // a runtime initdb.
226+ if template_available ( ) {
227+ info ! ( "materializing PGDATA from template, skipping initdb" ) ;
228+ materialize_template ( ) ?;
229+ return Ok ( ( ) ) ;
230+ }
231+
219232 run_initdb ( & cfg. postgres_password ) . await ?;
220233 Ok ( ( ) )
221234}
222235
236+ /// True iff a complete pre-baked PGDATA template is present in the rootfs.
237+ fn template_available ( ) -> bool {
238+ template_available_in ( crate :: template:: TEMPLATE_DIR )
239+ }
240+
241+ fn template_available_in ( template_dir : & str ) -> bool {
242+ Path :: new ( & format ! ( "{template_dir}/main/PG_VERSION" ) ) . exists ( )
243+ }
244+
245+ /// Copy the baked PGDATA template onto the fresh data volume, then chown it.
246+ fn materialize_template ( ) -> Result < ( ) , BootError > {
247+ materialize_template_into ( crate :: template:: TEMPLATE_DIR , PGDATA , PG_WAL_TARGET ) ?;
248+ chown_data_tree ( ) ;
249+ info ! ( "materialized PGDATA from template" ) ;
250+ Ok ( ( ) )
251+ }
252+
253+ /// Copy the template at `template_dir` (`main/` + `wal/`) onto `pgdata` + `wal_target`.
254+ ///
255+ /// Atomic + idempotent (per CLAUDE.md): the template is copied into staging dirs
256+ /// alongside the destinations, the `pg_wal` symlink is repointed to `wal_target`,
257+ /// the staged data is flushed, and only then are the staging dirs renamed into
258+ /// place. `pgdata/PG_VERSION` therefore becomes visible only once a complete,
259+ /// correct PGDATA is published — so `maybe_initdb`'s skip-on-`PG_VERSION` is safe
260+ /// and an interrupted materialize is simply redone on the next boot.
261+ fn materialize_template_into (
262+ template_dir : & str ,
263+ pgdata : & str ,
264+ wal_target : & str ,
265+ ) -> Result < ( ) , BootError > {
266+ let tmpl_main = format ! ( "{template_dir}/main" ) ;
267+ let tmpl_wal = format ! ( "{template_dir}/wal" ) ;
268+ let main_staging = format ! ( "{pgdata}.staging" ) ;
269+ let wal_staging = format ! ( "{wal_target}.staging" ) ;
270+
271+ // Idempotent clean slate. `pgdata` may exist empty (partial-PGDATA cleanup
272+ // recreated it above); a prior interrupted materialize may have left a wal
273+ // dir or *.staging dirs.
274+ for p in [ & main_staging, & wal_staging, & pgdata. to_string ( ) , & wal_target. to_string ( ) ] {
275+ rm_rf ( p) ?;
276+ }
277+
278+ // `cp -a` preserves the pg_wal symlink, file modes, and postgres ownership.
279+ cp_a ( & tmpl_wal, & wal_staging) ?;
280+ cp_a ( & tmpl_main, & main_staging) ?;
281+
282+ // Repoint the baked `pg_wal` symlink (template-relative) to the runtime WAL
283+ // dir BEFORE publishing, so a crash after the rename never leaves a wrong
284+ // target that `verify_wal_symlink` would reject.
285+ let staged_link = format ! ( "{main_staging}/pg_wal" ) ;
286+ match std:: fs:: symlink_metadata ( & staged_link) {
287+ Ok ( _) => std:: fs:: remove_file ( & staged_link) ?,
288+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => { }
289+ Err ( e) => return Err ( BootError :: Io ( e) ) ,
290+ }
291+ std:: os:: unix:: fs:: symlink ( wal_target, & staged_link) ?;
292+
293+ // Flush the staged tree (file contents + dir entries) so the data is durable
294+ // BEFORE it is published. Then rename atomically — WAL first, then PGDATA, so
295+ // `PG_VERSION` only appears once `main` is renamed, by which point both the
296+ // symlink and the data are correct and durable. Finally fsync the parent dir
297+ // to make the renames themselves durable. Targeted fsync (not a global
298+ // sync(2)) keeps this fast and non-blocking under concurrent I/O.
299+ fsync_tree ( Path :: new ( & wal_staging) ) ?;
300+ fsync_tree ( Path :: new ( & main_staging) ) ?;
301+ std:: fs:: rename ( & wal_staging, wal_target) ?;
302+ std:: fs:: rename ( & main_staging, pgdata) ?;
303+ if let Some ( parent) = Path :: new ( pgdata) . parent ( ) {
304+ fsync_dir ( parent) ?;
305+ }
306+
307+ Ok ( ( ) )
308+ }
309+
310+ /// Recursively fsync every file and directory under `path` (depth-first, so each
311+ /// directory is fsynced after its entries). Symlinks are not followed — the
312+ /// containing directory's fsync makes the symlink entry durable.
313+ fn fsync_tree ( path : & Path ) -> Result < ( ) , BootError > {
314+ let meta = std:: fs:: symlink_metadata ( path) ?;
315+ if meta. is_dir ( ) {
316+ for entry in std:: fs:: read_dir ( path) ? {
317+ fsync_tree ( & entry?. path ( ) ) ?;
318+ }
319+ fsync_dir ( path) ?;
320+ } else if meta. is_file ( ) {
321+ std:: fs:: File :: open ( path) ?. sync_all ( ) ?;
322+ }
323+ Ok ( ( ) )
324+ }
325+
326+ /// fsync a directory (durably commit its entries). On Linux a directory can be
327+ /// opened read-only and `fsync`'d via `sync_all`.
328+ fn fsync_dir ( path : & Path ) -> Result < ( ) , BootError > {
329+ std:: fs:: File :: open ( path) ?. sync_all ( ) ?;
330+ Ok ( ( ) )
331+ }
332+
333+ /// `rm -rf` a path (file, symlink, or directory). Idempotent — a missing path is
334+ /// not an error.
335+ fn rm_rf ( path : & str ) -> Result < ( ) , BootError > {
336+ match std:: fs:: symlink_metadata ( path) {
337+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => Ok ( ( ) ) ,
338+ Err ( e) => Err ( BootError :: Io ( e) ) ,
339+ Ok ( meta) if meta. is_dir ( ) => Ok ( std:: fs:: remove_dir_all ( path) ?) ,
340+ Ok ( _) => Ok ( std:: fs:: remove_file ( path) ?) ,
341+ }
342+ }
343+
344+ /// `cp -a src dst` — recursive copy preserving symlinks, modes, and ownership.
345+ fn cp_a ( src : & str , dst : & str ) -> Result < ( ) , BootError > {
346+ let status = std:: process:: Command :: new ( "cp" )
347+ . args ( [ "-a" , src, dst] )
348+ . status ( ) ?;
349+ if status. success ( ) {
350+ Ok ( ( ) )
351+ } else {
352+ Err ( BootError :: Io ( std:: io:: Error :: other ( format ! (
353+ "cp -a {src} {dst} failed: {status}"
354+ ) ) ) )
355+ }
356+ }
357+
358+
223359/// chown `/var/lib/postgresql` → postgres recursively. A fresh durable volume
224360/// mounts root-owned over the image dir, and a root-run `pg_basebackup` (replica
225361/// seeding) writes a root-owned PGDATA — either way postgres dies at startup with
@@ -258,7 +394,7 @@ async fn run_initdb(password: &str) -> Result<(), BootError> {
258394 . to_str ( )
259395 . ok_or_else ( || BootError :: Io ( std:: io:: Error :: other ( "tempfile path is not UTF-8" ) ) ) ?;
260396 info ! ( "running initdb" ) ;
261- pg:: initdb ( PGDATA , path_str)
397+ pg:: initdb ( PGDATA , PG_WAL_TARGET , path_str)
262398 . await
263399 . map_err ( |e| BootError :: InitDb ( e. to_string ( ) ) )
264400 // pwfile is dropped here — tempfile removes it from disk
@@ -465,6 +601,128 @@ mod tests {
465601 ) ;
466602 }
467603
604+ // -----------------------------------------------------------------------
605+ // PGDATA template materialize unit tests
606+ // -----------------------------------------------------------------------
607+
608+ /// Build a minimal fake template at `dir`: `main/` with PG_VERSION + a
609+ /// template-relative `pg_wal` symlink, and `wal/` with a segment file.
610+ fn make_fake_template ( dir : & Path ) {
611+ let tmain = dir. join ( "main" ) ;
612+ let twal = dir. join ( "wal" ) ;
613+ std:: fs:: create_dir_all ( & tmain) . unwrap ( ) ;
614+ std:: fs:: create_dir_all ( & twal) . unwrap ( ) ;
615+ std:: fs:: write ( tmain. join ( "PG_VERSION" ) , "18\n " ) . unwrap ( ) ;
616+ std:: fs:: write ( tmain. join ( "postgresql.conf" ) , "# initdb default\n " ) . unwrap ( ) ;
617+ std:: fs:: write ( twal. join ( "000000010000000000000001" ) , b"wal-seg" ) . unwrap ( ) ;
618+ // Baked symlink points template-relative — WRONG for runtime; the
619+ // materialize must repoint it to the real wal target.
620+ std:: os:: unix:: fs:: symlink ( & twal, tmain. join ( "pg_wal" ) ) . unwrap ( ) ;
621+ }
622+
623+ #[ test]
624+ fn template_available_detects_pg_version ( ) {
625+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
626+ let dir = tmp. path ( ) . join ( "template" ) ;
627+ assert ! ( !template_available_in( dir. to_str( ) . unwrap( ) ) ) ;
628+ std:: fs:: create_dir_all ( dir. join ( "main" ) ) . unwrap ( ) ;
629+ assert ! (
630+ !template_available_in( dir. to_str( ) . unwrap( ) ) ,
631+ "empty main/ is not a usable template"
632+ ) ;
633+ std:: fs:: write ( dir. join ( "main/PG_VERSION" ) , "18" ) . unwrap ( ) ;
634+ assert ! ( template_available_in( dir. to_str( ) . unwrap( ) ) ) ;
635+ }
636+
637+ #[ test]
638+ fn materialize_copies_template_and_repoints_wal_symlink ( ) {
639+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
640+ let template = tmp. path ( ) . join ( "template" ) ;
641+ make_fake_template ( & template) ;
642+
643+ let data = tmp. path ( ) . join ( "data" ) ;
644+ std:: fs:: create_dir_all ( & data) . unwrap ( ) ;
645+ let pgdata = data. join ( "main" ) ;
646+ let wal_target = data. join ( "wal" ) ;
647+
648+ materialize_template_into (
649+ template. to_str ( ) . unwrap ( ) ,
650+ pgdata. to_str ( ) . unwrap ( ) ,
651+ wal_target. to_str ( ) . unwrap ( ) ,
652+ )
653+ . unwrap ( ) ;
654+
655+ // PGDATA + WAL contents present.
656+ assert ! ( pgdata. join( "PG_VERSION" ) . exists( ) ) ;
657+ assert ! ( pgdata. join( "postgresql.conf" ) . exists( ) ) ;
658+ assert ! ( wal_target. join( "000000010000000000000001" ) . exists( ) ) ;
659+ // pg_wal repointed to the absolute runtime WAL target (what
660+ // verify_wal_symlink expects), not the template-relative path.
661+ assert_eq ! (
662+ std:: fs:: read_link( pgdata. join( "pg_wal" ) ) . unwrap( ) ,
663+ wal_target
664+ ) ;
665+ // No staging dirs left behind.
666+ assert ! ( !data. join( "main.staging" ) . exists( ) ) ;
667+ assert ! ( !data. join( "wal.staging" ) . exists( ) ) ;
668+ }
669+
670+ #[ test]
671+ fn materialize_is_idempotent ( ) {
672+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
673+ let template = tmp. path ( ) . join ( "template" ) ;
674+ make_fake_template ( & template) ;
675+ let data = tmp. path ( ) . join ( "data" ) ;
676+ std:: fs:: create_dir_all ( & data) . unwrap ( ) ;
677+ let pgdata = data. join ( "main" ) ;
678+ let wal_target = data. join ( "wal" ) ;
679+
680+ let run = || {
681+ materialize_template_into (
682+ template. to_str ( ) . unwrap ( ) ,
683+ pgdata. to_str ( ) . unwrap ( ) ,
684+ wal_target. to_str ( ) . unwrap ( ) ,
685+ )
686+ } ;
687+ run ( ) . unwrap ( ) ;
688+ // A second materialize over a published PGDATA succeeds (clean-slate
689+ // removes the prior copy) and yields the same correct layout.
690+ run ( ) . unwrap ( ) ;
691+ assert ! ( pgdata. join( "PG_VERSION" ) . exists( ) ) ;
692+ assert_eq ! (
693+ std:: fs:: read_link( pgdata. join( "pg_wal" ) ) . unwrap( ) ,
694+ wal_target
695+ ) ;
696+ }
697+
698+ #[ test]
699+ fn materialize_recovers_from_leftover_staging ( ) {
700+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
701+ let template = tmp. path ( ) . join ( "template" ) ;
702+ make_fake_template ( & template) ;
703+ let data = tmp. path ( ) . join ( "data" ) ;
704+ std:: fs:: create_dir_all ( & data) . unwrap ( ) ;
705+ let pgdata = data. join ( "main" ) ;
706+ let wal_target = data. join ( "wal" ) ;
707+
708+ // Simulate an interrupted prior run: stale staging dirs + an empty PGDATA
709+ // (as partial-PGDATA cleanup would have recreated).
710+ std:: fs:: create_dir_all ( data. join ( "main.staging" ) ) . unwrap ( ) ;
711+ std:: fs:: write ( data. join ( "main.staging/garbage" ) , b"x" ) . unwrap ( ) ;
712+ std:: fs:: create_dir_all ( data. join ( "wal.staging" ) ) . unwrap ( ) ;
713+ std:: fs:: create_dir_all ( & pgdata) . unwrap ( ) ;
714+
715+ materialize_template_into (
716+ template. to_str ( ) . unwrap ( ) ,
717+ pgdata. to_str ( ) . unwrap ( ) ,
718+ wal_target. to_str ( ) . unwrap ( ) ,
719+ )
720+ . unwrap ( ) ;
721+
722+ assert ! ( pgdata. join( "PG_VERSION" ) . exists( ) ) ;
723+ assert ! ( !pgdata. join( "garbage" ) . exists( ) , "stale staging must not leak in" ) ;
724+ }
725+
468726 // -----------------------------------------------------------------------
469727 // run_hook_scripts unit tests
470728 // -----------------------------------------------------------------------
0 commit comments