Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

DB intrinsic implementation in RocksDB #9529

Merged

Conversation

brianjohnson5972
Copy link
Contributor

@brianjohnson5972 brianjohnson5972 commented Sep 28, 2020

Change Description

Providing the implemenation for db_context_rocksdb, allowing db intrinsic data to be stored in rocksdb.
(WIll merge in Timothy's session changes as the last step before commiting this PR)

Change Type

Select ONE

  • Documentation
  • Stability bug fix
  • Other
  • Other - special case

Consensus Changes

  • Consensus Changes

API Changes

  • API Changes

Documentation Additions

  • Documentation Additions

libraries/chain/apply_context.cpp Show resolved Hide resolved
libraries/chain/backing_store/db_context_rocksdb.cpp Outdated Show resolved Hide resolved
int32_t db_idx_double_end(uint64_t code, uint64_t scope, uint64_t table) override {
return -1;
}
const int64_t billable_size = (int64_t)(value_size + db_key_value_any_lookup::overhead);
Copy link
Contributor

Choose a reason for hiding this comment

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

Two questions for my own information:

  1. Do we want to use auto in cases like this?
  2. Are C style or C++ style casts preferred? (static_cast probably in this case)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

1)I think the only advantage of using auto versus the type is if you are saving yourself typing, and going back and replacing would be more typing. (i.e. I think auto is just for a developers convenience, it doesn't make it more maintainable)
2) yes


// copy locally, since below the key_store memory will be changed
const auto old_payer = key_store.payer;
if (payer == name{}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How about payer.empty()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes

}

int32_t db_context_rocksdb::db_next_i64(int32_t itr, uint64_t& primary) {
if (itr < primary_iter_store.invalid_iterator()) return primary_iter_store.invalid_iterator(); // cannot increment past end iterator of table
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to make the logic here itr == primary_iter_store.invalid_iterator()? Would seem a better fit with how std iterators work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is a confusing use of iterator. It really should be called handle. But this was written to mimic the similar chainbase logic for DB, so I think it should match. "Handles" above 0 indicate a specifically cached database value, -1 is invalid, less than -1 indicates that you have a handle to the end of the table (-2 is the table[0], -3 is the table[1], etc). So in this case it is using this to indicate that db_next_i64 for "end handles" returns an invalid handle. As you can see as I talk about it, it is kind of an amalgam of handle and iterator.

}
}

void db_key_value_any_lookup::remove_table_if_empty(name scope, name table) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a version that removes the table if it is not empty? Could we shorten the name to remove_table if not?

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 prefer having the longer name, rather than having a short name and longer comment.

if (!view.get(parent.receiver.to_uint64_t(), slice_table_key)) {
std::string event_id;
apply_context& context = parent.context;
if (context.control.get_deep_mind_logger() != nullptr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we cache the result of get_deep_mind_logger and use that for the conditional below too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

const name payer = payer_payload{first_kv->value.data(), first_kv->value.size()}.payer;
std::string event_id;
apply_context& context = parent.context;
if (context.control.get_deep_mind_logger() != nullptr) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe cache the result of get_deep_mind_logger and use that for the conditional below too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok


struct db_key_value_any_lookup {
db_key_value_any_lookup(db_context& c, b1::chain_kv::view& v) : parent(c), view(v) {}
virtual ~db_key_value_any_lookup() {}
Copy link
Contributor

Choose a reason for hiding this comment

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

If the destructor is empty, we can let the compiler generate it for us.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

Copy link
Contributor

@linhuang-blockone linhuang-blockone left a comment

Choose a reason for hiding this comment

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

I looked at code at high level. I liked the refactor work, especially for payer related code.

@brianjohnson5972
Copy link
Contributor Author

@linhuang-blockone it is inside the if because we only need to construct the event_id if we are going to log it, otherwise we don't care. See the similar apply_context::dm_***_chainbase methods. This requirement comes from that code.

if( itr < primary_iter_store.invalid_iterator() ) { // is end iterator
const backing_store::unique_table* table_store = primary_iter_store.find_table_by_end_iterator(itr);
EOS_ASSERT( table_store, invalid_table_iterator, "not a valid end iterator" );
if (current_session.begin() == current_session.end()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be an opportunity for an optimization here, but session should just provide an empty and size method.


void db_key_value_any_lookup::add_table_if_needed(const shared_bytes& key, account_name payer) {
auto table_key = db_key_value_format::create_full_key_prefix(key, end_of_prefix::pre_type);
auto session_iter = current_session.lower_bound(table_key);
Copy link
Contributor

Choose a reason for hiding this comment

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

lower_bound is definitely the lynchpin method in all of this

auto entire_table_prefix_key = db_key_value_format::create_full_key_prefix(key, end_of_prefix::at_type);
// since this prefix key is just scope and table, it will include all primary, secondary, and table keys
auto session_itr = current_session.lower_bound(entire_table_prefix_key);
EOS_ASSERT( session_itr != current_session.end(), db_rocksdb_invalid_operation_exception,
Copy link
Contributor

Choose a reason for hiding this comment

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

Something that you might want to see if it makes a difference in performance is checking session_itr.key() instead. The end iterator will return an empty key and if you have an iterator that returns an empty key, it must be the end. That would bypass the needed creation of the end iterator. But I'm assuming that keys can never be empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

well, for any of my code the key should at the very least have the db type prefix, so empty key does mean we are not looking at anything in the db intrinsics portion of the code.

}

bool db_key_value_any_lookup::match(const shared_bytes& lhs, const shared_bytes& rhs) {
return lhs.size() == rhs.size() &&
Copy link
Contributor

Choose a reason for hiding this comment

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

shared_bytes has this operator.

if (iter == current_session.end()) {
return false;
}
return match(lhs, (*iter).first);
Copy link
Contributor

Choose a reason for hiding this comment

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

The iterator also has a key() method will only return the key instead of both the key and value.

const auto new_kt = static_cast<char>(key_type::table);
const char* new_kt_ptr = &new_kt;
// already have everything in char format/order, so just need to assemble it
return detail::assemble<2, eosio::session::shared_bytes>({rocksdb::Slice{prefix_key.data(), full_prefix_size},
Copy link
Contributor

Choose a reason for hiding this comment

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

We should get rid of the dependency of rocksdb in this file.

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 was planning on cleaning out the old code that used Slices, but figured I would use the same type that it is using throughout the cpp and hpp and then clean them all up together.

static const char db_type_prefix = detail::get_rocksdb_contract_db_prefix_as_char();
b1::chain_kv::bytes code_as_bytes;
b1::chain_kv::append_key(code_as_bytes, code.to_uint64_t());
auto ret = eosio::session::make_shared_bytes<std::string_view, 3>({std::string_view{&db_type_prefix, 1},
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I removed the make_shared_bytes function. We can add it back though since it would be much easier than converting all these over.

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 first tried making it one of the constructors, but was having lots of compiler problems so I just went the easy route. I'll circle back if this shows up as a performance hotspot.

eosio::session::shared_bytes as_payload() const {
char payer_buf[payer_in_value_size];
memcpy(payer_buf, &payer, payer_in_value_size);
return eosio::session::make_shared_bytes<std::string_view, 2>({std::string_view{payer_buf,
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a very nice concise of creating one of these by passing an array of string_view instances.

… rocksdb_db_implementation--last-merge

libraries/chain/include/eosio/chain/backing_store/kv_context_rocksdb.hpp
@brianjohnson5972 brianjohnson5972 merged commit d82288e into rocksdb_nodeos_integration Oct 8, 2020
@brianjohnson5972 brianjohnson5972 deleted the rocksdb_db_implementation branch January 27, 2021 19:50
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants