/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.cassandra.hints; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Iterator; import javax.annotation.Nullable; import com.google.common.primitives.Ints; import com.google.common.util.concurrent.RateLimiter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.db.UnknownColumnFamilyException; import org.apache.cassandra.io.FSReadError; import org.apache.cassandra.utils.AbstractIterator; import org.apache.cassandra.utils.ByteBufferUtil; import org.apache.cassandra.utils.CLibrary; /** * A paged non-compressed hints reader that provides two iterators: * - a 'raw' ByteBuffer iterator that doesn't deserialize the hints, but returns the pre-encoded hints verbatim * - a decoded iterator, that deserializes the underlying bytes into {@link Hint} instances. * * The former is an optimisation for when the messaging version of the file matches the messaging version of the destination * node. Extra decoding and reencoding is a waste of effort in this scenario, so we avoid it. * * The latter is required for dispatch of hints to nodes that have a different messaging version, and in general is just an * easy way to enable backward and future compatibilty. */ class HintsReader implements AutoCloseable, Iterable<HintsReader.Page> { private static final Logger logger = LoggerFactory.getLogger(HintsReader.class); // don't read more than 512 KB of hints at a time. private static final int PAGE_SIZE = 512 << 10; private final HintsDescriptor descriptor; private final File file; private final ChecksummedDataInput input; // we pass the RateLimiter into HintsReader itself because it's cheaper to calculate the size before the hint is deserialized @Nullable private final RateLimiter rateLimiter; protected HintsReader(HintsDescriptor descriptor, File file, ChecksummedDataInput reader, RateLimiter rateLimiter) { this.descriptor = descriptor; this.file = file; this.input = reader; this.rateLimiter = rateLimiter; } @SuppressWarnings("resource") // HintsReader owns input static HintsReader open(File file, RateLimiter rateLimiter) { ChecksummedDataInput reader = ChecksummedDataInput.open(file); try { HintsDescriptor descriptor = HintsDescriptor.deserialize(reader); if (descriptor.isCompressed()) { // since the hints descriptor is always uncompressed, it needs to be read with the normal ChecksummedDataInput. // The compressed input is instantiated with the uncompressed input's position reader = CompressedChecksummedDataInput.upgradeInput(reader, descriptor.createCompressor()); } return new HintsReader(descriptor, file, reader, rateLimiter); } catch (IOException e) { reader.close(); throw new FSReadError(e, file); } } static HintsReader open(File file) { return open(file, null); } public void close() { input.close(); } public HintsDescriptor descriptor() { return descriptor; } void seek(long newPosition) { input.seek(newPosition); } public Iterator<Page> iterator() { return new PagesIterator(); } public ChecksummedDataInput getInput() { return input; } final class Page { public final long offset; private Page(long offset) { this.offset = offset; } Iterator<Hint> hintsIterator() { return new HintsIterator(offset); } Iterator<ByteBuffer> buffersIterator() { return new BuffersIterator(offset); } } final class PagesIterator extends AbstractIterator<Page> { @SuppressWarnings("resource") protected Page computeNext() { CLibrary.trySkipCache(input.getChannel().getFileDescriptor(), 0, input.getFilePointer(), input.getPath()); if (input.isEOF()) return endOfData(); return new Page(input.getFilePointer()); } } /** * A decoding iterator that deserializes the hints as it goes. */ final class HintsIterator extends AbstractIterator<Hint> { private final long offset; HintsIterator(long offset) { super(); this.offset = offset; } protected Hint computeNext() { Hint hint; do { long position = input.getFilePointer(); if (input.isEOF()) return endOfData(); // reached EOF if (position - offset >= PAGE_SIZE) return endOfData(); // read page size or more bytes try { hint = computeNextInternal(); } catch (IOException e) { throw new FSReadError(e, file); } } while (hint == null); return hint; } private Hint computeNextInternal() throws IOException { input.resetCrc(); input.resetLimit(); int size = input.readInt(); // if we cannot corroborate the size via crc, then we cannot safely skip this hint if (!input.checkCrc()) throw new IOException("Digest mismatch exception"); return readHint(size); } private Hint readHint(int size) throws IOException { if (rateLimiter != null) rateLimiter.acquire(size); input.limit(size); Hint hint; try { hint = Hint.serializer.deserialize(input, descriptor.messagingVersion()); input.checkLimit(0); } catch (UnknownColumnFamilyException e) { logger.warn("Failed to read a hint for {} - table with id {} is unknown in file {}", descriptor.hostId, e.cfId, descriptor.fileName()); input.skipBytes(Ints.checkedCast(size - input.bytesPastLimit())); return null; } if (input.checkCrc()) return hint; // log a warning and skip the corrupted entry logger.warn("Failed to read a hint for {} - digest mismatch for hint at position {} in file {}", descriptor.hostId, input.getPosition() - size - 4, descriptor.fileName()); return null; } } /** * A verbatim iterator that simply returns the underlying ByteBuffers. */ final class BuffersIterator extends AbstractIterator<ByteBuffer> { private final long offset; BuffersIterator(long offset) { super(); this.offset = offset; } protected ByteBuffer computeNext() { ByteBuffer buffer; do { long position = input.getFilePointer(); if (input.isEOF()) return endOfData(); // reached EOF if (position - offset >= PAGE_SIZE) return endOfData(); // read page size or more bytes try { buffer = computeNextInternal(); } catch (IOException e) { throw new FSReadError(e, file); } } while (buffer == null); return buffer; } private ByteBuffer computeNextInternal() throws IOException { input.resetCrc(); input.resetLimit(); int size = input.readInt(); // if we cannot corroborate the size via crc, then we cannot safely skip this hint if (!input.checkCrc()) throw new IOException("Digest mismatch exception"); return readBuffer(size); } private ByteBuffer readBuffer(int size) throws IOException { if (rateLimiter != null) rateLimiter.acquire(size); input.limit(size); ByteBuffer buffer = ByteBufferUtil.read(input, size); if (input.checkCrc()) return buffer; // log a warning and skip the corrupted entry logger.warn("Failed to read a hint for {} - digest mismatch for hint at position {} in file {}", descriptor.hostId, input.getPosition() - size - 4, descriptor.fileName()); return null; } } }