1 /* 2 * Copyright (C) 2010, Google Inc. 3 * and other copyright owners as documented in the project's IP log. 4 * 5 * This program and the accompanying materials are made available 6 * under the terms of the Eclipse Distribution License v1.0 which 7 * accompanies this distribution, is reproduced below, and is 8 * available at http://www.eclipse.org/org/documents/edl-v10.php 9 * 10 * All rights reserved. 11 * 12 * Redistribution and use in source and binary forms, with or 13 * without modification, are permitted provided that the following 14 * conditions are met: 15 * 16 * - Redistributions of source code must retain the above copyright 17 * notice, this list of conditions and the following disclaimer. 18 * 19 * - Redistributions in binary form must reproduce the above 20 * copyright notice, this list of conditions and the following 21 * disclaimer in the documentation and/or other materials provided 22 * with the distribution. 23 * 24 * - Neither the name of the Eclipse Foundation, Inc. nor the 25 * names of its contributors may be used to endorse or promote 26 * products derived from this software without specific prior 27 * written permission. 28 * 29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 */ 43 44 package org.eclipse.jgit.internal.storage.file; 45 46 import java.io.File; 47 import java.text.DateFormat; 48 import java.text.SimpleDateFormat; 49 import java.util.Date; 50 import java.util.Locale; 51 52 import org.eclipse.jgit.util.FS; 53 54 /** 55 * Caches when a file was last read, making it possible to detect future edits. 56 * <p> 57 * This object tracks the last modified time of a file. Later during an 58 * invocation of {@link #isModified(File)} the object will return true if the 59 * file may have been modified and should be re-read from disk. 60 * <p> 61 * A snapshot does not "live update" when the underlying filesystem changes. 62 * Callers must poll for updates by periodically invoking 63 * {@link #isModified(File)}. 64 * <p> 65 * To work around the "racy git" problem (where a file may be modified multiple 66 * times within the granularity of the filesystem modification clock) this class 67 * may return true from isModified(File) if the last modification time of the 68 * file is less than 3 seconds ago. 69 */ 70 public class FileSnapshot { 71 /** 72 * A FileSnapshot that is considered to always be modified. 73 * <p> 74 * This instance is useful for application code that wants to lazily read a 75 * file, but only after {@link #isModified(File)} gets invoked. The returned 76 * snapshot contains only invalid status information. 77 */ 78 public static final FileSnapshot DIRTY = new FileSnapshot(-1, -1); 79 80 /** 81 * A FileSnapshot that is clean if the file does not exist. 82 * <p> 83 * This instance is useful if the application wants to consider a missing 84 * file to be clean. {@link #isModified(File)} will return false if the file 85 * path does not exist. 86 */ 87 public static final FileSnapshot MISSING_FILE = new FileSnapshot(0, 0) { 88 @Override 89 public boolean isModified(File path) { 90 return FS.DETECTED.exists(path); 91 } 92 }; 93 94 /** 95 * Record a snapshot for a specific file path. 96 * <p> 97 * This method should be invoked before the file is accessed. 98 * 99 * @param path 100 * the path to later remember. The path's current status 101 * information is saved. 102 * @return the snapshot. 103 */ 104 public static FileSnapshot save(File path) { 105 final long read = System.currentTimeMillis(); 106 final long modified = path.lastModified(); 107 return new FileSnapshot(read, modified); 108 } 109 110 /** 111 * Record a snapshot for a file for which the last modification time is 112 * already known. 113 * <p> 114 * This method should be invoked before the file is accessed. 115 * 116 * @param modified 117 * the last modification time of the file 118 * 119 * @return the snapshot. 120 */ 121 public static FileSnapshot save(long modified) { 122 final long read = System.currentTimeMillis(); 123 return new FileSnapshot(read, modified); 124 } 125 126 /** Last observed modification time of the path. */ 127 private final long lastModified; 128 129 /** Last wall-clock time the path was read. */ 130 private volatile long lastRead; 131 132 /** True once {@link #lastRead} is far later than {@link #lastModified}. */ 133 private boolean cannotBeRacilyClean; 134 135 private FileSnapshot(long read, long modified) { 136 this.lastRead = read; 137 this.lastModified = modified; 138 this.cannotBeRacilyClean = notRacyClean(read); 139 } 140 141 /** 142 * @return time of last snapshot update 143 */ 144 public long lastModified() { 145 return lastModified; 146 } 147 148 /** 149 * Check if the path may have been modified since the snapshot was saved. 150 * 151 * @param path 152 * the path the snapshot describes. 153 * @return true if the path needs to be read again. 154 */ 155 public boolean isModified(File path) { 156 return isModified(path.lastModified()); 157 } 158 159 /** 160 * Update this snapshot when the content hasn't changed. 161 * <p> 162 * If the caller gets true from {@link #isModified(File)}, re-reads the 163 * content, discovers the content is identical, and 164 * {@link #equals(FileSnapshot)} is true, it can use 165 * {@link #setClean(FileSnapshot)} to make a future 166 * {@link #isModified(File)} return false. The logic goes something like 167 * this: 168 * 169 * <pre> 170 * if (snapshot.isModified(path)) { 171 * FileSnapshot other = FileSnapshot.save(path); 172 * Content newContent = ...; 173 * if (oldContent.equals(newContent) && snapshot.equals(other)) 174 * snapshot.setClean(other); 175 * } 176 * </pre> 177 * 178 * @param other 179 * the other snapshot. 180 */ 181 public void setClean(FileSnapshot other) { 182 final long now = other.lastRead; 183 if (notRacyClean(now)) 184 cannotBeRacilyClean = true; 185 lastRead = now; 186 } 187 188 /** 189 * Compare two snapshots to see if they cache the same information. 190 * 191 * @param other 192 * the other snapshot. 193 * @return true if the two snapshots share the same information. 194 */ 195 public boolean equals(FileSnapshot other) { 196 return lastModified == other.lastModified; 197 } 198 199 @Override 200 public boolean equals(Object other) { 201 if (other instanceof FileSnapshot) 202 return equals((FileSnapshot) other); 203 return false; 204 } 205 206 @Override 207 public int hashCode() { 208 // This is pretty pointless, but override hashCode to ensure that 209 // x.hashCode() == y.hashCode() when x.equals(y) is true. 210 // 211 return (int) lastModified; 212 } 213 214 @Override 215 public String toString() { 216 if (this == DIRTY) 217 return "DIRTY"; //$NON-NLS-1$ 218 if (this == MISSING_FILE) 219 return "MISSING_FILE"; //$NON-NLS-1$ 220 DateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", //$NON-NLS-1$ 221 Locale.US); 222 return "FileSnapshot[modified: " + f.format(new Date(lastModified)) //$NON-NLS-1$ 223 + ", read: " + f.format(new Date(lastRead)) + "]"; //$NON-NLS-1$ //$NON-NLS-2$ 224 } 225 226 private boolean notRacyClean(final long read) { 227 // The last modified time granularity of FAT filesystems is 2 seconds. 228 // Using 2.5 seconds here provides a reasonably high assurance that 229 // a modification was not missed. 230 // 231 return read - lastModified > 2500; 232 } 233 234 private boolean isModified(final long currLastModified) { 235 // Any difference indicates the path was modified. 236 // 237 if (lastModified != currLastModified) 238 return true; 239 240 // We have already determined the last read was far enough 241 // after the last modification that any new modifications 242 // are certain to change the last modified time. 243 // 244 if (cannotBeRacilyClean) 245 return false; 246 247 if (notRacyClean(lastRead)) { 248 // Our last read should have marked cannotBeRacilyClean, 249 // but this thread may not have seen the change. The read 250 // of the volatile field lastRead should have fixed that. 251 // 252 return false; 253 } 254 255 // Our lastRead flag may be old, refresh and retry 256 lastRead = System.currentTimeMillis(); 257 if (notRacyClean(lastRead)) { 258 return false; 259 } 260 261 // We last read this path too close to its last observed 262 // modification time. We may have missed a modification. 263 // Scan again, to ensure we still see the same state. 264 // 265 return true; 266 } 267 }