/*
* Copyright 2013 Gordon Burgett and individual contributors
*
* 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 org.xflatdb.xflat.db;
import java.io.File;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom2.Element;
import org.jdom2.xpath.XPathExpression;
import org.xflatdb.xflat.EngineStateException;
import org.xflatdb.xflat.TableConfig;
import org.xflatdb.xflat.XFlatException;
import org.xflatdb.xflat.convert.PojoConverter;
import org.xflatdb.xflat.db.EngineBase.SpinDownEvent;
import org.xflatdb.xflat.db.EngineBase.SpinDownEventHandler;
/**
* A class containing metadata about a Table, and providing the ability to spin up
* engines for that table.
*
* TODO: this class really performs two responsibilities, one is managing the spin-up
* and spin-down of the engine for the table, the other is creating {@link TableBase} instances.
* The two responsibilities should be separated. You can see where the line is inside the
* {@link TableMetadataFactory} class.
* @author gordon
*/
public class TableMetadata implements EngineProvider {
//<editor-fold desc="EngineProvider dependencies">
String name;
public String getName(){
return name;
}
File engineFile;
AtomicReference<EngineBase> engine = new AtomicReference<>();
Element engineMetadata;
XFlatDatabase db;
private ReadWriteLock lock = new ReentrantReadWriteLock();
//</editor-fold>
IdGenerator idGenerator;
TableConfig config;
long lastActivity = System.currentTimeMillis();
Log log = LogFactory.getLog(getClass());
/**
* Called to determine whether the table has been inactive long enough
* that it can be spun down.
* @return true if the engine is inactive and has no uncommitted data.
*/
public boolean canSpinDown(){
EngineBase engine = this.engine.get();
return lastActivity + config.getInactivityShutdownMs() < System.currentTimeMillis() && engine != null && !engine.hasUncomittedData();
}
public EngineBase getEngine(){
return this.engine.get();
}
EngineState getEngineState(){
EngineBase engine = this.engine.get();
if(engine == null)
return EngineState.Uninitialized;
return engine.getState();
}
public TableMetadata(String name, XFlatDatabase db, File engineFile){
this.name = name;
this.db = db;
this.engineFile = engineFile;
}
//<editor-fold desc="table creation">
public TableBase getTable(Class<?> clazz){
if(clazz == null){
throw new IllegalArgumentException("clazz cannot be null");
}
//go ahead and spin up the engine if necessary
provideEngine();
TableBase table = makeTableForClass(clazz);
table.setIdGenerator(idGenerator);
table.setEngineProvider(this);
return table;
}
private <T> TableBase makeTableForClass(Class<T> clazz){
if(Element.class.equals(clazz)){
return new ElementTable(this.name);
}
IdAccessor accessor = IdAccessor.forClass(clazz);
if(accessor.hasId()){
if(!this.idGenerator.supports(accessor.getIdType())){
throw new XFlatException(String.format("Cannot serialize class %s to table %s: ID type %s not supported by the table's Id generator %s.",
clazz, this.name, accessor.getIdType(), this.idGenerator));
}
}
ConvertingTable<T> ret = new ConvertingTable<>(clazz, this.name);
ret.setConversionService(this.db.getConversionService());
//check if there's an alternate ID expression we can use for queries that come through
//the converting table.
if(accessor.hasId()){
XPathExpression<Object> alternateId = accessor.getAlternateIdExpression();
if(alternateId != null){
ret.setAlternateIdExpression(alternateId);
}
}
return ret;
}
//</editor-fold>
//<editor-fold desc="EngineProvider implementation">
@Override
public EngineBase provideEngine(){
long newActivity = System.currentTimeMillis();
boolean didLock = false;
try{
//are we within 100ms of being able to be spun down?
if(lastActivity + config.getInactivityShutdownMs() - 100 < newActivity){
//if so, lock
lock.readLock().lock();
didLock = true;
}
//for the next 2990ms (default inactivity shutdown) we should be OK to not lock,
//just want to be careful anytime we're close to interacting with a spin down.
this.lastActivity = System.currentTimeMillis();
return this.ensureSpinUp(didLock);
}
finally{
if(didLock)
lock.readLock().unlock();
}
}
public void notifyRecoveryComplete(){
this.lock.writeLock().lock();
try{
EngineBase engine = this.engine.get();
if(engine.getState() == EngineState.SpunUp){
//need to give the engine the go-ahead
engine.beginOperations();
}
}
finally{
this.lock.writeLock().unlock();
}
}
private EngineBase makeNewEngine(File file){
//TODO: engines will in the future be configurable & based on a strategy
EngineBase ret = db.getEngineFactory().newEngine(file, name, config);
ret.setConversionService(db.getConversionService());
ret.setExecutorService(db.getExecutorService());
ret.setTransactionManager(db.getEngineTransactionManager());
ret.setIdGenerator(this.idGenerator);
if(ret instanceof ShardedEngineBase){
//give it a metadata factory centered in its own file. If it uses this,
//it must also use the file as a directory.
((ShardedEngineBase)ret).setMetadataFactory(new TableMetadataFactory(this.db, file));
}
ret.loadMetadata(engineMetadata);
return ret;
}
private EngineBase ensureSpinUp(final boolean didLock){
EngineBase engine = this.engine.get();
EngineState state;
if(engine == null ||
(state = engine.getState()) == EngineState.SpinningDown ||
state == EngineState.SpunDown){
//must unlock a read lock before we enter a write lock
if(didLock)
lock.readLock().unlock();
lock.writeLock().lock();
try{
//re-check condition after locking
engine = this.engine.get();
if(engine == null ||
(state = engine.getState()) == EngineState.SpinningDown ||
state == EngineState.SpunDown){
EngineBase newEngine = makeNewEngine(engineFile);
if(this.engine.compareAndSet(engine, newEngine)){
engine = newEngine;
if(log.isTraceEnabled())
log.trace(String.format("Spinning up new engine for table %s", this.name));
}else{
//dunno how we got here, so long as we have the lock there ought to be no way.
newEngine = this.engine.get();
throw new EngineStateException("Synchronization error on spin up, could not put new engine",
newEngine == null ? EngineState.Uninitialized : newEngine.getState());
}
}
}finally{
//we can downgrade to a readlock
if(didLock)
lock.readLock().lock();
lock.writeLock().unlock();
}
}
else if(state == EngineState.SpinningUp ||
state == EngineState.SpunUp ||
state == EngineState.Running){
//good to go
return engine;
}
//spinUp returns true if this thread successfully spun it up
if(engine.spinUp()){
if(this.db.getState() == XFlatDatabase.DatabaseState.Running){
//spin-up could be called when initializing, in which case
//the engine needs to be ready to do recovery but not running yet.
engine.beginOperations();
}
}
return engine;
}
/**
* Spins down the engine, leaving the metadata in a state where it will
* be required to spin up a new engine before providing it.
* @param ignoreUncommitted Whether to require a spin down even if the engine has uncommitted
* data, effectively automatically reverting it. Usually only set when the
* entire database is being shut down.
* @param force whether to use forceSpinDown instead of a natural spin down.
* @return The engine that was spun down.
*/
public EngineBase spinDown(boolean ignoreUncommitted, boolean force){
lock.writeLock().lock();
try{
EngineBase engine = this.engine.get();
EngineState state;
if(engine == null ||
(state = engine.getState()) == EngineState.SpinningDown ||
state == EngineState.SpunDown){
//another thread already spinning it down
return engine;
}
try{
engine.getTableLock();
if(engine.hasUncomittedData() && !ignoreUncommitted){
//can't spin it down, return the engine
return engine;
}
this.engine.compareAndSet(engine, null);
}finally{
//table lock no longer needed
engine.releaseTableLock();
}
if(log.isTraceEnabled())
log.trace(String.format("Spinning down table %s", this.name));
if(!force && engine.spinDown(new SpinDownEventHandler(){
@Override
public void spinDownComplete(SpinDownEvent event) {
}
}))
{
//save metadata for the next engine
engine.saveMetadata(engineMetadata);
}
else{
engine.forceSpinDown();
}
return engine;
}
finally{
lock.writeLock().unlock();
}
}
//</editor-fold>
}