/* * 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.data2.dataset2.lib.table.leveldb; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.data2.transaction.stream.leveldb.LevelDBNameConverter; import co.cask.cdap.data2.util.TableId; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.inject.Inject; import com.google.inject.Singleton; import org.iq80.leveldb.DB; import org.iq80.leveldb.DBComparator; import org.iq80.leveldb.Options; import org.iq80.leveldb.WriteOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentMap; import static org.iq80.leveldb.impl.Iq80DBFactory.factory; /** * Service maintaining all LevelDB tables. */ @Singleton public class LevelDBTableService { private static final Logger LOG = LoggerFactory.getLogger(LevelDBTableService.class); private int blockSize; private long cacheSize; private String basePath; private WriteOptions writeOptions; private final ConcurrentMap<String, DB> tables = Maps.newConcurrentMap(); private static final LevelDBTableService SINGLETON = new LevelDBTableService(); public static LevelDBTableService getInstance() { return SINGLETON; } /** * Protect the constructor as this class needs to be singleton, but keep it package visible for testing. */ @VisibleForTesting public LevelDBTableService() { } /** * For guice injecting configuration object to this singleton. */ @Inject public void setConfiguration(CConfiguration config) throws IOException { basePath = config.get(Constants.CFG_DATA_LEVELDB_DIR); Preconditions.checkNotNull(basePath, "No base directory configured for LevelDB."); blockSize = config.getInt(Constants.CFG_DATA_LEVELDB_BLOCKSIZE, Constants.DEFAULT_DATA_LEVELDB_BLOCKSIZE); cacheSize = config.getLong(Constants.CFG_DATA_LEVELDB_CACHESIZE, Constants.DEFAULT_DATA_LEVELDB_CACHESIZE); writeOptions = new WriteOptions().sync( config.getBoolean(Constants.CFG_DATA_LEVELDB_FSYNC, Constants.DEFAULT_DATA_LEVELDB_FSYNC)); } /** * only use in unit test since the singleton may be reused for multiple tests. */ public void clearTables() { tables.clear(); } public Collection<String> list() throws Exception { File baseDir = new File(basePath); String[] subDirs = baseDir.list(); if (subDirs == null) { return ImmutableList.of(); } ImmutableCollection.Builder<String> builder = ImmutableList.builder(); for (String dir : subDirs) { builder.add(getTableName(dir)); } return builder.build(); } /** * Gets tables stats. * @return map of table name -> table stats entries * @throws Exception */ public Map<TableId, TableStats> getTableStats() throws Exception { File baseDir = new File(basePath); File[] subDirs = baseDir.listFiles(); if (subDirs == null) { return ImmutableMap.of(); } ImmutableMap.Builder<TableId, TableStats> builder = ImmutableMap.builder(); for (File dir : subDirs) { String tableName = getTableName(dir.getName()); // NOTE: we are using recursion to traverse file tree as we know that leveldb table fs tree is couple levels deep. long size = getSize(dir); builder.put(LevelDBNameConverter.from(tableName), new TableStats(size)); } return builder.build(); } // todo: use Guava's utils instead when we switch to v15+ private static long getSize(File f) { if (f.isFile()) { return f.length(); } File[] files = f.listFiles(); if (files == null) { return 0; } long size = 0; for (File file : files) { size += getSize(file); } return size; } public WriteOptions getWriteOptions() { return writeOptions; } public DB getTable(String tableName) throws IOException { DB db = tables.get(tableName); if (db == null) { synchronized (tables) { db = tables.get(tableName); if (db == null) { db = openTable(tableName); tables.put(tableName, db); } } } return db; } public void ensureTableExists(String tableName) throws IOException { DB db = tables.get(tableName); if (db == null) { synchronized (tables) { db = tables.get(tableName); if (db == null) { createTable(tableName); } } } } private DB openTable(String tableName) throws IOException { String dbPath = getDBPath(basePath, tableName); Options options = new Options(); options.createIfMissing(false); options.errorIfExists(false); options.comparator(new KeyValueDBComparator()); options.blockSize(blockSize); options.cacheSize(cacheSize); // unfortunately, with the java version of leveldb, with createIfMissing set to false, factory.open will // see that there is no table and throw an exception, but it wont clean up after itself and will leave a // directory there with a lock. So we want to avoid calling open if the path doesn't already exist and // throw the exception ourselves. File dbDir = new File(dbPath); if (!dbDir.exists()) { throw new IOException("Database " + dbPath + " does not exist and the create if missing option is disabled"); } DB db = factory.open(dbDir, options); tables.put(tableName, db); return db; } private void createTable(String name) throws IOException { String dbPath = getDBPath(basePath, name); Options options = new Options(); options.createIfMissing(true); options.errorIfExists(false); options.comparator(new KeyValueDBComparator()); options.blockSize(blockSize); options.cacheSize(cacheSize); DB db = factory.open(new File(dbPath), options); tables.put(name, db); } public void dropTable(String name) throws IOException { DB db = tables.remove(name); if (db != null) { db.close(); } String dbPath = getDBPath(basePath, name); factory.destroy(new File(dbPath), new Options()); } private static String getDBPath(String basePath, String tableName) { String encodedTableName; try { encodedTableName = URLEncoder.encode(tableName, "UTF-8"); } catch (UnsupportedEncodingException e) { LOG.error("Error encoding table name '" + tableName + "'", e); throw new RuntimeException(e); } return new File(basePath, encodedTableName).getAbsolutePath(); } private static String getTableName(String tableDir) throws UnsupportedEncodingException { return URLDecoder.decode(tableDir, "UTF-8"); } /** * A comparator for the keys of key/value pairs. */ public static class KeyValueDBComparator implements DBComparator { @Override public int compare(byte[] left, byte[] right) { return KeyValue.KEY_COMPARATOR.compare(left, right); } @Override public byte[] findShortSuccessor(byte[] key) { return key; } @Override public byte[] findShortestSeparator(byte[] start, byte[] limit) { return start; } @Override public String name() { return "hbase-kv"; } } /** * Represents LevelDB's table stats. */ public static final class TableStats { private final long diskSizeBytes; public TableStats(long sizeInBytes) { this.diskSizeBytes = sizeInBytes; } public long getDiskSizeBytes() { return diskSizeBytes; } } }