Skip to content

Commit 85b0538

Browse files
authored
feat(cli): allow setting --max-fetch-concurrency to prevent stalled validators (#7450)
1 parent 8bc721b commit 85b0538

File tree

5 files changed

+58
-11
lines changed

5 files changed

+58
-11
lines changed

packages/sanity/src/_internal/cli/actions/validation/validateAction.ts

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface ValidateFlags {
1818
'file'?: string
1919
'level'?: 'error' | 'warning' | 'info'
2020
'max-custom-validation-concurrency'?: number
21+
'max-fetch-concurrency'?: number
2122
'yes'?: boolean
2223
'y'?: boolean
2324
}
@@ -103,6 +104,15 @@ export default async function validateAction(
103104
throw new Error(`'--max-custom-validation-concurrency' must be an integer.`)
104105
}
105106

107+
const maxFetchConcurrency = flags['max-fetch-concurrency']
108+
if (
109+
maxFetchConcurrency &&
110+
typeof maxFetchConcurrency !== 'number' &&
111+
!Number.isInteger(maxFetchConcurrency)
112+
) {
113+
throw new Error(`'--max-fetch-concurrency' must be an integer.`)
114+
}
115+
106116
const clientConfig: Partial<ClientConfig> = {
107117
...apiClient({
108118
requireUser: true,
@@ -140,6 +150,7 @@ export default async function validateAction(
140150
workDir,
141151
level,
142152
maxCustomValidationConcurrency,
153+
maxFetchConcurrency,
143154
ndjsonFilePath,
144155
reporter: (worker) => {
145156
const reporter =

packages/sanity/src/_internal/cli/actions/validation/validateDocuments.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import {
1111
} from '../../threads/validateDocuments'
1212
import {createReceiver, type WorkerChannelReceiver} from '../../util/workerChannels'
1313

14-
const DEFAULT_MAX_CUSTOM_VALIDATION_CONCURRENCY = 5
15-
1614
export interface ValidateDocumentsOptions<TReturn = unknown> {
1715
level?: 'error' | 'warning' | 'info'
1816
workspace?: string
@@ -23,6 +21,7 @@ export interface ValidateDocumentsOptions<TReturn = unknown> {
2321
dataset?: string // override
2422
ndjsonFilePath?: string
2523
maxCustomValidationConcurrency?: number
24+
maxFetchConcurrency?: number
2625
reporter?: (worker: WorkerChannelReceiver<ValidationWorkerChannel>) => TReturn
2726
}
2827

@@ -72,6 +71,7 @@ export function validateDocuments(options: ValidateDocumentsOptions): unknown {
7271
reporter = defaultReporter,
7372
level,
7473
maxCustomValidationConcurrency,
74+
maxFetchConcurrency,
7575
ndjsonFilePath,
7676
} = options
7777

@@ -100,8 +100,8 @@ export function validateDocuments(options: ValidateDocumentsOptions): unknown {
100100
projectId,
101101
level,
102102
ndjsonFilePath,
103-
maxCustomValidationConcurrency:
104-
maxCustomValidationConcurrency ?? DEFAULT_MAX_CUSTOM_VALIDATION_CONCURRENCY,
103+
maxCustomValidationConcurrency,
104+
maxFetchConcurrency,
105105
} satisfies ValidateDocumentsWorkerData,
106106
// eslint-disable-next-line no-process-env
107107
env: process.env,

packages/sanity/src/_internal/cli/commands/documents/validateDocumentsCommand.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Options
1111
--format <pretty|ndjson|json> The output format used to print the found validation markers and report progress.
1212
--level <error|warning|info> The minimum level reported out. Defaults to warning.
1313
--max-custom-validation-concurrency <number> Specify how many custom validators can run concurrently. Defaults to 5.
14+
--max-fetch-concurrency <number> Specify how many \`client.fetch\` requests are allow concurrency at once. Defaults to 25.
1415
1516
Examples
1617
# Validates all documents in a Sanity project with more than one workspace
@@ -20,7 +21,7 @@ Examples
2021
sanity documents validate --workspace default --dataset staging
2122
2223
# Save the results of the report into a file
23-
sanity documents validate > report.txt
24+
sanity documents validate --yes > report.txt
2425
2526
# Report out info level validation markers too
2627
sanity documents validate --level info

packages/sanity/src/_internal/cli/threads/validateDocuments.ts

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface ValidateDocumentsWorkerData {
4343
ndjsonFilePath?: string
4444
level?: ValidationMarker['level']
4545
maxCustomValidationConcurrency?: number
46+
maxFetchConcurrency?: number
4647
}
4748

4849
/** @internal */
@@ -79,6 +80,7 @@ const {
7980
projectId,
8081
level,
8182
maxCustomValidationConcurrency,
83+
maxFetchConcurrency,
8284
} = _workerData as ValidateDocumentsWorkerData
8385

8486
if (isMainThread || !parentPort) {
@@ -359,6 +361,7 @@ async function validateDocuments() {
359361
getDocumentExists,
360362
environment: 'cli',
361363
maxCustomValidationConcurrency,
364+
maxFetchConcurrency,
362365
}),
363366
new Promise<typeof timeout>((resolve) =>
364367
setTimeout(() => resolve(timeout), DOCUMENT_VALIDATION_TIMEOUT),

packages/sanity/src/core/validation/validateDocument.ts

+38-6
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,23 @@ import {typeString} from './util/typeString'
2424
import {unknownFieldsValidator} from './validators/unknownFieldsValidator'
2525

2626
// this is the number of requests allowed inflight at once. this is done to prevent
27-
// the validation library from overwhelming our backend
28-
const MAX_FETCH_CONCURRENCY = 10
29-
30-
const limitConcurrency = createClientConcurrencyLimiter(MAX_FETCH_CONCURRENCY)
27+
// the validation library from overwhelming our backend.
28+
// NOTE: this was upped from 10 to prevent issues where many concurrency
29+
// `client.fetch` requests would "clog" custom validators from finishing due to
30+
// not enough concurrent requests being fulfilled
31+
//
32+
// NOTE: ensure to update the TSDoc and CLI help test if this is changed
33+
const DEFAULT_MAX_FETCH_CONCURRENCY = 25
34+
35+
// NOTE: ensure to update the TSDoc and CLI help test if this is changed
36+
const DEFAULT_MAX_CUSTOM_VALIDATION_CONCURRENCY = 5
37+
38+
let _limitConcurrency: ReturnType<typeof createClientConcurrencyLimiter> | undefined
39+
const getConcurrencyLimiter = (maxConcurrency: number) => {
40+
if (_limitConcurrency) return _limitConcurrency
41+
_limitConcurrency = createClientConcurrencyLimiter(maxConcurrency)
42+
return _limitConcurrency
43+
}
3144

3245
const isRecord = (maybeRecord: unknown): maybeRecord is Record<string, unknown> =>
3346
typeof maybeRecord === 'object' && maybeRecord !== null && !Array.isArray(maybeRecord)
@@ -104,8 +117,21 @@ export interface ValidateDocumentOptions {
104117
* concurrently at once. This helps prevent custom validators from
105118
* overwhelming backend services (e.g. called via fetch) used in async,
106119
* user-defined validation functions. (i.e. `rule.custom(async() => {})`)
120+
*
121+
* Note that lowering this number may also help in cases where a custom
122+
* validator could potentially exhaust the fetch concurrency. This is 5 by
123+
* default.
107124
*/
108125
maxCustomValidationConcurrency?: number
126+
127+
/**
128+
* The amount of allowed inflight fetch requests at once. You may need to up
129+
* this value if you have complex custom validations that require many
130+
* `client.fetch` requests at once. It's possible for custom validator to
131+
* stall if there are not enough concurrent fetch requests available to
132+
* fullfil the custom validation. This is 25 by default.
133+
*/
134+
maxFetchConcurrency?: number
109135
}
110136

111137
/**
@@ -118,9 +144,13 @@ export function validateDocument({
118144
document,
119145
workspace,
120146
environment = 'studio',
147+
maxFetchConcurrency,
121148
...options
122149
}: ValidateDocumentOptions): Promise<ValidationMarker[]> {
123150
const getClient = options.getClient || workspace.getClient
151+
const limitConcurrency = getConcurrencyLimiter(
152+
maxFetchConcurrency ?? DEFAULT_MAX_FETCH_CONCURRENCY,
153+
)
124154
const getConcurrencyLimitedClient = (clientOptions: SourceClientOptions) =>
125155
limitConcurrency(getClient(clientOptions))
126156

@@ -190,8 +220,10 @@ export function validateDocumentObservable({
190220
}
191221

192222
let customValidationConcurrencyLimiter = customValidationConcurrencyLimiters.get(schema)
193-
if (!customValidationConcurrencyLimiter && maxCustomValidationConcurrency) {
194-
customValidationConcurrencyLimiter = new ConcurrencyLimiter(maxCustomValidationConcurrency)
223+
if (!customValidationConcurrencyLimiter) {
224+
customValidationConcurrencyLimiter = new ConcurrencyLimiter(
225+
maxCustomValidationConcurrency ?? DEFAULT_MAX_CUSTOM_VALIDATION_CONCURRENCY,
226+
)
195227
customValidationConcurrencyLimiters.set(schema, customValidationConcurrencyLimiter)
196228
}
197229

0 commit comments

Comments
 (0)