Add support for polling some directories

It is not always possible to monitor a directory using OS file system
events. For example, inotify does not work with nfs. To work around
this, I add support for a hybrid FileTreeViewConfig that caches a
portion of the file system and monitors it with os file system
notification, but that polls a subset of the directories. When we query
the view using list or listEntries, we will actually query the file
system for the polling directories while we will read from the cache for
the remainder. When we are not in a continuous build (~ *), there is no
polling of the pollingDirectories but the cache will continue to update
the regular directories in the background. When we are in a continuous
build, we use a PollingWatchService to poll the pollingDirectories and
continue to use the regular repository callbacks for the other
directories.

I suspect that #4179 may be resolved by adding the directories for which
monitoring is not working to the pollingDirectories task.
This commit is contained in:
Ethan Atkins 2018-08-28 09:17:58 -07:00
parent 2b2b84f589
commit b155ffb77b
3 changed files with 67 additions and 3 deletions

View File

@ -7,7 +7,7 @@
package sbt
import sbt.Watched.WatchSource
import sbt.internal.io.{ WatchServiceBackedObservable, WatchState }
import sbt.internal.io.{ HybridPollingFileTreeRepository, WatchServiceBackedObservable, WatchState }
import sbt.io._
import FileTreeDataView.{ Observable, Observer }
import sbt.util.Logger
@ -109,4 +109,64 @@ object FileTreeViewConfig {
FileEventMonitor.antiEntropy(copied, antiEntropy, msg => logger.debug(msg.toString))
}
)
/**
* Provides a default [[FileTreeViewConfig]]. When the pollingSources argument is empty, it
* returns the same config as [[sbt.FileTreeViewConfig.default(antiEntropy:scala\.concurrent\.duration\.FiniteDuration)*]].
* Otherwise, it returns the same config as [[polling]].
* @param antiEntropy the duration of the period after a path triggers a build for which it is
* quarantined from triggering another build
* @param pollingInterval the frequency with which the sbt.io.FileEventMonitor polls the file
* system for the paths included in pollingSources
* @param pollingSources the sources that will not be cached in the sbt.io.FileTreeRepository and that
* will be periodically polled for changes during continuous builds.
* @return
*/
def default(
antiEntropy: FiniteDuration,
pollingInterval: FiniteDuration,
pollingSources: Seq[WatchSource]
): FileTreeViewConfig = {
if (pollingSources.isEmpty) default(antiEntropy)
else polling(antiEntropy, pollingInterval, pollingSources)
}
/**
* Provides a polling [[FileTreeViewConfig]]. Unlike the view returned by newDataView in
* [[sbt.FileTreeViewConfig.default(antiEntropy:scala\.concurrent\.duration\.FiniteDuration)*]],
* the view returned by newDataView will not cache any portion of the file system tree that is is
* covered by the pollingSources parameter. The monitor that is generated by newMonitor, will
* poll these directories for changes rather than relying on file system events from the
* operating system. Any paths that are registered with the view that are not included in the
* pollingSources will be cached and monitored using file system events from the operating system
* in the same way that they are in the default view.
*
* @param antiEntropy the duration of the period after a path triggers a build for which it is
* quarantined from triggering another build
* @param pollingInterval the frequency with which the FileEventMonitor polls the file system
* for the paths included in pollingSources
* @param pollingSources the sources that will not be cached in the sbt.io.FileTreeRepository and that
* will be periodically polled for changes during continuous builds.
* @return a [[FileTreeViewConfig]] instance.
*/
def polling(
antiEntropy: FiniteDuration,
pollingInterval: FiniteDuration,
pollingSources: Seq[WatchSource],
): FileTreeViewConfig = FileTreeViewConfig(
() => FileTreeRepository.hybrid(StampedFile.converter, pollingSources: _*),
(
repository: HybridPollingFileTreeRepository[StampedFile],
sources: Seq[WatchSource],
logger: Logger
) => {
repository.register(sources)
FileEventMonitor
.antiEntropy(
repository.toPollingObservable(pollingInterval, sources, NullWatchLogger),
antiEntropy,
msg => logger.debug(msg.toString)
)
}
)
}

View File

@ -250,6 +250,7 @@ object Defaults extends BuildCommon {
extraLoggers :== { _ =>
Nil
},
pollingDirectories :== Nil,
watchSources :== Nil,
watchProjectSources :== Nil,
skip :== false,
@ -271,7 +272,8 @@ object Defaults extends BuildCommon {
None
},
watchStartMessage := Watched.defaultStartWatch,
fileTreeViewConfig := FileTreeViewConfig.default(watchAntiEntropy.value),
fileTreeViewConfig := FileTreeViewConfig
.default(watchAntiEntropy.value, pollInterval.value, pollingDirectories.value),
fileTreeView := state.value
.get(BasicKeys.globalFileTreeView)
.getOrElse(FileTreeView.DEFAULT.asDataView(StampedFile.converter)),
@ -655,7 +657,8 @@ object Defaults extends BuildCommon {
},
watchStartMessage := Watched.projectOnWatchMessage(thisProjectRef.value.project),
watch := watchSetting.value,
fileTreeViewConfig := FileTreeViewConfig.default(watchAntiEntropy.value)
fileTreeViewConfig := FileTreeViewConfig
.default(watchAntiEntropy.value, pollInterval.value, pollingDirectories.value)
)
def generate(generators: SettingKey[Seq[Task[Seq[File]]]]): Initialize[Task[Seq[File]]] =

View File

@ -147,6 +147,7 @@ object Keys {
val suppressSbtShellNotification = settingKey[Boolean]("""True to suppress the "Executing in batch mode.." message.""").withRank(CSetting)
val fileTreeView = taskKey[FileTreeDataView[StampedFile]]("A view of the file system")
val pollInterval = settingKey[FiniteDuration]("Interval between checks for modified sources by the continuous execution command.").withRank(BMinusSetting)
val pollingDirectories = settingKey[Seq[Watched.WatchSource]]("Directories that cannot be cached and must always be rescanned. Typically these will be NFS mounted or something similar.").withRank(DSetting)
val watchAntiEntropy = settingKey[FiniteDuration]("Duration for which the watch EventMonitor will ignore events for a file after that file has triggered a build.").withRank(BMinusSetting)
val watchConfig = taskKey[WatchConfig]("The configuration for continuous execution.").withRank(BMinusSetting)
val watchLogger = taskKey[Logger]("A logger that reports watch events.").withRank(DSetting)