org.eclipse.jetty.deploy.DeploymentManager Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.deploy;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.eclipse.jetty.deploy.bindings.StandardDeployer;
import org.eclipse.jetty.deploy.bindings.StandardStarter;
import org.eclipse.jetty.deploy.bindings.StandardStopper;
import org.eclipse.jetty.deploy.bindings.StandardUndeployer;
import org.eclipse.jetty.deploy.graph.Edge;
import org.eclipse.jetty.deploy.graph.Node;
import org.eclipse.jetty.deploy.graph.Route;
import org.eclipse.jetty.server.Deployable;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.util.ExceptionUtil;
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.annotation.Name;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.Environment;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The Deployment Manager.
*
* Responsibilities:
*
*
*
* - Tracking Apps and their LifeCycle Location
* - Managing AppProviders and the Apps that they provide.
* - Executing AppLifeCycle on App based on current and desired LifeCycle Location.
*
*
*
*/
@ManagedObject("Deployment Manager")
public class DeploymentManager extends ContainerLifeCycle
{
private static final Logger LOG = LoggerFactory.getLogger(DeploymentManager.class);
/**
* Represents a single tracked app within the deployment manager.
*/
public class AppEntry
{
/**
* Version of the app.
*
* Note: Auto-increments on each {@link DeploymentManager#addApp(App)}
*/
private int version;
/**
* The app being tracked.
*/
private App app;
/**
* The lifecycle node location of this App
*/
private Node lifecyleNode;
/**
* Tracking the various AppState timestamps (in system milliseconds)
*/
private Map stateTimestamps = new HashMap();
public App getApp()
{
return app;
}
public Node getLifecyleNode()
{
return lifecyleNode;
}
public Map getStateTimestamps()
{
return stateTimestamps;
}
public int getVersion()
{
return version;
}
void setLifeCycleNode(Node node)
{
this.lifecyleNode = node;
this.stateTimestamps.put(node, System.currentTimeMillis());
}
}
private final AutoLock _lock = new AutoLock();
private Throwable _onStartupErrors;
private final List _providers = new ArrayList<>();
private final AppLifeCycle _lifecycle = new AppLifeCycle();
private final Queue _apps = new ConcurrentLinkedQueue();
private ContextHandlerCollection _contexts;
private boolean _useStandardBindings = true;
private String _defaultLifeCycleGoal = AppLifeCycle.STARTED;
/**
* Get the default {@link Environment} name for deployed applications, which is
* the maximal name when using the {@link Deployable#ENVIRONMENT_COMPARATOR}.
* @return The default {@link Environment} name or null.
*/
public String getDefaultEnvironmentName()
{
return _providers.stream()
.map(AppProvider::getEnvironmentName)
.max(Deployable.ENVIRONMENT_COMPARATOR)
.orElse(null);
}
/**
* Receive an app for processing.
*
* Most commonly used by the various {@link AppProvider} implementations.
*
* @param app the app
*/
public void addApp(App app)
{
LOG.info("addApp: {}", app);
AppEntry entry = new AppEntry();
entry.app = app;
entry.setLifeCycleNode(_lifecycle.getNodeByName("undeployed"));
_apps.add(entry);
if (isRunning() && _defaultLifeCycleGoal != null)
{
// Immediately attempt to go to default lifecycle state
this.requestAppGoal(entry, _defaultLifeCycleGoal);
}
}
/**
* Set the AppProviders.
* The providers passed are added via {@link #addBean(Object)} so that
* their lifecycles may be managed as a {@link ContainerLifeCycle}.
*
* @param providers the app provider list
*/
public void setAppProviders(Collection providers)
{
if (isRunning())
throw new IllegalStateException();
_providers.clear();
removeBeans();
for (AppProvider provider : providers)
{
if (_providers.add(provider))
addBean(provider, true);
}
}
public boolean hasAppProviderFor(String environmentName)
{
return environmentName != null && getAppProviders().stream()
.map(AppProvider::getEnvironmentName).anyMatch(environmentName::equalsIgnoreCase);
}
public Collection getAppProviders()
{
return Collections.unmodifiableList(_providers);
}
public void addAppProvider(AppProvider provider)
{
if (isRunning())
throw new IllegalStateException();
_providers.add(provider);
addBean(provider, true);
}
public void setLifeCycleBindings(Collection bindings)
{
if (isRunning())
throw new IllegalStateException();
for (AppLifeCycle.Binding b : _lifecycle.getBindings())
{
_lifecycle.removeBinding(b);
}
for (AppLifeCycle.Binding b : bindings)
{
_lifecycle.addBinding(b);
}
}
public Collection getLifeCycleBindings()
{
return Collections.unmodifiableSet(_lifecycle.getBindings());
}
public void addLifeCycleBinding(AppLifeCycle.Binding binding)
{
_lifecycle.addBinding(binding);
}
/**
* Convenience method to allow for insertion of nodes into the lifecycle.
*
* @param existingFromNodeName the existing node start
* @param existingToNodeName the existing node end
* @param insertedNodeName the new node to create between the existing nodes
*/
public void insertLifeCycleNode(String existingFromNodeName, String existingToNodeName, String insertedNodeName)
{
Node fromNode = _lifecycle.getNodeByName(existingFromNodeName);
Node toNode = _lifecycle.getNodeByName(existingToNodeName);
Edge edge = new Edge(fromNode, toNode);
_lifecycle.insertNode(edge, insertedNodeName);
}
@Override
protected void doStart() throws Exception
{
if (getContexts() == null)
throw new IllegalStateException("No Contexts");
if (_useStandardBindings)
{
LOG.debug("DeploymentManager using standard bindings");
addLifeCycleBinding(new StandardDeployer());
addLifeCycleBinding(new StandardStarter());
addLifeCycleBinding(new StandardStopper());
addLifeCycleBinding(new StandardUndeployer());
}
// Start all of the AppProviders
for (AppProvider provider : _providers)
{
startAppProvider(provider);
}
try (AutoLock l = _lock.lock())
{
ExceptionUtil.ifExceptionThrow(_onStartupErrors);
}
super.doStart();
}
@Override
protected void doStop() throws Exception
{
// Stop all of the AppProviders
for (AppProvider provider : _providers)
{
try
{
provider.stop();
}
catch (Exception e)
{
LOG.warn("Unable to start AppProvider", e);
}
}
super.doStop();
}
private AppEntry findApp(String appId)
{
if (appId == null)
return null;
for (AppEntry entry : _apps)
{
Path path = entry.app.getPath();
if (appId.equals(path.getName(path.getNameCount() - 1).toString()))
return entry;
}
return null;
}
private AppEntry findApp(Path path)
{
if (path == null)
return null;
for (AppEntry entry : _apps)
{
if (path.equals(entry.app.getPath()))
{
return entry;
}
}
return null;
}
public App getApp(String appId)
{
AppEntry entry = findApp(appId);
return entry == null ? null : entry.getApp();
}
public App getApp(Path path)
{
AppEntry entry = findApp(path);
if (entry == null)
return null;
return entry.app;
}
public Collection getAppEntries()
{
return Collections.unmodifiableCollection(_apps);
}
public Collection getApps()
{
List ret = new ArrayList<>();
for (AppEntry entry : _apps)
{
ret.add(entry.app);
}
return ret;
}
/**
* Get Set of {@link App}s by {@link Node}
*
* @param node the node to look for.
* @return the collection of apps for the node
*/
public Collection getApps(Node node)
{
Objects.requireNonNull(node);
List ret = new ArrayList<>();
for (AppEntry entry : _apps)
{
if (node.equals(entry.lifecyleNode))
{
ret.add(entry.app);
}
}
return ret;
}
public Collection getApps(String nodeName)
{
return getApps(_lifecycle.getNodeByName(nodeName));
}
public List getAppsWithSameContext(App app)
{
List ret = new ArrayList();
if (app == null)
{
return ret;
}
String contextId = app.getContextPath();
if (contextId == null)
{
// No context? Likely not deployed or started yet.
return ret;
}
for (AppEntry entry : _apps)
{
if (entry.app.equals(app))
{
// Its the input app. skip it.
// TODO: is this filter needed?
continue;
}
if (contextId.equals(entry.app.getContextPath()))
{
ret.add(entry.app);
}
}
return ret;
}
@ManagedAttribute("Deployed Contexts")
public ContextHandlerCollection getContexts()
{
return _contexts;
}
public String getDefaultLifeCycleGoal()
{
return _defaultLifeCycleGoal;
}
public AppLifeCycle getLifeCycle()
{
return _lifecycle;
}
public Server getServer()
{
if (_contexts == null)
{
return null;
}
return _contexts.getServer();
}
/**
* Remove the app from the tracking of the DeploymentManager
*
* @param app if the app is Unavailable remove it from the deployment manager.
*/
public void removeApp(App app)
{
LOG.info("removeApp: {}", app);
Iterator it = _apps.iterator();
while (it.hasNext())
{
AppEntry entry = it.next();
if (entry.app.equals(app))
{
if (!AppLifeCycle.UNDEPLOYED.equals(entry.lifecyleNode.getName()))
requestAppGoal(entry.app, AppLifeCycle.UNDEPLOYED);
it.remove();
}
}
}
public void removeAppProvider(AppProvider provider)
{
if (_providers.remove(provider))
removeBean(provider);
try
{
provider.stop();
}
catch (Exception e)
{
LOG.warn("Unable to stop Provider", e);
}
}
/**
* Move an {@link App} through the {@link AppLifeCycle} to the desired {@link Node}, executing each lifecycle step
* in the process to reach the desired state.
*
* @param app the app to move through the process
* @param nodeName the name of the node to attain
*/
public void requestAppGoal(App app, String nodeName)
{
AppEntry appentry = findApp(app.getPath());
if (appentry == null)
{
throw new IllegalStateException("App not being tracked by Deployment Manager: " + app);
}
requestAppGoal(appentry, nodeName);
}
/**
* Move an {@link App} through the {@link AppLifeCycle} to the desired {@link Node}, executing each lifecycle step
* in the process to reach the desired state.
*
* @param appentry the internal appentry to move through the process
* @param nodeName the name of the node to attain
*/
private void requestAppGoal(AppEntry appentry, String nodeName)
{
Node destinationNode = _lifecycle.getNodeByName(nodeName);
if (destinationNode == null)
{
throw new IllegalStateException("Node not present in Deployment Manager: " + nodeName);
}
// Compute lifecycle steps
Route path = _lifecycle.getPath(appentry.lifecyleNode, destinationNode);
if (path.isEmpty())
{
// nothing to do. already there.
return;
}
// Execute each Node binding. Stopping at any thrown exception.
try
{
Iterator it = path.getNodes().iterator();
if (it.hasNext()) // Any entries?
{
// The first entry in the path is always the start node
// We don't want to run bindings on that entry (again)
it.next(); // skip first entry
while (it.hasNext())
{
Node node = it.next();
LOG.debug("Executing Node {}", node);
_lifecycle.runBindings(node, appentry.app, this);
appentry.setLifeCycleNode(node);
}
}
}
catch (Throwable t)
{
LOG.warn("Unable to reach node goal: {}", nodeName, t);
// migrate to FAILED node
Node failed = _lifecycle.getNodeByName(AppLifeCycle.FAILED);
appentry.setLifeCycleNode(failed);
try
{
_lifecycle.runBindings(failed, appentry.app, this);
}
catch (Throwable ignore)
{
// The runBindings failed for 'failed' node
LOG.trace("IGNORED", ignore);
}
if (isStarting())
{
addOnStartupError(t);
}
}
}
private void addOnStartupError(Throwable cause)
{
try (AutoLock l = _lock.lock())
{
_onStartupErrors = ExceptionUtil.combine(_onStartupErrors, cause);
}
}
/**
* Move an {@link App} through the {@link AppLifeCycle} to the desired {@link Node}, executing each lifecycle step
* in the process to reach the desired state.
*
* @param appId the id of the app to move through the process
* @param nodeName the name of the node to attain
*/
@ManagedOperation(value = "request the app to be moved to the specified lifecycle node", impact = "ACTION")
public void requestAppGoal(@Name("appId") String appId, @Name("nodeName") String nodeName)
{
AppEntry appentry = findApp(appId);
if (appentry == null)
{
throw new IllegalStateException("App not being tracked by Deployment Manager: " + appId);
}
requestAppGoal(appentry, nodeName);
}
public void setContexts(ContextHandlerCollection contexts)
{
this._contexts = contexts;
}
public void setDefaultLifeCycleGoal(String defaultLifeCycleState)
{
this._defaultLifeCycleGoal = defaultLifeCycleState;
}
private void startAppProvider(AppProvider provider)
{
try
{
provider.setDeploymentManager(this);
provider.start();
}
catch (Exception e)
{
LOG.warn("Unable to start AppProvider", e);
}
}
public void undeployAll()
{
LOG.debug("Undeploy All");
for (AppEntry appentry : _apps)
{
requestAppGoal(appentry, "undeployed");
}
}
public boolean isUseStandardBindings()
{
return _useStandardBindings;
}
public void setUseStandardBindings(boolean useStandardBindings)
{
this._useStandardBindings = useStandardBindings;
}
public Collection getNodes()
{
return _lifecycle.getNodes();
}
}