Add library pseudocode
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
|||||||
|
Architecture
|
||||||
|
A ToastyFS instance is composed by a metadata server, a number
|
||||||
|
of chunk servers, and a number of clients.
|
||||||
|
|
||||||
|
The metadata server stores the full file system hieararchy,
|
||||||
|
except instead of storing the file contents, it stores an
|
||||||
|
array of hashes of the chunks of each file. A "chunk" is a
|
||||||
|
file range that is fixed for a single file but may vary
|
||||||
|
between files. Chunk servers hold an array of chunks that
|
||||||
|
are identified by their hash. The metadata server keeps
|
||||||
|
track of which chunks each chunk server is holding.
|
||||||
|
|
||||||
|
Clients are users of the file system that can read and
|
||||||
|
write metadata and files. They are assumed to behave
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
Any read and write operation that doesn't involve file
|
||||||
|
contents can be performed by clients by talking to the
|
||||||
|
metadata server directly. Such operations include creating
|
||||||
|
an empty file or a directory, deleting a file or directory,
|
||||||
|
listing files.
|
||||||
|
|
||||||
|
If a client wants to read a range of bytes from a file,
|
||||||
|
it sends the metadata server the file name and range.
|
||||||
|
The metadata server responds with the chunk size of that
|
||||||
|
file, the list of hashes for the chunks involved in the
|
||||||
|
read, and the IP addresses of the chunk servers that hold
|
||||||
|
each chunk. The metadata server also adds the IP addresses
|
||||||
|
of three chunk servers any new chunks should be written
|
||||||
|
to. The client can then download the chunks from the chunk
|
||||||
|
servers and reassemble the result.
|
||||||
|
|
||||||
|
If a client wants to write at a range of bytes of a file,
|
||||||
|
it starts by reading that range from the metadata server,
|
||||||
|
getting the list of hashes it will modify, their locations,
|
||||||
|
and locations for any new chunks. The client then modifies
|
||||||
|
the chunk by sending to each chunk server the hash to modify
|
||||||
|
and the patch (a range of bytes within a chunk plus the new
|
||||||
|
data). The chunk server creates a new modified chunk and
|
||||||
|
keeps the old version, then returns the new hash. If all
|
||||||
|
modifications are successful, the client holds the set of
|
||||||
|
old hashes and new hashes for that file range. It completes
|
||||||
|
the write by telling the metadata server to swap the old
|
||||||
|
hashes with the new ones (optionally including write flags
|
||||||
|
such as TOASTY_WRITE_CREATE_IF_MISSING). If the old hashes
|
||||||
|
don't match, another write succeded in the mean time and
|
||||||
|
touched that range, therefore the write fails. If the old
|
||||||
|
hashes match, the write succeded. If the client fails to
|
||||||
|
modify any chunks, it doesn't commit the write with the
|
||||||
|
metadata server.
|
||||||
|
|
||||||
|
If the file doesn't exist and the TOASTY_WRITE_CREATE_IF_MISSING
|
||||||
|
flag is set, the metadata server atomically creates the file
|
||||||
|
with a default chunk size (4096 bytes) and retries the write.
|
||||||
|
This operation is logged to the WAL to ensure crash consistency.
|
||||||
|
|
||||||
|
If the TOASTY_WRITE_TRUNCATE_AFTER flag is set, the file is
|
||||||
|
truncated after the write, setting its size to exactly offset+length
|
||||||
|
and discarding any data beyond that point. This is useful for HTTP
|
||||||
|
PUT semantics where the entire file content should be replaced.
|
||||||
|
|
||||||
|
Note that write failures may cause chunks to be orphaned
|
||||||
|
on chunk servers. This is solved by a garbage collection
|
||||||
|
algorithm implemented by the synchronization messages
|
||||||
|
between metadata and chunk server.
|
||||||
|
|
||||||
|
Note that clients may cache chunks and index them by their
|
||||||
|
hash. When they read a file and receive its hashes, they may
|
||||||
|
avoid reaching for the chunk servers if they already cached
|
||||||
|
the chunks with those hashes. This allows reading files with
|
||||||
|
only one round trip at no cost of correctness. If getting
|
||||||
|
the up-to-date contents is not a concern, clients may also
|
||||||
|
cache file metadata.
|
||||||
|
|
||||||
|
Metadata and chunk server exchange:
|
||||||
|
|
||||||
|
The metadata server is only aware of each chunk server
|
||||||
|
as long as they have a TCP connection. When a chunk server
|
||||||
|
first connects to the metadata server, it authenticates
|
||||||
|
itself and sends its own IP addresses. If the server is
|
||||||
|
authentic, the metadata server requests the full list
|
||||||
|
of chunks the chunk server is holding. Upon receiving the
|
||||||
|
state of chunk server, the metadata server adds all useful
|
||||||
|
chunks to the "old_list" and all useless chunks to the
|
||||||
|
"rem_list", then sends the rem_list to the chunk server
|
||||||
|
which removes those chunks.
|
||||||
|
|
||||||
|
When writes are committed to the metadata server involving
|
||||||
|
new chunks to a chunk server, the metadata server adds those
|
||||||
|
hashes to an "add_list" and any hashes that are not useful
|
||||||
|
anymore to the rem_list.
|
||||||
|
|
||||||
|
Periodically, the metadata server sends the add_list and
|
||||||
|
rem_list to the chunk server. These list tell the chunk
|
||||||
|
server the ideal state it should have from the point of
|
||||||
|
view of the metadata server. Elements in the add_list should
|
||||||
|
already be in the chunk servers, and elements from the
|
||||||
|
rem_list are to be removed. A chunk server marks any chunk
|
||||||
|
in the rem_list as to be removed and checks that hashes
|
||||||
|
in the add list are present. If a chunk in the add list
|
||||||
|
is marked as to be removed, it is unmarked. When a chunk
|
||||||
|
is marked as to be removed for a certain amount of time,
|
||||||
|
it is permanently deleted. When the synchronization is
|
||||||
|
complete, the metadata server merges the add_list into
|
||||||
|
the old_list and clears the rem_list. If chunks in the
|
||||||
|
add_list are not present in the chunk server, it responds
|
||||||
|
with an error message containing the list of missing chunks.
|
||||||
|
The metadata server then responds with a list of chunk
|
||||||
|
server addresses where the chunk server with the missing
|
||||||
|
chunk can download it from. Each chunk server goes
|
||||||
|
through its download list one at the time downloading
|
||||||
|
the missing chunks.
|
||||||
|
|
||||||
|
Note that if the chunk server finds that its holding some
|
||||||
|
chunks that are not in the hash list of the metadata server,
|
||||||
|
that does not mean they are orphaned. It's possible that
|
||||||
|
some writes are being performed by clients that have uploaded
|
||||||
|
chunks to that chunk server but didn't yet acknowledge it
|
||||||
|
to the metadata server. If all goes well and the write
|
||||||
|
succeded, the metadata server will add those hashes to the
|
||||||
|
hash list. Chunk servers should only drop chunks if they
|
||||||
|
are not referenced by the metadata server for a period of
|
||||||
|
time (say, 30 minutes).
|
||||||
|
|
||||||
|
Security
|
||||||
|
All nodes of the system share a secret key and use it to
|
||||||
|
authenticate each other and encrypt messages. This allows
|
||||||
|
the server to accept new chunk servers and clients with
|
||||||
|
no prior setup
|
||||||
|
|
||||||
|
Reliability
|
||||||
|
The metadata server is a single point of failure. To reduce
|
||||||
|
the impact of crashes, the metadata server stores all write
|
||||||
|
operations into a write-ahead log that is replayed any time
|
||||||
|
the process goes online.
|
||||||
|
|
||||||
|
Chunk Management:
|
||||||
|
Chunks are added to the system when:
|
||||||
|
1. A chunk server connects
|
||||||
|
2. A write operation on metadata occurs (adding chunks)
|
||||||
|
|
||||||
|
They are removed when:
|
||||||
|
1. A chunk server disconnects
|
||||||
|
2. A write operation on metadata occurs (overwriting old chunks)
|
||||||
|
3. A delete operation on metadata occurs
|
||||||
|
4. A chunk is corrupted or removed forcefully from the chunk server
|
||||||
|
|
||||||
|
The system must make sure that chunks are not over-replicated
|
||||||
|
or under-replicated. If they are over-replicated, some chunk
|
||||||
|
servers need to forget some copies. If they are under-replicated,
|
||||||
|
some chunk servers need to copy chunks from elsewhere.
|
||||||
|
|
||||||
|
Metadata server variables for a chunk server:
|
||||||
|
ms_old_list: List of chunks that are known to be held by CS
|
||||||
|
ms_add_list: List of chunks that should be held by CS
|
||||||
|
ms_rem_list: List of chunks that may be held by CS but should removed from it
|
||||||
|
|
||||||
|
Chunk server variables:
|
||||||
|
cs_add_list: List of chunks added since the last update
|
||||||
|
cs_rem_list: List of chunks marked for removal after a timeout
|
||||||
|
cs_lst_list: List of chunks that were lost due to errors or forceful removals of chunk files
|
||||||
|
|
||||||
|
Metadata change for write:
|
||||||
|
When clients commit a write by adding new hashes to the metadata,
|
||||||
|
MS adds those hashes to the ms_add_list for the CS where the client
|
||||||
|
uploaded the chunks. If a hash was overwritten and became useless,
|
||||||
|
that hash is added to the ms_rem_list for all CS holding it.
|
||||||
|
|
||||||
|
Metadata change for delete:
|
||||||
|
All hashes that are no longer reachable by the file tree are added
|
||||||
|
to the ms_rem_list of their holders
|
||||||
|
|
||||||
|
Chunk upload:
|
||||||
|
When a chunk is uploaded to a chunk server, its hash is added to
|
||||||
|
the cs_add_list.
|
||||||
|
|
||||||
|
Periodically on the chunk server:
|
||||||
|
Elements in the cs_rem_list have a 30 minute timeout after which they are deleted
|
||||||
|
permanently.
|
||||||
|
|
||||||
|
Periodically:
|
||||||
|
CS sends cs_add_list to MS
|
||||||
|
MS may add a subset of cs_add_list to ms_add_list based on the chunk replication and distribution policy
|
||||||
|
MS sends ms_add_list and ms_rem_list to CS
|
||||||
|
CS (1) Adds all elements from ms_rem_list to cs_rem_list
|
||||||
|
(2) Elements in ms_add_list that are not held by the chunk server are added to
|
||||||
|
a temporary list tmp_list
|
||||||
|
(3) Removes elements in ms_add_list from cs_add_list and cs_rem_list, then merges cs_add_list into cs_rem_list and clears cs_add_list.
|
||||||
|
(4) Elements in cs_lst_list are added to tmp_list, then cs_lst_list is cleared.
|
||||||
|
(6) tmp_list is sent to MS
|
||||||
|
(7) cs_add_list is cleared
|
||||||
|
MS (1) Receives tmp_list and sends download locations to CS for those chunks
|
||||||
|
(2) Merges ms_add_list into ms_old_list, then removes all items in tmp_list from ms_old_list
|
||||||
|
(3) Sets ms_add_list equal to tmp_list
|
||||||
|
|
||||||
|
Chunk replication and distribution policy:
|
||||||
|
During an update, when CS reports a new chunk to MS, MS has to decide whether
|
||||||
|
to allow the CS to keep it or not.
|
||||||
|
|
||||||
|
There are 4 cases:
|
||||||
|
- The chunk is useless (not referenced by any file)
|
||||||
|
- The chunk is under-replicated even counting the new copy
|
||||||
|
- The chunk is properly replicated with the new copy
|
||||||
|
- The chunk is over-replicated with the new copy
|
||||||
|
|
||||||
|
If the chunk is not referenced by the file tree, do nothing.
|
||||||
|
|
||||||
|
If the chunk is properly replicated or under-replicated, add it to the ms_add_list.
|
||||||
|
|
||||||
|
If the chunk is over-replicated, either don't add it to the ms_add_list or add it to the ms_rem_list of some other holder.
|
||||||
|
|
||||||
|
TODO: The way chunk servers tell about the chunks they are holding
|
||||||
|
recently changed. Now instead of sending a full chunk list when
|
||||||
|
connecting, they send batches of chunks to the metadata server
|
||||||
|
during state updates. It also changed that now chunk servers
|
||||||
|
initiate updates.
|
||||||
|
|
||||||
|
When a chunk server connects (and authenticates)
|
||||||
|
it sends alongside the auth message the hash of
|
||||||
|
the hash list of all chunks. The metadata server
|
||||||
|
then replies with a message saying whether it
|
||||||
|
already cached that chunk list or not. If it didn't, the chunk server sends the entire list
|
||||||
|
in chunks during state updates, with an increased
|
||||||
|
update frequency.
|
||||||
|
|
||||||
|
Metadata Persistence & Crash Recovery:
|
||||||
|
The metadata server uses a write-ahead log (WAL) file to store its state on disk.
|
||||||
|
|
||||||
|
Log files start with a full snapshot of the metadata and continues with operation
|
||||||
|
log entries.
|
||||||
|
|
||||||
|
When a file gets too big, the metadata server creates a new WAL file by writing
|
||||||
|
a snapshot to it and continuing logging there.
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
1. Introduction & Notation
|
||||||
|
|
||||||
|
The DESIGN.txt file gives an overview of the system
|
||||||
|
and how nodes of a cluster interact with each other.
|
||||||
|
This file documents the specific binary format used
|
||||||
|
to exchange information between nodes.
|
||||||
|
|
||||||
|
All messages start with a shared header, defined as:
|
||||||
|
|
||||||
|
struct Header {
|
||||||
|
uint16_t version;
|
||||||
|
uint16_t type;
|
||||||
|
uint32_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
1.1 Generation Counter Special Values
|
||||||
|
|
||||||
|
Generation counters (uint64_t) have two special values:
|
||||||
|
|
||||||
|
NO_GENERATION (0):
|
||||||
|
When used in expect_gen, means "skip generation check entirely".
|
||||||
|
Files/directories are never assigned this generation value.
|
||||||
|
|
||||||
|
MISSING_FILE_GENERATION (UINT64_MAX):
|
||||||
|
When used in expect_gen, means "expect file/directory to NOT exist".
|
||||||
|
- For DELETE: succeeds if file doesn't exist (no-op)
|
||||||
|
- For WRITE: fails with BADGEN if file exists, fails with NOENT if missing
|
||||||
|
- For existing operations: causes generation mismatch if file exists
|
||||||
|
Files/directories are never assigned this generation value.
|
||||||
|
|
||||||
|
2. Client Messages
|
||||||
|
|
||||||
|
2.1 Client to Metadata Server messages
|
||||||
|
|
||||||
|
Let's start from the interactions between a client and
|
||||||
|
the metadata server:
|
||||||
|
|
||||||
|
[ CREATE | C -> MS ]
|
||||||
|
Upon file creation, the client sends a "CREATE" message with the following layout:
|
||||||
|
|
||||||
|
struct CreateMessage {
|
||||||
|
Header header; // type=CREATE
|
||||||
|
uint16_t path_len;
|
||||||
|
char path[path_len];
|
||||||
|
uint8_t is_dir;
|
||||||
|
if (is_dir != 0) {
|
||||||
|
uint32_t chunk_size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Note that in general only paths up to 65K bytes are
|
||||||
|
supported and that the layout of fields may depend
|
||||||
|
on the value of others.
|
||||||
|
|
||||||
|
The server then responds with a CREATE_SUCCESS or CREATE_ERROR message.
|
||||||
|
|
||||||
|
[ DELETE | C -> MS ]
|
||||||
|
When a client deletes a file, it sends a DELETE
|
||||||
|
message with the following layout:
|
||||||
|
|
||||||
|
struct DeleteMessage {
|
||||||
|
Header header; // type=DELETE
|
||||||
|
uint64_t expect_gen;
|
||||||
|
uint16_t path_len;
|
||||||
|
char path[path_len];
|
||||||
|
};
|
||||||
|
|
||||||
|
The file/directory at the given path is only deleted if its generation counter matches expect_gen. If expect_gen is 0, the file/directory is deleted regardless.
|
||||||
|
|
||||||
|
The server then responds with a DELETE_SUCCESS or DELETE_ERROR message.
|
||||||
|
|
||||||
|
[ LIST | C -> MS ]
|
||||||
|
When a client requests a directory listing, it sends a LIST message whith the following format:
|
||||||
|
|
||||||
|
struct ListMessage {
|
||||||
|
Header header; // type=LIST
|
||||||
|
uint64_t expect_gen;
|
||||||
|
uint16_t path_len;
|
||||||
|
char path[path_len];
|
||||||
|
};
|
||||||
|
|
||||||
|
If the expect_gen field doesn't match the generation counter of the directory, the operation fails. If expect_gen is 0, the check is skipped.
|
||||||
|
|
||||||
|
The server then responds with a LIST_SUCCESS or LIST_ERROR message.
|
||||||
|
|
||||||
|
[ READ | C -> MS ]
|
||||||
|
When a client requests to read a file, it sends a READ message with the following format:
|
||||||
|
|
||||||
|
struct ReadMessage {
|
||||||
|
Header header; // type=READ
|
||||||
|
uint64_t expect_gen;
|
||||||
|
uint16_t path_len;
|
||||||
|
char path[path_len];
|
||||||
|
uint32_t offset;
|
||||||
|
uint32_t length;
|
||||||
|
};
|
||||||
|
|
||||||
|
The expect_gen field is the expected generation counter for the target resource. If it doesn't match, the operation fails. If expect_gen is 0, the check is skipped.
|
||||||
|
|
||||||
|
The offset and length fields determine the region to be read from the file.
|
||||||
|
|
||||||
|
[ WRITE | C -> MS ]
|
||||||
|
When a client wants to write to a file, it sends a WRITE message with the following layout:
|
||||||
|
|
||||||
|
struct Location {
|
||||||
|
uint8_t is_ipv4;
|
||||||
|
if (is_ipv4) {
|
||||||
|
uint32_t ipv4;
|
||||||
|
} else {
|
||||||
|
uint128_t ipv6;
|
||||||
|
}
|
||||||
|
uint16_t port;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WriteChunk {
|
||||||
|
SHA256 hash;
|
||||||
|
uint32_t num_locations;
|
||||||
|
Location locations[num_locations];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WriteMessage {
|
||||||
|
Header header; // type=WRITE
|
||||||
|
uint64_t expect_gen;
|
||||||
|
uint32_t flags;
|
||||||
|
uint16_t path_len;
|
||||||
|
char path[path_len];
|
||||||
|
uint32_t offset;
|
||||||
|
uint32_t length;
|
||||||
|
uint32_t num_chunks;
|
||||||
|
WriteChunk chunks[num_chunks];
|
||||||
|
};
|
||||||
|
|
||||||
|
If the expect_gen field doesn't match the generation of the target file, the operation fails. Note that unlike other operations, the expect_gen CAN'T be 0. This is due to the assumption that the chunk size hasn't change for that file since the writer originally retrieved the file's metadata.
|
||||||
|
|
||||||
|
The flags field contains write operation flags. Currently defined flags:
|
||||||
|
- TOASTY_WRITE_CREATE_IF_MISSING (0x01): If the file doesn't exist,
|
||||||
|
the metadata server will atomically create it with a default chunk
|
||||||
|
size of 4096 bytes and retry the write operation. The creation is
|
||||||
|
logged to the WAL for crash consistency.
|
||||||
|
- TOASTY_WRITE_TRUNCATE_AFTER (0x02): Truncate the file after the write
|
||||||
|
operation, setting the file size to exactly offset+length. Any data
|
||||||
|
beyond this point is discarded. Useful for HTTP PUT semantics where
|
||||||
|
the entire file content should be replaced.
|
||||||
|
|
||||||
|
The offset and length mark the region that is being written to.
|
||||||
|
|
||||||
|
Then comes an array of num_chunks sections each specifying where a given chunk was written to. Note that the number of chunks is equal to
|
||||||
|
|
||||||
|
num_chunks == length / chunk_size
|
||||||
|
|
||||||
|
Where chunk_size is the one for the target file at the specified generation.
|
||||||
|
|
||||||
|
Each WriteChunk lists the new hashes for the file in the write range and for each one it lists all the chunk servers that are now holding a copy of it.
|
||||||
|
|
||||||
|
2.2 Client to Chunk Server messages
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
3. Metadata Server messages
|
||||||
|
|
||||||
|
The following is the list of messages the Metadata Server may send to a Client:
|
||||||
|
|
||||||
|
[ CREATE_ERROR | MS -> C ]
|
||||||
|
When a client sends a CREATE request to the metadata server and the operation fails, the metadata server responds with a CREATE_ERROR message with the following layout:
|
||||||
|
|
||||||
|
struct CreateErrorMessage {
|
||||||
|
Header header; // type=CREATE_ERROR
|
||||||
|
uint16_t message_len;
|
||||||
|
char message[message_len];
|
||||||
|
};
|
||||||
|
|
||||||
|
[ CREATE_SUCCESS | MS -> C ]
|
||||||
|
When a client sends a CREATE requests to the metadata server which succedes, the metadata server replies with a CREATE_SUCCESS message with the following layout:
|
||||||
|
|
||||||
|
struct CreateSuccessMessage {
|
||||||
|
Header header; // type=CREATE_SUCCESS
|
||||||
|
uint64_t gen;
|
||||||
|
};
|
||||||
|
|
||||||
|
The gen field is the generation counter given to the file.
|
||||||
|
|
||||||
|
[ DELETE_ERROR | MS -> C ]
|
||||||
|
See CREATE_ERROR
|
||||||
|
|
||||||
|
[ DELETE_SUCCESS | MS -> C ]
|
||||||
|
When a client sends a DELETE operation to the metadata server which succedes, a DELETE_SUCCESS message is sent back with the following layout:
|
||||||
|
|
||||||
|
struct DeleteSuccessMessage {
|
||||||
|
Header header; // type=DELETE_SUCCESS
|
||||||
|
};
|
||||||
|
|
||||||
|
It does not store any fields other than the header.
|
||||||
|
|
||||||
|
[ LIST_ERROR | MS -> C ]
|
||||||
|
When a client sends a LIST request to the metadata server and it fails, the server replies with a LIST_ERROR message with the following layout:
|
||||||
|
|
||||||
|
struct ListErrorMessage {
|
||||||
|
Header header; // type=LIST_ERROR
|
||||||
|
uint16_t message_len;
|
||||||
|
char message[message_len];
|
||||||
|
};
|
||||||
|
|
||||||
|
[ LIST_SUCCESS | MS -> C ]
|
||||||
|
When a client sends a LIST request to the metadata server and it succedes, the server replies with a LIST_SUCCESSS message with the following layout:
|
||||||
|
|
||||||
|
struct ListItem {
|
||||||
|
uint64_t gen;
|
||||||
|
uint8_t is_dir;
|
||||||
|
uint16_t name_len;
|
||||||
|
char name[name_len];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ListSuccessMessage {
|
||||||
|
Header header; // type=LIST_SUCCESS
|
||||||
|
uint64_t gen;
|
||||||
|
uint8_t truncated;
|
||||||
|
uint32_t item_count;
|
||||||
|
ListItem items[item_count];
|
||||||
|
};
|
||||||
|
|
||||||
|
The ListSuccessMessage gen field contains the generation counter for the directory, while the gen field in ListItem is the counter for that specific child.
|
||||||
|
|
||||||
|
If the truncated field is non-zero, the actual item count for this directory is greater than the one sent in the message.
|
||||||
|
|
||||||
|
[ READ_ERROR | MS -> C ]
|
||||||
|
TODO
|
||||||
|
|
||||||
|
[ READ_SUCCESS | MS -> C ]
|
||||||
|
|
||||||
|
struct IPv4AndPort {
|
||||||
|
uint32_t ipv4;
|
||||||
|
uint16_t port;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct IPv6AndPort {
|
||||||
|
uint128_t ipv6;
|
||||||
|
uint16_t port;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ChunkServerAddrList {
|
||||||
|
uint32_t num_ipv4;
|
||||||
|
IPv4AndPort ipv4s[num_ipv4];
|
||||||
|
uint32_t num_ipv6;
|
||||||
|
IPv6AndPort ipv6s[num_ipv6];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReadChunk {
|
||||||
|
SHA256 hash;
|
||||||
|
uint32_t num_holders;
|
||||||
|
ChunkServerAddrList holders[num_holders];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReadSuccessMessage {
|
||||||
|
Header header; // type=READ_SUCCESS
|
||||||
|
uint64_t gen;
|
||||||
|
uint32_t chunk_size;
|
||||||
|
uint32_t file_length;
|
||||||
|
uint32_t num_hashes;
|
||||||
|
ReadChunk chunks[num_hashes];
|
||||||
|
uint32_t num_write_locations;
|
||||||
|
ChunkServerAddrList write_locations[num_write_locations];
|
||||||
|
};
|
||||||
|
|
||||||
|
The message returns general information about the file such as its generation counter, length in bytes, and chunk size.
|
||||||
|
|
||||||
|
The chunks list contains the hashes of the chunks touched by the read and the list of chunk servers that are holding them.
|
||||||
|
|
||||||
|
The write locations are a list of chunk servers that clients may write new chunks to.
|
||||||
|
|
||||||
|
[ WRITE_ERROR | MS -> C ]
|
||||||
|
See CREATE_ERROR
|
||||||
|
|
||||||
|
[ WRITE_SUCCESS | MS -> C ]
|
||||||
|
When a client sends a WRITE message which succedes, the metadata server responds with a WRITE_SUCCESS message with the following layout:
|
||||||
|
|
||||||
|
struct WriteSuccessMessage {
|
||||||
|
Header header; // type=WRITE_SUCCESS
|
||||||
|
uint64_t gen;
|
||||||
|
};
|
||||||
|
|
||||||
|
The gen field is the new generation counter for that file.
|
||||||
+310
-5
@@ -35,15 +35,94 @@ metadata_server_tick:
|
|||||||
|
|
||||||
if the peer is a client:
|
if the peer is a client:
|
||||||
if the message has type CREATE:
|
if the message has type CREATE:
|
||||||
TODO
|
Read path_len, path, and is_dir from the message
|
||||||
|
If is_dir is false:
|
||||||
|
Read chunk_size from message
|
||||||
|
Else:
|
||||||
|
Set chunk_size to 0
|
||||||
|
|
||||||
|
Append the create operation to the WAL
|
||||||
|
Attempt to create entity in the file_tree
|
||||||
|
|
||||||
|
If file_tree creation fails:
|
||||||
|
Send CREATE_ERROR with the error description
|
||||||
|
Else:
|
||||||
|
Send CREATE_SUCCESS containing the new generation number
|
||||||
|
|
||||||
if the message has type DELETE:
|
if the message has type DELETE:
|
||||||
TODO
|
Read expect_gen, path_len, and path from message
|
||||||
|
Append delete operation to the WAL
|
||||||
|
Attempt to delete entity in the file_tree
|
||||||
|
|
||||||
|
If file_tree deletion fails:
|
||||||
|
Send DELETE_ERROR with the error description
|
||||||
|
Else:
|
||||||
|
Send DELETE_SUCCESS (no payload)
|
||||||
|
|
||||||
if the message has type LIST:
|
if the message has type LIST:
|
||||||
TODO
|
Read expect_gen, path_len, and path from message
|
||||||
|
Attempt to list directory contents from file_tree
|
||||||
|
|
||||||
|
If listing fails:
|
||||||
|
Send LIST_ERROR with error description
|
||||||
|
Else:
|
||||||
|
Send LIST_SUCCESS containing:
|
||||||
|
- Directory generation
|
||||||
|
- Truncated flag (if results exceed MAX_LIST_SIZE)
|
||||||
|
- Item count
|
||||||
|
- List of items (generation, is_dir, name_len, name)
|
||||||
|
|
||||||
if the message has type READ:
|
if the message has type READ:
|
||||||
TODO
|
Read expect_gen, path_len, path, offset, and length
|
||||||
|
Attempt to read file info from file_tree
|
||||||
|
|
||||||
|
If read fails:
|
||||||
|
Send READ_ERROR containing:
|
||||||
|
- Error description
|
||||||
|
- A list of suggested chunk server addresses for writing (load balanced)
|
||||||
|
NOTE: Providing write locations on read error allows clients to handle
|
||||||
|
"missing file" scenarios by immediately writing if desired.
|
||||||
|
Else:
|
||||||
|
Send READ_SUCCESS containing:
|
||||||
|
- File generation
|
||||||
|
- Chunk size
|
||||||
|
- Actual bytes available (may be less than requested length)
|
||||||
|
- Number of chunks
|
||||||
|
- List of chunks, where each contains:
|
||||||
|
- SHA256 Hash
|
||||||
|
- List of chunk servers holding this chunk (holders)
|
||||||
|
- A list of suggested chunk server addresses for writing new chunks
|
||||||
|
(load balanced via choose_servers_for_write)
|
||||||
|
|
||||||
if the message has type WRITE:
|
if the message has type WRITE:
|
||||||
TODO
|
Read expect_gen, flags, path_len, path
|
||||||
|
Read offset, length, num_chunks
|
||||||
|
|
||||||
|
For each chunk in num_chunks:
|
||||||
|
Read the hash and the list of Chunk Server addresses (locations) that hold it
|
||||||
|
(This implies the client has already uploaded the data to these servers)
|
||||||
|
|
||||||
|
Append write operation to WAL
|
||||||
|
|
||||||
|
Attempt to apply write to file_tree (returns new_gen and a list of removed_hashes)
|
||||||
|
|
||||||
|
If result is NOENT (File Not Found) AND flags has TOASTY_WRITE_CREATE_IF_MISSING:
|
||||||
|
- Append create operation (default chunk_size 4096) to WAL
|
||||||
|
- Create entity in file_tree
|
||||||
|
- Retry the write operation to file_tree
|
||||||
|
|
||||||
|
If write fails:
|
||||||
|
Send WRITE_ERROR with error description
|
||||||
|
Else:
|
||||||
|
For each new chunk hash provided in the message:
|
||||||
|
Find the chunk servers associated with that hash in the message
|
||||||
|
Add the hash to those servers' ms_add_list
|
||||||
|
|
||||||
|
For each removed_hash returned by the file_tree:
|
||||||
|
Find all chunk servers currently holding this hash
|
||||||
|
Add the hash to their ms_rem_list (marking it for deletion)
|
||||||
|
|
||||||
|
Send WRITE_SUCCESS containing the new generation number
|
||||||
|
|
||||||
if the peer is a CS:
|
if the peer is a CS:
|
||||||
if the message has type AUTH:
|
if the message has type AUTH:
|
||||||
@@ -175,3 +254,229 @@ chunk_server_tick:
|
|||||||
=== CLIENT LIBRARY ======================================
|
=== CLIENT LIBRARY ======================================
|
||||||
=========================================================
|
=========================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 1. DATA STRUCTURES
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class ToastyClient:
|
||||||
|
metadata_server_conn: Connection
|
||||||
|
chunk_server_pool: Map<Address, Connection>
|
||||||
|
pending_operations: Map<OpID, Operation>
|
||||||
|
|
||||||
|
class Operation:
|
||||||
|
id: Integer
|
||||||
|
type: Enum(READ, WRITE, CREATE, DELETE, LIST)
|
||||||
|
status: Enum(PENDING, COMPLETED, FAILED)
|
||||||
|
|
||||||
|
# Context
|
||||||
|
path: String
|
||||||
|
user_buffer: Byte[]
|
||||||
|
offset: Integer
|
||||||
|
length: Integer
|
||||||
|
flags: Bitmask
|
||||||
|
expected_generation: Integer
|
||||||
|
|
||||||
|
# Internal State
|
||||||
|
chunks_to_process: List<ChunkTask>
|
||||||
|
bytes_transferred: Integer
|
||||||
|
result_data: Any
|
||||||
|
|
||||||
|
class ChunkTask:
|
||||||
|
# Used for tracking individual chunk uploads/downloads
|
||||||
|
chunk_index: Integer
|
||||||
|
hash: String
|
||||||
|
target_servers: List<Address>
|
||||||
|
status: Enum(WAITING, IN_PROGRESS, DONE)
|
||||||
|
final_hash: String # Returned by CS after upload/patch
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 2. PUBLIC BLOCKING API (User Facing)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function toasty_connect(address, port):
|
||||||
|
client = new ToastyClient()
|
||||||
|
client.metadata_server_conn = tcp_connect(address, port)
|
||||||
|
return client
|
||||||
|
|
||||||
|
function toasty_read(client, path, offset, length):
|
||||||
|
op_handle = begin_read_op(client, path, offset, length)
|
||||||
|
return wait_for_result(client, op_handle)
|
||||||
|
|
||||||
|
function toasty_write(client, path, data, offset, flags):
|
||||||
|
op_handle = begin_write_op(client, path, data, offset, flags)
|
||||||
|
return wait_for_result(client, op_handle)
|
||||||
|
|
||||||
|
# (Create, Delete, and List follow the same pattern: begin_op -> wait_for_result)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 3. INTERNAL ASYNCHRONOUS LOGIC
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- READ LOGIC ---
|
||||||
|
|
||||||
|
function begin_read_op(client, path, offset, length):
|
||||||
|
op = new Operation(type=READ, path, offset, length)
|
||||||
|
|
||||||
|
# Step 1: Ask Metadata Server (MS) for the file layout
|
||||||
|
send_message(client.metadata_server_conn,
|
||||||
|
type=READ_REQUEST,
|
||||||
|
path=path, range=(offset, length))
|
||||||
|
|
||||||
|
return register_operation(client, op)
|
||||||
|
|
||||||
|
function handle_read_metadata_response(op, msg):
|
||||||
|
if msg.type == ERROR:
|
||||||
|
op.status = FAILED
|
||||||
|
return
|
||||||
|
|
||||||
|
# Step 2: Calculate chunk plan
|
||||||
|
chunk_size = msg.chunk_size
|
||||||
|
op.chunks_to_process = calculate_required_chunks(op.offset, op.length, chunk_size)
|
||||||
|
|
||||||
|
# Step 3: Queue downloads from Chunk Servers (CS)
|
||||||
|
for chunk in op.chunks_to_process:
|
||||||
|
# MS returns a list of holders for each chunk hash
|
||||||
|
best_cs = load_balance_select(chunk.holders)
|
||||||
|
conn = get_cs_connection(best_cs)
|
||||||
|
|
||||||
|
send_message(conn,
|
||||||
|
type=DOWNLOAD_CHUNK,
|
||||||
|
hash=chunk.hash)
|
||||||
|
|
||||||
|
function handle_chunk_download_success(op, msg):
|
||||||
|
# Step 4: Assemble data
|
||||||
|
copy_bytes(src=msg.data, dest=op.user_buffer, at_offset=msg.chunk_index)
|
||||||
|
|
||||||
|
mark_task_complete(op, msg.chunk_index)
|
||||||
|
|
||||||
|
if all_tasks_complete(op):
|
||||||
|
op.status = COMPLETED
|
||||||
|
|
||||||
|
# --- WRITE LOGIC (The Complex Part) ---
|
||||||
|
|
||||||
|
function begin_write_op(client, path, data, offset, flags):
|
||||||
|
op = new Operation(type=WRITE, path, data, offset, flags)
|
||||||
|
|
||||||
|
# Step 1: Send READ to MS first.
|
||||||
|
# We need the current generation number and existing chunk hashes to perform
|
||||||
|
# patches or ensure consistency, even if we are overwriting.
|
||||||
|
send_message(client.metadata_server_conn,
|
||||||
|
type=READ_REQUEST,
|
||||||
|
path=path, range=(offset, len(data)))
|
||||||
|
|
||||||
|
return register_operation(client, op)
|
||||||
|
|
||||||
|
function handle_write_metadata_ready(op, msg):
|
||||||
|
# Step 2: Prepare Upload Tasks
|
||||||
|
# If file missing & CREATE_IF_MISSING flag is set, we use defaults (size 0, gen 0).
|
||||||
|
current_generation = msg.generation
|
||||||
|
chunk_size = msg.chunk_size
|
||||||
|
write_targets = msg.write_locations # Recommended CS IPs from MS
|
||||||
|
|
||||||
|
# Slice user data into chunks
|
||||||
|
sliced_chunks = split_into_chunks(op.user_buffer, chunk_size)
|
||||||
|
|
||||||
|
for slice in sliced_chunks:
|
||||||
|
task = new ChunkTask()
|
||||||
|
task.target_servers = write_targets # We must replicate to 3 servers
|
||||||
|
|
||||||
|
# Determine if this is a Create (New) or Update (Patch)
|
||||||
|
if slice.index >= len(msg.existing_hashes):
|
||||||
|
task.type = NEW_CHUNK
|
||||||
|
else:
|
||||||
|
task.type = PATCH_CHUNK
|
||||||
|
task.base_hash = msg.existing_hashes[slice.index]
|
||||||
|
|
||||||
|
op.chunks_to_process.add(task)
|
||||||
|
|
||||||
|
# Step 3: Start Uploads (Parallel up to limit)
|
||||||
|
pump_upload_queue(op)
|
||||||
|
|
||||||
|
function pump_upload_queue(op):
|
||||||
|
while active_uploads(op) < MAX_PARALLEL_UPLOADS:
|
||||||
|
task = get_next_waiting_task(op)
|
||||||
|
if not task: break
|
||||||
|
|
||||||
|
# We must upload to N replicas (usually 3)
|
||||||
|
for replica_addr in task.target_servers:
|
||||||
|
conn = get_cs_connection(replica_addr)
|
||||||
|
|
||||||
|
if task.type == NEW_CHUNK:
|
||||||
|
# Full upload
|
||||||
|
send_message(conn, type=CREATE_CHUNK, data=task.data)
|
||||||
|
else:
|
||||||
|
# Differential patch (Rsync-style or offset-based)
|
||||||
|
send_message(conn, type=UPLOAD_CHUNK,
|
||||||
|
base_hash=task.base_hash,
|
||||||
|
patch_data=task.data)
|
||||||
|
|
||||||
|
function handle_upload_ack(op, msg):
|
||||||
|
# Step 4: Collect new hashes
|
||||||
|
task = get_task(op, msg.task_id)
|
||||||
|
|
||||||
|
# The CS computes the new SHA256 after applying the patch/data
|
||||||
|
task.final_hash = msg.new_hash
|
||||||
|
task.replicas_confirmed++
|
||||||
|
|
||||||
|
if task.replicas_confirmed >= REQUIRED_REPLICATION:
|
||||||
|
task.status = DONE
|
||||||
|
|
||||||
|
if all_tasks_complete(op):
|
||||||
|
commit_write(op)
|
||||||
|
else:
|
||||||
|
pump_upload_queue(op) # Start next batch
|
||||||
|
|
||||||
|
function commit_write(op):
|
||||||
|
# Step 5: Atomic Commit at Metadata Server
|
||||||
|
commit_msg = new Message(type=WRITE_REQUEST)
|
||||||
|
commit_msg.path = op.path
|
||||||
|
commit_msg.expected_generation = op.expected_generation # Optimistic Locking
|
||||||
|
commit_msg.flags = op.flags
|
||||||
|
|
||||||
|
# Map the linear chunk index to the new Hash + Location list
|
||||||
|
commit_msg.chunks = map_chunks_to_hashes_and_locations(op.chunks_to_process)
|
||||||
|
|
||||||
|
send_message(client.metadata_server_conn, commit_msg)
|
||||||
|
|
||||||
|
function handle_commit_response(op, msg):
|
||||||
|
if msg.type == WRITE_SUCCESS:
|
||||||
|
op.status = COMPLETED
|
||||||
|
op.result_data = msg.new_generation
|
||||||
|
elif msg.type == ERROR and msg.code == GENERATION_MISMATCH:
|
||||||
|
# Optimistic concurrency failed (someone else wrote).
|
||||||
|
# Retry the whole operation from Step 1 (READ).
|
||||||
|
reset_and_retry(op)
|
||||||
|
else:
|
||||||
|
op.status = FAILED
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 4. EVENT LOOP (Driver)
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function wait_for_result(client, op_handle):
|
||||||
|
while get_op_status(op_handle) == PENDING:
|
||||||
|
# Wait for network activity (Poll/Select)
|
||||||
|
events = wait_for_network_events(timeout=DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
if event.type == DISCONNECT:
|
||||||
|
handle_disconnect(client, event.conn)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Route message to the specific Operation ID embedded in the packet tag
|
||||||
|
op = client.pending_operations[event.tag.op_id]
|
||||||
|
msg = read_message(event.conn)
|
||||||
|
|
||||||
|
# Dispatcher
|
||||||
|
switch (op.type, msg.type):
|
||||||
|
case (READ, READ_SUCCESS): handle_read_metadata_response(op, msg)
|
||||||
|
case (READ, CHUNK_DATA): handle_chunk_download_success(op, msg)
|
||||||
|
|
||||||
|
case (WRITE, READ_SUCCESS): handle_write_metadata_ready(op, msg)
|
||||||
|
case (WRITE, UPLOAD_ACK): handle_upload_ack(op, msg)
|
||||||
|
case (WRITE, WRITE_SUCCESS): handle_commit_response(op, msg)
|
||||||
|
|
||||||
|
case (_, ERROR): handle_error(op, msg)
|
||||||
|
|
||||||
|
return get_op_result(op_handle)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user