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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
[State Sync] Add a new Storage Service implementation (server-side) #9199
Conversation
/// The underlying implementation of the StorageReaderInterface, used by the | ||
/// storage server. | ||
pub struct StorageReader { | ||
storage: Arc<RwLock<DbReaderWriter>>, |
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.
Does it ever write to the DB? Why not a Arc<dyn DbReader>
?
And why is the RwLock
needed?
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.
@msmouse, it doesn't ever write to the db. However, I'm trying to be consistent in the state sync and storage service code. Everything will get an Arc<RwLock<DbReaderWriter>>
. The reason for this is we'll have several state sync (and storage service threads) reading and writing to the db concurrently and it's simpler to give them all a shared RwLock object than to try and do some magic around reader and writer only. IIUC, the DiemDB struct today does not have a global lock (e.g. mutex), correct? If so, I want to avoid any scary situations.
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.
it looks like the DbReaderWriter
is just
pub struct DbReaderWriter {
pub reader: Arc<dyn DbReader>,
pub writer: Arc<dyn DbWriter>,
}
so whoever is constructing the StorageReader
just could just clone the inner Arc<dyn DbReader>
and hand it down.
as far as concurrency, part of the DbReader
API contract is to be thread safe, so you don't need any locks/synchronization to call it. after all, we're effectively doing read-only calls to an MVCC database here, which supports unlimited read concurrency
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 happens if a single DbReader call (e.g., get_transactions()) makes several independent calls to the database (e.g., get_transactions, get_transaction_info and get_events_by_version). Each of these calls is going to read different objects in the database. MVCC might guarantee that each read works, but there's no guarantees across several independent reads, correct? A writer might write between them and violate the higher level assumptions (at the DbReader API). Likewise with interleaved writes. Note: the get_transactions()
example is a little simple (because you could argue it's bound by versions, which shouldn't be changed once written, but I'm not comfortable making that assumption for everything). Perhaps this has all been thought about before, though? With a single read/write lock around the entire API, it's much easier to reason about (each API call can be thought of as executing as a single "database transaction" or atomic operation). Happy to be convinced, but I'm still not there yet 馃槃
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 DB interfaces always take a ledger_version
parameter if necessary, to guarantee a consistent view of the blockchain across reads. If not a flaw, it never assumes the user asks for "the latest".
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.
@msmouse, yeah, my feeling is that in most cases we might be alright. But, race conditions can be very sneaky and I'm still not 100% sure we won't have issues. We're also moving to a world where state sync is going to be multi-threaded and writing to the DB concurrently (e.g., writing different epochs, account states, transactions in parallel, etc.). Thus, I want to do this simple thing now (i.e., wrap all uses of state sync and storage code in a single RwLock). If we find that the single RwLock becomes a point of contention, I'll happily optimize it, but I'd like to make sure that everything is safe and simple for now. Does that make sense?
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.
As long as we are on the same page.
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.
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.
But still, we shouldn't need to write the DB in concurrency. It's not worth it. 馃槣
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.
@msmouse.. I think we'll see. I just don't want to rule this out now, especially as our current design is going to be processing data chunks in parallel 馃槃
// Returns the frozen subtrees at the specified version. | ||
//fn get_frozen_subtrees( | ||
// version, | ||
//) -> Result<Vec<HashValue>, Error> |
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 is this API for? bootstrapping the TransactionAccumulatorSummary
?
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.
It's for the case where a node grabs all account states and needs to bootstrap its view of the transaction accumulator. However, I'll remove it for now until I'm absolutely sure we need it. I think there's a way around it.
#[error("Unexpected error encountered: {0}")] | ||
UnexpectedErrorEncountered(String), | ||
} | ||
|
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.
add a pub type Result<T, E = Error> = ::std::result::Result<T, E>;
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.
Can you elaborate? Why would we want this? These errors should be internal to the storage service only.
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.
This I think is just more convenience along with the Error
so you don't have to keep typing Error
. It's optional
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.
As weirdly as it sounds, I prefer typing Error
everywhere. Especially since so much of our code uses anyhow (which I'm not a big fan of). This way, I know exactly what Error type I'm getting 馃槃 . It's also consistent with the rest of our code (elsewhere)
pub fn handle_request( | ||
&self, | ||
request: StorageServiceRequest, | ||
) -> Result<StorageServiceResponse, Error> { |
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.
I guess we probably want handle_request
to return just StorageServiceResponse
and convert any errors into StorageServiceError
s? Even if DiemDB returns an error, it should probably just return an "Internal Error" variant
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.
I've done this for now but we'll need to give it more attention in the future. For example, if some errors are really bad (e.g., internal invariant violations), we may want them to bubble up to main to cause the node to die. But we can deal with that later 馃槃
4fa6d44
to
a2a8bf7
Compare
"state-sync/storage-service/server", | ||
"state-sync/storage-service/types", |
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.
So, something I hadn't been thinking about before now, think about how this works out from the logging perspective, and how you find the logs with all of these crates. You'll have to check how the modules work out and hopefully they're consistent. For example, I found out there were 3 different ones for storage yesterday, and only one for say state sync
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.
Yeah, it's not great. But, I'd argue that this is a limitation with the logger and setup and not with our code structure. For example, it's important to split the code here up because of circular dependencies and not wanting to tie the server and client together (which is what state sync currently does). When I come to add the logger I'll try to dig a little
#[error("Unexpected error encountered: {0}")] | ||
UnexpectedErrorEncountered(String), | ||
} | ||
|
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.
This I think is just more convenience along with the Error
so you don't have to keep typing Error
. It's optional
a2a8bf7
to
1279fad
Compare
Thanks, all! 馃槃 /land |
Cluster Test Result
Repro cmd:
馃帀 Land-blocking cluster test passed! 馃憣 |
1279fad
to
6122cb7
Compare
Motivation
This PR adds a new server-side implementation for the Storage Service. The Storage Service is an inter-node service allowing Diem nodes to request storage data (e.g., transactions and proofs) directly from other nodes over the Diem network. This PR sets up the framework (template) for this service by implementing the plumbing between request/response processing from the server-side, as well as outlines the required interface between the server and local Diem DB.
To achieve this, the PR offers 2 commits:
Notes:
Have you read the Contributing Guidelines on pull requests?
Yes.
Test Plan
A set of simple unit tests have been added for this functionality.
Related PRs
None, but this PR relates to: #8906