Skip to content

Commit

Permalink
Dev mode - make it possibile to watch files matching a predicate
Browse files Browse the repository at this point in the history
- HotDeploymentWatchedFileBuildItem#builder().setPredicate(p).build()
  • Loading branch information
mkouba committed May 2, 2023
1 parent 9ea8387 commit 19c5691
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,29 +1,83 @@
package io.quarkus.deployment.builditem;

import java.util.function.Predicate;

import io.quarkus.builder.item.MultiBuildItem;

/**
* A file that if modified may result in a hot redeployment when in the dev mode.
* Identifies a file from a
* {@link io.quarkus.bootstrap.devmode.DependenciesFilter#getReloadableModules(io.quarkus.bootstrap.model.ApplicationModel)
* reloadable module} that, if modified, may result in a hot redeployment when in the dev mode.
* <p>
* A file may be identified with an exact location or a matching predicate. See {@link Builder#setLocation(String)} and
* {@link Builder#setLocationPredicate(Predicate)}.
* <p>
* If multiple build items match the same file then the final value of {@code restartNeeded} is computed as a logical OR of all
* the {@link #isRestartNeeded()} values.
*/
public final class HotDeploymentWatchedFileBuildItem extends MultiBuildItem {

public static Builder builder() {
return new Builder();
}

private final String location;
private final Predicate<String> locationPredicate;

private final boolean restartNeeded;

/**
*
* @param location
* @see #builder()
*/
public HotDeploymentWatchedFileBuildItem(String location) {
this(location, true);
}

/**
*
* @param location
* @param restartNeeded
* @see #builder()
*/
public HotDeploymentWatchedFileBuildItem(String location, boolean restartNeeded) {
this(location, null, restartNeeded);
}

private HotDeploymentWatchedFileBuildItem(String location, Predicate<String> locationPredicate, boolean restartNeeded) {
if (location == null && locationPredicate == null) {
throw new IllegalArgumentException("Either location or predicate must be set");
}
this.location = location;
this.locationPredicate = locationPredicate;
this.restartNeeded = restartNeeded;
}

/**
*
* @return a location a file from a reloadable module
*/
public String getLocation() {
return location;
}

public boolean hasLocation() {
return location != null;
}

/**
*
* @return a predicate used to match a file from a reloadable module
*/
public Predicate<String> getLocationPredicate() {
return locationPredicate;
}

public boolean hasLocationPredicate() {
return locationPredicate != null;
}

/**
*
* @return {@code true} if a file change should result in an application restart, {@code false} otherwise
Expand All @@ -32,4 +86,37 @@ public boolean isRestartNeeded() {
return restartNeeded;
}

public static class Builder {

private String location;
private Predicate<String> locationPredicate;
private boolean restartNeeded = true;

public Builder setLocation(String location) {
if (locationPredicate != null) {
throw new IllegalArgumentException("Predicate already set");
}
this.location = location;
return this;
}

public Builder setLocationPredicate(Predicate<String> locationPredicate) {
if (location != null) {
throw new IllegalArgumentException("Location already set");
}
this.locationPredicate = locationPredicate;
return this;
}

public Builder setRestartNeeded(boolean restartNeeded) {
this.restartNeeded = restartNeeded;
return this;
}

public HotDeploymentWatchedFileBuildItem build() {
return new HotDeploymentWatchedFileBuildItem(location, locationPredicate, restartNeeded);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -18,14 +20,21 @@ ServiceStartBuildItem setupWatchedFileHotDeployment(List<HotDeploymentWatchedFil
// TODO: this should really be an output of the RuntimeRunner
RuntimeUpdatesProcessor processor = RuntimeUpdatesProcessor.INSTANCE;
if (processor != null || launchModeBuildItem.isAuxiliaryApplication()) {
Map<String, Boolean> watchedFilePaths = files.stream()

Map<String, Boolean> watchedFilePaths = files.stream().filter(HotDeploymentWatchedFileBuildItem::hasLocation)
.collect(Collectors.toMap(HotDeploymentWatchedFileBuildItem::getLocation,
HotDeploymentWatchedFileBuildItem::isRestartNeeded,
(isRestartNeeded1, isRestartNeeded2) -> isRestartNeeded1 || isRestartNeeded2));

List<Entry<Predicate<String>, Boolean>> watchedFilePredicates = files.stream()
.filter(HotDeploymentWatchedFileBuildItem::hasLocationPredicate)
.map(f -> Map.entry(f.getLocationPredicate(), f.isRestartNeeded()))
.collect(Collectors.toUnmodifiableList());

if (launchModeBuildItem.isAuxiliaryApplication()) {
TestWatchedFiles.setWatchedFilePaths(watchedFilePaths);
TestWatchedFiles.setWatchedFilePaths(watchedFilePaths, watchedFilePredicates);
} else {
processor.setWatchedFilePaths(watchedFilePaths, false);
processor.setWatchedFilePaths(watchedFilePaths, watchedFilePredicates, false);
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -331,11 +332,10 @@ private void periodicTestCompile() {
}
Set<String> filesChanges = new HashSet<>(checkForFileChange(s -> s.getTest().orElse(null), test));
filesChanges.addAll(checkForFileChange(DevModeContext.ModuleInfo::getMain, test));
boolean configFileRestartNeeded = filesChanges.stream().map(test.watchedFilePaths::get)
.anyMatch(Boolean.TRUE::equals);
boolean fileRestartNeeded = filesChanges.stream().anyMatch(test::isWatchedFileRestartNeeded);

ClassScanResult merged = ClassScanResult.merge(changedTestClassResult, changedApp);
if (configFileRestartNeeded) {
if (fileRestartNeeded) {
if (testCompileProblem == null) {
testSupport.runTests(null);
}
Expand Down Expand Up @@ -462,21 +462,19 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) {
main, false);
Set<String> filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, main);

boolean configFileRestartNeeded = forceRestart || filesChanged.stream().map(main.watchedFilePaths::get)
.anyMatch(Boolean.TRUE::equals);
boolean fileRestartNeeded = forceRestart || filesChanged.stream().anyMatch(main::isWatchedFileRestartNeeded);
boolean instrumentationChange = false;

List<Path> changedFilesForRestart = new ArrayList<>();
if (configFileRestartNeeded) {
changedFilesForRestart
.addAll(filesChanged.stream().filter(fn -> Boolean.TRUE.equals(main.watchedFilePaths.get(fn)))
.map(Paths::get).collect(Collectors.toList()));
if (fileRestartNeeded) {
filesChanged.stream().filter(main::isWatchedFileRestartNeeded).map(Paths::get)
.forEach(changedFilesForRestart::add);
}
changedFilesForRestart.addAll(changedClassResults.getChangedClasses());
changedFilesForRestart.addAll(changedClassResults.getAddedClasses());
changedFilesForRestart.addAll(changedClassResults.getDeletedClasses());

if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !configFileRestartNeeded
if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !fileRestartNeeded
&& devModeType != DevModeType.REMOTE_LOCAL_SIDE && instrumentationEnabled()) {
//attempt to do an instrumentation based reload
//if only code has changed and not the class structure, then we can do a reload
Expand Down Expand Up @@ -530,7 +528,7 @@ public boolean doScan(boolean userInitiated, boolean forceRestart) {
//all broken we just assume the reason that they have refreshed is because they have fixed something
//trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case
boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged()
|| (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || configFileRestartNeeded);
|| (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || fileRestartNeeded);
if (restartNeeded) {
String changeString = changedFilesForRestart.stream().map(Path::getFileName).map(Object::toString)
.collect(Collectors.joining(", "));
Expand Down Expand Up @@ -1083,12 +1081,14 @@ public RuntimeUpdatesProcessor setDisableInstrumentationForIndexPredicate(
return this;
}

public RuntimeUpdatesProcessor setWatchedFilePaths(Map<String, Boolean> watchedFilePaths, boolean isTest) {
public RuntimeUpdatesProcessor setWatchedFilePaths(Map<String, Boolean> watchedFilePaths,
List<Entry<Predicate<String>, Boolean>> watchedFilePredicates, boolean isTest) {
if (isTest) {
setWatchedFilePathsInternal(watchedFilePaths, test,
s -> s.getTest().isPresent() ? asList(s.getTest().get(), s.getMain()) : singletonList(s.getMain()));
} else {
main.watchedFileTimestamps.clear();
main.watchedFilePredicates = watchedFilePredicates;
setWatchedFilePathsInternal(watchedFilePaths, main, s -> singletonList(s.getMain()));
}
return this;
Expand Down Expand Up @@ -1225,6 +1225,7 @@ static class TimestampSet {
final Map<Path, Path> classFilePathToSourceFilePath = new ConcurrentHashMap<>();
// file path -> isRestartNeeded
private volatile Map<String, Boolean> watchedFilePaths = Collections.emptyMap();
volatile List<Entry<Predicate<String>, Boolean>> watchedFilePredicates = Collections.emptyList();

public void merge(TimestampSet other) {
watchedFileTimestamps.putAll(other.watchedFileTimestamps);
Expand All @@ -1233,7 +1234,21 @@ public void merge(TimestampSet other) {
Map<String, Boolean> newVal = new HashMap<>(watchedFilePaths);
newVal.putAll(other.watchedFilePaths);
watchedFilePaths = newVal;
// The list of predicates should be effectively immutable
watchedFilePredicates = other.watchedFilePredicates;
}

boolean isWatchedFileRestartNeeded(String changedFile) {
Boolean ret = watchedFilePaths.get(changedFile);
if (ret == null) {
ret = false;
for (Entry<Predicate<String>, Boolean> e : watchedFilePredicates) {
if (e.getKey().test(changedFile)) {
ret = ret || e.getValue();
}
}
}
return ret;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ private static Pattern getCompiledPatternOrNull(Optional<String> patternStr) {

public void init() {
if (moduleRunners.isEmpty()) {
TestWatchedFiles.setWatchedFilesListener((s) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(s, true));
TestWatchedFiles.setWatchedFilesListener(
(paths, predicates) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(paths, predicates, true));
final Pattern includeModulePattern = getCompiledPatternOrNull(config.includeModulePattern);
final Pattern excludeModulePattern = getCompiledPatternOrNull(config.excludeModulePattern);
for (var module : context.getAllModules()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.quarkus.dev.testing;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
* provides a way for a test run to tell the external application about watched paths.
Expand All @@ -11,19 +15,55 @@
public class TestWatchedFiles {

private static volatile Map<String, Boolean> watchedFilePaths;
private static volatile Consumer<Map<String, Boolean>> watchedFilesListener;
private static volatile BiConsumer<Map<String, Boolean>, List<Entry<Predicate<String>, Boolean>>> watchedFilesListener;
private static volatile List<Entry<Predicate<String>, Boolean>> watchedFilePredicates;

/**
*
* @param watchedFilePaths
* @deprecated Use {@link #setWatchedFilePaths(Map, List)} instead.
*/
@Deprecated(forRemoval = true)
public synchronized static void setWatchedFilePaths(Map<String, Boolean> watchedFilePaths) {
TestWatchedFiles.watchedFilePaths = watchedFilePaths;
if (watchedFilesListener != null) {
watchedFilesListener.accept(watchedFilePaths);
watchedFilesListener.accept(watchedFilePaths, List.of());
}
}

/**
*
* @param watchedFilesListener
* @deprecated Use {@link #setWatchedFilesListener(BiConsumer)} instead.
*/
@Deprecated(forRemoval = true)
public synchronized static void setWatchedFilesListener(Consumer<Map<String, Boolean>> watchedFilesListener) {
TestWatchedFiles.watchedFilesListener = watchedFilesListener;
TestWatchedFiles.watchedFilesListener = new BiConsumer<Map<String, Boolean>, List<Entry<Predicate<String>, Boolean>>>() {

@Override
public void accept(Map<String, Boolean> files, List<Entry<Predicate<String>, Boolean>> predicates) {
watchedFilesListener.accept(files);
}
};
if (watchedFilePaths != null) {
watchedFilesListener.accept(watchedFilePaths);
}
}

public synchronized static void setWatchedFilePaths(Map<String, Boolean> watchedFilePaths,
List<Entry<Predicate<String>, Boolean>> watchedFilePredicates) {
TestWatchedFiles.watchedFilePaths = watchedFilePaths;
TestWatchedFiles.watchedFilePredicates = watchedFilePredicates;
if (watchedFilesListener != null) {
watchedFilesListener.accept(watchedFilePaths, watchedFilePredicates);
}
}

public synchronized static void setWatchedFilesListener(
BiConsumer<Map<String, Boolean>, List<Entry<Predicate<String>, Boolean>>> watchedFilesListener) {
TestWatchedFiles.watchedFilesListener = watchedFilesListener;
if (watchedFilePaths != null) {
watchedFilesListener.accept(watchedFilePaths, watchedFilePredicates);
}
}
}

0 comments on commit 19c5691

Please sign in to comment.