Package com.foundationdb.server.store

Source Code of com.foundationdb.server.store.FDBSchemaManager

/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.foundationdb.server.store;

import com.foundationdb.ais.model.AbstractVisitor;
import com.foundationdb.ais.model.AkibanInformationSchema;
import com.foundationdb.ais.model.Columnar;
import com.foundationdb.ais.model.DefaultNameGenerator;
import com.foundationdb.ais.model.Group;
import com.foundationdb.ais.model.Index;
import com.foundationdb.ais.model.NameGenerator;
import com.foundationdb.ais.model.Routine;
import com.foundationdb.ais.model.SQLJJar;
import com.foundationdb.ais.model.Sequence;
import com.foundationdb.ais.model.Table;
import com.foundationdb.ais.model.TableName;
import com.foundationdb.ais.model.validation.AISValidations;
import com.foundationdb.ais.protobuf.ProtobufReader;
import com.foundationdb.ais.protobuf.ProtobufWriter;
import com.foundationdb.blob.BlobAsync;
import com.foundationdb.directory.DirectorySubspace;
import com.foundationdb.directory.PathUtil;
import com.foundationdb.qp.storeadapter.FDBAdapter;
import com.foundationdb.server.FDBTableStatusCache;
import com.foundationdb.server.error.FDBAdapterException;
import com.foundationdb.server.error.MetadataVersionNewerException;
import com.foundationdb.server.error.MetadataVersionTooOldException;
import com.foundationdb.server.rowdata.RowDefBuilder;
import com.foundationdb.server.service.Service;
import com.foundationdb.server.service.ServiceManager;
import com.foundationdb.server.service.config.ConfigurationService;
import com.foundationdb.server.service.listener.ListenerService;
import com.foundationdb.server.service.listener.TableListener;
import com.foundationdb.server.service.session.Session;
import com.foundationdb.server.service.session.SessionService;
import com.foundationdb.server.service.transaction.TransactionService;
import com.foundationdb.server.store.FDBTransactionService.TransactionState;
import com.foundationdb.server.store.TableChanges.ChangeSet;
import com.foundationdb.server.store.format.FDBStorageFormatRegistry;
import com.foundationdb.KeyValue;
import com.foundationdb.Range;
import com.foundationdb.Transaction;
import com.foundationdb.server.types.service.TypesRegistryService;
import com.foundationdb.subspace.Subspace;
import com.foundationdb.tuple.ByteArrayUtil;
import com.foundationdb.tuple.Tuple2;
import com.google.inject.Inject;
import com.persistit.Key;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;

/**
* Directory usage:
* <pre>
* root_dir/
*   schemaManager/
*     online/
*       id/
*         dml/
*           tid/           => hKeys of concurrent DML
*         protobuf/
*           schema_name    => byte[] (AIS Protobuf)
*         changes/
*           tid            => byte[] (ChangeSet Protobuf)
*         generation       => long   (session's generation)
*         error            => string (error message, only set on error)
*     protobuf/
*       schema_name/       => byte[] (AIS Protobuf)
*     generation           => long
*     dataVersion          => long
*     metaDataVersion      => long
*     onlineSession        => long
* </pre>
*
* Transactional Reasoning:
* <ul>
*     <li>All consumers of getAis() do a full read of the generation key to determine the proper version.</li>
*     <li>All DDL executors increment the generation while making the AIS changes</li>
*     <li>Whenever a new AIS is read, the name generator and table version map is re-set</li>
*     <li>Since there can be exactly one change to the generation at a time, all generated names and ids will be unique</li>
* </ul>
*/
public class FDBSchemaManager extends AbstractSchemaManager implements Service, TableListener
{
    private static final Logger LOG = LoggerFactory.getLogger(FDBSchemaManager.class);

    static final String CLEAR_INCOMPATIBLE_DATA_PROP = "fdbsql.fdb.clear_incompatible_data";
    static final String EXTERNAL_CLEAR_MSG = "SQL Layer metadata has been externally modified. Restart required.";
    static final String EXTERNAL_VER_CHANGE_MSG = "SQL Layer version has been changed from another node.";

    private static final List<String> SCHEMA_MANAGER_PATH = Arrays.asList("schemaManager");
    private static final List<String> PROTOBUF_PATH = Arrays.asList("protobuf");
    private static final List<String> ONLINE_PATH = Arrays.asList("online");
    private static final List<String> CHANGES_PATH = Arrays.asList("changes");
    private static final List<String> DML_PATH = Arrays.asList("dml");
    private static final String GENERATION_KEY = "generation";
    private static final String DATA_VERSION_KEY = "dataVersion";
    private static final String META_VERSION_KEY = "metaDataVersion";
    private static final String ONLINE_SESSION_KEY = "onlineSession";
    private static final String ERROR_KEY = "error";

    /**
     * 1) Initial
     * 2) Fixed charset width computation
     * 3) No long string digest in indexes
     * 4) Unique index format change
     * 5) Remove group index row counts
     * 6) Metadata stored using blob layer
     */
    private static final long CURRENT_DATA_VERSION = 6;
    /**
     * 1) Initial directory based
     * 2) Online metadata support
     * 3) Type bundles
     * 4) Online DDL error-ing
     * 5) ????
     * 6) ????
     * 7) Hidden PK to Sequence/__row_id
     */
    private static final long CURRENT_META_VERSION = 7;

    private static final Session.Key<AkibanInformationSchema> SESSION_AIS_KEY = Session.Key.named("AIS_KEY");
    private static final AkibanInformationSchema SENTINEL_AIS = new AkibanInformationSchema(Integer.MIN_VALUE);

    private final FDBHolder holder;
    private final FDBTransactionService txnService;
    private final ListenerService listenerService;
    private final ServiceManager serviceManager;
    private final Object AIS_LOCK = new Object();

    private DirectorySubspace rootDir;
    private DirectorySubspace smDirectory;
    private byte[] packedGenKey;
    private byte[] packedDataVerKey;
    private byte[] packedMetaVerKey;
    private FDBTableStatusCache tableStatusCache;
    private AkibanInformationSchema curAIS;
    private NameGenerator nameGenerator;
    private AkibanInformationSchema memoryTableAIS;


    @Inject
    public FDBSchemaManager(ConfigurationService config,
                            SessionService sessionService,
                            FDBHolder holder,
                            TransactionService txnService,
                            ListenerService listenerService,
                            ServiceManager serviceManager,
                            TypesRegistryService typesRegistryService) {
        super(config, sessionService, txnService, typesRegistryService, new FDBStorageFormatRegistry(config));
        this.holder = holder;
        if(txnService instanceof FDBTransactionService) {
            this.txnService = (FDBTransactionService)txnService;
        } else {
            throw new IllegalStateException("May only be used with FDBTransactionService");
        }
        this.listenerService = listenerService;
        this.serviceManager = serviceManager;
    }


    //
    // Service
    //

    @Override
    public void start() {
        super.start();
        final boolean clearIncompatibleData = Boolean.parseBoolean(config.getProperty(CLEAR_INCOMPATIBLE_DATA_PROP));

        initSchemaManagerDirectory();
        this.memoryTableAIS = new AkibanInformationSchema();
        this.tableStatusCache = new FDBTableStatusCache(holder, txnService);

        try(Session session = sessionService.createSession()) {
            txnService.run(session, new Runnable() {
                @Override
                public void run() {
                    TransactionState txn = txnService.getTransaction(session);
                    Boolean isCompatible = isDataCompatible(txn, false);
                    if(isCompatible == Boolean.FALSE) {
                        if(!clearIncompatibleData) {
                            isDataCompatible(txn, true);
                            assert false; // Throw expected
                        }
                        LOG.warn("Clearing incompatible data directory: {}", rootDir.getPath());
                        // Delicate: Directory removal is safe as this is the first service started that consumes it.
                        //           Remove after the 1.9.2 release, which includes entry point for doing this.
                        rootDir.remove(txn.getTransaction()).get();
                        initSchemaManagerDirectory();
                        isCompatible = null;
                    }
                    if(isCompatible == null) {
                        saveInitialState(txn);
                    }
                    AkibanInformationSchema newAIS = loadFromStorage(session);
                    buildRowDefs(session, newAIS);
                    FDBSchemaManager.this.curAIS = newAIS;
                }
            });

            this.nameGenerator = new DefaultNameGenerator(curAIS);

            txnService.run(session, new Runnable() {
                @Override
                public void run() {
                    mergeNewAIS(session, curAIS);
                }
            });
        }

        listenerService.registerTableListener(this);

        registerSystemTables();
    }

    @Override
    public void stop() {
        listenerService.deregisterTableListener(this);
        super.stop();
        this.tableStatusCache = null;
        this.curAIS = null;
        this.nameGenerator = null;
        this.memoryTableAIS = null;
    }

    @Override
    public void crash() {
        stop();
    }


    //
    // SchemaManager
    //
    // Called through BasicDDLFunctions, but only via
    // TransactionService.run(Session, Runnable), which handles the
    // Exceptions

    @Override
    public void addOnlineChangeSet(Session session, ChangeSet changeSet) {
        OnlineSession onlineSession = getOnlineSession(session, true);
        LOG.debug("addOnlineChangeSet: {} -> {}", onlineSession.id, changeSet);
        onlineSession.tableIDs.add(changeSet.getTableId());
        TransactionState txn = txnService.getTransaction(session);
        // Require existence
        DirectorySubspace onlineDir = openDirectory (txn, smDirectory, onlineDirPath(onlineSession.id));
        // Create on demand
       DirectorySubspace changeDir = onlineDir.createOrOpen(txn.getTransaction(), CHANGES_PATH).get();
        byte[] packedKey = changeDir.pack(changeSet.getTableId());
        byte[] value = ChangeSetHelper.save(changeSet);
        txn.setBytes(packedKey, value);
        // TODO: Cleanup into Abstract. For consistency with PSSM.
        if(getAis(session).getGeneration() == getOnlineAIS(session).getGeneration()) {
            bumpGeneration(session);
        }
    }

    @Override
    public Set<String> getTreeNames(final Session session) {
        return txnService.run(session, new Callable<Set<String>>() {
            @Override
            public Set<String> call() {
                AkibanInformationSchema ais = getAis(session);
                StorageNameVisitor visitor = new StorageNameVisitor();
                ais.visit(visitor);
                for(Sequence s : ais.getSequences().values()) {
                    visitor.visit(s);
                }
                return visitor.pathNames;
            }
        });
    }


    //
    // AbstractSchemaManager
    //

    @Override
    protected NameGenerator getNameGenerator(Session session) {
        return (getOnlineSession(session, null) != null) ?
            FDBNameGenerator.createForOnlinePath(txnService.getTransaction(session), rootDir, nameGenerator) :
            FDBNameGenerator.createForDataPath(txnService.getTransaction(session), rootDir, nameGenerator);
    }

    @Override
    protected void storedAISChange(Session session,
                                   AkibanInformationSchema newAIS,
                                   Collection<String> schemaNames) {
        ByteBuffer buffer = null;
        validateForSession(session, newAIS, null);
        try {
            TransactionState txn = txnService.getTransaction(session);
            for(String schema : schemaNames) {
                DirectorySubspace dir = smDirectory.createOrOpen(txn.getTransaction(), PROTOBUF_PATH).get();
                buffer = storeProtobuf(txn, dir, buffer, newAIS, schema);
            }
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(session, e);
        }
        buildRowDefs(session, newAIS);
    }

    @Override
    protected void unStoredAISChange(Session session, final AkibanInformationSchema newAIS) {
        //
        // The *after* commit callback below is acceptable because in the real system, this
        // this method is only called during startup or shutdown and those are both single threaded.
        //
        // If that ever changes, that needs adjusted. However, the worst that can happen is a read
        // of the new system table (or routine, etc) after the commit but before the callback fires
        // This does not affect any user data whatsoever.
        //
        //
        // To be more strict and prevent that possibility, checks below would handle it but require test workarounds:
        //
        //ServiceManager.State state = serviceManager.getState();
        //if((state != ServiceManager.State.STARTING) && (state != ServiceManager.State.STOPPING)) {
        //    throw new IllegalStateException("Unexpected unSaved change: " + serviceManager.getState());
        //}

        // A new generation isn't needed as we evict the current copy below and, as above, single threaded startup
        validateForSession(session, newAIS, null);
        buildRowDefs(session, newAIS);

        txnService.addCallback(session, TransactionService.CallbackType.COMMIT, new TransactionService.Callback() {
            @Override
            public void run(Session session, long timestamp) {
                synchronized(AIS_LOCK) {
                    saveMemoryTables(newAIS);
                    FDBSchemaManager.this.curAIS = SENTINEL_AIS;
                }
            }
        });
    }

    @Override
    protected void clearTableStatus(Session session, Table table) {
        tableStatusCache.clearTableStatus(session, table);
    }

    @Override
    protected void bumpGeneration(Session session) {
        getNextGeneration(session, txnService.getTransaction(session));
    }

    @Override
    protected long generateSaveOnlineSessionID(Session session) {
        TransactionState txn = txnService.getTransaction(session);
        // New ID
        byte[] packedKey = smDirectory.pack(ONLINE_SESSION_KEY);
        byte[] value = txn.getValue(packedKey);
        long newID = (value == null) ? 1 : Tuple2.fromBytes(value).getLong(0) + 1;
        txn.setBytes(packedKey, Tuple2.from(newID).pack());
        // Create directory
        DirectorySubspace dir = createDirectory(txn, smDirectory,onlineDirPath(newID));
        packedKey = dir.pack(GENERATION_KEY);
        value = Tuple2.from(-1L).pack(); // No generation yet
        txn.setBytes(packedKey, value);
        return newID;
    }

    @Override
    protected void storedOnlineChange(Session session,
                                      OnlineSession onlineSession,
                                      AkibanInformationSchema newAIS,
                                      Collection<String> schemas) {
        // Get a unique generation for this AIS, but will only be visible to owning session
        validateForSession(session, newAIS, null);
        // Again so no other transactions see the new one from validate
        bumpGeneration(session);
        // Save online schemas
        TransactionState txn = txnService.getTransaction(session);
        List<String> idPath = onlineDirPath(onlineSession.id);
        DirectorySubspace idDir = openDirectory(txn, smDirectory, idPath);
        txn.setBytes(idDir.pack(GENERATION_KEY), Tuple2.from(newAIS.getGeneration()).pack());
       
        try {
            DirectorySubspace protobufDir = idDir.createOrOpen(txn.getTransaction(), PROTOBUF_PATH).get();
            ByteBuffer buffer = null;
            for(String name : schemas) {
                buffer = storeProtobuf(txn, protobufDir, buffer, newAIS, name);
            }
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(session, e);
        }
    }

    @Override
    protected void clearOnlineState(Session session, OnlineSession onlineSession) {
        try {
            TransactionState txn = txnService.getTransaction(session);
            smDirectory.remove(txn.getTransaction(), onlineDirPath(onlineSession.id)).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(session, e);
        }
    }

    @Override
    protected OnlineCache buildOnlineCache(Session session) {
        OnlineCache onlineCache = new OnlineCache();

        TransactionState txnState = txnService.getTransaction(session);
        Transaction txn = txnState.getTransaction();
       
        try {
           
            DirectorySubspace onlineDir = smDirectory.createOrOpen(txn, ONLINE_PATH).get();
   
            // For each online ID
            for(String idStr : onlineDir.list(txn).get()) {
                long onlineID = Long.parseLong(idStr);
                DirectorySubspace idDir = onlineDir.open(txn, Arrays.asList(idStr)).get();
                byte[] genBytes = txnState.getValue(idDir.pack(GENERATION_KEY));
                long generation = Tuple2.fromBytes(genBytes).getLong(0);
   
                // load protobuf
                if(idDir.exists(txn, PROTOBUF_PATH).get()) {
                    DirectorySubspace protobufDir = idDir.open(txn, PROTOBUF_PATH).get();
                    int schemaCount = 0;
                    for(String schema : protobufDir.list(txn).get()) {
                        Long prev = onlineCache.schemaToOnline.put(schema, onlineID);
                        assert (prev == null) : String.format("%s, %d, %d", schema, prev, onlineID);
                        ++schemaCount;
                    }
                    if(generation != -1) {
                        ProtobufReader reader = newProtobufReader();
                        loadProtobufChildren(txnState, protobufDir, reader, null);
                        loadPrimaryProtobuf(txnState, reader, onlineCache.schemaToOnline.keySet());
   
                        // Reader will have two copies of affected schemas, skip second (i.e. non-online)
                        AkibanInformationSchema newAIS = finishReader(reader);
                        validateAndFreeze(session, newAIS, generation);
                        buildRowDefs(session, newAIS);
                        onlineCache.onlineToAIS.put(onlineID, newAIS);
                    } else if(schemaCount != 0) {
                        throw new IllegalStateException("No generation but had schemas");
                    }
                }
   
                // Load ChangeSets
                if(idDir.exists(txn, CHANGES_PATH).get()) {
                    DirectorySubspace changesDir = idDir.open(txn, CHANGES_PATH).get();
                    for(KeyValue kv : txn.getRange(Range.startsWith(changesDir.pack()))) {
                        ChangeSet cs = ChangeSetHelper.load(kv.getValue());
                        Long prev = onlineCache.tableToOnline.put(cs.getTableId(), onlineID);
                        assert (prev == null) : String.format("%d, %d, %d", cs.getTableId(), prev, onlineID);
                        onlineCache.onlineToChangeSets.put(onlineID, cs);
                    }
                }
            }
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(session, e);
        }
        return onlineCache;
    }

    @Override
    protected void newTableVersions(Session session, Map<Integer, Integer> versions) {
        // None
    }

    @Override
    protected void renamingTable(Session session, TableName oldName, TableName newName) {
        try {
            Transaction txn = txnService.getTransaction(session).getTransaction();
            // Ensure destination schema exists. Can go away if schema lifetime becomes explicit.
            rootDir.createOrOpen(txn, PathUtil.popBack(FDBNameGenerator.dataPath(newName))).get();
            rootDir.move(txn, FDBNameGenerator.dataPath(oldName), FDBNameGenerator.dataPath(newName)).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(session, e);
        }
    }

    @Override
    public AkibanInformationSchema getSessionAIS(Session session) {
        AkibanInformationSchema localAIS = session.get(SESSION_AIS_KEY);
        if(localAIS != null) {
            return localAIS;
        }
        TransactionState txn = txnService.getTransaction(session);
        long generation = getTransactionalGeneration(txn);
        localAIS = curAIS;
        if(generation != localAIS.getGeneration()) {
            synchronized(AIS_LOCK) {
                // May have been waiting
                if(generation == curAIS.getGeneration()) {
                    localAIS = curAIS;
                } else {
                    localAIS = loadFromStorage(session);
                    buildRowDefs(session, localAIS);
                    if(localAIS.getGeneration() > curAIS.getGeneration()) {
                        curAIS = localAIS;
                        mergeNewAIS(session, curAIS);
                    }
                }
            }
        }
        attachToSession(session, localAIS);
        return localAIS;
    }

    @Override
    public long getOldestActiveAISGeneration() {
        return curAIS.getGeneration();
    }

    @Override
    public Set<Long> getActiveAISGenerations() {
        return Collections.singleton(curAIS.getGeneration());
    }

    @Override
    public boolean hasTableChanged(Session session, int tableID) {
        // Handled by serializable transactions
        return false;
    }

    //
    // TableListener
    //

    @Override
    public void onCreate(Session session, Table table) {
        // None
    }

    @Override
    public void onDrop(Session session, Table table) {
        try {
            Transaction txn = txnService.getTransaction(session).getTransaction();
            rootDir.removeIfExists(txn, FDBNameGenerator.dataPath(table.getName())).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(session, e);
        }
    }

    @Override
    public void onTruncate(Session session, Table table, boolean isFast) {
        // None
    }

    @Override
    public void onCreateIndex(Session session, Collection<? extends Index> indexes) {
        // None
    }

    @Override
    public void onDropIndex(Session session, Collection<? extends Index> indexes) {
        // None
    }

     // TODO: Remove when FDB shutdown hook issue is resolved
     @Override
     public void unRegisterMemoryInformationSchemaTable(TableName tableName) {
         if(serviceManager.getState() == ServiceManager.State.STOPPING) {
             return; // Skip as to avoid DB access
         }
         super.unRegisterMemoryInformationSchemaTable(tableName);
     }

    // TODO: Remove when FDB shutdown hook issue is resolved
     @Override
     public void unRegisterSystemRoutine(TableName routineName) {
         if(serviceManager.getState() == ServiceManager.State.STOPPING) {
             return; // Skip as to avoid DB access
         }
         super.unRegisterSystemRoutine(routineName);
     }

    @Override
    public void addOnlineHandledHKey(Session session, int tableID, Key hKey) {
        AkibanInformationSchema ais = getAis(session);
        OnlineCache onlineCache = getOnlineCache(session, ais);
        Long onlineID = onlineCache.tableToOnline.get(tableID);
        if(onlineID == null) {
            throw new IllegalArgumentException("No online change for table: " + tableID);
        }
        TransactionState txn = txnService.getTransaction(session);
        DirectorySubspace tableDMLDir = getOnlineTableDMLDir(txn, onlineID, tableID);
        byte[] hKeyBytes = Arrays.copyOf(hKey.getEncodedBytes(), hKey.getEncodedSize());
        byte[] packedKey = tableDMLDir.pack(Tuple2.from(hKeyBytes));
        txn.setBytes(packedKey, new byte[0]);
    }

    @Override
    public void setOnlineDMLError(Session session, int tableID, String message) {
        AkibanInformationSchema ais = getAis(session);
        OnlineCache onlineCache = getOnlineCache(session, ais);
        Long onlineID = onlineCache.tableToOnline.get(tableID);
        if(onlineID == null) {
            throw new IllegalArgumentException("No online change for table: " + tableID);
        }
        TransactionState txn = txnService.getTransaction(session);
        DirectorySubspace onlineDir = getOnlineDir(txn, onlineID);
        byte[] packedKey = onlineDir.pack(ERROR_KEY);
        byte[] packedValue = Tuple2.from(message).pack();
        txn.setBytes(packedKey, packedValue);
    }

    @Override
    public String getOnlineDMLError(Session session) {
        OnlineSession onlineSession = getOnlineSession(session, true);
        TransactionState txn = txnService.getTransaction(session);
        DirectorySubspace dir = getOnlineDir(txn, onlineSession.id);
        byte[] value = txn.getValue(dir.pack(ERROR_KEY));
        return (value == null) ? null : Tuple2.fromBytes(value).getString(0);
    }

    @Override
    public Iterator<byte[]> getOnlineHandledHKeyIterator(Session session, int tableID, Key hKey) {
        OnlineSession onlineSession = getOnlineSession(session, true);
        if(LOG.isDebugEnabled()) {
            LOG.debug("addOnlineHandledHKey: {}/{} -> {}", new Object[] { onlineSession.id, tableID, hKey });
        }
        TransactionState txn = txnService.getTransaction(session);
        DirectorySubspace tableDMLDir = getOnlineTableDMLDir(txn, onlineSession.id, tableID);
        byte[] startKey = tableDMLDir.pack();
        byte[] endKey = ByteArrayUtil.strinc(startKey);
        if(hKey != null) {
            startKey = ByteArrayUtil.join(tableDMLDir.pack(), Arrays.copyOf(hKey.getEncodedBytes(), hKey.getEncodedSize()));
        }
        final Iterator<KeyValue> iterator = txn.getRangeIterator(startKey, endKey);
        final int prefixLength = tableDMLDir.pack().length;
        return new Iterator<byte[]>() {
            @Override
            public boolean hasNext() {
                throw new UnsupportedOperationException();
            }

            @Override
            public byte[] next() {
                if(!iterator.hasNext()) {
                    return null;
                }
                byte[] keyBytes = iterator.next().getKey();
                return Arrays.copyOfRange(keyBytes, prefixLength, keyBytes.length);
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }


    //
    // Helpers
    //

    private DirectorySubspace openDirectory (TransactionState txn, DirectorySubspace dir, List<String> dirs) {
        try {
            return dir.open(txn.getTransaction(), dirs).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(txn.session, e);
        }
    }
   
    private DirectorySubspace createDirectory (TransactionState txn, DirectorySubspace dir, List<String>dirs) {
        try {
            // Create directory
           return dir.create(txn.getTransaction(), dirs).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(txn.session, e);
        }
    }

    private void initSchemaManagerDirectory() {
        rootDir = holder.getRootDirectory();
        smDirectory = rootDir.createOrOpen(holder.getDatabase(), SCHEMA_MANAGER_PATH).get();
        // Cache as this is checked on every transaction
        packedGenKey = smDirectory.pack(GENERATION_KEY);
        // And these are checked for every AIS load
        packedDataVerKey = smDirectory.pack(DATA_VERSION_KEY);
        packedMetaVerKey = smDirectory.pack(META_VERSION_KEY);
    }

    private long getNextGeneration(Session session, TransactionState txn) {
        long newGeneration = getTransactionalGeneration(txn) + 1;
        saveGeneration(txn, newGeneration);
        return newGeneration;
    }

    private void saveGeneration(TransactionState txn, long newValue) {
        byte[] packedGen = Tuple2.from(newValue).pack();
        txn.setBytes(packedGenKey, packedGen);
    }

    /** Validate and freeze {@code newAIS} at {@code generation} (or allocate a new one if {@code null}). */
    private void validateAndFreeze(Session session, AkibanInformationSchema newAIS, Long generation) {
        newAIS.validate(AISValidations.ALL_VALIDATIONS).throwIfNecessary();
        if(generation == null) {
            generation = getNextGeneration(session, txnService.getTransaction(session));
        }
        newAIS.setGeneration(generation);
        newAIS.freeze();
    }

    /** {@link #validateAndFreeze} and {@link #attachToSession}. For AISs {@code session} should continue to see. */
    private void validateForSession(Session session, AkibanInformationSchema newAIS, Long generation) {
        validateAndFreeze(session, newAIS, generation);
        attachToSession(session, newAIS);
    }

    private void saveInitialState(TransactionState txn) {
        txn.setBytes(packedDataVerKey, Tuple2.from(CURRENT_DATA_VERSION).pack());
        txn.setBytes(packedMetaVerKey, Tuple2.from(CURRENT_META_VERSION).pack());
        txn.setBytes(packedGenKey, Tuple2.from(0).pack());
    }

    private ByteBuffer storeProtobuf(TransactionState txn,
                                     DirectorySubspace dir,
                                     ByteBuffer buffer,
                                     AkibanInformationSchema newAIS,
                                     String schema) {
        final ProtobufWriter.WriteSelector selector;
        switch(schema) {
            case TableName.INFORMATION_SCHEMA:
            case TableName.SECURITY_SCHEMA:
                selector = new ProtobufWriter.SingleSchemaSelector(schema) {
                    @Override
                    public Columnar getSelected(Columnar columnar) {
                        if(columnar.isTable() && ((Table)columnar).hasMemoryTableFactory()) {
                            return null;
                        }
                        return columnar;
                    }
                };
            break;
            case TableName.SYS_SCHEMA:
            case TableName.SQLJ_SCHEMA:
                selector = new ProtobufWriter.SingleSchemaSelector(schema) {
                    @Override
                    public boolean isSelected(Routine routine) {
                        return false;
                    }
                };
            break;
            default:
                selector = new ProtobufWriter.SingleSchemaSelector(schema);
        }

        if(newAIS.getSchema(schema) != null) {
            Subspace blobDir = dir.createOrOpen(txn.getTransaction(), PathUtil.from(schema)).get();
            buffer = serialize(buffer, newAIS, selector);
            byte[] newValue;
            if((buffer.position() == 0) && (buffer.limit() == buffer.capacity())) {
                newValue = buffer.array();
            } else {
                newValue = Arrays.copyOfRange(buffer.array(), buffer.position(), buffer.limit());
            }
            BlobAsync blob = new BlobAsync(blobDir);
            blob.truncate(txn.getTransaction(), 0).get();
            blob.write(txn.getTransaction(), 0, newValue).get();
        } else {
            dir.removeIfExists(txn.getTransaction(), PathUtil.from(schema)).get();
        }
        return buffer;
    }

    private void saveMemoryTables(AkibanInformationSchema newAIS) {
        // Want *just* non-persisted memory tables and system routines
        this.memoryTableAIS = aisCloner.clone(newAIS, new ProtobufWriter.TableFilterSelector() {
            @Override
            public Columnar getSelected(Columnar columnar) {
                if(columnar.isTable() && ((Table)columnar).hasMemoryTableFactory()) {
                    return columnar;
                }
                return null;
            }

            @Override
            public boolean isSelected(Sequence sequence) {
                return false;
            }

            @Override
            public boolean isSelected(Routine routine) {
                return isSystemName(routine.getName());
            }

            @Override
            public boolean isSelected(SQLJJar sqljJar) {
                return isSystemName(sqljJar.getName());
            }

            protected boolean isSystemName(TableName name) {
                return TableName.SYS_SCHEMA.equals(name.getSchemaName()) ||
                       TableName.SQLJ_SCHEMA.equals(name.getSchemaName()) ||
                       TableName.SECURITY_SCHEMA.equals(name.getSchemaName());
            }
        });
    }

    private void buildRowDefs(Session session, AkibanInformationSchema newAIS) {
        tableStatusCache.detachAIS();
        RowDefBuilder rowDefBuilder = new RowDefBuilder(session, newAIS, tableStatusCache);
        rowDefBuilder.build();
    }

    /** {@code null} = no data present, {@code true} = compatible, {@code false} = incompatible */
    private Boolean isDataCompatible(TransactionState txn, boolean throwIfIncompatible) {
        byte[] dataVerValue = txn.getValue(packedDataVerKey);
        byte[] metaVerValue = txn.getValue(packedMetaVerKey);
        if(dataVerValue == null || metaVerValue == null) {
            return null;
        }
        long storedDataVer = Tuple2.fromBytes(dataVerValue).getLong(0);
        long storedMetaVer = Tuple2.fromBytes(metaVerValue).getLong(0);
        if((storedDataVer != CURRENT_DATA_VERSION) || (storedMetaVer != CURRENT_META_VERSION)) {
            if(throwIfIncompatible) {
                if ((storedDataVer >= CURRENT_DATA_VERSION) || (storedMetaVer >= CURRENT_META_VERSION)) {
                    throw new MetadataVersionNewerException (CURRENT_META_VERSION, CURRENT_DATA_VERSION, storedMetaVer, storedDataVer);
                } else {
                    throw new MetadataVersionTooOldException(CURRENT_META_VERSION, CURRENT_DATA_VERSION, storedMetaVer, storedDataVer);
                }
            }
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    private void checkDataVersions(TransactionState txn) {
        Boolean isCompatible = isDataCompatible(txn, false);
        // Can only be missing if clear()-ed outside SQL Layer. Give clear message but no recovery attempt.
        if(isCompatible == null) {
            throw new FDBAdapterException(EXTERNAL_CLEAR_MSG);
        }
        if(isCompatible == Boolean.FALSE) {
            throw new FDBAdapterException(EXTERNAL_VER_CHANGE_MSG);
        }
        assert isCompatible;
    }

    private AkibanInformationSchema loadFromStorage(Session session) {
        TransactionState txn = txnService.getTransaction(session);
        checkDataVersions(txn);
        ProtobufReader reader = newProtobufReader();
        loadPrimaryProtobuf(txn, reader, null);
        finishReader(reader);
        validateAndFreeze(session, reader.getAIS(), getTransactionalGeneration(txn));
        return reader.getAIS();
    }

    private void loadProtobufChildren(TransactionState txn, DirectorySubspace dir, ProtobufReader reader, Collection<String> skip) {
        for(String subDirName : dir.list(txn.getTransaction()).get()) {
            if((skip != null) && skip.contains(subDirName)) {
                continue;
            }
            Subspace subDir = dir.open(txn.getTransaction(), PathUtil.from(subDirName)).get();
            BlobAsync blob = new BlobAsync(subDir);
            byte[] data = blob.read(txn.getTransaction()).get();
            if(data != null) {
                ByteBuffer buffer = ByteBuffer.wrap(data);
                reader.loadBuffer(buffer);
            }
        }
    }

    private void loadPrimaryProtobuf(TransactionState txn, ProtobufReader reader, Collection<String> skipSchemas) {
        DirectorySubspace dir = smDirectory.createOrOpen(txn.getTransaction(), PROTOBUF_PATH).get();
        loadProtobufChildren(txn, dir, reader, skipSchemas);
    }

    private long getTransactionalGeneration(TransactionState txn) {
        byte[] packedGen;
        packedGen = txn.getValue(packedGenKey);
        if(packedGen == null) {
            throw new FDBAdapterException(EXTERNAL_CLEAR_MSG);
        }
        return Tuple2.fromBytes(packedGen).getLong(0);
    }

    private void mergeNewAIS(Session session, AkibanInformationSchema newAIS) {
        OnlineCache onlineCache = getOnlineCache(session, newAIS);
        nameGenerator.mergeAIS(newAIS);
        for(AkibanInformationSchema onlineAIS : onlineCache.onlineToAIS.values()) {
            nameGenerator.mergeAIS(onlineAIS);
        }
    }

    private void attachToSession(Session session, AkibanInformationSchema ais) {
        AkibanInformationSchema prev = session.put(SESSION_AIS_KEY, ais);
        if(prev == null) {
            txnService.addCallback(session, TransactionService.CallbackType.END, CLEAR_SESSION_KEY_CALLBACK);
        }
    }

    private ProtobufReader newProtobufReader() {
        // Start with existing memory tables, merge in stored ones
        final AkibanInformationSchema newAIS = aisCloner.clone(memoryTableAIS);
        return new ProtobufReader(typesRegistryService.getTypesRegistry(), storageFormatRegistry, newAIS);
    }

    private DirectorySubspace getOnlineDir(TransactionState txn, long onlineID) {
        try {
            // Require existence
            return smDirectory.open(txn.getTransaction(), onlineDirPath(onlineID)).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(txn.session, e);
        }
    }

    private DirectorySubspace getOnlineTableDMLDir(TransactionState txn, long onlineID, int tableID) {
        try {
            // Create on demand
            return getOnlineDir(txn, onlineID).createOrOpen(txn.getTransaction(), PathUtil.extend(DML_PATH, String.valueOf(tableID))).get();
        } catch (RuntimeException e) {
            throw FDBAdapter.wrapFDBException(txn.session, e);
        }
    }

    //
    // Test helpers
    //

    byte[] getPackedGenKey() {
        return packedGenKey;
    }

    byte[] getPackedDataVerKey() {
        return packedDataVerKey;
    }

    byte[] getPackedMetaVerKey() {
        return packedMetaVerKey;
    }

    //
    // Static helpers
    //

    private static AkibanInformationSchema finishReader(ProtobufReader reader) {
        reader.loadAIS();
        for(Table table : reader.getAIS().getTables().values()) {
            // nameGenerator is only needed to generate hidden PK, which shouldn't happen here
            table.endTable(null);
           
        }
        return reader.getAIS();
    }

    private static List<String> onlineDirPath(long onlineID) {
        return PathUtil.extend(ONLINE_PATH, Long.toString(onlineID));
    }

    /** Serialize given AIS. Allocates a new buffer if necessary so always use <i>returned</i> buffer. */
    private static ByteBuffer serialize(ByteBuffer buffer, AkibanInformationSchema ais, ProtobufWriter.WriteSelector selector) {
        ProtobufWriter writer = new ProtobufWriter(selector);
        writer.save(ais);
        int size = writer.getBufferSize();
        if(buffer == null || (buffer.capacity() < size)) {
            buffer = ByteBuffer.allocate(size);
        }
        buffer.clear();
        writer.serialize(buffer);
        buffer.flip();
        return buffer;
    }

    /** Collect all StorageDescriptions into a test/debug friendly set of path descriptions. */
    private static class StorageNameVisitor extends AbstractVisitor {
        public final Set<String> pathNames = new TreeSet<>();

        @Override
        public void visit(Group group) {
            track(FDBNameGenerator.dataPath(group.getName()));
        }

        @Override
        public void visit(Table table) {
            track(FDBNameGenerator.dataPath(table.getName()));
        }

        @Override
        public void visit(Index index) {
            track(FDBNameGenerator.dataPath(index));
        }

        public void visit(Sequence sequence) {
            track(FDBNameGenerator.dataPath(sequence));
        }

        private void track(List<String> path) {
            pathNames.add(path.toString());
        }
    }

    private static final TransactionService.Callback CLEAR_SESSION_KEY_CALLBACK = new TransactionService.Callback() {
        @Override
        public void run(Session session, long timestamp) {
            session.remove(SESSION_AIS_KEY);
        }
    };
}
TOP

Related Classes of com.foundationdb.server.store.FDBSchemaManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.