Skip to content

Commit c50da5e

Browse files
committed
feat(action): Verify no matching cache before save
Prior to saving cache, verify that there is no cache with a matching key already cached. Previously, we checked if a cache key matching our cache was present in the load step; if not, we would then proceed to attempt to save the cache in the save step (assuming we were not in read-only mode and that there were indeed new Docker images to save). This was problematic when the action was run in parallel; in that case, there were multiple, unnecessary cache save attempts when the cache initially missed but was subsequently saved.
1 parent 055e746 commit c50da5e

File tree

4 files changed

+60
-17
lines changed

4 files changed

+60
-17
lines changed

dist/main/index.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/post/index.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/docker.test.ts

+47-15
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ describe("Docker images", (): void => {
8686

8787
const assertSaveDockerImages = (
8888
cacheHit: boolean,
89+
key: string,
8990
readOnly = false,
91+
prevSave = false,
9092
): void => {
9193
expect(core.getInput).nthCalledWith<[string, InputOptions]>(1, "key", {
9294
required: true,
@@ -95,12 +97,20 @@ describe("Docker images", (): void => {
9597
if (!cacheHit) {
9698
expect(core.getInput).lastCalledWith("read-only");
9799
if (!readOnly) {
98-
expect(core.getState).lastCalledWith(docker.DOCKER_IMAGES_LIST);
99-
expect(core.info).nthCalledWith<[string]>(1, "Listing Docker images.");
100-
expect(util.execBashCommand).nthCalledWith<[string]>(
101-
1,
102-
'docker image list --format "{{ .Repository }}:{{ .Tag }}"',
103-
);
100+
expect(cache.restoreCache).lastCalledWith([""], key, [], {
101+
lookupOnly: true,
102+
});
103+
if (!prevSave) {
104+
expect(core.getState).lastCalledWith(docker.DOCKER_IMAGES_LIST);
105+
expect(core.info).nthCalledWith<[string]>(
106+
1,
107+
"Listing Docker images.",
108+
);
109+
expect(util.execBashCommand).nthCalledWith<[string]>(
110+
1,
111+
'docker image list --format "{{ .Repository }}:{{ .Tag }}"',
112+
);
113+
}
104114
}
105115
}
106116
};
@@ -109,6 +119,7 @@ describe("Docker images", (): void => {
109119
key: string,
110120
cacheHit: boolean,
111121
readOnly: boolean,
122+
prevSave: boolean,
112123
preexistingImages: string[],
113124
newImages: string[],
114125
): Promise<void> => {
@@ -117,14 +128,19 @@ describe("Docker images", (): void => {
117128
if (!cacheHit) {
118129
core.getInput.mockReturnValueOnce(readOnly.toString());
119130
if (!readOnly) {
120-
core.getState.mockReturnValueOnce(preexistingImages.join("\n"));
121-
const images = preexistingImages.concat(newImages);
122-
util.execBashCommand.mockResolvedValueOnce(images.join("\n"));
131+
if (prevSave) {
132+
cache.restoreCache.mockResolvedValueOnce(key);
133+
} else {
134+
cache.restoreCache.mockResolvedValueOnce(undefined);
135+
core.getState.mockReturnValueOnce(preexistingImages.join("\n"));
136+
const images = preexistingImages.concat(newImages);
137+
util.execBashCommand.mockResolvedValueOnce(images.join("\n"));
138+
}
123139
}
124140
}
125141
await docker.saveDockerImages();
126142

127-
assertSaveDockerImages(cacheHit, readOnly);
143+
assertSaveDockerImages(cacheHit, key, readOnly, prevSave);
128144
};
129145

130146
const assertCacheNotSaved = (): void => {
@@ -147,6 +163,15 @@ describe("Docker images", (): void => {
147163
assertCacheNotSaved();
148164
};
149165

166+
const assertSavePrevSave = (key: string): void => {
167+
expect(core.info).lastCalledWith(
168+
"A cache miss occurred during the initial attempt to load Docker " +
169+
`images, but subsequently a cache with a matching key, ${key}, was saved. ` +
170+
"This can occur when run in parallel. Not saving cache.",
171+
);
172+
assertCacheNotSaved();
173+
};
174+
150175
const assertNoNewImagesToSave = (): void => {
151176
expect(util.execBashCommand).toHaveBeenCalledTimes(1);
152177
expect(core.info).lastCalledWith("No Docker images to save");
@@ -230,24 +255,28 @@ describe("Docker images", (): void => {
230255
);
231256

232257
testProp(
233-
"are saved unless cache hit, in read-only mode, or new Docker image list is empty",
258+
"are saved unless cache hit, in read-only mode, cache already saved, or " +
259+
"new Docker image list is empty",
234260
[
235261
fullUnicodeString(),
236262
boolean(),
237263
boolean(),
264+
boolean(),
238265
uniquePair(dockerImages(), dockerImages()),
239266
],
240267
async (
241268
key: string,
242269
cacheHit: boolean,
243270
readOnly: boolean,
271+
prevSave: boolean,
244272
[preexistingImages, newImages]: [string[], string[]],
245273
): Promise<void> => {
246274
jest.clearAllMocks();
247275
await mockedSaveDockerImages(
248276
key,
249277
cacheHit,
250278
readOnly,
279+
prevSave,
251280
preexistingImages,
252281
newImages,
253282
);
@@ -256,6 +285,8 @@ describe("Docker images", (): void => {
256285
assertSaveCacheHit(key);
257286
} else if (readOnly) {
258287
assertSaveReadOnly(key);
288+
} else if (prevSave) {
289+
assertSavePrevSave(key);
259290
} else if (newImages.length === 0) {
260291
assertNoNewImagesToSave();
261292
} else {
@@ -264,10 +295,11 @@ describe("Docker images", (): void => {
264295
},
265296
{
266297
examples: [
267-
["my-key", false, false, [["preexisting-image"], ["new-image"]]],
268-
["my-key", false, false, [["preexisting-image"], []]],
269-
["my-key", false, true, [["preexisting-image"], ["new-image"]]],
270-
["my-key", true, false, [["preexisting-image"], ["new-image"]]],
298+
["my-key", false, false, false, [["preexisting-image"], ["new-image"]]],
299+
["my-key", false, false, false, [["preexisting-image"], []]],
300+
["my-key", false, true, false, [["preexisting-image"], ["new-image"]]],
301+
["my-key", true, false, false, [["preexisting-image"], ["new-image"]]],
302+
["my-key", false, false, true, [["preexisting-image"], ["new-image"]]],
271303
],
272304
},
273305
);

src/docker.ts

+11
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ const saveDockerImages = async (): Promise<void> => {
3636
`Cache miss occurred on the primary key ${key}. Not saving cache as ` +
3737
"read-only option was selected.",
3838
);
39+
/* Check if a cache with our key has been saved between when we checked in
40+
* loadDockerImages and now.
41+
*/
42+
} else if (
43+
key === (await restoreCache([""], key, [], { lookupOnly: true }))
44+
) {
45+
info(
46+
"A cache miss occurred during the initial attempt to load Docker " +
47+
`images, but subsequently a cache with a matching key, ${key}, was saved. ` +
48+
"This can occur when run in parallel. Not saving cache.",
49+
);
3950
} else {
4051
const preexistingImages = getState(DOCKER_IMAGES_LIST).split("\n");
4152
info("Listing Docker images.");

0 commit comments

Comments
 (0)