diff --git a/include/bitcoin/database/impl/primitives/nomap.ipp b/include/bitcoin/database/impl/primitives/nomap.ipp index 0f466bf08..31fe9a329 100644 --- a/include/bitcoin/database/impl/primitives/nomap.ipp +++ b/include/bitcoin/database/impl/primitives/nomap.ipp @@ -127,6 +127,12 @@ bool CLASS::reserve(const Link& size) NOEXCEPT return body_.reserve(size); } +TEMPLATE +Link CLASS::allocate(const Link& size) NOEXCEPT +{ + return body_.allocate(size); +} + TEMPLATE memory CLASS::get_memory() const NOEXCEPT { @@ -233,6 +239,39 @@ bool CLASS::put(const memory& ptr, const Element& element) NOEXCEPT return element.to_data(sink); } +TEMPLATE +template > +bool CLASS::put(const memory& ptr, const Link& link, + const Element& element) NOEXCEPT +{ + using namespace system; + if (!ptr || link.is_terminal()) + return false; + + const auto start = body::link_to_position(link); + if (is_limited(start)) + return false; + + const auto size = ptr.size(); + const auto position = possible_narrow_and_sign_cast(start); + if (position >= size) + return false; + + const auto offset = ptr.offset(start); + if (is_null(offset)) + return false; + + iostream stream{ offset, size - position }; + flipper sink{ stream }; + + if constexpr (!is_slab) + { + BC_DEBUG_ONLY(sink.set_limit(Size * element.count());) + } + + return element.to_data(sink); +} + TEMPLATE template > inline bool CLASS::put_link(Link& link, const Element& element) NOEXCEPT diff --git a/include/bitcoin/database/impl/query/archive/wire_writer.ipp b/include/bitcoin/database/impl/query/archive/wire_writer.ipp index 61c40f930..07d54d174 100644 --- a/include/bitcoin/database/impl/query/archive/wire_writer.ipp +++ b/include/bitcoin/database/impl/query/archive/wire_writer.ipp @@ -27,10 +27,12 @@ namespace database { // set transaction_view // ---------------------------------------------------------------------------- // This is the only multitable write query (except initialize/genesis). +// The caller holds the transactor and all table accessors, with all table rows +// preallocated by the block writer. No allocation may occur under accessors. TEMPLATE -code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, - bool bypass) NOEXCEPT +code CLASS::set_code(std::vector& twins, const accessors& ptrs, + const allocation& fks, const transaction_view& tx, bool bypass) NOEXCEPT { using namespace system; using ix = linkage; @@ -44,56 +46,45 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, const auto outputs = possible_narrow_cast(tx.outputs()); const auto coinbase = tx.is_coinbase(); - // ======================================================================== - const auto scope = get_transactor(); - - // Allocate contiguously and store inputs. - input_link in_fk{}; - if (!store_.input.put_link(in_fk, + // Contiguously store inputs (preallocated). + if (!store_.input.put(ptrs.input, fks.in_fk, table::input::put_view{ {}, tx })) return error::tx_input_put; - // Allocate contiguously and store outputs. - output_link out_fk{}; - if (!store_.output.put_link(out_fk, - table::output::put_view{ {}, tx_fk, tx })) + // Contiguously store outputs (preallocated). + if (!store_.output.put(ptrs.output, fks.out_fk, + table::output::put_view{ {}, fks.tx_fk, tx })) return error::tx_output_put; - // Allocate ins/point rows and contiguously store input links. + // Contiguously store input links (preallocated, rows shared with points). // Point elements are set into the same rows following tx set, below. - auto ins_fk = store_.ins.allocate(inputs); - if (ins_fk.is_terminal()) + // The caller's ins accessor guards raw sequence writes against remap. + if (!store_.ins.sequence.put(fks.ins_fk, + table::ins_sequence::put_view{ {}, fks.in_fk, fks.tx_fk, tx })) return error::tx_ins_put; - // Ins put is unguarded, the accessor guards its rows against remap. - auto insert = store_.ins.get_memory(); - if (!insert || !store_.ins.sequence.put(ins_fk, - table::ins_sequence::put_view{ {}, in_fk, tx_fk, tx })) - return error::tx_ins_put; - - insert.reset(); - - // Allocate and contiguously store output links. - outs_link outs_fk{}; - if (!store_.outs.put_link(outs_fk, - table::outs::put_view{ {}, out_fk, tx })) + // Contiguously store output links (preallocated). + if (!store_.outs.put(ptrs.outs, fks.outs_fk, + table::outs::put_view{ {}, fks.out_fk, tx })) return error::tx_outs_put; - // Create tx record. + // Create tx record (preallocated). // Commit is deferred for point/address index consistency. - if (!store_.tx.set(tx_fk, tx.hash(false), table::transaction::put_view - { - {}, - tx, - inputs, - outputs, - ins_fk, - outs_fk - })) + if (!store_.tx.set(ptrs.tx, fks.tx_fk, tx.hash(false), + table::transaction::put_view + { + {}, + tx, + inputs, + outputs, + fks.ins_fk, + fks.outs_fk + })) { return error::tx_tx_set; } + auto ins_fk = fks.ins_fk; auto ins = tx.get_inputs_stream(); read::bytes::fast isource{ ins }; @@ -104,7 +95,7 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, for (size_t in{}; in < inputs; ++in) { // Should always be a null point - but could be invalid. - if (!store_.ins.put(ins_fk++, chain::point(isource), + if (!store_.ins.put(ptrs.ins, ins_fk++, chain::point(isource), table::ins_point::record{})) return error::tx_null_point_put; @@ -117,17 +108,14 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, { if (store_.is_dirty() || !bypass) { - // Collect duplicates to store in duplicate table. - std::vector twins{}; - auto ptr = store_.ins.get_memory(); - + // Collect duplicates for deferred store in duplicate table. for (size_t in{}; in < inputs; ++in) { bool duplicate{}; - if (!store_.ins.put(duplicate, ptr, ins_fk++, + if (!store_.ins.put(duplicate, ptrs.ins, ins_fk++, chain::point(isource), table::ins_point::record{})) return error::tx_point_put; - + if (duplicate) { isource.rewind_bytes(chain::point::serialized_size()); @@ -138,23 +126,12 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, isource.skip_bytes(isource.read_size()); isource.skip_bytes(sizeof(uint32_t)); } - - ptr.reset(); - - // As few duplicates are expected, duplicate domain is only 2^16. - // Return of tx_duplicate_put implies link domain has overflowed. - for (const auto& twin: twins) - if (!store_.duplicate.exists(twin)) - if (!store_.duplicate.put(twin, table::duplicate::record{})) - return error::tx_duplicate_put; } else { - auto ptr = store_.ins.get_memory(); - for (size_t in{}; in < inputs; ++in) { - if (!store_.ins.put(ptr, ins_fk++, chain::point(isource), + if (!store_.ins.put(ptrs.ins, ins_fk++, chain::point(isource), table::ins_point::record{})) return error::tx_point_put; @@ -162,8 +139,6 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, isource.skip_bytes(isource.read_size()); isource.skip_bytes(sizeof(uint32_t)); } - - ptr.reset(); } } @@ -171,14 +146,11 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, if (!isource) return error::tx_null_point_put; - // Commit address index records (hashmap). + // Commit address index records (hashmap, preallocated). if (address_enabled()) { - auto ad_fk = store_.address.allocate(outputs); - if (ad_fk.is_terminal()) - return error::tx_address_allocate; - - const auto ptr = store_.address.get_memory(); + auto ad_fk = fks.ad_fk; + auto out_fk = fks.out_fk; auto outs = tx.get_outputs_stream(); read::bytes::fast osource{ outs }; @@ -187,7 +159,7 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, const auto value = osource.read_8_bytes_little_endian(); const auto bytes = osource.read_size(); - if (!store_.address.put(ptr, ad_fk++, + if (!store_.address.put(ptrs.address, ad_fk++, sha256_hash(osource.read_bytes(bytes)), table::address::record{ {}, out_fk })) return error::tx_address_put; @@ -203,9 +175,8 @@ code CLASS::set_code(const tx_link& tx_fk, const transaction_view& tx, } // Commit tx to search (hashmap). - return store_.tx.commit(tx_fk, tx.hash(false)) ? + return store_.tx.commit(ptrs.tx, fks.tx_fk, tx.hash(false)) ? error::success : error::tx_tx_commit; - // ======================================================================== } // set txs from block @@ -242,6 +213,12 @@ code CLASS::set_code(const block_view& block, const header_link& key, bool strong, bool bypass, size_t height) NOEXCEPT { using namespace system; + using in_t = input_link::integer; + using out_t = output_link::integer; + using ins_t = ins_link::integer; + using outs_t = outs_link::integer; + using address_t = address_link::integer; + if (key.is_terminal()) return error::txs_header; @@ -249,16 +226,19 @@ code CLASS::set_code(const block_view& block, const header_link& key, if (is_zero(txs)) return error::txs_empty; - const auto count = possible_narrow_cast>(txs); - const auto tx_fks = store_.tx.allocate(count); - if (tx_fks.is_terminal()) - return error::tx_tx_allocate; - - code ec{}; - auto fk = tx_fks; + // Sum full block allocation for each table from cached view metadata. + // Output rows are parent fk prefixed (see table::output::put_view). + size_t points{}; + size_t outputs{}; + size_t input_bytes{}; + size_t output_bytes{}; for (const auto& tx: block.views()) - if ((ec = set_code(fk++, tx, bypass))) - return ec; + { + points += tx.inputs(); + outputs += tx.outputs(); + input_bytes += tx.input_table_size(); + output_bytes += tx.outputs() * tx_link::size + tx.output_table_size(); + } // Optional hash, only has value on height intervals. auto interval = create_interval(key, height); @@ -268,13 +248,104 @@ code CLASS::set_code(const block_view& block, const header_link& key, const auto forks = store_.fork_flags(); using bytes = linkage::integer; - const auto light = possible_narrow_cast( - block.serialized_size(false)); - const auto heavy = possible_narrow_cast - (block.serialized_size(true)); + const auto count = possible_narrow_cast>(txs); + const auto light = possible_narrow_cast(block.serialized_size(false)); + const auto heavy = possible_narrow_cast(block.serialized_size(true)); // ======================================================================== const auto scope = get_transactor(); + + // Allocate all block rows for each table (one allocation lock each). + const auto tx_fks = store_.tx.allocate(count); + if (tx_fks.is_terminal()) + return error::tx_tx_allocate; + + allocation fks{}; + fks.tx_fk = tx_fks; + + fks.in_fk = store_.input.allocate( + possible_narrow_cast(input_bytes)); + if (fks.in_fk.is_terminal()) + return error::tx_input_put; + + fks.out_fk = store_.output.allocate( + possible_narrow_cast(output_bytes)); + if (fks.out_fk.is_terminal()) + return error::tx_output_put; + + fks.ins_fk = store_.ins.allocate( + possible_narrow_cast(points)); + if (fks.ins_fk.is_terminal()) + return error::tx_ins_put; + + fks.outs_fk = store_.outs.allocate( + possible_narrow_cast(outputs)); + if (fks.outs_fk.is_terminal()) + return error::tx_outs_put; + + const auto address = address_enabled(); + if (address) + { + fks.ad_fk = store_.address.allocate( + possible_narrow_cast(outputs)); + if (fks.ad_fk.is_terminal()) + return error::tx_address_allocate; + } + + // Guard all tables against remap for the duration of the block write. + // No table may be allocated while any of these accessors are held. + accessors ptrs{}; + ptrs.tx = store_.tx.get_memory(); + ptrs.ins = store_.ins.get_memory(); + ptrs.outs = store_.outs.get_memory(); + ptrs.input = store_.input.get_memory(); + ptrs.output = store_.output.get_memory(); + if (address) + ptrs.address = store_.address.get_memory(); + + if (!ptrs.input || + !ptrs.output || + !ptrs.ins || + !ptrs.outs || + !ptrs.tx || + (address && !ptrs.address)) + return error::unloaded_file; + + // Write all txs into their preallocated rows (write order preserved). + code ec{}; + std::vector twins{}; + for (const auto& tx: block.views()) + { + if ((ec = set_code(twins, ptrs, fks, tx, bypass))) + return ec; + + // Output rows are parent fk prefixed (see table::output::put_view). + const auto out_bytes = tx.outputs() * tx_link::size + + tx.output_table_size(); + + fks.tx_fk++; + fks.ins_fk.value += possible_narrow_cast(tx.inputs()); + fks.outs_fk.value += possible_narrow_cast(tx.outputs()); + fks.in_fk.value += possible_narrow_cast(tx.input_table_size()); + fks.out_fk.value += possible_narrow_cast(out_bytes); + fks.ad_fk.value += possible_narrow_cast(tx.outputs()); + } + + // Release all accessors (subsequent writes allocate). + ptrs.tx.reset(); + ptrs.ins.reset(); + ptrs.outs.reset(); + ptrs.input.reset(); + ptrs.output.reset(); + ptrs.address.reset(); + + // As few duplicates are expected, duplicate domain is only 2^16. + // Return of tx_duplicate_put implies link domain has overflowed. + for (const auto& twin: twins) + if (!store_.duplicate.exists(twin)) + if (!store_.duplicate.put(twin, table::duplicate::record{})) + return error::tx_duplicate_put; + constexpr auto positive = true; // Transactor assures cannot be restored without txs, as required to unset. diff --git a/include/bitcoin/database/primitives/nomap.hpp b/include/bitcoin/database/primitives/nomap.hpp index 0b20d34a3..36fc4cc63 100644 --- a/include/bitcoin/database/primitives/nomap.hpp +++ b/include/bitcoin/database/primitives/nomap.hpp @@ -85,6 +85,9 @@ class nomap /// any subsequent element is reserved or put, or will overwrite. bool reserve(const Link& size) NOEXCEPT; + /// Allocate count records or slab bytes at returned link (follow with put). + Link allocate(const Link& size) NOEXCEPT; + /// Return ptr for batch processing, holds shared lock on storage remap. memory get_memory() const NOEXCEPT; @@ -122,6 +125,11 @@ class nomap template = true> bool put(const memory& ptr, const Element& element) NOEXCEPT; + /// Put previously allocated element at link, using get_memory() ptr. + template = true> + bool put(const memory& ptr, const Link& link, + const Element& element) NOEXCEPT; + /// Put element and return link. template = true> bool put_link(Link& link, const Element& element) NOEXCEPT; diff --git a/include/bitcoin/database/query.hpp b/include/bitcoin/database/query.hpp index 7d8d46404..d6401ee8f 100644 --- a/include/bitcoin/database/query.hpp +++ b/include/bitcoin/database/query.hpp @@ -919,8 +919,34 @@ class query /// ----------------------------------------------------------------------- code set_code(const tx_link& tx_fk, const transaction& tx, bool bypass) NOEXCEPT; - code set_code(const tx_link& tx_fk, const transaction_view& tx, - bool bypass) NOEXCEPT; + + /// Block write batching (all rows preallocated, all accessors held). + /// ----------------------------------------------------------------------- + + /// Per-table accessors, each holds a shared lock on its storage remap. + struct accessors + { + memory input; + memory output; + memory ins; + memory outs; + memory tx; + memory address; + }; + + /// Per-tx allocated table links (bases advanced by the block writer). + struct allocation + { + tx_link tx_fk; + input_link in_fk; + output_link out_fk; + ins_link ins_fk; + outs_link outs_fk; + address_link ad_fk; + }; + + code set_code(std::vector& twins, const accessors& ptrs, + const allocation& fks,const transaction_view& tx, bool bypass) NOEXCEPT; /// History. /// ----------------------------------------------------------------------- diff --git a/test/primitives/nomap.cpp b/test/primitives/nomap.cpp index 68ff974bd..50e247e6a 100644 --- a/test/primitives/nomap.cpp +++ b/test/primitives/nomap.cpp @@ -823,4 +823,63 @@ BOOST_AUTO_TEST_CASE(nomap__slab_drop__populated__empties_body_and_head) BOOST_REQUIRE(!instance.get_fault()); } +// allocate/put (batch) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(nomap__record_allocate_put__batch__expected) +{ + test::chunk_storage head_store{}; + test::chunk_storage body_store{}; + record_table instance{ head_store, body_store }; + + // Allocate before ptr acquisition (allocation must not follow accessor). + BOOST_REQUIRE_EQUAL(instance.allocate(2), 0u); + BOOST_REQUIRE_EQUAL(instance.count(), 2u); + + const auto ptr = instance.get_memory(); + BOOST_REQUIRE(instance.put(ptr, 0, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE(instance.put(ptr, 1, big_record{ 0x04030201_u32 })); + BOOST_REQUIRE(!instance.put(ptr, 2, little_record{ 0x04030201_u32 })); + BOOST_REQUIRE(!instance.put(ptr, link5::terminal, little_record{})); + + little_record record1{}; + BOOST_REQUIRE(instance.get(0, record1)); + BOOST_REQUIRE_EQUAL(record1.value, 0x04030201_u32); + + big_record record2{}; + BOOST_REQUIRE(instance.get(1, record2)); + BOOST_REQUIRE_EQUAL(record2.value, 0x04030201_u32); + + const auto expected_file = base16_chunk("0102030404030201"); + BOOST_REQUIRE_EQUAL(body_store.buffer(), expected_file); + BOOST_REQUIRE(!instance.get_fault()); +} + +BOOST_AUTO_TEST_CASE(nomap__slab_allocate_put__batch__expected) +{ + test::chunk_storage head_store{}; + test::chunk_storage body_store{}; + slab_table instance{ head_store, body_store }; + + // Slab allocation is byte count. + BOOST_REQUIRE_EQUAL(instance.allocate(8), 0u); + BOOST_REQUIRE_EQUAL(instance.count(), 8u); + + const auto ptr = instance.get_memory(); + BOOST_REQUIRE(instance.put(ptr, 0, little_slab{ 0x04030201_u32 })); + BOOST_REQUIRE(instance.put(ptr, 4, big_slab{ 0x04030201_u32 })); + + little_slab slab1{}; + BOOST_REQUIRE(instance.get(0, slab1)); + BOOST_REQUIRE_EQUAL(slab1.value, 0x04030201_u32); + + big_slab slab2{}; + BOOST_REQUIRE(instance.get(4, slab2)); + BOOST_REQUIRE_EQUAL(slab2.value, 0x04030201_u32); + + const auto expected_file = base16_chunk("0102030404030201"); + BOOST_REQUIRE_EQUAL(body_store.buffer(), expected_file); + BOOST_REQUIRE(!instance.get_fault()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/query/archive/wire_writer.cpp b/test/query/archive/wire_writer.cpp index 6491f83a6..383d20595 100644 --- a/test/query/archive/wire_writer.cpp +++ b/test/query/archive/wire_writer.cpp @@ -22,4 +22,89 @@ BOOST_FIXTURE_TEST_SUITE(query_wire_writer_writer_tests, test::directory_setup_fixture) +// The view (wire) block writer must produce a store byte-identical to the +// chain block writer (see analogous chain_writer expectations). +BOOST_AUTO_TEST_CASE(query_wire_writer__set_block_view__genesis__expected) +{ + constexpr auto milestone = true; + const auto genesis_header_body = system::base16_chunk( + "ffff7f" // next-> + "6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000" // sk (block.hash) + "04030201" // flags + "141312" // height + "24232221" // mtp + "ffffff" // previous_block_hash (header_fk - not found) (milestone true) + "01000000" // version + "29ab5f49" // timestamp + "ffff001d" // bits + "1dac2b7c" // nonce + "3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a"); // merkle_root + const auto genesis_tx_body = system::base16_chunk( + "ffffff7f" // next-> + "3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a" // sk (tx.hash(false)) + "cc0080" // light (coinbase merged) + "cc0000" // heavy + "00000000" // locktime + "01000000" // version + "010000" // ins_count + "010000" // outs_count + "00000000" // point_fk-> (ins_fk) + "00000000"); // outs_fk-> + const auto genesis_outs_body = system::base16_chunk( + "0000000000"); // output0_fk-> + const auto genesis_output_body = system::base16_chunk( + "00000000" // parent_fk-> + "ff00f2052a01000000" // value + "434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac"); // script + const auto genesis_ins_body = system::base16_chunk( + "ffffffff" // next-> + "0000000000000000000000000000000000000000000000000000000000000000" + "ffffff"); // index + const auto genesis_sequence_body = system::base16_chunk( + "ffffffff" // sequence + "0000000000" // input_fk-> (coinbase) + "00000000"); // parent_fk-> + const auto genesis_input_body = system::base16_chunk( + "4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73" // script + "00"); // witness + const auto genesis_txs_body = system::base16_chunk( + "1d0100" // size light (285) + "1d0100" // size heavy (285) + "0100" // txs count (1) + "00000000" // transaction[0] + "ff" // depth (255) - genesis only + "00000000"); // forks (0) - genesis only + + settings settings{}; + settings.header_buckets = 8; + settings.tx_buckets = 8; + settings.ins_buckets = 8; + settings.txs_buckets = 16; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_CHECK(!store.create(test::events_handler)); + + // Set header, then txs from the parsed view of the serialized block. + BOOST_CHECK(query.set(test::genesis.header(), test::context, milestone)); + system::chain::block_view view{ test::genesis.to_data(true), true }; + BOOST_CHECK(view.is_valid()); + BOOST_CHECK_EQUAL(query.set_code(view, false, true), error::success); + BOOST_CHECK(query.is_block(test::genesis.hash())); + BOOST_CHECK(!store.close(test::events_handler)); + + BOOST_CHECK_EQUAL(store.header_body(), genesis_header_body); + BOOST_CHECK_EQUAL(store.tx_body(), genesis_tx_body); + BOOST_CHECK_EQUAL(store.ins_body(), genesis_ins_body); + BOOST_CHECK_EQUAL(store.ins_sequence_body(), genesis_sequence_body); + BOOST_CHECK_EQUAL(store.input_body(), genesis_input_body); + BOOST_CHECK_EQUAL(store.output_body(), genesis_output_body); + BOOST_CHECK_EQUAL(store.outs_body(), genesis_outs_body); + BOOST_CHECK_EQUAL(store.txs_body(), genesis_txs_body); + + const auto pointer = query.get_block(query.to_header(test::genesis.hash()), false); + BOOST_CHECK(pointer); + BOOST_CHECK(*pointer == test::genesis); +} + BOOST_AUTO_TEST_SUITE_END()