Skip to content

Commit

Permalink
Merge pull request #14775 from geoand/dev-instr-allow-disable
Browse files Browse the repository at this point in the history
When a change to startup related code is made disable dev mode instrumentation
  • Loading branch information
stuartwdouglas authored Feb 5, 2021
2 parents 15d6405 + 6643b08 commit 9480b5f
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.deployment.dev;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;

@ConfigRoot(phase = ConfigPhase.BUILD_TIME)
public class DevConfig {

/**
* Whether or not Quarkus should disable it's ability to not do a full restart
* when changes to classes are compatible with JVM instrumentation.
* If this is set to true, Quarkus will always restart on changes and never perform class redefinition.
*/
@ConfigItem(defaultValue = "true")
boolean instrumentation;

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.Index;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.logging.Logger;

import io.quarkus.bootstrap.runner.Timing;
Expand All @@ -45,12 +50,18 @@
import io.quarkus.dev.spi.DevModeType;
import io.quarkus.dev.spi.HotReplacementContext;
import io.quarkus.dev.spi.HotReplacementSetup;
import io.quarkus.runtime.Startup;
import io.quarkus.runtime.StartupEvent;

public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable {

private static final Logger log = Logger.getLogger(RuntimeUpdatesProcessor.class);

private static final String CLASS_EXTENSION = ".class";
private static final DotName STARTUP_NAME = DotName.createSimple(Startup.class.getName());
private static final DotName STARTUP_EVENT_NAME = DotName.createSimple(StartupEvent.class.getName());
private static final DotName OBSERVES_NAME = DotName.createSimple("javax.enterprise.event.Observes");

static volatile RuntimeUpdatesProcessor INSTANCE;

private final Path applicationRoot;
Expand Down Expand Up @@ -209,12 +220,15 @@ public boolean doScan(boolean userInitiated) throws IOException {
classTransformers.apply(name, bytes));
}
Index current = indexer.complete();
boolean ok = true;
for (ClassInfo clazz : current.getKnownClasses()) {
ClassInfo old = lastStartIndex.getClassByName(clazz.name());
if (!ClassComparisonUtil.isSameStructure(clazz, old)) {
ok = false;
break;
boolean ok = instrumentationEnabled()
&& !containsStartupCode(current);
if (ok) {
for (ClassInfo clazz : current.getKnownClasses()) {
ClassInfo old = lastStartIndex.getClassByName(clazz.name());
if (!ClassComparisonUtil.isSameStructure(clazz, old)) {
ok = false;
break;
}
}
}

Expand Down Expand Up @@ -258,6 +272,36 @@ public boolean doScan(boolean userInitiated) throws IOException {
return false;
}

private Boolean instrumentationEnabled() {
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
return ConfigProvider.getConfig()
.getOptionalValue("quarkus.dev.instrumentation", boolean.class).orElse(true);
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}

private boolean containsStartupCode(Index index) {
if (!index.getAnnotations(STARTUP_NAME).isEmpty()) {
return true;
}
List<AnnotationInstance> observesInstances = index.getAnnotations(OBSERVES_NAME);
if (!observesInstances.isEmpty()) {
for (AnnotationInstance observesInstance : observesInstances) {
if (observesInstance.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
MethodParameterInfo methodParameterInfo = observesInstance.target().asMethodParameter();
short paramPos = methodParameterInfo.position();
if (STARTUP_EVENT_NAME.equals(methodParameterInfo.method().parameters().get(paramPos).name())) {
return true;
}
}
}
}
return false;
}

@Override
public void addPreScanStep(Runnable runnable) {
preScanSteps.add(runnable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@

/**
* @author <a href="http://escoffier.me">Clement Escoffier</a>
*
* <p>
* NOTE to anyone diagnosing failures in this test, to run a single method use:
*
* <p>
* mvn install -Dit.test=DevMojoIT#methodName
*/
@DisableForNative
Expand Down Expand Up @@ -234,6 +234,48 @@ public void testThatInstrumentationBasedReloadWorks() throws MavenInvocationExce

//verify that this was an instrumentation based reload
Assertions.assertEquals(firstUuid, DevModeTestUtils.getHttpResponse("/app/uuid"));

source = new File(testDir, "src/main/java/org/acme/HelloService.java");
filter(source, Collections.singletonMap("\"Stuart\"", "\"Stuart Douglas\""));

// Wait until we get "Stuart Douglas"
await()
.pollDelay(100, TimeUnit.MILLISECONDS)
.atMost(1, TimeUnit.MINUTES)
.until(() -> DevModeTestUtils.getHttpResponse("/app/name").contains("Stuart Douglas"));

//this bean observes startup event, so it should be different UUID
String secondUUid = DevModeTestUtils.getHttpResponse("/app/uuid");
Assertions.assertNotEquals(secondUUid, firstUuid);

//now disable instrumentation based restart, and try again
//change it back to hello
DevModeTestUtils.getHttpResponse("/app/disable");
source = new File(testDir, "src/main/java/org/acme/HelloResource.java");
filter(source, Collections.singletonMap("return \"" + uuid + "\";", "return \"hello\";"));

// Wait until we get "hello"
await()
.pollDelay(100, TimeUnit.MILLISECONDS)
.atMost(1, TimeUnit.MINUTES).until(() -> DevModeTestUtils.getHttpResponse("/app/hello").contains("hello"));

//verify that this was not instrumentation based reload
Assertions.assertNotEquals(secondUUid, DevModeTestUtils.getHttpResponse("/app/uuid"));
secondUUid = DevModeTestUtils.getHttpResponse("/app/uuid");

//now re-enable
//and repeat
DevModeTestUtils.getHttpResponse("/app/enable");
source = new File(testDir, "src/main/java/org/acme/HelloResource.java");
filter(source, Collections.singletonMap("return \"hello\";", "return \"" + uuid + "\";"));

// Wait until we get uuid
await()
.pollDelay(100, TimeUnit.MILLISECONDS)
.atMost(1, TimeUnit.MINUTES).until(() -> DevModeTestUtils.getHttpResponse("/app/hello").contains(uuid));

//verify that this was an instrumentation based reload
Assertions.assertEquals(secondUUid, DevModeTestUtils.getHttpResponse("/app/uuid"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,16 @@ public String name() {
public String uuid() {
return uuid.toString();
}

@GET
@Path("disable")
public void disable() {
System.setProperty("quarkus.dev.instrumentation", "false");
}

@GET
@Path("enable")
public void enable() {
System.setProperty("quarkus.dev.instrumentation","true");
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package org.acme;

import io.quarkus.runtime.StartupEvent;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

@ApplicationScoped
public class HelloService {

public void start(@Observes StartupEvent startupEvent) {

}

public String name() {
return "Stuart";
}
Expand Down

0 comments on commit 9480b5f

Please sign in to comment.