Skip to content

Commit 897cd88

Browse files
committed
MB-41944: Item::removeUserXattrs() operates on a copy
Item::removeUserXattrs is executed in the ActiveStream path for DCP connections that set IncludeDeletedUserXattrs::No. When we make any change to the payload being streamed, we must operate on a copy of Item::value. Our changes will reflect to other connections and to the persistence cursor otherwise, as Item::value points to the shared in-memory blob referenced by items in the CheckpointManager. Change-Id: I5e8ec8df788b695a3388fdfd95c4db9c79dd0849 Reviewed-on: http://review.couchbase.org/c/kv_engine/+/137837 Reviewed-by: Dave Rigby <[email protected]> Well-Formed: Build Bot <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 927dbde commit 897cd88

File tree

4 files changed

+139
-18
lines changed

4 files changed

+139
-18
lines changed

engines/ep/src/item.cc

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,12 +490,15 @@ Item::WasValueInflated Item::removeUserXattrs() {
490490
const auto bodySize = valNBytes - cb::xattr::get_body_offset(valBuf);
491491
Expects(bodySize == 0);
492492

493-
cb::xattr::Blob xattrBlob(valBuf, false);
494-
xattrBlob.prune_user_keys();
495-
setData(xattrBlob.data(), xattrBlob.size());
493+
// Operate on a copy
494+
const cb::xattr::Blob originalBlob(valBuf, false /*compressed*/);
495+
auto copy = cb::xattr::Blob(originalBlob);
496+
copy.prune_user_keys();
497+
const auto final = copy.finalize();
498+
setData(final.data(), final.size());
496499

497500
// We have removed all user-xattrs, clear the xattr dt if no xattr left
498-
if (xattrBlob.get_system_size() == 0) {
501+
if (copy.get_system_size() == 0) {
499502
setDataType(getDataType() & ~PROTOCOL_BINARY_DATATYPE_XATTR);
500503
}
501504

engines/ep/tests/module_tests/dcp_stream_test.cc

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,8 +3015,8 @@ void SingleThreadedActiveStreamTest::testProducerPrunesUserXattrsForDelete(
30153015
// only configurations that trigger user-xattr pruning in deletes.
30163016
ASSERT_TRUE((flags & DcpOpenFlag::IncludeDeletedUserXattrs) == 0);
30173017

3018-
auto vb = engine->getVBucket(vbid);
3019-
recreateProducerAndStream(*vb, flags);
3018+
auto& vb = *engine->getVBucket(vbid);
3019+
recreateProducerAndStream(vb, flags);
30203020

30213021
const auto currIncDelUserXattr =
30223022
(flags & DcpOpenFlag::IncludeDeletedUserXattrs) != 0
@@ -3045,9 +3045,34 @@ void SingleThreadedActiveStreamTest::testProducerPrunesUserXattrsForDelete(
30453045

30463046
auto* cookie = create_mock_cookie();
30473047

3048-
// Store a Deleted doc
3048+
struct Sizes {
3049+
Sizes(const Item& item) {
3050+
value = item.getNBytes();
3051+
3052+
cb::char_buffer valBuf{const_cast<char*>(item.getData()),
3053+
item.getNBytes()};
3054+
cb::xattr::Blob xattrBlob(valBuf, false);
3055+
xattrs = xattrBlob.size();
3056+
userXattrs = xattrBlob.get_user_size();
3057+
sysXattrs = xattrBlob.get_system_size();
3058+
body = item.getNBytes() - cb::xattr::get_body_offset(valBuf);
3059+
}
3060+
3061+
size_t value;
3062+
size_t xattrs;
3063+
size_t userXattrs;
3064+
size_t sysXattrs;
3065+
size_t body;
3066+
};
3067+
3068+
// Make an item..
30493069
auto item = makeCommittedItem(makeStoredDocKey("keyD"), value);
30503070
item->setDataType(bodyType | PROTOCOL_BINARY_DATATYPE_XATTR);
3071+
// .. and save the payload sizes for later checks.
3072+
const auto originalValue = value;
3073+
const auto originalSizes = Sizes(*item);
3074+
3075+
// Store the item as deleted
30513076
uint64_t cas = 0;
30523077
const auto expectedStoreRes = durReqs ? ENGINE_EWOULDBLOCK : ENGINE_SUCCESS;
30533078
ASSERT_EQ(expectedStoreRes,
@@ -3058,6 +3083,75 @@ void SingleThreadedActiveStreamTest::testProducerPrunesUserXattrsForDelete(
30583083
durReqs,
30593084
DocumentState::Deleted));
30603085

3086+
auto& readyQ = stream->public_readyQ();
3087+
ASSERT_EQ(0, readyQ.size());
3088+
3089+
// Verfies that the payload pointed by the item in CM is the same as the
3090+
// original one
3091+
const auto checkPayloadInCM =
3092+
[&vb, &originalValue, &originalSizes, &durReqs]() -> void {
3093+
const auto& manager = *vb.checkpointManager;
3094+
const auto& ckptList =
3095+
CheckpointManagerTestIntrospector::public_getCheckpointList(
3096+
manager);
3097+
// 1 checkpoint
3098+
ASSERT_EQ(1, ckptList.size());
3099+
const auto* ckpt = ckptList.front().get();
3100+
ASSERT_EQ(checkpoint_state::CHECKPOINT_OPEN, ckpt->getState());
3101+
// empty-item
3102+
auto it = ckpt->begin();
3103+
ASSERT_EQ(queue_op::empty, (*it)->getOperation());
3104+
// 1 metaitem (checkpoint-start)
3105+
it++;
3106+
ASSERT_EQ(3, ckpt->getNumMetaItems());
3107+
EXPECT_EQ(queue_op::checkpoint_start, (*it)->getOperation());
3108+
it++;
3109+
EXPECT_EQ(queue_op::set_vbucket_state, (*it)->getOperation());
3110+
it++;
3111+
EXPECT_EQ(queue_op::set_vbucket_state, (*it)->getOperation());
3112+
// 1 non-metaitem is our deletion
3113+
it++;
3114+
ASSERT_EQ(1, ckpt->getNumItems());
3115+
ASSERT_TRUE((*it)->isDeleted());
3116+
const auto expectedOp =
3117+
durReqs ? queue_op::pending_sync_write : queue_op::mutation;
3118+
EXPECT_EQ(expectedOp, (*it)->getOperation());
3119+
3120+
// Byte-by-byte comparison
3121+
EXPECT_EQ(originalValue, (*it)->getValue()->to_s());
3122+
3123+
// The latest check should already fail if even a single byte in the
3124+
// payload has changed, but check also the sizes of the specific value
3125+
// chunks.
3126+
const auto cmSizes = Sizes(**it);
3127+
EXPECT_EQ(originalSizes.value, cmSizes.value);
3128+
EXPECT_EQ(originalSizes.xattrs, cmSizes.xattrs);
3129+
EXPECT_EQ(originalSizes.userXattrs, cmSizes.userXattrs);
3130+
EXPECT_EQ(originalSizes.sysXattrs, cmSizes.sysXattrs);
3131+
ASSERT_EQ(originalSizes.body, cmSizes.body);
3132+
};
3133+
3134+
// Verify that the value of the item in CM has not changed
3135+
{
3136+
SCOPED_TRACE("");
3137+
checkPayloadInCM();
3138+
}
3139+
3140+
// Push items to the readyQ and check what we get
3141+
stream->nextCheckpointItemTask();
3142+
ASSERT_EQ(2, readyQ.size());
3143+
3144+
// MB-41944: The call to Stream::nextCheckpointItemTask() has removed
3145+
// UserXattrs from the payload. Before the fix we modified the item's value
3146+
// (which is a reference-counted object in memory) rather that a copy of it.
3147+
// So here we check that the item's value in CM is still untouched.
3148+
{
3149+
SCOPED_TRACE("");
3150+
checkPayloadInCM();
3151+
}
3152+
3153+
// Note: Doing this check after Stream::nextCheckpointItemTask() is another
3154+
// coverage for MB-41944, so I move it here.
30613155
if (persistent()) {
30623156
// Flush and ensure docs on disk
30633157
flush_vbucket_to_disk(vbid, 1 /*expectedNumFlushed*/);
@@ -3074,13 +3168,6 @@ void SingleThreadedActiveStreamTest::testProducerPrunesUserXattrsForDelete(
30743168
doc.item->getNBytes()));
30753169
}
30763170

3077-
auto& readyQ = stream->public_readyQ();
3078-
ASSERT_EQ(0, readyQ.size());
3079-
3080-
// Push items to the readyQ and check what we get
3081-
stream->nextCheckpointItemTask();
3082-
ASSERT_EQ(2, readyQ.size());
3083-
30843171
auto resp = stream->public_nextQueuedItem();
30853172
ASSERT_TRUE(resp);
30863173
ASSERT_EQ(DcpResponse::Event::SnapshotMarker, resp->getEvent());

include/xattr/blob.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,15 @@ class XATTR_PUBLIC_API Blob {
136136
}
137137

138138
/**
139-
* Get the size of the system xattr's located in the blob
139+
* Get the size of the system xattrs located in the blob
140140
*/
141141
size_t get_system_size() const;
142142

143+
/**
144+
* Get the size of the user xattrs located in the blob
145+
*/
146+
size_t get_user_size() const;
147+
143148
/**
144149
* Get pointer to the xattr data (raw data, including the len word)
145150
*/
@@ -273,6 +278,13 @@ class XATTR_PUBLIC_API Blob {
273278
void remove_segment(const size_t offset, const size_t size);
274279

275280
private:
281+
enum class Type : uint8_t { System, User };
282+
283+
/**
284+
* Get the size of the specific category of Xattrs located in the blob
285+
*/
286+
size_t get_xattrs_size(Type type) const;
287+
276288
cb::char_buffer blob;
277289

278290
/// When the incoming data is compressed will auto-decompress into this

xattr/blob.cc

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ uint32_t Blob::read_length(size_t offset) const {
277277
return ntohl(*ptr);
278278
}
279279

280-
size_t Blob::get_system_size() const {
280+
size_t Blob::get_xattrs_size(Type type) const {
281281
// special case.. there are no xattr's
282282
if (blob.len == 0) {
283283
return 0;
@@ -291,9 +291,20 @@ size_t Blob::get_system_size() const {
291291
while (current < blob.len) {
292292
// Get the length of the next kv-pair
293293
const auto size = read_length(current);
294-
if (blob.buf[current + 4] == '_') {
295-
ret += size + 4;
294+
295+
switch (type) {
296+
case Type::System:
297+
if (blob.buf[current + 4] == '_') {
298+
ret += size + 4;
299+
}
300+
break;
301+
case Type::User:
302+
if (blob.buf[current + 4] != '_') {
303+
ret += size + 4;
304+
}
305+
break;
296306
}
307+
297308
current += 4 + size;
298309
}
299310
} catch (const std::out_of_range&) {
@@ -302,6 +313,14 @@ size_t Blob::get_system_size() const {
302313
return ret;
303314
}
304315

316+
size_t Blob::get_system_size() const {
317+
return get_xattrs_size(Type::System);
318+
}
319+
320+
size_t Blob::get_user_size() const {
321+
return get_xattrs_size(Type::User);
322+
}
323+
305324
nlohmann::json Blob::to_json() const {
306325
nlohmann::json ret;
307326

0 commit comments

Comments
 (0)