Persistence

SafeFileWriter and revisions

Revision helpers, atomic writes with a revision check, backups, restoring, and the rules that keep conflict detection reliable.

A revision is an opaque string of 1 to 128 printable ASCII characters without spaces. For file-backed configuration, use the content hash. The helpers live in com.comficonf.api.io:

HelperUse
Revisions.of(bytes)Lowercase hex SHA-256 of the bytes. Use it with the bytes you just parsed.
Revisions.ofFile(path)Same for a file; Revisions.ABSENT when it does not exist.
SafeFileWriter.write(target, bytes, expectedRevision, policy)Takes a per-path lock, compares the file's revision with expectedRevision (pass null to skip), copies the old file to the backup directory (unless the newest backup already has the same content), writes a temporary file in the same directory, fsyncs it and moves it over the target atomically where the file system supports it. Older backups beyond the retention are deleted only after the move succeeded; when any step fails, the target is unchanged and the backup taken by that call is deleted again. Throws RevisionConflictException (with currentRevision()) when the file changed; nothing is written then. Returns the new revision and the backup path.
BackupPolicy.in(dir) / new BackupPolicy(dir, retain)Backup directory and number of backups kept per file (default 10). Backups are named <file>.<path id>.<yyyyMMdd-HHmmss-SSS>.bak, where the path id is the first 12 hex characters of the SHA-256 of the file's absolute real path, so same-named files in different directories (arenas/a/settings.yml, arenas/b/settings.yml) can share one backup directory.
SafeFileWriter.listBackups(target, policy)Backups of the file, newest first.
SafeFileWriter.restoreLatestBackup(target, policy)Copies the newest backup over the file (backing up the replaced content first).
SafeFileWriter.lockFor(target)The lock used by write, for your own writes to the same file. It is keyed by the path as given, without resolving links.
VoucherConfigProvider.java
try {
    newRevision = SafeFileWriter.write(configFile, updated, context.baseRevision(), backups)
            .newRevision();
} catch (RevisionConflictException e) {
    return ApplyResult.conflict(e.currentRevision());
}

SafeFileWriter puts a new file under the target's name. If the target is a symbolic link, the link is resolved first, so the file it points to is replaced and the link stays. The new file gets the POSIX permissions of the replaced file, and its owner and group where the server process may set them. A hard link of the target is not updated: its other names keep the old content.

Files shared between servers should therefore be symbolic links, not hard links.

Rules that keep conflict detection reliable

  • Compute the revision from the same bytes you parsed in read(). Hashing a re-serialized document or reading the file twice opens a window where an edit goes unnoticed.
  • Never put timestamps or counters into the revision of unchanged content, or every apply ends in CONFLICT.
  • In apply, check baseRevision against the current content before you merge, and pass baseRevision as expectedRevision to SafeFileWriter so the check is repeated immediately before the write.
  • Values that do not live in a file (a database, an in-memory object) need their own revision, for example a version counter that you increment on every write.

Built from f6347d4 on

On this page