/*
* Copyright (c) 2016, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.core.v3.replication;
import org.postgresql.copy.CopyDual;
import org.postgresql.replication.LogSequenceNumber;
import org.postgresql.replication.PGReplicationStream;
import org.postgresql.replication.ReplicationType;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.sql.SQLException;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class V3PGReplicationStream implements PGReplicationStream {
private static final Logger LOGGER = Logger.getLogger(V3PGReplicationStream.class.getName());
public static final long POSTGRES_EPOCH_2000_01_01 = 946684800000L;
private final CopyDual copyDual;
private final long updateInterval;
private final ReplicationType replicationType;
private long lastStatusUpdate;
private boolean closeFlag = false;
private LogSequenceNumber lastServerLSN = LogSequenceNumber.INVALID_LSN;
/**
* Last receive LSN + payload size
*/
private LogSequenceNumber lastReceiveLSN = LogSequenceNumber.INVALID_LSN;
private LogSequenceNumber lastAppliedLSN = LogSequenceNumber.INVALID_LSN;
private LogSequenceNumber lastFlushedLSN = LogSequenceNumber.INVALID_LSN;
/**
* @param copyDual bidirectional copy protocol
* @param updateIntervalMs the number of millisecond between status packets sent back to the
* server. A value of zero disables the periodic status updates
* completely, although an update will still be sent when requested by the
* server, to avoid timeout disconnect.
*/
public V3PGReplicationStream(CopyDual copyDual, LogSequenceNumber startLSN, long updateIntervalMs,
ReplicationType replicationType
) {
this.copyDual = copyDual;
this.updateInterval = updateIntervalMs;
this.lastStatusUpdate = System.currentTimeMillis() - updateIntervalMs;
this.lastReceiveLSN = startLSN;
this.replicationType = replicationType;
}
@Override
public ByteBuffer read() throws SQLException {
checkClose();
ByteBuffer payload = null;
while (payload == null && copyDual.isActive()) {
payload = readInternal(true);
}
return payload;
}
public ByteBuffer readPending() throws SQLException {
checkClose();
return readInternal(false);
}
@Override
public LogSequenceNumber getLastReceiveLSN() {
return lastReceiveLSN;
}
@Override
public LogSequenceNumber getLastFlushedLSN() {
return lastFlushedLSN;
}
@Override
public LogSequenceNumber getLastAppliedLSN() {
return lastAppliedLSN;
}
@Override
public void setFlushedLSN(LogSequenceNumber flushed) {
this.lastFlushedLSN = flushed;
}
@Override
public void setAppliedLSN(LogSequenceNumber applied) {
this.lastAppliedLSN = applied;
}
@Override
public void forceUpdateStatus() throws SQLException {
checkClose();
updateStatusInternal(lastReceiveLSN, lastFlushedLSN, lastAppliedLSN, true);
}
@Override
public boolean isClosed() {
return closeFlag || !copyDual.isActive();
}
private ByteBuffer readInternal(boolean block) throws SQLException {
boolean updateStatusRequired = false;
while (copyDual.isActive()) {
if (updateStatusRequired || isTimeUpdate()) {
timeUpdateStatus();
}
ByteBuffer buffer = receiveNextData(block);
if (buffer == null) {
return null;
}
int code = buffer.get();
switch (code) {
case 'k': //KeepAlive message
updateStatusRequired = processKeepAliveMessage(buffer);
updateStatusRequired |= updateInterval == 0;
break;
case 'w': //XLogData
return processXLogData(buffer);
default:
throw new PSQLException(
GT.tr("Unexpected packet type during replication: {0}", Integer.toString(code)),
PSQLState.PROTOCOL_VIOLATION
);
}
}
return null;
}
private ByteBuffer receiveNextData(boolean block) throws SQLException {
try {
byte[] message = copyDual.readFromCopy(block);
if (message != null) {
return ByteBuffer.wrap(message);
} else {
return null;
}
} catch (PSQLException e) { //todo maybe replace on thread sleep?
if (e.getCause() instanceof SocketTimeoutException) {
//signal for keep alive
return null;
}
throw e;
}
}
private boolean isTimeUpdate() {
long diff = System.currentTimeMillis() - lastStatusUpdate;
return diff >= updateInterval;
}
private void timeUpdateStatus() throws SQLException {
updateStatusInternal(lastReceiveLSN, lastFlushedLSN, lastAppliedLSN, false);
}
private void updateStatusInternal(
LogSequenceNumber received, LogSequenceNumber flushed, LogSequenceNumber applied,
boolean replyRequired)
throws SQLException {
byte[] reply = prepareUpdateStatus(received, flushed, applied, replyRequired);
copyDual.writeToCopy(reply, 0, reply.length);
copyDual.flushCopy();
lastStatusUpdate = System.currentTimeMillis();
}
private byte[] prepareUpdateStatus(LogSequenceNumber received, LogSequenceNumber flushed,
LogSequenceNumber applied, boolean replyRequired) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1 + 8 + 8 + 8 + 8 + 1);
long now = System.currentTimeMillis();
long systemClock = TimeUnit.MICROSECONDS.convert((now - POSTGRES_EPOCH_2000_01_01),
TimeUnit.MICROSECONDS);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, " FE=> StandbyStatusUpdate(received: {0}, flushed: {1}, applied: {2}, clock: {3})",
new Object[]{received.asString(), flushed.asString(), applied.asString(), new Date(now)});
}
byteBuffer.put((byte) 'r');
byteBuffer.putLong(received.asLong());
byteBuffer.putLong(flushed.asLong());
byteBuffer.putLong(applied.asLong());
byteBuffer.putLong(systemClock);
if (replyRequired) {
byteBuffer.put((byte) 1);
} else {
byteBuffer.put(received == LogSequenceNumber.INVALID_LSN ? (byte) 1 : (byte) 0);
}
lastStatusUpdate = now;
return byteBuffer.array();
}
private boolean processKeepAliveMessage(ByteBuffer buffer) {
lastServerLSN = LogSequenceNumber.valueOf(buffer.getLong());
long lastServerClock = buffer.getLong();
boolean replyRequired = buffer.get() != 0;
if (LOGGER.isLoggable(Level.FINEST)) {
Date clockTime = new Date(
TimeUnit.MILLISECONDS.convert(lastServerClock, TimeUnit.MICROSECONDS)
+ POSTGRES_EPOCH_2000_01_01);
LOGGER.log(Level.FINEST, " <=BE Keepalive(lastServerWal: {0}, clock: {1} needReply: {2})",
new Object[]{lastServerLSN.asString(), clockTime, replyRequired});
}
return replyRequired;
}
private ByteBuffer processXLogData(ByteBuffer buffer) {
long startLsn = buffer.getLong();
lastServerLSN = LogSequenceNumber.valueOf(buffer.getLong());
long systemClock = buffer.getLong();
switch (replicationType) {
case LOGICAL:
lastReceiveLSN = LogSequenceNumber.valueOf(startLsn);
break;
case PHYSICAL:
int payloadSize = buffer.limit() - buffer.position();
lastReceiveLSN = LogSequenceNumber.valueOf(startLsn + payloadSize);
break;
}
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.log(Level.FINEST, " <=BE XLogData(currWal: {0}, lastServerWal: {1}, clock: {2})",
new Object[]{lastReceiveLSN.asString(), lastServerLSN.asString(), systemClock});
}
return buffer.slice();
}
private void checkClose() throws PSQLException {
if (isClosed()) {
throw new PSQLException(GT.tr("This replication stream has been closed."),
PSQLState.CONNECTION_DOES_NOT_EXIST);
}
}
public void close() throws SQLException {
if (isClosed()) {
return;
}
LOGGER.log(Level.FINEST, " FE=> StopReplication");
copyDual.endCopy();
closeFlag = true;
}
}