Skip to content
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

feat(server): New auto-journal types/read/write #560

Merged
merged 3 commits into from
Dec 15, 2022

Conversation

dranikpg
Copy link
Contributor

Reduced version of #540

Contains only declaration of new journal type, reader and writer

@romange
Copy link
Collaborator

romange commented Dec 13, 2022

please install presubmit hooks in your git client

}

// Test serializing and de-serializing entries.
TEST(Journal, WriteRead) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stat might look like much but the tests need some utils to mock dragonfly slice types

Comment on lines 51 to 53
// This struct represents a single journal entry.
// Those are either control instructions or commands.
struct EntryNew { // Called this "New" because I can't delete the old neither replace it partially
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I called this New because we need both, because this PR contains only the declaration & read+write utils

Comment on lines 18 to 38
using nonstd::make_unexpected;

// TODO: Stolen from rdb, unite in common utils

#define SET_OR_RETURN(expr, dest) \
do { \
auto exp_val = (expr); \
if (!exp_val) { \
VLOG(1) << "Error while calling " #expr; \
return exp_val.error(); \
} \
dest = exp_val.value(); \
} while (0)

#define SET_OR_UNEXPECT(expr, dest) \
{ \
auto exp_res = (expr); \
if (!exp_res) \
return make_unexpected(exp_res.error()); \
dest = std::move(exp_res.value()); \
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still in favour of moving all the serialization into one folder and share common utils

Comment on lines 117 to 118
if (read < 1) // TODO: Custom errc namespace? Generic opcodes?
return make_unexpected(std::make_error_code(std::errc::result_out_of_range));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And sharing common error codes

However this would make this PR bigger than intended 🤔

@@ -18,7 +18,8 @@ add_library(dragonfly_lib channel_slice.cc command_registry.cc
list_family.cc main_service.cc memory_cmd.cc rdb_load.cc rdb_save.cc replica.cc
snapshot.cc script_mgr.cc server_family.cc malloc_stats.cc
set_family.cc stream_family.cc string_family.cc
zset_family.cc version.cc bitops_family.cc container_utils.cc)
zset_family.cc version.cc bitops_family.cc container_utils.cc
journal/serializer.cc)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that files in journal dir are under dfly_transaction lib
Maybe it makes more sense to put the journal/serializer.cc also there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, its only part of the replication flow, so I'm putting it into the outermost dependency

#include "io/io.h"
#include "server/common.h"
#include "server/journal/types.h"
#include "server/main_service.h"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is #include "server/main_service.h" used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to remove it, was from Journal executor


// JournalWriter serializes journal entries to a sink.
// It automatically keeps track of the current database index.
struct JournalWriter {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struct -> class


// JournalReader allows deserializing journal entries from a source.
// Like the writer, it automatically keeps track of the database index.
struct JournalReader {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struct -> class


namespace dfly {

// JournalWriter serializes journal entries to a sink.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I know know we have an option to write journal to file. Is this flow going to be dropped?
If not and you will use the JournalWriter, it is writing directly to sink without any buffering

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what sense dropped?

Its a bit more complicated. For now we can leave it as it is so the PR doesn't grow

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropped i.e we are going to remove this flow of writing to the journal to file.
If you want to write the buffering code later maybe add TODO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? The source is generic enough

For now its not part of the PR. Its a split of the original, its not finished

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont follow..
I dont see in the original PR buffering in the JournalWriter class
If you remember in the RDB serializer we had some buffering code which I removed cause it was redundant as we use it with StringSink and buffering is not needed
If this class is used with a sink which is a file (not a socket as in the replica flow) you will have multiple writes of 1/2/8 bytes and in this case we will need buffering in the JournalWriter or JournalWriter will write to a StringSink and there should be some wrapping code to write to the journal file.
Anyway I can wait to the next PRs and see how you use JournalWriter with File Sink

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • There is no journal file yet, I didn't cover this part at all
  • In my original PR we write to a string sink in the rdb saver
  • In stable state we write directly to the sink simply because I didn't yet develop a mechanism for doing it efficiently. Once we have it working and have full test coverage, we can start benchmarking it

return sink_->Write(io::Bytes{buf, sizeof(buf)});
}

error_code JournalWriter::WriteU64(uint64_t v) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about writing something similar to SerializeLen in rdb save?
Than you dont need to assume anything about the values you write . This can prevent bugs in calling the incorrect function and also may reduce the data size

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only for reducing data size.

I want to use SerializeLen from rdb, so I'll create a commons file for them

But in the future we should move all serialization logic into one folder

vec->resize(size);
for (auto& span : *vec) {
size_t len;
SET_OR_RETURN(ReadString(), len);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can change ReadString to return IoBuf::Bytes and this way you dont need the second loop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need it, because I can't keep any references to it while its growing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, please consider writing the cmd length in the writer so you could use reserve on the buffer at the beginning of the loop

struct JournalWriter {
// Initialize with sink and optional start database index. If no start index is set,
// a SELECT will be issued before the first entry.
JournalWriter(io::Sink* sink, std::optional<DbIndex> dbid = std::nullopt);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you use the dbid in any flow? I didnt see this flow in the bigger PR.
If you do initialize with db id than caller mast make sure to call the select this is a bit confusing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do. In which case must the caller care about the id? The should absorb all select entries

// Payload represents a non-owning view into a command executed on the shard.
using Payload =
std::variant<std::monostate, // No payload.
CmdArgList, // Parts of a full command.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give example when CmdArgList is set , and why only parts of the full command enter the payload
and when the std::pair<std::string_view, ArgSlice> is set
Why do we need this 2 options?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CmdArgList is for single shard commands, the pair for a combination of command name shard args.

Please look at how it's used in my original PR

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I believe you can simplify this by using only std::pair<std::string_view, ArgSlice> and for single shard commands the ShardArgsInShard will return all args of the command, the command name is stored in Transaction class cid_ var I think, this way you dont need full_args_ .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DF has a weird split, where multi shard commands use shard args for all values, and single shard commands use it only for determining the shard to run on.

For single shard commands it contains only the key, but not the full data. Changing this internal behavior is possible but requires revisiting transaction code and all the commands

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. If this will simplify the code you could still use full_args_ and only std::pair<std::string_view, ArgSlice> init with full_args_.front(), full_args.Span(1) . If you think this makes the flow simpler...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, but there is currently no simple way of converting a Span<Span<char>> (aka a CmdArgList) into a Span<string_view> (aka an ArgSlice) without allocating the backing container for storing the string views.

I referenced this issue in the original PR, emphasising that the differently typed spans are not easily interchangeable. But this is a difficult question on its own because it is used all over the place

DbIndex dbid;

Payload payload;
OwnedPayload owned_payload;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

payload is used for writing journal entries and owned_payload used for reading entries. I would consider using 2 struct one for writer and one for reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? What does it simplify or make more coherent?

We can make it extend a common base and use two subtypes for the varying subfields

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It simplifies understanding code and flow. You wrote the code so you understand that on the master and in writer you need to use payload and in replica and reader you need to use owned_payload, but for someone who reads it or want to change or add things to the flow I need to read the whole flow to understand it.
You could add a comment to the struct on this but it will be better to enforce it buy using 2 types and than when I add to the flow I cant access the incorrect filed.
Of course you can use base class.

@dranikpg dranikpg merged commit 08803e6 into dragonflydb:main Dec 15, 2022
@dranikpg dranikpg deleted the jf2-base-serde branch December 25, 2022 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants