Archive gets created despite failed database hook #1032
Labels
No labels
blocked
breaking
bug
data loss
design finalized
good first issue
new feature area
question / support
security
waiting for response
No milestone
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
borgmatic-collective/borgmatic#1032
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What I'm trying to do and why
When a database hook fails (see reproduction for full example), the
borgmatic createcommand fails (as expected). However, an archive gets created nevertheless (unexpected).As a result, there is a correct looking archive with incomplete data.
IMHO, this is problematic for at least the following reasons:
Steps to reproduce
borgmatic create log
Actual behavior
Expected behavior
Other notes / implementation ideas
I will keep investigating if I can find a fix/workaround for this.
borgmatic version
1.7.7 and 1.9.14
borgmatic installation method
Debian package on bookworm for 1.7.7 / pip install borgmatic for 1.9.14
Borg version
borg 1.2.4
Python version
Python 3.11.2
Database version (if applicable)
No response
Operating system and version
Debian 12 (bookworm)
I have looked at the current implementation and existing borg options, and I'm afraid it looks like this is not fixable without additional support from borg.
Note that there is the
--content-from-commandwhich pretty much fulfills this need:https://borgbackup.readthedocs.io/en/stable/usage/create.html#reading-backup-data-from-stdin
Unfortunately, it only supports a single command.
Thanks for filing this! I swear this limitation was discussed in a previous ticket, but I can't seem to find it. If I recall correctly though, the consensus was basically what you found—there's not really a good solution right now given: 1. Borg's lack of support for anything like this, and: 2. borgmatic's current approach of streaming data sources to Borg via multiple named pipes. And yeah,
--content-from-commandwon't work for the general case because of that borgmatic support for shoving multiple data sources into a single archive.Just brainstorming though, here are a few different ideas:
Another idea:
EDIT: Yet another idea:
Heya, thanks a lot for the quick and comprehensive reply.
My thoughts:
keeping a partial archive instead of deleting
100% agree with you there. a partial archive is better than no archive, as long as it is clear it is partial.
deleting / adjusting / ... the archive on database error
I would caution against "going back and fixing things", as it is not crash safe (arguably, it's a tiny fraction of time, but it can still happen that a full archive gets created when it shouldn't and the removal / conversion to checkpoint fails).
Instead, have you considered creating a checkpoint(-like) archive first and renaming it on success? It feels this could be quite easily added:
borg create <archive-name>.checkpoint(borg 1.2.4 let's you do this)borg rename <archive-name>.checkpoint <archive-name>(borg 1.2.4 let's you do this)IIUC this would pretty much give us the behavior we'd want:
WDYT? (we should probably talk to the borg maintainer's to make sure they are onboard with borgmatic using this behavior).
killing the borg process with & pipe
I have been toying with this idea as well; I have even considered building it in user space. If necessary, it feels to me this would be better than having this bug.
IMHO the additional zero byte file is absolutely acceptable. However, I'm a bit concerned that the additional pipe magic will add quite some complexity to borgmatic itself (you're the better judge of this of course).
FWIW: I think the "create a checkpoint" option is really neat. Let me know what you think about it.
I like your "start with a checkpoint" idea as well, and I agree that the sentinel pipe approach feels really brittle. I've asked the Borg dev about the checkpoint approach on IRC; we'll see what he says. The only downside I can see is that we'd lose Borg 1.0.x compatibility, but that's a small price to pay.
I have looked at borg code for this, and from code inspection alone, it seems like borg would write checkpoints for
foo.checkpointtofoo.checkpoint.checkpoint:github.com/ThomasWaldmann/borg@839434891c/src/borg/archive.py (L466)I will try and see if I can verify this experimentally.
The upshot of using this hack is that borg would identify the "pipe-checkpoint" archive as checkpoint as well (for listing and more importantly pruning):
github.com/ThomasWaldmann/borg@839434891c/src/borg/archiver.py (L1545-L1551)Otherwise we need another way to make sure these archives get pruned and hidden from lists (or marked differently in lists).
Thanks for looking into that. Yeah,
foo.checkpoint.checkpointwould probably be fine as long as Borg still considers it a checkpoint archive. And based on that regular expression you linked, it looks like it would.I have been trying to reproduce this, but I cannot get borg to actually write checkpoints (at all).
I've tried the following (on Debian so borgmatic 1.7.7 / borg 1.2.4).
Config:
I would have expected a checkpoint to be there, but there isn't.
My plan was to then set
archive_name_format: '{hostname}-test-{now}.checkpoint'and see what happens, but I didn't even get there.Any help of how to actually get a checkpoint (w/o running a backup for a long time) would be very much appreciated.
Set the checkpoint interval to a low value! Checkpoints are every 30 minutes by default.
I have tried setting it to 1, but there was no checkpoint created (after 2-3 secs). Do you know if there are conditions (like non zero amount of data? In my experiment I've only used a sparse file).
Oh, I didn't see that you were already using
checkpoint_interval! I don't know the answer about whether there are other conditions by default, but related to that, another thing you can try ischeckpoint_volume—if you (temporarily) switch to Borg 2:Ok, I managed to get borg to checkpoint. Two adjustments I did:
dd if=/dev/random of=file bs=512k count=10pg_dump_command: "/root/drip.sh"If we set
archive_name_format: '{hostname}-test-{now}.checkpoint', we'll get.checkpoint.checkpoint:drip.sh
This means we're on a good path here. My next steps are to test with different versions of borgmatic / borg. Notably newest borg 1.x and borg 2.x.
Nice work! Yeah, this is looking like a promising approach. If your investigations pan out, then I could see creating all borgmatic archives with a
.checkpointsuffix and then renaming them to remove the suffix upon successful completion.I have spent some time analyzing this.
Overall findings:
.checkpointarchives (anymore). It allows creating archives ending in.checkpoint..checkpoint.checkpointarchives (see "Test Results" and following for analysis).Proposal:
.checkpoint..checkpointsuffix.ensure
.checkpoints(now only from streaming) get collected correctly.I'm happy to take a stab at the first bullet. @witten WDYT?
Test Results
Overall, getting borg to actually write checkpoints was the hardest here. I have stopped testing with later borgmatic versions, since it seemed that I'm mostly chasing changes in borgmatic.
✅ =
.checkpoint.checkpointarchive created.⚠️ = Unable to produce any checkpoint in my setup.
Test Setup
Then run:
Test Commands
Borg 1.x
Borg 2.x
Test Configs
Borgmatic 1.7.x
Borgmatic > 1.8.x
Borgmatic >= 1.9.2
Borg 2.0.0b14:
Error logs
Borg 2.0.0b14 / Borgmatic 1.7.7, 1.8.13
Borg 2.0.0b14 / Borgmatic >= 1.9.x
--checkpoint-intervalseems not accepted--checkpoint-volumeseems not accepted either:I've started looking at code to make this happen. Two things we need to figure for the proposed design to work:
nameparameter inarchivewith the renamed archiveborgmatic_checkpoint_namefield with the actual archive name we requested from borg.archive_name_format):create_positional_arguments = flags.make_repository_archive_flags(repository_path, archive_name_format, local_borg_version)As a result, we wouldn't know which archive to rename (and what to rename to).
A way to fix this would be to force
--jsonoutput and retrieve the created archive name.borgmatic create --verbosity 2 --jsonThanks for doing all the investigatory work! Some thoughts:
create --json, as convenient as it would be for getting the created archive name. That's because we're currently relying on Borg to present itscreateoutput to the user. But if we switch to capturing all of its output so we can consume the JSON output, then all of the user-facing output will get swallowed.--json) and then rename it accordingly. It's not nearly as nice as getting it straight fromcreateoutput, but it has the nice property of not swallowing thatcreateoutput.If running with Borg 2, borgmatic could just always delete all
.checkpointarchives upon success, as presumably borgmatic will be the only one creating.checkpointarchives. (Although maybe it would have to restricted to archives matching the currentarchive_name_format, which would make it a little more complicated.)I think that could work. There is technically a race condition, but it would surprise me to find a use case that is susceptible to that race condition and not to other (intrinsic) race conditions (e.g. two borgmatic processes).
I am wondering if it tips the balance in terms of complexity w.r.t. a "magic" pipe solution. When considering it, I did not consider the output of the borgmatic command itself. From that POV, a "magic" pipe might be much simpler.
I will keep thinking about it fiddle with some code to see what it would take.
Ah yes, that's a quite neat solution indeed.
If you really wanted to prevent the race, you could probably just create the archive with a name only known to this borgmatic process, e.g.
f'{randomhexdigest}.checkpoint'and then after thecreateis done,renamethat very particular temporary archive name using the configured archive name format. So, to the name it would normally be without this ticket implemented.Say more?
I appreciate your persistence here!
Sorry for the late reply here, I was struggling to find focus time.
With the "magic pipe" I mean a solution where we create an additional named pipe for the sole purpose of ensuring the borg process cannot complete before borgmatic has checked all exit codes of all streaming processes.
This gives borgmatic the chance to kill the borg process in case a streaming process fails.
IIUC we have briefly discussed this, but dismissed the idea because checkpointing seems cleaner.
However, after looking at the borgmatic cli interface in more detail, I feel it might be a simpler solution overall and also make the borg 2 story much simpler.
I'll try to spend some cycles on seeing what the necessary changes would need to be (seems like the trickiest bit will be
borgmatic.execute.log_outputs: we need some form of callback when individual processes complete).jJust to set expectations: I'll be out for 2 weeks starting today, so nothing is going to happen for sure in this time :)
No worries.. I know exactly how it is.
Ah gotcha!
Yeah, that seems a little tricky. But on second thought, it might actually not be needed...? For instance, once the magic pipe that's blocking Borg from exiting is put in place, then as soon the database process exits with error, that might be enough for the existing
log_outputs()code to kill all other open processes (so, Borg) and return. Then you could do any cleanup work (removing the pipe, etc.) afterwards.I might be missing something though!
No worries. Enjoy your presumed time off!
Hah, yes! Good point. There is one thing we need to do though: If all database processes exit successfully, we need to close the pipe. Otherwise the system will deadlock itself.
However, that simplifies things quite a bit I think. I'll try and come up with a draft for this this week.
Sounds good! Let me know how it goes.
I have a WIP version using a named pipe / FIFO here:
https://projects.torsion.org/borgmatic-collective/borgmatic/compare/main...gzm0/borgmatic:dump-safe-pipe
However, it currently hangs. I suspect the approach with a named pipe / FIFO is doomed to fail: it assumes that borg will make independent progress on all files it needs to back up. Now that I think about it, that's not a reasonable assumption.
Or in other words: for the "FIFO blocks completion of the archive" approach to work, we'd need borg to backup the special FIFO last. At least without additional config (if even possible), we cannot expect that to just happen.
@witten I'd appreciate if you could give this some thought. But as it stands now, I don't think the FIFO approach is actually workable :(
#1102 contains an implementation of the checkpoint + rename approach.
For the FIFO approach: The only thing I can think of is that the
borgmatic_runtime_directoryis already in the source patterns, and therefore your FIFO lock is getting included twice—and hanging. Also, the first time it's included in source patterns, it's probably not last like you need it to be.The checkpoint + rename approach is now implemented in main by @gzm0 and will be part of the next release!
Released in borgmatic 2.0.7!
FYI here's an edge case where the checkpoint + rename approach causes problems: Borg 2's
no-deletepermission, which when enabled apparently breaksrenames but notcreates. See #1098 for more info.