// This software is released into the Public Domain. See copying.txt for details.
package org.openstreetmap.osmosis.replicationhttp.v0_6.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.LinkedList;
import java.util.Queue;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.WriteCompletionEvent;
import org.jboss.netty.handler.codec.http.DefaultHttpChunk;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.util.CharsetUtil;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.util.PropertiesPersister;
import org.openstreetmap.osmosis.replication.common.ReplicationSequenceFormatter;
import org.openstreetmap.osmosis.replication.common.ReplicationState;
/**
* A sequence server handler implementation that sends the replication data
* associated with sequence numbers.
*
* @author Brett Henderson
*/
public class ReplicationDataServerHandler extends SequenceServerHandler {
private static final Logger LOG = Logger.getLogger(ReplicationDataServerHandler.class.getName());
private static final String REQUEST_DATE_FORMAT = "yyyy-MM-dd-HH-mm-ss";
private static final int CHUNK_SIZE = 4096;
private File dataDirectory;
private ReplicationSequenceFormatter sequenceFormatter;
private FileChannel chunkedFileChannel;
private boolean fileSizeSent;
private boolean includeData;
private ChannelFuture sequenceFuture;
/**
* Creates a new instance.
*
* @param control
* Provides the Netty handlers with access to the controller.
* @param dataDirectory
* The directory containing the replication data files.
*/
public ReplicationDataServerHandler(SequenceServerControl control, File dataDirectory) {
super(control);
this.dataDirectory = dataDirectory;
sequenceFormatter = new ReplicationSequenceFormatter(9, 3);
}
private DateFormat getRequestDateParser() {
SimpleDateFormat dateParser = new SimpleDateFormat(REQUEST_DATE_FORMAT);
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
dateParser.setCalendar(calendar);
return dateParser;
}
private File getStateFile(long sequenceNumber) {
return new File(dataDirectory, sequenceFormatter.getFormattedName(sequenceNumber, ".state.txt"));
}
private File getDataFile(long sequenceNumber) {
return new File(dataDirectory, sequenceFormatter.getFormattedName(sequenceNumber, ".osc.gz"));
}
private ReplicationState getReplicationState(long sequenceNumber) {
PropertiesPersister persister = new PropertiesPersister(getStateFile(sequenceNumber));
ReplicationState state = new ReplicationState();
state.load(persister.loadMap());
return state;
}
/**
* Search through the replication state records and find the nearest
* replication number with a timestamp earlier or equal to the requested
* date. It is not sufficient to find the minimum known sequence record with
* a timestamp greater than the requested date because there may be missing
* replication records in between.
*
* @param lastDate
* The last date known by the client.
* @return The associated sequence number.
*/
private long getNextSequenceNumberByDate(Date lastDate) {
long startBound = 0;
long endBound = getControl().getLatestSequenceNumber();
// If the requested date is greater than or equal to the latest known
// timestamp we should return our latest sequence number so that the
// client will start receiving all new records as they arrive with
// possibly some duplicated change records.
if (lastDate.compareTo(getReplicationState(endBound).getTimestamp()) >= 0) {
return endBound;
}
// Continue splitting our range in half until either we find the
// requested record, or we only have one possibility remaining.
while ((endBound - startBound) > 1) {
// Calculate the current midpoint.
long midPoint = startBound + ((endBound - startBound) / 2);
// If the midpoint doesn't exist we need to reset the start bound to
// the midpoint and search again.
if (!getStateFile(midPoint).exists()) {
startBound = midPoint;
continue;
}
// If the midpoint timestamp is greater we search in the lower half,
// otherwise the higher half.
int comparison = lastDate.compareTo(getReplicationState(midPoint).getTimestamp());
if (comparison == 0) {
// We have an exact match so stop processing now.
return midPoint;
} else if (comparison < 0) {
// We will now search in the lower half of the search range.
// Even though we know the midpoint is not the right value, we
// include it in the next range because our search assumes that
// the right sequence number is less than the end point.
endBound = midPoint;
} else {
// We will now search in the upper half of the search range.
// Even though the mid point has a timestamp less than the
// requested value, it still may be the selected value if the
// next timestamp is greater.
startBound = midPoint;
}
}
// We only have one possibility remaining which is the start bound. This
// is the requested record if it exists and has a timestamp less than or
// equal to that requested.
if (getStateFile(startBound).exists()
&& lastDate.compareTo(getReplicationState(startBound).getTimestamp()) >= 0) {
return startBound;
} else {
// We cannot find any replication records with an early enough date.
// This typically means that replication records for that time
// period either no longer exist or never existed.
throw new ResourceGoneException();
}
}
private FileChannel openFileChannel(File file) {
try {
return new FileInputStream(file).getChannel();
} catch (FileNotFoundException e) {
throw new OsmosisRuntimeException("Unable to open file " + file, e);
}
}
private ChannelBuffer readFromFile(FileChannel fileChannel, int bytesToRead) {
try {
// Allocate a buffer for the data to be read.
byte[] rawBuffer = new byte[bytesToRead];
// Copy data into the buffer using NIO.
ByteBuffer nioBuffer = ByteBuffer.wrap(rawBuffer);
for (int bytesRead = 0; bytesRead < bytesToRead;) {
int lastBytesRead = fileChannel.read(nioBuffer);
// We always expect to read data.
if (lastBytesRead < 0) {
throw new OsmosisRuntimeException("Unexpectedly reached the end of the replication data file");
}
if (lastBytesRead == 0) {
throw new OsmosisRuntimeException("Last read of the replication data file returned 0 bytes");
}
bytesRead += lastBytesRead;
}
// Create and return a Netty buffer.
return ChannelBuffers.wrappedBuffer(rawBuffer);
} catch (IOException e) {
throw new OsmosisRuntimeException("Unable to read from the replication data file", e);
}
}
private ChannelBuffer loadFile(File file) {
FileChannel fileChannel = openFileChannel(file);
try {
if (fileChannel.size() > Integer.MAX_VALUE) {
throw new OsmosisRuntimeException("Maximum file size supported is " + Integer.MAX_VALUE + " bytes");
}
// Determine the size of the file.
int fileSize = (int) fileChannel.size();
// Read the entire file.
ChannelBuffer buffer = readFromFile(fileChannel, fileSize);
// We no longer need access to the file.
fileChannel.close();
return buffer;
} catch (IOException e) {
throw new OsmosisRuntimeException("Unable to read from file " + file, e);
} finally {
try {
fileChannel.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Unable to close channel for file " + file, e);
}
}
}
private ChannelBuffer getFileChunk() {
try {
// Determine how many bytes are left in the file.
long remaining = chunkedFileChannel.size() - chunkedFileChannel.position();
// We will only send up to our maximum chunk size.
if (remaining > CHUNK_SIZE) {
remaining = CHUNK_SIZE;
}
// Read the next data for the next chunk.
ChannelBuffer buffer = readFromFile(chunkedFileChannel, (int) remaining);
// Close the file if we've reached the end.
if (chunkedFileChannel.position() >= chunkedFileChannel.size()) {
chunkedFileChannel.close();
chunkedFileChannel = null;
}
return buffer;
} catch (IOException e) {
throw new OsmosisRuntimeException("Unable to read from the replication data file", e);
}
}
private ChannelBuffer buildChunkHeader(long chunkSize) {
return ChannelBuffers.copiedBuffer(Long.toString(chunkSize) + "\r\n", CharsetUtil.UTF_8);
}
@Override
protected void handleRequest(ChannelHandlerContext ctx, HttpRequest request) {
final String replicationStateUri = "replicationState";
final String replicationDataUri = "replicationData";
final String textContentType = "text/plain";
final String dataContentType = "application/octet-stream";
// Split the request Uri into its path elements.
String uri = request.getUri();
if (!uri.startsWith("/")) {
throw new OsmosisRuntimeException("Uri doesn't start with a / character: " + uri);
}
Queue<String> uriElements = new LinkedList<String>(Arrays.asList(uri.split("/")));
uriElements.remove(); // First element is empty due to leading '/'.
// First element must be either the replication state or replication
// data uri which determines whether replication data will be included
// or just the replication state.
String contentType;
if (uriElements.isEmpty()) {
throw new ResourceNotFoundException();
}
String requestTypeString = uriElements.remove();
if (replicationStateUri.equals(requestTypeString)) {
contentType = textContentType;
includeData = false;
} else if (replicationDataUri.equals(requestTypeString)) {
contentType = dataContentType;
includeData = true;
} else {
throw new ResourceNotFoundException();
}
/*
* The next element determines which replication number to start from.
* The request is one of "current" or N where is the last sequence
* number received by the client.
*/
long nextSequenceNumber;
if (uriElements.isEmpty()) {
throw new ResourceNotFoundException();
}
String sequenceStartString = uriElements.remove();
if ("current".equals(sequenceStartString)) {
nextSequenceNumber = getControl().getLatestSequenceNumber();
} else {
// Try to parse the sequence start string as a number. If that fails
// try to parse as a date.
try {
nextSequenceNumber = Long.parseLong(sequenceStartString);
} catch (NumberFormatException e) {
try {
Date lastDate = getRequestDateParser().parse(sequenceStartString);
nextSequenceNumber = getNextSequenceNumberByDate(lastDate);
} catch (ParseException e1) {
throw new BadRequestException("Requested sequence number of " + sequenceStartString
+ " is not a number, or a date in format yyyy-MM-dd-HH-mm-ss.");
}
}
}
// If the next element exists and is "tail" it means that the client
// wants to stay connected and receive updated sequences as they become
// available.
boolean follow;
if (!uriElements.isEmpty()) {
String tailElement = uriElements.remove();
if ("tail".equals(tailElement)) {
follow = true;
} else {
throw new ResourceNotFoundException();
}
} else {
follow = false;
}
// Validate that that no more URI elements are available.
if (!uriElements.isEmpty()) {
throw new ResourceNotFoundException();
}
// Begin sending replication sequence information to the client.
if (LOG.isLoggable(Level.FINER)) {
LOG.finer("New request details, includeData=" + includeData + ", sequenceNumber=" + nextSequenceNumber
+ ", tail=" + follow);
}
initiateSequenceWriting(ctx, contentType, nextSequenceNumber, follow);
}
@Override
protected void writeSequence(ChannelHandlerContext ctx, ChannelFuture future, long sequenceNumber) {
// We do not support sending new replication data until the previous
// send has completed.
if (chunkedFileChannel != null) {
throw new OsmosisRuntimeException(
"We cannot send new replication data until the previous write has completed");
}
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest("Sequence being written, includeData=" + includeData + ", sequenceNumber="
+ sequenceNumber);
}
// We must save the future to attach to the final write.
sequenceFuture = future;
// Get the name of the replication data file.
File stateFile = getStateFile(sequenceNumber);
File dataFile = getDataFile(sequenceNumber);
// Load the contents of the state file.
ChannelBuffer stateFileBuffer = loadFile(stateFile);
// Add a chunk length header.
stateFileBuffer = ChannelBuffers.wrappedBuffer(buildChunkHeader(stateFileBuffer.readableBytes()),
stateFileBuffer);
// Only include replication data if initially requested by the client
// and if this is not sequence 0.
if (includeData && sequenceNumber > 0) {
// Open the data file read for sending.
chunkedFileChannel = openFileChannel(dataFile);
fileSizeSent = false;
}
/*
* Send the state file to the client. If replication data is to be sent
* we will continue when we receive completion information via the
* writeComplete method. We must create a new future now if we have more
* data coming because we don't want the future of the current event to
* fire until we're completely finished processing.
*/
ChannelFuture writeFuture;
if (chunkedFileChannel != null) {
writeFuture = Channels.future(ctx.getChannel());
} else {
writeFuture = sequenceFuture;
}
Channels.write(ctx, writeFuture, new DefaultHttpChunk(stateFileBuffer));
}
@Override
public void writeComplete(ChannelHandlerContext ctx, WriteCompletionEvent e) throws Exception {
if (chunkedFileChannel != null) {
// We have an open file channel so we are still sending replication
// data.
ChannelBuffer buffer;
ChannelFuture future;
if (!fileSizeSent) {
// Send a chunk header containing the size of the file.
ChannelBuffer fileSizeBuffer = buildChunkHeader(chunkedFileChannel.size());
fileSizeSent = true;
future = Channels.future(ctx.getChannel());
buffer = fileSizeBuffer;
} else {
// Send the next chunk to the client.
buffer = getFileChunk();
if (chunkedFileChannel != null) {
future = Channels.future(ctx.getChannel());
} else {
// This is the last write for this sequence so attach the original future.
future = sequenceFuture;
}
}
// Write the data to the channel.
Channels.write(ctx, future, new DefaultHttpChunk(buffer));
} else {
super.writeComplete(ctx, e);
}
}
@Override
public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// Close the in-progress chunk file channel if it exists.
if (chunkedFileChannel != null) {
try {
chunkedFileChannel.close();
} catch (IOException ex) {
LOG.log(Level.WARNING, "Unable to close the replication data file.", ex);
}
chunkedFileChannel = null;
}
super.channelClosed(ctx, e);
}
}