/*
* Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved.
*
* Licensed 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 com.hazelcast.topic.impl.reliable;
import com.hazelcast.core.ExecutionCallback;
import com.hazelcast.core.HazelcastInstanceNotActiveException;
import com.hazelcast.core.ICompletableFuture;
import com.hazelcast.core.Message;
import com.hazelcast.instance.MemberImpl;
import com.hazelcast.internal.cluster.ClusterService;
import com.hazelcast.logging.ILogger;
import com.hazelcast.ringbuffer.ReadResultSet;
import com.hazelcast.ringbuffer.Ringbuffer;
import com.hazelcast.ringbuffer.StaleSequenceException;
import com.hazelcast.spi.NodeEngine;
import com.hazelcast.spi.exception.DistributedObjectDestroyedException;
import com.hazelcast.spi.serialization.SerializationService;
import com.hazelcast.topic.ReliableMessageListener;
/**
* An {@link com.hazelcast.core.ExecutionCallback} that will try to read an item from the ringbuffer or blocks
* if no item is available. All data that are read is pushed into the {@link com.hazelcast.core.MessageListener}. It is
* a self-perpetuating stream of async calls.
* <p/>
* The ReliableTopicRunner keeps track of the sequence.
*/
class ReliableMessageListenerRunner<E> implements ExecutionCallback<ReadResultSet<ReliableTopicMessage>> {
final ReliableMessageListener<E> listener;
private final Ringbuffer<ReliableTopicMessage> ringbuffer;
private final String topicName;
private final SerializationService serializationService;
private final ClusterService clusterService;
private final ILogger logger;
private final String id;
private final ReliableTopicProxy<E> proxy;
private long sequence;
private volatile boolean cancelled;
private final int batchSze;
public ReliableMessageListenerRunner(String id,
ReliableMessageListener<E> listener,
ReliableTopicProxy<E> proxy) {
this.id = id;
this.listener = listener;
this.proxy = proxy;
this.ringbuffer = proxy.ringbuffer;
this.topicName = proxy.getName();
NodeEngine nodeEngine = proxy.getNodeEngine();
this.serializationService = nodeEngine.getSerializationService();
this.clusterService = nodeEngine.getClusterService();
this.logger = nodeEngine.getLogger(ReliableMessageListenerRunner.class);
this.batchSze = proxy.topicConfig.getReadBatchSize();
// we are going to listen to next publication. We don't care about what already has been published.
long initialSequence = listener.retrieveInitialSequence();
if (initialSequence == -1) {
initialSequence = ringbuffer.tailSequence() + 1;
}
this.sequence = initialSequence;
}
void next() {
if (cancelled) {
return;
}
ICompletableFuture<ReadResultSet<ReliableTopicMessage>> f = ringbuffer.readManyAsync(sequence, 1, batchSze, null);
f.andThen(this, proxy.executor);
}
// This method is called from the provided executor.
@Override
public void onResponse(ReadResultSet<ReliableTopicMessage> result) {
// we process all messages in batch. So we don't release the thread and reschedule ourselves;
// but we'll process whatever was received in 1 go.
for (Object item : result) {
ReliableTopicMessage message = (ReliableTopicMessage) item;
if (cancelled) {
return;
}
try {
listener.storeSequence(sequence);
process(message);
} catch (Throwable t) {
if (terminate(t)) {
cancel();
return;
}
}
sequence++;
}
next();
}
private void process(ReliableTopicMessage message) throws Throwable {
proxy.localTopicStats.incrementReceives();
listener.onMessage(toMessage(message));
}
private Message<E> toMessage(ReliableTopicMessage m) {
MemberImpl member = clusterService.getMember(m.getPublisherAddress());
E payload = serializationService.toObject(m.getPayload());
return new Message<E>(topicName, payload, m.getPublishTime(), member);
}
// This method is called from the provided executor.
@Override
public void onFailure(Throwable t) {
if (cancelled) {
return;
}
if (t instanceof StaleSequenceException) {
StaleSequenceException staleSequenceException = (StaleSequenceException) t;
if (listener.isLossTolerant()) {
if (logger.isFinestEnabled()) {
logger.finest("MessageListener " + listener + " on topic: " + topicName + " ran into a stale sequence. "
+ "Jumping from oldSequence: " + sequence
+ " to sequence: " + staleSequenceException.getHeadSeq());
}
sequence = staleSequenceException.getHeadSeq();
next();
return;
}
logger.warning("Terminating MessageListener:" + listener + " on topic: " + topicName + ". "
+ "Reason: The listener was too slow or the retention period of the message has been violated. "
+ "head: " + staleSequenceException.getHeadSeq() + " sequence:" + sequence);
} else if (t instanceof HazelcastInstanceNotActiveException) {
if (logger.isFinestEnabled()) {
logger.finest("Terminating MessageListener " + listener + " on topic: " + topicName + ". "
+ " Reason: HazelcastInstance is shutting down");
}
} else if (t instanceof DistributedObjectDestroyedException) {
if (logger.isFinestEnabled()) {
logger.finest("Terminating MessageListener " + listener + " on topic: " + topicName + ". "
+ "Reason: Topic is destroyed");
}
} else {
logger.warning("Terminating MessageListener " + listener + " on topic: " + topicName + ". "
+ "Reason: Unhandled exception, message: " + t.getMessage(), t);
}
cancel();
}
void cancel() {
cancelled = true;
proxy.runnersMap.remove(id);
}
private boolean terminate(Throwable failure) {
if (cancelled) {
return true;
}
try {
boolean terminate = listener.isTerminal(failure);
if (terminate) {
logger.warning("Terminating MessageListener " + listener + " on topic: " + topicName + ". "
+ "Reason: Unhandled exception, message: " + failure.getMessage(), failure);
} else {
if (logger.isFinestEnabled()) {
logger.finest("MessageListener " + listener + " on topic: " + topicName + " ran into an exception:"
+ " message:" + failure.getMessage(), failure);
}
}
return terminate;
} catch (Throwable t) {
logger.warning("Terminating messageListener:" + listener + " on topic: " + topicName + ". "
+ "Reason: Unhandled exception while calling ReliableMessageListener.isTerminal() method", t);
return true;
}
}
}