Skip to content

ALTER DB: never damage an existing target database (#2800) - #2801

Closed
WingedGuardian wants to merge 1 commit into
BaseXdb:mainfrom
WingedGuardian:alterdb-target-aside
Closed

WingedGuardian wants to merge 1 commit into
BaseXdb:mainfrom
WingedGuardian:alterdb-target-aside

Conversation

@WingedGuardian

Copy link
Copy Markdown

Fixes #2800.

Problem

AlterDB.alter dropped an existing target database (DropDB.drop, result ignored) before
moving the source into place, and ALTER DB checked only the source for being opened.
On Windows, a file that another process holds open cannot be deleted. The drop then stopped part
way through, the rename failed, and the target was left with files missing and could no longer be
opened. The error named only the source ("Database 'A' could not be renamed.").

Change

org.basex.core.cmd.AlterDB:

  1. run(): an existing target is treated like the source. It is closed if the current context
    holds it, and the command is refused with DB_PINNED_X naming the target if it is still
    pinned or locked (Context.pinned, the same check as for the source and as in db:alter,
    which already checks the new name via NameUpdate.close).
  2. alter(): instead of deleting the target first, it is renamed aside to a hidden sibling
    (.<target>.<n>; dot-directories are skipped by Databases.list). Then the source is moved
    into place, and only then is the old target deleted. If the source cannot be moved, the target
    is moved back. On Windows, renaming a directory fails cleanly while one of its files is open,
    so a foreign handle now makes the operation fail without touching either database.

db:alter, db:create (temporary database replacing an existing one) and OPTIMIZE ALL call the
same alter() method and get the same guarantee.

Behaviour

Measured on Windows 11, Java 24, with the patched class on top of the 13.0 jar (two processes on
one DBPATH):

Situation Before After
nothing held target replaced target replaced; no aside directory left behind
another BaseX process has the target open refused only by luck of file order (13.0); target damaged (12.4) refused: "Database 'B' is opened by another process."; target intact
foreign handle on inf, tbl, txtl or atv of the target target unopenable (except atv) refused; target intact and opens
server client has OPEN B as for another process refused, naming B; target intact
another process has the source open refused refused (unchanged)

POSIX

On Linux/macOS, open files never blocked deleting or renaming, so the old code "succeeded" there.
The new code also succeeds there. The aside rename is a directory rename within the same parent,
which is atomic. A reader holding files of the old target keeps its unlinked inodes, exactly as
after the old drop. The new pinned(target) check refuses where the target is opened by another
BaseX instance. That case was previously allowed on POSIX and could delete files under a
reader, so this is a deliberate behaviour change, consistent with how the source is handled.

Tests

New org.basex.core.AlterDBTest:

  • replace: a target is replaced and no hidden aside directory remains
  • targetOpened: target opened by another session leads to refusal naming the target (command), DB_LOCK1_X (db:alter)
  • targetFileOpened (Windows only, via Assumptions): a RandomAccessFile on the target's
    inf.basex leads to the command and db:alter failing with the target intact; the command succeeds after the handle closes
  • sourceFileOpened (Windows only): the source cannot be moved, so the target is restored

On POSIX an open file handle cannot block a rename, so the handle-based cases cannot be simulated
there and are skipped. targetOpened runs everywhere.

Without the fix, targetOpened and targetFileOpened fail. With it: AlterDBTest 4/4,
CommandTest 43/43, ServerCommandTest, DbModuleTest 56/56 and DbModuleConcurrencyTest all pass.
These were run on Windows against the 13.0 jar; there was no full Maven build.

Open points

  • If a foreign handle blocks the aside rename, the error is still the generic
    DB_NOT_RENAMED_X naming the source. Naming the target there would need a new message key in
    every .lang file, so I left it out. The target is no longer modified in that case.
  • If deleting the aside directory fails after a successful rename (a handle opened in the gap
    between rename and delete), a hidden .<target>.<n> directory is left behind and logged with
    Util.debugln only.

🤖 Generated with Claude Code

An existing target is now checked for being pinned like the source, and is
renamed aside before the source is moved in; it is deleted only after the
move succeeded, and restored if the move fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ChristianGruen

Copy link
Copy Markdown
Member

See 29d0262

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ALTER DB onto an existing database that another process has open (Windows) fails and leaves the target unopenable

2 participants