@@ -5855,3 +5855,144 @@ INSTANTIATE_TEST_CASE_P(Persistent,
58555855 STParamPersistentBucketTest,
58565856 STParameterizedBucketTest::persistentConfigValues (),
58575857 STParameterizedBucketTest::PrintToStringParamName);
5858+
5859+ TEST_P (STParamPersistentBucketTest,
5860+ RemovingXattrDoesNotCauseIncorrectDatatypeOnReplica) {
5861+ // MB-52793: Under value eviction, if a replica:
5862+ // 1. Is a version without the fix for MB-50423 (6.6.5, 7.0.4 and down)
5863+ // 2. Has metadata for a deleted item with xattrs, but a non-resident value
5864+ // (e.g., has been bgfetched and evicted)
5865+ // 3. Receives a deletion over DCP changing a document from
5866+ // Xattrs+value -> no xattrs+no value
5867+ // The replica will persist a deleted item with datatype=xattrs but no
5868+ // value.
5869+ // NOTE: this has already been (incidentally) prevented by the fix for
5870+ // MB-50423 in some versions, this test is to guard against future
5871+ // regressions in this specific case.
5872+
5873+ setVBucketStateAndRunPersistTask (vbid, vbucket_state_replica);
5874+
5875+ // 1) Store an item with a (system) xattr
5876+ auto key = makeStoredDocKey (" key" );
5877+
5878+ cb::xattr::Blob xattrBlob;
5879+
5880+ xattrBlob.set (" _sync" , " somexattrvalue" );
5881+ auto xattrs = xattrBlob.finalize ();
5882+ auto xattrsStr = std::string (xattrs.begin (), xattrs.end ());
5883+
5884+ // store the deleted item
5885+ {
5886+ auto item = make_item (
5887+ vbid, key, xattrsStr, 0 , PROTOCOL_BINARY_DATATYPE_XATTR);
5888+
5889+ item.setCas (1234 );
5890+ item.setDeleted (DeleteSource::Explicit);
5891+
5892+ uint64_t seqno;
5893+ ASSERT_EQ (ENGINE_SUCCESS,
5894+ store->setWithMeta (item,
5895+ 0 /* cas */ ,
5896+ &seqno,
5897+ cookie,
5898+ {vbucket_state_replica},
5899+ CheckConflicts::No,
5900+ /* allowExisting*/ true ));
5901+ }
5902+
5903+ flushVBucketToDiskIfPersistent (vbid, 1 );
5904+
5905+ auto vb = store->getVBucket (vbid);
5906+ auto & ht = vb->ht ;
5907+
5908+ // persistence callback removes the delete from the HT
5909+ {
5910+ auto res = ht.findForRead (key,
5911+ TrackReference::No,
5912+ WantsDeleted::Yes,
5913+ ForGetReplicaOp::No);
5914+ ASSERT_FALSE (res.storedValue );
5915+ }
5916+
5917+ {
5918+ // cause the deleted item to be bgfetched back into the HT
5919+ get_options_t options =
5920+ static_cast <get_options_t >(QUEUE_BG_FETCH | GET_DELETED_VALUE);
5921+ auto gv = store->get (key, vbid, cookie, options);
5922+ ASSERT_EQ (ENGINE_EWOULDBLOCK, gv.getStatus ());
5923+
5924+ runBGFetcherTask ();
5925+ gv = store->get (key, vbid, cookie, options);
5926+ ASSERT_EQ (ENGINE_SUCCESS, gv.getStatus ());
5927+ ASSERT_EQ (PROTOCOL_BINARY_DATATYPE_XATTR, gv.item ->getDataType ());
5928+ ASSERT_NE (0 , gv.item ->getNBytes ());
5929+ ASSERT_EQ (xattrsStr, gv.item ->getValue ()->to_s ());
5930+ }
5931+
5932+ // now evict the value
5933+ {
5934+ auto res = ht.findForWrite (key, WantsDeleted::Yes);
5935+ ht.unlocked_ejectItem (
5936+ res.lock , res.storedValue , store->getEvictionPolicy ());
5937+ }
5938+
5939+ // check it exists in the desired state
5940+ {
5941+ auto res = ht.findForRead (key,
5942+ TrackReference::No,
5943+ WantsDeleted::Yes,
5944+ ForGetReplicaOp::No);
5945+ const auto * v = res.storedValue ;
5946+ if (fullEviction ()) {
5947+ // Item should be entirely removed.
5948+ EXPECT_FALSE (v);
5949+ } else {
5950+ // Item should still be present.
5951+ ASSERT_TRUE (v);
5952+ ASSERT_TRUE (v->isDeleted ());
5953+ ASSERT_FALSE (v->isResident ());
5954+ ASSERT_EQ (PROTOCOL_BINARY_DATATYPE_XATTR, v->getDatatype ());
5955+ }
5956+ }
5957+
5958+ // now drop the xattrs and store again, as if the only xattr has been
5959+ // removed by subdoc (as per what SyncGW does).
5960+ {
5961+ auto item = make_item (vbid, key, " " , 0 , PROTOCOL_BINARY_RAW_BYTES);
5962+ item.setCas (5678 );
5963+ item.setDeleted (DeleteSource::Explicit);
5964+
5965+ uint64_t cas = 0 ;
5966+ uint64_t seqno;
5967+
5968+ EXPECT_EQ (ENGINE_SUCCESS,
5969+ store->deleteWithMeta (key,
5970+ cas,
5971+ &seqno,
5972+ vbid,
5973+ cookie,
5974+ {vbucket_state_replica},
5975+ CheckConflicts::No,
5976+ item.getMetaData (),
5977+ GenerateBySeqno::Yes,
5978+ GenerateCas::No,
5979+ 0 ,
5980+ nullptr ,
5981+ DeleteSource::Explicit));
5982+ }
5983+
5984+ // At this point the damage has been done - the document has been corrupted
5985+ // (zero byte value but datatype==XATTR) both on-disk and in-memory.
5986+
5987+ // MB-50423: The item _must_ have datatype RAW_BYTES now, as it does not
5988+ // have any xattrs or a value.
5989+ {
5990+ auto res = ht.findForRead (key,
5991+ TrackReference::No,
5992+ WantsDeleted::Yes,
5993+ ForGetReplicaOp::No);
5994+ const auto * v = res.storedValue ;
5995+ EXPECT_TRUE (v->isDeleted ());
5996+ EXPECT_EQ (PROTOCOL_BINARY_RAW_BYTES, v->getDatatype ());
5997+ }
5998+ }
0 commit comments