Skip to content

Commit c92be65

Browse files
afrindmeta-codesync[bot]
authored andcommitted
Fix descending fetch gap-range bounds and tail-mark
Summary: Three bugs in the descending-fetch path of FetchWriteback / getGapRanges, found by review of the LocationIntervalSet conversion: 1. getGapRanges DESC range #1 extended to {startGroup, MAX} regardless of the user's fetch end, marking positions above fetchEnd as nonexistent. Likewise range #3 started at {endGroup, 0} regardless of fetchStart. Either could shadow positions outside the requested range and suppress later upstream fetches. Fixed by adding fetchStart/fetchEnd parameters and clamping ranges #1 and #3 against them when start.group / end.group is the fetch boundary. 2. cacheImpl streaming objectPayload finFetch path called markNonExistentTo(end_) without first advancing the iterator past the just-completed object, so the gap range overlapped it. Fixed by stepping fetchRangeIt_.next() before tail-marking. 3. Both finFetch tail-marks called markNonExistentTo(end_), where end_ is the user's highest endpoint. In DESC iteration, the iteration end is the lowest position, so the wrong endpoint was used. Fixed by switching to fetchRangeIt_.end(), which returns the order-aware iteration end. Adds three regression tests covering each bug. Differential Revision: D103210059
1 parent d4ec330 commit c92be65

3 files changed

Lines changed: 239 additions & 27 deletions

File tree

moxygen/relay/MoQCache.cpp

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,16 @@ bool exists(ObjectStatus status) {
131131
// Helper to compute gap ranges for markNonExistentTo.
132132
// Returns vector of (start, end) intervals to mark as gaps.
133133
// Handles both ascending and descending iteration orders.
134-
std::vector<std::pair<AbsoluteLocation, AbsoluteLocation>>
135-
getGapRanges(AbsoluteLocation start, AbsoluteLocation end, GroupOrder order) {
134+
//
135+
// fetchStart/fetchEnd are the user's original fetch range (inclusive start,
136+
// exclusive end), used to clamp DESC top-group and bottom-group ranges so we
137+
// never mark positions outside the requested range as non-existent.
138+
std::vector<std::pair<AbsoluteLocation, AbsoluteLocation>> getGapRanges(
139+
AbsoluteLocation start,
140+
AbsoluteLocation end,
141+
GroupOrder order,
142+
AbsoluteLocation fetchStart,
143+
AbsoluteLocation fetchEnd) {
136144
std::vector<std::pair<AbsoluteLocation, AbsoluteLocation>> ranges;
137145

138146
// Check if range is empty based on iteration order
@@ -167,10 +175,13 @@ getGapRanges(AbsoluteLocation start, AbsoluteLocation end, GroupOrder order) {
167175
// Descending: groups are iterated in reverse, but objects within groups
168176
// are still ascending. This may require up to 3 ranges.
169177

170-
// 1. Rest of current group (start group in iteration, but higher group
171-
// number)
172-
ranges.emplace_back(
173-
start, AbsoluteLocation{start.group, kLocationMax.object});
178+
// 1. Rest of start.group, clamped to fetchEnd when start.group is the
179+
// fetch's top group. start <= startGroupEnd is guaranteed by the
180+
// same-group early return above and the invariant start <= fetchEnd-1.
181+
AbsoluteLocation startGroupEnd = (start.group == fetchEnd.group)
182+
? *fetchEnd.prevInGroup()
183+
: AbsoluteLocation{start.group, kLocationMax.object};
184+
ranges.emplace_back(start, startGroupEnd);
174185

175186
// 2. Intermediate groups (between start.group-1 and end.group+1)
176187
// start.group > end.group guaranteed (descending, different groups),
@@ -182,10 +193,16 @@ getGapRanges(AbsoluteLocation start, AbsoluteLocation end, GroupOrder order) {
182193
ranges.emplace_back(*endNextGroup, *startPrevGroupEnd);
183194
} // else the groups were consecutive
184195

185-
// 3. Partial target group (lower group number, objects before target.object)
186-
if (auto prev = end.prevInGroup()) {
187-
ranges.emplace_back(AbsoluteLocation{end.group, 0}, *prev);
188-
} // else end.object == 0, no partial group to cover
196+
// 3. Partial end.group, clamped to fetchStart when end.group is the fetch's
197+
// bottom group. endGroupStart < end skips both end.object == 0 (no partial
198+
// group) and end == fetchStart (iterator at its terminal position), and
199+
// proves end.prevInGroup() is non-empty.
200+
AbsoluteLocation endGroupStart = (end.group == fetchStart.group)
201+
? fetchStart
202+
: AbsoluteLocation{end.group, 0};
203+
if (endGroupStart < end) {
204+
ranges.emplace_back(endGroupStart, *end.prevInGroup());
205+
}
189206

190207
return ranges;
191208
}
@@ -922,7 +939,7 @@ class MoQCache::FetchWriteback : public FetchConsumer {
922939
? end
923940
: AbsoluteLocation{start.group, kLocationMax.object},
924941
this);
925-
inProgressItersList_.push_back(setIt);
942+
inProgressItersList_.emplace_back(start.group, setIt);
926943
fetchRangeIt_.track->activeFetchCount++;
927944

928945
// Handle middle groups (if any)
@@ -931,14 +948,14 @@ class MoQCache::FetchWriteback : public FetchConsumer {
931948
AbsoluteLocation{currGroup, 0},
932949
AbsoluteLocation{currGroup, kLocationMax.object},
933950
this);
934-
inProgressItersList_.push_back(setIt);
951+
inProgressItersList_.emplace_back(currGroup, setIt);
935952
fetchRangeIt_.track->activeFetchCount++;
936953
}
937954
// Handle end group (if different from start)
938955
if (end.group != start.group) {
939956
setIt = fetchRangeIt_.track->fetchInProgress.insert(
940957
AbsoluteLocation{end.group, 0}, end, this);
941-
inProgressItersList_.push_back(setIt);
958+
inProgressItersList_.emplace_back(end.group, setIt);
942959
fetchRangeIt_.track->activeFetchCount++;
943960
}
944961

@@ -952,8 +969,8 @@ class MoQCache::FetchWriteback : public FetchConsumer {
952969
inProgress_.post();
953970
if (!inProgressItersList_.empty()) {
954971
while (dualIter_ != dualIter_.end()) {
955-
auto it = *dualIter_;
956-
fetchRangeIt_.track->fetchInProgress.erase(it->start.group, it);
972+
auto [groupKey, it] = *dualIter_;
973+
fetchRangeIt_.track->fetchInProgress.erase(groupKey, it);
957974
fetchRangeIt_.track->activeFetchCount--;
958975
++dualIter_;
959976
}
@@ -971,22 +988,22 @@ class MoQCache::FetchWriteback : public FetchConsumer {
971988
if (dualIter_ == dualIter_.end()) {
972989
return;
973990
}
991+
auto& [groupKey, it] = *dualIter_;
974992
// iterators in dualIter_ are group scoped
975-
if (start_.group == (*dualIter_)->start.group &&
976-
start_ < (*dualIter_)->end) {
993+
if (start_.group == groupKey && start_ < it->end) {
977994
// Update the start_ value of this interval
978-
(*dualIter_)->start = start_;
995+
it->start = start_;
979996
inProgress_.reset();
980997
} else {
981998
// Remove the iterator from track level tracking
982-
fetchRangeIt_.track->fetchInProgress.erase(
983-
(*dualIter_)->start.group, (*dualIter_));
999+
fetchRangeIt_.track->fetchInProgress.erase(groupKey, it);
9841000
++dualIter_;
9851001

9861002
// Iterator has processed the last element
9871003
if (dualIter_ != dualIter_.end()) {
9881004
// Update the start_ value of this interval
989-
(*dualIter_)->start = *fetchRangeIt_;
1005+
auto& [nextGroupKey, nextIt] = *dualIter_;
1006+
nextIt->start = *fetchRangeIt_;
9901007
inProgress_.reset();
9911008
}
9921009
}
@@ -1102,7 +1119,11 @@ class MoQCache::FetchWriteback : public FetchConsumer {
11021119
cache_.totalCachedBytes_ += addedBytes;
11031120
cache_.evictForByteLimitIfNeeded();
11041121
if (finFetch) {
1105-
markNonExistentTo(end_);
1122+
// Iterator still ON the just-completed object; step past it before
1123+
// tail-marking so the gap range doesn't overlap it. Use the iterator's
1124+
// order-aware end (DESC: lowest position; ASC: end_).
1125+
fetchRangeIt_.next();
1126+
markNonExistentTo(fetchRangeIt_.end());
11061127
updateInProgress();
11071128
}
11081129
return consumer_->objectPayload(std::move(payload), finFetch && proxyFin_);
@@ -1207,7 +1228,7 @@ class MoQCache::FetchWriteback : public FetchConsumer {
12071228
AbsoluteLocation end_;
12081229
bool proxyFin_{false};
12091230
std::shared_ptr<FetchConsumer> consumer_;
1210-
std::vector<FetchInProgressSet::IntervalList::iterator> inProgressItersList_;
1231+
std::vector<MoQCache::InProgressFetchEntry> inProgressItersList_;
12111232
MoQCache::InProgressFetchesIter dualIter_;
12121233
folly::coro::Baton inProgress_;
12131234
folly::coro::Baton complete_;
@@ -1225,7 +1246,12 @@ class MoQCache::FetchWriteback : public FetchConsumer {
12251246
return;
12261247
}
12271248

1228-
auto ranges = getGapRanges(*fetchRangeIt_, target, fetchRangeIt_.order);
1249+
auto ranges = getGapRanges(
1250+
*fetchRangeIt_,
1251+
target,
1252+
fetchRangeIt_.order,
1253+
fetchRangeIt_.minLocation,
1254+
fetchRangeIt_.maxLocation);
12291255
for (const auto& [start, end] : ranges) {
12301256
fetchRangeIt_.track->gaps.insert(start, end);
12311257
}
@@ -1274,7 +1300,10 @@ class MoQCache::FetchWriteback : public FetchConsumer {
12741300
fetchRangeIt_.next();
12751301
updateInProgress();
12761302
if (finFetch) {
1277-
markNonExistentTo(end_);
1303+
// Use the iterator's order-aware end. In DESC, end_ is the user's
1304+
// highest endpoint (the wrong direction); fetchRangeIt_.end()
1305+
// returns the iteration end (the lowest position).
1306+
markNonExistentTo(fetchRangeIt_.end());
12781307
complete_.post();
12791308
}
12801309
}

moxygen/relay/MoQCache.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,10 @@ class MoQCache {
209209

210210
// Entry for a track
211211
using FetchInProgressSet = FetchIntervalSet<FetchWriteback*>;
212+
using InProgressFetchEntry =
213+
std::pair<uint64_t, FetchInProgressSet::IntervalList::iterator>;
212214
// Type alias for the complex BidiIterator type used in FetchWriteback
213-
using InProgressFetchesIter =
214-
BidiIterator<std::vector<FetchInProgressSet::IntervalList::iterator>>;
215+
using InProgressFetchesIter = BidiIterator<std::vector<InProgressFetchEntry>>;
215216

216217
struct CacheTrack {
217218
folly::F14FastMap<uint64_t, std::shared_ptr<CacheGroup>> groups;

moxygen/relay/test/MoQCacheTests.cpp

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,6 +2047,188 @@ CO_TEST_F(MoQCacheTest, TestDescendingGapNearGroupZero) {
20472047
EXPECT_TRUE(res.hasValue());
20482048
}
20492049

2050+
// Regression: DESC getGapRanges range #1 used to extend to {topGroup, MAX}
2051+
// instead of clamping to fetchEnd, so a cold DESC fetch could mark positions
2052+
// above fetchEnd.object as nonexistent and suppress later upstream fetches.
2053+
CO_TEST_F(MoQCacheTest, TestDescendingFetchDoesNotMarkTopGroupTailAsGap) {
2054+
auto firstConsumer = std::make_shared<NiceMock<MockFetchConsumer>>();
2055+
ON_CALL(*firstConsumer, object(_, _, _, _, _, _, _))
2056+
.WillByDefault(Return(folly::unit));
2057+
ON_CALL(*firstConsumer, endOfFetch()).WillByDefault(Return(folly::unit));
2058+
EXPECT_CALL(*upstream_, fetch(_, _))
2059+
.WillOnce([this](Fetch fetch, std::shared_ptr<FetchConsumer> consumer) {
2060+
auto [standalone, joining] = fetchType(fetch);
2061+
EXPECT_EQ(standalone->start, (AbsoluteLocation{3, 3}));
2062+
EXPECT_EQ(standalone->end, (AbsoluteLocation{5, 10}));
2063+
EXPECT_EQ(fetch.groupOrder, GroupOrder::NewestFirst);
2064+
auto res =
2065+
consumer->object(3, 0, 3, makeBuf(100), noExtensions(), true);
2066+
EXPECT_FALSE(res.hasError());
2067+
res = consumer->endOfFetch();
2068+
EXPECT_FALSE(res.hasError());
2069+
upstreamFetchHandle_ =
2070+
std::make_shared<moxygen::MockFetchHandle>(FetchOk{
2071+
0,
2072+
GroupOrder::NewestFirst,
2073+
false,
2074+
AbsoluteLocation{3, 3},
2075+
});
2076+
return folly::coro::makeTask<Publisher::FetchResult>(
2077+
upstreamFetchHandle_);
2078+
})
2079+
.RetiresOnSaturation();
2080+
2081+
auto res = co_await cache_.fetch(
2082+
getFetch({3, 3}, {5, 10}, GroupOrder::NewestFirst),
2083+
firstConsumer,
2084+
upstream_);
2085+
EXPECT_TRUE(res.hasValue());
2086+
2087+
// {5, 20} was outside the original fetch range; cache must reach upstream.
2088+
auto laterConsumer = std::make_shared<NiceMock<MockFetchConsumer>>();
2089+
ON_CALL(*laterConsumer, endOfFetch()).WillByDefault(Return(folly::unit));
2090+
EXPECT_CALL(*upstream_, fetch(_, _))
2091+
.WillOnce([this](Fetch fetch, std::shared_ptr<FetchConsumer> consumer) {
2092+
auto [standalone, joining] = fetchType(fetch);
2093+
EXPECT_EQ(standalone->start, (AbsoluteLocation{5, 20}));
2094+
EXPECT_EQ(standalone->end, (AbsoluteLocation{5, 21}));
2095+
EXPECT_EQ(fetch.groupOrder, GroupOrder::OldestFirst);
2096+
auto res = consumer->endOfFetch();
2097+
EXPECT_FALSE(res.hasError());
2098+
upstreamFetchHandle_ =
2099+
std::make_shared<moxygen::MockFetchHandle>(FetchOk{
2100+
0,
2101+
GroupOrder::OldestFirst,
2102+
false,
2103+
AbsoluteLocation{5, 20},
2104+
});
2105+
return folly::coro::makeTask<Publisher::FetchResult>(
2106+
upstreamFetchHandle_);
2107+
})
2108+
.RetiresOnSaturation();
2109+
2110+
res = co_await cache_.fetch(
2111+
getFetch({5, 20}, {5, 21}, GroupOrder::OldestFirst),
2112+
laterConsumer,
2113+
upstream_);
2114+
EXPECT_TRUE(res.hasValue());
2115+
}
2116+
2117+
// Regression: DESC getGapRanges range #3 used to start at {bottomGroup, 0}
2118+
// instead of clamping to fetchStart.object, shadowing positions below the
2119+
// requested lower bound.
2120+
CO_TEST_F(MoQCacheTest, TestDescendingFetchDoesNotMarkBottomGroupHeadAsGap) {
2121+
auto firstConsumer = std::make_shared<NiceMock<MockFetchConsumer>>();
2122+
ON_CALL(*firstConsumer, object(_, _, _, _, _, _, _))
2123+
.WillByDefault(Return(folly::unit));
2124+
ON_CALL(*firstConsumer, endOfFetch()).WillByDefault(Return(folly::unit));
2125+
EXPECT_CALL(*upstream_, fetch(_, _))
2126+
.WillOnce([this](Fetch fetch, std::shared_ptr<FetchConsumer> consumer) {
2127+
auto [standalone, joining] = fetchType(fetch);
2128+
EXPECT_EQ(standalone->start, (AbsoluteLocation{3, 5}));
2129+
EXPECT_EQ(standalone->end, (AbsoluteLocation{5, 10}));
2130+
EXPECT_EQ(fetch.groupOrder, GroupOrder::NewestFirst);
2131+
auto res =
2132+
consumer->object(3, 0, 5, makeBuf(100), noExtensions(), true);
2133+
EXPECT_FALSE(res.hasError());
2134+
res = consumer->endOfFetch();
2135+
EXPECT_FALSE(res.hasError());
2136+
upstreamFetchHandle_ =
2137+
std::make_shared<moxygen::MockFetchHandle>(FetchOk{
2138+
0,
2139+
GroupOrder::NewestFirst,
2140+
false,
2141+
AbsoluteLocation{3, 5},
2142+
});
2143+
return folly::coro::makeTask<Publisher::FetchResult>(
2144+
upstreamFetchHandle_);
2145+
})
2146+
.RetiresOnSaturation();
2147+
2148+
auto res = co_await cache_.fetch(
2149+
getFetch({3, 5}, {5, 10}, GroupOrder::NewestFirst),
2150+
firstConsumer,
2151+
upstream_);
2152+
EXPECT_TRUE(res.hasValue());
2153+
2154+
// {3, 1} was below fetchStart.object=5; cache must reach upstream.
2155+
auto laterConsumer = std::make_shared<NiceMock<MockFetchConsumer>>();
2156+
ON_CALL(*laterConsumer, endOfFetch()).WillByDefault(Return(folly::unit));
2157+
EXPECT_CALL(*upstream_, fetch(_, _))
2158+
.WillOnce([this](Fetch fetch, std::shared_ptr<FetchConsumer> consumer) {
2159+
auto [standalone, joining] = fetchType(fetch);
2160+
EXPECT_EQ(standalone->start, (AbsoluteLocation{3, 1}));
2161+
EXPECT_EQ(standalone->end, (AbsoluteLocation{3, 2}));
2162+
auto res = consumer->endOfFetch();
2163+
EXPECT_FALSE(res.hasError());
2164+
upstreamFetchHandle_ =
2165+
std::make_shared<moxygen::MockFetchHandle>(FetchOk{
2166+
0,
2167+
GroupOrder::OldestFirst,
2168+
false,
2169+
AbsoluteLocation{3, 1},
2170+
});
2171+
return folly::coro::makeTask<Publisher::FetchResult>(
2172+
upstreamFetchHandle_);
2173+
})
2174+
.RetiresOnSaturation();
2175+
2176+
res = co_await cache_.fetch(
2177+
getFetch({3, 1}, {3, 2}, GroupOrder::OldestFirst),
2178+
laterConsumer,
2179+
upstream_);
2180+
EXPECT_TRUE(res.hasValue());
2181+
}
2182+
2183+
// Regression: DESC finFetch tail-marked against raw end_ (the user's highest
2184+
// endpoint) instead of the iterator's order-aware end. Combined with the
2185+
// off-by-one in cacheImpl, this could shadow the just-cached object.
2186+
CO_TEST_F(MoQCacheTest, TestDescendingFetchDoesNotGapCachedObject) {
2187+
auto firstConsumer = std::make_shared<NiceMock<MockFetchConsumer>>();
2188+
ON_CALL(*firstConsumer, object(_, _, _, _, _, _, _))
2189+
.WillByDefault(Return(folly::unit));
2190+
ON_CALL(*firstConsumer, endOfFetch()).WillByDefault(Return(folly::unit));
2191+
EXPECT_CALL(*upstream_, fetch(_, _))
2192+
.WillOnce([this](Fetch fetch, std::shared_ptr<FetchConsumer> consumer) {
2193+
auto [standalone, joining] = fetchType(fetch);
2194+
EXPECT_EQ(standalone->start, (AbsoluteLocation{3, 3}));
2195+
EXPECT_EQ(standalone->end, (AbsoluteLocation{5, 10}));
2196+
EXPECT_EQ(fetch.groupOrder, GroupOrder::NewestFirst);
2197+
auto res =
2198+
consumer->object(3, 0, 3, makeBuf(100), noExtensions(), true);
2199+
EXPECT_FALSE(res.hasError());
2200+
res = consumer->endOfFetch();
2201+
EXPECT_FALSE(res.hasError());
2202+
upstreamFetchHandle_ =
2203+
std::make_shared<moxygen::MockFetchHandle>(FetchOk{
2204+
0,
2205+
GroupOrder::NewestFirst,
2206+
false,
2207+
AbsoluteLocation{3, 3},
2208+
});
2209+
return folly::coro::makeTask<Publisher::FetchResult>(
2210+
upstreamFetchHandle_);
2211+
})
2212+
.RetiresOnSaturation();
2213+
2214+
auto res = co_await cache_.fetch(
2215+
getFetch({3, 3}, {5, 10}, GroupOrder::NewestFirst),
2216+
firstConsumer,
2217+
upstream_);
2218+
EXPECT_TRUE(res.hasValue());
2219+
2220+
// The cached object at {3, 3} must remain servable from cache.
2221+
auto serveConsumer = std::make_shared<NiceMock<MockFetchConsumer>>();
2222+
ON_CALL(*serveConsumer, endOfFetch()).WillByDefault(Return(folly::unit));
2223+
EXPECT_CALL(*serveConsumer, object(3, 0, 3, _, _, true, _))
2224+
.WillOnce(Return(folly::unit));
2225+
res = co_await cache_.fetch(
2226+
getFetch({3, 3}, {3, 4}, GroupOrder::OldestFirst),
2227+
serveConsumer,
2228+
upstream_);
2229+
EXPECT_TRUE(res.hasValue());
2230+
}
2231+
20502232
TEST_F(MoQCacheTest, TestForwardingPreferenceMismatchIsMalformedTrack) {
20512233
// If an object is cached with one forwarding preference and we try to cache
20522234
// the same object with a different forwarding preference, it should fail

0 commit comments

Comments
 (0)