All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opendaylight.controller.cluster.messaging.MessageSlicer Maven / Gradle / Ivy

There is a newer version: 10.0.4
Show newest version
/*
 * Copyright (c) 2017 Inocybe Technologies and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */
package org.opendaylight.controller.cluster.messaging;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import akka.actor.ActorRef;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalNotification;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.controller.cluster.io.FileBackedOutputStream;
import org.opendaylight.controller.cluster.io.FileBackedOutputStreamFactory;
import org.opendaylight.yangtools.concepts.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class slices messages into smaller chunks. {@link MessageAssembler} is used to re-assemble the messages.
 *
 * @author Thomas Pantelis
 * @see MessageAssembler
 */
public class MessageSlicer implements AutoCloseable {
    private static final Logger LOG = LoggerFactory.getLogger(MessageSlicer.class);
    private static final AtomicLong SLICER_ID_COUNTER = new AtomicLong(1);
    public static final int DEFAULT_MAX_SLICING_TRIES = 3;

    private final Cache> stateCache;
    private final FileBackedOutputStreamFactory fileBackedStreamFactory;
    private final int messageSliceSize;
    private final int maxSlicingTries;
    private final String logContext;
    private final long id;

    MessageSlicer(final Builder builder) {
        fileBackedStreamFactory = builder.fileBackedStreamFactory;
        messageSliceSize = builder.messageSliceSize;
        maxSlicingTries = builder.maxSlicingTries;

        id = SLICER_ID_COUNTER.getAndIncrement();
        logContext = builder.logContext + "_slicer-id-" + id;

        CacheBuilder> cacheBuilder =
                CacheBuilder.newBuilder().removalListener(this::stateRemoved);
        if (builder.expireStateAfterInactivityDuration > 0) {
            cacheBuilder = cacheBuilder.expireAfterAccess(builder.expireStateAfterInactivityDuration,
                    builder.expireStateAfterInactivityUnit);
        }
        stateCache = cacheBuilder.build();
    }

    @VisibleForTesting
    long getId() {
        return id;
    }

    /**
     * Returns a new Builder for creating MessageSlicer instances.
     *
     * @return a Builder instance
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Checks if the given message is handled by this class. If so, it should be forwarded to the
     * {@link #handleMessage(Object)} method
     *
     * @param message the message to check
     * @return true if handled, false otherwise
     */
    public static boolean isHandledMessage(final Object message) {
        return message instanceof MessageSliceReply;
    }

    /**
     * Slices a message into chunks based on the serialized size, the maximum message slice size and the given
     * options.
     *
     * @param options the SliceOptions
     * @return true if the message was sliced, false otherwise
     */
    public boolean slice(final SliceOptions options) {
        final Identifier identifier = options.getIdentifier();
        final Serializable message = options.getMessage();
        final FileBackedOutputStream fileBackedStream;
        if (message != null) {
            LOG.debug("{}: slice: identifier: {}, message: {}", logContext, identifier, message);

            requireNonNull(fileBackedStreamFactory,
                    "The FiledBackedStreamFactory must be set in order to call this slice method");

            // Serialize the message to a FileBackedOutputStream.
            fileBackedStream = fileBackedStreamFactory.newInstance();
            try (ObjectOutputStream out = new ObjectOutputStream(fileBackedStream)) {
                out.writeObject(message);
            } catch (IOException e) {
                LOG.debug("{}: Error serializing message for {}", logContext, identifier, e);
                fileBackedStream.cleanup();
                options.getOnFailureCallback().accept(e);
                return false;
            }
        } else {
            fileBackedStream = options.getFileBackedStream();
        }

        return initializeSlicing(options, fileBackedStream);
    }

    private boolean initializeSlicing(final SliceOptions options, final FileBackedOutputStream fileBackedStream) {
        final Identifier identifier = options.getIdentifier();
        MessageSliceIdentifier messageSliceId = new MessageSliceIdentifier(identifier, id);
        SlicedMessageState state = null;
        try {
            state = new SlicedMessageState<>(messageSliceId, fileBackedStream, messageSliceSize, maxSlicingTries,
                    options.getReplyTo(), options.getOnFailureCallback(), logContext);

            final Serializable message = options.getMessage();
            if (state.getTotalSlices() == 1 && message != null) {
                LOG.debug("{}: Message does not need to be sliced - sending original message", logContext);
                state.close();
                sendTo(options, message, options.getReplyTo());
                return false;
            }

            final MessageSlice firstSlice = getNextSliceMessage(state);

            LOG.debug("{}: Sending first slice: {}", logContext, firstSlice);

            stateCache.put(messageSliceId, state);
            sendTo(options, firstSlice, ActorRef.noSender());
            return true;
        } catch (IOException e) {
            LOG.error("{}: Error initializing SlicedMessageState for {}", logContext, identifier, e);
            if (state != null) {
                state.close();
            } else {
                fileBackedStream.cleanup();
            }

            options.getOnFailureCallback().accept(e);
            return false;
        }
    }

    private static void sendTo(final SliceOptions options, final Object message, final ActorRef sender) {
        if (options.getSendToRef() != null) {
            options.getSendToRef().tell(message, sender);
        } else {
            options.getSendToSelection().tell(message, sender);
        }
    }

    /**
     * Invoked to handle messages pertaining to this class.
     *
     * @param message the message
     * @return true if the message was handled, false otherwise
     */
    public boolean handleMessage(final Object message) {
        if (message instanceof MessageSliceReply sliceReply) {
            LOG.debug("{}: handleMessage: {}", logContext, sliceReply);
            return onMessageSliceReply(sliceReply);
        }

        return false;
    }

    /**
     * Checks for and removes sliced message state that has expired due to inactivity from the assembling component
     * on the other end.
     */
    public void checkExpiredSlicedMessageState() {
        if (stateCache.size() > 0) {
            stateCache.cleanUp();
        }
    }

    /**
     * Closes and removes all in-progress sliced message state.
     */
    @Override
    public void close() {
        LOG.debug("{}: Closing", logContext);
        stateCache.invalidateAll();
    }

    /**
     * Cancels all in-progress sliced message state that matches the given filter.
     *
     * @param filter filters by Identifier
     */
    public void cancelSlicing(final @NonNull Predicate filter) {
        stateCache.asMap().keySet().removeIf(
            messageSliceIdentifier -> filter.test(messageSliceIdentifier.getClientIdentifier()));
    }

    private static MessageSlice getNextSliceMessage(final SlicedMessageState state) throws IOException {
        final byte[] firstSliceBytes = state.getNextSlice();
        return new MessageSlice(state.getIdentifier(), firstSliceBytes, state.getCurrentSliceIndex(),
                state.getTotalSlices(), state.getLastSliceHashCode(), state.getReplyTarget());
    }

    private boolean onMessageSliceReply(final MessageSliceReply reply) {
        final Identifier identifier = reply.getIdentifier();
        if (!(identifier instanceof MessageSliceIdentifier sliceIdentifier) || sliceIdentifier.getSlicerId() != id) {
            return false;
        }

        final SlicedMessageState state = stateCache.getIfPresent(identifier);
        if (state == null) {
            LOG.warn("{}: SlicedMessageState not found for {}", logContext, reply);
            reply.getSendTo().tell(new AbortSlicing(identifier), ActorRef.noSender());
            return true;
        }

        synchronized (state) {
            try {
                final Optional failure = reply.getFailure();
                if (failure.isPresent()) {
                    LOG.warn("{}: Received failed {}", logContext, reply);
                    processMessageSliceException(failure.orElseThrow(), state, reply.getSendTo());
                    return true;
                }

                if (state.getCurrentSliceIndex() != reply.getSliceIndex()) {
                    LOG.warn("{}: Slice index {} in {} does not match expected index {}", logContext,
                            reply.getSliceIndex(), reply, state.getCurrentSliceIndex());
                    reply.getSendTo().tell(new AbortSlicing(identifier), ActorRef.noSender());
                    possiblyRetrySlicing(state, reply.getSendTo());
                    return true;
                }

                if (state.isLastSlice(reply.getSliceIndex())) {
                    LOG.debug("{}: Received last slice reply for {}", logContext, identifier);
                    removeState(identifier);
                } else {
                    final MessageSlice nextSlice = getNextSliceMessage(state);
                    LOG.debug("{}: Sending next slice: {}", logContext, nextSlice);
                    reply.getSendTo().tell(nextSlice, ActorRef.noSender());
                }
            } catch (IOException e) {
                LOG.warn("{}: Error processing {}", logContext, reply, e);
                fail(state, e);
            }
        }

        return true;
    }

    private void processMessageSliceException(final MessageSliceException exception,
            final SlicedMessageState state, final ActorRef sendTo) throws IOException {
        if (exception.isRetriable()) {
            possiblyRetrySlicing(state, sendTo);
        } else {
            fail(state, exception.getCause() != null ? exception.getCause() : exception);
        }
    }

    private void possiblyRetrySlicing(final SlicedMessageState state, final ActorRef sendTo)
            throws IOException {
        if (state.canRetry()) {
            LOG.info("{}: Retrying message slicing for {}", logContext, state.getIdentifier());
            state.reset();
            sendTo.tell(getNextSliceMessage(state), ActorRef.noSender());
        } else {
            String message = String.format("Maximum slicing retries reached for identifier %s - failing the message",
                    state.getIdentifier());
            LOG.warn(message);
            fail(state, new RuntimeException(message));
        }
    }

    private void removeState(final Identifier identifier) {
        LOG.debug("{}: Removing state for {}", logContext, identifier);
        stateCache.invalidate(identifier);
    }

    private void stateRemoved(final RemovalNotification> notification) {
        final SlicedMessageState state = notification.getValue();
        state.close();
        if (notification.wasEvicted()) {
            LOG.warn("{}: SlicedMessageState for {} was expired from the cache", logContext, notification.getKey());
            state.getOnFailureCallback().accept(new RuntimeException(String.format(
                    "The slicing state for message identifier %s was expired due to inactivity from the assembling "
                     + "component on the other end", state.getIdentifier())));
        } else {
            LOG.debug("{}: SlicedMessageState for {} was removed from the cache due to {}", logContext,
                    notification.getKey(), notification.getCause());
        }
    }

    private void fail(final SlicedMessageState state, final Throwable failure) {
        removeState(state.getIdentifier());
        state.getOnFailureCallback().accept(failure);
    }

    @VisibleForTesting
    boolean hasState(final Identifier forIdentifier) {
        boolean exists = stateCache.getIfPresent(forIdentifier) != null;
        stateCache.cleanUp();
        return exists;
    }

    public static class Builder {
        private FileBackedOutputStreamFactory fileBackedStreamFactory;
        private int messageSliceSize = -1;
        private long expireStateAfterInactivityDuration = -1;
        private TimeUnit expireStateAfterInactivityUnit = TimeUnit.MINUTES;
        private int maxSlicingTries = DEFAULT_MAX_SLICING_TRIES;
        private String logContext = "";

        /**
         * Sets the factory for creating FileBackedOutputStream instances used for streaming messages. This factory
         * is used by the {@link MessageSlicer#slice(SliceOptions)} method if a Serializable message is passed.
         * If Serializable messages aren't passed then the factory need not be set.
         *
         * @param newFileBackedStreamFactory the factory for creating FileBackedOutputStream instances
         * @return this Builder
         */
        public Builder fileBackedStreamFactory(final FileBackedOutputStreamFactory newFileBackedStreamFactory) {
            fileBackedStreamFactory = requireNonNull(newFileBackedStreamFactory);
            return this;
        }

        /**
         * Sets the maximum size (in bytes) for a message slice.
         *
         * @param newMessageSliceSize the maximum size (in bytes)
         * @return this Builder
         */
        public Builder messageSliceSize(final int newMessageSliceSize) {
            checkArgument(newMessageSliceSize > 0, "messageSliceSize must be > 0");
            messageSliceSize = newMessageSliceSize;
            return this;
        }

        /**
         * Sets the maximum number of tries for slicing a message. If exceeded, slicing fails. The default is
         * defined by {@link #DEFAULT_MAX_SLICING_TRIES}
         *
         * @param newMaxSlicingTries the maximum number of tries
         * @return this Builder
         */
        public Builder maxSlicingTries(final int newMaxSlicingTries) {
            checkArgument(newMaxSlicingTries > 0, "newMaxSlicingTries must be > 0");
            maxSlicingTries = newMaxSlicingTries;
            return this;
        }

        /**
         * Sets the duration and time unit whereby sliced message state is purged from the cache and the associated
         * failure callback is notified due to inactivity from the assembling component on the other end. By default,
         * state is not purged due to inactivity.
         *
         * @param duration the length of time after which a state entry is purged
         * @param unit the unit the duration is expressed in
         * @return this Builder
         */
        public Builder expireStateAfterInactivity(final long duration, final TimeUnit unit) {
            checkArgument(duration > 0, "duration must be > 0");
            expireStateAfterInactivityDuration = duration;
            expireStateAfterInactivityUnit = unit;
            return this;
        }

        /**
         * Sets the context for log messages.
         *
         * @param newLogContext the log context
         * @return this Builder
         */
        public Builder logContext(final String newLogContext) {
            logContext = requireNonNull(newLogContext);
            return this;
        }

        /**
         * Builds a new MessageSlicer instance.
         *
         * @return a new MessageSlicer
         */
        public MessageSlicer build() {
            return new MessageSlicer(this);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy