/* * 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.junit.Assert.assertThat; import static org.junit.Assert.fail; 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.postgresql.util.PSQLException; import org.postgresql.util.PSQLState; import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import java.nio.ByteBuffer; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @HaveMinimalServerVersion("9.4") public class LogicalReplicationTest { private static final String SLOT_NAME = "pgjdbc_logical_replication_slot"; @Rule public ServerVersionRule versionRule = new ServerVersionRule(); @Rule public ExpectedException exception = ExpectedException.none(); private Connection replConnection; private Connection sqlConnection; private static String toString(ByteBuffer buffer) { int offset = buffer.arrayOffset(); byte[] source = buffer.array(); int length = source.length - offset; return new String(source, offset, length); } @Before public void setUp() throws Exception { sqlConnection = TestUtil.openDB(); //DriverManager.setLogWriter(new PrintWriter(System.out)); replConnection = 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 { replConnection.close(); TestUtil.dropTable(sqlConnection, "test_logic_table"); TestUtil.dropReplicationSlot(sqlConnection, SLOT_NAME); sqlConnection.close(); } @Test(timeout = 1000) public void testNotAvailableStartNotExistReplicationSlot() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); try { PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName("notExistSlotName") .withStartPosition(lsn) .start(); fail("For logical decoding replication slot name it required parameter " + "that should be create on server before start replication"); } catch (PSQLException e) { String state = e.getSQLState(); assertThat("When replication slot doesn't exists, server can't start replication " + "and should throw exception about it", state, equalTo(PSQLState.UNDEFINED_OBJECT.getState()) ); } } @Test(timeout = 1000) public void testReceiveChangesOccursBeforStartReplication() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('previous value')"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .withSlotOption("include-xids", false) .start(); String result = group(receiveMessage(stream, 3)); String wait = group( Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'previous value'", "COMMIT" ) ); assertThat("Logical replication can be start from some LSN position and all changes that " + "occurs between last server LSN and specified LSN position should be available to read " + "via stream in correct order", result, equalTo(wait) ); } @Test(timeout = 1000) public void testReceiveChangesAfterStartReplication() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); List<String> result = new ArrayList<String>(); Statement st = sqlConnection.createStatement(); st.execute( "insert into test_logic_table(name) values('first message after start replication')"); st.close(); result.addAll(receiveMessage(stream, 3)); st = sqlConnection.createStatement(); st.execute( "insert into test_logic_table(name) values('second message after start replication')"); st.close(); result.addAll(receiveMessage(stream, 3)); String groupedResult = group(result); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first message after start replication'", "COMMIT", "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:2 name[character varying]:'second message after start replication'", "COMMIT" )); assertThat( "After starting replication, from stream should be available also new changes that occurs after start replication", groupedResult, equalTo(wait) ); } @Test(timeout = 1000) public void testStartFromCurrentServerLSNWithoutSpecifyLSNExplicitly() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('last server message')"); st.close(); String result = group(receiveMessage(stream, 3)); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'last server message'", "COMMIT" )); assertThat( "When start LSN position not specify explicitly, wal should be stream from actual server position", result, equalTo(wait)); } @Test(timeout = 1000) public void testAfterStartStreamingDBSlotStatusActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); boolean isActive = isActiveOnView(); assertThat( "After start streaming, database status should be update on view pg_replication_slots to active", isActive, equalTo(true) ); } /** * <p>Bug in postgreSQL that should be fixed in 10 version after code review patch <a * href="http://www.postgresql.org/message-id/CAFgjRd3hdYOa33m69TbeOfNNer2BZbwa8FFjt2V5VFzTBvUU3w@mail.gmail.com"> * Stopping logical replication protocol</a>. * * <p>If you try to run it test on version before 10 they fail with time out, because postgresql * wait new changes and until waiting messages from client ignores. */ @Test(timeout = 1000) @HaveMinimalServerVersion("10.1") public void testAfterCloseReplicationStreamDBSlotStatusNotActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); boolean isActive = isActiveOnView(); assumeThat(isActive, equalTo(true)); stream.close(); isActive = isActiveOnView(); 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(timeout = 1000) public void testAfterCloseConnectionDBSLotStatusNotActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(lsn) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); boolean isActive = isActiveOnView(); assumeThat(isActive, equalTo(true)); replConnection.close(); isActive = isActiveOnView(); //we doesn't wait replay from server about stop connection that why some delay exists on update view and should wait some time before check view if (isActive) { TimeUnit.MILLISECONDS.sleep(200L); isActive = isActiveOnView(); } assertThat( "Execute close method on Connection should lead to stop replication as fast as possible, " + "as result we wait that on view pg_replication_slots status for slot will change to no active", isActive, equalTo(false) ); } /** * <p>Bug in postgreSQL that should be fixed in 10 version after code review patch <a * href="http://www.postgresql.org/message-id/CAFgjRd3hdYOa33m69TbeOfNNer2BZbwa8FFjt2V5VFzTBvUU3w@mail.gmail.com"> * Stopping logical replication protocol</a>. * * <p>If you try to run it test on version before 10 they fail with time out, because postgresql * wait new changes and until waiting messages from client ignores. */ @Test(timeout = 10000) @HaveMinimalServerVersion("10.1") public void testDuringSendBigTransactionConnectionCloseSlotStatusNotActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table\n" + " select id, md5(random()::text) as name from generate_series(1, 200000) as id;"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withStartPosition(lsn) .withSlotName(SLOT_NAME) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); //wait first message stream.read(); replConnection.close(); boolean isActive = isActiveOnView(); //we doesn't wait replay from server about stop connection that why some delay exists on update view and should wait some time before check view if (!isActive) { TimeUnit.SECONDS.sleep(2L); isActive = isActiveOnView(); } assertThat( "Execute close method on Connection should lead to stop replication as fast as possible, " + "as result we wait that on view pg_replication_slots status for slot will change to no active", isActive, equalTo(false) ); } /** * <p>Bug in postgreSQL that should be fixed in 10 version after code review patch <a * href="http://www.postgresql.org/message-id/CAFgjRd3hdYOa33m69TbeOfNNer2BZbwa8FFjt2V5VFzTBvUU3w@mail.gmail.com"> * Stopping logical replication protocol</a>. * * <p>If you try to run it test on version before 10 they fail with time out, because postgresql * wait new changes and until waiting messages from client ignores. */ @Test(timeout = 60000) @HaveMinimalServerVersion("10.1") public void testDuringSendBigTransactionReplicationStreamCloseNotActive() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber lsn = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table\n" + " select id, md5(random()::text) as name from generate_series(1, 200000) as id;"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withStartPosition(lsn) .withSlotName(SLOT_NAME) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); //wait first message stream.read(); stream.close(); //after replay from server that replication stream stopped, view already should be updated boolean isActive = isActiveOnView(); 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(timeout = 5000) //todo fix, fail because backend for logical decoding not reply with CommandComplate & ReadyForQuery public void testRepeatWalPositionTwice() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber startLSN = getCurrentLSN(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('message to repeat')"); st.close(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(startLSN) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); List<String> result = new ArrayList<String>(); result.addAll(receiveMessage(stream, 3)); replConnection.close(); replConnection = openReplicationConnection(); pgConnection = (PGConnection) replConnection; stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(startLSN) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); result.addAll(receiveMessage(stream, 3)); String groupedResult = group(result); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'message to repeat'", "COMMIT", "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'message to repeat'", "COMMIT" )); assertThat("Logical replication stream after start streaming can be close and " + "reopen on previous LSN, that allow reply wal logs, if they was not recycled yet", groupedResult, equalTo(wait) ); } @Test(timeout = 3000) public void testDoesNotHavePendingMessageWhenStartFromLastLSN() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(getCurrentLSN()) .start(); ByteBuffer result = stream.readPending(); assertThat("Read pending message allow without lock on socket read message, " + "and if message absent return null. In current test we start replication from last LSN on server, " + "so changes absent on server and readPending message will always lead to null ByteBuffer", result, equalTo(null) ); } @Test(timeout = 3000) public void testReadPreviousChangesWithoutBlock() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; 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) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); String received = group(receiveMessageWithoutBlock(stream, 3)); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'previous changes'", "COMMIT" )); assertThat( "Messages from stream can be read by readPending method for avoid long block on Socket, " + "in current test we wait that behavior will be same as for read message with block", received, equalTo(wait) ); } @Test(timeout = 3000) public void testReadActualChangesWithoutBlock() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(getCurrentLSN()) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('actual changes')"); st.close(); String received = group(receiveMessageWithoutBlock(stream, 3)); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'actual changes'", "COMMIT" )); assertThat( "Messages from stream can be read by readPending method for avoid long block on Socket, " + "in current test we wait that behavior will be same as for read message with block", received, equalTo(wait) ); } @Test(timeout = 10000 /* default client keep alive 10s*/) public void testAvoidTimeoutDisconnectWithDefaultStatusInterval() throws Exception { final int statusInterval = getKeepAliveTimeout(); ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = null; boolean done; try { future = executor.submit(new Callable<Object>() { @Override public Object call() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(getCurrentLSN()) .withStatusInterval(Math.round(statusInterval / 3), TimeUnit.MILLISECONDS) .start(); while (!Thread.interrupted()) { stream.read(); } return null; } }); future.get(5, TimeUnit.SECONDS); done = future.isDone(); } catch (TimeoutException timeout) { done = future.isDone(); } finally { executor.shutdownNow(); } assertThat( "ReplicationStream should periodically send keep alive message to postgresql to avoid disconnect from server", done, CoreMatchers.equalTo(false) ); } @Test public void testRestartReplicationFromRestartSlotLSNWhenFeedbackAbsent() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber startLSN = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(startLSN) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('first tx changes')"); st.close(); st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('second tx change')"); st.close(); List<String> consumedData = new ArrayList<String>(); consumedData.addAll(receiveMessageWithoutBlock(stream, 3)); //emulate replication break replConnection.close(); waitStopReplicationSlot(); replConnection = openReplicationConnection(); pgConnection = (PGConnection) replConnection; stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(LogSequenceNumber.INVALID_LSN) /* Invalid LSN indicate for start from restart lsn */ .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); consumedData.addAll(receiveMessageWithoutBlock(stream, 3)); String result = group(consumedData); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first tx changes'", "COMMIT", "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first tx changes'", "COMMIT" )); assertThat( "If was consume message via logical replication stream but wasn't send feedback about apply and flush " + "consumed LSN, if replication crash, server should restart from last success apllyed lsn, " + "in this case it lsn of start replication slot, so we should consume first 3 message twice", result, equalTo(wait) ); } @Test public void testReplicationRestartFromLastFeedbackPosition() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber startLSN = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(startLSN) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('first tx changes')"); st.close(); st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('second tx change')"); st.close(); List<String> consumedData = new ArrayList<String>(); consumedData.addAll(receiveMessageWithoutBlock(stream, 3)); stream.setFlushedLSN(stream.getLastReceiveLSN()); stream.setAppliedLSN(stream.getLastReceiveLSN()); stream.forceUpdateStatus(); //emulate replication break replConnection.close(); waitStopReplicationSlot(); replConnection = openReplicationConnection(); pgConnection = (PGConnection) replConnection; stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(LogSequenceNumber.INVALID_LSN) /* Invalid LSN indicate for start from restart lsn */ .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); consumedData.addAll(receiveMessageWithoutBlock(stream, 3)); String result = group(consumedData); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first tx changes'", "COMMIT", "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:2 name[character varying]:'second tx change'", "COMMIT" )); assertThat( "When we add feedback about applied lsn to replication stream(in this case it's force update status)" + "after restart consume changes via this slot should be started from last success lsn that " + "we send before via force status update, that why we wait consume both transaction without duplicates", result, equalTo(wait)); } @Test public void testReplicationRestartFromLastFeedbackPositionParallelTransaction() throws Exception { PGConnection pgConnection = (PGConnection) replConnection; LogSequenceNumber startLSN = getCurrentLSN(); PGReplicationStream stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(startLSN) .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); Connection tx1Connection = TestUtil.openDB(); tx1Connection.setAutoCommit(false); Connection tx2Connection = TestUtil.openDB(); tx2Connection.setAutoCommit(false); Statement stTx1 = tx1Connection.createStatement(); Statement stTx2 = tx2Connection.createStatement(); stTx1.execute("BEGIN"); stTx2.execute("BEGIN"); stTx1.execute("insert into test_logic_table(name) values('first tx changes')"); stTx2.execute("insert into test_logic_table(name) values('second tx changes')"); tx1Connection.commit(); tx2Connection.commit(); tx1Connection.close(); tx2Connection.close(); List<String> consumedData = new ArrayList<String>(); consumedData.addAll(receiveMessageWithoutBlock(stream, 3)); stream.setFlushedLSN(stream.getLastReceiveLSN()); stream.setAppliedLSN(stream.getLastReceiveLSN()); stream.forceUpdateStatus(); //emulate replication break replConnection.close(); waitStopReplicationSlot(); replConnection = openReplicationConnection(); pgConnection = (PGConnection) replConnection; stream = pgConnection .getReplicationAPI() .replicationStream() .logical() .withSlotName(SLOT_NAME) .withStartPosition(LogSequenceNumber.INVALID_LSN) /* Invalid LSN indicate for start from restart lsn */ .withSlotOption("include-xids", false) .withSlotOption("skip-empty-xacts", true) .start(); Statement st = sqlConnection.createStatement(); st.execute("insert into test_logic_table(name) values('third tx changes')"); st.close(); consumedData.addAll(receiveMessageWithoutBlock(stream, 3)); String result = group(consumedData); String wait = group(Arrays.asList( "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:1 name[character varying]:'first tx changes'", "COMMIT", "BEGIN", "table public.test_logic_table: INSERT: pk[integer]:2 name[character varying]:'second tx changes'", "COMMIT" )); assertThat( "When we add feedback about applied lsn to replication stream(in this case it's force update status)" + "after restart consume changes via this slot should be started from last success lsn that " + "we send before via force status update, that why we wait consume both transaction without duplicates", result, equalTo(wait)); } private void waitStopReplicationSlot() throws SQLException, InterruptedException { while (true) { PreparedStatement statement = sqlConnection.prepareStatement( "select 1 from pg_replication_slots where slot_name = ? and active = true" ); statement.setString(1, SLOT_NAME); ResultSet rs = statement.executeQuery(); boolean active = rs.next(); rs.close(); statement.close(); if (!active) { return; } TimeUnit.MILLISECONDS.sleep(10); } } private int getKeepAliveTimeout() throws SQLException { Statement statement = sqlConnection.createStatement(); ResultSet resultSet = statement.executeQuery( "select setting, unit from pg_settings where name = 'wal_sender_timeout'"); int result = 0; if (resultSet.next()) { result = resultSet.getInt(1); String unit = resultSet.getString(2); if ("sec".equals(unit)) { result = (int) TimeUnit.SECONDS.toMillis(result); } } return result; } 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 String group(List<String> messages) { StringBuilder builder = new StringBuilder(); boolean isFirst = true; for (String str : messages) { if (isFirst) { isFirst = false; } else { builder.append("\n"); } builder.append(str); } return builder.toString(); } private List<String> receiveMessage(PGReplicationStream stream, int count) throws SQLException { List<String> result = new ArrayList<String>(count); for (int index = 0; index < count; index++) { result.add(toString(stream.read())); } return result; } 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 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); } }