Class AtomicConfigWriter

java.lang.Object
com.ultikits.ultitools.config.document.AtomicConfigWriter

@Internal public final class AtomicConfigWriter extends Object
The only way the config storage layer replaces a file.

Normally the new UTF-8 text is written to a same-directory temporary file with the target's POSIX permissions, forced to disk, and atomically moved over the target. Eligible atomic-replace failures (unsupported atomic moves, EBUSY or EXDEV) and temporary-creation permission/read-only refusals use a narrow fallback: exclusively create a backup named <file name>.ultitools-backup-<16 lower-case hex>, copy and force the old content, then overwrite and force the existing target in place. A backup this server run already recorded for the same target, whose bytes still match the record, is refreshed through a forced same-directory temporary and atomic replacement before the target is opened. A complete forced backup remains until the next successful strict UTF-8 load, which deletes it only while its bytes still match what the writer recorded; any other file of that pattern is kept and named once at INFO. Each fallback attempt logs its path, cause and outcome.

Why it cannot overwrite operator content. The writer reads, writes or deletes only the target it was given and names of its own two patterns, <file name>.tmp-<16 lower-case hex> and the backup pattern above; an operator's <file>.bak, or any other name, is never touched (UltiKits/UltiTools-Reborn#601).

Before the in-place target is opened, failures leave its bytes unchanged. Once it is open, a failure (a write that stops part-way, a failed force or close) puts the target back before the failure is reported: the bytes the target held immediately before - exactly what the forced backup holds - are written back in place, the same way, because the atomic move this fallback stands in for was refused; the backup is then removed, so the target holds its previous bytes when the IOException reaches the caller (UltiKits/UltiTools-Reborn#622). If putting it back fails too, one SEVERE names the target and the backup (never content), the backup is kept and no later load deletes it, and the failure is a AtomicConfigWriter.RestoreFailedException: the target may hold part of the new content. A symbolic link stays a link: writes and backup cleanup use its resolved target. Stale temporary files are cleaned on load; live staged writes are protected. The atomic path copies only POSIX permission bits, not ownership, ACLs or extended attributes. Directory syncing is best effort. This is a framework-internal writer for config files; crash-safe writes of a module's own files are a separate, later feature (UltiKits/UltiTools-Reborn#545).

Since:
6.3.0
  • Field Details

  • Constructor Details

    • AtomicConfigWriter

      private AtomicConfigWriter()
  • Method Details

    • underWriteLock

      public static <T> T underWriteLock(Supplier<T> section)
      Runs section while holding the lock every configuration load, last-moment check and replacement in this JVM holds. A writer outside this package that compares a file's bytes and then replaces them (the panel's files.write) runs both steps here, so a configuration commit can never land between its comparison and its replacement, and its replacement can never land between a configuration commit's re-read and its rename: one of the two sees the other's bytes and refuses. The section must not block on anything but the file system.
      Type Parameters:
      T - the section's result type
      Parameters:
      section - the compare-and-replace to run
      Returns:
      what section returned
      Since:
      6.3.1
    • write

      public static void write(Path target, String text) throws IOException
      Replaces target with text, encoded as UTF-8.
      Parameters:
      target - the file to replace; its parent directory must exist
      text - the complete new content
      Throws:
      IOException - if a step fails; the target then holds its previous bytes, except after a AtomicConfigWriter.RestoreFailedException, when its complete backup remains beside it
    • write

      static void write(Path target, String text, AtomicConfigWriter.FileOperations files) throws IOException
      Throws:
      IOException
    • stage

      public static AtomicConfigWriter.StagedWrite stage(Path target, String text) throws IOException
      Writes text to a temporary file beside target without touching the target, so several files can be prepared before any of them is replaced. The caller must commit or discard it, and may load the target in between; in-process staged files are protected from stale cleanup. If temporary creation is refused for permissions or a read-only file system, staging keeps the UTF-8 bytes in memory without touching the target or backup. Commit attempts the backed in-place path.
      Parameters:
      target - the file to replace later; its parent directory must exist
      text - the complete new content
      Returns:
      the staged write
      Throws:
      IOException - if the temporary file cannot be written; nothing is then left behind
    • stage

      Throws:
      IOException
    • stage

      public static AtomicConfigWriter.StagedWrite stage(Path target, byte[] data) throws IOException
      As stage(Path, String), for content that is not UTF-8 text: the bytes are written exactly as given. Used for a file whose reader is not UTF-8 only (server.properties, which the server reads as ISO-8859-1 when it is not valid UTF-8), so its other lines can keep their bytes.
      Parameters:
      target - the file to replace later; its parent directory must exist
      data - the complete new content
      Returns:
      the staged write
      Throws:
      IOException - if the temporary file cannot be written; nothing is then left behind
    • stage

      static AtomicConfigWriter.StagedWrite stage(Path target, byte[] content, AtomicConfigWriter.FileOperations files) throws IOException
      Throws:
      IOException
    • deleteStaleTemporaries

      static void deleteStaleTemporaries(Path target)
      Deletes the temporary files an interrupted write left beside target: only names of the exact form <target file name>.tmp-<16 lower-case hex digits>. Each deletion is logged at FINE. Failures are ignored; the next load tries again.
      Parameters:
      target - the config file about to be loaded
    • release

      private static void release(Path identity)
    • temporaryIdentity

      private static Path temporaryIdentity(Path path) throws IOException
      Identifies a reserved file through its real parent, even before the file exists.
      Throws:
      IOException
    • isTemporaryOf

      static boolean isTemporaryOf(String fileName, String candidate)
    • temporaryName

      static String temporaryName(String fileName)
    • isBackupOf

      static boolean isBackupOf(String fileName, String candidate)
      Whether candidate has the exact form <fileName>.ultitools-backup-<16 lower-case hex>.
    • randomName

      private static String randomName(String fileName, String infix)
    • temporaryCreationRefused

      private static boolean temporaryCreationRefused(IOException failure)
    • atomicReplacementRefused

      private static boolean atomicReplacementRefused(IOException failure)
    • writeAll

      private static void writeAll(AtomicConfigWriter.FileOperations files, FileChannel channel, byte[] data) throws IOException
      Throws:
      IOException
    • deleteBackupAfterLoad

      static void deleteBackupAfterLoad(Path destination, AtomicConfigWriter.FileOperations files)
      After a successful strict load: deletes the backup this server run recorded for destination while its bytes still match the record, and names once at INFO every other file of the backup pattern beside it (left by an earlier run, or changed since it was written), which is kept. Nothing else is read or deleted. Failures are logged at FINE; the next load tries again. Called under WRITE_LOAD_LOCK.
      Parameters:
      destination - the loaded file (a symbolic link already resolved)
      files - the file operations
    • deleteRecorded

      private static void deleteRecorded(Path destination, Path backup, AtomicConfigWriter.FileOperations files)
    • resolve

      static Path resolve(Path target) throws IOException
      The file actually replaced: the target itself, or the file an existing symbolic link points to.
      Throws:
      IOException
    • deleteQuietly

      private static void deleteQuietly(Path temporary)
    • syncDirectory

      private static void syncDirectory(Path directory)