/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.tests.integration.cluster.util;
import java.util.concurrent.locks.Lock;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Interceptor;
import org.apache.activemq.artemis.core.protocol.core.Channel;
import org.apache.activemq.artemis.core.protocol.core.ChannelHandler;
import org.apache.activemq.artemis.core.protocol.core.CommandConfirmationHandler;
import org.apache.activemq.artemis.core.protocol.core.CoreRemotingConnection;
import org.apache.activemq.artemis.core.protocol.core.Packet;
import org.apache.activemq.artemis.core.protocol.core.impl.PacketImpl;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReplicationResponseMessage;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReplicationResponseMessageV2;
import org.apache.activemq.artemis.core.protocol.core.impl.wireformat.ReplicationStartSyncMessage;
import org.apache.activemq.artemis.core.replication.ReplicationEndpoint;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.impl.SharedNothingBackupActivation;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
/**
* An interceptor to keep a replicated backup server from reaching "up-to-date" status.
* <p>
* There are 3 test scenarios for 'adding data to a remote backup':<br/>
* 1. sync data that already existed <br/>
* 2. adding `new` Journal updates (that arrived after the backup appeared) WHILE sync'ing happens<br/>
* 3. adding `new` Journal updates AFTER sync'ing is done<br/>
* <p>
* These 'withDelay' tests were created to test/verify data transfers of type 2. because there is so
* little data, if we don't delay the sync message, we cannot be sure we are testing scenario .2.
* because the sync will be done too fast.
* <p>
* One problem is that we can't add an interceptor to the backup before starting it. So we add the
* interceptor to the 'live' which will place a different {@link ChannelHandler} in the backup
* during the initialization of replication.
* <p>
* We need to hijack the replication channel handler, because we need to
* <ol>
* <li>send an early answer to the {@link PacketImpl#REPLICATION_SYNC_FILE} packet that signals
* being up-to-date
* <li>not send an answer to it, when we deliver the packet later.
* </ol>
*/
public class BackupSyncDelay implements Interceptor {
private final ReplicationChannelHandler handler;
private final ActiveMQServer backup;
private final ActiveMQServer live;
public void deliverUpToDateMsg() {
live.getRemotingService().removeIncomingInterceptor(this);
if (backup.isStarted())
handler.deliver();
}
/**
* @param backup
* @param live
* @param packetCode which packet is going to be intercepted.
*/
public BackupSyncDelay(ActiveMQServer backup, ActiveMQServer live, byte packetCode) {
this.backup = backup;
this.live = live;
live.getRemotingService().addIncomingInterceptor(this);
handler = new ReplicationChannelHandler(packetCode);
}
/**
* @param backupServer
* @param liveServer
*/
public BackupSyncDelay(TestableServer backupServer, TestableServer liveServer) {
this(backupServer.getServer(), liveServer.getServer(), PacketImpl.REPLICATION_START_FINISH_SYNC);
}
@Override
public boolean intercept(Packet packet, RemotingConnection connection) throws ActiveMQException {
if (packet.getType() == PacketImpl.BACKUP_REGISTRATION) {
try {
SharedNothingBackupActivation activation = (SharedNothingBackupActivation) backup.getActivation();
ReplicationEndpoint repEnd = activation.getReplicationEndpoint();
handler.addSubHandler(repEnd);
Channel repChannel = repEnd.getChannel();
repChannel.setHandler(handler);
handler.setChannel(repChannel);
live.getRemotingService().removeIncomingInterceptor(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return true;
}
public static class ReplicationChannelHandler implements ChannelHandler {
public ReplicationChannelHandler(byte type) {
this.typeToIntercept = type;
}
private ReplicationEndpoint handler;
private Packet onHold;
private Channel channel;
public volatile boolean deliver;
private volatile boolean delivered;
private boolean receivedUpToDate;
private boolean mustHold = true;
private final byte typeToIntercept;
public void addSubHandler(ReplicationEndpoint handler) {
this.handler = handler;
}
public synchronized void deliver() {
deliver = true;
if (!receivedUpToDate)
return;
if (delivered)
return;
if (onHold == null) {
throw new NullPointerException("Don't have the 'sync is done' packet to deliver");
}
// Use wrapper to avoid sending a response
ChannelWrapper wrapper = new ChannelWrapper(channel);
handler.setChannel(wrapper);
try {
handler.handlePacket(onHold);
delivered = true;
} finally {
handler.setChannel(channel);
channel.setHandler(handler);
onHold = null;
}
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public void setHold(boolean hold) {
mustHold = hold;
}
@Override
public synchronized void handlePacket(Packet packet) {
if (onHold != null && deliver) {
deliver();
}
if (typeToIntercept == PacketImpl.REPLICATION_START_FINISH_SYNC) {
if (packet.getType() == PacketImpl.REPLICATION_START_FINISH_SYNC && mustHold) {
ReplicationStartSyncMessage syncMsg = (ReplicationStartSyncMessage) packet;
if (syncMsg.isSynchronizationFinished() && !deliver) {
receivedUpToDate = true;
assert onHold == null;
onHold = packet;
PacketImpl response = new ReplicationResponseMessageV2(true);
channel.send(response);
return;
}
}
} else if (typeToIntercept == packet.getType()) {
channel.send(new ReplicationResponseMessage());
return;
}
handler.handlePacket(packet);
}
}
public static class ChannelWrapper implements Channel {
private final Channel channel;
public ChannelWrapper(Channel channel) {
this.channel = channel;
}
@Override
public String toString() {
return "ChannelWrapper(" + channel + ")";
}
@Override
public long getID() {
return channel.getID();
}
@Override
public boolean send(Packet packet) {
// no-op
// channel.send(packet);
return true;
}
@Override
public boolean sendBatched(Packet packet) {
throw new UnsupportedOperationException();
}
@Override
public boolean sendAndFlush(Packet packet) {
throw new UnsupportedOperationException();
}
@Override
public Packet sendBlocking(Packet packet, byte expected) throws ActiveMQException {
throw new UnsupportedOperationException();
}
@Override
public void setHandler(ChannelHandler handler) {
throw new UnsupportedOperationException();
}
@Override
public ChannelHandler getHandler() {
throw new UnsupportedOperationException();
}
@Override
public void close() {
throw new UnsupportedOperationException();
}
@Override
public void transferConnection(CoreRemotingConnection newConnection) {
throw new UnsupportedOperationException();
}
@Override
public int getReconnectID() {
return 0;
}
@Override
public boolean send(Packet packet, int reconnectID) {
return false;
}
@Override
public Packet sendBlocking(Packet packet, int reconnectID, byte expectedPacket) throws ActiveMQException {
return null;
}
@Override
public void replayCommands(int lastConfirmedCommandID) {
throw new UnsupportedOperationException();
}
@Override
public int getLastConfirmedCommandID() {
throw new UnsupportedOperationException();
}
@Override
public void lock() {
throw new UnsupportedOperationException();
}
@Override
public void unlock() {
throw new UnsupportedOperationException();
}
@Override
public void returnBlocking() {
throw new UnsupportedOperationException();
}
@Override
public void returnBlocking(Throwable cause) {
throw new UnsupportedOperationException();
}
@Override
public Lock getLock() {
throw new UnsupportedOperationException();
}
@Override
public CoreRemotingConnection getConnection() {
throw new UnsupportedOperationException();
}
@Override
public void confirm(Packet packet) {
throw new UnsupportedOperationException();
}
@Override
public void setCommandConfirmationHandler(CommandConfirmationHandler handler) {
throw new UnsupportedOperationException();
}
@Override
public void flushConfirmations() {
throw new UnsupportedOperationException();
}
@Override
public void handlePacket(Packet packet) {
throw new UnsupportedOperationException();
}
@Override
public void clearCommands() {
throw new UnsupportedOperationException();
}
@Override
public int getConfirmationWindowSize() {
throw new UnsupportedOperationException();
}
@Override
public void setTransferring(boolean transferring) {
throw new UnsupportedOperationException();
}
@Override
public boolean supports(byte packetID) {
return true;
}
@Override
public boolean supports(byte packetID, int version) {
return true;
}
}
}