Skip to content

Commit f58042f

Browse files
authored
Merge pull request #2118 from actions/ghadimir/cache_size_restriction
Remove 10GB Cache Size Limit for Cache Service V2
2 parents 227b1ce + 091616a commit f58042f

File tree

4 files changed

+303
-55
lines changed

4 files changed

+303
-55
lines changed

packages/cache/__tests__/saveCache.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ test('save with server error should fail', async () => {
237237
.mockReturnValue(
238238
Promise.resolve({
239239
ok: true,
240-
signedUploadUrl: 'https://blob-storage.local?signed=true'
240+
signedUploadUrl: 'https://blob-storage.local?signed=true',
241+
message: ''
241242
})
242243
)
243244

packages/cache/__tests__/saveCacheV2.test.ts

Lines changed: 254 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -59,47 +59,14 @@ test('save with missing input should fail', async () => {
5959
)
6060
})
6161

62-
test('save with large cache outputs should fail using', async () => {
63-
const paths = 'node_modules'
64-
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
65-
const cachePaths = [path.resolve(paths)]
66-
67-
const createTarMock = jest.spyOn(tar, 'createTar')
68-
const logWarningMock = jest.spyOn(core, 'warning')
69-
70-
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
71-
jest
72-
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
73-
.mockReturnValueOnce(cacheSize)
74-
const compression = CompressionMethod.Gzip
75-
const getCompressionMock = jest
76-
.spyOn(cacheUtils, 'getCompressionMethod')
77-
.mockReturnValueOnce(Promise.resolve(compression))
78-
79-
const cacheId = await saveCache([paths], key)
80-
expect(cacheId).toBe(-1)
81-
expect(logWarningMock).toHaveBeenCalledWith(
82-
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
83-
)
84-
85-
const archiveFolder = '/foo/bar'
86-
87-
expect(createTarMock).toHaveBeenCalledWith(
88-
archiveFolder,
89-
cachePaths,
90-
compression
91-
)
92-
expect(getCompressionMock).toHaveBeenCalledTimes(1)
93-
})
94-
9562
test('create cache entry failure on non-ok response', async () => {
9663
const paths = ['node_modules']
9764
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
9865
const infoLogMock = jest.spyOn(core, 'info')
9966

10067
const createCacheEntryMock = jest
10168
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
102-
.mockResolvedValue({ok: false, signedUploadUrl: ''})
69+
.mockResolvedValue({ok: false, signedUploadUrl: '', message: ''})
10370

10471
const createTarMock = jest.spyOn(tar, 'createTar')
10572
const finalizeCacheEntryMock = jest.spyOn(
@@ -182,7 +149,7 @@ test('save cache fails if a signedUploadURL was not passed', async () => {
182149
const createCacheEntryMock = jest
183150
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
184151
.mockReturnValue(
185-
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
152+
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
186153
)
187154

188155
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -240,7 +207,7 @@ test('finalize save cache failure', async () => {
240207
const createCacheEntryMock = jest
241208
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
242209
.mockReturnValue(
243-
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
210+
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
244211
)
245212

246213
const createTarMock = jest.spyOn(tar, 'createTar')
@@ -260,7 +227,7 @@ test('finalize save cache failure', async () => {
260227

261228
const finalizeCacheEntryMock = jest
262229
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
263-
.mockReturnValue(Promise.resolve({ok: false, entryId: ''}))
230+
.mockReturnValue(Promise.resolve({ok: false, entryId: '', message: ''}))
264231

265232
const cacheId = await saveCache([paths], key, options)
266233

@@ -319,7 +286,7 @@ test('save with valid inputs uploads a cache', async () => {
319286
jest
320287
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
321288
.mockReturnValue(
322-
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL})
289+
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
323290
)
324291

325292
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
@@ -332,7 +299,9 @@ test('save with valid inputs uploads a cache', async () => {
332299

333300
const finalizeCacheEntryMock = jest
334301
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
335-
.mockReturnValue(Promise.resolve({ok: true, entryId: cacheId.toString()}))
302+
.mockReturnValue(
303+
Promise.resolve({ok: true, entryId: cacheId.toString(), message: ''})
304+
)
336305

337306
const expectedCacheId = await saveCache([paths], key)
338307

@@ -360,6 +329,252 @@ test('save with valid inputs uploads a cache', async () => {
360329
expect(expectedCacheId).toBe(cacheId)
361330
})
362331

332+
test('save with extremely large cache should succeed in v2 (no size limit)', async () => {
333+
const paths = 'node_modules'
334+
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
335+
const cachePaths = [path.resolve(paths)]
336+
const signedUploadURL = 'https://blob-storage.local?signed=true'
337+
const createTarMock = jest.spyOn(tar, 'createTar')
338+
// Simulate a very large cache (20GB)
339+
const archiveFileSize = 20 * 1024 * 1024 * 1024 // 20GB
340+
const options: UploadOptions = {
341+
archiveSizeBytes: archiveFileSize,
342+
useAzureSdk: true,
343+
uploadChunkSize: 64 * 1024 * 1024,
344+
uploadConcurrency: 8
345+
}
346+
347+
jest
348+
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
349+
.mockReturnValueOnce(archiveFileSize)
350+
351+
const cacheId = 4
352+
jest
353+
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
354+
.mockReturnValue(
355+
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
356+
)
357+
358+
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
359+
360+
const compression = CompressionMethod.Zstd
361+
const getCompressionMock = jest
362+
.spyOn(cacheUtils, 'getCompressionMethod')
363+
.mockReturnValue(Promise.resolve(compression))
364+
const cacheVersion = cacheUtils.getCacheVersion([paths], compression)
365+
366+
const finalizeCacheEntryMock = jest
367+
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
368+
.mockReturnValue(
369+
Promise.resolve({ok: true, entryId: cacheId.toString(), message: ''})
370+
)
371+
372+
const expectedCacheId = await saveCache([paths], key)
373+
374+
const archiveFolder = '/foo/bar'
375+
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
376+
expect(saveCacheMock).toHaveBeenCalledWith(
377+
-1,
378+
archiveFile,
379+
signedUploadURL,
380+
options
381+
)
382+
expect(createTarMock).toHaveBeenCalledWith(
383+
archiveFolder,
384+
cachePaths,
385+
compression
386+
)
387+
388+
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
389+
key,
390+
version: cacheVersion,
391+
sizeBytes: archiveFileSize.toString()
392+
})
393+
394+
expect(getCompressionMock).toHaveBeenCalledTimes(1)
395+
expect(expectedCacheId).toBe(cacheId)
396+
})
397+
398+
test('save with create cache entry failure and specific error message', async () => {
399+
const paths = ['node_modules']
400+
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
401+
const infoLogMock = jest.spyOn(core, 'info')
402+
const warningLogMock = jest.spyOn(core, 'warning')
403+
const errorMessage = 'Cache storage quota exceeded for repository'
404+
405+
const createCacheEntryMock = jest
406+
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
407+
.mockResolvedValue({ok: false, signedUploadUrl: '', message: errorMessage})
408+
409+
const createTarMock = jest.spyOn(tar, 'createTar')
410+
const compression = CompressionMethod.Zstd
411+
const getCompressionMock = jest
412+
.spyOn(cacheUtils, 'getCompressionMethod')
413+
.mockResolvedValueOnce(compression)
414+
const archiveFileSize = 1024
415+
jest
416+
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
417+
.mockReturnValueOnce(archiveFileSize)
418+
419+
const cacheId = await saveCache(paths, key)
420+
expect(cacheId).toBe(-1)
421+
expect(warningLogMock).toHaveBeenCalledWith(
422+
`Cache reservation failed: ${errorMessage}`
423+
)
424+
expect(infoLogMock).toHaveBeenCalledWith(
425+
`Failed to save: Unable to reserve cache with key ${key}, another job may be creating this cache.`
426+
)
427+
428+
expect(createCacheEntryMock).toHaveBeenCalledWith({
429+
key,
430+
version: cacheUtils.getCacheVersion(paths, compression)
431+
})
432+
expect(createTarMock).toHaveBeenCalledTimes(1)
433+
expect(getCompressionMock).toHaveBeenCalledTimes(1)
434+
})
435+
436+
test('save with finalize cache entry failure and specific error message', async () => {
437+
const paths = 'node_modules'
438+
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
439+
const cachePaths = [path.resolve(paths)]
440+
const logWarningMock = jest.spyOn(core, 'warning')
441+
const signedUploadURL = 'https://blob-storage.local?signed=true'
442+
const archiveFileSize = 1024
443+
const errorMessage =
444+
'Cache entry finalization failed due to concurrent access'
445+
const options: UploadOptions = {
446+
archiveSizeBytes: archiveFileSize,
447+
useAzureSdk: true,
448+
uploadChunkSize: 64 * 1024 * 1024,
449+
uploadConcurrency: 8
450+
}
451+
452+
const createCacheEntryMock = jest
453+
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
454+
.mockReturnValue(
455+
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
456+
)
457+
458+
const createTarMock = jest.spyOn(tar, 'createTar')
459+
const saveCacheMock = jest
460+
.spyOn(cacheHttpClient, 'saveCache')
461+
.mockResolvedValue()
462+
463+
const compression = CompressionMethod.Zstd
464+
const getCompressionMock = jest
465+
.spyOn(cacheUtils, 'getCompressionMethod')
466+
.mockReturnValueOnce(Promise.resolve(compression))
467+
468+
const cacheVersion = cacheUtils.getCacheVersion([paths], compression)
469+
jest
470+
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
471+
.mockReturnValueOnce(archiveFileSize)
472+
473+
const finalizeCacheEntryMock = jest
474+
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
475+
.mockReturnValue(
476+
Promise.resolve({ok: false, entryId: '', message: errorMessage})
477+
)
478+
479+
const cacheId = await saveCache([paths], key, options)
480+
481+
expect(createCacheEntryMock).toHaveBeenCalledWith({
482+
key,
483+
version: cacheVersion
484+
})
485+
486+
const archiveFolder = '/foo/bar'
487+
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
488+
expect(createTarMock).toHaveBeenCalledWith(
489+
archiveFolder,
490+
cachePaths,
491+
compression
492+
)
493+
494+
expect(saveCacheMock).toHaveBeenCalledWith(
495+
-1,
496+
archiveFile,
497+
signedUploadURL,
498+
options
499+
)
500+
expect(getCompressionMock).toHaveBeenCalledTimes(1)
501+
502+
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
503+
key,
504+
version: cacheVersion,
505+
sizeBytes: archiveFileSize.toString()
506+
})
507+
508+
expect(cacheId).toBe(-1)
509+
expect(logWarningMock).toHaveBeenCalledWith(errorMessage)
510+
})
511+
512+
test('save with multiple large caches should succeed in v2 (testing 50GB)', async () => {
513+
const paths = ['large-dataset', 'node_modules', 'build-artifacts']
514+
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
515+
const cachePaths = paths.map(p => path.resolve(p))
516+
const signedUploadURL = 'https://blob-storage.local?signed=true'
517+
const createTarMock = jest.spyOn(tar, 'createTar')
518+
// Simulate an extremely large cache (50GB)
519+
const archiveFileSize = 50 * 1024 * 1024 * 1024 // 50GB
520+
const options: UploadOptions = {
521+
archiveSizeBytes: archiveFileSize,
522+
useAzureSdk: true,
523+
uploadChunkSize: 64 * 1024 * 1024,
524+
uploadConcurrency: 8
525+
}
526+
527+
jest
528+
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
529+
.mockReturnValueOnce(archiveFileSize)
530+
531+
const cacheId = 7
532+
jest
533+
.spyOn(CacheServiceClientJSON.prototype, 'CreateCacheEntry')
534+
.mockReturnValue(
535+
Promise.resolve({ok: true, signedUploadUrl: signedUploadURL, message: ''})
536+
)
537+
538+
const saveCacheMock = jest.spyOn(cacheHttpClient, 'saveCache')
539+
540+
const compression = CompressionMethod.Zstd
541+
const getCompressionMock = jest
542+
.spyOn(cacheUtils, 'getCompressionMethod')
543+
.mockReturnValue(Promise.resolve(compression))
544+
const cacheVersion = cacheUtils.getCacheVersion(paths, compression)
545+
546+
const finalizeCacheEntryMock = jest
547+
.spyOn(CacheServiceClientJSON.prototype, 'FinalizeCacheEntryUpload')
548+
.mockReturnValue(
549+
Promise.resolve({ok: true, entryId: cacheId.toString(), message: ''})
550+
)
551+
552+
const expectedCacheId = await saveCache(paths, key)
553+
554+
const archiveFolder = '/foo/bar'
555+
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
556+
expect(saveCacheMock).toHaveBeenCalledWith(
557+
-1,
558+
archiveFile,
559+
signedUploadURL,
560+
options
561+
)
562+
expect(createTarMock).toHaveBeenCalledWith(
563+
archiveFolder,
564+
cachePaths,
565+
compression
566+
)
567+
568+
expect(finalizeCacheEntryMock).toHaveBeenCalledWith({
569+
key,
570+
version: cacheVersion,
571+
sizeBytes: archiveFileSize.toString()
572+
})
573+
574+
expect(getCompressionMock).toHaveBeenCalledTimes(1)
575+
expect(expectedCacheId).toBe(cacheId)
576+
})
577+
363578
test('save with non existing path should not save cache using v2 saveCache', async () => {
364579
const path = 'node_modules'
365580
const key = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'

0 commit comments

Comments
 (0)