mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:07:37 +00:00
fix(redis): let fuzzy search cover keyspace aware scan budget
This commit is contained in:
@@ -1638,7 +1638,7 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
expect(mocks.toast).toHaveBeenCalledWith("deleted 3001", 3000);
|
||||
});
|
||||
|
||||
it("preserves the cursor and finds a sparse fuzzy match after a bounded continuation", async () => {
|
||||
it("finds a sparse fuzzy match within the keyspace-aware budget without manual load-more (#12412)", async () => {
|
||||
mocks.redisScanPageSize = 1_000;
|
||||
mountBrowser();
|
||||
await settle();
|
||||
@@ -1654,15 +1654,17 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
return Promise.resolve({ cursor: batch * 100, keys: [], total_keys: batch === 1 ? 200_000 : 0 });
|
||||
});
|
||||
|
||||
// DBSIZE 200k → 200 SCAN iterations of keyspace-aware budget: the
|
||||
// automatic first page keeps scanning (8 backend calls) until the sparse
|
||||
// match at iteration 64 instead of stopping after the old 50-iteration
|
||||
// shared-budget slice (7 calls), so the match surfaces without a manual
|
||||
// load-more click (#12412).
|
||||
await submitKeySearch("issue5012:target");
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(7));
|
||||
await settle();
|
||||
expect(document.body.textContent).not.toContain("target");
|
||||
expect(document.body.textContent).toContain("redis.noKeysInScanHint");
|
||||
|
||||
clickButtonWithText("redis.loadMoreKeys");
|
||||
await vi.waitFor(() => expect(groupRowOrUndefined("issue5012")).toBeDefined());
|
||||
expect(Array.from(document.querySelectorAll<HTMLElement>(".dbx-editor-font-family")).map((element) => element.textContent)).not.toContain("target");
|
||||
await settle();
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(8);
|
||||
expect(document.body.textContent).not.toContain("target");
|
||||
|
||||
groupRow("issue5012").dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
|
||||
await settle();
|
||||
await vi.waitFor(() => {
|
||||
@@ -1670,7 +1672,14 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
expect(labels).toContain("target");
|
||||
});
|
||||
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(8);
|
||||
// The cursor handed back with the matching page is preserved: the manual
|
||||
// continuation resumes from cursor 801 and scans its own keyspace-aware
|
||||
// page (200 iterations = 25 backend calls) past the end of the mock.
|
||||
clickButtonWithText("redis.loadMoreKeys");
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(33));
|
||||
await settle();
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(33);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls[8]?.[2]).toBe(801);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls.every((call) => call[3] === "*issue5012:target*")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1688,20 +1697,27 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
return Promise.resolve({ cursor: batch, keys: [], total_keys: batch === 1 ? 5_000_000 : 0 });
|
||||
});
|
||||
|
||||
// DBSIZE 5M → keyspace-aware budget = 1M COUNT = 1000 SCAN iterations, so
|
||||
// the automatic first page spends up to 1000 iterations (125 backend calls
|
||||
// at 8 iterations each) instead of the old shared 50-iteration slice (7
|
||||
// calls), then stops with the cursor saved (#12412).
|
||||
await submitKeySearch("missing-key");
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(7));
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(125));
|
||||
await settle();
|
||||
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(7);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls[mocks.redisScanKeysBatch.mock.calls.length - 1]?.[2]).toBe(6);
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(125);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls[124]?.[2]).toBe(124);
|
||||
expect(document.body.textContent).toContain("redis.noKeysInScanHint");
|
||||
expect(Array.from(document.querySelectorAll("button")).some((button) => button.textContent?.includes("redis.loadMoreKeys"))).toBe(true);
|
||||
|
||||
// The manual continuation resumes from the saved cursor with the same
|
||||
// keyspace-aware per-page budget (another 1000 iterations) instead of
|
||||
// falling back to the old 50k COUNT floor.
|
||||
clickButtonWithText("redis.loadMoreKeys");
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(14));
|
||||
await vi.waitFor(() => expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(250));
|
||||
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(14);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls[7]?.[2]).toBe(7);
|
||||
expect(mocks.redisScanKeysBatch).toHaveBeenCalledTimes(250);
|
||||
expect(mocks.redisScanKeysBatch.mock.calls[125]?.[2]).toBe(125);
|
||||
expect(document.body.textContent).toContain("redis.noKeysInScanHint");
|
||||
});
|
||||
|
||||
@@ -1731,11 +1747,10 @@ describe("RedisKeyBrowser fuzzy key hierarchy", () => {
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain("first"));
|
||||
expect(requiredElement<HTMLElement>(".redis-key-count").textContent).toBe("1+ keys");
|
||||
|
||||
clickButtonWithText("redis.loadMoreKeys");
|
||||
await vi.waitFor(() => expect(continuationBatch).toBe(7));
|
||||
expect(document.body.textContent).toContain("first");
|
||||
expect(document.body.textContent).not.toContain("next");
|
||||
|
||||
// One continuation click now scans the keyspace-aware budget (200k keys →
|
||||
// 200 iterations) and reaches the next sparse match while keeping the
|
||||
// already found key; previously the same click was cut off at the old
|
||||
// 50-iteration slice just short of the match and needed a second click (#12412).
|
||||
clickButtonWithText("redis.loadMoreKeys");
|
||||
await vi.waitFor(() => expect(document.body.textContent).toContain("next"));
|
||||
|
||||
|
||||
@@ -246,6 +246,10 @@ let loadMoreOperationId = 0;
|
||||
// backend calls, without changing its return type.
|
||||
interface ScanIterationBudget {
|
||||
remaining: number;
|
||||
// Current chain allowance. A fuzzy key search raises both fields together
|
||||
// once DBSIZE is known (see `fetchScanPage`), so later pages observe an
|
||||
// already-expanded budget instead of re-inflating it from scratch (#12412).
|
||||
total: number;
|
||||
}
|
||||
// Automatic continuation (see `maybeAutoLoadMoreRedisKeys`) has no natural stop
|
||||
// condition when a search is sparse: unique visible keys barely grow, so the
|
||||
@@ -262,9 +266,12 @@ interface ScanIterationBudget {
|
||||
// same retained operation. An explicit "Load more" click or a real scroll
|
||||
// event is a single user-triggered request and keeps its own uncapped
|
||||
// per-call budget (see `fetchScanPage`); only the automatic follow-up check
|
||||
// they hand off to afterward is constrained by this shared budget.
|
||||
// they hand off to afterward is constrained by this shared budget. A fuzzy
|
||||
// key search raises this allowance to its keyspace-aware budget once DBSIZE
|
||||
// is observed (see `fetchScanPage`); ordinary loads keep the conservative
|
||||
// default below.
|
||||
const AUTO_LOAD_TOTAL_SCAN_ITERATIONS = 50;
|
||||
let autoLoadBudget: ScanIterationBudget = { remaining: AUTO_LOAD_TOTAL_SCAN_ITERATIONS };
|
||||
let autoLoadBudget: ScanIterationBudget = { remaining: AUTO_LOAD_TOTAL_SCAN_ITERATIONS, total: AUTO_LOAD_TOTAL_SCAN_ITERATIONS };
|
||||
let redisBrowserIsActive = true;
|
||||
let reloadKeysOnActivation = false;
|
||||
let redisDbFlushedListenerRegistered = false;
|
||||
@@ -1002,7 +1009,7 @@ function invalidateScanRequests(resetAutoLoadBudget = true): number {
|
||||
loadMoreOperationId++;
|
||||
loadingMore.value = false;
|
||||
subtreeFillOperations.clear();
|
||||
if (resetAutoLoadBudget) autoLoadBudget = { remaining: AUTO_LOAD_TOTAL_SCAN_ITERATIONS };
|
||||
if (resetAutoLoadBudget) autoLoadBudget = { remaining: AUTO_LOAD_TOTAL_SCAN_ITERATIONS, total: AUTO_LOAD_TOTAL_SCAN_ITERATIONS };
|
||||
return searchRequestId;
|
||||
}
|
||||
|
||||
@@ -1027,7 +1034,11 @@ async function fetchScanPage(requestId = searchRequestId, operationId?: number,
|
||||
// preserve the returned cursor, and let the existing "Load more" action
|
||||
// continue sparse searches without turning one request into a full scan.
|
||||
const isFuzzyKeySearch = searchMode.value === "key" && fuzzyKeySearch.value;
|
||||
let scanCountBudget = redisFuzzySearchScanBudget(0);
|
||||
// DBSIZE only travels with the cursor==0 batch; carry the last observed
|
||||
// keyspace across continuation pages so their COUNT budget stays
|
||||
// keyspace-aware instead of collapsing back to the 50k floor (#12412).
|
||||
let totalKeys = isFuzzyKeySearch ? lastTotalKeys.value : 0;
|
||||
let scanCountBudget = redisFuzzySearchScanBudget(totalKeys);
|
||||
const iterationsPerCall = 8;
|
||||
const perCallMaxIterations = Math.max(1, Math.ceil(scanCountBudget / Math.max(1, pageSize)));
|
||||
// When part of the automatic-fill chain, also cap this call to whatever is
|
||||
@@ -1037,7 +1048,6 @@ async function fetchScanPage(requestId = searchRequestId, operationId?: number,
|
||||
let maxIterations = iterationBudget ? Math.max(0, Math.min(perCallMaxIterations, iterationBudget.remaining)) : perCallMaxIterations;
|
||||
let completedIterations = 0;
|
||||
let cursor = scanCursor.value;
|
||||
let totalKeys = 0;
|
||||
|
||||
while (completedIterations < maxIterations) {
|
||||
if (!isCurrentScanOperation(requestId, operationId)) break;
|
||||
@@ -1049,6 +1059,18 @@ async function fetchScanPage(requestId = searchRequestId, operationId?: number,
|
||||
if (isFuzzyKeySearch && totalKeys > 0) {
|
||||
scanCountBudget = redisFuzzySearchScanBudget(totalKeys);
|
||||
const fuzzyMaxIterations = Math.ceil(scanCountBudget / Math.max(1, pageSize));
|
||||
if (iterationBudget) {
|
||||
// The shared automatic-fill budget starts at the conservative
|
||||
// AUTO_LOAD_TOTAL_SCAN_ITERATIONS default, which suits ordinary loads
|
||||
// but clamps a fuzzy search to a handful of SCAN pages no matter how
|
||||
// large the keyspace is (#12412). Raise the chain allowance to the
|
||||
// keyspace-aware total (never shrink it) so the `availableIterations`
|
||||
// below can hand the grown budget to this page; iterations already
|
||||
// spent still count against the raised total.
|
||||
const expandedTotal = Math.max(iterationBudget.total, fuzzyMaxIterations);
|
||||
iterationBudget.remaining += expandedTotal - iterationBudget.total;
|
||||
iterationBudget.total = expandedTotal;
|
||||
}
|
||||
const availableIterations = iterationBudget ? completedIterations + iterationBudget.remaining : fuzzyMaxIterations;
|
||||
maxIterations = Math.min(fuzzyMaxIterations, Math.max(maxIterations, availableIterations));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user