org.apache.flink.runtime.blob.BlobServer Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.flink.runtime.blob;
import org.apache.flink.api.common.JobID;
import org.apache.flink.configuration.ConfigConstants;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.runtime.jobmanager.HighAvailabilityMode;
import org.apache.flink.runtime.net.SSLUtils;
import org.apache.flink.util.ExceptionUtils;
import org.apache.flink.util.FileUtils;
import org.apache.flink.util.NetUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static org.apache.flink.util.Preconditions.checkArgument;
import static org.apache.flink.util.Preconditions.checkNotNull;
/**
* This class implements the BLOB server. The BLOB server is responsible for listening for incoming requests and
* spawning threads to handle these requests. Furthermore, it takes care of creating the directory structure to store
* the BLOBs or temporarily cache them.
*/
public class BlobServer extends Thread implements BlobService {
/** The log object used for debugging. */
private static final Logger LOG = LoggerFactory.getLogger(BlobServer.class);
/** Counter to generate unique names for temporary files. */
private final AtomicInteger tempFileCounter = new AtomicInteger(0);
/** The server socket listening for incoming connections. */
private final ServerSocket serverSocket;
/** The SSL server context if ssl is enabled for the connections */
private SSLContext serverSSLContext = null;
/** Blob Server configuration */
private final Configuration blobServiceConfiguration;
/** Indicates whether a shutdown of server component has been requested. */
private final AtomicBoolean shutdownRequested = new AtomicBoolean();
/** Root directory for local file storage */
private final File storageDir;
/** Blob store for distributed file storage, e.g. in HA */
private final BlobStore blobStore;
/** Set of currently running threads */
private final Set activeConnections = new HashSet<>();
/** The maximum number of concurrent connections */
private final int maxConnections;
/** Lock guarding concurrent file accesses */
private final ReadWriteLock readWriteLock;
/**
* Shutdown hook thread to ensure deletion of the storage directory (or null
if
* the configured high availability mode does not equal{@link HighAvailabilityMode#NONE})
*/
private final Thread shutdownHook;
/**
* Instantiates a new BLOB server and binds it to a free network port.
*
* @param config Configuration to be used to instantiate the BlobServer
* @param blobStore BlobStore to store blobs persistently
*
* @throws IOException
* thrown if the BLOB server cannot bind to a free network port or if the
* (local or distributed) file storage cannot be created or is not usable
*/
public BlobServer(Configuration config, BlobStore blobStore) throws IOException {
this.blobServiceConfiguration = checkNotNull(config);
this.blobStore = checkNotNull(blobStore);
this.readWriteLock = new ReentrantReadWriteLock();
// configure and create the storage directory
String storageDirectory = config.getString(ConfigConstants.BLOB_STORAGE_DIRECTORY_KEY, null);
this.storageDir = BlobUtils.initStorageDirectory(storageDirectory);
LOG.info("Created BLOB server storage directory {}", storageDir);
// configure the maximum number of concurrent connections
final int maxConnections = config.getInteger(
ConfigConstants.BLOB_FETCH_CONCURRENT_KEY, ConfigConstants.DEFAULT_BLOB_FETCH_CONCURRENT);
if (maxConnections >= 1) {
this.maxConnections = maxConnections;
}
else {
LOG.warn("Invalid value for maximum connections in BLOB server: {}. Using default value of {}",
maxConnections, ConfigConstants.DEFAULT_BLOB_FETCH_CONCURRENT);
this.maxConnections = ConfigConstants.DEFAULT_BLOB_FETCH_CONCURRENT;
}
// configure the backlog of connections
int backlog = config.getInteger(ConfigConstants.BLOB_FETCH_BACKLOG_KEY, ConfigConstants.DEFAULT_BLOB_FETCH_BACKLOG);
if (backlog < 1) {
LOG.warn("Invalid value for BLOB connection backlog: {}. Using default value of {}",
backlog, ConfigConstants.DEFAULT_BLOB_FETCH_BACKLOG);
backlog = ConfigConstants.DEFAULT_BLOB_FETCH_BACKLOG;
}
this.shutdownHook = BlobUtils.addShutdownHook(this, LOG);
if (config.getBoolean(ConfigConstants.BLOB_SERVICE_SSL_ENABLED,
ConfigConstants.DEFAULT_BLOB_SERVICE_SSL_ENABLED)) {
try {
serverSSLContext = SSLUtils.createSSLServerContext(config);
} catch (Exception e) {
throw new IOException("Failed to initialize SSLContext for the blob server", e);
}
}
// ----------------------- start the server -------------------
String serverPortRange = config.getString(ConfigConstants.BLOB_SERVER_PORT, ConfigConstants.DEFAULT_BLOB_SERVER_PORT);
Iterator ports = NetUtils.getPortRangeFromString(serverPortRange);
final int finalBacklog = backlog;
ServerSocket socketAttempt = NetUtils.createSocketFromPorts(ports, new NetUtils.SocketFactory() {
@Override
public ServerSocket createSocket(int port) throws IOException {
if (serverSSLContext == null) {
return new ServerSocket(port, finalBacklog);
} else {
LOG.info("Enabling ssl for the blob server");
return serverSSLContext.getServerSocketFactory().createServerSocket(port, finalBacklog);
}
}
});
if(socketAttempt == null) {
throw new IOException("Unable to allocate socket for blob server in specified port range: "+serverPortRange);
} else {
SSLUtils.setSSLVerAndCipherSuites(socketAttempt, config);
this.serverSocket = socketAttempt;
}
// start the server thread
setName("BLOB Server listener at " + getPort());
setDaemon(true);
start();
if (LOG.isInfoEnabled()) {
LOG.info("Started BLOB server at {}:{} - max concurrent requests: {} - max backlog: {}",
serverSocket.getInetAddress().getHostAddress(), getPort(), maxConnections, backlog);
}
}
// --------------------------------------------------------------------------------------------
// Path Accessors
// --------------------------------------------------------------------------------------------
/**
* Returns a file handle to the file associated with the given blob key on the blob
* server.
*
* This is only called from the {@link BlobServerConnection}
*
* @param key identifying the file
* @return file handle to the file
*/
File getStorageLocation(BlobKey key) {
return BlobUtils.getStorageLocation(storageDir, key);
}
/**
* Returns a file handle to the file identified by the given jobID and key.
*
*
This is only called from the {@link BlobServerConnection}
*
* @param jobID to which the file is associated
* @param key to identify the file within the job context
* @return file handle to the file
*/
File getStorageLocation(JobID jobID, String key) {
return BlobUtils.getStorageLocation(storageDir, jobID, key);
}
/**
* Method which deletes all files associated with the given jobID.
*
*
This is only called from the {@link BlobServerConnection}
*
* @param jobID all files associated to this jobID will be deleted
* @throws IOException
*/
void deleteJobDirectory(JobID jobID) throws IOException {
BlobUtils.deleteJobDirectory(storageDir, jobID);
}
/**
* Returns a temporary file inside the BLOB server's incoming directory.
*
* @return a temporary file inside the BLOB server's incoming directory
*/
File createTemporaryFilename() {
return new File(BlobUtils.getIncomingDirectory(storageDir),
String.format("temp-%08d", tempFileCounter.getAndIncrement()));
}
/**
* Returns the blob store.
*/
BlobStore getBlobStore() {
return blobStore;
}
/**
* Returns the lock used to guard file accesses
*/
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
@Override
public void run() {
try {
while (!this.shutdownRequested.get()) {
BlobServerConnection conn = new BlobServerConnection(serverSocket.accept(), this);
try {
synchronized (activeConnections) {
while (activeConnections.size() >= maxConnections) {
activeConnections.wait(2000);
}
activeConnections.add(conn);
}
conn.start();
conn = null;
}
finally {
if (conn != null) {
conn.close();
synchronized (activeConnections) {
activeConnections.remove(conn);
}
}
}
}
}
catch (Throwable t) {
if (!this.shutdownRequested.get()) {
LOG.error("BLOB server stopped working. Shutting down", t);
try {
close();
} catch (Throwable closeThrowable) {
LOG.error("Could not properly close the BlobServer.", closeThrowable);
}
}
}
}
/**
* Shuts down the BLOB server.
*/
@Override
public void close() throws IOException {
if (shutdownRequested.compareAndSet(false, true)) {
Exception exception = null;
try {
this.serverSocket.close();
}
catch (IOException ioe) {
exception = ioe;
}
// wake the thread up, in case it is waiting on some operation
interrupt();
try {
join();
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
LOG.debug("Error while waiting for this thread to die.", ie);
}
synchronized (activeConnections) {
if (!activeConnections.isEmpty()) {
for (BlobServerConnection conn : activeConnections) {
LOG.debug("Shutting down connection {}.", conn.getName());
conn.close();
}
activeConnections.clear();
}
}
// Clean up the storage directory
try {
FileUtils.deleteDirectory(storageDir);
}
catch (IOException e) {
exception = ExceptionUtils.firstOrSuppressed(e, exception);
}
// Remove shutdown hook to prevent resource leaks, unless this is invoked by the
// shutdown hook itself
if (shutdownHook != null && shutdownHook != Thread.currentThread()) {
try {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
catch (IllegalStateException e) {
// race, JVM is in shutdown already, we can safely ignore this
}
catch (Throwable t) {
LOG.warn("Exception while unregistering BLOB server's cleanup shutdown hook.", t);
}
}
if(LOG.isInfoEnabled()) {
LOG.info("Stopped BLOB server at {}:{}", serverSocket.getInetAddress().getHostAddress(), getPort());
}
ExceptionUtils.tryRethrowIOException(exception);
}
}
@Override
public BlobClient createClient() throws IOException {
return new BlobClient(new InetSocketAddress(serverSocket.getInetAddress(), getPort()),
blobServiceConfiguration);
}
/**
* Method which retrieves the URL of a file associated with a blob key. The blob server looks
* the blob key up in its local storage. If the file exists, then the URL is returned. If the
* file does not exist, then a FileNotFoundException is thrown.
*
* @param requiredBlob blob key associated with the requested file
* @return URL of the file
* @throws IOException
*/
@Override
public URL getURL(BlobKey requiredBlob) throws IOException {
checkArgument(requiredBlob != null, "BLOB key cannot be null.");
final File localFile = BlobUtils.getStorageLocation(storageDir, requiredBlob);
if (localFile.exists()) {
return localFile.toURI().toURL();
}
else {
try {
// Try the blob store
blobStore.get(requiredBlob, localFile);
}
catch (Exception e) {
throw new IOException("Failed to copy from blob store.", e);
}
if (localFile.exists()) {
return localFile.toURI().toURL();
}
else {
throw new FileNotFoundException("Local file " + localFile + " does not exist " +
"and failed to copy from blob store.");
}
}
}
/**
* This method deletes the file associated to the blob key if it exists in the local storage
* of the blob server.
*
* @param key associated with the file to be deleted
* @throws IOException
*/
@Override
public void delete(BlobKey key) throws IOException {
final File localFile = BlobUtils.getStorageLocation(storageDir, key);
readWriteLock.writeLock().lock();
try {
if (localFile.exists()) {
if (!localFile.delete()) {
LOG.warn("Failed to delete locally BLOB " + key + " at " + localFile.getAbsolutePath());
}
}
blobStore.delete(key);
} finally {
readWriteLock.writeLock().unlock();
}
}
/**
* Returns the port on which the server is listening.
*
* @return port on which the server is listening
*/
@Override
public int getPort() {
return this.serverSocket.getLocalPort();
}
/**
* Tests whether the BLOB server has been requested to shut down.
*
* @return True, if the server has been requested to shut down, false otherwise.
*/
public boolean isShutdown() {
return this.shutdownRequested.get();
}
/**
* Access to the server socket, for testing
*/
ServerSocket getServerSocket() {
return this.serverSocket;
}
void unregisterConnection(BlobServerConnection conn) {
synchronized (activeConnections) {
activeConnections.remove(conn);
activeConnections.notifyAll();
}
}
/**
* Returns all the current active connections in the BlobServer.
*
* @return the list of all the active in current BlobServer
*/
List getCurrentActiveConnections() {
synchronized (activeConnections) {
return new ArrayList(activeConnections);
}
}
}