/*
* Copyright (c) 2016, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.replication;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import org.postgresql.PGConnection;
import org.postgresql.PGProperty;
import org.postgresql.core.BaseConnection;
import org.postgresql.core.ServerVersion;
import org.postgresql.test.TestUtil;
import org.postgresql.test.util.rules.ServerVersionRule;
import org.postgresql.test.util.rules.annotation.HaveMinimalServerVersion;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
@HaveMinimalServerVersion("9.4")
public class LogicalReplicationStatusTest {
private static final String SLOT_NAME = "pgjdbc_logical_replication_slot";
@Rule
public ServerVersionRule versionRule = new ServerVersionRule();
private Connection replicationConnection;
private Connection sqlConnection;
@Before
public void setUp() throws Exception {
//statistic available only for privileged user
sqlConnection = TestUtil.openPrivilegedDB();
//DriverManager.setLogWriter(new PrintWriter(System.out));
replicationConnection = openReplicationConnection();
TestUtil.createTable(sqlConnection, "test_logic_table",
"pk serial primary key, name varchar(100)");
TestUtil.recreateLogicalReplicationSlot(sqlConnection, SLOT_NAME, "test_decoding");
}
@After
public void tearDown() throws Exception {
replicationConnection.close();
TestUtil.dropTable(sqlConnection, "test_logic_table");
TestUtil.dropReplicationSlot(sqlConnection, SLOT_NAME);
sqlConnection.close();
}
@Test()
public void testSentLocationEqualToLastReceiveLSN() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
final int countMessage = 3;
List<String> received = receiveMessageWithoutBlock(stream, countMessage);
LogSequenceNumber lastReceivedLSN = stream.getLastReceiveLSN();
stream.forceUpdateStatus();
LogSequenceNumber sentByServer = getSentLocationOnView();
assertThat("When changes absent on server last receive by stream LSN "
+ "should be equal to last sent by server LSN",
sentByServer, equalTo(lastReceivedLSN)
);
}
/**
* Test fail on PG version 9.4.5 because postgresql have bug
*/
@Test
@HaveMinimalServerVersion("9.4.8")
public void testReceivedLSNDependentOnProcessMessage() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 1);
LogSequenceNumber firstLSN = stream.getLastReceiveLSN();
receiveMessageWithoutBlock(stream, 1);
LogSequenceNumber secondLSN = stream.getLastReceiveLSN();
assertThat("After receive each new message current LSN updates in stream",
firstLSN, not(equalTo(secondLSN))
);
}
@Test
public void testLastReceiveLSNCorrectOnView() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 2);
LogSequenceNumber lastReceivedLSN = stream.getLastReceiveLSN();
stream.forceUpdateStatus();
assertThat(
"Replication stream by execute forceUpdateStatus should send to view actual received position "
+ "that allow monitoring lag",
lastReceivedLSN, equalTo(getWriteLocationOnView())
);
}
@Test
public void testWriteLocationCanBeLessThanSendLocation() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 2);
stream.forceUpdateStatus();
LogSequenceNumber writeLocation = getWriteLocationOnView();
LogSequenceNumber sentLocation = getSentLocationOnView();
assertThat(
"In view pg_stat_replication column write_location define which position consume client "
+ "but sent_location define which position was sent to client, so in current test we have 1 pending message, "
+ "so write and sent can't be equals",
writeLocation, not(equalTo(sentLocation))
);
}
@Test
public void testFlushLocationEqualToSetLocation() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 1);
LogSequenceNumber flushLSN = stream.getLastReceiveLSN();
stream.setFlushedLSN(flushLSN);
//consume another messages
receiveMessageWithoutBlock(stream, 2);
stream.forceUpdateStatus();
LogSequenceNumber result = getFlushLocationOnView();
assertThat("Flush LSN use for define which wal can be recycled and it parameter should be "
+ "specify manually on replication stream, because only client "
+ "of replication stream now which wal not necessary. We wait that it status correct "
+ "send to backend and available via view, because if status will "
+ "not send it lead to problem when WALs never recycled",
result, equalTo(flushLSN)
);
}
@Test
public void testFlushLocationDoNotChangeDuringReceiveMessage() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 1);
final LogSequenceNumber flushLSN = stream.getLastReceiveLSN();
stream.setFlushedLSN(flushLSN);
receiveMessageWithoutBlock(stream, 2);
assertThat(
"Flush LSN it parameter that specify manually on stream and they can not automatically "
+ "change during receive another messages, "
+ "because auto update can lead to problem when WAL recycled on postgres "
+ "because we send feedback that current position successfully flush, but in real they not flush yet",
stream.getLastFlushedLSN(), equalTo(flushLSN)
);
}
@Test
public void testApplyLocationEqualToSetLocation() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 1);
final LogSequenceNumber applyLSN = stream.getLastReceiveLSN();
stream.setAppliedLSN(applyLSN);
stream.setFlushedLSN(applyLSN);
receiveMessageWithoutBlock(stream, 2);
stream.forceUpdateStatus();
LogSequenceNumber result = getReplayLocationOnView();
assertThat(
"During receive message from replication stream all feedback parameter "
+ "that we set to stream should be sent to backend"
+ "because it allow monitoring replication status and also recycle old WALs",
result, equalTo(applyLSN)
);
}
/**
* Test fail on PG version 9.4.5 because postgresql have bug
*/
@Test
@HaveMinimalServerVersion("9.4.8")
public void testApplyLocationDoNotDependOnFlushLocation() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 1);
stream.setAppliedLSN(stream.getLastReceiveLSN());
stream.setFlushedLSN(stream.getLastReceiveLSN());
receiveMessageWithoutBlock(stream, 1);
stream.setFlushedLSN(stream.getLastReceiveLSN());
receiveMessageWithoutBlock(stream, 1);
stream.forceUpdateStatus();
LogSequenceNumber flushed = getFlushLocationOnView();
LogSequenceNumber applied = getReplayLocationOnView();
assertThat(
"Last applied LSN and last flushed LSN it two not depends parameters and they can be not equal between",
applied, not(equalTo(flushed))
);
}
@Test
public void testApplyLocationDoNotChangeDuringReceiveMessage() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.start();
receiveMessageWithoutBlock(stream, 1);
final LogSequenceNumber applyLSN = stream.getLastReceiveLSN();
stream.setAppliedLSN(applyLSN);
receiveMessageWithoutBlock(stream, 2);
assertThat(
"Apply LSN it parameter that specify manually on stream and they can not automatically "
+ "change during receive another messages, "
+ "because auto update can lead to problem when WAL recycled on postgres "
+ "because we send feedback that current position successfully flush, but in real they not flush yet",
stream.getLastAppliedLSN(), equalTo(applyLSN)
);
}
@Test
public void testStatusCanBeSentToBackendAsynchronously() throws Exception {
PGConnection pgConnection = (PGConnection) replicationConnection;
final int intervalTime = 100;
final TimeUnit timeFormat = TimeUnit.MILLISECONDS;
LogSequenceNumber startLSN = getCurrentLSN();
Statement st = sqlConnection.createStatement();
st.execute("insert into test_logic_table(name) values('previous changes')");
st.close();
PGReplicationStream stream =
pgConnection
.getReplicationAPI()
.replicationStream()
.logical()
.withSlotName(SLOT_NAME)
.withStartPosition(startLSN)
.withStatusInterval(intervalTime, timeFormat)
.start();
receiveMessageWithoutBlock(stream, 3);
LogSequenceNumber waitLSN = stream.getLastReceiveLSN();
stream.setAppliedLSN(waitLSN);
stream.setFlushedLSN(waitLSN);
timeFormat.sleep(intervalTime + 1);
//get pending message and trigger update status by timeout
stream.readPending();
LogSequenceNumber flushLSN = getFlushLocationOnView();
assertThat("Status can be sent to backend by some time interval, "
+ "by default it parameter equals to 10 second, but in current test we change it on few millisecond "
+ "and wait that set status on stream will be auto send to backend",
flushLSN, equalTo(waitLSN)
);
}
private LogSequenceNumber getSentLocationOnView() throws Exception {
return getLSNFromView((((BaseConnection) sqlConnection).haveMinimumServerVersion(ServerVersion.v10)
? "sent_lsn" : "sent_location"));
}
private LogSequenceNumber getWriteLocationOnView() throws Exception {
return getLSNFromView((((BaseConnection) sqlConnection).haveMinimumServerVersion(ServerVersion.v10)
? "write_lsn" : "write_location"));
}
private LogSequenceNumber getFlushLocationOnView() throws Exception {
return getLSNFromView((((BaseConnection) sqlConnection).haveMinimumServerVersion(ServerVersion.v10)
? "flush_lsn" : "flush_location"));
}
private LogSequenceNumber getReplayLocationOnView() throws Exception {
return getLSNFromView((((BaseConnection) sqlConnection).haveMinimumServerVersion(ServerVersion.v10)
? "replay_lsn" : "replay_location"));
}
private List<String> receiveMessageWithoutBlock(PGReplicationStream stream, int count)
throws Exception {
List<String> result = new ArrayList<String>(3);
for (int index = 0; index < count; index++) {
ByteBuffer message;
do {
message = stream.readPending();
if (message == null) {
TimeUnit.MILLISECONDS.sleep(2);
}
} while (message == null);
result.add(toString(message));
}
return result;
}
private String toString(ByteBuffer buffer) {
int offset = buffer.arrayOffset();
byte[] source = buffer.array();
int length = source.length - offset;
return new String(source, offset, length);
}
private LogSequenceNumber getLSNFromView(String columnName) throws Exception {
int pid = ((PGConnection) replicationConnection).getBackendPID();
int repeatCount = 0;
while (true) {
Statement st = sqlConnection.createStatement();
ResultSet rs = null;
try {
rs = st.executeQuery("select * from pg_stat_replication where pid = " + pid);
String result = null;
if (rs.next()) {
result = rs.getString(columnName);
}
if (result == null || result.isEmpty()) {
//replication monitoring view updates with some delay, wait some time and try again
TimeUnit.MILLISECONDS.sleep(100L);
repeatCount++;
if (repeatCount == 10) {
return null;
}
} else {
return LogSequenceNumber.valueOf(result);
}
} finally {
if (rs != null) {
rs.close();
}
st.close();
}
}
}
private LogSequenceNumber getCurrentLSN() throws SQLException {
Statement st = sqlConnection.createStatement();
ResultSet rs = null;
try {
rs = st.executeQuery("select "
+ (((BaseConnection) sqlConnection).haveMinimumServerVersion(ServerVersion.v10)
? "pg_current_wal_lsn()" : "pg_current_xlog_location()"));
if (rs.next()) {
String lsn = rs.getString(1);
return LogSequenceNumber.valueOf(lsn);
} else {
return LogSequenceNumber.INVALID_LSN;
}
} finally {
if (rs != null) {
rs.close();
}
st.close();
}
}
private Connection openReplicationConnection() throws Exception {
Properties properties = new Properties();
PGProperty.ASSUME_MIN_SERVER_VERSION.set(properties, "9.4");
PGProperty.REPLICATION.set(properties, "database");
return TestUtil.openDB(properties);
}
}