Add configuration to allow deferring the initial Deployment until after Server is started (#10667)

* Delayed Deployment configuration until after Server is started.

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2023-10-17 06:25:01 -05:00 committed by GitHub
parent 536a38aa8e
commit 909e99ec37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 334 additions and 70 deletions

View File

@ -59,5 +59,9 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -53,6 +53,7 @@
</Default>
</Property>
</Set>
<Set name="deferInitialScan"><Property name="jetty.deploy.deferInitialScan" default="false"/></Set>
<Set name="scanInterval"><Property name="jetty.deploy.scanInterval" default="1"/></Set>
<Set name="extractWars"><Property name="jetty.deploy.extractWars" default="true"/></Set>
<Set name="configurationManager">

View File

@ -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

View File

@ -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.
*
* <ul>
* <li>{@code true} - to have initial scan deferred until the {@link Server} component
* has reached it's STARTED state.<br>
* Note: any failures in a deploy will not fail the Server startup in this mode.</li>
* <li>{@code false} - (default value) to have initial scan occur as normal on
* ScanningAppProvider startup.</li>
* </ul>
*
* @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;

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<Resource> configurations = jetty.getConfigurations();
Server server = XmlConfiguredJetty.loadConfigurations(configurations, properties);
try
{
BlockingQueue<String> 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)

View File

@ -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<Resource> configurations, Map<String, String> properties)
throws Exception
{
XmlConfiguration last = null;
Object[] obj = new Object[configurations.size()];
Map<String, Object> 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<String, Object> configure(List<Resource> xmls, Map<String, String> properties) throws Exception
{
Map<String, Object> 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);
}
}

View File

@ -19,6 +19,7 @@
<Set name="scanInterval">1</Set>
<Set name="tempDir"><Property name="jetty.home" default="target" />/workish</Set>
<Set name="useRealPaths">false</Set>
<Set name="deferInitialScan"><Property name="jetty.deploy.deferInitialScan" default="false"/></Set>
</New>
</Item>
</Array>

View File

@ -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

View File

@ -70,6 +70,8 @@ public class Scanner extends ContainerLifeCycle
private Map<Path, MetaData> _prevScan;
private FilenameFilter _filter;
private final Map<Path, IncludeExcludeSet<PathMatcher, Path>> _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.
*
* <ul>
* <li>{@code true} - to have scanning automatically start with the Scanner.{@link #start()}</li>
* <li>{@code false} - to have scanning deferred until a future call to {@link #startScanning()}</li>
* </ul>
*
* <p>
* 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)}
* </p>
*
* @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.
* <p>
* 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)})
* </p>
*/
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<Path, MetaData> scanFiles()
{