1818
1919import logging
2020import os
21+ import shutil
2122import subprocess
2223import threading
2324
2829_lock = threading .RLock ()
2930_data_dir = None # remembered so stop() can target the right cluster
3031
32+ # Written only after initdb AND our config edits both succeed, so its presence
33+ # proves a *complete* cluster. PG_VERSION alone is not enough: initdb writes it
34+ # partway through, so a crash/SIGKILL mid-initdb leaves a half-built dir that
35+ # looks initialized forever and wedges every later start.
36+ _READY_MARKER = "audiomuse_initialized"
37+
3138
3239def _pg_env ():
3340 """Environment for the bundled Postgres tools.
@@ -56,8 +63,41 @@ def _bin(name):
5663
5764
5865def _initialized (data_dir ):
59- # PG_VERSION exists only after a successful initdb.
60- return os .path .exists (os .path .join (data_dir , "PG_VERSION" ))
66+ if os .path .exists (os .path .join (data_dir , _READY_MARKER )):
67+ return True
68+ # Legacy clusters created before the completion marker existed: adopt them if
69+ # initdb actually finished. global/pg_control is written by initdb and is
70+ # required for the server to start, so treat it as the completeness signal,
71+ # then stamp the marker so future starts take the fast path.
72+ if os .path .exists (os .path .join (data_dir , "global" , "pg_control" )):
73+ try :
74+ with open (os .path .join (data_dir , _READY_MARKER ), "w" , encoding = "utf-8" ) as fh :
75+ fh .write ("ok\n " )
76+ except OSError :
77+ pass
78+ return True
79+ return False
80+
81+
82+ def _reset_data_dir (data_dir ):
83+ """Empty a partially-initialized data dir so initdb can retry.
84+
85+ initdb refuses a non-empty target, so a half-built cluster (interrupted
86+ initdb, no completion marker) would otherwise wedge every start forever.
87+ Only ever called when :func:`_initialized` is False, so a complete cluster's
88+ data is never touched."""
89+ if not (os .path .isdir (data_dir ) and os .listdir (data_dir )):
90+ return
91+ logger .warning ("Clearing incomplete PostgreSQL data dir %s before re-init" , data_dir )
92+ for entry in os .listdir (data_dir ):
93+ target = os .path .join (data_dir , entry )
94+ try :
95+ if os .path .isdir (target ) and not os .path .islink (target ):
96+ shutil .rmtree (target )
97+ else :
98+ os .unlink (target )
99+ except OSError :
100+ logger .exception ("Could not remove %s" , target )
61101
62102
63103def _dsn (data_dir ):
@@ -100,6 +140,11 @@ def _init_cluster(data_dir, env):
100140 fh .write ("\n # --- AudioMuse-AI embedded overrides ---\n " )
101141 fh .write (f"unix_socket_directories = '{ data_dir } '\n " )
102142 fh .write ("listen_addresses = ''\n " )
143+ # Stamp the completion marker last: only now is the cluster fully usable, so
144+ # an interruption before this point leaves _initialized() False and triggers
145+ # a clean re-init on the next start.
146+ with open (os .path .join (data_dir , _READY_MARKER ), "w" , encoding = "utf-8" ) as fh :
147+ fh .write ("ok\n " )
103148
104149
105150def start (data_dir ):
@@ -109,6 +154,7 @@ def start(data_dir):
109154 env = _pg_env ()
110155 if not _initialized (data_dir ):
111156 logger .info ("Initializing embedded PostgreSQL cluster at %s" , data_dir )
157+ _reset_data_dir (data_dir ) # clear any half-built cluster first
112158 _init_cluster (data_dir , env )
113159 if not _is_running (data_dir , env ):
114160 _run_checked ([_bin ("pg_ctl" ), "-D" , data_dir , "-w" , "start" ], env )
0 commit comments