Skip to content

Commit 370501b

Browse files
authored
Has Cache use last modified time as metadata. (#1031)
1 parent 5b531fc commit 370501b

File tree

4 files changed

+277
-24
lines changed

4 files changed

+277
-24
lines changed

jib-core/src/main/java/com/google/cloud/tools/jib/ncache/Cache.java

+34-9
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@
2323
import java.io.IOException;
2424
import java.nio.file.Files;
2525
import java.nio.file.Path;
26+
import java.nio.file.attribute.FileTime;
2627
import java.util.Optional;
2728
import javax.annotation.concurrent.Immutable;
2829

2930
/**
3031
* Cache for storing data to be shared between Jib executions.
3132
*
32-
* <p>Uses the default cache storage engine ({@link DefaultCacheStorage}) and layer entries as the
33-
* selector ({@link LayerEntriesSelector}).
33+
* <p>Uses the default cache storage engine ({@link DefaultCacheStorage}), layer entries as the
34+
* selector ({@link LayerEntriesSelector}), and last modified time as the metadata.
3435
*
3536
* <p>This class is immutable and safe to use across threads.
3637
*/
@@ -56,8 +57,8 @@ private Cache(CacheStorage cacheStorage) {
5657
}
5758

5859
/**
59-
* Saves a cache entry with only a layer {@link Blob}. Use {@link #write(Blob, ImmutableList,
60-
* Blob)} to include a selector and metadata.
60+
* Saves a cache entry with only a layer {@link Blob}. Use {@link #write(Blob, ImmutableList)} to
61+
* include a selector and metadata.
6162
*
6263
* @param layerBlob the layer {@link Blob}
6364
* @return the {@link CacheEntry} for the written layer
@@ -73,19 +74,22 @@ public CacheEntry write(Blob layerBlob) throws IOException {
7374
*
7475
* @param layerBlob the layer {@link Blob}
7576
* @param layerEntries the layer entries that make up the layer
76-
* @param metadataBlob the metadata {@link Blob}
7777
* @return the {@link CacheEntry} for the written layer and metadata
7878
* @throws IOException if an I/O exception occurs
7979
*/
80-
public CacheEntry write(Blob layerBlob, ImmutableList<LayerEntry> layerEntries, Blob metadataBlob)
80+
public CacheEntry write(Blob layerBlob, ImmutableList<LayerEntry> layerEntries)
8181
throws IOException {
8282
return cacheStorage.write(
8383
CacheWrite.withSelectorAndMetadata(
84-
layerBlob, LayerEntriesSelector.generateSelector(layerEntries), metadataBlob));
84+
layerBlob,
85+
LayerEntriesSelector.generateSelector(layerEntries),
86+
LastModifiedTimeMetadata.generateMetadata(layerEntries)));
8587
}
8688

8789
/**
88-
* Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}.
90+
* Retrieves the {@link CacheEntry} that was built from the {@code layerEntries}. The last
91+
* modified time of the {@code layerEntries} must match the last modified time as stored by the
92+
* metadata of the {@link CacheEntry}.
8993
*
9094
* @param layerEntries the layer entries to match against
9195
* @return a {@link CacheEntry} that was built from {@code layerEntries}, if found
@@ -100,7 +104,28 @@ public Optional<CacheEntry> retrieve(ImmutableList<LayerEntry> layerEntries)
100104
return Optional.empty();
101105
}
102106

103-
return cacheStorage.retrieve(optionalSelectedLayerDigest.get());
107+
Optional<CacheEntry> optionalCacheEntry =
108+
cacheStorage.retrieve(optionalSelectedLayerDigest.get());
109+
if (!optionalCacheEntry.isPresent()) {
110+
return Optional.empty();
111+
}
112+
113+
CacheEntry cacheEntry = optionalCacheEntry.get();
114+
115+
Optional<FileTime> optionalRetrievedLastModifiedTime =
116+
LastModifiedTimeMetadata.getLastModifiedTime(cacheEntry);
117+
if (!optionalRetrievedLastModifiedTime.isPresent()) {
118+
return Optional.empty();
119+
}
120+
121+
FileTime retrievedLastModifiedTime = optionalRetrievedLastModifiedTime.get();
122+
FileTime expectedLastModifiedTime = LastModifiedTimeMetadata.getLastModifiedTime(layerEntries);
123+
124+
if (!expectedLastModifiedTime.equals(retrievedLastModifiedTime)) {
125+
return Optional.empty();
126+
}
127+
128+
return Optional.of(cacheEntry);
104129
}
105130

106131
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2018 Google LLC.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.google.cloud.tools.jib.ncache;
18+
19+
import com.google.cloud.tools.jib.blob.Blob;
20+
import com.google.cloud.tools.jib.blob.Blobs;
21+
import com.google.cloud.tools.jib.image.LayerEntry;
22+
import com.google.common.collect.ImmutableList;
23+
import java.io.IOException;
24+
import java.nio.file.Files;
25+
import java.nio.file.attribute.FileTime;
26+
import java.time.Instant;
27+
import java.util.Optional;
28+
29+
/**
30+
* Serializes/deserializes metadata storing the latest last modified time of all the source files in
31+
* {@link LayerEntry}s for a layer.
32+
*
33+
* <p>Use {@link #generateMetadata} to serialize the latest last modified time of all the source
34+
* files in {@link LayerEntry}s for a layer into a {@link Blob} containing the serialized last
35+
* modified time. Use {@link #getLastModifiedTime(CacheEntry)} to deserialize the metadata in a
36+
* {@link CacheEntry} into a last modified time.
37+
*/
38+
class LastModifiedTimeMetadata {
39+
40+
/**
41+
* Generates the metadata {@link Blob} for the list of {@link LayerEntry}s. The metadata is the
42+
* latest last modified time of all the source files in the list of {@link LayerEntry}s serialized
43+
* using {@link Instant#toString}.
44+
*
45+
* @param layerEntries the list of {@link LayerEntry}s
46+
* @return the generated metadata
47+
*/
48+
static Blob generateMetadata(ImmutableList<LayerEntry> layerEntries) throws IOException {
49+
return Blobs.from(getLastModifiedTime(layerEntries).toInstant().toString());
50+
}
51+
52+
/**
53+
* Gets the latest last modified time of all the source files in the list of {@link LayerEntry}s.
54+
*
55+
* @param layerEntries the list of {@link LayerEntry}s
56+
* @return the last modified time
57+
*/
58+
static FileTime getLastModifiedTime(ImmutableList<LayerEntry> layerEntries) throws IOException {
59+
FileTime maxLastModifiedTime = FileTime.from(Instant.MIN);
60+
61+
for (LayerEntry layerEntry : layerEntries) {
62+
FileTime lastModifiedTime = Files.getLastModifiedTime(layerEntry.getSourceFile());
63+
if (lastModifiedTime.compareTo(maxLastModifiedTime) > 0) {
64+
maxLastModifiedTime = lastModifiedTime;
65+
}
66+
}
67+
68+
return maxLastModifiedTime;
69+
}
70+
71+
/**
72+
* Gets the last modified time from the metadata of a {@link CacheEntry}.
73+
*
74+
* @param cacheEntry the {@link CacheEntry}
75+
* @return the last modified time, if the metadata is present
76+
* @throws IOException if deserialization of the metadata failed
77+
*/
78+
static Optional<FileTime> getLastModifiedTime(CacheEntry cacheEntry) throws IOException {
79+
if (!cacheEntry.getMetadataBlob().isPresent()) {
80+
return Optional.empty();
81+
}
82+
83+
Blob metadataBlob = cacheEntry.getMetadataBlob().get();
84+
return Optional.of(FileTime.from(Instant.parse(Blobs.writeToString(metadataBlob))));
85+
}
86+
87+
private LastModifiedTimeMetadata() {}
88+
}

jib-core/src/test/java/com/google/cloud/tools/jib/ncache/CacheTest.java

+29-15
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
import java.io.ByteArrayInputStream;
2828
import java.io.IOException;
2929
import java.nio.file.FileAlreadyExistsException;
30+
import java.nio.file.Files;
3031
import java.nio.file.Path;
31-
import java.nio.file.Paths;
32+
import java.nio.file.attribute.FileTime;
33+
import java.time.Instant;
3234
import java.util.zip.GZIPInputStream;
3335
import java.util.zip.GZIPOutputStream;
3436
import org.junit.Assert;
@@ -104,35 +106,38 @@ private static long sizeOf(Blob blob) throws IOException {
104106
private DescriptorDigest layerDiffId1;
105107
private long layerSize1;
106108
private ImmutableList<LayerEntry> layerEntries1;
107-
private Blob metadataBlob1;
108109

109110
private Blob layerBlob2;
110111
private DescriptorDigest layerDigest2;
111112
private DescriptorDigest layerDiffId2;
112113
private long layerSize2;
113114
private ImmutableList<LayerEntry> layerEntries2;
114-
private Blob metadataBlob2;
115115

116116
@Before
117117
public void setUp() throws IOException {
118+
Path directory = temporaryFolder.newFolder().toPath();
119+
Files.createDirectory(directory.resolve("source"));
120+
Files.createFile(directory.resolve("source/file"));
121+
Files.createDirectories(directory.resolve("another/source"));
122+
Files.createFile(directory.resolve("another/source/file"));
123+
118124
layerBlob1 = Blobs.from("layerBlob1");
119125
layerDigest1 = digestOf(compress(layerBlob1));
120126
layerDiffId1 = digestOf(layerBlob1);
121127
layerSize1 = sizeOf(compress(layerBlob1));
122128
layerEntries1 =
123129
ImmutableList.of(
124-
new LayerEntry(Paths.get("source/file"), AbsoluteUnixPath.get("/extraction/path")),
125130
new LayerEntry(
126-
Paths.get("another/source/file"),
131+
directory.resolve("source/file"), AbsoluteUnixPath.get("/extraction/path")),
132+
new LayerEntry(
133+
directory.resolve("another/source/file"),
127134
AbsoluteUnixPath.get("/another/extraction/path")));
128-
metadataBlob1 = Blobs.from("metadata");
129135

130136
layerBlob2 = Blobs.from("layerBlob2");
131137
layerDigest2 = digestOf(compress(layerBlob2));
132138
layerDiffId2 = digestOf(layerBlob2);
133139
layerSize2 = sizeOf(compress(layerBlob2));
134140
layerEntries2 = ImmutableList.of();
135-
metadataBlob2 = Blobs.from("metadata");
136141
}
137142

138143
@Test
@@ -159,31 +164,36 @@ public void testWriteLayerOnly_retrieveByLayerDigest()
159164
}
160165

161166
@Test
162-
public void testWriteWithSelectorAndMetadata_retrieveByLayerDigest()
167+
public void testWriteWithLayerEntries_retrieveByLayerDigest()
163168
throws IOException, CacheCorruptedException {
164169
Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath());
165170

166-
verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1));
171+
verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1));
167172
verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new));
168173
Assert.assertFalse(cache.retrieve(layerDigest2).isPresent());
169174
}
170175

171176
@Test
172-
public void testWriteWithSelectorAndMetadata_retrieveByLayerEntries()
177+
public void testWriteWithLayerEntries_retrieveByLayerEntries()
173178
throws IOException, CacheCorruptedException {
174179
Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath());
175180

176-
verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1));
181+
verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1));
177182
verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new));
178183
Assert.assertFalse(cache.retrieve(layerDigest2).isPresent());
184+
185+
// A source file modification results in the cached layer to be out-of-date and not retrieved.
186+
Files.setLastModifiedTime(
187+
layerEntries1.get(0).getSourceFile(), FileTime.from(Instant.now().plusSeconds(1)));
188+
Assert.assertFalse(cache.retrieve(layerEntries1).isPresent());
179189
}
180190

181191
@Test
182192
public void testRetrieveWithTwoEntriesInCache() throws IOException, CacheCorruptedException {
183193
Cache cache = Cache.withDirectory(temporaryFolder.newFolder().toPath());
184194

185-
verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1, metadataBlob1));
186-
verifyIsLayer2WithMetadata(cache.write(layerBlob2, layerEntries2, metadataBlob2));
195+
verifyIsLayer1WithMetadata(cache.write(layerBlob1, layerEntries1));
196+
verifyIsLayer2WithMetadata(cache.write(layerBlob2, layerEntries2));
187197
verifyIsLayer1WithMetadata(cache.retrieve(layerDigest1).orElseThrow(AssertionError::new));
188198
verifyIsLayer2WithMetadata(cache.retrieve(layerDigest2).orElseThrow(AssertionError::new));
189199
verifyIsLayer1WithMetadata(cache.retrieve(layerEntries1).orElseThrow(AssertionError::new));
@@ -212,7 +222,9 @@ private void verifyIsLayer1NoMetadata(CacheEntry cacheEntry) throws IOException
212222
private void verifyIsLayer1WithMetadata(CacheEntry cacheEntry) throws IOException {
213223
verifyIsLayer1(cacheEntry);
214224
Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent());
215-
Assert.assertEquals("metadata", Blobs.writeToString(cacheEntry.getMetadataBlob().get()));
225+
Assert.assertEquals(
226+
Blobs.writeToString(LastModifiedTimeMetadata.generateMetadata(layerEntries1)),
227+
Blobs.writeToString(cacheEntry.getMetadataBlob().get()));
216228
}
217229

218230
/**
@@ -242,6 +254,8 @@ private void verifyIsLayer2WithMetadata(CacheEntry cacheEntry) throws IOExceptio
242254
Assert.assertEquals(layerDiffId2, cacheEntry.getLayerDiffId());
243255
Assert.assertEquals(layerSize2, cacheEntry.getLayerSize());
244256
Assert.assertTrue(cacheEntry.getMetadataBlob().isPresent());
245-
Assert.assertEquals("metadata", Blobs.writeToString(cacheEntry.getMetadataBlob().get()));
257+
Assert.assertEquals(
258+
Blobs.writeToString(LastModifiedTimeMetadata.generateMetadata(layerEntries2)),
259+
Blobs.writeToString(cacheEntry.getMetadataBlob().get()));
246260
}
247261
}

0 commit comments

Comments
 (0)