Skip to content

Should not the "graph_db::commit_transaction()" be thread safe? ( at least, while writing to Pmem version). Without it, uncommitted versions are read.

1) Issue Description: graph_db::commit_transaction() is not thread safe and probably must not throw(). Please refer to the attached pdf for a visual description.

2) Scenario : During the commit process:

step 1.The properties are copied from the dirty node to Pmem version.

step 2. The time stamps on the Pmem version is updated by the committing transaction.

Between this step:1 and step:2, an older "read transaction" can read the uncommitted version.

3) How to reproduce?

Consider 3 transactions :

Txn-1 : add a node.

Txn-2 : Read the node.

Txn-3 : Updates the node ( Txn-2 and Txn-3 are concurrent transactions)

Insert the below test case in "transaction_test.cpp" and comment out all remaining test cases.

4) Test case and debug code:

` TEST_CASE("Test concurrency: update during read" "[transaction]") {

#ifdef USE_PMDK

auto pop = prepare_pool();

auto gdb = create_graph_db(pop);

#else

auto gdb = create_graph_db();

#endif

node::id_t nid = 0;

barrier b1{}, b2{};

   // Just add a node

    auto tx = gdb->begin_transaction(); // Txn-1

    nid = gdb->add_node("Actor",
                        {{"name", boost::any(std::string("Mark Wahlberg"))},
                         {"age", boost::any(48)}});

    gdb->commit_transaction();

// Thread 1:

auto t1 = std::thread( [&] () { // Txn-2: read the node

	// read the node

	auto tx = gdb->begin_transaction();

	auto &n = gdb->node_by_id(nid);

	b1.notify();  // so that txn-3 can start.

	b2.wait(); // wiat till txn-3 does a update but not yet committed

	auto nd = gdb->get_node_description(n);

	REQUIRE(nd.label == "Actor"); // here it fails because this txn-2 sees "updated Actor"

	REQUIRE(get_property<int>(nd.properties, "age") == 48); //here too.. it sees 52 instead of 48

	gdb->commit_transaction();

});

//Thread 2:

auto t2 = std::thread( [&] () { // Txn-3 : update the same node

	// update the node

	b1.wait(); // ensure that update starts after read Txn

	auto tx = gdb->begin_transaction();

	auto &n = gdb->node_by_id(nid);

	gdb->update_node(n,  //update

		   {
		 { "age", boost::any(52)},
		  },
		   "Updated Actor");

	b2.notify(); // notify Txn-2 that update is over but not yet committed

	gdb->commit_transaction();

});

t1.join(); t2.join();

#ifdef USE_PMDK

drop_graph_db(pop, gdb);

#endif

} `

Since there are only two concurrent transactions in the above scenario, the issue cannot be reproduced consistently. So, introduce a small delay-code after line https://dbgit.prakinf.tu-ilmenau.de/code/poseidon_core/-/blob/null_values/src/storage/graph_db.cpp#L150

The delay-code to be inserted into void graph_db::commit_dirty_node(transaction_ptr tx, node::id_t node_id) {} after the above line is shown below:

std::this_thread::sleep_for(std::chrono::milliseconds(100));

5)Note : The delay-code may not be needed when there are n-concurrent transactions.

** 6)The output :**

transaction_test.cpp:131: FAILED:

REQUIRE( nd.label == "Actor" )

with expansion:

"Updated Actor" == "Actor" //=====> Txn-2 has read the uncommitted version instead of reading valid version.

terminate called after throwing an instance of 'Catch::TestFailureException'

Similar logic applies to relationships.

Scenario-2

Let Txn-1 : add node Txn-2 : read node (where Txn-1 and Txn-2 are concurrent transactions)

The same above concurrency issue may occur.

new_concurr_issue.pdf

Edited by Arun Kumar Tharanatha