Repository navigation
Use the HC-Tree journal on flagged leaders and followers - #2818
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
| SQResult latest; | ||
| SASSERT(!SQuery(db, "SELECT MAX(id) FROM (" + _getJournalQuery(journalNames, {"SELECT MAX(id) AS id FROM"}, true) + ")", latest)); | ||
| if (latest.empty() || latest[0][0].empty()) { | ||
| SASSERT(!SQuery(db, "INSERT INTO " + journalNames.front() + " (id, query, hash) VALUES (1, X'', '')")); |
There was a problem hiding this comment.
what does X'' do here?
There was a problem hiding this comment.
This is binary data and inserts a blank BLOB instead of a blanks STRING. It doesn't make a huge practical difference here when it's blank, but since we're inserting compressed binary data in the journal, this just matches that.
| } | ||
|
|
||
| if (experimentalHCTree && !hctJournalExists) { | ||
| initializeHCTreeJournal(db, journalNames); |
There was a problem hiding this comment.
This is sending the same db handle used in the insert above, but we don't commit that insert. In this function, we call begin/create table/commit|rollback. Is that a problem? Or since we didn't create a BEGIN/COMMIT for the insert above, SQlite is doing it automatically?
There was a problem hiding this comment.
It does. I will add a comment about it next to the insert.
| {":cid", Parameter::i(legacyCommitID)}, | ||
| {":query", Parameter::blob(legacy[0]["hash"] + ":" + legacy[0]["query"])}, |
There was a problem hiding this comment.
I like this. Should we start using this format in Auth queries?
There was a problem hiding this comment.
I added this here in May:
#2600
And also added it to Auth:
https://github.com/Expensify/Auth/pull/21810
So there's no reason not to use it.
| if (!_journalNames.empty() && (!experimentalHCTree || (_sharedData.legacyMaxID && fromIndex <= _sharedData.legacyMaxID))) { | ||
| const string where = " WHERE id >= " + SQ(fromIndex) + (toIndex ? " AND id <= " + SQ(toIndex) : ""); | ||
| for (const string& name : _journalNames) { | ||
| sources.push_back("SELECT id, query, hash FROM " + name + where); | ||
| } | ||
| } | ||
| if (experimentalHCTree && (!toIndex || toIndex >= _sharedData.hctMinID)) { | ||
| sources.push_back(_getHCTreeJournalQuery() + " WHERE cid >= " + SQ(fromIndex) + | ||
| (toIndex ? " AND cid <= " + SQ(toIndex) : "")); | ||
| } |
There was a problem hiding this comment.
A comment here would be helpful
| SINFO("writeLock Locked"); | ||
| uint64_t count; | ||
| while ((count = _sharedData.openWriteTransactionCount.load())) { | ||
| _sharedData.openWriteTransactionCount.wait(count); |
There was a problem hiding this comment.
TIL that atomic has a wait implementation
There was a problem hiding this comment.
It's relatively new in C++20.
| // After locking writeLock, we wait for all existing writers to finish. They will likely require commitLock | ||
| // as well, which we only acquire once they all complete. No new writes can start once we lock writeLock. | ||
| // This function blocks until in-progress writes finish. | ||
| try { |
There was a problem hiding this comment.
This is a pretty nice change that make things easier to understand. Thanks
| rollback(); | ||
| STHROW("Attempted to begin transaction while in invalid state: _uncommittedQuery not empty"); | ||
| } | ||
| if (type == TRANSACTION_TYPE::EXCLUSIVE) { |
There was a problem hiding this comment.
Why only on type exclusive?
There was a problem hiding this comment.
and if this is readlly necessary, why are we doing it here instead of in like 745 for example?
There was a problem hiding this comment.
So in general, a transaction is marked as a write transaction on the first attempt it makes to write. This is used for when we need to let all of the write transactions complete - anything that was marked as write gets to complete, but nothing not marked as write gets to start writing. If any non-write transaction wants to write, it will get blocked attempting to acquire writeLock.
We simplified the rules around acquiring writeLock and commitLock so that we always lock writeLock first, when we start writing, and then commitLock later, when we want to commit.
EXCLUSIVE transactions lock commitLock immediately, which is how they enforce exclusiveness, so in order to avoid deadlocks, they also need to lock writeLock immediately - before they lock commitLock, so we need to do this right away here.
But yes, it can move to line 746, I'll move it.
| if (SToUInt64(bounds[0][0]) >= oldestCommitToKeep || bounds[0][0] == bounds[0][1]) { | ||
| return true; | ||
| } | ||
| // Delete a prefix, never the final entry. HC-Tree requires the journal |
There was a problem hiding this comment.
I don't understand what this means. What's prefix/final entry?
| uint64_t oldestCommitToKeep = commitCount < _maxJournalSize ? 0 : commitCount - _maxJournalSize; | ||
| if (state.hashCommitID) { | ||
| oldestCommitToKeep = min(oldestCommitToKeep, state.hashCommitID); | ||
| } | ||
| if (oldestCommitToKeep <= _sharedData.hctMinID) { | ||
| return true; | ||
| } | ||
| SQResult bounds; | ||
| if (!read("SELECT MIN(cid), MAX(cid) FROM hct_journal", bounds) || bounds.empty() || bounds[0][0].empty()) { | ||
| return false; | ||
| } | ||
| if (SToUInt64(bounds[0][0]) >= oldestCommitToKeep || bounds[0][0] == bounds[0][1]) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Coments would help here
|
The Auth-side test compatibility fixes for the re-land are in Auth PR 25180: local test handles now match the servers' native-journal mode, and the query-plan test uses its own table instead of the legacy journal. The affected fixtures pass against both Bedrock versions. The full native Auth suite still has 30 failures, including a reproduced read-corruption error and shutdown hang; details are in the Auth PR. |
Revert the revert in 11fbdb2, restoring the original change from Expensify#2818 before applying follow-up fixes.
Reviewer Notes
There are a ton of test changes in this PR but the bulk of the actual change is approximately 400 lines of changes in
SQLite.cpp.The rollout plan for this:
After that, we can move forward with the next HC-Tree changes.
Explanation of Change
This adds an
-hctreeExperimentalModeflag to bedrock. This flag is intended to be re-used for future changes as what counts as "experimental" changes over time. For this change, this allows us to start using HC-Tree's built-in journal.-hctreeExperimentalModeis ignored on databases that are not HC-Tree, i.e., WAL2 dbs.The rest of this describes what
-hctreeExperimentalModedoes in this change, when it is enabled on a HC-Tree database.Followers, or essentially any node that is not leader (i.e., this includes nodes that are SYNCHRONIZING, etc) write replicated commits to
hct_journalinstead of the legacy journals. When leader is not also on-hctreeExperimentalMode(more on if leader is in-hctreeExperimentalModebelow), then these look largely like existing journal commits - namely, commit conflicts do not allocate blank journal entries on the leader. The layout is different -hct_journaldoes not have a separatehashcolumn, and so we concatenate$HASH:$QUERYinhct_journal.In a transitional phase, it is possible that nodes will need to synchronize commits that are in both the old and new journals. This is handled by looking up commits to send to SYNCHRONIZING peers in both journal types. They are sent in the existing format, and can be committed to the old or new journals as appropriate on the receiver (only other HC-Tree nodes with
-hctreeExperimentalModewill write them tohct_journal).One-way switch
Once a DB has had
-hctreeExperimentalModeturned on andhct_journalinitialized, we do not allow switching back. Bedrock will log an error and exit if you start this DB without-hctreeExperimentalModeafter this point. The expected way to disable this on a node is to restore the node from a backup with nohct_journal.This prevents interleaved journal entries in different journal types that break what's required for contiguous journal entries on leader with
-hctreeExperimentalModeenabled.Enabling
-hctreeExperimentalModeon leader.When leader has
-hctreeExperimentalModeit can result in blank journal entries in the case of conflicts. These are recorded and broadcast to followers, in accordance with the HC-Tree spec. This is expected but this will be the first time this is tested in production.BlockWrites
We have a
BlockWritescommand that does what it says. The behavior of this changes - previously, it froze transactions in place, refusing to let them start new writes. This provided ambiguous behavior (and possible race conditions) if you called this on a transaction that was mid-write. The behavior for blocking writes now allows all existing write transactions to complete.Minor differences
hct_journalto seed it.hct_journalFixed Issues
For https://github.com/Expensify/Expensify/issues/337537
Tests
Several new tests added.