de.fhg.igd.mongomvcc.impl.MongoDBVBranch.java Source code

Java tutorial

Introduction

Here is the source code for de.fhg.igd.mongomvcc.impl.MongoDBVBranch.java

Source

// This file is part of MongoMVCC.
//
// Copyright (c) 2012 Fraunhofer IGD
//
// MongoMVCC is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// MongoMVCC 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with MongoMVCC. If not, see <http://www.gnu.org/licenses/>.

package de.fhg.igd.mongomvcc.impl;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.gridfs.GridFS;

import de.fhg.igd.mongomvcc.VBranch;
import de.fhg.igd.mongomvcc.VCollection;
import de.fhg.igd.mongomvcc.VException;
import de.fhg.igd.mongomvcc.VLargeCollection;
import de.fhg.igd.mongomvcc.helper.IdMap;
import de.fhg.igd.mongomvcc.helper.IdMapIterator;
import de.fhg.igd.mongomvcc.helper.IdSet;
import de.fhg.igd.mongomvcc.helper.IdSetIterator;
import de.fhg.igd.mongomvcc.impl.internal.Commit;
import de.fhg.igd.mongomvcc.impl.internal.CompatibilityHelper;
import de.fhg.igd.mongomvcc.impl.internal.Index;
import de.fhg.igd.mongomvcc.impl.internal.MongoDBConstants;
import de.fhg.igd.mongomvcc.impl.internal.Tree;

/**
 * <p>Implementation of {@link VBranch} for MongoDB. Each
 * thread owns an index and a head of the branch/commit it has checked out.
 * This ensures isolation. If two threads try to create a commit with the
 * same head, the {@link #commit()} method will fail. The rule is: come first,
 * serve first.</p>
 * <p><strong>Thread-safety:</strong> this class is thread-safe. Instances
 * can be shared between threads, but each thread has its own index and head.</p>
 * @author Michel Kraemer
 */
public class MongoDBVBranch implements VBranch {
    /**
     * The head of this branch. A thread-local variable since objects of
     * this class can be shared between threads.
     */
    private final ThreadLocal<Commit> _head = new ThreadLocal<Commit>();

    /**
     * The index. Provides access to all objects from the currently
     * checked out branch/commit and stores information on dirty objects.
     */
    private final ThreadLocal<Index> _index = new ThreadLocal<Index>();

    /**
     * Holds the query object for the current head. A query object is used to
     * limit the number of objects transferred from the database.
     */
    private final ThreadLocal<Map<String, Object>> _currentQueryObject = new ThreadLocal<Map<String, Object>>();

    /**
     * The branch's name (may be null)
     */
    private final String _name;

    /**
     * The CID of the branch's root
     */
    private final long _rootCid;

    /**
     * The tree of commits
     */
    private final Tree _tree;

    /**
     * The database object
     */
    private final MongoDBVDatabase _db;

    /**
     * Constructs a new branch object (not the branch itself)
     * @param name the branch's name (may be null for unnamed branches)
     * @param rootCid the CID of the branch's root
     * @param tree the tree of commits
     * @param db the database object
     */
    public MongoDBVBranch(String name, long rootCid, Tree tree, MongoDBVDatabase db) {
        _name = name;
        _rootCid = rootCid;
        _tree = tree;
        _db = db;
    }

    /**
     * @return the thread-local head of this branch
     */
    private Commit getHeadCommit() {
        Commit r = _head.get();
        if (r == null) {
            if (_name != null) {
                r = _tree.resolveBranch(_name);
            } else {
                r = _tree.resolveCommit(_rootCid);
            }
            _head.set(r);
        }
        return r;
    }

    private Map<String, Object> makeQueryObject() {
        BasicDBList l = new BasicDBList();
        String lba = MongoDBConstants.LIFETIME + "." + getRootCid();
        String iba = MongoDBConstants.LIFETIME + ".i" + getRootCid();

        //(1) check if there is information about the document's insertion
        //    available. if not just include this object.
        //    TODO this condition must be removed once we know the whole branching history
        l.add(new BasicDBObject(iba, new BasicDBObject("$exists", false)));

        if (!CompatibilityHelper.supportsAnd(getDB())) {
            //(2) check if the object has been deleted after this commit.
            //    we use a $not here, so the query will also return 'true' if
            //    the attribute is not set.
            l.add(new BasicDBObject(lba, new BasicDBObject("$not", new BasicDBObject("$lte", getHead()))));
        } else {
            BasicDBList l2 = new BasicDBList();
            //(2) check if the object has been inserted in this commit or later
            l2.add(new BasicDBObject(iba, new BasicDBObject("$lte", getHead())));

            //(3) check if the object has been deleted after this commit.
            //    we use a $not here, so the query will also return 'true' if
            //    the attribute is not set.
            l2.add(new BasicDBObject(lba, new BasicDBObject("$not", new BasicDBObject("$lte", getHead()))));

            l.add(new BasicDBObject("$and", l2));
        }

        return Collections.unmodifiableMap(new BasicDBObject("$or", l));
    }

    /**
     * @return a query object which limits the number of objects returned
     * by accessing their lifetime information
     */
    public Map<String, Object> getQueryObject() {
        Map<String, Object> r = _currentQueryObject.get();
        if (r == null) {
            r = makeQueryObject();
            _currentQueryObject.set(r);
        }
        return r;
    }

    /**
     * Updates the thread-local head of the currently checked out branch/commit
     * @param newHead the new head
     */
    private void updateHead(Commit newHead) {
        _currentQueryObject.remove();
        _head.set(newHead);
    }

    @Override
    public long getHead() {
        return getHeadCommit().getCID();
    }

    @Override
    public VCollection getCollection(String name) {
        return new MongoDBVCollection(_db.getDB().getCollection(name), this, _db.getCounter());
    }

    @Override
    public VLargeCollection getLargeCollection(String name) {
        DB db = _db.getDB();
        return new MongoDBVLargeCollection(db.getCollection(name), new GridFS(db, name), this, _db.getCounter());
    }

    /**
     * Gets or creates a database collection that can handle large
     * objects (BLOBs) and uses a given access strategy.
     * @param name the collection's name
     * @param accessStrategy the strategy used to access large objects
     * @return the collection (never null)
     */
    public VLargeCollection getLargeCollection(String name, AccessStrategy accessStrategy) {
        DB db = _db.getDB();
        return new MongoDBVLargeCollection(db.getCollection(name), new GridFS(db, name), this, _db.getCounter(),
                accessStrategy);
    }

    /**
     * @return the index. The index provides access to all objects from
     * the currently checked out branch/commit and stores information on
     * dirty objects.
     */
    public Index getIndex() {
        Index r = _index.get();
        if (r == null) {
            r = new Index(getHeadCommit(), _tree);
            _index.set(r);
        }
        return r;
    }

    /**
     * @return the database from which this branch has been checked out
     */
    public MongoDBVDatabase getDB() {
        return _db;
    }

    /**
     * @return the CID of this branch's root
     */
    public long getRootCid() {
        return _rootCid;
    }

    @Override
    public long commit() {
        Index idx = getIndex();
        //clone dirty objects because we clear them below
        Map<String, IdMap> dos = new HashMap<String, IdMap>(idx.getDirtyObjects());
        Commit head = getHeadCommit();
        Commit c = new Commit(_db.getCounter().getNextId(), head.getCID(), _rootCid, dos);
        _tree.addCommit(c);
        updateHead(c);

        //mark deleted objects as deleted in the database
        DB db = _db.getDB();
        String lifetimeAttr = MongoDBConstants.LIFETIME + "." + getRootCid();
        for (Map.Entry<String, IdSet> e : idx.getDeletedOids().entrySet()) {
            DBCollection dbc = db.getCollection(e.getKey());
            IdSetIterator li = e.getValue().iterator();
            while (li.hasNext()) {
                long oid = li.next();
                //save the CID of the commit where the object has been deleted
                dbc.update(new BasicDBObject(MongoDBConstants.ID, oid),
                        new BasicDBObject("$set", new BasicDBObject(lifetimeAttr, head.getCID())));
            }
        }

        //mark dirty objects as inserted
        String instimeAttr = MongoDBConstants.LIFETIME + ".i" + getRootCid();
        for (Map.Entry<String, IdMap> e : dos.entrySet()) {
            DBCollection dbc = db.getCollection(e.getKey());
            IdMap m = e.getValue();
            IdMapIterator li = m.iterator();
            while (li.hasNext()) {
                li.advance();
                long oid = li.value();
                if (oid == -1) {
                    //the document has been inserted and then deleted again
                    //do not save time of insertion
                    continue;
                }
                //save the CID of the commit where the object has been inserted
                dbc.update(new BasicDBObject(MongoDBConstants.ID, oid),
                        new BasicDBObject("$set", new BasicDBObject(instimeAttr, head.getCID())));
            }
        }

        //reset index
        idx.clearDirtyObjects();

        //if we fail below, the commit has already been performed and the
        //index is clear. failing below simply means the named branch's
        //head could not be updated. If the caller wants to keep the commit
        //he/she just has to create a new named branch based on this
        //branch's head.

        //update named branch's head
        if (_name != null) {
            //synchronize the following part, because we first resolve the branch
            //and then update it
            synchronized (this) {
                //check for conflicts (i.e. if another thread has already updated the branch's head)
                if (_tree.resolveBranch(_name).getCID() != c.getParentCID()) {
                    throw new VException("Branch " + _name + " has already been " + "updated by another commit");
                }
                _tree.updateBranchHead(_name, c.getCID());
            }
        }

        return c.getCID();
    }

    @Override
    public void rollback() {
        //simply reset the whole index
        _index.remove();
    }
}