Skip to content

Commit 787071e

Browse files
jimwwalkerdaverigby
authored andcommitted
MB-31978: DCP close-stream support for stream-ID
Allow the client to encode (in flex-frame) a stream-ID so that a single stream can be closed. Change-Id: I22fde5e5696b9a951b598c241399d215bdf9588a Reviewed-on: http://review.couchbase.org/102723 Reviewed-by: Ben Huddleston <[email protected]> Tested-by: Build Bot <[email protected]>
1 parent 0b2ae88 commit 787071e

File tree

12 files changed

+210
-51
lines changed

12 files changed

+210
-51
lines changed

daemon/mcbp_validators.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ Status McbpValidator::verify_header(Cookie& cookie,
406406
return false;
407407
}
408408
case cb::mcbp::request::FrameInfoId::DcpStreamId:
409-
if (data.size() != sizeof(cb::mcbp::DcpStreamIdFrameInfo)) {
409+
if (data.size() != sizeof(cb::mcbp::DcpStreamId)) {
410410
status = Status::Einval;
411411
cookie.setErrorContext("DcpStreamId invalid size:" +
412412
std::to_string(data.size()));

daemon/protocol/mcbp/dcp_close_stream_executor.cc

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,21 @@ void dcp_close_stream_executor(Cookie& cookie) {
2828
auto& connection = cookie.getConnection();
2929
if (ret == ENGINE_SUCCESS) {
3030
const auto& header = cookie.getHeader().getRequest();
31-
ret = dcpCloseStream(cookie,
32-
header.getOpaque(),
33-
header.getVBucket(),
34-
{/*@todo read a DcpStreamId from flex-frame*/});
31+
cb::mcbp::DcpStreamId dcpStreamId; // Initialises to 'none'
32+
header.parseFrameExtras([&dcpStreamId](
33+
cb::mcbp::request::FrameInfoId id,
34+
cb::const_byte_buffer data) -> bool {
35+
if (id == cb::mcbp::request::FrameInfoId::DcpStreamId) {
36+
// Data is the u16 stream-ID in network byte-order
37+
dcpStreamId = cb::mcbp::DcpStreamId(
38+
ntohs(*reinterpret_cast<const uint16_t*>(data.data())));
39+
return false;
40+
}
41+
return true;
42+
});
43+
44+
ret = dcpCloseStream(
45+
cookie, header.getOpaque(), header.getVBucket(), dcpStreamId);
3546
}
3647

3748
ret = connection.remapErrorCode(ret);

engines/ep/src/dcp/active_stream.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ class ActiveStream : public Stream,
129129
return cursor;
130130
}
131131

132+
bool compareStreamId(cb::mcbp::DcpStreamId id) const override {
133+
return id == sid;
134+
}
135+
132136
protected:
133137
/**
134138
* @param vb reference to the associated vbucket

engines/ep/src/dcp/producer.cc

Lines changed: 63 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,18 @@ ENGINE_ERROR_CODE DcpProducer::step(struct dcp_message_producers* producers) {
631631
se->getVbucket(),
632632
mapEndStreamStatus(getCookie(), se->getFlags()),
633633
resp->getStreamId());
634+
635+
if (resp->getStreamId() && sendStreamEndOnClientStreamClose) {
636+
if (!closeStreamInner(
637+
se->getVbucket(), resp->getStreamId(), true)
638+
.first) {
639+
throw std::logic_error(
640+
"DcpProducer::step(StreamEnd): no stream was found "
641+
"for " +
642+
se->getVbucket().to_string() + " " +
643+
resp->getStreamId().to_string());
644+
}
645+
}
634646
break;
635647
}
636648
case DcpResponse::Event::Commit: {
@@ -1053,6 +1065,42 @@ bool DcpProducer::handleResponse(const protocol_binary_response_header* resp) {
10531065
return false;
10541066
}
10551067

1068+
std::pair<std::shared_ptr<Stream>, bool> DcpProducer::closeStreamInner(
1069+
Vbid vbucket, cb::mcbp::DcpStreamId sid, bool eraseFromMapIfFound) {
1070+
std::shared_ptr<Stream> stream;
1071+
bool vbFound = false;
1072+
1073+
// Obtain exclusive access to the streams map and see if the vbucket is
1074+
// mapped.
1075+
std::lock_guard<StreamsMap> guard(streams);
1076+
auto rv = streams.find(vbucket, guard);
1077+
if (rv.second) {
1078+
vbFound = true;
1079+
// Vbucket is mapped, get exclusive access to the StreamContainer
1080+
auto handle = rv.first->wlock();
1081+
// Try and locate a matching stream
1082+
for (; !handle.end(); handle.next()) {
1083+
if (handle.get()->compareStreamId(sid)) {
1084+
stream = handle.get();
1085+
break;
1086+
}
1087+
}
1088+
1089+
if (eraseFromMapIfFound && stream) {
1090+
// Need to tidy up the map, we first call erase on the handle,
1091+
// which will erase the current element from the container
1092+
handle.erase();
1093+
1094+
// If the container is now empty, remove it from the map, the
1095+
// shared_ptr (held by rv) will do the real destruction.
1096+
if (handle.empty()) {
1097+
streams.erase(vbucket, guard);
1098+
}
1099+
}
1100+
}
1101+
return {stream, vbFound};
1102+
}
1103+
10561104
ENGINE_ERROR_CODE DcpProducer::closeStream(uint32_t opaque,
10571105
Vbid vbucket,
10581106
cb::mcbp::DcpStreamId sid) {
@@ -1078,55 +1126,30 @@ ENGINE_ERROR_CODE DcpProducer::closeStream(uint32_t opaque,
10781126
}
10791127

10801128
/* We should not remove the stream from the streams map if we have to
1081-
send the "STREAM_END" response asynchronously to the consumer */
1082-
std::shared_ptr<Stream> stream;
1083-
1084-
{
1085-
// Obtain exclusive access to the streams map and see if the vbucket is
1086-
// mapped.
1087-
std::lock_guard<StreamsMap> guard(streams);
1088-
auto rv = streams.find(vbucket, guard);
1089-
if (rv.second) {
1090-
// Vbucket is mapped, get exclusive access to the StreamContainer
1091-
auto handle = rv.first->wlock();
1092-
// Try and locate a matching stream
1093-
for (; !handle.end(); handle.next()) {
1094-
if (handle.get()->compareStreamId(sid)) {
1095-
stream = handle.get();
1096-
break;
1097-
}
1098-
}
1099-
1100-
if (!sendStreamEndOnClientStreamClose) {
1101-
// Need to tidy up the map, we first call erase on the handle,
1102-
// which will erase the current element from the container
1103-
handle.erase();
1104-
1105-
// If the container is now empty, remove it from the map, the
1106-
// shared_ptr (held by rv) will do the real destruction.
1107-
if (handle.empty()) {
1108-
streams.erase(vbucket, guard);
1109-
}
1110-
}
1111-
}
1112-
} // end streams lock scope
1129+
send the "STREAM_END" response asynchronously to the consumer, so
1130+
use the value of sendStreamEndOnClientStreamClose to determine if the
1131+
stream should be removed if found*/
1132+
auto rv = closeStreamInner(vbucket, sid, !sendStreamEndOnClientStreamClose);
11131133

11141134
ENGINE_ERROR_CODE ret;
1115-
if (!stream) {
1135+
if (!rv.first) {
11161136
logger->warn(
11171137
"({}) Cannot close stream because no "
1118-
"stream exists for this vbucket",
1119-
vbucket);
1120-
return ENGINE_KEY_ENOENT;
1138+
"stream exists for this vbucket {}",
1139+
vbucket,
1140+
sid);
1141+
return sid && rv.second ? ENGINE_DCP_STREAMID_INVALID
1142+
: ENGINE_KEY_ENOENT;
11211143
} else {
1122-
if (!stream->isActive()) {
1144+
if (!rv.first->isActive()) {
11231145
logger->warn(
11241146
"({}) Cannot close stream because "
1125-
"stream is already marked as dead",
1126-
vbucket);
1147+
"stream is already marked as dead {}",
1148+
vbucket,
1149+
sid);
11271150
ret = ENGINE_KEY_ENOENT;
11281151
} else {
1129-
stream->setDead(END_STREAM_CLOSED);
1152+
rv.first->setDead(END_STREAM_CLOSED);
11301153
ret = ENGINE_SUCCESS;
11311154
}
11321155
if (!sendStreamEndOnClientStreamClose) {

engines/ep/src/dcp/producer.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,29 @@ class DcpProducer : public ConnHandler,
448448
cb::mcbp::DcpStreamId sid,
449449
std::shared_ptr<Stream>& stream);
450450

451+
/**
452+
* Attempt to locate a stream associated with the vbucket/stream-id and
453+
* return it (this function is dedicated to the closeStream path)
454+
* The function returns a pair because in the case the shared_ptr is null
455+
* it could be because 1) the vbucket has no streams or 2) the vbucket
456+
* has streams, but none that matched the given sid. If the vbucket does
457+
* have streams, the pair.second will be true.
458+
* The function is invoked in two places in the case that closeStream wants
459+
* to send a CLOSE_STREAM message we leave the stream in the map... if a
460+
* new stream is created, we would replace that stream... however with
461+
* stream-ID enabled, it's possible a new stream is created with a new-ID
462+
* leaking the dead stream, hence a second path now frees the dead stream
463+
* by using this function and forcing erasure from the map.
464+
*
465+
* @param vbucket look for a stream associated with this vbucket
466+
* @param sid and with a stream-ID matching sid
467+
* @param eraseFromMapIfFound remove the shared_ptr from the streamsMap
468+
* @return a pair the shared_ptr (can be null if not found) and a bool which
469+
* allows the caller to determine if the vbucket or sid caused not found
470+
*/
471+
std::pair<std::shared_ptr<Stream>, bool> closeStreamInner(
472+
Vbid vbucket, cb::mcbp::DcpStreamId sid, bool eraseFromMapIfFound);
473+
451474
// stash response for retry if E2BIG was hit
452475
std::unique_ptr<DcpResponse> rejectResp;
453476

engines/ep/src/dcp/stream.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ class Stream {
134134

135135
void clear();
136136

137-
bool compareStreamId(cb::mcbp::DcpStreamId id) const {
138-
return id == this->id;
137+
virtual bool compareStreamId(cb::mcbp::DcpStreamId id) const {
138+
return id == cb::mcbp::DcpStreamId(0);
139139
}
140140

141141
protected:
@@ -196,8 +196,6 @@ class Stream {
196196

197197
Cursor noCursor;
198198

199-
cb::mcbp::DcpStreamId id;
200-
201199
private:
202200
/* readyQueueMemory tracks the memory occupied by elements
203201
* in the readyQ. It is an atomic because otherwise

engines/ep/tests/mock/mock_dcp_producer.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,20 @@ std::shared_ptr<Stream> MockDcpProducer::findStream(Vbid vbid) {
7878
return handle.get();
7979
}
8080
return nullptr;
81+
}
82+
83+
std::pair<std::shared_ptr<Stream>, bool> MockDcpProducer::findStream(
84+
Vbid vbid, cb::mcbp::DcpStreamId sid) {
85+
auto rv = streams.find(vbid);
86+
if (rv.second) {
87+
auto handle = rv.first->rlock();
88+
// Try and locate a matching stream
89+
for (; !handle.end(); handle.next()) {
90+
if (handle.get()->compareStreamId(sid)) {
91+
return {handle.get(), true};
92+
}
93+
}
94+
return {nullptr, true};
95+
}
96+
return {nullptr, false};
8197
}

engines/ep/tests/mock/mock_dcp_producer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ class MockDcpProducer : public DcpProducer {
119119
*/
120120
std::shared_ptr<Stream> findStream(Vbid vbid);
121121

122+
/**
123+
* Finds the stream for a given vbucket/sid
124+
* @returns a pair where second indicates if the VB has no entries at all
125+
* first is the stream (or null if no stream)
126+
*/
127+
std::pair<std::shared_ptr<Stream>, bool> findStream(
128+
Vbid vbid, cb::mcbp::DcpStreamId sid);
129+
122130
/**
123131
* Sets the backfill buffer size (max limit) to a particular value
124132
*/
@@ -181,4 +189,8 @@ class MockDcpProducer : public DcpProducer {
181189
void enableMultipleStreamRequests() {
182190
multipleStreamRequests = MultipleStreamRequests::Yes;
183191
}
192+
193+
void enableStreamEndOnClientStreamClose() {
194+
sendStreamEndOnClientStreamClose = true;
195+
}
184196
};

engines/ep/tests/module_tests/collections/evp_store_collections_dcp_stream_test.cc

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ class CollectionsDcpStreamsTest : public CollectionsDcpTest {
3737
CollectionsDcpTest::consumer = std::make_shared<MockDcpConsumer>(
3838
*engine, cookieC, "test_consumer");
3939
}
40+
41+
void close_stream_by_id_test(bool enableStreamEnd, bool manyStreams);
4042
};
4143

4244
TEST_F(CollectionsDcpStreamsTest, request_validation) {
@@ -104,6 +106,71 @@ TEST_F(CollectionsDcpStreamsTest, close_stream_validation2) {
104106
producer->closeStream(0, vbid, cb::mcbp::DcpStreamId(99)));
105107
}
106108

109+
// Core test method for close with a stream ID.
110+
// Allows a number of cases to be covered where we have many streams mapped to
111+
// the vbid and stream_end message enabled/disabled.
112+
void CollectionsDcpStreamsTest::close_stream_by_id_test(bool enableStreamEnd,
113+
bool manyStreams) {
114+
CollectionsManifest cm;
115+
cm.add(CollectionEntry::fruit);
116+
auto vb = store->getVBucket(vbid);
117+
vb->updateFromManifest({cm});
118+
if (enableStreamEnd) {
119+
producer->enableStreamEndOnClientStreamClose();
120+
}
121+
producer->enableMultipleStreamRequests();
122+
createDcpStream({{R"({"collections":["9"], "sid":99})"}}, vbid);
123+
124+
if (manyStreams) {
125+
createDcpStream({{R"({"collections":["9", "0"], "sid":101})"}}, vbid);
126+
createDcpStream({{R"({"collections":["0"], "sid":555})"}}, vbid);
127+
}
128+
129+
EXPECT_EQ(ENGINE_SUCCESS,
130+
producer->closeStream(0, vbid, cb::mcbp::DcpStreamId(99)));
131+
{
132+
auto rv = producer->findStream(vbid, cb::mcbp::DcpStreamId(99));
133+
// enableStreamEnd:true we must still find the stream in the producer
134+
EXPECT_EQ(enableStreamEnd, rv.first != nullptr);
135+
// manyStreams:true we always expect to find streams
136+
// manyStreams:false only expect streams if enableStreamEnd:true
137+
EXPECT_EQ(enableStreamEnd | manyStreams, rv.second);
138+
}
139+
140+
if (enableStreamEnd) {
141+
EXPECT_EQ(
142+
ENGINE_SUCCESS,
143+
producer->stepAndExpect(producers.get(),
144+
cb::mcbp::ClientOpcode::DcpStreamEnd));
145+
EXPECT_EQ(cb::mcbp::DcpStreamId(99), producers->last_stream_id);
146+
}
147+
148+
{
149+
auto rv = producer->findStream(vbid, cb::mcbp::DcpStreamId(99));
150+
// Always expect at this point to not find stream for sid 99
151+
EXPECT_FALSE(rv.first);
152+
// However the vb may still have streams for manyStreams test
153+
EXPECT_EQ(manyStreams, rv.second);
154+
}
155+
}
156+
157+
TEST_F(CollectionsDcpStreamsTest, close_stream_by_id_with_end_stream) {
158+
close_stream_by_id_test(true, false);
159+
}
160+
161+
TEST_F(CollectionsDcpStreamsTest, close_stream_by_id) {
162+
close_stream_by_id_test(false, false);
163+
}
164+
165+
TEST_F(CollectionsDcpStreamsTest,
166+
close_stream_by_id_with_end_stream_and_many_streams) {
167+
close_stream_by_id_test(true, true);
168+
}
169+
170+
TEST_F(CollectionsDcpStreamsTest, close_stream_by_id_and_many_streams) {
171+
close_stream_by_id_test(false, true);
172+
}
173+
107174
TEST_F(CollectionsDcpStreamsTest, two_streams) {
108175
CollectionsManifest cm;
109176
cm.add(CollectionEntry::fruit);

include/mcbp/protocol/request.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ class Request {
8383

8484
ServerOpcode getServerOpcode() const {
8585
if (is_client_magic(getMagic())) {
86-
throw std::logic_error("getServerOpcode: magic != server request");
86+
throw std::logic_error(
87+
"getServerOpcode: magic != server request: " +
88+
std::to_string(uint8_t(getMagic())));
8789
}
8890
return ServerOpcode(opcode);
8991
}

0 commit comments

Comments
 (0)