Skip to content

mvcc: get-after-replace in single transaction may cause dirty read in secondary index #11687

@Astronomax

Description

@Astronomax

Bug description

MVCC assumes that if a get statement in a transaction sees a gap due to some other statement in that transaction (own_change == true), then no gap-tracker is required. But that's not true for secondary indexes. The same applies to the case when get read something - MVCC will incorrectly assume that a read-tracker is not required. To understand the issue, see the comment below regarding the reproducer.

The check that decides not to create a tracker:

if (txn != NULL && !own_change) {

Steps to reproduce

Run the following reproducer.lua script:

fiber = require('fiber')

box.cfg{memtx_use_mvcc_engine=true}

box.schema.space.create("test")
box.space.test:format{{'a', type='unsigned'}, {'b', type='unsigned'}}
box.space.test:create_index("pk", {parts={{'a'}}})
box.space.test:create_index("sk", {parts={{'b'}}, unique=true})

box.space.test:truncate()

box.space.test:replace{0, 2}

f1 = fiber.create(function()
	box.begin()
	box.space.test:replace{0, 1}
	local res = box.space.test.index.sk:get{2}
	print(res)
	fiber.sleep(1)
	box.commit()
end)
f1:set_joinable(true)

box.begin()
box.space.test:delete{0}
box.space.test:insert{2, 2}
box.commit()

local ok, err = f1:join()
print(ok, err)

print(require('yaml').encode(box.space.test:select{}))

box.space.test:drop()
os.exit()

How to run:

$ tarantool -i reproducer.lua

Actual output:

nil
true	nil
---
- [0, 1]
- [2, 2]
...

It is obvious that in the serialized sequence, the second transaction TXN2 (fiber f1) was the last one. This is because each of the transactions replaced 0 in the primary key (the first TXN1 performed replace{0, 2}, the second TXN2 performed replace{0, 1}, and the third TXN3 performed delete{0}). However, as seen in the output, the index ended up containing the tuple {0, 1}.
Thus, two schedules are possible: 1, 3, 2 and 3, 1, 2. 1, 3, 2 could not be, because then get(2) in transaction TXN2 should have returned {2, 2}, but it returned nil. But 3, 1, 2 is also impossible, because then replace{0, 2} in TXN1 should have failed with error:

Duplicate key exists in unique index "sk" in space "test" with old tuple
    - [2, 2] and new tuple - [0, 2]

because TXN3 transaction inserted {2, 2} before TXN1 tried to replace{0, 2}.

Actual behavior

The second transaction was not rolled back, resulting in a non-serializable execution.

Expected behavior

The second transaction is rolled back.

Metadata

Metadata

Assignees

Labels

3.2Target is 3.2 and all newer release/master branchesbugSomething isn't workingmvcc

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions