com.sleepycat.je.rep.impl.networkRestore.LogFileFeeder Maven / Gradle / Ivy
The newest version!
/*-
* Copyright (C) 2002, 2018, Oracle and/or its affiliates. All rights reserved.
*
* This file was distributed by Oracle as part of a version of Oracle Berkeley
* DB Java Edition made available at:
*
* http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/downloads/index.html
*
* Please see the LICENSE file included in the top-level directory of the
* appropriate version of Oracle Berkeley DB Java Edition for a copy of the
* license and additional information.
*/
package com.sleepycat.je.rep.impl.networkRestore;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.logging.Logger;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.EnvironmentFailureException;
import com.sleepycat.je.dbi.EnvironmentFailureReason;
import com.sleepycat.je.log.FileManager;
import com.sleepycat.je.rep.impl.RepImpl;
import com.sleepycat.je.rep.impl.networkRestore.FeederManager.Lease;
import com.sleepycat.je.rep.impl.networkRestore.Protocol.FeederInfoReq;
import com.sleepycat.je.rep.impl.networkRestore.Protocol.FileInfoReq;
import com.sleepycat.je.rep.impl.networkRestore.Protocol.FileInfoResp;
import com.sleepycat.je.rep.impl.networkRestore.Protocol.FileReq;
import com.sleepycat.je.rep.net.DataChannel;
import com.sleepycat.je.rep.utilint.BinaryProtocol.ClientVersion;
import com.sleepycat.je.rep.utilint.BinaryProtocol.ProtocolException;
import com.sleepycat.je.rep.utilint.NamedChannel;
import com.sleepycat.je.rep.utilint.RepUtils;
import com.sleepycat.je.rep.vlsn.VLSNRange;
import com.sleepycat.je.util.DbBackup;
import com.sleepycat.je.utilint.LogVerifier;
import com.sleepycat.je.utilint.LoggerUtils;
import com.sleepycat.je.utilint.StoppableThread;
import com.sleepycat.je.utilint.VLSN;
/**
* The LogFileFeeder supplies log files to a client. There is one instance of
* this class per client that's currently active. LogFileFeeders are created by
* the FeederManager and exist for the duration of the session with the client.
*/
public class LogFileFeeder extends StoppableThread {
/**
* Time to wait for the next request from the client, 5 minutes.
*/
private static final int SOCKET_TIMEOUT_MS = 5 * 60 * 1000;
/*
* 8K transfer size to take advantage of increasingly prevalent jumbo
* frame sizes and to keep disk i/o contention to a minimum.
*/
static final int TRANSFER_BYTES = 0x2000;
/*
* The parent FeederManager that creates and maintains LogFileFeeder
* instances.
*/
private final FeederManager feederManager;
/* The channel on which the feeder communicates with the client. */
private final NamedChannel namedChannel;
/* The client node requesting the log files. */
private int clientId;
/*
* The dbBackup instance that's used to manage the list of files that will
* be transferred. It's used to ensure that a consistent set is transferred
* over to the client. If an open dbBackup exists for the client, it's
* established in the checkProtocol method immediately after the client has
* been identified.
*/
private DbBackup dbBackup = null;
/* Used to compute a SHA1 during a transfer, or if a client requests it. */
final MessageDigest messageDigest;
/* Logger shared with the FeederManager. */
final private Logger logger;
public LogFileFeeder(FeederManager feederManager,
DataChannel channel)
throws DatabaseException {
super(feederManager.getEnvImpl(), "Log File Feeder");
this.feederManager = feederManager;
logger = feederManager.logger;
this.namedChannel = new NamedChannel(channel, feederManager.nameIdPair);
try {
messageDigest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
LoggerUtils.severe(logger, feederManager.getEnvImpl(),
"The SHA1 algorithm was not made available " +
"by the security provider");
throw EnvironmentFailureException.unexpectedException(e);
}
}
public void shutdown() {
if (shutdownDone(logger)) {
return;
}
shutdownThread(logger);
feederManager.feeders.remove(clientId);
LoggerUtils.info(logger, feederManager.getEnvImpl(),
"Log file feeder for client:" + clientId +
" is shutdown.");
}
@Override
protected int initiateSoftShutdown() {
/*
* The feeder will get an I/O exception and exit, since it can't use
* the channel after it has been closed.
*/
RepUtils.shutdownChannel(namedChannel);
return SOCKET_TIMEOUT_MS;
}
/**
* The main driver loop that enforces the protocol message sequence and
* implements it.
*/
@Override
public void run() {
/* The initial protocol */
Protocol protocol = new Protocol(feederManager.nameIdPair,
Protocol.VERSION,
feederManager.getEnvImpl());
try {
configureChannel();
protocol = checkProtocol(protocol);
checkFeeder(protocol);
sendFileList(protocol);
sendRequestedFiles(protocol);
/* Done, cleanup */
dbBackup.endBackup();
dbBackup = null;
} catch (ClosedByInterruptException e) {
LoggerUtils.fine
(logger, feederManager.getEnvImpl(),
"Ignoring ClosedByInterruptException normal shutdown");
} catch (IOException e) {
LoggerUtils.warning(logger, feederManager.getEnvImpl(),
" IO Exception: " + e.getMessage());
} catch (ProtocolException e) {
LoggerUtils.severe(logger, feederManager.getEnvImpl(),
" Protocol Exception: " + e.getMessage());
} catch (Exception e) {
throw new EnvironmentFailureException
(feederManager.getEnvImpl(),
EnvironmentFailureReason.UNCAUGHT_EXCEPTION,
e);
} finally {
try {
namedChannel.getChannel().close();
} catch (IOException e) {
LoggerUtils.warning(logger, feederManager.getEnvImpl(),
"Log File feeder io exception on " +
"channel close: " + e.getMessage());
}
shutdown();
if (dbBackup != null) {
if (feederManager.shutdown.get()) {
dbBackup.endBackup();
} else {
/*
* Establish lease so client can resume within the lease
* period.
*/
@SuppressWarnings("unused")
final Lease lease =
feederManager.new Lease(clientId,
feederManager.leaseDuration,
dbBackup);
LoggerUtils.info(logger, feederManager.getEnvImpl(),
"Lease created for node: " + clientId);
}
}
LoggerUtils.info
(logger, feederManager.getEnvImpl(),
"Log file feeder for client: " + clientId + " exited");
}
}
/**
* Implements the message exchange used to determine whether this feeder
* is suitable for use the client's backup needs. The feeder may be
* unsuitable if it's already busy, or it's not current enough to service
* the client's needs.
*/
private void checkFeeder(Protocol protocol)
throws IOException, DatabaseException {
protocol.read(namedChannel.getChannel(), FeederInfoReq.class);
int feeders = feederManager.getActiveFeederCount() -
1 /* Exclude this one */;
VLSN rangeFirst = VLSN.NULL_VLSN;
VLSN rangeLast = VLSN.NULL_VLSN;
if (feederManager.getEnvImpl() instanceof RepImpl) {
/* Include replication stream feeders as a load component. */
RepImpl repImpl = (RepImpl) feederManager.getEnvImpl();
feeders +=
repImpl.getRepNode().feederManager().activeReplicaCount();
VLSNRange range = repImpl.getVLSNIndex().getRange();
rangeFirst = range.getFirst();
rangeLast = range.getLast();
}
protocol.write(protocol.new FeederInfoResp
(feeders, rangeFirst, rangeLast), namedChannel);
}
/**
* Send files in response to request messages. The request sequence looks
* like the following:
*
* [FileReq | FileInfoReq]+ Done
*
* The response sequence to a FileReq looks like:
*
* FileStart FileEnd
*
* and that for a FileInfoReq, is simply a FileInfoResp
*/
private void sendRequestedFiles(Protocol protocol)
throws IOException, ProtocolException, DatabaseException {
String prevFileName = null;
try {
while (true) {
FileReq fileReq = protocol.read(namedChannel.getChannel(),
FileReq.class);
final String fileName = fileReq.getFileName();
/*
* Calculate the full path for a specified log file name,
* especially when this Feeder is configured to run with sub
* directories.
*/
FileManager fMgr = feederManager.getEnvImpl().getFileManager();
File file = new File(fMgr.getFullFileName(fileName));
if (!file.exists()) {
throw EnvironmentFailureException.unexpectedState
("Log file not found: " + fileName);
}
/* Freeze the length and last modified date. */
final long length = file.length();
final long lastModified = file.lastModified();
byte digest[] = null;
FileInfoResp resp = null;
Protocol.FileInfoResp cachedResp =
feederManager.statResponses.get(fileName);
byte cachedDigest[] =
((cachedResp != null) &&
(cachedResp.getFileLength() == length) &&
(cachedResp.getLastModifiedTime() == lastModified)) ?
cachedResp.getDigestSHA1() : null;
if (fileReq instanceof FileInfoReq) {
if (cachedDigest != null) {
digest = cachedDigest;
} else if (((FileInfoReq) fileReq).getNeedSHA1()) {
digest = getSHA1Digest(file, length).digest();
} else {
// Digest not requested
digest = new byte[0];
}
resp = protocol.new FileInfoResp
(fileName, length, lastModified, digest);
} else {
/* Allow deletion of previous file. */
if (prevFileName != null &&
!fileName.equals(prevFileName)) {
dbBackup.removeFileProtection(prevFileName);
}
prevFileName = fileName;
protocol.write(protocol.new FileStart
(fileName, length, lastModified),
namedChannel);
digest = sendFileContents(file, length);
if ((cachedDigest != null) &&
!Arrays.equals(cachedDigest, digest)) {
throw EnvironmentFailureException.unexpectedState
("Inconsistent cached and computed digests");
}
resp = protocol.new FileEnd
(fileName, length, lastModified, digest);
}
/* Cache for subsequent requests, if it was computed. */
if (digest.length > 0) {
feederManager.statResponses.put(fileName, resp);
}
protocol.write(resp, namedChannel);
}
} catch (ProtocolException pe) {
if (pe.getUnexpectedMessage() instanceof Protocol.Done) {
return;
}
throw pe;
}
}
/**
* Returns the SHA1 has associated with the file.
*
* @param file
* @param length
* @return
* @throws IOException
* @throws DatabaseException
*/
static MessageDigest getSHA1Digest(File file, long length)
throws IOException, DatabaseException {
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
throw EnvironmentFailureException.unexpectedException(e);
}
final FileInputStream fileStream = new FileInputStream(file);
try {
ByteBuffer buffer = ByteBuffer.allocate(TRANSFER_BYTES);
for (long bytes = length; bytes > 0; ) {
int readSize = (int)Math.min(TRANSFER_BYTES, bytes);
int readBytes =
fileStream.read(buffer.array(), 0, readSize);
if (readBytes == -1) {
throw new IOException("Premature EOF. Was expecting: " +
readSize);
}
messageDigest.update(buffer.array(), 0, readBytes);
bytes -= readBytes;
}
} finally {
fileStream.close();
}
return messageDigest;
}
/**
* Sends over the contents of the file and computes the SHA-1 hash. Note
* that the method does not rely on EOF detection, but rather on the
* promised file size, since the final log file might be growing while the
* transfer is in progress. The client uses the length sent in the FileResp
* message to maintain its position in the network stream. It expects to
* see a FileInfoResp once it has read the agreed upon number of bytes.
*
* Since JE log files are append only, there is no danger that we will send
* over any uninitialized file blocks.
*
* @param file the log file to be sent.
* @param length the number of bytes to send
* @return the digest associated with the file that was sent
*
* @throws IOException
*/
private byte[] sendFileContents(File file, long length)
throws IOException {
final LogVerifier verifier =
new LogVerifier(feederManager.getEnvImpl(), file.getName(), -1L);
final FileInputStream fileStream = new FileInputStream(file);
try {
final FileChannel fileChannel = fileStream.getChannel();
messageDigest.reset();
final ByteBuffer buffer =
ByteBuffer.allocateDirect(TRANSFER_BYTES);
final byte[] array =
(buffer.hasArray()) ? buffer.array() : new byte[TRANSFER_BYTES];
int transmitBytes = 0;
while (true) {
buffer.clear();
if (fileChannel.read(buffer) < 0) {
verifier.verifyAtEof();
break;
}
buffer.flip();
final int lim = buffer.limit();
final int off;
if (buffer.hasArray()) {
off = buffer.arrayOffset();
} else {
off = 0;
buffer.get(array, 0, lim);
buffer.rewind();
}
verifier.verify(array, off, lim);
messageDigest.update(array, off, lim);
transmitBytes += namedChannel.getChannel().write(buffer);
}
if (transmitBytes != length) {
String msg = "File length:" + length + " does not match the " +
"number of bytes that were transmitted:" +
transmitBytes;
throw new IllegalStateException(msg);
}
final String msg =
String.format("Sent file: %s Length:%,d bytes to client:%d",
file, length, clientId);
LoggerUtils.info(logger, feederManager.getEnvImpl(), msg);
} finally {
fileStream.close();
}
return messageDigest.digest();
}
/**
* Processes the request for the list of files that constitute a valid
* backup. If a leased DbBackup instance is available, it uses it,
* otherwise it creates a new instance and uses it instead.
*/
private void sendFileList(Protocol protocol)
throws IOException, ProtocolException, DatabaseException {
/* Wait for the request message. */
protocol.read(namedChannel.getChannel(), Protocol.FileListReq.class);
if (dbBackup == null) {
dbBackup = new DbBackup(feederManager.getEnvImpl());
dbBackup.setNetworkRestore();
dbBackup.startBackup();
} else {
feederManager.leaseRenewalCount++;
}
/*
* Remove the subdirectory header of the log files, because the nodes
* that need to copy those log files may not configure the spreading
* log files into sub directories feature.
*/
String[] files = dbBackup.getLogFilesInBackupSet();
for (int i = 0; i < files.length; i++) {
if (files[i].contains(File.separator)) {
files[i] = files[i].substring
(files[i].indexOf(File.separator) + 1, files[i].length());
}
}
protocol.write(protocol.new FileListResp(files), namedChannel);
}
/**
* Verify that the protocols are compatible, switch to a different protocol
* version, if we need to.
*/
private Protocol checkProtocol(Protocol protocol)
throws IOException, ProtocolException {
ClientVersion clientVersion =
protocol.read(namedChannel.getChannel(),
Protocol.ClientVersion.class);
clientId = clientVersion.getNodeId();
FeederManager.Lease lease = feederManager.leases.get(clientId);
if (lease != null) {
dbBackup = lease.terminate();
}
LogFileFeeder prev = feederManager.feeders.put(clientId, this);
if (prev != null) {
final SocketAddress prevFeederAddress =
prev.namedChannel.getChannel().getRemoteAddress();
LoggerUtils.warning(logger, feederManager.getEnvImpl(),
"Log file feeder with client id:" + clientId +
" already present; originated from " +
prevFeederAddress +
" new connection originated from:" +
namedChannel.getChannel().getRemoteAddress());
}
if (clientVersion.getVersion() != protocol.getVersion()) {
String message = "Client requested protocol version: " +
clientVersion.getVersion() + " but the server version is " +
protocol.getVersion();
/*
* Simply log the difference on the server, it's up to the client
* to reject the protocol version, if it can't accommodate it.
*/
LoggerUtils.warning(logger, feederManager.getEnvImpl(), message);
}
protocol.write(protocol.new ServerVersion(), namedChannel);
/* In future we may switch protocol versions to accommodate the client.
* For now, simply return the one and only version.
*/
return protocol;
}
/**
* Sets up the channel to facilitate efficient transfer of large log files.
*/
private DataChannel configureChannel()
throws IOException {
LoggerUtils.fine
(logger, feederManager.getEnvImpl(),
"Log File Feeder accepted connection from " + namedChannel);
namedChannel.getChannel().socket().setSoTimeout(SOCKET_TIMEOUT_MS);
/*
* Enable Nagle's algorithm since throughput is important for the large
* files we will be transferring.
*/
namedChannel.getChannel().socket().setTcpNoDelay(false);
return namedChannel.getChannel();
}
/**
* @see StoppableThread#getLogger
*/
@Override
protected Logger getLogger() {
return logger;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy