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

org.ow2.petals.bc.gateway.inbound.ConsumerDomain Maven / Gradle / Ivy

There is a newer version: 1.2.0
Show newest version
/**
 * Copyright (c) 2015-2016 Linagora
 * 
 * This program/library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at your
 * option) any later version.
 * 
 * This program/library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program/library; If not, see http://www.gnu.org/licenses/
 * for the GNU Lesser General Public License version 2.1.
 */
package org.ow2.petals.bc.gateway.inbound;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

import javax.jbi.JBIException;
import javax.jbi.servicedesc.ServiceEndpoint;
import javax.xml.namespace.QName;

import org.eclipse.jdt.annotation.Nullable;
import org.ow2.petals.bc.gateway.JBISender;
import org.ow2.petals.bc.gateway.BcGatewaySUManager;
import org.ow2.petals.bc.gateway.commons.AbstractDomain;
import org.ow2.petals.bc.gateway.commons.messages.ServiceKey;
import org.ow2.petals.bc.gateway.commons.messages.TransportedDocument;
import org.ow2.petals.bc.gateway.commons.messages.TransportedMessage;
import org.ow2.petals.bc.gateway.commons.messages.TransportedPropagations;
import org.ow2.petals.bc.gateway.jbidescriptor.generated.JbiConsumerDomain;
import org.ow2.petals.bc.gateway.utils.BcGatewayConsumeExtFlowStepBeginLogData;
import org.ow2.petals.commons.log.FlowAttributes;
import org.ow2.petals.commons.log.Level;
import org.ow2.petals.commons.log.PetalsExecutionContext;
import org.ow2.petals.component.framework.api.exception.PEtALSCDKException;
import org.ow2.petals.component.framework.jbidescriptor.generated.Consumes;
import org.ow2.petals.component.framework.logger.StepLogHelper;
import org.ow2.petals.component.framework.su.ServiceUnitDataHandler;
import org.w3c.dom.Document;

import io.netty.channel.Channel;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.concurrent.ScheduledFuture;

/**
 * There is one instance of this class per consumer domain in an SU configuration (jbi.xml).
 * 
 * It is responsible of notifying the channels (to consumer partner) of existing Consumes propagated to them.
 * 
 * The main idea is that a given consumer partner can contact us (a provider partner) with multiple connections (for
 * example in case of HA) and each of these needs to know what are the consumes propagated to them.
 * 
 * @author vnoel
 *
 */
public class ConsumerDomain extends AbstractDomain {

    /**
     * The {@link Consumes} propagated to this consumer domain.
     */
    private final Collection consumes;

    private final BcGatewaySUManager sum;

    private final TransportListener tl;

    /**
     * Lock for synchronising changes to {@link #channels}, {@link #open}, {@link #propagations} and {@link #jcd}.
     */
    private final Lock mainLock = new ReentrantLock(true);

    /**
     * The channels from this consumer domain (there can be more than one in case of HA or stuffs like that for example)
     */
    private final Set channels = new HashSet<>();

    private JbiConsumerDomain jcd;

    private volatile boolean open = false;

    private TransportedPropagations propagations = TransportedPropagations.EMPTY;

    /**
     * @see #polling
     */
    private final Lock pollingLock = new ReentrantLock();

    /**
     * Access is controlled by {@link #pollingLock} (except for the first poll that is controlled by {@link #mainLock} in
     * {@link #open()}).
     */
    private @Nullable ScheduledFuture polling = null;

    public ConsumerDomain(final ServiceUnitDataHandler handler, final TransportListener tl,
            final BcGatewaySUManager sum, final JbiConsumerDomain jcd, final Collection consumes,
            final JBISender sender, final Logger logger) throws PEtALSCDKException {
        super(sender, handler, logger);
        this.tl = tl;
        this.sum = sum;
        this.jcd = jcd;
        this.consumes = consumes;
        // Consumer partner will be able to connect to us (but no consumes will be propagated until open() is called)
        tl.register(jcd.getAuthName(), this);
    }

    @Override
    public String getId() {
        final String id = jcd.getId();
        assert id != null;
        return id;
    }

    public void reload(final JbiConsumerDomain newJCD) throws PEtALSCDKException {
        mainLock.lock();
        try {
            if (!jcd.getAuthName().equals(newJCD.getAuthName()) || !jcd.getCertificate().equals(newJCD.getCertificate())
                    || !jcd.getRemoteCertificate().equals(newJCD.getRemoteCertificate())
                    || !jcd.getKey().equals(newJCD.getKey()) || !jcd.getPassphrase().equals(newJCD.getPassphrase())) {
                if (!jcd.getAuthName().equals(newJCD.getAuthName())) {
                    tl.register(newJCD.getAuthName(), this);
                    tl.deregistrer(jcd.getAuthName());
                }
                jcd = newJCD;
                // this will disconnect clients and they should reconnect by themselves and use the new jcd and
                // authname!
                disconnect();
            }
        } finally {
            mainLock.unlock();
        }
    }

    public JbiConsumerDomain getJCD() {
        return jcd;
    }

    /**
     * Consumer partner will be disconnected
     */
    public void disconnect() {
        mainLock.lock();
        try {
            tl.deregistrer(jcd.getAuthName());
            for (final Channel c : channels) {
                // this will trigger deregisterChannel btw
                // Note: locking is ok, simply all the close will be sent before the deregistration is executed
                c.close();
            }
        } finally {
            mainLock.unlock();
        }
    }

    public void open() {
        mainLock.lock();
        try {
            open = true;

            sendPropagations(true);

            final long propagationPollingMaxDelay = jcd.getPropagationPollingMaxDelay();
            if (propagationPollingMaxDelay > 0) {
                final double propagationPollingAccel = jcd.getPropagationPollingAcceleration();
                if (logger.isLoggable(Level.CONFIG)) {
                    logger.config(
                            "Propagation refresh polling is enabled (max delay: " + propagationPollingMaxDelay
                                    + "ms, acceleration: " + propagationPollingAccel + ")");
                }
                scheduleNextPolling(5000, propagationPollingAccel, propagationPollingMaxDelay);
            } else {
                logger.config("Propagation refresh polling is disabled");
            }
        } finally {
            mainLock.unlock();
        }
    }

    private void scheduleNextPolling(final long currentDelay, final double accel, final long maxDelay) {
        final Runnable command = new Runnable() {
            @Override
            public void run() {
                final long nextDelay;
                if (accel > 1) {
                    nextDelay = Math.min((long) (currentDelay * accel), maxDelay);
                } else {
                    nextDelay = maxDelay;
                }

                try {
                    if (logger.isLoggable(Level.FINE)) {
                        logger.fine("Propagation refresh polling (next in " + nextDelay + "ms)");
                    }

                    // TODO catch exceptions?!
                    if (sendPropagations(false)) {
                        logger.info("Changes in propagations detected: refreshed!");
                    }

                } finally {
                    try {
                        // in case it was interrupted during the propagation sending
                        // this will also reset the interrupted flag
                        pollingLock.lockInterruptibly();
                        try {
                            // polling corresponds to the current task
                            // if it's null, it was cancelled (thus the test for
                            // isCancelled is not really needed but well...)
                            if (polling != null && !polling.isCancelled()) {
                                scheduleNextPolling(nextDelay, accel, maxDelay);
                            }
                        } finally {
                            pollingLock.unlock();
                        }
                    } catch (final InterruptedException e) {
                        // we were interrupted, it's ok, we stop there
                    }
                }
            }
        };

        final long delay;
        if (accel > 1) {
            delay = Math.min(currentDelay, maxDelay);
        } else {
            delay = maxDelay;
        }

        polling = (ScheduledFuture) GlobalEventExecutor.INSTANCE.schedule(command, delay, TimeUnit.MILLISECONDS)
                .addListener(new FutureListener() {
                    @Override
                    public void operationComplete(final @Nullable Future future) throws Exception {
                        assert future != null;
                        if (!future.isSuccess() && !future.isCancelled()) {
                            logger.log(Level.WARNING, "Error during propagation refresh polling", future.cause());
                        }
                    }
                });

    }

    public void close() {
        mainLock.lock();
        try {
            open = false;
            synchronized (pollingLock) {
                if (polling != null) {
                    // interruption will stop any current sending
                    polling.cancel(true);
                    polling = null;
                }
            }
            sendPropagations(TransportedPropagations.EMPTY);
        } finally {
            mainLock.unlock();
        }
    }

    public void registerChannel(final Channel c) {
        mainLock.lock();
        try {
            channels.add(c);

            if (open) {
                if (!sendPropagations(false)) {
                    // let's simply notify the new channel with the already known propagations
                    c.writeAndFlush(propagations);
                }
            }
        } finally {
            mainLock.unlock();
        }
    }

    public void deregisterChannel(final Channel c) {
        mainLock.lock();
        try {
            channels.remove(c);
        } finally {
            mainLock.unlock();
        }
    }

    public void refreshPropagations() {
        if (logger.isLoggable(Level.INFO)) {
            logger.info("Refreshing propagations");
        }
        sendPropagations(true);
    }

    /**
     * Note, this can be interrupted, for example if the polling is cancelled: in that case we simply returns
     * false and the interrupt flag is kept as set to be handled by the caller.
     * 
     * Sends the propagation to all channels if the domain is open, and either if force is true or if there
     * is some changes.
     * 
     * @return true if the propagations were sent.
     */
    private boolean sendPropagations(final boolean force) {
        mainLock.lock();
        try {
            if (!open) {
                return false;
            }
            boolean changes = false;
            final Map propagated = new HashMap<>();
            for (final Consumes c : consumes) {
                assert c != null;
                final String endpointName = c.getEndpointName();
                final QName interfaceName = c.getInterfaceName();
                assert interfaceName != null;

                final Collection allEndpoints = sum.getEndpointsForConsumes(c);
                assert allEndpoints != null;

                if (Thread.currentThread().isInterrupted()) {
                    return false;
                }

                // there will be one propagation per interface/service, even if the consumes only declares an interface!
                for (final Entry> entry : splitPerService(allEndpoints).entrySet()) {
                    final QName serviceName = entry.getKey();
                    assert serviceName != null;
                    final Collection endpoints = entry.getValue();

                    final TransportedDocument description = getFirstDescription(endpoints);

                    if (Thread.currentThread().isInterrupted()) {
                        return false;
                    }

                    final ServiceKey service = new ServiceKey(endpointName, serviceName, interfaceName);

                    if (!force) {
                        // careful, the map can contains null for some keys!
                        // so containsKey is not the same as get != null!
                        if (!propagations.getPropagations().containsKey(service)) {
                            // it means an endpoint was added/changed
                            changes = true;
                        } else if (description != null && propagations.getPropagations().get(service) == null) {
                            // if they have the same value, but the description changed from null to non-null,
                            // then it means something changed too!
                            changes = true;
                        }
                    }

                    propagated.put(service, description);

                    if (Thread.currentThread().isInterrupted()) {
                        return false;
                    }
                }
            }

            if (!force) {
                for (ServiceKey prevKey : propagations.getPropagations().keySet()) {
                    if (!propagated.containsKey(prevKey)) {
                        // it means endpoints were removed
                        changes = true;
                        break;
                    }
                }
            }

            final boolean sendPropagations = force || changes;
            if (sendPropagations) {
                sendPropagations(new TransportedPropagations(propagated));
            }

            return sendPropagations;
        } finally {
            mainLock.unlock();
        }
    }

    private static Map> splitPerService(
            final Collection endpoints) {
        final Map> res = new HashMap<>();
        for (final ServiceEndpoint endpoint : endpoints) {
            final QName serviceName = endpoint.getServiceName();
            Collection c = res.get(serviceName);
            if (c == null) {
                c = new LinkedList<>();
                res.put(serviceName, c);
            }
            c.add(endpoint);
        }

        assert validate(res);

        return res;
    }

    /**
     * each collection shouldn't be empty!
     */
    private static boolean validate(final Map> res) {

        for (final Collection c : res.values()) {
            if (c.isEmpty()) {
                return false;
            }
        }

        return true;
    }

    private void sendPropagations(final TransportedPropagations toPropagate) {
        propagations = toPropagate;
        for (final Channel c : channels) {
            assert c != null;
            c.writeAndFlush(toPropagate);
        }
    }

    private @Nullable TransportedDocument getFirstDescription(final Collection endpoints) {
        for (final ServiceEndpoint endpoint : endpoints) {
            try {
                Document desc = sum.getComponent().getContext().getEndpointDescriptor(endpoint);
                if (desc != null) {
                    return new TransportedDocument(desc);
                }
            } catch (final JBIException e) {
                logger.log(Level.WARNING, "Failed to retrieve endpoint descriptor of " + endpoint, e);
            }
        }

        return null;
    }

    @Override
    protected void logAfterReceivingFromChannel(final TransportedMessage m) {
        // acting as a provider partner, a new consumes ext starts here
        if (m.step == 1) {
            // we remember the step of the consumer partner through the correlated flow attributes
            logger.log(Level.MONIT, "",
                    new BcGatewayConsumeExtFlowStepBeginLogData(PetalsExecutionContext.getFlowAttributes(),
                            m.provideExtStep, getId()));
        }
    }

    @Override
    protected void logBeforeSendingToChannel(final TransportedMessage m) {
        // the end of the one started in ConsumerDomain.logBeforeSendingToNMR
        if (m.step == 2) {
            final FlowAttributes consumeExtStep = PetalsExecutionContext.getFlowAttributes();
            // it was set by the CDK
            assert consumeExtStep != null;
            StepLogHelper.addMonitExtEndOrFailureTrace(logger, m.exchange, consumeExtStep, true);
        }
    }
}