blob: 2e5a2bc551e25357ed0b4974d41a526cc6424238 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.util.io;
import com.google.devtools.build.lib.clock.Clock;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.profiler.Profiler;
import com.google.devtools.build.lib.profiler.ProfilerTask;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.logging.Logger;
/**
* A utility class for dealing with filesystem timestamp granularity issues.
*
* <p>
* Consider a sequence of commands such as
* <pre>
* echo ... &gt; foo/bar
* blaze query ...
* echo ... &gt; foo/bar
* blaze query ...
* </pre>
*
* If these commands all run very fast, it is possible that the timestamp
* on foo/bar is not changed by the second command, even though some time has
* passed, because the times are the same when rounded to the file system
* timestamp granularity (often 1 second).
* For performance, we assume that files whose
* timestamps haven't changed can safely be cached without reexamining their contents.
* But this assumption would be violated in the above scenario.
*
* <p>
* To address this, we record the current time at the start of executing
* a Blaze command, and whenever we check the timestamp of a source file
* or BUILD file, we check if the timestamp of that source file matches
* the current time. If so, we set a flag. At the end of the command,
* if the flag was set, then we wait until the clock has advanced, so
* that any file modifications performed after the command exits will
* result in a different file timestamp.
*
* <p>
* This class implicitly assumes that each filesystem's clock
* is the same as either System.currentTimeMillis() or
* System.currentTimeMillis() rounded down to the nearest second.
* That is not strictly correct; there might be clock skew between
* the cpu clock and the file system clocks (e.g. for NFS file systems),
* and some file systems might have different granularity (e.g. the old
* DOS FAT filesystem has TWO-second granularity timestamps).
* Clock skew can be addressed using NTP.
* Other granularities could be addressed by small changes to this class,
* if it turns out to be needed.
*
* <p>
* Another alternative design that we considered was to write to a file and
* read its timestamp. But doing that is a little tricky because we don't have
* a FileSystem or Path handy. Also, if we were going to do this, the stamp
* file that is used should be in the same file system as the input files.
* But the input file system(s) might not be writeable, and even if it is,
* it's hard for Blaze to find a writable file on the same filesystem as the
* input files.
*/
@ThreadCompatible
public class TimestampGranularityMonitor {
private static final Logger logger =
Logger.getLogger(TimestampGranularityMonitor.class.getName());
/**
* The time of the start of the current Blaze command,
* in milliseconds.
*/
private long commandStartTimeMillis;
/**
* The time of the start of the current Blaze command,
* in milliseconds, rounded to one second granularity.
*/
private long commandStartTimeMillisRounded;
/**
* True iff we detected a source file or BUILD file whose (unrounded)
* timestamp matched the time at the start of the current Blaze command
* rounded to the nearest second.
*/
private volatile boolean waitASecond;
/**
* True iff we detected a source file or BUILD file whose timestamp
* exactly matched the time at the start of the current Blaze command
* (measuring both in integral numbers of milliseconds).
*/
private volatile boolean waitAMillisecond;
private final Clock clock;
public TimestampGranularityMonitor(Clock clock) {
this.clock = clock;
}
/**
* Record the time at which the Blaze command started.
* This is needed for use by waitForTimestampGranularity().
*/
public void setCommandStartTime() {
this.commandStartTimeMillis = clock.currentTimeMillis();
this.commandStartTimeMillisRounded = roundDown(this.commandStartTimeMillis);
this.waitASecond = false;
this.waitAMillisecond = false;
}
/**
* Record that the output of this Blaze command depended on the contents
* of a build file or source file with the specified time stamp.
*/
@ThreadSafe
public void notifyDependenceOnFileTime(PathFragment path, long ctimeMillis) {
if (!this.waitAMillisecond && ctimeMillis == this.commandStartTimeMillis) {
if (path != null) {
logger.info("Will have to wait for a millisecond on completion because of " + path);
}
this.waitAMillisecond = true;
}
if (!this.waitASecond && ctimeMillis == this.commandStartTimeMillisRounded) {
if (path != null) {
logger.info("Will have to wait for a second on completion because of " + path);
}
this.waitASecond = true;
}
}
/**
* If needed, wait until the next "tick" of the filesystem timestamp clock.
* This is done to ensure that files created after the current Blaze command
* finishes will have timestamps different than files created before the
* current Blaze command started. Otherwise a sequence of commands
* such as
* <pre>
* echo ... &gt; foo/BUILD
* blaze query ...
* echo ... &gt; foo/BUILD
* blaze query ...
* </pre>
* could return wrong results, due to the contents of package foo
* being cached even though foo/BUILD changed.
*/
public void waitForTimestampGranularity(OutErr outErr) {
if (this.waitASecond || this.waitAMillisecond) {
long before = clock.currentTimeMillis();
long startedWaiting = Profiler.nanoTimeMaybe();
boolean interrupted = false;
if (waitASecond) {
// 50ms slack after the whole-second boundary
while (clock.currentTimeMillis() < commandStartTimeMillisRounded + 1050) {
try {
Thread.sleep(50 /* milliseconds */);
} catch (InterruptedException e) {
if (!interrupted) {
outErr.printErrLn("INFO: Hang on a second...");
interrupted = true;
}
}
}
} else {
while (clock.currentTimeMillis() == commandStartTimeMillis) {
try {
Thread.sleep(1 /* milliseconds */);
} catch (InterruptedException e) {
if (!interrupted) {
outErr.printErrLn("INFO: Hang on a millisecond...");
interrupted = true;
}
}
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
Profiler.instance().logSimpleTask(startedWaiting, ProfilerTask.WAIT,
"Timestamp granularity");
logger.info(
"Waited for "
+ (clock.currentTimeMillis() - before)
+ "ms for file system"
+ " to catch up");
}
}
/**
* Rounds the specified time, in milliseconds, down to the nearest second,
* and returns the result in milliseconds.
*/
private static long roundDown(long millis) {
return millis / 1000 * 1000;
}
}