blob: 7a5beb9b3b8a5e13552edda68b7135efbddb57c6 [file] [log] [blame]
/*
* 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.jena.tdb2.sys;
import java.io.*;
import java.nio.file.*;
import java.util.List;
import java.util.zip.GZIPOutputStream;
import org.apache.jena.atlas.RuntimeIOException;
import org.apache.jena.atlas.lib.DateTimeUtils;
import org.apache.jena.atlas.lib.Pair;
import org.apache.jena.atlas.logging.Log;
import org.apache.jena.dboe.base.file.Location;
import org.apache.jena.dboe.sys.Names;
import org.apache.jena.dboe.transaction.txn.TransactionCoordinator;
import org.apache.jena.dboe.transaction.txn.TransactionalSystem;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.sparql.core.DatasetGraph;
import org.apache.jena.system.Txn;
import org.apache.jena.tdb2.TDBException;
import org.apache.jena.tdb2.params.StoreParams;
import org.apache.jena.tdb2.store.DatasetGraphSwitchable;
import org.apache.jena.tdb2.store.DatasetGraphTDB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Operations on and about TDB2 databases.
* <p>
* TDB2 uses a hierarchical structure to manage on disk.
* <p>
* If in-memory (non-scalable, not-performant, perfect simulation for functionality using a RAM disk, for testing mainly),
* then the switchable layer is just a convenience. DatasetGrapOps such as {@link #compact}
* and {@link #backup} do not apply.
* <p>
* Directory and files on disk:
* <ul>
* <li> {@code Data-NNNN/} -- databases by version. Compacting creates a new directory, leaving
* <li> store params, static (disk layout related - can not change once created) and dynamic (settings related to in-memory).
* <li> {@code Backups/} -- backups.
* <li> External indexes like {@code TextIndex/} -- the text index only applies to the current, latest database.
* </ul>
*/
public class DatabaseOps {
private static Logger LOG = LoggerFactory.getLogger(DatabaseOps.class);
public static final String dbPrefix = "Data";
public static final String SEP = "-";
private static final String startCount = "0001";
private static final String BACKUPS_DIR = "Backups";
// Basename of the backup file. "backup_{DateTime}.nq.gz
private static final String BACKUPS_FN = "backup";
/** Create a fresh database - called by {@code DatabaseMgr}.
* It is important to go via {@code DatabaseConnection} to avoid
* duplicate {@code DatasetGraphSwitchable}s for the same location.
*/
/*package*/ static DatasetGraph create(Location location, StoreParams params) {
// Hide implementation class.
return createSwitchable(location, params);
}
private static DatasetGraphSwitchable createSwitchable(Location location, StoreParams params) {
if ( location.isMem() ) {
DatasetGraph dsg = StoreConnection.connectCreate(location).getDatasetGraph();
return new DatasetGraphSwitchable(null, location, dsg);
}
// Exists?
if ( ! location.exists() )
throw new TDBException("No such location: "+location);
Path path = IOX.asPath(location);
// Scan for DBs
Path db = findLocation(path, dbPrefix);
if ( db == null ) {
db = path.resolve(dbPrefix+SEP+startCount);
IOX.createDirectory(db);
}
Location loc2 = IOX.asLocation(db);
DatasetGraphTDB dsg = StoreConnection.connectCreate(loc2, params).getDatasetGraphTDB();
DatasetGraphSwitchable appDSG = new DatasetGraphSwitchable(path, location, dsg);
return appDSG;
}
public static String backup(DatasetGraphSwitchable container) {
checkSupportsAdmin(container);
Path dbPath = container.getContainerPath();
Path backupDir = dbPath.resolve(BACKUPS_DIR);
if ( ! Files.exists(backupDir) )
IOX.createDirectory(backupDir);
DatasetGraph dsg = container;
// // Per backup source lock.
// synchronized(activeBackups) {
// // Atomically check-and-set
// if ( activeBackups.contains(dsg) )
// Log.warn(Fuseki.serverLog, "Backup already in progress");
// activeBackups.add(dsg);
// }
Pair<OutputStream, Path> x = openUniqueFileForWriting(backupDir, BACKUPS_FN, "nq.gz");
try (OutputStream out2 = x.getLeft();
OutputStream out1 = new GZIPOutputStream(out2, 8 * 1024);
OutputStream out = new BufferedOutputStream(out1)) {
Txn.executeRead(dsg, ()->RDFDataMgr.write(out, dsg, Lang.NQUADS));
} catch (IOException e) {
throw IOX.exception(e);
}
return x.getRight().toString();
}
private static void checkSupportsAdmin(DatasetGraphSwitchable container) {
if ( ! container.hasContainerPath() )
throw new TDBException("Dataset does not support admin operations");
}
// --> IOX
private static Pair<OutputStream, Path> openUniqueFileForWriting(Path dirPath, String basename, String ext) {
if ( ! Files.isDirectory(dirPath) )
throw new IllegalArgumentException("Not a directory: "+dirPath);
if ( basename.contains("/") || basename.contains("\\") )
throw new IllegalArgumentException("Basename must not contain a file path separator (\"/\" or \"\\\")");
String timestamp = DateTimeUtils.nowAsString("yyyy-MM-dd_HHmmss");
String filename = basename + "_" + timestamp;
Path p = dirPath.resolve(filename+"."+ext);
int x = 0;
for(;;) {
try {
OutputStream out = Files.newOutputStream(p, StandardOpenOption.CREATE_NEW);
return Pair.create(out, p);
} catch (AccessDeniedException ex) {
throw IOX.exception("Access denied", ex);
} catch (FileAlreadyExistsException ex) {
// Drop through and try again.
} catch (IOException ex) {
throw IOX.exception(ex);
}
// Try again.
x++;
if ( x >= 5 )
throw new RuntimeIOException("Can't create the unique name: number of attempts exceeded");
p = dirPath.resolve(filename+"_"+x+"."+ext);
}
}
// JVM-wide :-(
private static Object compactionLock = new Object();
public static void compact(DatasetGraphSwitchable container) {
checkSupportsAdmin(container);
synchronized(compactionLock) {
Path base = container.getContainerPath();
Path db1 = findLocation(base, dbPrefix);
if ( db1 == null )
throw new TDBException("No location: ("+base+","+dbPrefix+")");
Location loc1 = IOX.asLocation(db1);
// -- Checks
Location loc1a = ((DatasetGraphTDB)container.get()).getLocation();
if ( loc1a.isMem() ) {}
if ( ! loc1a.exists() )
throw new TDBException("No such location: "+loc1a);
// Is this the same database location?
if ( ! loc1.equals(loc1a) )
throw new TDBException("Inconsistent (not latest?) : "+loc1a+" : "+loc1);
// -- Checks
// Version
int v = IOX.extractIndex(db1.getFileName().toString(), dbPrefix, SEP);
String next = FilenameUtils.filename(dbPrefix, SEP, v+1);
Path db2 = db1.getParent().resolve(next);
IOX.createDirectory(db2);
Location loc2 = IOX.asLocation(db2);
LOG.debug(String.format("Compact %s -> %s\n", db1.getFileName(), db2.getFileName()));
compact(container, loc1, loc2);
}
}
// XXX Later - switch in a recording dataset, not block writers, and reply after
// switch over before releasing the new dataset to the container.
// Maybe copy indexes and switch the DSG over (drop switchable).
/** Copy the latest version from one location to another. */
private static void compact(DatasetGraphSwitchable container, Location loc1, Location loc2) {
if ( loc1.isMem() || loc2.isMem() )
throw new TDBException("Compact involves a memory location: "+loc1+" : "+loc2);
copyFiles(loc1, loc2);
StoreConnection srcConn = StoreConnection.connectExisting(loc1);
if ( srcConn == null )
throw new TDBException("No database at location : "+loc1);
if ( ! ( container.get() instanceof DatasetGraphTDB ) )
throw new TDBException("Not a TDB2 database in DatasetGraphSwitchable");
DatasetGraphTDB dsgCurrent = (DatasetGraphTDB)container.get();
if ( ! dsgCurrent.getLocation().equals(loc1) )
throw new TDBException("Inconsistent locations for base : "+dsgCurrent.getLocation()+" , "+dsgCurrent.getLocation());
DatasetGraphTDB dsgBase = srcConn.getDatasetGraphTDB();
if ( dsgBase != dsgCurrent )
throw new TDBException("Inconsistent datasets : "+dsgCurrent.getLocation()+" , "+dsgBase.getLocation());
TransactionalSystem txnSystem = dsgBase.getTxnSystem();
TransactionCoordinator txnMgr = dsgBase.getTxnSystem().getTxnMgr();
// Stop update. On exit there are no writers and none will start until switched over.
txnMgr.tryBlockWriters();
// txnMgr.begin(WRITE, false) will now bounce.
// Copy the latest generation.
DatasetGraphTDB dsgCompact = StoreConnection.connectCreate(loc2).getDatasetGraphTDB();
CopyDSG.copy(dsgBase, dsgCompact);
TransactionCoordinator txnMgr2 = dsgCompact.getTxnSystem().getTxnMgr();
txnMgr2.startExclusiveMode();
txnMgr.startExclusiveMode();
// No transactions on either database.
// Switch.
if ( ! container.change(dsgCurrent, dsgCompact) ) {
Log.warn(DatabaseOps.class, "Inconistent: old datasetgraph not as expected");
container.set(dsgCompact);
}
txnMgr2.finishExclusiveMode();
// New database running.
// Clean-up.
// txnMgr.finishExclusiveMode();
// Don't call : txnMgr.startWriters();
StoreConnection.release(dsgBase.getLocation());
}
/** Copy certain configuration files from {@code loc1} to {@code loc2}. */
private static void copyFiles(Location loc1, Location loc2) {
FileFilter copyFiles = (pathname)->{
String fn = pathname.getName();
if ( fn.equals(Names.TDB_CONFIG_FILE) )
return true;
if ( fn.endsWith(".opt") )
return true;
return false;
};
File d = new File(loc1.getDirectoryPath());
File[] files = d.listFiles(copyFiles);
copyFiles(loc1, loc2, files);
}
/** Copy a number of files from one location to another location. */
private static void copyFiles(Location loc1, Location loc2, File[] files) {
if ( files == null || files.length == 0 )
return;
for ( File f : files ) {
String fn = f.getName();
IOX.copy(loc1.getPath(fn), loc2.getPath(fn));
}
}
private static Path findLocation(Path directory, String namebase) {
if ( ! Files.exists(directory) )
return null;
// In-order, low to high.
List<Path> maybe = IOX.scanForDirByPattern(directory, namebase, SEP);
return Util.getLastOrNull(maybe);
}
}