-
Notifications
You must be signed in to change notification settings - Fork 205
headerfs: fail gracefully on header write #313
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
headerfs: fail gracefully on header write #313
Conversation
| if err != nil { | ||
| t.Fatalf("Failed to read file: %v", err) | ||
| } | ||
|
|
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.
Related to my other suggestion: can we also test that if we go down in a partially truncated state, then once we come up we're able to recover? May warrant an entirely distinct change.
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.
My current understanding is:
- A
Truncateoperation is a fatal error. - If a
Truncateerror occurs, it likely indicates a serious issue. - The probability of both a partial write failure and a truncate failure occurring together is probably unlikely and would typically require manual intervention.
Possible mitigation:
- Peer Recovery of Invalid/Incomplete Tail Headers:
- Request missing or invalid/incomplete tail header from peers upon detection on reads and index it in the store.
- In-Place Recovery During Reads
- If the data read is not a complete header (32 bytes for the filter header, 80 bytes for the index), remove it from the binary file.
- The idea is to handle incomplete or invalid entries immediately (i.e., perform a delete operation during read), so the same issue doesn't recur on the next startup. This also include a delete operation in read operation
- This serves as a recovery mechanism if the data is recoverable, depending on the mix of operations.
- However, if the delete operation itself fails, it's unclear how recovery would be handled in that scenario.
- If we simply choose to ignore the incomplete or invalid entry, we risk leaving the binary file in an inconsistent state: the invalid data would remain in the headers binary file, yet we might still be able to read it successfully.
- This approach assumes the partial write entry was not fully truncated, since a truncate failure should have already been reported as an error.
- My concern is that those methods might mask the root cause of the issue, rather than addressing them directly.
What do you think?
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.
Request missing or invalid/incomplete tail header from peers upon detection on reads and index it in the store.
I don't think this makes sense. At this point, we have already fetched the headers.
The probability of both a partial write failure and a truncate failure occurring together is probably unlikely and would typically require manual intervention.
This is precisely what we're attempting to solve. Consider that a user running w/o this patch might have a partial write. Today we require them to go in and delete the file manually. On a mobile platform, this isn't feasible for end users.
If we can make sure that both we write properly, and we can recover from botched writes (rn we fail on read), then we're able to cover all bases.
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.
Consider that a user running w/o this patch might have a partial write. Today we require them to go in and delete the file manually. On a mobile platform, this isn't feasible for end users. If we can make sure that both we write properly, and we can recover from botched writes (rn we fail on read), then we're able to cover all bases.
Thanks that makes sense. I have opened this issue so we could track the recovery on read for partial written headers #315. That said if you wanna me to approach that recovery on read in a specific way would be happy to hear
351e6cd to
91bd469
Compare
|
Can we make a basic benchmark here to gauge if this affects write perf in a traditional set up? |
8d5bacd to
0743efd
Compare
That makes sense, thanks. I have added that Without this PR (commit afcfeb1)With this PRBenchmarking Report - Generated by GeminiBased on the benchmark results: Before the change:
After the change:
Analysis:
Conclusion:
The tradeoff appears reasonable. The performance cost is minimal while the reliability improvements are significant. For a storage operation where data integrity is critical, this seems like an acceptable performance trade-off to gain much better error handling and recovery capability. |
|
From the benchmarking results, it appears there's overhead on the seek operation. However, this is somehow misleading because the benchmark calls Lines 402 to 406 in c932ae4
Lines 867 to 871 in c932ae4
I think integrating this code change in the codebase would be fine. What do you think @Roasbeef? |
Roasbeef
left a comment
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.
LGTM 🌾
ziggie1984
left a comment
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.
Nice work ⚡️, had some some minor questions other than that that is good to go.
Could you also add a linter routine for 80 chars in one line so that we adhere to it across all our projects.
|
|
||
| // BenchmarkHeaderStoreAppendRaw measures performance of headerStore.appendRaw | ||
| // by writing 80-byte headers to a file and resetting position between writes | ||
| // to isolate raw append performance from file size effects. |
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.
are we confident with the results of the benchmark, do we lose some performance now with the enhanced partial writing checking ?
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.
The performance impact depends on how many headers we receive from getheaders or getcfheaders. The maximum number of headers for both block headers and filter headers is configured as 2000 in btcsuite/btcd, which is also the limit Neutrino uses.
When I benchmarked appendRaw with 1000 rows before and after this patch, the performance was nearly the same. This is because the cost and frequency of the seek operation are significantly reduced.
However, in the worst case—when receiving only a single header via getheaders or getcfheaders—the performance cost is similar to the benchmark above, and we lose roughly 7% performance. This depends on various factors.
Overall, I think this is okay because we tradeoff a bit of optimal write performance for improved data integrity during writes.
Batched headers form btcsuite/btcd used in Neutrino
// MsgHeaders implements the Message interface and represents a bitcoin headers
// message. It delivers block header information in response to a getheaders message (MsgGetHeaders).
// The maximum number of block headers per message is currently 2000. See MsgGetHeaders for more details.
type MsgHeaders struct {
Headers []*BlockHeader
}// MsgCFHeaders implements the Message interface and represents a bitcoin cfheaders message.
// It delivers committed filter header information in response to a getcfheaders message (MsgGetCFHeaders).
// The maximum number of committed filter headers per message is currently 2000. See MsgGetCFHeaders for details.
type MsgCFHeaders struct {
FilterType FilterType
StopHash chainhash.Hash
PrevFilterHeader chainhash.Hash
FilterHashes []*chainhash.Hash
}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.
do you know what the defaults for bitcoin-core are ?
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.
do you know what the defaults for bitcoin-core are ?
It uses the following:
/** Maximum number of compact filters that may be requested with one getcfilters. See BIP 157. */
static constexpr uint32_t MAX_GETCFILTERS_SIZE = 1000;
/** Maximum number of cf hashes that may be requested with one getcfheaders. See BIP 157. */
static constexpr uint32_t MAX_GETCFHEADERS_SIZE = 2000;ea1821b to
3f075e0
Compare
|
Thanks @ziggie1984 for the feedback. Regards the linter routine for 80 chars It looks like this would be blocking adding that check? Lines 25 to 27 in c932ae4
|
|
Added the column linter here: #318 |
This commit adds `FileInterface` that would makes it easier to mock the behavior of file header store while testing.
9e7798c to
9b69343
Compare
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.
LGTM, pending godoc.
This commit adds recovery mechanism from failures that may happen during headers I/O write
9b69343 to
b7bf07a
Compare
Description
This PR improves error handling by failing gracefully when header writes fail. This helps prevent unexpected
EOFerrors during the process.