/* * 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.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assume.assumeThat; 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.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Assert; 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.Arrays; import java.util.Properties; @HaveMinimalServerVersion("9.4") public class PhysicalReplicationTest { private static final String SLOT_NAME = "pgjdbc_physical_replication_slot"; @Rule public ServerVersionRule versionRule = new ServerVersionRule(); private Connection replConnection; private Connection sqlConnection; @Before public void setUp() throws Exception { sqlConnection = TestUtil.openDB(); //DriverManager.setLogWriter(new PrintWriter(System.out)); replConnection = openReplicationConnection(); TestUtil.createTable(sqlConnection, "test_physic_table", "pk serial primary key, name varchar(100)"); TestUtil.recreatePhysicalReplicationSlot(sqlConnection, SLOT_NAME); } @After public void tearDown() throws Exception { replConnection.close(); TestUtil.dropTable(sqlConnection, "test_physic_table"); TestUtil.dropReplicationSlot(sqlConnection, SLOT_NAME); sqlConnection.close(); } @Test public void testReceiveChangesWithoutReplicationSlot() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_physic_table(name) values('previous value')"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withStartPosition(lsn) .start(); ByteBuffer read = stream.read(); assertThat("Physical replication can be start without replication slot", read, CoreMatchers.notNullValue() ); } @Test public void testReceiveChangesWithReplicationSlot() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_physic_table(name) values('previous value')"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .start(); ByteBuffer read = stream.read(); assertThat(read, CoreMatchers.notNullValue()); } @Test public void testAfterStartStreamingDBSlotStatusActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .start(); boolean isActive = isActiveOnView(); stream.close(); Assert.assertThat( "After start streaming, database status should be update on view pg_replication_slots to active", isActive, equalTo(true) ); } @Test public void testAfterCloseReplicationStreamDBSlotStatusNotActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .start(); boolean isActive = isActiveOnView(); assumeThat(isActive, equalTo(true)); stream.close(); isActive = isActiveOnView(); Assert.assertThat( "Execute close method on PGREplicationStream should lead to stop replication, " + "as result we wait that on view pg_replication_slots status for slot will change to no active", isActive, equalTo(false) ); } @Test public void testWalRecordCanBeRepeatBeRestartReplication() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_physic_table(name) values('previous value')"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .start(); byte[] first = toByteArray(stream.read()); stream.close(); //reopen stream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .start(); byte[] second = toByteArray(stream.read()); stream.close(); boolean arrayEquals = Arrays.equals(first, second); assertThat("On same replication connection we can restart replication from already " + "received LSN if they not recycled yet on backend", arrayEquals, CoreMatchers.equalTo(true) ); } @Test public void restartPhysicalReplicationWithoutRepeatMessage() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_physic_table(name) values('first value')"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .start(); byte[] streamOneFirstPart = toByteArray(stream.read()); LogSequenceNumber restartLSN = stream.getLastReceiveLSN(); st = sqlConnection.createStatement(); st.execute("insert into test_physic_table(name) values('second value')"); st.close(); byte[] streamOneSecondPart = toByteArray(stream.read()); stream.close(); //reopen stream stream = pgConnection .getReplicationAPI() .replicationStream() .physical() .withSlotName(SLOT_NAME) .withStartPosition(restartLSN) .start(); byte[] streamTwoFirstPart = toByteArray(stream.read()); stream.close(); boolean arrayEquals = Arrays.equals(streamOneSecondPart, streamTwoFirstPart); assertThat("Interrupt physical replication and restart from lastReceiveLSN should not " + "lead to repeat messages skip part of them", arrayEquals, CoreMatchers.equalTo(true) ); } private boolean isActiveOnView() throws SQLException { boolean result = false; Statement st = sqlConnection.createStatement(); ResultSet rs = st.executeQuery("select * from pg_replication_slots where slot_name = '" + SLOT_NAME + "'"); if (rs.next()) { result = rs.getBoolean("active"); } rs.close(); st.close(); return result; } private byte[] toByteArray(ByteBuffer buffer) { int offset = buffer.arrayOffset(); byte[] source = buffer.array(); return Arrays.copyOfRange(source, offset, source.length); } 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); } }