blob: e83ac868de0331b79e53acc58d2c7f98a37f64ef [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.util;
16
17import com.google.common.collect.ForwardingMap;
janakrc3bcb982020-04-14 06:50:08 -070018import com.google.common.flogger.GoogleLogger;
Googler5582d892017-02-23 20:40:15 +000019import com.google.common.io.ByteStreams;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010020import com.google.devtools.build.lib.vfs.FileSystemUtils;
21import com.google.devtools.build.lib.vfs.Path;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010022import java.io.BufferedInputStream;
23import java.io.BufferedOutputStream;
Googler5582d892017-02-23 20:40:15 +000024import java.io.ByteArrayInputStream;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010025import java.io.DataInputStream;
26import java.io.DataOutputStream;
27import java.io.IOException;
Googler5582d892017-02-23 20:40:15 +000028import java.io.InputStream;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010029import java.util.HashMap;
30import java.util.Hashtable;
31import java.util.LinkedHashMap;
32import java.util.Map;
33
34/**
35 * A map that is backed by persistent storage. It uses two files on disk for
36 * this: The first file contains all the entries and gets written when invoking
37 * the {@link #save()} method. The second file contains a journal of all entries
38 * that were added to or removed from the map since constructing the instance of
39 * the map or the last invocation of {@link #save()} and gets written after each
40 * update of the map although sub-classes are free to implement their own
41 * journal update strategy.
42 *
43 * <p><b>Ceci n'est pas un Map</b>. Strictly speaking, the {@link Map}
44 * interface doesn't permit the possibility of failure. This class uses
45 * persistence; persistence means I/O, and I/O means the possibility of
46 * failure. Therefore the semantics of this may deviate from the Map contract
47 * in failure cases. In particular, updates are not guaranteed to succeed.
48 * However, I/O failures are guaranteed to be reported upon the subsequent call
49 * to a method that throws {@code IOException} such as {@link #save}.
50 *
51 * <p>To populate the map entries using the previously persisted entries call
52 * {@link #load()} prior to invoking any other map operation.
53 * <p>
54 * Like {@link Hashtable} but unlike {@link HashMap}, this class does
55 * <em>not</em> allow <tt>null</tt> to be used as a key or a value.
56 * <p>
57 * IO failures during reading or writing the map entries to disk may result in
58 * {@link AssertionError} getting thrown from the failing method.
59 * <p>
60 * The implementation of the map is not synchronized. If access from multiple
61 * threads is required it must be synchronized using an external object.
62 * <p>
63 * The constructor allows passing in a version number that gets written to the
64 * files on disk and checked before reading from disk. Files with an
65 * incompatible version number will be ignored. This allows the client code to
66 * change the persistence format without polluting the file system name space.
67 */
68public abstract class PersistentMap<K, V> extends ForwardingMap<K, V> {
69
70 private static final int MAGIC = 0x20071105;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010071 private static final int ENTRY_MAGIC = 0xfe;
Googler5582d892017-02-23 20:40:15 +000072 private static final int MIN_MAPFILE_SIZE = 16;
73 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
janakrc3bcb982020-04-14 06:50:08 -070074 private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010075
76 private final int version;
77 private final Path mapFile;
78 private final Path journalFile;
79 private final Map<K, V> journal;
80 private DataOutputStream journalOut;
81
82 /**
83 * 'dirty' is true when the in-memory representation of the map is more recent
84 * than the on-disk representation.
85 */
86 private boolean dirty;
87
88 /**
89 * If non-null, contains the message from an {@code IOException} thrown by a
90 * previously failed write. This error is deferred until the next call to a
91 * method which is able to throw an exception.
92 */
93 private String deferredIOFailure = null;
94
95 /**
96 * 'loaded' is true when the in-memory representation is at least as recent as
97 * the on-disk representation.
98 */
99 private boolean loaded;
100
101 private final Map<K, V> delegate;
102
103 /**
104 * Creates a new PersistentMap instance using the specified backing map.
105 *
106 * @param version the version tag. Changing the version tag allows updating
107 * the on disk format. The map will never read from a file that was
108 * written using a different version tag.
109 * @param map the backing map to use for this PersistentMap.
110 * @param mapFile the file to save the map entries to.
111 * @param journalFile the journal file to write entries between invocations of
112 * {@link #save()}.
113 */
114 public PersistentMap(int version, Map<K, V> map, Path mapFile, Path journalFile) {
115 this.version = version;
116 journal = new LinkedHashMap<>();
117 this.mapFile = mapFile;
118 this.journalFile = journalFile;
119 delegate = map;
120 }
121
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000122
123 @Override
124 protected Map<K, V> delegate() {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100125 return delegate;
126 }
127
128 @Override
129 public V put(K key, V value) {
130 if (key == null) {
131 throw new NullPointerException();
132 }
133 if (value == null) {
134 throw new NullPointerException();
135 }
136 V previous = delegate().put(key, value);
137 journal.put(key, value);
138 markAsDirty();
139 return previous;
140 }
141
142 /**
143 * Marks the map as dirty and potentially writes updated entries to the
144 * journal.
145 */
Nathan Harmata803ef932016-04-14 03:11:27 +0000146 protected void markAsDirty() {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100147 dirty = true;
148 if (updateJournal()) {
149 writeJournal();
150 }
151 }
152
153 /**
154 * Determines if the journal should be updated. The default implementation
155 * always returns 'true', but subclasses are free to override this to
156 * implement their own journal updating strategy. For example it is possible
157 * to implement an update at most every five seconds using the following code:
158 *
159 * <pre>
160 * private long nextUpdate;
161 * protected boolean updateJournal() {
162 * long time = System.currentTimeMillis();
163 * if (time &gt; nextUpdate) {
164 * nextUpdate = time + 5 * 1000;
165 * return true;
166 * }
167 * return false;
168 * }
169 * </pre>
170 */
171 protected boolean updateJournal() {
172 return true;
173 }
174
175 @Override
176 @SuppressWarnings("unchecked")
177 public V remove(Object object) {
178 V previous = delegate().remove(object);
179 if (previous != null) {
180 // we know that 'object' must be an instance of K, because the
181 // remove call succeeded, i.e. 'object' was mapped to 'previous'.
182 journal.put((K) object, null); // unchecked
183 markAsDirty();
184 }
185 return previous;
186 }
187
188 /**
189 * Updates the persistent journal by writing all entries to the
190 * {@link #journalOut} stream and clearing the in memory journal.
191 */
192 private void writeJournal() {
193 try {
194 if (journalOut == null) {
Benjamin Peterson184faf62017-04-07 09:40:11 +0000195 if (journalFile.exists()) {
196 // The journal file was left around after the last save() because
197 // keepJournal() was true. Append to it.
198 journalOut =
199 new DataOutputStream(new BufferedOutputStream(journalFile.getOutputStream(true)));
200 } else {
201 // Create new journal.
202 journalOut = createMapFile(journalFile);
203 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100204 }
205 writeEntries(journalOut, journal);
206 journalOut.flush();
207 journal.clear();
208 } catch (IOException e) {
209 this.deferredIOFailure = e.getMessage() + " during journal append";
210 }
211 }
212
213 protected void forceFlush() {
214 if (dirty) {
215 writeJournal();
216 }
217 }
218
219 /**
220 * Load the previous written map entries from disk.
221 *
222 * @param failFast if true, throw IOException rather than silently ignoring.
223 * @throws IOException
224 */
225 public void load(boolean failFast) throws IOException {
226 if (!loaded) {
227 loadEntries(mapFile, failFast);
228 if (journalFile.exists()) {
229 try {
230 loadEntries(journalFile, failFast);
231 } catch (IOException e) {
232 if (failFast) {
233 throw e;
234 }
235 //Else: ignore any errors reading the journal file as it may contain
236 //partial entries.
237 }
238 // Force the map to be dirty, so that we can save it to disk.
239 dirty = true;
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000240 save(/*fullSave=*/ true);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100241 } else {
242 dirty = false;
243 }
244 loaded = true;
245 }
246 }
247
248 /**
249 * Load the previous written map entries from disk.
250 *
251 * @throws IOException
252 */
253 public void load() throws IOException {
cushon03e70182017-09-15 09:33:27 +0200254 load(/* failFast= */ false);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100255 }
256
257 @Override
258 public void clear() {
259 super.clear();
260 markAsDirty();
261 try {
262 save();
263 } catch (IOException e) {
264 this.deferredIOFailure = e.getMessage() + " during map write";
265 }
266 }
267
268 /**
269 * Saves all the entries of this map to disk and deletes the journal file.
270 *
271 * @throws IOException if there was an I/O error during this call, or any previous call since the
272 * last save().
273 */
274 public long save() throws IOException {
275 return save(false);
276 }
277
278 /**
279 * Saves all the entries of this map to disk and deletes the journal file.
280 *
281 * @param fullSave if true, always write the full cache to disk, without the
282 * journal.
283 * @throws IOException if there was an I/O error during this call, or any
284 * previous call since the last save().
285 */
286 private long save(boolean fullSave) throws IOException {
287 /* Report a previously failing I/O operation. */
288 if (deferredIOFailure != null) {
289 try {
290 throw new IOException(deferredIOFailure);
291 } finally {
292 deferredIOFailure = null;
293 }
294 }
295 if (dirty) {
296 if (!fullSave && keepJournal()) {
297 forceFlush();
298 journalOut.close();
299 journalOut = null;
300 return journalSize() + cacheSize();
301 } else {
302 dirty = false;
303 Path mapTemp =
304 mapFile.getRelative(FileSystemUtils.replaceExtension(mapFile.asFragment(), ".tmp"));
305 try {
306 saveEntries(delegate(), mapTemp);
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000307 mapFile.delete();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100308 mapTemp.renameTo(mapFile);
309 } finally {
310 mapTemp.delete();
311 }
312 clearJournal();
313 journalFile.delete();
314 return cacheSize();
315 }
316 } else {
317 return cacheSize();
318 }
319 }
320
321 protected final long journalSize() throws IOException {
322 return journalFile.exists() ? journalFile.getFileSize() : 0;
323 }
324
325 protected final long cacheSize() throws IOException {
326 return mapFile.exists() ? mapFile.getFileSize() : 0;
327 }
328
329 /**
330 * If true, keep the journal during the save(). The journal is flushed, but
331 * the map file is not touched. This may be useful in cases where the journal
332 * is much smaller than the map.
333 */
334 protected boolean keepJournal() {
335 return false;
336 }
337
338 private void clearJournal() throws IOException {
339 journal.clear();
340 if (journalOut != null) {
341 journalOut.close();
342 journalOut = null;
343 }
344 }
345
346 private void loadEntries(Path mapFile, boolean failFast) throws IOException {
347 if (!mapFile.exists()) {
348 return;
349 }
Googler5582d892017-02-23 20:40:15 +0000350
351 long fileSize = mapFile.getFileSize();
352 if (fileSize < MIN_MAPFILE_SIZE) {
353 if (failFast) {
354 throw new IOException(mapFile + " is too short: Only " + fileSize + " bytes");
355 } else {
356 return;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100357 }
Googler5582d892017-02-23 20:40:15 +0000358 } else if (fileSize > MAX_ARRAY_SIZE) {
359 if (failFast) {
360 throw new IOException(mapFile + " is too long: " + fileSize + " bytes");
361 } else {
362 return;
363 }
364 }
365
366 // We read the whole file up front as a performance optimization; otherwise calling available()
367 // on the stream over and over does a lot of syscalls.
368 byte[] mapBytes;
369 try (InputStream fileInput = mapFile.getInputStream()) {
370 mapBytes = ByteStreams.toByteArray(new BufferedInputStream(fileInput));
371 }
372 DataInputStream in = new DataInputStream(new ByteArrayInputStream(mapBytes));
373 try {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100374 if (in.readLong() != MAGIC) { // not a PersistentMap
375 if (failFast) {
376 throw new IOException("Unexpected format");
377 }
378 return;
379 }
380 if (in.readLong() != version) { // PersistentMap version incompatible
381 if (failFast) {
382 throw new IOException("Unexpected format");
383 }
384 return;
385 }
386 readEntries(in, failFast);
387 } finally {
388 in.close();
389 }
Googler5582d892017-02-23 20:40:15 +0000390
janakrc3bcb982020-04-14 06:50:08 -0700391 logger.atInfo().log("Loaded cache '%s' [%d bytes]", mapFile, fileSize);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100392 }
393
394 /**
395 * Saves the entries in the specified map into the specified file.
396 *
397 * @param map the map to be written into the file.
398 * @param mapFile the file the map is written to.
399 * @throws IOException
400 */
401 private void saveEntries(Map<K, V> map, Path mapFile) throws IOException {
Ulf Adams07dba942015-03-05 14:47:37 +0000402 try (DataOutputStream out = createMapFile(mapFile)) {
403 writeEntries(out, map);
404 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100405 }
406
407 /**
408 * Creates the specified file and returns the DataOuputStream suitable for writing entries.
409 *
410 * @param mapFile the file the map is written to.
411 * @return the DataOutputStream that was can be used for saving the map to the file.
412 * @throws IOException
413 */
414 private DataOutputStream createMapFile(Path mapFile) throws IOException {
415 FileSystemUtils.createDirectoryAndParents(mapFile.getParentDirectory());
416 DataOutputStream out =
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000417 new DataOutputStream(new BufferedOutputStream(mapFile.getOutputStream()));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100418 out.writeLong(MAGIC);
419 out.writeLong(version);
420 return out;
421 }
422
423 /**
424 * Writes the Map entries to the specified DataOutputStream.
425 *
426 * @param out the DataOutputStream to write the Map entries to.
427 * @param map the Map containing the entries to be written to the
428 * DataOutputStream.
429 * @throws IOException
430 */
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000431 private void writeEntries(DataOutputStream out, Map<K, V> map) throws IOException {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100432 for (Map.Entry<K, V> entry : map.entrySet()) {
433 out.writeByte(ENTRY_MAGIC);
434 writeKey(entry.getKey(), out);
435 V value = entry.getValue();
436 boolean isEntry = (value != null);
437 out.writeBoolean(isEntry);
438 if (isEntry) {
439 writeValue(value, out);
440 }
441 }
442 }
443
444 /**
445 * Reads the Map entries from the specified DataInputStream.
446 *
447 * @param failFast if true, throw IOException if entries are in an unexpected
448 * format.
449 * @param in the DataInputStream to read the Map entries from.
450 * @throws IOException
451 */
452 private void readEntries(DataInputStream in, boolean failFast) throws IOException {
453 Map<K, V> map = delegate();
454 while (hasEntries(in, failFast)) {
455 K key = readKey(in);
456 boolean isEntry = in.readBoolean();
457 if (isEntry) {
458 V value = readValue(in);
459 map.put(key, value);
460 } else {
461 map.remove(key);
462 }
463 }
464 }
465
466 private boolean hasEntries(DataInputStream in, boolean failFast) throws IOException {
467 if (in.available() <= 0) {
468 return false;
Ulf Adams07dba942015-03-05 14:47:37 +0000469 } else if (in.readUnsignedByte() != ENTRY_MAGIC) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100470 if (failFast) {
471 throw new IOException("Corrupted entry separator");
472 } else {
473 return false;
474 }
475 }
476 return true;
477 }
478
479 /**
480 * Writes a key of this map into the specified DataOutputStream.
481 *
482 * @param key the key to write to the DataOutputStream.
483 * @param out the DataOutputStream to write the entry to.
484 * @throws IOException
485 */
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000486 protected abstract void writeKey(K key, DataOutputStream out) throws IOException;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100487
488 /**
489 * Writes a value of this map into the specified DataOutputStream.
490 *
491 * @param value the value to write to the DataOutputStream.
492 * @param out the DataOutputStream to write the entry to.
493 * @throws IOException
494 */
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000495 protected abstract void writeValue(V value, DataOutputStream out) throws IOException;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100496
497 /**
498 * Reads an entry of this map from the specified DataInputStream.
499 *
500 * @param in the DataOutputStream to read the entry from.
501 * @return the entry that was read from the DataInputStream.
502 * @throws IOException
503 */
504 protected abstract K readKey(DataInputStream in) throws IOException;
505
506 /**
507 * Reads an entry of this map from the specified DataInputStream.
508 *
509 * @param in the DataOutputStream to read the entry from.
510 * @return the entry that was read from the DataInputStream.
511 * @throws IOException
512 */
513 protected abstract V readValue(DataInputStream in) throws IOException;
514}
Yun Pengeb2f5ea2016-06-14 12:47:49 +0000515