mirror of https://github.com/apache/cassandra
585 lines
19 KiB
Java
585 lines
19 KiB
Java
/**
|
|
* 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.io;
|
|
|
|
import java.io.*;
|
|
import java.util.*;
|
|
|
|
import org.apache.log4j.Logger;
|
|
import org.apache.commons.lang.StringUtils;
|
|
|
|
import org.apache.cassandra.db.RowMutation;
|
|
import org.apache.cassandra.dht.IPartitioner;
|
|
import org.apache.cassandra.io.SequenceFile.ColumnGroupReader;
|
|
import org.apache.cassandra.utils.BloomFilter;
|
|
import org.apache.cassandra.utils.FileUtils;
|
|
import org.cliffc.high_scale_lib.NonBlockingHashMap;
|
|
|
|
/**
|
|
* This class is built on top of the SequenceFile. It stores
|
|
* data on disk in sorted fashion. However the sorting is upto
|
|
* the application. This class expects keys to be handed to it
|
|
* in sorted order.
|
|
*
|
|
* A separate index file is maintained as well, containing the
|
|
* SSTable keys and the offset into the SSTable at which they are found.
|
|
* Every 1/indexInterval key is read into memory when the SSTable is opened.
|
|
*
|
|
* Finally, a bloom filter file is also kept for the keys in each SSTable.
|
|
*/
|
|
|
|
public class SSTable
|
|
{
|
|
private static Logger logger_ = Logger.getLogger(SSTable.class);
|
|
/* Every 128th index entry is loaded into memory so we know where to start looking for the actual key w/o seeking */
|
|
private static final int indexInterval_ = 128;
|
|
/* Required extension for temporary files created during compactions. */
|
|
public static final String temporaryFile_ = "tmp";
|
|
|
|
private static FileSSTableMap openedFiles = new FileSSTableMap();
|
|
|
|
|
|
public static int indexInterval()
|
|
{
|
|
return indexInterval_;
|
|
}
|
|
|
|
// todo can we refactor to take list of sstables?
|
|
public static int getApproximateKeyCount(List<String> dataFiles)
|
|
{
|
|
int count = 0;
|
|
|
|
for (String dataFileName : dataFiles)
|
|
{
|
|
SSTable sstable = openedFiles.get(dataFileName);
|
|
assert sstable != null;
|
|
int indexKeyCount = sstable.getIndexPositions().size();
|
|
count = count + (indexKeyCount + 1) * indexInterval_;
|
|
logger_.debug("index size for bloom filter calc for file : " + dataFileName + " : " + count);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Get all indexed keys in the SSTable.
|
|
*/
|
|
public static List<String> getIndexedKeys()
|
|
{
|
|
List<String> indexedKeys = new ArrayList<String>();
|
|
|
|
for (SSTable sstable : openedFiles.values())
|
|
{
|
|
for (KeyPosition kp : sstable.getIndexPositions())
|
|
{
|
|
indexedKeys.add(kp.key);
|
|
}
|
|
}
|
|
Collections.sort(indexedKeys);
|
|
|
|
return indexedKeys;
|
|
}
|
|
|
|
String dataFile_;
|
|
private long keysWritten;
|
|
private IFileWriter dataWriter_;
|
|
private BufferedRandomAccessFile indexRAF_;
|
|
private String lastWrittenKey_;
|
|
private IPartitioner partitioner_;
|
|
List<KeyPosition> indexPositions_;
|
|
BloomFilter bf;
|
|
|
|
public static synchronized SSTable open(String dataFileName, IPartitioner partitioner) throws IOException
|
|
{
|
|
SSTable sstable = openedFiles.get(dataFileName);
|
|
if (sstable == null)
|
|
{
|
|
assert partitioner != null;
|
|
sstable = new SSTable(dataFileName, partitioner);
|
|
|
|
long start = System.currentTimeMillis();
|
|
sstable.loadIndexFile();
|
|
sstable.loadBloomFilter();
|
|
logger_.debug("INDEX LOAD TIME for " + dataFileName + ": " + (System.currentTimeMillis() - start) + " ms.");
|
|
|
|
openedFiles.put(dataFileName, sstable);
|
|
}
|
|
return sstable;
|
|
}
|
|
|
|
public static synchronized SSTable get(String dataFileName) throws IOException
|
|
{
|
|
SSTable sstable = openedFiles.get(dataFileName);
|
|
assert sstable != null;
|
|
return sstable;
|
|
}
|
|
|
|
private SSTable(String filename, IPartitioner partitioner)
|
|
{
|
|
assert filename.endsWith("-Data.db");
|
|
dataFile_ = filename;
|
|
partitioner_ = partitioner;
|
|
}
|
|
|
|
public SSTable(String filename, int keyCount, IPartitioner partitioner) throws IOException
|
|
{
|
|
assert filename.endsWith("-Data.db");
|
|
dataFile_ = filename;
|
|
partitioner_ = partitioner;
|
|
dataWriter_ = SequenceFile.bufferedWriter(dataFile_, 4 * 1024 * 1024);
|
|
indexRAF_ = new BufferedRandomAccessFile(indexFilename(), "rw", 1024 * 1024);
|
|
bf = new BloomFilter(keyCount, 15);
|
|
}
|
|
|
|
static String parseTableName(String filename)
|
|
{
|
|
String[] parts = new File(filename).getName().split("-"); // table, cf, index, [filetype]
|
|
return parts[0];
|
|
}
|
|
|
|
public List<KeyPosition> getIndexPositions()
|
|
{
|
|
return indexPositions_;
|
|
}
|
|
|
|
private void loadBloomFilter() throws IOException
|
|
{
|
|
DataInputStream stream = new DataInputStream(new FileInputStream(filterFilename()));
|
|
bf = BloomFilter.serializer().deserialize(stream);
|
|
}
|
|
|
|
private void loadIndexFile() throws IOException
|
|
{
|
|
BufferedRandomAccessFile input = new BufferedRandomAccessFile(indexFilename(), "r");
|
|
indexPositions_ = new ArrayList<KeyPosition>();
|
|
|
|
int i = 0;
|
|
long indexSize = input.length();
|
|
while (true)
|
|
{
|
|
long indexPosition = input.getFilePointer();
|
|
if (indexPosition == indexSize)
|
|
{
|
|
break;
|
|
}
|
|
String decoratedKey = input.readUTF();
|
|
input.readLong();
|
|
if (i++ % indexInterval_ == 0)
|
|
{
|
|
indexPositions_.add(new KeyPosition(decoratedKey, indexPosition));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static String indexFilename(String dataFile)
|
|
{
|
|
String[] parts = dataFile.split("-");
|
|
parts[parts.length - 1] = "Index.db";
|
|
return StringUtils.join(parts, "-");
|
|
}
|
|
private String indexFilename()
|
|
{
|
|
return indexFilename(dataFile_);
|
|
}
|
|
|
|
private static String filterFilename(String dataFile)
|
|
{
|
|
String[] parts = dataFile.split("-");
|
|
parts[parts.length - 1] = "Filter.db";
|
|
return StringUtils.join(parts, "-");
|
|
}
|
|
private String filterFilename()
|
|
{
|
|
return filterFilename(dataFile_);
|
|
}
|
|
|
|
public String getFilename()
|
|
{
|
|
return dataFile_;
|
|
}
|
|
|
|
private long beforeAppend(String decoratedKey) throws IOException
|
|
{
|
|
if (decoratedKey == null)
|
|
{
|
|
throw new IOException("Keys must not be null.");
|
|
}
|
|
Comparator<String> c = partitioner_.getDecoratedKeyComparator();
|
|
if (lastWrittenKey_ != null && c.compare(lastWrittenKey_, decoratedKey) > 0)
|
|
{
|
|
logger_.info("Last written key : " + lastWrittenKey_);
|
|
logger_.info("Current key : " + decoratedKey);
|
|
logger_.info("Writing into file " + dataFile_);
|
|
throw new IOException("Keys must be written in ascending order.");
|
|
}
|
|
return (lastWrittenKey_ == null) ? 0 : dataWriter_.getCurrentPosition();
|
|
}
|
|
|
|
private void afterAppend(String decoratedKey, long position) throws IOException
|
|
{
|
|
bf.add(decoratedKey);
|
|
lastWrittenKey_ = decoratedKey;
|
|
long indexPosition = indexRAF_.getFilePointer();
|
|
indexRAF_.writeUTF(decoratedKey);
|
|
indexRAF_.writeLong(position);
|
|
logger_.trace("wrote " + decoratedKey + " at " + position);
|
|
|
|
if (keysWritten++ % indexInterval_ != 0)
|
|
return;
|
|
if (indexPositions_ == null)
|
|
{
|
|
indexPositions_ = new ArrayList<KeyPosition>();
|
|
}
|
|
indexPositions_.add(new KeyPosition(decoratedKey, indexPosition));
|
|
logger_.trace("wrote index of " + decoratedKey + " at " + indexPosition);
|
|
}
|
|
|
|
// TODO make this take a DataOutputStream and wrap the byte[] version to combine them
|
|
public void append(String decoratedKey, DataOutputBuffer buffer) throws IOException
|
|
{
|
|
long currentPosition = beforeAppend(decoratedKey);
|
|
dataWriter_.append(decoratedKey, buffer);
|
|
afterAppend(decoratedKey, currentPosition);
|
|
}
|
|
|
|
public void append(String decoratedKey, byte[] value) throws IOException
|
|
{
|
|
long currentPosition = beforeAppend(decoratedKey);
|
|
dataWriter_.append(decoratedKey, value);
|
|
afterAppend(decoratedKey, currentPosition);
|
|
}
|
|
|
|
/** get the position in the index file to start scanning to find the given key (at most indexInterval keys away) */
|
|
private long getIndexScanPosition(String decoratedKey, IPartitioner partitioner)
|
|
{
|
|
assert indexPositions_ != null && indexPositions_.size() > 0;
|
|
int index = Collections.binarySearch(indexPositions_, new KeyPosition(decoratedKey, -1));
|
|
if (index < 0)
|
|
{
|
|
// binary search gives us the first index _greater_ than the key searched for,
|
|
// i.e., its insertion position
|
|
int greaterThan = (index + 1) * -1;
|
|
if (greaterThan == 0)
|
|
return -1;
|
|
return indexPositions_.get(greaterThan - 1).position;
|
|
}
|
|
else
|
|
{
|
|
return indexPositions_.get(index).position;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* returns the position in the data file to find the given key, or -1 if the key is not present
|
|
*/
|
|
public long getPosition(String decoratedKey, IPartitioner partitioner) throws IOException
|
|
{
|
|
long start = getIndexScanPosition(decoratedKey, partitioner);
|
|
if (start < 0)
|
|
{
|
|
return -1;
|
|
}
|
|
// TODO mmap the index file?
|
|
BufferedRandomAccessFile input = new BufferedRandomAccessFile(indexFilename(dataFile_), "r");
|
|
input.seek(start);
|
|
int i = 0;
|
|
try
|
|
{
|
|
do
|
|
{
|
|
String indexDecoratedKey;
|
|
try
|
|
{
|
|
indexDecoratedKey = input.readUTF();
|
|
}
|
|
catch (EOFException e)
|
|
{
|
|
return -1;
|
|
}
|
|
long position = input.readLong();
|
|
int v = partitioner.getDecoratedKeyComparator().compare(indexDecoratedKey, decoratedKey);
|
|
if (v == 0)
|
|
return position;
|
|
if (v > 0)
|
|
return -1;
|
|
} while (++i < indexInterval_);
|
|
}
|
|
finally
|
|
{
|
|
input.close();
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/** like getPosition, but if key is not found will return the location of the first key _greater_ than the desired one, or -1 if no such key exists. */
|
|
public long getNearestPosition(String decoratedKey) throws IOException
|
|
{
|
|
long start = getIndexScanPosition(decoratedKey, partitioner_);
|
|
if (start < 0)
|
|
{
|
|
return 0;
|
|
}
|
|
BufferedRandomAccessFile input = new BufferedRandomAccessFile(indexFilename(dataFile_), "r");
|
|
input.seek(start);
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
String indexDecoratedKey;
|
|
try
|
|
{
|
|
indexDecoratedKey = input.readUTF();
|
|
}
|
|
catch (EOFException e)
|
|
{
|
|
return -1;
|
|
}
|
|
long position = input.readLong();
|
|
int v = partitioner_.getDecoratedKeyComparator().compare(indexDecoratedKey, decoratedKey);
|
|
if (v >= 0)
|
|
return position;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
input.close();
|
|
}
|
|
}
|
|
|
|
public DataInputBuffer next(final String clientKey, String cfName, List<String> columnNames) throws IOException
|
|
{
|
|
return next(clientKey, cfName, columnNames, null);
|
|
}
|
|
|
|
public DataInputBuffer next(final String clientKey, String cfName, List<String> columnNames, IndexHelper.TimeRange timeRange) throws IOException
|
|
{
|
|
IFileReader dataReader = null;
|
|
try
|
|
{
|
|
dataReader = SequenceFile.reader(dataFile_);
|
|
String decoratedKey = partitioner_.decorateKey(clientKey);
|
|
long position = getPosition(decoratedKey, partitioner_);
|
|
|
|
DataOutputBuffer bufOut = new DataOutputBuffer();
|
|
DataInputBuffer bufIn = new DataInputBuffer();
|
|
long bytesRead = dataReader.next(decoratedKey, bufOut, cfName, columnNames, timeRange, position);
|
|
if (bytesRead != -1L)
|
|
{
|
|
if (bufOut.getLength() > 0)
|
|
{
|
|
bufIn.reset(bufOut.getData(), bufOut.getLength());
|
|
/* read the key even though we do not use it */
|
|
bufIn.readUTF();
|
|
bufIn.readInt();
|
|
}
|
|
}
|
|
return bufIn;
|
|
}
|
|
finally
|
|
{
|
|
if (dataReader != null)
|
|
{
|
|
dataReader.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
public DataInputBuffer next(String clientKey, String columnFamilyColumn) throws IOException
|
|
{
|
|
String[] values = RowMutation.getColumnAndColumnFamily(columnFamilyColumn);
|
|
String columnFamilyName = values[0];
|
|
List<String> cnNames = (values.length == 1) ? null : Arrays.asList(values[1]);
|
|
return next(clientKey, columnFamilyName, cnNames);
|
|
}
|
|
|
|
private static String rename(String tmpFilename)
|
|
{
|
|
String filename = tmpFilename.replace("-" + temporaryFile_, "");
|
|
new File(tmpFilename).renameTo(new File(filename));
|
|
return filename;
|
|
}
|
|
|
|
/**
|
|
* Renames temporary SSTable files to valid data, index, and bloom filter files
|
|
*/
|
|
public void close() throws IOException
|
|
{
|
|
// bloom filter
|
|
FileOutputStream fos = new FileOutputStream(filterFilename());
|
|
DataOutputStream stream = new DataOutputStream(fos);
|
|
BloomFilter.serializer().serialize(bf, stream);
|
|
stream.flush();
|
|
fos.getFD().sync();
|
|
stream.close();
|
|
|
|
// index
|
|
indexRAF_.getChannel().force(true);
|
|
indexRAF_.close();
|
|
|
|
// main data
|
|
dataWriter_.close(); // calls force
|
|
|
|
rename(indexFilename());
|
|
rename(filterFilename());
|
|
dataFile_ = rename(dataFile_); // important to do this last since index & filter file names are derived from it
|
|
|
|
openedFiles.put(dataFile_, this);
|
|
}
|
|
|
|
/**
|
|
* obtain a BlockReader for the getColumnSlice call.
|
|
*/
|
|
public ColumnGroupReader getColumnGroupReader(String key, String cfName, String startColumn, boolean isAscending) throws IOException
|
|
{
|
|
IFileReader dataReader = SequenceFile.reader(dataFile_);
|
|
|
|
try
|
|
{
|
|
/* Morph key into actual key based on the partition type. */
|
|
String decoratedKey = partitioner_.decorateKey(key);
|
|
long position = getPosition(decoratedKey, partitioner_);
|
|
return new ColumnGroupReader(dataFile_, decoratedKey, cfName, startColumn, isAscending, position);
|
|
}
|
|
finally
|
|
{
|
|
dataReader.close();
|
|
}
|
|
}
|
|
|
|
public void delete() throws IOException
|
|
{
|
|
FileUtils.deleteWithConfirm(new File(dataFile_));
|
|
FileUtils.deleteWithConfirm(new File(indexFilename(dataFile_)));
|
|
FileUtils.deleteWithConfirm(new File(filterFilename(dataFile_)));
|
|
openedFiles.remove(dataFile_);
|
|
}
|
|
|
|
/** obviously only for testing */
|
|
public void forceBloomFilterFailures()
|
|
{
|
|
bf = BloomFilter.alwaysMatchingBloomFilter();
|
|
}
|
|
|
|
static void reopenUnsafe() throws IOException // testing only
|
|
{
|
|
Collection<SSTable> sstables = new ArrayList<SSTable>(openedFiles.values());
|
|
openedFiles.clear();
|
|
for (SSTable sstable : sstables)
|
|
{
|
|
SSTable.open(sstable.dataFile_, sstable.partitioner_);
|
|
}
|
|
}
|
|
|
|
public boolean isKeyPossible(String clientKey)
|
|
{
|
|
return bf.isPresent(partitioner_.decorateKey(clientKey));
|
|
}
|
|
|
|
IPartitioner getPartitioner()
|
|
{
|
|
return partitioner_;
|
|
}
|
|
|
|
public FileStruct getFileStruct() throws IOException
|
|
{
|
|
return new FileStruct(this);
|
|
}
|
|
|
|
public static void deleteAll() throws IOException
|
|
{
|
|
for (SSTable sstable : openedFiles.values())
|
|
{
|
|
sstable.delete();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is a simple container for the index Key and its corresponding position
|
|
* in the data file. Binary search is performed on a list of these objects
|
|
* to lookup keys within the SSTable data file.
|
|
*
|
|
* All keys are decorated.
|
|
*/
|
|
class KeyPosition implements Comparable<KeyPosition>
|
|
{
|
|
public final String key; // decorated
|
|
public final long position;
|
|
|
|
public KeyPosition(String key, long position)
|
|
{
|
|
this.key = key;
|
|
this.position = position;
|
|
}
|
|
|
|
public int compareTo(KeyPosition kp)
|
|
{
|
|
return partitioner_.getDecoratedKeyComparator().compare(key, kp.key);
|
|
}
|
|
|
|
public String toString()
|
|
{
|
|
return key + ":" + position;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
class FileSSTableMap
|
|
{
|
|
private final Map<String, SSTable> map = new NonBlockingHashMap<String, SSTable>();
|
|
|
|
public SSTable get(String filename)
|
|
{
|
|
try
|
|
{
|
|
return map.get(new File(filename).getCanonicalPath());
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
public SSTable put(String filename, SSTable value)
|
|
{
|
|
try
|
|
{
|
|
return map.put(new File(filename).getCanonicalPath(), value);
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
public Collection<SSTable> values()
|
|
{
|
|
return map.values();
|
|
}
|
|
|
|
public void clear()
|
|
{
|
|
map.clear();
|
|
}
|
|
|
|
public void remove(String filename) throws IOException
|
|
{
|
|
map.remove(new File(filename).getCanonicalPath());
|
|
}
|
|
} |