Skip to content

Commit cbaf50a

Browse files
committed
Harden stale data directory cleanup with sidecar ownership markers
Fix stale embedded-postgres data directory cleanup by replacing in-directory ownership markers with per-instance sidecar marker files stored in the managed parent directory. This change includes: - sidecar ownership markers named from a hash of the absolute data directory path - stale cleanup keyed off ownership markers instead of epg-lock - deletion of stale directories without requiring epg-lock to exist - releasing any acquired file lock before directory deletion - default data directories now created under the managed parent working directory - updated tests covering sidecar marker creation, stale cleanup behavior, and default directory placement
1 parent be25484 commit cbaf50a

2 files changed

Lines changed: 282 additions & 37 deletions

File tree

src/main/java/com/smushytaco/postgres/embedded/EmbeddedPostgres.java

Lines changed: 114 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import javax.sql.DataSource;
2929
import java.io.*;
3030
import java.net.ServerSocket;
31+
import java.nio.charset.StandardCharsets;
3132
import java.nio.ByteBuffer;
3233
import java.nio.channels.*;
3334
import java.nio.file.*;
@@ -71,6 +72,7 @@ public class EmbeddedPostgres implements Closeable {
7172
private static final String PG_SUPERUSER = "postgres";
7273
private static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(10);
7374
private static final String LOCK_FILE_NAME = "epg-lock";
75+
private static final String OWNERSHIP_MARKER_FILE_NAME = ".embedded-postgres-owned";
7476

7577
private final Path pgDir;
7678

@@ -121,19 +123,20 @@ public class EmbeddedPostgres implements Closeable {
121123
Objects.requireNonNull(this.pgStartupWait, "Wait time cannot be null");
122124

123125
if (parentDirectory != null) {
124-
mkdirs(parentDirectory);
126+
makeDirectories(parentDirectory);
125127
cleanOldDataDirectories(parentDirectory);
126128
this.dataDirectory = Objects.requireNonNullElseGet(dataDirectory, () -> parentDirectory.resolve(instanceId.toString()));
127129
} else {
128130
this.dataDirectory = dataDirectory;
129131
}
130132
if (this.dataDirectory == null) throw new IllegalArgumentException("no data directory");
131133
LOG.trace("{} postgres data directory is {}", instanceId, this.dataDirectory);
132-
mkdirs(this.dataDirectory);
134+
makeDirectories(this.dataDirectory);
135+
markDataDirectoryAsOwned();
133136

134137
final Path lockFile = this.dataDirectory.resolve(LOCK_FILE_NAME);
135138

136-
if (cleanDataDirectory || Files.notExists(this.dataDirectory.resolve("postgresql.conf"))) initdb();
139+
if (cleanDataDirectory || Files.notExists(this.dataDirectory.resolve("postgresql.conf"))) initDb();
137140

138141
this.lockChannel = FileChannel.open(lockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
139142
this.lock = this.lockChannel.tryLock();
@@ -146,7 +149,7 @@ public class EmbeddedPostgres implements Closeable {
146149
throw new IllegalStateException("could not lock " + lockFile);
147150
}
148151

149-
if (dataDirectoryCustomizer != null) dataDirectoryCustomizer.accept(dataDirectory);
152+
if (dataDirectoryCustomizer != null) dataDirectoryCustomizer.accept(this.dataDirectory);
150153

151154
startPostmaster();
152155
}
@@ -268,14 +271,41 @@ private static String formatElapsedSince(final Instant instant) {
268271
return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis);
269272
}
270273

271-
private void initdb() {
274+
private static String ownershipMarkerNameFor(final Path dataDirectory) {
275+
final Path normalizedPath = dataDirectory.toAbsolutePath().normalize();
276+
try {
277+
final MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
278+
final byte[] hash = messageDigest.digest(normalizedPath.toString().getBytes(StandardCharsets.UTF_8));
279+
return OWNERSHIP_MARKER_FILE_NAME + "-" + HexFormat.of().formatHex(hash);
280+
} catch (final NoSuchAlgorithmException e) {
281+
throw new IllegalStateException("SHA-256 algorithm is not available", e);
282+
}
283+
}
284+
285+
private static Path ownershipMarkerPathFor(final Path dataDirectory) {
286+
final Path normalizedPath = dataDirectory.toAbsolutePath().normalize();
287+
final Path parentDirectory = normalizedPath.getParent();
288+
if (parentDirectory == null) throw new IllegalStateException("could not determine parent directory for " + normalizedPath);
289+
return parentDirectory.resolve(ownershipMarkerNameFor(normalizedPath));
290+
}
291+
292+
private void markDataDirectoryAsOwned() {
293+
final Path ownershipMarker = ownershipMarkerPathFor(dataDirectory);
294+
try {
295+
Files.writeString(ownershipMarker, dataDirectory.toAbsolutePath().normalize().toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
296+
} catch (final IOException e) {
297+
throw new IllegalStateException("could not create ownership marker " + ownershipMarker, e);
298+
}
299+
}
300+
301+
private void initDb() {
272302
final Instant start = Instant.now();
273303
final List<String> args = new ArrayList<>(Arrays.asList(
274304
"-A", "trust", "-U", PG_SUPERUSER,
275305
"-D", dataDirectory.toString(), "-E", "UTF-8"));
276306
args.addAll(createLocaleOptions());
277307
system(initDb, args, null);
278-
if (LOG.isInfoEnabled()) LOG.info("{} initdb completed in {}", instanceId, formatElapsedSince(start));
308+
if (LOG.isInfoEnabled()) LOG.info("{} initDb completed in {}", instanceId, formatElapsedSince(start));
279309
}
280310

281311
private void startPostmaster() {
@@ -375,6 +405,7 @@ public void close() throws IOException {
375405
if (cleanDataDirectory && System.getProperty("ot.epg.no-cleanup") == null) {
376406
try {
377407
deleteDirectoryRecursively(dataDirectory);
408+
Files.deleteIfExists(ownershipMarkerPathFor(dataDirectory));
378409
} catch (final IOException _) {
379410
LOG.error("Could not clean up directory {}", dataDirectory.toAbsolutePath());
380411
}
@@ -406,37 +437,79 @@ private void pgCtl(final Path dir, final String action) {
406437
system(pgCtl, args, null);
407438
}
408439

409-
@SuppressWarnings("java:S1141")
440+
@SuppressWarnings({"java:S1141", "java:S1192"})
410441
private void cleanOldDataDirectories(final Path parentDirectory) {
442+
final Path normalizedParentDirectory = parentDirectory.toAbsolutePath().normalize();
411443
try (final Stream<Path> children = Files.list(parentDirectory)) {
412-
for (final Path dir : children.toList()) {
413-
if (!Files.isDirectory(dir)) continue;
444+
for (final Path ownershipMarker : children.filter(Files::isRegularFile).toList()) {
445+
final String fileName = ownershipMarker.getFileName().toString();
446+
if (!fileName.startsWith(OWNERSHIP_MARKER_FILE_NAME + "-")) continue;
447+
448+
final Path dir;
449+
try {
450+
final String content = Files.readString(ownershipMarker).trim();
451+
if (content.isEmpty()) {
452+
Files.deleteIfExists(ownershipMarker);
453+
continue;
454+
}
455+
dir = Path.of(content).toAbsolutePath().normalize();
456+
} catch (final Exception e) {
457+
LOG.warn("Could not read ownership marker {}", ownershipMarker, e);
458+
continue;
459+
}
460+
461+
if (!Objects.equals(dir.getParent(), normalizedParentDirectory)) {
462+
LOG.warn("Skipping ownership marker {} because it points outside {}", ownershipMarker, normalizedParentDirectory);
463+
continue;
464+
}
465+
466+
if (Files.notExists(dir)) {
467+
try {
468+
Files.deleteIfExists(ownershipMarker);
469+
} catch (final IOException e) {
470+
LOG.warn("Could not remove stale ownership marker {}", ownershipMarker, e);
471+
}
472+
continue;
473+
}
414474

415475
final Path theLockFile = dir.resolve(LOCK_FILE_NAME);
416-
if (Files.notExists(theLockFile)) continue;
476+
final Path freshnessFile = Files.exists(theLockFile) ? theLockFile : ownershipMarker;
477+
if (System.currentTimeMillis() - Files.getLastModifiedTime(freshnessFile).toMillis() < 10 * 60 * 1000) continue;
478+
479+
boolean canDelete = false;
480+
if (Files.exists(theLockFile)) {
481+
try (final FileChannel fileChannel = FileChannel.open(theLockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
482+
final FileLock theLock = fileChannel.tryLock()) {
483+
if (theLock != null) canDelete = true;
484+
} catch (final OverlappingFileLockException e) {
485+
LOG.trace("While cleaning old data directories", e);
486+
continue;
487+
} catch (final Exception e) {
488+
LOG.warn("While cleaning old data directories", e);
489+
continue;
490+
}
491+
} else {
492+
canDelete = true;
493+
}
417494

418-
if (System.currentTimeMillis() - Files.getLastModifiedTime(theLockFile).toMillis() < 10 * 60 * 1000) continue;
495+
if (!canDelete) continue;
419496

420-
try (final FileChannel fileChannel = FileChannel.open(theLockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
421-
final FileLock theLock = fileChannel.tryLock()) {
422-
if (theLock != null) {
423-
LOG.info("Found stale data directory {}", dir);
424-
if (Files.exists(dir.resolve("postmaster.pid"))) {
425-
try {
426-
pgCtl(dir, "stop");
427-
LOG.info("Shut down orphaned postmaster!");
428-
} catch (final Exception e) {
429-
if (LOG.isDebugEnabled()) {
430-
LOG.warn("Failed to stop postmaster {}", dir, e);
431-
} else {
432-
LOG.warn("Failed to stop postmaster {}: {}", dir, e.getMessage());
433-
}
434-
}
497+
LOG.info("Found stale data directory {}", dir);
498+
if (Files.exists(dir.resolve("postmaster.pid"))) {
499+
try {
500+
pgCtl(dir, "stop");
501+
LOG.info("Shut down orphaned postmaster!");
502+
} catch (final Exception e) {
503+
if (LOG.isDebugEnabled()) {
504+
LOG.warn("Failed to stop postmaster {}", dir, e);
505+
} else {
506+
LOG.warn("Failed to stop postmaster {}: {}", dir, e.getMessage());
435507
}
436-
deleteDirectoryRecursively(dir);
437508
}
438-
} catch (final OverlappingFileLockException e) {
439-
LOG.trace("While cleaning old data directories", e);
509+
}
510+
try {
511+
deleteDirectoryRecursively(dir);
512+
Files.deleteIfExists(ownershipMarker);
440513
} catch (final Exception e) {
441514
LOG.warn("While cleaning old data directories", e);
442515
}
@@ -447,6 +520,7 @@ private void cleanOldDataDirectories(final Path parentDirectory) {
447520
}
448521

449522

523+
450524
private static Path getWorkingDirectory() {
451525
return Path.of(System.getProperty("ot.epg.working-dir",
452526
Path.of(System.getProperty("java.io.tmpdir")).resolve("embedded-pg").toString()));
@@ -690,7 +764,10 @@ public Builder setDataDirectoryCustomizer(final Consumer<Path> dataDirectoryCust
690764
*/
691765
public EmbeddedPostgres start() throws IOException {
692766
if (builderPort == 0) builderPort = detectFreePort();
693-
if (builderDataDirectory == null) builderDataDirectory = Files.createTempDirectory("epg");
767+
if (builderDataDirectory == null) {
768+
Files.createDirectories(parentDirectory);
769+
builderDataDirectory = Files.createTempDirectory(parentDirectory, "epg-");
770+
}
694771
return new EmbeddedPostgres(parentDirectory, builderDataDirectory, builderCleanDataDirectory, builderRegisterShutdownHook, config,
695772
localeConfig, builderPort, connectConfig, pgBinaryResolver, errRedirector, outRedirector,
696773
pgStartupWait, overrideWorkingDirectory, dataDirectoryCustomizer);
@@ -765,7 +842,7 @@ private void system(final Command command, final List<String> args, final Durati
765842
}
766843
}
767844

768-
private static void mkdirs(final Path dir) {
845+
private static void makeDirectories(final Path dir) {
769846
try {
770847
Files.createDirectories(dir);
771848
} catch (final IOException e) {
@@ -834,7 +911,7 @@ private static void extractTxz(final InputStream stream, final Path targetDir) t
834911
final byte[] content = new byte[(int) entry.getSize()];
835912
final int read = tarIn.read(content, 0, content.length);
836913
if (read == -1) throw new IllegalStateException("could not read " + individualFile);
837-
mkdirs(fsObject.getParent());
914+
makeDirectories(fsObject.getParent());
838915

839916
final AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(fsObject, CREATE, WRITE);
840917
final ByteBuffer buffer = ByteBuffer.wrap(content);
@@ -863,7 +940,7 @@ private void closeChannel(final Channel channel) {
863940
}
864941
});
865942
} else if (entry.isDirectory()) {
866-
mkdirs(fsObject);
943+
makeDirectories(fsObject);
867944
} else {
868945
throw new UnsupportedOperationException(String.format("Unsupported entry found: %s", individualFile));
869946
}
@@ -910,14 +987,14 @@ private static Path prepareBinaries(final PgBinaryResolver pgBinaryResolver, fin
910987
}
911988

912989
try (final DigestInputStream pgArchiveData = new DigestInputStream(pgBinary, MessageDigest.getInstance("SHA-512"));
913-
final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
914-
pgArchiveData.transferTo(baos);
990+
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
991+
pgArchiveData.transferTo(byteArrayOutputStream);
915992

916993
final String pgDigest = HexFormat.of().formatHex(pgArchiveData.getMessageDigest().digest());
917994
final Path workingDirectory = Optional.ofNullable(overrideWorkingDirectory).orElse(getWorkingDirectory());
918995
pgDir = workingDirectory.resolve(String.format("PG-%s", pgDigest));
919996

920-
mkdirs(pgDir);
997+
makeDirectories(pgDir);
921998

922999
final FileStore store = Files.getFileStore(workingDirectory);
9231000
if (store.supportsFileAttributeView(PosixFileAttributeView.class)) {
@@ -938,7 +1015,7 @@ private static Path prepareBinaries(final PgBinaryResolver pgBinaryResolver, fin
9381015
final FileLock unpackLock = fileChannel.tryLock()) {
9391016
if (unpackLock != null) {
9401017
LOG.info("Extracting Postgres...");
941-
try (final ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray())) {
1018+
try (final ByteArrayInputStream bais = new ByteArrayInputStream(byteArrayOutputStream.toByteArray())) {
9421019
extractTxz(bais, pgDir);
9431020
}
9441021
if (Files.notExists(pgDirExists)) {

0 commit comments

Comments
 (0)