/* * 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.file; import com.google.common.base.Objects; import com.google.common.base.Throwables; import com.google.common.collect.AbstractIterator; import com.google.common.collect.Iterators; import com.google.common.collect.Maps; import com.google.common.collect.PeekingIterator; import com.google.common.io.Closeables; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Iterator; import java.util.Map; import javax.annotation.concurrent.NotThreadSafe; /** * An abstract base class for implementation partitioned {@link FileWriter}. * * @param <T> Type of event. * @param <P> Type of partition. */ @NotThreadSafe public abstract class PartitionedFileWriter<T, P> implements FileWriter<T> { private static final Logger LOG = LoggerFactory.getLogger(PartitionedFileWriter.class); private final PartitionedFileWriterFactory<T, P> fileWriterFactory; private final Map<P, FileWriter<T>> writers; private P currentPartition; private FileWriter<T> currentWriter; private boolean closed; /** * Constructs with the given file writer factory. */ protected PartitionedFileWriter(PartitionedFileWriterFactory<T, P> fileWriterFactory) { this.fileWriterFactory = fileWriterFactory; this.writers = Maps.newHashMap(); } @Override public void append(T event) throws IOException { if (closed) { throw new IOException("Attempts to write to a closed FileWriter."); } try { getWriter(event).append(event); } catch (Throwable t) { LOG.error("Exception on append.", t); Closeables.closeQuietly(this); Throwables.propagateIfInstanceOf(t, IOException.class); throw Throwables.propagate(t); } } @Override public void appendAll(final Iterator<? extends T> events) throws IOException { if (closed) { throw new IOException("Attempts to write to a closed FileWriter."); } PeekingIterator<T> iterator = Iterators.peekingIterator(events); while (iterator.hasNext()) { getWriter(iterator.peek()).appendAll(new AppendIterator(iterator)); } } @Override public void flush() throws IOException { // Special case to avoid looping the map values if (writers.size() == 1) { currentWriter.flush(); } else if (!writers.isEmpty()) { IOException flushException = null; for (FileWriter<T> writer : writers.values()) { try { writer.flush(); } catch (IOException e) { flushException = e; } } if (flushException != null) { throw flushException; } } } @Override public void close() throws IOException { try { IOException closeException = null; for (FileWriter<T> writer : writers.values()) { try { writer.close(); } catch (IOException e) { // Catch the exception and throw after the loop as we want to close all writers. closeException = e; } } if (closeException != null) { throw closeException; } } finally { closed = true; } } /** * Gets a {@link FileWriter} for the given event. */ private FileWriter<T> getWriter(T event) throws IOException { P partition = getPartition(event); // If the partition changed, get the file writer for the new partition. if (!Objects.equal(currentPartition, partition)) { // Notify that partition changed, so that children class can take action if necessary. partitionChanged(currentPartition, partition); currentWriter = writers.get(partition); if (currentWriter == null) { currentWriter = fileWriterFactory.create(partition); writers.put(partition, currentWriter); } currentPartition = partition; } return currentWriter; } /** * Closes the {@link FileWriter} for the given partition. */ protected final void closePartitionWriter(P partition) throws IOException { FileWriter<T> writer = writers.remove(partition); if (writer != null) { writer.close(); } } /** * Invoked when partition changed (different from the last event get appended). */ protected void partitionChanged(P oldPartition, P newPartition) throws IOException { // No-op by default. } /** * Returns the partition ID for the given event. * * @param event The event for computing a partition ID. * @return A long representing the partition ID. */ protected abstract P getPartition(T event); /** * * @param <T> Type of event that can be appended to the {@link FileWriter} created by this factory. * @param <P> Type of partition. */ protected interface PartitionedFileWriterFactory<T, P> { /** * Creates a {@link FileWriter} for writing event of type {@code T} for the given partition. * * @param partition Partition information. * @return A {@link FileWriter}. * @throws java.io.IOException If creation failed. */ FileWriter<T> create(P partition) throws IOException; } /** * An {@link Iterator} to support the {@link PartitionedFileWriter#appendAll(Iterator)} operation. * It will write as many events as possible as long as the event partition stays the same. */ private final class AppendIterator extends AbstractIterator<T> { private final PeekingIterator<? extends T> events; AppendIterator(PeekingIterator<? extends T> events) { this.events = events; } @Override protected T computeNext() { if (!events.hasNext()) { return endOfData(); } T event = events.peek(); P partition = getPartition(event); if (Objects.equal(currentPartition, partition)) { events.next(); return event; } return endOfData(); } } }