org.neo4j.server.AbstractNeoWebServer Maven / Gradle / Ivy
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.neo4j.server;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.eclipse.jetty.io.ArrayByteBufferPool;
import java.net.InetSocketAddress;
import java.net.URI;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import javax.ws.rs.ext.MessageBodyWriter;
import org.neo4j.bolt.dbapi.BoltGraphDatabaseManagementServiceSPI;
import org.neo4j.bolt.transaction.TransactionManager;
import org.neo4j.collection.Dependencies;
import org.neo4j.configuration.Config;
import org.neo4j.configuration.GraphDatabaseSettings;
import org.neo4j.configuration.connectors.ConnectorPortRegister;
import org.neo4j.configuration.connectors.HttpConnector;
import org.neo4j.configuration.connectors.HttpsConnector;
import org.neo4j.configuration.helpers.SocketAddress;
import org.neo4j.dbms.DatabaseStateService;
import org.neo4j.dbms.api.DatabaseManagementService;
import org.neo4j.io.fs.FileSystemAbstraction;
import org.neo4j.kernel.api.security.AuthManager;
import org.neo4j.kernel.availability.CompositeDatabaseAvailabilityGuard;
import org.neo4j.kernel.database.DefaultDatabaseResolver;
import org.neo4j.kernel.impl.factory.DbmsInfo;
import org.neo4j.kernel.internal.Version;
import org.neo4j.kernel.lifecycle.LifeSupport;
import org.neo4j.kernel.lifecycle.LifecycleAdapter;
import org.neo4j.logging.Log;
import org.neo4j.logging.LogProvider;
import org.neo4j.logging.internal.LogService;
import org.neo4j.memory.MemoryPool;
import org.neo4j.memory.MemoryPools;
import org.neo4j.scheduler.JobScheduler;
import org.neo4j.server.bind.ComponentsBinder;
import org.neo4j.server.configuration.ServerSettings;
import org.neo4j.server.http.HttpMemoryPool;
import org.neo4j.server.http.HttpTransactionMemoryPool;
import org.neo4j.server.http.cypher.HttpTransactionManager;
import org.neo4j.server.http.cypher.TransactionRegistry;
import org.neo4j.server.modules.ServerModule;
import org.neo4j.server.rest.repr.RepresentationBasedMessageBodyWriter;
import org.neo4j.server.web.RotatingRequestLog;
import org.neo4j.server.web.SimpleUriBuilder;
import org.neo4j.server.web.WebServer;
import org.neo4j.ssl.config.SslPolicyLoader;
import org.neo4j.time.Clocks;
import org.neo4j.time.SystemNanoClock;
import static java.lang.String.format;
import static java.util.stream.Collectors.toUnmodifiableList;
import static org.neo4j.configuration.GraphDatabaseSettings.db_timezone;
import static org.neo4j.configuration.ssl.SslPolicyScope.HTTPS;
import static org.neo4j.server.configuration.ServerSettings.http_log_format;
import static org.neo4j.server.configuration.ServerSettings.http_log_path;
import static org.neo4j.server.configuration.ServerSettings.http_logging_enabled;
import static org.neo4j.server.configuration.ServerSettings.http_logging_rotation_keep_number;
import static org.neo4j.server.configuration.ServerSettings.http_logging_rotation_size;
public abstract class AbstractNeoWebServer extends LifecycleAdapter implements NeoWebServer
{
private static final long MINIMUM_TIMEOUT = 1000L;
/**
* We add a second to the timeout if the user configures a 1-second timeout.
*
* This ensures the expiry time displayed to the user is always at least 1 second, even after it is rounded down.
*/
private static final long ROUNDING_SECOND = 1000L;
static final String NEO4J_IS_STARTING_MESSAGE = "======== Neo4j " + Version.getNeo4jVersion() + " ========";
protected final LogProvider userLogProvider;
private final Log log;
private final DbmsInfo dbmsInfo;
private final MemoryPool requestMemoryPool;
private final MemoryPool transactionMemoryPool;
private final List serverModules = new ArrayList<>();
private final List authWhitelist;
private final DatabaseManagementService databaseManagementService;
private final Dependencies globalDependencies;
private final Config config;
private final TransactionManager transactionManager;
private final BoltGraphDatabaseManagementServiceSPI boltSPI;
private final LifeSupport life = new LifeSupport();
private final boolean httpEnabled;
private final boolean httpsEnabled;
private SocketAddress httpListenAddress;
private SocketAddress httpsListenAddress;
private SocketAddress httpAdvertisedAddress;
private SocketAddress httpsAdvertisedAddress;
protected WebServer webServer;
protected Supplier authManagerSupplier;
protected ArrayByteBufferPool byteBufferPool;
private final Supplier sslPolicyFactorySupplier;
private final HttpTransactionManager httpTransactionManager;
private final CompositeDatabaseAvailabilityGuard globalAvailabilityGuard;
protected final SystemNanoClock clock;
protected ConnectorPortRegister connectorPortRegister;
private RotatingRequestLog requestLog;
protected abstract Iterable createServerModules();
protected abstract WebServer createWebServer();
public AbstractNeoWebServer( DatabaseManagementService databaseManagementService, Dependencies globalDependencies, Config config,
LogProvider userLogProvider, DbmsInfo dbmsInfo, MemoryPools memoryPools, TransactionManager transactionManager,
SystemNanoClock clock )
{
this.databaseManagementService = databaseManagementService;
this.globalDependencies = globalDependencies;
this.transactionManager = transactionManager;
this.config = config;
this.userLogProvider = userLogProvider;
this.log = userLogProvider.getLog( getClass() );
this.dbmsInfo = dbmsInfo;
this.clock = clock;
log.info( NEO4J_IS_STARTING_MESSAGE );
byteBufferPool = new ArrayByteBufferPool();
requestMemoryPool = new HttpMemoryPool( memoryPools, byteBufferPool );
life.add( new MemoryPoolLifecycleAdapter( requestMemoryPool ) );
transactionMemoryPool = new HttpTransactionMemoryPool( memoryPools );
life.add( new MemoryPoolLifecycleAdapter( transactionMemoryPool ) );
verifyConnectorsConfiguration( config );
httpEnabled = config.get( HttpConnector.enabled );
if ( httpEnabled )
{
httpListenAddress = config.get( HttpConnector.listen_address );
httpAdvertisedAddress = config.get( HttpConnector.advertised_address );
}
httpsEnabled = config.get( HttpsConnector.enabled );
if ( httpsEnabled )
{
httpsListenAddress = config.get( HttpsConnector.listen_address );
httpsAdvertisedAddress = config.get( HttpsConnector.advertised_address );
}
this.authWhitelist = parseAuthWhitelist( config );
authManagerSupplier = globalDependencies.provideDependency( AuthManager.class );
boltSPI = globalDependencies.resolveDependency( BoltGraphDatabaseManagementServiceSPI.class );
sslPolicyFactorySupplier = globalDependencies.provideDependency( SslPolicyLoader.class );
connectorPortRegister = globalDependencies.resolveDependency( ConnectorPortRegister.class );
httpTransactionManager = createHttpTransactionManager();
globalAvailabilityGuard = globalDependencies.resolveDependency( CompositeDatabaseAvailabilityGuard.class );
life.add( new ServerComponentsLifecycleAdapter() );
}
protected Dependencies getGlobalDependencies()
{
return globalDependencies;
}
@Override
public DbmsInfo getDbmsInfo()
{
return dbmsInfo;
}
@Override
public void init()
{
life.init();
}
@Override
public void start() throws ServerStartupException
{
try
{
life.start();
}
catch ( Throwable t )
{
// If the database has been started, attempt to cleanly shut it down to avoid unclean shutdowns.
life.shutdown();
var rootCause = ExceptionUtils.getRootCause( t );
throw new ServerStartupException( format( "Starting Neo4j failed: %s", rootCause.getMessage() ), rootCause );
}
}
private HttpTransactionManager createHttpTransactionManager()
{
JobScheduler jobScheduler = globalDependencies.resolveDependency( JobScheduler.class );
Clock clock = Clocks.systemClock();
Duration transactionTimeout = getTransactionTimeout();
var readByDefault = config.get( GraphDatabaseSettings.mode ) != GraphDatabaseSettings.Mode.SINGLE &&
!config.get( GraphDatabaseSettings.routing_enabled )
;
return new HttpTransactionManager( databaseManagementService, transactionMemoryPool, jobScheduler, clock, transactionTimeout,
userLogProvider, transactionManager, boltSPI, authManagerSupplier.get(), readByDefault );
}
/**
* We are going to ensure the minimum timeout is 2 seconds. The timeout value is communicated to the user in
* seconds rounded down, meaning if a user set a 1 second timeout, he would be told there was less than 1 second
* remaining before he would need to renew the timeout.
*/
private Duration getTransactionTimeout()
{
final long timeout = config.get( ServerSettings.transaction_idle_timeout ).toMillis();
return Duration.ofMillis( Math.max( timeout, MINIMUM_TIMEOUT + ROUNDING_SECOND ) );
}
/**
* Use this method to register server modules from subclasses
*/
private void registerModule( ServerModule module )
{
serverModules.add( module );
}
private void startModules()
{
for ( ServerModule module : serverModules )
{
module.start();
}
}
private void stopModules()
{
final List errors = new ArrayList<>();
for ( final ServerModule module : serverModules )
{
try
{
module.stop();
}
catch ( Exception e )
{
errors.add( e );
}
}
if ( !errors.isEmpty() )
{
final RuntimeException e = new RuntimeException();
errors.forEach( e::addSuppressed );
throw e;
}
}
private void clearModules()
{
serverModules.clear();
}
@Override
public Config getConfig()
{
return config;
}
protected void configureWebServer()
{
webServer.setHttpAddress( httpListenAddress );
webServer.setHttpsAddress( httpsListenAddress );
webServer.setMaxThreads( config.get( ServerSettings.webserver_max_threads ) );
webServer.setWadlEnabled( config.get( ServerSettings.wadl_enabled ) );
webServer.setComponentsBinder( createComponentsBinder() );
if ( httpsEnabled ) // only load sslPolicy when encryption is enabled
{
SslPolicyLoader sslPolicyLoader = sslPolicyFactorySupplier.get();
if ( sslPolicyLoader.hasPolicyForSource( HTTPS ) )
{
webServer.setSslPolicy( sslPolicyLoader.getPolicy( HTTPS ) );
}
}
}
protected void startWebServer() throws Exception
{
try
{
setUpHttpLogging();
webServer.start();
registerHttpAddressAfterStartup();
registerHttpsAddressAfterStartup();
log.info( "Remote interface available at %s", getBaseUri() );
}
catch ( Exception e )
{
SocketAddress address = httpListenAddress != null ? httpListenAddress : httpsListenAddress;
log.error( "Failed to start Neo4j on %s: %s", address, e.getMessage() );
throw e;
}
}
private void registerHttpAddressAfterStartup()
{
if ( httpEnabled )
{
InetSocketAddress localHttpAddress = webServer.getLocalHttpAddress();
connectorPortRegister.register( HttpConnector.NAME, localHttpAddress );
if ( httpAdvertisedAddress.getPort() == 0 )
{
httpAdvertisedAddress = new SocketAddress( localHttpAddress.getHostString(), localHttpAddress.getPort() );
}
}
}
private void registerHttpsAddressAfterStartup()
{
if ( httpsEnabled )
{
InetSocketAddress localHttpsAddress = webServer.getLocalHttpsAddress();
connectorPortRegister.register( HttpsConnector.NAME, localHttpsAddress );
if ( httpsAdvertisedAddress.getPort() == 0 )
{
httpsAdvertisedAddress = new SocketAddress( localHttpsAddress.getHostString(), localHttpsAddress.getPort() );
}
}
}
private void setUpHttpLogging()
{
if ( !getConfig().get( http_logging_enabled ) )
{
return;
}
requestLog = new RotatingRequestLog(
globalDependencies.resolveDependency( FileSystemAbstraction.class ),
config.get( db_timezone ),
config.get( http_log_path ).toString(),
config.get( http_logging_rotation_size ),
config.get( http_logging_rotation_keep_number ),
config.get( http_log_format ) );
webServer.setRequestLog( requestLog );
}
protected List getUriWhitelist()
{
return authWhitelist;
}
@Override
public void stop()
{
shutdownGlobalAvailabilityGuard();
life.stop();
}
private void shutdownGlobalAvailabilityGuard()
{
try
{
// Although the globalGuard availability guard is shutdown as part of LifeSupport#stop(), we never hit that if we're
// blocking in LifeSupport#start() and the blocked starting components may be using this guard as a bail out signal
if ( globalAvailabilityGuard != null )
{
globalAvailabilityGuard.stop();
}
}
catch ( Throwable t )
{
// Not much we can do other than log - we're trying to shutdown anyway
log.error( "Failed to set the global availability guard to shutdown in the process of stopping the Neo4j server", t );
}
}
private void stopWebServer() throws Exception
{
if ( webServer != null )
{
webServer.stop();
}
if ( requestLog != null )
{
requestLog.stop();
}
}
@Override
public TransactionRegistry getTransactionRegistry()
{
return httpTransactionManager.getTransactionRegistry();
}
@Override
public URI getBaseUri()
{
return httpAdvertisedAddress != null
? SimpleUriBuilder.buildURI( httpAdvertisedAddress, false )
: SimpleUriBuilder.buildURI( httpsAdvertisedAddress, true );
}
@Override
public Optional httpsUri()
{
return Optional.ofNullable( httpsAdvertisedAddress )
.map( address -> SimpleUriBuilder.buildURI( address, true ) );
}
public WebServer getWebServer()
{
return webServer;
}
private ComponentsBinder createComponentsBinder()
{
ComponentsBinder binder = new ComponentsBinder();
var databaseStateService = getGlobalDependencies().resolveDependency( DatabaseStateService.class );
var databaseResolver = getGlobalDependencies().resolveDependency( DefaultDatabaseResolver.class );
binder.addSingletonBinding( databaseManagementService, DatabaseManagementService.class );
binder.addSingletonBinding( databaseStateService, DatabaseStateService.class );
binder.addSingletonBinding( this, NeoWebServer.class );
binder.addSingletonBinding( getConfig(), Config.class );
binder.addSingletonBinding( transactionMemoryPool, MemoryPool.class );
binder.addSingletonBinding( getWebServer(), WebServer.class );
binder.bind( RepresentationBasedMessageBodyWriter.class ).to( MessageBodyWriter.class );
binder.addSingletonBinding( httpTransactionManager, HttpTransactionManager.class );
binder.addSingletonBinding( databaseResolver, DefaultDatabaseResolver.class );
binder.addLazyBinding( authManagerSupplier, AuthManager.class );
binder.addSingletonBinding( userLogProvider, LogProvider.class );
binder.addSingletonBinding( userLogProvider.getLog( NeoWebServer.class ), Log.class );
return binder;
}
private static void verifyConnectorsConfiguration( Config config )
{
boolean httpAndHttpsDisabled = !config.get( HttpConnector.enabled ) && !config.get( HttpsConnector.enabled );
if ( httpAndHttpsDisabled )
{
throw new IllegalArgumentException( "Either HTTP or HTTPS connector must be configured to run the server" );
}
}
private static List parseAuthWhitelist( Config config )
{
return config.get( ServerSettings.http_auth_allowlist )
.stream()
.map( Pattern::compile )
.collect( toUnmodifiableList() );
}
private class ServerComponentsLifecycleAdapter extends LifecycleAdapter
{
@Override
public void init()
{
webServer = createWebServer();
for ( ServerModule moduleClass : createServerModules() )
{
registerModule( moduleClass );
}
}
@Override
public void start() throws Exception
{
LogService logService = globalDependencies.resolveDependency( LogService.class );
Log serverLog = logService.getInternalLog( ServerComponentsLifecycleAdapter.class );
serverLog.info( "Starting web server" );
configureWebServer();
startModules();
startWebServer();
serverLog.info( "Web server started." );
}
@Override
public void stop() throws Exception
{
stopWebServer();
stopModules();
clearModules();
}
}
private class MemoryPoolLifecycleAdapter extends LifecycleAdapter
{
private final MemoryPool memoryPool;
private MemoryPoolLifecycleAdapter( MemoryPool memoryPool )
{
this.memoryPool = memoryPool;
}
@Override
public void shutdown() throws Exception
{
memoryPool.free();
}
}
}