/* * Copyright © 2014 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; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.io.Locations; import co.cask.cdap.data.file.FileReader; import co.cask.cdap.data.file.LiveFileReader; import co.cask.cdap.data.file.ReadFilter; import co.cask.cdap.data2.transaction.stream.StreamConfig; import org.apache.twill.filesystem.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Collection; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; /** * A {@link LiveFileReader} that gives infinite stream event. */ public final class LiveStreamFileReader extends LiveFileReader<PositionStreamEvent, StreamFileOffset> { private static final Logger LOG = LoggerFactory.getLogger(LiveStreamFileReader.class); private final StreamFileOffset beginOffset; private final StreamConfig streamConfig; private final long maxFileCheckInterval; private StreamPositionTransformFileReader reader; private int retries; private long nextCheckTime = 0; /** * Construct a reader with {@link co.cask.cdap.common.conf.Constants.Stream#NEW_FILE_CHECK_INTERVAL} * as the max interval to check for new file. * * <p> * Same as calling * {@link #LiveStreamFileReader(StreamConfig, StreamFileOffset, long) * LiveStreamFileReader(streamConfig, beginOffset, Constants.Stream.NEW_FILE_CHECK_INTERVAL). * } */ public LiveStreamFileReader(StreamConfig streamConfig, StreamFileOffset beginOffset) { this(streamConfig, beginOffset, Constants.Stream.NEW_FILE_CHECK_INTERVAL); } /** * Creates a new file reader. * * @param streamConfig the stream configuration. * @param beginOffset the offset information to begin with. * @param maxFileCheckInterval maximum interval in milliseconds for checking for new stream file. */ public LiveStreamFileReader(StreamConfig streamConfig, StreamFileOffset beginOffset, long maxFileCheckInterval) { this.streamConfig = streamConfig; this.beginOffset = beginOffset; this.maxFileCheckInterval = (maxFileCheckInterval <= 0) ? Constants.Stream.NEW_FILE_CHECK_INTERVAL : maxFileCheckInterval; } @Nullable @Override protected FileReader<PositionStreamEvent, StreamFileOffset> renewReader() throws IOException { // If no reader has yet opened, start with the beginning offset. if (reader == null) { reader = new StreamPositionTransformFileReader(beginOffset); reader.initialize(); return reader; } StreamFileOffset offset = reader.getPosition(); long now = System.currentTimeMillis(); if (now < nextCheckTime && now < offset.getPartitionEnd()) { return null; } // See if there is a higher sequence file available. Location eventLocation = StreamUtils.createStreamLocation(reader.getPartitionLocation(), offset.getNamePrefix(), offset.getSequenceId() + 1, StreamFileType.EVENT); StreamPositionTransformFileReader nextReader = createReader(eventLocation, true, offset.getGeneration()); // If no higher sequence file and if current time is later than partition end time, see if there is new partition. if (nextReader == null && now > offset.getPartitionEnd()) { // Always create a new reader for the next partition when the current reader partition times up. eventLocation = StreamUtils.createStreamLocation( StreamUtils.createPartitionLocation(Locations.getParent(reader.getPartitionLocation()), offset.getPartitionEnd(), streamConfig.getPartitionDuration()), offset.getNamePrefix(), 0, StreamFileType.EVENT); nextReader = createReader(eventLocation, false, offset.getGeneration()); } if (nextReader != null) { reader = nextReader; retries = 0; nextCheckTime = 0; } else { // Update the next check time to the next check interval if there is no new file found. // This is for reducing load to the file system. nextCheckTime = now + getCheckInterval(); } return nextReader; } /** * Computes the new file check interval based on number of retries that a new file hasn't been found. */ private long getCheckInterval() { retries = Math.min((retries + 1), Integer.SIZE); int multiplier = retries >= Integer.SIZE ? Integer.MAX_VALUE : (1 << (retries - 1)); return Math.min(100L * multiplier, maxFileCheckInterval); } /** * Returns a new reader if that exists, or null, unless checkExists is false. */ private StreamPositionTransformFileReader createReader(Location eventLocation, boolean checkExists, int generation) throws IOException { if (checkExists && !eventLocation.exists()) { return null; } StreamPositionTransformFileReader reader = new StreamPositionTransformFileReader(new StreamFileOffset(eventLocation, 0L, generation)); reader.initialize(); return reader; } @NotThreadSafe private static final class StreamPositionTransformFileReader implements FileReader<PositionStreamEvent, StreamFileOffset> { private final FileReader<PositionStreamEvent, Long> reader; private final Location partitionLocation; private StreamFileOffset offset; private StreamPositionTransformFileReader(StreamFileOffset offset) throws IOException { this.reader = StreamDataFileReader.createWithOffset(Locations.newInputSupplier(offset.getEventLocation()), Locations.newInputSupplier(offset.getIndexLocation()), offset.getOffset()); this.offset = new StreamFileOffset(offset); this.partitionLocation = Locations.getParent(offset.getEventLocation()); LOG.trace("Stream reader created for {}", offset.getEventLocation()); } @Override public void initialize() throws IOException { LOG.trace("Initialize stream reader {}", offset); reader.initialize(); offset = new StreamFileOffset(offset, reader.getPosition()); LOG.trace("Stream reader initialized {}", offset); } @Override public int read(Collection<? super PositionStreamEvent> events, int maxEvents, long timeout, TimeUnit unit) throws IOException, InterruptedException { return read(events, maxEvents, timeout, unit, ReadFilter.ALWAYS_ACCEPT); } @Override public int read(Collection<? super PositionStreamEvent> events, int maxEvents, long timeout, TimeUnit unit, ReadFilter readFilter) throws IOException, InterruptedException { int eventCount = reader.read(events, maxEvents, timeout, unit, readFilter); offset = new StreamFileOffset(offset, reader.getPosition()); return eventCount; } @Override public void close() throws IOException { reader.close(); } @Override public StreamFileOffset getPosition() { return offset; } Location getPartitionLocation() { return partitionLocation; } } }