co.cask.cdap.data.stream.service.StreamFetchHandler.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.data.stream.service.StreamFetchHandler.java

Source

/*
 * Copyright  2014-2015 Cask Data, Inc.
 *
 * 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 co.cask.cdap.data.stream.service;

import co.cask.cdap.api.flow.flowlet.StreamEvent;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.stream.StreamEventTypeAdapter;
import co.cask.cdap.common.utils.TimeMathParser;
import co.cask.cdap.data.file.FileReader;
import co.cask.cdap.data.file.ReadFilter;
import co.cask.cdap.data.stream.MultiLiveStreamFileReader;
import co.cask.cdap.data.stream.StreamEventOffset;
import co.cask.cdap.data.stream.StreamFileOffset;
import co.cask.cdap.data.stream.StreamFileType;
import co.cask.cdap.data.stream.StreamUtils;
import co.cask.cdap.data.stream.TimeRangeReadFilter;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.data2.transaction.stream.StreamConfig;
import co.cask.cdap.proto.Id;
import co.cask.http.AbstractHttpHandler;
import co.cask.http.ChunkResponder;
import co.cask.http.HttpResponder;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Lists;
import com.google.common.io.Closeables;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonWriter;
import com.google.inject.Inject;
import org.apache.twill.filesystem.Location;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferOutputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;

/**
 * A HTTP handler for handling getting stream events.
 */
@Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}/streams")
public final class StreamFetchHandler extends AbstractHttpHandler {

    private static final Gson GSON = StreamEventTypeAdapter.register(new GsonBuilder()).create();
    private static final int MAX_EVENTS_PER_READ = 100;
    private static final int CHUNK_SIZE = 8192;

    private final CConfiguration cConf;
    private final StreamAdmin streamAdmin;
    private final StreamMetaStore streamMetaStore;

    @Inject
    public StreamFetchHandler(CConfiguration cConf, StreamAdmin streamAdmin, StreamMetaStore streamMetaStore) {
        this.cConf = cConf;
        this.streamAdmin = streamAdmin;
        this.streamMetaStore = streamMetaStore;
    }

    /**
     * Handler for the HTTP API {@code /streams/[stream_name]/events?start=[start_ts]&end=[end_ts]&limit=[event_limit]}
     * <p>
     * Responds with:
     * <ul>
     * <li>404 if stream does not exist</li>
     * <li>204 if no event in the given start/end time range exists</li>
     * <li>200 if there is are one or more events</li>
     * </ul>
     * </p>
     * <p>
     * Response body is a JSON array of the StreamEvent object.
     * </p>
     *
     * @see StreamEventTypeAdapter StreamEventTypeAdapter for the format of the StreamEvent object
     */
    @GET
    @Path("/{stream}/events")
    public void fetch(HttpRequest request, HttpResponder responder, @PathParam("namespace-id") String namespaceId,
            @PathParam("stream") String stream, @QueryParam("start") @DefaultValue("0") String start,
            @QueryParam("end") @DefaultValue("9223372036854775807") String end,
            @QueryParam("limit") @DefaultValue("2147483647") int limit) throws Exception {
        long startTime = TimeMathParser.parseTime(start, TimeUnit.MILLISECONDS);
        long endTime = TimeMathParser.parseTime(end, TimeUnit.MILLISECONDS);

        Id.Stream streamId = Id.Stream.from(namespaceId, stream);
        if (!verifyGetEventsRequest(streamId, startTime, endTime, limit, responder)) {
            return;
        }

        StreamConfig streamConfig = streamAdmin.getConfig(streamId);
        long now = System.currentTimeMillis();
        startTime = Math.max(startTime, now - streamConfig.getTTL());
        endTime = Math.min(endTime, now);

        // Create the stream event reader
        try (FileReader<StreamEventOffset, Iterable<StreamFileOffset>> reader = createReader(streamConfig,
                startTime)) {
            TimeRangeReadFilter readFilter = new TimeRangeReadFilter(startTime, endTime);
            List<StreamEvent> events = Lists.newArrayListWithCapacity(100);

            // Reads the first batch of events from the stream.
            int eventsRead = readEvents(reader, events, limit, readFilter);

            // If empty already, return 204 no content
            if (eventsRead <= 0) {
                responder.sendStatus(HttpResponseStatus.NO_CONTENT);
                return;
            }

            // Send with chunk response, as we don't want to buffer all events in memory to determine the content-length.
            ChunkResponder chunkResponder = responder.sendChunkStart(HttpResponseStatus.OK,
                    ImmutableMultimap.of(HttpHeaders.Names.CONTENT_TYPE, "application/json; charset=utf-8"));
            ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
            JsonWriter jsonWriter = new JsonWriter(
                    new OutputStreamWriter(new ChannelBufferOutputStream(buffer), Charsets.UTF_8));
            // Response is an array of stream event
            jsonWriter.beginArray();
            while (limit > 0 && eventsRead > 0) {
                limit -= eventsRead;

                for (StreamEvent event : events) {
                    GSON.toJson(event, StreamEvent.class, jsonWriter);
                    jsonWriter.flush();

                    // If exceeded chunk size limit, send a new chunk.
                    if (buffer.readableBytes() >= CHUNK_SIZE) {
                        // If the connect is closed, sendChunk will throw IOException.
                        // No need to handle the exception as it will just propagated back to the netty-http library
                        // and it will handle it.
                        // Need to copy the buffer because the buffer will get reused and send chunk is an async operation
                        chunkResponder.sendChunk(buffer.copy());
                        buffer.clear();
                    }
                }
                events.clear();

                if (limit > 0) {
                    eventsRead = readEvents(reader, events, limit, readFilter);
                }
            }
            jsonWriter.endArray();
            jsonWriter.close();

            // Send the last chunk that still has data
            if (buffer.readable()) {
                // No need to copy the last chunk, since the buffer will not be reused
                chunkResponder.sendChunk(buffer);
            }
            Closeables.closeQuietly(chunkResponder);
        }
    }

    /**
     * Reads events from the given reader.
     */
    private int readEvents(FileReader<StreamEventOffset, Iterable<StreamFileOffset>> reader,
            List<StreamEvent> events, int limit, TimeRangeReadFilter readFilter)
            throws IOException, InterruptedException {
        // Keeps reading as long as the filter is active.
        // This mean there are events in the stream, just that they are rejected by the filter.
        int eventsRead = reader.read(events, getReadLimit(limit), 0, TimeUnit.SECONDS, readFilter);
        while (eventsRead == 0 && readFilter.isActive()) {
            readFilter.reset();
            eventsRead = reader.read(events, getReadLimit(limit), 0, TimeUnit.SECONDS, readFilter);
        }
        return eventsRead;
    }

    /**
     * Verifies query properties.
     */
    private boolean verifyGetEventsRequest(Id.Stream streamId, long startTime, long endTime, int count,
            HttpResponder responder) throws Exception {
        if (startTime < 0) {
            responder.sendString(HttpResponseStatus.BAD_REQUEST, "Start time must be >= 0");
            return false;
        }
        if (endTime < 0) {
            responder.sendString(HttpResponseStatus.BAD_REQUEST, "End time must be >= 0");
            return false;
        }
        if (startTime >= endTime) {
            responder.sendString(HttpResponseStatus.BAD_REQUEST, "Start time must be smaller than end time");
            return false;
        }
        if (count <= 0) {
            responder.sendString(HttpResponseStatus.BAD_REQUEST, "Cannot request for <=0 events");
            return false;
        }
        if (!streamMetaStore.streamExists(streamId)) {
            responder.sendStatus(HttpResponseStatus.NOT_FOUND);
            return false;
        }

        return true;
    }

    /**
     * Get the first partition location that contains events as specified in start time.
     *
     * @return the partition location if found, or {@code null} if not found.
     */
    private Location getStartPartitionLocation(StreamConfig streamConfig, long startTime, int generation)
            throws IOException {
        Location baseLocation = StreamUtils.createGenerationLocation(streamConfig.getLocation(), generation);

        // Find the partition location with the largest timestamp that is <= startTime
        // Also find the partition location with the smallest timestamp that is >= startTime
        long maxPartitionStartTime = 0L;
        long minPartitionStartTime = Long.MAX_VALUE;

        Location maxPartitionLocation = null;
        Location minPartitionLocation = null;

        for (Location location : baseLocation.list()) {
            // Partition must be a directory
            if (!location.isDirectory() || !StreamUtils.isPartition(location.getName())) {
                continue;
            }
            long partitionStartTime = StreamUtils.getPartitionStartTime(location.getName());

            if (partitionStartTime > maxPartitionStartTime && partitionStartTime <= startTime) {
                maxPartitionStartTime = partitionStartTime;
                maxPartitionLocation = location;
            }
            if (partitionStartTime < minPartitionStartTime && partitionStartTime >= startTime) {
                minPartitionStartTime = partitionStartTime;
                minPartitionLocation = location;
            }
        }

        return maxPartitionLocation == null ? minPartitionLocation : maxPartitionLocation;
    }

    /**
     * Creates a {@link FileReader} that starts reading stream event from the given partition.
     */
    private FileReader<StreamEventOffset, Iterable<StreamFileOffset>> createReader(StreamConfig streamConfig,
            long startTime) throws IOException {
        int generation = StreamUtils.getGeneration(streamConfig);
        Location startPartition = getStartPartitionLocation(streamConfig, startTime, generation);
        if (startPartition == null) {
            return createEmptyReader();
        }

        List<StreamFileOffset> fileOffsets = Lists.newArrayList();
        int instances = cConf.getInt(Constants.Stream.CONTAINER_INSTANCES);
        String filePrefix = cConf.get(Constants.Stream.FILE_PREFIX);
        for (int i = 0; i < instances; i++) {
            // The actual file prefix is formed by file prefix in cConf + writer instance id
            String streamFilePrefix = filePrefix + '.' + i;
            Location eventLocation = StreamUtils.createStreamLocation(startPartition, streamFilePrefix, 0,
                    StreamFileType.EVENT);
            fileOffsets.add(new StreamFileOffset(eventLocation, 0, generation));
        }

        MultiLiveStreamFileReader reader = new MultiLiveStreamFileReader(streamConfig, fileOffsets);
        reader.initialize();
        return reader;
    }

    /**
     * Creates a reader that has no event to read.
     */
    private <T, P> FileReader<T, P> createEmptyReader() {
        return new FileReader<T, P>() {
            @Override
            public void initialize() throws IOException {
                // no-op
            }

            @Override
            public int read(Collection<? super T> events, int maxEvents, long timeout, TimeUnit unit)
                    throws IOException, InterruptedException {
                return -1;
            }

            @Override
            public int read(Collection<? super T> events, int maxEvents, long timeout, TimeUnit unit,
                    ReadFilter readFilter) throws IOException, InterruptedException {
                return -1;
            }

            @Override
            public void close() throws IOException {
                // no-op
            }

            @Override
            public P getPosition() {
                throw new UnsupportedOperationException("Position not supported for empty FileReader");
            }
        };
    }

    /**
     * Returns the events limit for each round of read from the stream reader.
     *
     * @param count Number of events wanted to read.
     * @return The actual number of events to read.
     */
    private int getReadLimit(int count) {
        return (count > MAX_EVENTS_PER_READ) ? MAX_EVENTS_PER_READ : count;
    }
}