org.hyperledger.fabric.sdk.EventHub.java Source code

Java tutorial

Introduction

Here is the source code for org.hyperledger.fabric.sdk.EventHub.java

Source

/*
 *  Copyright 2016 IBM, DTCC, Fujitsu Australia Software Technology - 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 org.hyperledger.fabric.sdk;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

import javax.xml.bind.DatatypeConverter;

import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import io.grpc.ManagedChannel;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import io.netty.util.internal.StringUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hyperledger.fabric.protos.peer.EventsGrpc;
import org.hyperledger.fabric.protos.peer.PeerEvents;
import org.hyperledger.fabric.sdk.exception.CryptoException;
import org.hyperledger.fabric.sdk.exception.EventHubException;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.helper.Config;
import org.hyperledger.fabric.sdk.transaction.ProtoUtils;
import org.hyperledger.fabric.sdk.transaction.TransactionContext;

import static java.lang.String.format;
import static org.hyperledger.fabric.sdk.helper.Utils.checkGrpcUrl;

/**
 * Class to manage fabric events.
 * <p>
 * Feeds Channel event queues with events
 */

public class EventHub implements Serializable {
    private static final long serialVersionUID = 2882609588201108148L;
    private static final Config config = Config.getConfig();
    private transient String id = config.getNextID();
    private static final Log logger = LogFactory.getLog(EventHub.class);
    private static final boolean IS_TRACE_LEVEL = logger.isTraceEnabled();

    private static final long EVENTHUB_CONNECTION_WAIT_TIME = config.getEventHubConnectionWaitTime();
    private static final long EVENTHUB_RECONNECTION_WARNING_RATE = config.getEventHubReconnectionWarningRate();

    private final transient ExecutorService executorService;

    private final String url;
    private final String name;
    private final Properties properties;
    private transient ManagedChannel managedChannel;
    private transient boolean connected = false;
    private transient EventsGrpc.EventsStub events;
    private transient StreamObserver<PeerEvents.SignedEvent> sender;
    /**
     * Event queue for all events from eventhubs in the channel
     */
    private transient Channel.ChannelEventQue eventQue;
    private transient long connectedTime = 0L; // 0 := never connected
    private transient boolean shutdown = false;
    private Channel channel;
    private transient TransactionContext transactionContext;
    private transient byte[] clientTLSCertificateDigest;
    private transient long reconnectCount;
    private transient long lastBlockNumber;
    private transient BlockEvent lastBlockEvent;
    private String channelName;

    /**
     * Get disconnected time.
     *
     * @return Time in milli seconds disconnect occurred. Zero if never disconnected
     */
    public long getDisconnectedTime() {
        return disconnectedTime;
    }

    private long disconnectedTime;

    /**
     * Is event hub connected.
     *
     * @return boolean if true event hub is connected.
     */
    public boolean isConnected() {
        return connected;
    }

    String getStatus() {

        StringBuilder sb = new StringBuilder(1000);
        sb.append(toString()).append(", connected: ").append(connected);
        ManagedChannel lmanagedChannel = managedChannel;
        if (lmanagedChannel == null) {
            sb.append("managedChannel: null");
        } else {
            sb.append(", isShutdown: ").append(lmanagedChannel.isShutdown());
            sb.append(", isTerminated: ").append(lmanagedChannel.isTerminated());
            sb.append(", state: ").append("" + lmanagedChannel.getState(false));
        }

        return sb.toString();
    }

    /**
     * Get last connect time.
     *
     * @return Time in milli seconds the event hub last connected. Zero if never connected.
     */
    public long getConnectedTime() {
        return connectedTime;
    }

    /**
     * Get last attempt time to connect the event hub.
     *
     * @return Last attempt time to connect the event hub in milli seconds. Zero when never attempted.
     */

    public long getLastConnectedAttempt() {
        return lastConnectedAttempt;
    }

    private long lastConnectedAttempt;

    EventHub(String name, String grpcURL, ExecutorService executorService, Properties properties)
            throws InvalidArgumentException {

        Exception e = checkGrpcUrl(grpcURL);
        if (e != null) {
            throw new InvalidArgumentException("Bad event hub url.", e);

        }

        if (StringUtil.isNullOrEmpty(name)) {
            throw new InvalidArgumentException("Invalid name for eventHub");
        }

        this.url = grpcURL;
        this.name = name;
        this.executorService = executorService;
        this.properties = properties == null ? null : (Properties) properties.clone(); //keep our own copy.
        logger.debug("Created " + toString());
    }

    /**
     * Create a new instance.
     *
     * @param name
     * @param url
     * @param properties
     * @return
     */

    static EventHub createNewInstance(String name, String url, ExecutorService executorService,
            Properties properties) throws InvalidArgumentException {
        return new EventHub(name, url, executorService, properties);
    }

    /**
     * Event hub name
     *
     * @return event hub name
     */

    public String getName() {
        return name;
    }

    /**
     * Event hub properties
     *
     * @return Event hub properties
     * @see HFClient#newEventHub(String, String, Properties)
     */
    public Properties getProperties() {
        return properties == null ? null : (Properties) properties.clone();
    }

    private transient StreamObserver<PeerEvents.Event> eventStream = null; // Saved here to avoid potential garbage collection

    synchronized boolean connect(final TransactionContext transactionContext) throws EventHubException {
        return connect(transactionContext, false);
    }

    synchronized boolean connect(final TransactionContext transactionContext, final boolean reconnection)
            throws EventHubException {
        if (connected) {
            logger.warn(format("%s already connected.", toString()));
            return true;
        }
        eventStream = null;

        final CountDownLatch finishLatch = new CountDownLatch(1);

        logger.debug(format("%s is connecting.", toString()));

        lastConnectedAttempt = System.currentTimeMillis();

        Endpoint endpoint = Endpoint.createEndpoint(url, properties);
        managedChannel = endpoint.getChannelBuilder().build();

        clientTLSCertificateDigest = endpoint.getClientTLSCertificateDigest();

        events = EventsGrpc.newStub(managedChannel);

        final ArrayList<Throwable> threw = new ArrayList<>();

        final StreamObserver<PeerEvents.Event> eventStreamLocal = new StreamObserver<PeerEvents.Event>() {
            @Override
            public void onNext(PeerEvents.Event event) {

                logger.debug(
                        format("%s got  event type: %s", EventHub.this.toString(), event.getEventCase().name()));

                if (event.getEventCase() == PeerEvents.Event.EventCase.BLOCK) {
                    try {

                        BlockEvent blockEvent = new BlockEvent(EventHub.this, event);

                        logger.trace(format("%s got block number: %d", EventHub.this.toString(),
                                blockEvent.getBlockNumber()));
                        setLastBlockSeen(blockEvent);

                        eventQue.addBEvent(blockEvent); //add to channel queue
                    } catch (InvalidProtocolBufferException e) {
                        EventHubException eventHubException = new EventHubException(
                                format("%s onNext error %s", this, e.getMessage()), e);
                        logger.error(eventHubException.getMessage());
                        threw.add(eventHubException);
                    }
                } else if (event.getEventCase() == PeerEvents.Event.EventCase.REGISTER) {

                    if (reconnectCount > 1) {
                        logger.info(format("%s has reconnecting after %d attempts", EventHub.this.toString(),
                                reconnectCount));
                    }

                    connected = true;
                    connectedTime = System.currentTimeMillis();
                    reconnectCount = 0L;

                    finishLatch.countDown();
                } else {
                    logger.error(format("%s got a unexpected block type: %s", EventHub.this.toString(),
                            event.getEventCase().name()));
                }
            }

            @Override
            public void onError(Throwable t) {
                connected = false;
                eventStream = null;
                disconnectedTime = System.currentTimeMillis();
                if (shutdown) { //IF we're shutdown don't try anything more.
                    logger.trace(format("%s was shutdown.", EventHub.this.toString()));

                    finishLatch.countDown();
                    return;
                }

                final ManagedChannel lmanagedChannel = managedChannel;

                final boolean isTerminated = lmanagedChannel == null ? true : lmanagedChannel.isTerminated();
                final boolean isChannelShutdown = lmanagedChannel == null ? true : lmanagedChannel.isShutdown();

                if (EVENTHUB_RECONNECTION_WARNING_RATE > 1
                        && reconnectCount % EVENTHUB_RECONNECTION_WARNING_RATE == 1) {
                    logger.warn(format("%s terminated is %b shutdown is %b, retry count %d  has error %s.",
                            EventHub.this.toString(), isTerminated, isChannelShutdown, reconnectCount,
                            t.getMessage()));
                } else {
                    logger.trace(format("%s terminated is %b shutdown is %b, retry count %d  has error %s.",
                            EventHub.this.toString(), isTerminated, isChannelShutdown, reconnectCount,
                            t.getMessage()));
                }

                finishLatch.countDown();

                //              logger.error("Error in stream: " + t.getMessage(), new EventHubException(t));
                if (t instanceof StatusRuntimeException) {
                    StatusRuntimeException sre = (StatusRuntimeException) t;
                    Status sreStatus = sre.getStatus();
                    if (EVENTHUB_RECONNECTION_WARNING_RATE > 1
                            && reconnectCount % EVENTHUB_RECONNECTION_WARNING_RATE == 1) {
                        logger.warn(format("%s :StatusRuntimeException Status %s.  Description %s ", EventHub.this,
                                sreStatus + "", sreStatus.getDescription()));
                    } else {
                        logger.trace(format("%s :StatusRuntimeException Status %s.  Description %s ", EventHub.this,
                                sreStatus + "", sreStatus.getDescription()));
                    }

                    try {
                        reconnect();
                    } catch (Exception e) {
                        logger.warn(
                                format("%s Failed shutdown msg:  %s", EventHub.this.toString(), e.getMessage()));
                    }

                }

            }

            @Override
            public void onCompleted() {

                logger.debug(format("Stream completed %s", EventHub.this.toString()));
                finishLatch.countDown();

            }
        };

        sender = events.chat(eventStreamLocal);
        try {
            blockListen(transactionContext);
        } catch (Exception e) {
            throw new EventHubException(e);
        }

        try {

            //On reconnection don't wait here.

            if (!reconnection && !finishLatch.await(EVENTHUB_CONNECTION_WAIT_TIME, TimeUnit.MILLISECONDS)) {

                logger.warn(format("%s failed to connect in %s ms.", toString(), EVENTHUB_CONNECTION_WAIT_TIME));

            } else {
                logger.trace(format("%s done waiting for reply!", toString()));
            }

        } catch (InterruptedException e) {
            logger.error(e);
        }

        logger.debug(format("%s connect is done with connect status: %b ", toString(), connected));

        if (connected) {
            eventStream = eventStreamLocal;
        }

        return connected;

    }

    private void reconnect() throws EventHubException {

        final ManagedChannel lmanagedChannel = managedChannel;

        if (lmanagedChannel != null) {
            managedChannel = null;
            lmanagedChannel.shutdownNow();
        }

        EventHubDisconnected ldisconnectedHandler = disconnectedHandler;
        if (!shutdown && null != ldisconnectedHandler) {
            ++reconnectCount;
            ldisconnectedHandler.disconnected(this);

        }

    }

    private void blockListen(TransactionContext transactionContext)
            throws CryptoException, InvalidArgumentException {

        this.transactionContext = transactionContext;

        PeerEvents.Register register = PeerEvents.Register.newBuilder()
                .addEvents(PeerEvents.Interest.newBuilder().setEventType(PeerEvents.EventType.BLOCK).build())
                .build();
        PeerEvents.Event.Builder blockEventBuilder = PeerEvents.Event.newBuilder().setRegister(register)
                .setCreator(transactionContext.getIdentity().toByteString())
                .setTimestamp(ProtoUtils.getCurrentFabricTimestamp());

        if (null != clientTLSCertificateDigest) {
            logger.trace("Setting clientTLSCertificate digest for event registration to "
                    + DatatypeConverter.printHexBinary(clientTLSCertificateDigest));
            blockEventBuilder.setTlsCertHash(ByteString.copyFrom(clientTLSCertificateDigest));

        }

        ByteString blockEventByteString = blockEventBuilder.build().toByteString();

        PeerEvents.SignedEvent signedBlockEvent = PeerEvents.SignedEvent.newBuilder()
                .setEventBytes(blockEventByteString)
                .setSignature(transactionContext.signByteString(blockEventByteString.toByteArray())).build();
        sender.onNext(signedBlockEvent);
    }

    /**
     * Get the GRPC URL used to connect.
     *
     * @return GRPC URL.
     */
    public String getUrl() {
        return url;
    }

    /**
     * Set the channel queue that will receive events
     *
     * @param eventQue
     */
    void setEventQue(Channel.ChannelEventQue eventQue) {
        this.eventQue = eventQue;
    }

    @Override
    public String toString() {
        return "EventHub{" + "id: " + id + ", name: " + getName() + ", channelName: " + channelName + ", url: "
                + getUrl() + "}";
    }

    public synchronized void shutdown() {
        if (shutdown) {
            return;
        }
        logger.trace(toString() + " being shutdown.");
        shutdown = true;
        lastBlockEvent = null;
        lastBlockNumber = 0;
        connected = false;
        disconnectedHandler = null;
        eventStream = null;
        final ManagedChannel lmanagedChannel = managedChannel;
        managedChannel = null;
        if (lmanagedChannel != null) {
            lmanagedChannel.shutdownNow();
        }
    }

    void setChannel(Channel channel) throws InvalidArgumentException {
        if (channel == null) {
            throw new InvalidArgumentException("setChannel Channel can not be null");
        }

        if (null != channelName) {
            throw new InvalidArgumentException(
                    format("Can not add event hub  %s to channel %s because it already belongs to channel %s.",
                            name, channel.getName(), channelName));
        }
        logger.debug(toString() + " set to channel: " + channel);
        this.channelName = channel.getName();
    }

    private synchronized void setLastBlockSeen(BlockEvent lastBlockSeen) {
        long newLastBlockNumber = lastBlockSeen.getBlockNumber();
        // overkill but make sure.
        if (lastBlockNumber < newLastBlockNumber) {
            lastBlockNumber = newLastBlockNumber;
            this.lastBlockEvent = lastBlockSeen;
            if (IS_TRACE_LEVEL) {
                logger.trace(toString() + " last block seen: " + lastBlockNumber);
            }
        }
    }

    /**
     * Eventhub disconnection notification interface
     */
    public interface EventHubDisconnected {

        /**
         * Called when a disconnect is detected.
         *
         * @param eventHub
         * @throws EventHubException
         */
        void disconnected(EventHub eventHub) throws EventHubException;

    }

    /**
     * Default reconnect event hub implementation.  Applications are free to replace
     */

    protected transient EventHubDisconnected disconnectedHandler = new EventHub.EventHubDisconnected() {
        @Override
        public synchronized void disconnected(final EventHub eventHub) {
            if (reconnectCount == 1) {
                logger.warn(format("%s detected disconnect.", eventHub.toString()));
            }

            executorService.execute(() -> {

                try {
                    Thread.sleep(500);

                    if (transactionContext == null) {
                        logger.warn(EventHub.this.toString() + " reconnect failed with no user context");
                        return;
                    }

                    eventHub.connect(transactionContext, true);

                } catch (Exception e) {

                    logger.warn(format("Failed %s to reconnect. %s", toString(), e.getMessage()));

                }

            });

        }
    };

    /**
     * Set class to handle Event hub disconnects
     *
     * @param newEventHubDisconnectedHandler New handler to replace.  If set to null no retry will take place.
     * @return the old handler.
     */

    public EventHubDisconnected setEventHubDisconnectedHandler(
            EventHubDisconnected newEventHubDisconnectedHandler) {
        EventHubDisconnected ret = disconnectedHandler;
        disconnectedHandler = newEventHubDisconnectedHandler;
        return ret;
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        id = config.getNextID();
    }

    public void finalize() throws Throwable {
        logger.trace(format("%s finalized", toString()));
        shutdown();
        super.finalize();

    }

}