-
Notifications
You must be signed in to change notification settings - Fork 37.8k
-masterdatadir for datadir bootstrapping #13746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
518a108
to
3ef0f47
Compare
Concept ACK! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if there is a node (A) running in the master data dir of node (B) and A is writing a block at the same time B is reading?
src/validation.cpp
Outdated
@@ -226,6 +226,7 @@ std::atomic_bool fImporting(false); | |||
std::atomic_bool fReindex(false); | |||
bool fHavePruned = false; | |||
bool fPruneMode = false; | |||
bool has_master_datadir = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use prefix g_
, g_has_master_datadir
.
src/validation.cpp
Outdated
uint8_t buffer[16384]; | ||
while (!feof(mfile)) { | ||
size_t r = fread(buffer, 1, 16384, mfile); | ||
if (r) fwrite(buffer, 1, r, file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should handle failure instead of ignoring? Could lead to corrupt blocks?
@@ -0,0 +1,88 @@ | |||
#!/usr/bin/env python3 | |||
# Copyright (c) 2014-2017 The Bitcoin Core developers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2018
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, this pull request conflicts with the following ones:
If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first. |
I'm not sure what the chances are that this results in actual corruption. The master is safe always, but the slave may end up with a copy that has incomplete data. That depends on whether the master flushes the block file atomically or not, though. I would expect it makes an effort to always keep files in a clean state, in case of crashes and such. That said, the block files are checked on startup and I expect a corruption would be detected almost immediately, since it is almost guaranteed to be the newest block file. I'll experiment a bit with the 'open existing file as readonly' by leaving out the last 1 byte and seeing how it behaves. |
Experiment: skip last byte for case "!readonly & copy existing": diff --git a/src/validation.cpp b/src/validation.cpp
index b5646f107..7af6956b3 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -3804,6 +3804,11 @@ static FILE* OpenDiskFile(const CDiskBlockPos &pos, const char *prefix, bool fRe
while (!feof(mfile)) {
size_t r = fread(buffer, 1, 16384, mfile);
if (r) {
+ // TEST: CORRUPTION
+ if (r > 0 && r < 16384) {
+ LogPrintf("*** Corrupting file %s by writing %d/%d bytes from master\n", path.string(), r-1, r);
+ r--;
+ }
if (r != fwrite(buffer, 1, r, file)) {
fclose(mfile);
fclose(file); Result: slave node crashes with a 'corrupt block database' error:
Disabling corruption patch and restarting results in another crash. Only fix was to -reindex, which initiated a full redownload of blocks (master was pruned). I think it would be interesting to patch the 'corrupt!' logic with a fallback to grab master file again before failing. Actually, I think I'm going to do that. Edit: I did some testing, and the above idea to copy again does not work, because the only time it copies is when it is about to write something, and it does not check the block file consistency at this time. I.e. it gets a corrupt file, writes to it, closes it, then later opens it, finds that it's corrupted, recopies it, but now it has lost the data it wrote before. |
src/init.cpp
Outdated
@@ -1256,6 +1260,27 @@ bool AppInitMain() | |||
gArgs.GetArg("-datadir", ""), fs::current_path().string()); | |||
} | |||
|
|||
if (g_has_master_datadir) { | |||
// LDB does not support readonly access, so we need to copy the entire |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/LDB/levelDB
// LDB does not support readonly access, so we need to copy the entire | ||
// chainstate and blocks/index dirs over to the new location, if not found already | ||
fs::path path = GetDataDir() / "chainstate"; | ||
if (!fs::exists(path)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this copy action require a protection that the masterdatadir is currently not in use/write? If a Bitcoin Core instance is using this (master) datadir it may result in a corrupted copy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Master is guaranteed to be safe as no write operation is ever done from the slave, but the opposite is not the case. See #13746 (comment) for experiment on corruption. There's no good way to recover, as the slave will have already written data and closed the file before realizing it was truncated.
The question remains how frequent this kind of race condition is, though.
Concept ACK |
Actually, I realized that it should be possible to make a perfectly safe slave instance by checking the master directory for the last block file in existence, and marking that minus 1 as the last available master block file. I.e. if master has blk000 ~ blk500, slave will use files blk000 ~ blk499 from master, copy blk500 over the first time it starts up, and then make its own files from there. Important to note is that it will not, for example, try to use master's blk501, in the case where slave is shutdown and master keeps chugging along to the point where it makes a new block file before the slave, something this PR does not actually address. Still means master should preferably be stopped during initial start-up of slave, but it can run safely without slave risking corruption after slave has finished first startup. |
I'm more worried about hot copying the levelDB part (chainstate/). Imo to be safe, the master must close the leveldb environment. |
You can't do this while an instance is running on the master (unless you can somehow force it to temporarily close its database environment and pause operations). If you don't need it to work while the master is running you should grab the lock on the master to avoid it. Alternatively you can just not copy databases at all, and require a reindex to setup a second datadir. |
2609bc9
to
65ce137
Compare
@sipa I am now locking master temporarily while doing the copying, and init-error-shutdown if it is already locked. I am also now tracking the last block file that the slave relies on in master, so it won't accidentally start reading from master block files if master runs ahead in file count (e.g. slave writes blk123, master then creates blk124 and slave starts trying to read from it).
This should be addressed now that the master is locked before copy.
The slave will now copy the very last block file over on initial start. It also holds a lock on the master dir for the duration of these operations, so your concerns have been addressed, I think. |
d3cbc0c
to
e103bb3
Compare
e103bb3
to
8aec15f
Compare
…s used Also find and track the last blk/rev files that we rely on from master, as we do not want to load anything beyond that, in case master runs ahead of us at some point.
8aec15f
to
d88e6c3
Compare
Needs rebase |
Concept ACK |
This PR adds the ability to bootstrap another node from an existing datadir.
To fire up a new node with access to an existing node's datadir
y
, you would simply do(presumably permanently in
bitcoin.conf
) and it would use the blocks files from the former up until it needed to change something, at which point it would copy. (See accompanying test.)It currently only supports
blocks/blk*
caching, which is implemented as copy-on-write. Due to limitations in LevelDB, thechainstate
andblocks/index
dirs are copied to-datadir
on first launch. Latter is only ~75 MB, but former is ~4 GB, so this is sad but better than nothing.Note: the
feature_masterdatadir.py
test makes ~150 MB of block data. May want to combine this with the pruning tests to not redo the work of mining, but it is in its own test for now. Also may wanna move it to extended, but keeping it in base while this PR is ongoing.