@@ -35,6 +35,11 @@ import { SubjectId } from "../auth/subject-id";
3535const defaultRevLimit = 20 ;
3636// It should keep it aligned with client_max_body_size for /code-sync location.
3737const defaultContentLimit = "1Mb" ;
38+ // Max server-side retries when a rev mismatch (412) occurs during insert.
39+ // Absorbs concurrency conflicts from multiple workspaces writing the same resource,
40+ // preventing the VS Code client from entering an unbounded retry loop that exhausts
41+ // its 100-request/5-min budget and triggers "Settings sync is suspended" banners.
42+ const maxInsertRetries = 3 ;
3843export type CodeSyncConfig = Partial < {
3944 revLimit : number ;
4045 contentLimit : number ;
@@ -330,46 +335,55 @@ export class CodeSyncService {
330335 const isEditSessionsResource = resourceKey === "editSessions" ;
331336 const userId = req . user ! . id ;
332337 const contentType = req . headers [ "content-type" ] || "*/*" ;
333- const newRev = await this . db . insert (
334- userId ,
335- resourceKey ,
336- collection ,
337- latestRev ,
338- async ( rev , oldRevs ) => {
339- const request = new UploadUrlRequest ( ) ;
340- request . setOwnerId ( userId ) ;
341- request . setName ( toObjectName ( resourceKey , rev , collection ) ) ;
342- request . setContentType ( contentType ) ;
343- const blobsClient = this . blobsProvider . getDefault ( ) ;
344- const urlResponse = await util . promisify < UploadUrlRequest , UploadUrlResponse > (
345- blobsClient . uploadUrl . bind ( blobsClient ) ,
346- ) ( request ) ;
347- const url = urlResponse . getUrl ( ) ;
348- const content = req . body as string ;
349- const response = await fetch ( url , {
350- timeout : 10000 ,
351- method : "PUT" ,
352- body : content ,
353- headers : {
354- "content-length" : req . headers [ "content-length" ] || String ( content . length ) ,
355- "content-type" : contentType ,
356- } ,
357- } ) ;
358- if ( response . status !== 200 ) {
359- throw new Error (
360- `code sync: blob service: upload failed with ${ response . status } ${ response . statusText } ` ,
361- ) ;
362- }
363338
364- if ( oldRevs . length ) {
365- // Asynchonously delete old revs from storage
366- Promise . allSettled (
367- oldRevs . map ( ( rev ) => this . doDeleteResource ( userId , resourceKey , rev , collection ) ) ,
368- ) . catch ( ( ) => { } ) ;
369- }
370- } ,
371- { revLimit, overwrite : ! isEditSessionsResource } ,
372- ) ;
339+ const doInsert = async ( rev : string , oldRevs : string [ ] ) => {
340+ const request = new UploadUrlRequest ( ) ;
341+ request . setOwnerId ( userId ) ;
342+ request . setName ( toObjectName ( resourceKey , rev , collection ) ) ;
343+ request . setContentType ( contentType ) ;
344+ const blobsClient = this . blobsProvider . getDefault ( ) ;
345+ const urlResponse = await util . promisify < UploadUrlRequest , UploadUrlResponse > (
346+ blobsClient . uploadUrl . bind ( blobsClient ) ,
347+ ) ( request ) ;
348+ const url = urlResponse . getUrl ( ) ;
349+ const content = req . body as string ;
350+ const response = await fetch ( url , {
351+ timeout : 10000 ,
352+ method : "PUT" ,
353+ body : content ,
354+ headers : {
355+ "content-length" : req . headers [ "content-length" ] || String ( content . length ) ,
356+ "content-type" : contentType ,
357+ } ,
358+ } ) ;
359+ if ( response . status !== 200 ) {
360+ throw new Error (
361+ `code sync: blob service: upload failed with ${ response . status } ${ response . statusText } ` ,
362+ ) ;
363+ }
364+
365+ if ( oldRevs . length ) {
366+ // Asynchronously delete old revs from storage
367+ Promise . allSettled (
368+ oldRevs . map ( ( rev ) => this . doDeleteResource ( userId , resourceKey , rev , collection ) ) ,
369+ ) . catch ( ( ) => { } ) ;
370+ }
371+ } ;
372+
373+ const insertOptions = { revLimit, overwrite : ! isEditSessionsResource } ;
374+
375+ // Try the insert with the client-provided rev first.
376+ // On rev mismatch, retry with the server's current latest rev. This absorbs
377+ // concurrency conflicts (e.g. multiple workspaces syncing globalState) server-side,
378+ // preventing the VS Code client from entering an unbounded retry loop that
379+ // exhausts its request budget and triggers "Settings sync is suspended" banners.
380+ let newRev = await this . db . insert ( userId , resourceKey , collection , latestRev , doInsert , insertOptions ) ;
381+ if ( ! newRev && ! isEditSessionsResource && latestRev ) {
382+ for ( let attempt = 0 ; attempt < maxInsertRetries && ! newRev ; attempt ++ ) {
383+ const currentLatest = await this . db . getLatestRevision ( userId , resourceKey , collection ) ;
384+ newRev = await this . db . insert ( userId , resourceKey , collection , currentLatest , doInsert , insertOptions ) ;
385+ }
386+ }
373387
374388 if ( ! newRev ) {
375389 res . sendStatus ( isEditSessionsResource ? 400 : 412 ) ;
0 commit comments