Skip to content

Commit 698df08

Browse files
committed
improvement(knowledge): stop consuming access batches once both overview probes saturate
The source overview loops over the access batches the read-access generator yields. The first batch is free, but every later one costs a connector discovery query plus a per-connector live source proof over the network. Both probes in the loop already stop issuing queries once they saturate — the searchable probe on its first hit, the indexing probe once every configured provider type is accounted for — but the loop kept pulling batches afterwards, paying the producer's cost for no probe at all. Hoist the two probe guards into closures so the loop body and the exit share one definition of each, and break once neither can change the result. Output is unchanged; only the work is dropped.
1 parent d0a9497 commit 698df08

2 files changed

Lines changed: 68 additions & 15 deletions

File tree

‎apps/sim/lib/knowledge/application/search-source-overview.test.ts‎

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,16 @@ const indexingProbeCount = () =>
4545
dbChainMockFns.limit.mock.calls.filter(([rows]) => rows === MAX_SEARCH_SOURCE_PROVIDER_TYPES)
4646
.length - CONFIGURED_PROVIDER_READS
4747

48+
/** Counted at the yield, so batches the use case never asks for stay uncounted. */
4849
function yieldBatches(count: number) {
50+
const consumed = { batches: 0 }
4951
mocks.batches.mockImplementation(async function* () {
50-
for (let index = 0; index < count; index += 1) yield sql`batch-${sql.raw(String(index))}`
52+
for (let index = 0; index < count; index += 1) {
53+
consumed.batches += 1
54+
yield sql`batch-${sql.raw(String(index))}`
55+
}
5156
})
57+
return consumed
5258
}
5359

5460
beforeEach(() => {
@@ -84,15 +90,20 @@ describe('readSearchSourceOverview', () => {
8490
})
8591

8692
it('stops probing for indexing once every configured provider type is known', async () => {
87-
yieldBatches(3)
93+
const consumed = yieldBatches(3)
8894
queueTableRows(member, [{ role: 'owner' }])
8995
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
9096
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
9197

9298
const result = await readSearchSourceOverview.execute({ principal, input })
9399

94-
expect(result.providers).toEqual([{ connectorType: 'gmail', isSyncing: true }])
100+
expect(result).toEqual({
101+
providers: [{ connectorType: 'gmail', isSyncing: true }],
102+
hasSearchableDocuments: false,
103+
})
95104
expect(indexingProbeCount()).toBe(1)
105+
/** The searchable probe is still unsatisfied, so the batches keep being consumed. */
106+
expect(consumed.batches).toBe(3)
96107
})
97108

98109
it('keeps probing every batch while a configured provider type is still unaccounted for', async () => {
@@ -109,4 +120,39 @@ describe('readSearchSourceOverview', () => {
109120
])
110121
expect(indexingProbeCount()).toBe(3)
111122
})
123+
124+
it('stops consuming access batches once neither probe can change the result', async () => {
125+
const consumed = yieldBatches(3)
126+
queueTableRows(member, [{ role: 'owner' }])
127+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
128+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
129+
queueTableRows(document, [{ id: 'doc-1' }])
130+
131+
const result = await readSearchSourceOverview.execute({ principal, input })
132+
133+
expect(result).toEqual({
134+
providers: [{ connectorType: 'gmail', isSyncing: true }],
135+
hasSearchableDocuments: true,
136+
})
137+
expect(consumed.batches).toBe(1)
138+
})
139+
140+
it('keeps consuming access batches for a provider type still unaccounted for', async () => {
141+
const consumed = yieldBatches(3)
142+
queueTableRows(member, [{ role: 'owner' }])
143+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }, { connectorType: 'notion' }])
144+
queueTableRows(knowledgeConnector, [{ connectorType: 'gmail' }])
145+
queueTableRows(document, [{ id: 'doc-1' }])
146+
147+
const result = await readSearchSourceOverview.execute({ principal, input })
148+
149+
expect(result).toEqual({
150+
providers: [
151+
{ connectorType: 'gmail', isSyncing: true },
152+
{ connectorType: 'notion', isSyncing: false },
153+
],
154+
hasSearchableDocuments: true,
155+
})
156+
expect(consumed.batches).toBe(3)
157+
})
112158
})

‎apps/sim/lib/knowledge/application/search-source-overview.ts‎

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,27 @@ export const readSearchSourceOverview = instrumentSourceOverviewUseCase(
107107
const indexingTypes = new Set<string>()
108108
let searchableProbes = 0
109109
let hasSearchableDocuments = false
110+
const probesSources: boolean = availability.memberScoped || availability.sourceMirrored
111+
/** One searchable document is the whole answer, so later batches skip the probe entirely. */
112+
const probesSearchable = (): boolean => probesSources && !hasSearchableDocuments
113+
/**
114+
* A provider type is only read back as membership of `indexingTypes`, so once every
115+
* configured type is in the set no later batch can change the answer.
116+
*/
117+
const probesIndexing = (): boolean =>
118+
probesSources && providers.some(({ connectorType }) => !indexingTypes.has(connectorType))
110119
for await (const accessCondition of knowledgeReadAccessBatches(access, [
111120
configured,
112121
available,
113122
documentConditions,
114123
])) {
115124
const readableDocument = and(documentConditions, accessCondition)
116-
const probesSources: boolean = availability.memberScoped || availability.sourceMirrored
117-
/** One searchable document is the whole answer, so later batches skip the probe entirely. */
118-
const probesSearchable: boolean = probesSources && !hasSearchableDocuments
119-
if (probesSearchable) searchableProbes += 1
120-
/**
121-
* A provider type is only read back as membership of `indexingTypes`, so once every
122-
* configured type is in the set no later batch can change the answer.
123-
*/
124-
const probesIndexing: boolean =
125-
probesSources && providers.some(({ connectorType }) => !indexingTypes.has(connectorType))
125+
const probesSearchableNow = probesSearchable()
126+
if (probesSearchableNow) searchableProbes += 1
126127
/** Annotated so the searchable probe's guard does not infer through its own result. */
127128
const [indexing, searchable]: [{ connectorType: string }[], { id: string }[]] =
128129
await Promise.all([
129-
probesIndexing
130+
probesIndexing()
130131
? measureSearchStage('source_overview.indexing', () =>
131132
configuredProvidersQuery()
132133
.where(
@@ -164,7 +165,7 @@ export const readSearchSourceOverview = instrumentSourceOverviewUseCase(
164165
.limit(MAX_SEARCH_SOURCE_PROVIDER_TYPES)
165166
)
166167
: [],
167-
probesSearchable
168+
probesSearchableNow
168169
? measureSearchStage('source_overview.searchable', () =>
169170
db
170171
.select({ id: document.id })
@@ -199,6 +200,12 @@ export const readSearchSourceOverview = instrumentSourceOverviewUseCase(
199200
])
200201
for (const provider of indexing) indexingTypes.add(provider.connectorType)
201202
hasSearchableDocuments ||= searchable.length > 0
203+
/**
204+
* Both probes are saturated, so every remaining batch would be discovered and live-proved
205+
* for no probe. `accessBatchCount` and `liveProofConnectorCount` stay what they document:
206+
* the batches and proofs this read actually spent, not the batches the owner could produce.
207+
*/
208+
if (!probesSearchable() && !probesIndexing()) break
202209
}
203210
annotateSearchDiagnostics({ searchableProbeCount: searchableProbes })
204211
return {

0 commit comments

Comments
 (0)