diff --git a/jetty-deploy/pom.xml b/jetty-deploy/pom.xml index be078b68cf0..ddb938f75a0 100644 --- a/jetty-deploy/pom.xml +++ b/jetty-deploy/pom.xml @@ -59,5 +59,9 @@ jetty-test-helper test + + org.awaitility + awaitility + diff --git a/jetty-deploy/src/main/config/etc/jetty-deploy.xml b/jetty-deploy/src/main/config/etc/jetty-deploy.xml index 45d71dde039..7a1e8d02d15 100644 --- a/jetty-deploy/src/main/config/etc/jetty-deploy.xml +++ b/jetty-deploy/src/main/config/etc/jetty-deploy.xml @@ -53,6 +53,7 @@ + diff --git a/jetty-deploy/src/main/config/modules/deploy.mod b/jetty-deploy/src/main/config/modules/deploy.mod index bdcfe19725b..09df9b02edf 100644 --- a/jetty-deploy/src/main/config/modules/deploy.mod +++ b/jetty-deploy/src/main/config/modules/deploy.mod @@ -20,8 +20,15 @@ etc/jetty-deploy.xml # Defaults Descriptor for all deployed webapps # jetty.deploy.defaultsDescriptorPath=${jetty.base}/etc/webdefault.xml +# Defer Initial Scan +# true to have the initial scan deferred until the Server component is started. +# Note: deploy failures do not fail server startup in a deferred initial scan mode. +# false (default) to have initial scan occur as normal. +# jetty.deploy.deferInitialScan=false + # Monitored directory scan period (seconds) # jetty.deploy.scanInterval=1 # Whether to extract *.war files # jetty.deploy.extractWars=true + diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java index 1310aad82c8..34da525ce16 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java @@ -27,11 +27,13 @@ import java.util.stream.Collectors; import org.eclipse.jetty.deploy.App; import org.eclipse.jetty.deploy.AppProvider; import org.eclipse.jetty.deploy.DeploymentManager; +import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.Scanner; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.component.ContainerLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +51,7 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements private int _scanInterval = 10; private Scanner _scanner; private boolean _useRealPaths; + private boolean _deferInitialScan = false; private final Scanner.DiscreteListener _scannerListener = new Scanner.DiscreteListener() { @@ -152,9 +155,30 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements _scanner.setReportDirs(true); _scanner.setScanDepth(1); //consider direct dir children of monitored dir _scanner.addListener(_scannerListener); - + _scanner.setReportExistingFilesOnStartup(true); + _scanner.setAutoStartScanning(!_deferInitialScan); addBean(_scanner); + if (isDeferInitialScan()) + { + // Setup listener to wait for Server in STARTED state, which + // triggers the first scan of the monitored directories + getDeploymentManager().getServer().addEventListener( + new LifeCycle.Listener() + { + @Override + public void lifeCycleStarted(LifeCycle event) + { + if (event instanceof Server) + { + if (LOG.isDebugEnabled()) + LOG.debug("Triggering Deferred Scan of {}", _monitored); + _scanner.startScanning(); + } + } + }); + } + super.doStart(); } @@ -296,6 +320,34 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements } } + /** + * Test if initial scan should be deferred. + * + * @return true if initial scan is deferred, false to have initial scan occur on startup of ScanningAppProvider. + */ + public boolean isDeferInitialScan() + { + return _deferInitialScan; + } + + /** + * Flag to control initial scan behavior. + * + * + * + * @param defer true to defer initial scan, false to have initial scan occur on startup of ScanningAppProvider. + */ + public void setDeferInitialScan(boolean defer) + { + _deferInitialScan = defer; + } + public void setScanInterval(int scanInterval) { _scanInterval = scanInterval; diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java index e2eb0bd3e2c..ac764dc0f26 100644 --- a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java @@ -17,30 +17,41 @@ import java.io.File; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.deploy.AppProvider; +import org.eclipse.jetty.deploy.DeploymentManager; import org.eclipse.jetty.deploy.test.XmlConfiguredJetty; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.Scanner; +import org.eclipse.jetty.util.component.Container; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.extension.ExtendWith; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.condition.OS.LINUX; @@ -52,8 +63,7 @@ public class WebAppProviderTest private static XmlConfiguredJetty jetty; private boolean symlinkSupported = false; - @BeforeEach - public void setupEnvironment() throws Exception + private void startEnvironment() throws Exception { Path p = testdir.getEmptyPathDir(); jetty = new XmlConfiguredJetty(p); @@ -97,8 +107,9 @@ public class WebAppProviderTest } @Test - public void testStartupContext() + public void testStartupContext() throws Exception { + startEnvironment(); assumeTrue(symlinkSupported); // Check Server for Handlers @@ -115,8 +126,9 @@ public class WebAppProviderTest } @Test - public void testStartupSymlinkContext() + public void testStartupSymlinkContext() throws Exception { + startEnvironment(); assumeTrue(symlinkSupported); // Check for path @@ -139,17 +151,11 @@ public class WebAppProviderTest @EnabledOnOs({LINUX}) public void testWebappSymlinkDir() throws Exception { - jetty.stop(); //reconfigure jetty - - testdir.ensureEmpty(); - jetty = new XmlConfiguredJetty(testdir.getEmptyPathDir()); jetty.addConfiguration("jetty.xml"); jetty.addConfiguration("jetty-http.xml"); jetty.addConfiguration("jetty-deploy-wars.xml"); - assumeTrue(symlinkSupported); - //delete the existing webapps directory File webapps = jetty.getJettyDir("webapps"); assertTrue(IO.delete(webapps)); @@ -159,14 +165,25 @@ public class WebAppProviderTest Files.createDirectory(x.toPath()); //Put a webapp into it - File srcDir = MavenTestingUtils.getTestResourceDir("webapps"); + Path srcDir = MavenTestingUtils.getTestResourcePathDir("webapps"); File fooWar = new File(x, "foo.war"); - IO.copy(new File(srcDir, "foo-webapp-1.war"), fooWar); + IO.copy(srcDir.resolve("foo-webapp-1.war").toFile(), fooWar); assertTrue(Files.exists(fooWar.toPath())); - //make a link from x to webapps - Files.createSymbolicLink(jetty.getJettyDir("webapps").toPath(), x.toPath()); - assertTrue(Files.exists(jetty.getJettyDir("webapps").toPath())); + try + { + //make a link from x to webapps + Files.createSymbolicLink(jetty.getJettyDir("webapps").toPath(), x.toPath()); + assertTrue(Files.exists(jetty.getJettyDir("webapps").toPath())); + symlinkSupported = true; + } + catch (UnsupportedOperationException | FileSystemException e) + { + // if unable to create symlink, no point testing that feature + // this is the path that Microsoft Windows takes. + symlinkSupported = false; + } + assumeTrue(symlinkSupported); jetty.load(); jetty.start(); @@ -179,10 +196,6 @@ public class WebAppProviderTest @EnabledOnOs({LINUX}) public void testBaseDirSymlink() throws Exception { - jetty.stop(); //reconfigure jetty - - testdir.ensureEmpty(); - Path realBase = testdir.getEmptyPathDir(); //set jetty up on the real base @@ -202,8 +215,8 @@ public class WebAppProviderTest jetty.stop(); //Make a symbolic link to the real base - File testsDir = MavenTestingUtils.getTargetTestingDir(); - Path symlinkBase = Files.createSymbolicLink(testsDir.toPath().resolve("basedirsymlink-" + System.currentTimeMillis()), jettyHome); + Path testsDir = MavenTestingUtils.getTargetTestingPath(); + Files.createSymbolicLink(testsDir.resolve("basedirsymlink-" + System.currentTimeMillis()), jettyHome); Map properties = new HashMap<>(); properties.put("jetty.home", jettyHome.toString()); //Start jetty, but this time running from the symlinked base @@ -215,7 +228,6 @@ public class WebAppProviderTest try { server.start(); - HandlerCollection handlers = (HandlerCollection)server.getHandler(); Handler[] children = server.getChildHandlersByClass(WebAppContext.class); assertEquals(1, children.length); assertEquals("/foo", ((WebAppContext)children[0]).getContextPath()); @@ -225,12 +237,143 @@ public class WebAppProviderTest server.stop(); } } - - private Map setupJettyProperties(Path jettyHome) + + @Test + public void testDelayedDeploy() throws Exception { + Path realBase = testdir.getEmptyPathDir(); + + //set jetty up on the real base + jetty = new XmlConfiguredJetty(realBase); + jetty.addConfiguration("jetty.xml"); + jetty.addConfiguration("jetty-http.xml"); + jetty.addConfiguration("jetty-deploy-wars.xml"); + + //Put a webapp into the base + jetty.copyWebapp("foo-webapp-1.war", "foo.war"); + + Path jettyHome = jetty.getJettyHome().toPath(); + Map properties = new HashMap<>(); - properties.put("jetty.home", jettyHome.toFile().getAbsolutePath()); - return properties; + properties.put("jetty.home", jettyHome.toString()); + properties.put("jetty.deploy.deferInitialScan", "true"); + //Start jetty, but this time running from the symlinked base + System.setProperty("jetty.home", properties.get("jetty.home")); + + List configurations = jetty.getConfigurations(); + Server server = XmlConfiguredJetty.loadConfigurations(configurations, properties); + + try + { + BlockingQueue eventQueue = new LinkedBlockingDeque<>(); + + LifeCycle.Listener eventCaptureListener = new LifeCycle.Listener() + { + @Override + public void lifeCycleStarted(LifeCycle event) + { + if (event instanceof Server) + { + eventQueue.add("Server started"); + } + if (event instanceof ScanningAppProvider) + { + eventQueue.add("ScanningAppProvider started"); + } + if (event instanceof Scanner) + { + eventQueue.add("Scanner started"); + } + } + }; + + server.addEventListener(eventCaptureListener); + + ScanningAppProvider scanningAppProvider = null; + DeploymentManager deploymentManager = server.getBean(DeploymentManager.class); + for (AppProvider appProvider : deploymentManager.getAppProviders()) + { + if (appProvider instanceof ScanningAppProvider) + { + scanningAppProvider = (ScanningAppProvider)appProvider; + } + } + assertNotNull(scanningAppProvider, "Should have found ScanningAppProvider"); + assertTrue(scanningAppProvider.isDeferInitialScan(), "The DeferInitialScan configuration should be true"); + + scanningAppProvider.addEventListener(eventCaptureListener); + scanningAppProvider.addEventListener(new Container.InheritedListener() + { + @Override + public void beanAdded(Container parent, Object child) + { + if (child instanceof Scanner) + { + Scanner scanner = (Scanner)child; + scanner.addEventListener(eventCaptureListener); + scanner.addListener(new Scanner.ScanCycleListener() + { + @Override + public void scanStarted(int cycle) throws Exception + { + eventQueue.add("Scan Started [" + cycle + "]"); + } + + @Override + public void scanEnded(int cycle) throws Exception + { + eventQueue.add("Scan Ended [" + cycle + "]"); + } + }); + } + } + + @Override + public void beanRemoved(Container parent, Object child) + { + // no-op + } + }); + + server.start(); + + // Wait till the webapp is deployed and started + await().atMost(Duration.ofSeconds(5)).until(() -> + { + Handler[] children = server.getChildHandlersByClass(WebAppContext.class); + if (children == null || children.length == 0) + return false; + WebAppContext webAppContext = (WebAppContext)children[0]; + if (webAppContext.isStarted()) + return webAppContext.getContextPath(); + return null; + }, is("/foo")); + + String[] expectedOrderedEvents = { + // The deepest component starts first + "Scanner started", + "ScanningAppProvider started", + "Server started", + // We should see scan events after the server has started + "Scan Started [1]", + "Scan Ended [1]", + "Scan Started [2]", + "Scan Ended [2]" + }; + + assertThat(eventQueue.size(), is(expectedOrderedEvents.length)); + // collect string array representing ACTUAL scan events (useful for meaningful error message on failed assertion) + String scanEventsStr = "[\"" + String.join("\", \"", eventQueue) + "\"]"; + for (int i = 0; i < expectedOrderedEvents.length; i++) + { + String event = eventQueue.poll(5, TimeUnit.SECONDS); + assertThat("Expected Event [" + i + "]: " + scanEventsStr, event, is(expectedOrderedEvents[i])); + } + } + finally + { + server.stop(); + } } private static boolean hasJettyGeneratedPath(File basedir, String expectedWarFilename) diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java index 60d36f28c88..41c15ad1b68 100644 --- a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/test/XmlConfiguredJetty.java @@ -42,6 +42,7 @@ import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.PathAssert; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.URIUtil; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.resource.PathResource; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.WebAppContext; @@ -69,46 +70,41 @@ public class XmlConfiguredJetty public static Server loadConfigurations(List configurations, Map properties) throws Exception { - XmlConfiguration last = null; - Object[] obj = new Object[configurations.size()]; + Map idMap = configure(configurations, properties); - // Configure everything - for (int i = 0; i < configurations.size(); i++) - { - Resource config = configurations.get(i); - XmlConfiguration configuration = new XmlConfiguration(config); - if (last != null) - configuration.getIdMap().putAll(last.getIdMap()); - configuration.getProperties().putAll(properties); - obj[i] = configuration.configure(); - last = configuration; - } + Server server = (Server)idMap.get("Server"); - // Test for Server Instance. - Server foundServer = null; - int serverCount = 0; - for (int i = 0; i < configurations.size(); i++) - { - if (obj[i] instanceof Server) - { - if (obj[i].equals(foundServer)) - { - // Identical server instance found - break; - } - foundServer = (Server)obj[i]; - serverCount++; - } - } - - if (serverCount <= 0) + if (server == null) { throw new Exception("Load failed to configure a " + Server.class.getName()); } - assertEquals(1, serverCount, "Server load count"); + return server; + } - return foundServer; + /** + * Configure for the list of XML Resources and Properties. + * + * @param xmls the xml resources (in order of execution) + * @param properties the properties to use with the XML + * @return the ID Map of configured objects (key is the id name in the XML, and the value is configured object) + * @throws Exception if unable to create objects or read XML + */ + public static Map configure(List xmls, Map properties) throws Exception + { + Map idMap = new HashMap<>(); + + // Configure everything + for (Resource xmlResource : xmls) + { + XmlConfiguration configuration = new XmlConfiguration(xmlResource); + configuration.getIdMap().putAll(idMap); + configuration.getProperties().putAll(properties); + configuration.configure(); + idMap.putAll(configuration.getIdMap()); + } + + return idMap; } public XmlConfiguredJetty(Path testdir) throws IOException @@ -424,6 +420,6 @@ public class XmlConfiguredJetty public void stop() throws Exception { - _server.stop(); + LifeCycle.stop(_server); } } diff --git a/jetty-deploy/src/test/resources/jetty-deploy-wars.xml b/jetty-deploy/src/test/resources/jetty-deploy-wars.xml index 8730443711b..6d66339e7d3 100644 --- a/jetty-deploy/src/test/resources/jetty-deploy-wars.xml +++ b/jetty-deploy/src/test/resources/jetty-deploy-wars.xml @@ -19,6 +19,7 @@ 1 /workish false + diff --git a/jetty-deploy/src/test/resources/jetty-logging.properties b/jetty-deploy/src/test/resources/jetty-logging.properties index ad1ad3a27ee..e9a87c1d3e3 100644 --- a/jetty-deploy/src/test/resources/jetty-logging.properties +++ b/jetty-deploy/src/test/resources/jetty-logging.properties @@ -2,4 +2,4 @@ #org.eclipse.jetty.deploy.DeploymentTempDirTest.LEVEL=DEBUG #org.eclipse.jetty.deploy.LEVEL=DEBUG #org.eclipse.jetty.webapp.LEVEL=DEBUG -#org.eclipse.jetty.util.Scanner=DEBUG +#org.eclipse.jetty.util.Scanner.LEVEL=DEBUG diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java index 483d9faabdf..c606a4c330e 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java @@ -70,6 +70,8 @@ public class Scanner extends ContainerLifeCycle private Map _prevScan; private FilenameFilter _filter; private final Map> _scannables = new ConcurrentHashMap<>(); + private boolean _autoStartScanning = true; + private boolean _scanningStarted = false; private boolean _reportExisting = true; private boolean _reportDirs = true; private Scheduler.Task _task; @@ -520,6 +522,36 @@ public class Scanner extends ContainerLifeCycle _scanDepth = scanDepth; } + /** + * Test if scanning should start automatically with {@code Scanner}.{@link #start()} + * + * @return true if scanning should start automatically, false to have scanning is deferred to a later manual call to {@link #startScanning()} + */ + public boolean isAutoStartScanning() + { + return _autoStartScanning; + } + + /** + * Flag to control scanning auto start feature. + * + *
    + *
  • {@code true} - to have scanning automatically start with the Scanner.{@link #start()}
  • + *
  • {@code false} - to have scanning deferred until a future call to {@link #startScanning()}
  • + *
+ * + *

+ * If choosing to defer the automatic scanning, a future call to {@link #startScanning()} + * is required to initiate this Scanner so that it can begin report files in the {@link #setScanDirs(List)} + *

+ * + * @param autostart true if scanning should start automatically, false to defer start of scanning to a later call to {@link #startScanning()} + */ + public void setAutoStartScanning(boolean autostart) + { + this._autoStartScanning = autostart; + } + /** * Whether or not an initial scan will report all files as being * added. @@ -587,8 +619,37 @@ public class Scanner extends ContainerLifeCycle public void doStart() throws Exception { if (LOG.isDebugEnabled()) - LOG.debug("Scanner start: rprtExists={}, depth={}, rprtDirs={}, interval={}, filter={}, scannables={}", - _reportExisting, _scanDepth, _reportDirs, _scanInterval, _filter, _scannables); + LOG.debug("Scanner start: autoStartScanning={}, reportExists={}, depth={}, rprtDirs={}, interval={}, filter={}, scannables={}", + isAutoStartScanning(), _reportExisting, _scanDepth, _reportDirs, _scanInterval, _filter, _scannables); + + // Start the scanner and managed beans (eg: the scheduler) + super.doStart(); + + if (isAutoStartScanning()) + { + startScanning(); + } + } + + /** + * Start scanning. + *

+ * This will perform the initial scan of the directories {@link #setScanDirs(List)} + * and schedule future scans, following all of the configuration + * of the scan (eg: {@link #setReportExistingFilesOnStartup(boolean)}) + *

+ */ + public void startScanning() + { + if (!isRunning()) + throw new IllegalStateException("Scanner not started"); + + if (_scanningStarted) + return; + _scanningStarted = true; + + if (LOG.isDebugEnabled()) + LOG.debug("{}.startup()", this.getClass().getSimpleName()); if (_reportExisting) { @@ -602,9 +663,7 @@ public class Scanner extends ContainerLifeCycle _prevScan = scanFiles(); } - super.doStart(); - - //schedule the scan + // schedule further scans schedule(); } @@ -624,6 +683,7 @@ public class Scanner extends ContainerLifeCycle _task = null; if (task != null) task.cancel(); + _scanningStarted = false; } /** @@ -712,7 +772,7 @@ public class Scanner extends ContainerLifeCycle } /** - * Scan all of the given paths. + * Scan all the given paths. */ private Map scanFiles() {