org.attribyte.api.pubsub.impl.server.BroadcastServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.attribyte.api.pubsub.impl.server.BroadcastServlet.java

Source

/*
 * Copyright 2010, 2014 Attribyte, LLC
 * 
 * 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.attribyte.api.pubsub.impl.server;

import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Charsets;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import org.attribyte.api.DatastoreException;
import org.attribyte.api.Logger;
import org.attribyte.api.http.Header;
import org.attribyte.api.http.Response;
import org.attribyte.api.http.ResponseBuilder;
import org.attribyte.api.http.impl.BasicAuthScheme;
import org.attribyte.api.http.impl.servlet.Bridge;
import org.attribyte.api.pubsub.BasicAuthFilter;
import org.attribyte.api.pubsub.HubDatastore;
import org.attribyte.api.pubsub.HubEndpoint;
import org.attribyte.api.pubsub.Notification;
import org.attribyte.api.pubsub.NotificationMetrics;
import org.attribyte.api.pubsub.Topic;
import org.attribyte.api.pubsub.impl.server.util.NotificationRecord;
import org.attribyte.api.pubsub.impl.server.util.ServerUtil;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

@SuppressWarnings("serial")
/**
 * A servlet that immediately queues notifications for broadcast
 * to subscribers.
 */
public class BroadcastServlet extends ServletBase implements NotificationRecord.Source {

    /**
     * The default maximum body size (1MB).
     */
    public static final int DEFAULT_MAX_BODY_BYTES = 1024 * 1000;

    /**
     * The default for topic auto-create (false).
     */
    public static final boolean DEFAULT_AUTOCREATE_TOPICS = false;

    /**
     * Creates a servlet with a maximum body size of 1MB and topics that must exist
     * before notifications are accepted.
     * @param endpoint The hub endpoint.
     * @param logger A logger.
     * @param filters A list of filters to be applied.
     * @param topicCache A topic cache.
     * @param replicationTopic A system topic to which all notifications are replicated. May be <code>null</code>.
     * @param maxSavedNotifications The maximum number of notifications saved in-memory for debugging purposes.
     * @param jsonEnabled If <code>true</code> a JSON body will be sent with the notification response.
     */
    public BroadcastServlet(final HubEndpoint endpoint, final Logger logger, final List<BasicAuthFilter> filters,
            final Cache<String, Topic> topicCache, final Topic replicationTopic, final int maxSavedNotifications,
            final boolean jsonEnabled) {
        this(endpoint, DEFAULT_MAX_BODY_BYTES, DEFAULT_AUTOCREATE_TOPICS, logger, filters, topicCache,
                replicationTopic, maxSavedNotifications, jsonEnabled);
    }

    /**
     * Creates a servlet with a specified maximum body size.
     * @param endpoint The hub endpoint.
     * @param maxBodyBytes The maximum size of accepted for a notification body.
     * @param autocreateTopics If <code>true</code>, topics will be automatically created if they do not exist.
     * @param logger The logger.
     * @param filters A list of filters to be applied.
     * @param topicCache A topic cache.
     * @param replicationTopic A system topic to which all notifications are replicated. May be <code>null</code>.
     * @param maxSavedNotifications The maximum number of notifications saved in-memory for debugging purposes.
     * @param jsonEnabled If <code>true</code> a JSON body will be sent with the notification response.
     */
    public BroadcastServlet(final HubEndpoint endpoint, final int maxBodyBytes, final boolean autocreateTopics,
            final Logger logger, final List<BasicAuthFilter> filters, final Cache<String, Topic> topicCache,
            final Topic replicationTopic, final int maxSavedNotifications, final boolean jsonEnabled) {
        this.endpoint = endpoint;
        this.datastore = endpoint.getDatastore();
        this.maxBodyBytes = maxBodyBytes;
        this.autocreateTopics = autocreateTopics;
        this.logger = logger;
        this.filters = filters != null ? ImmutableList.copyOf(filters) : ImmutableList.<BasicAuthFilter>of();
        this.topicCache = topicCache;
        this.replicationTopic = replicationTopic;
        this.maxSavedNotifications = maxSavedNotifications;
        this.jsonEnabled = jsonEnabled;

        final int queueLimit = maxSavedNotifications * 2;
        final int drainTriggerLimit = queueLimit - maxSavedNotifications / 2;

        this.recentNotifications = maxSavedNotifications > 0 ? new ArrayBlockingQueue<>(queueLimit) : null;
        this.recentNotificationsSize = new AtomicInteger();
        if (recentNotifications != null) {
            this.recentNotificationsMonitor = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            int currSize = recentNotificationsSize.get();
                            if (currSize >= drainTriggerLimit) {
                                int maxDrained = currSize - maxSavedNotifications;
                                List<NotificationRecord> drain = Lists.newArrayListWithCapacity(maxDrained);
                                int numDrained = recentNotifications.drainTo(drain, maxSavedNotifications);
                                recentNotificationsSize.addAndGet(-1 * numDrained);
                            } else {
                                Thread.sleep(100L);
                            }
                        } catch (InterruptedException ie) {
                            return;
                        }
                    }
                }
            });
            this.recentNotificationsMonitor.setName("recent-notifications-monitor");
            this.recentNotificationsMonitor.setDaemon(true);
            this.recentNotificationsMonitor.start();
        } else {
            this.recentNotificationsMonitor = null;
        }
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

        long startNanos = System.nanoTime();

        byte[] broadcastContent = ByteStreams.toByteArray(request.getInputStream());

        long endNanos = System.nanoTime();

        String topicURL = request.getPathInfo();

        if (maxBodyBytes > 0 && broadcastContent.length > maxBodyBytes) {
            logNotification(request, topicURL, NOTIFICATION_TOO_LARGE.statusCode, null);
            Bridge.sendServletResponse(NOTIFICATION_TOO_LARGE, response);
            return;
        }

        Response endpointResponse;
        if (topicURL != null) {
            if (filters.size() > 0) {
                String checkHeader = request.getHeader(BasicAuthScheme.AUTH_HEADER);
                for (BasicAuthFilter filter : filters) {
                    if (filter.reject(topicURL, checkHeader)) {
                        logNotification(request, topicURL, Response.Code.UNAUTHORIZED, broadcastContent);
                        response.sendError(Response.Code.UNAUTHORIZED, "Unauthorized");
                        return;
                    }
                }
            }

            try {

                Topic topic = topicCache != null ? topicCache.getIfPresent(topicURL) : null;
                if (topic == null) {
                    topic = datastore.getTopic(topicURL, autocreateTopics);
                    if (topicCache != null && topic != null) {
                        topicCache.put(topicURL, topic);
                    }
                }

                if (topic != null) {
                    NotificationMetrics globalMetrics = endpoint.getGlobalNotificationMetrics();
                    NotificationMetrics metrics = endpoint.getNotificationMetrics(topic.getId());
                    metrics.notificationSize.update(broadcastContent.length);
                    globalMetrics.notificationSize.update(broadcastContent.length);
                    long acceptTimeNanos = endNanos - startNanos;
                    metrics.notifications.update(acceptTimeNanos, TimeUnit.NANOSECONDS);
                    globalMetrics.notifications.update(acceptTimeNanos, TimeUnit.NANOSECONDS);
                    Notification notification = new Notification(topic, null, broadcastContent); //No custom headers...

                    final boolean queued = endpoint.enqueueNotification(notification);
                    if (queued) {
                        if (replicationTopic != null) {
                            final boolean replicationQueued = endpoint
                                    .enqueueNotification(new Notification(replicationTopic,
                                            Collections.singleton(
                                                    new Header(REPLICATION_TOPIC_HEADER, topic.getURL())),
                                            broadcastContent));
                            if (!replicationQueued) { //What to do?
                                logger.error("Replication failure due to notification capacity limits!");
                            }
                        }
                        if (!jsonEnabled) {
                            endpointResponse = ACCEPTED_RESPONSE;
                        } else {
                            ResponseBuilder builder = new ResponseBuilder();
                            builder.setStatusCode(ACCEPTED_RESPONSE.statusCode);
                            builder.addHeader("Content-Type", ServerUtil.JSON_CONTENT_TYPE);
                            ObjectNode responseNode = JsonNodeFactory.instance.objectNode();
                            ArrayNode idsNode = responseNode.putArray("messageIds");
                            idsNode.add(Long.toString(notification.getCreateTimestampMicros()));
                            builder.setBody(responseNode.toString().getBytes(Charsets.UTF_8));
                            endpointResponse = builder.create();
                        }
                    } else {
                        endpointResponse = CAPACITY_ERROR_RESPONSE;
                    }
                } else {
                    endpointResponse = UNKNOWN_TOPIC_RESPONSE;
                }
            } catch (DatastoreException de) {
                logger.error("Problem selecting topic", de);
                endpointResponse = INTERNAL_ERROR_RESPONSE;
            }
        } else {
            endpointResponse = NO_TOPIC_RESPONSE;
        }

        logNotification(request, topicURL, endpointResponse.statusCode, broadcastContent);
        Bridge.sendServletResponse(endpointResponse, response);
    }

    @Override
    public void destroy() {
        shutdown();
    }

    /**
     * Shutdown the servlet.
     */
    public void shutdown() {
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down broadcast servlet...");
            if (recentNotificationsMonitor != null) {
                logger.info("Shutting down recent notifications monitor...");
                recentNotificationsMonitor.interrupt();
            }
            endpoint.shutdown();
            logger.info("Broadcast servlet shutdown.");
        }
    }

    /**
     * Invalidates all entries in internal caches.
     */
    public void invalidateCaches() {
        endpoint.invalidateCaches();
    }

    /**
     * Logs a notification if recent notifications are configured.
     */
    private void logNotification(final HttpServletRequest request, final String topicURL, final int responseCode,
            final byte[] body) {
        if (recentNotifications != null) {
            if (!recentNotifications.offer(new NotificationRecord(request, topicURL, responseCode, body))) {
                logger.warn("Recent notifications buffer is full! ");
            } else {
                recentNotificationsSize.incrementAndGet();
            }
        }
    }

    /**
     * Gets the most recently added notifications (if configured).
     * @param limit The maximum number returned.
     * @return A list of recent notifications.
     */
    public List<NotificationRecord> latestNotifications(final int limit) {
        if (recentNotifications != null) {
            List<NotificationRecord> records = Lists.newArrayListWithCapacity(maxSavedNotifications);
            records.addAll(recentNotifications);
            Collections.sort(records);
            if (records.size() >= limit) {
                return records.subList(0, limit);
            } else {
                return records;
            }
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * The header sent to identify the topic when replicating all notifications
     * to the special replication topic ('X-Attribyte-Topic').
     */
    public static final String REPLICATION_TOPIC_HEADER = "X-Attribyte-Topic";

    /**
     * The hub endpoint.
     */
    private final HubEndpoint endpoint;

    /**
     * The hub datastore.
     */
    private final HubDatastore datastore;

    /**
     * The maximum accepted body size.
     */
    private final int maxBodyBytes;

    /**
     * The logger.
     */
    private final Logger logger;

    /**
     * Ensure shutdown happens only once.
     */
    private AtomicBoolean isShutdown = new AtomicBoolean(false);

    /**
     * A list of basic auth filters.
     */
    private final List<BasicAuthFilter> filters;

    /**
     * Should unknown topics be automatically created?
     */
    private final boolean autocreateTopics;

    /**
     * The topic cache.
     */
    private final Cache<String, Topic> topicCache;

    /**
     * A special topic to which all notifications are sent.
     */
    private final Topic replicationTopic;

    /**
     * Saves the N most recent notifications.
     */
    private final BlockingQueue<NotificationRecord> recentNotifications;

    /**
     * Monitors the recent notifications queue and periodically evicts.
     */
    private final Thread recentNotificationsMonitor;

    /**
     * Tracks the size of the notification queue.
     */
    private final AtomicInteger recentNotificationsSize;

    /**
     * The maximum number of recent notifications.
     */
    private final int maxSavedNotifications;

    /**
     * If <code>true</code>, a JSON response body is returned with the notification response.
     */
    private final boolean jsonEnabled;
}