All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eiichiro.acidhouse.appengine.AppEngineResourceManager Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2011 Eiichiro Uchiumi. All Rights Reserved.
 * 
 * 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.eiichiro.acidhouse.appengine;

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;

import org.eiichiro.acidhouse.Entities;
import org.eiichiro.acidhouse.EntityExistsException;
import org.eiichiro.acidhouse.IndoubtException;
import org.eiichiro.acidhouse.Lock;
import org.eiichiro.acidhouse.Log;
import org.eiichiro.acidhouse.Log.State;
import org.eiichiro.acidhouse.ResourceManager;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Transaction;

/**
 * {@code AppEngineResourceManager} is a App Engine Low-level Datastore API 
 * based {@code ResourceManager} implementation.
 * 
 * @author Eiichiro Uchiumi
 */
public class AppEngineResourceManager implements ResourceManager {

	private final Logger logger = Logger.getLogger(getClass().getName());
	
	private final AppEngineDatastoreService datastore;
	
	private final Transaction local;
	
	private final AppEngineGlobalTransaction global;
	
	private Object entity;
	
	/**
	 * Constructs a new {@code AppEngineResourceManager} with the specified 
	 * {@code AppEngineDatastoreService}.
	 * 
	 * @param datastoreService {@code AppEngineDatastoreService}.
	 */
	public AppEngineResourceManager(AppEngineDatastoreService datastoreService) {
		this(datastoreService, null, null);
	}
	
	/**
	 * Constructs a new {@code AppEngineResourceManager} with the specified 
	 * {@code AppEngineDatastoreService}, Google App Engine Datastore local 
	 * {@code Transaction} and Acid House {@code AppEngineGlobalTransaction}.
	 * 
	 * @param datastore {@code AppEngineDatastoreService}.
	 * @param local Google App Engine Datastore local {@code Transaction}.
	 * @param global Acid House {@code AppEngineGlobalTransaction}.
	 */
	public AppEngineResourceManager(AppEngineDatastoreService datastore,
			Transaction local, AppEngineGlobalTransaction global) {
		this.datastore = datastore;
		this.local = local;
		this.global = global;
	}
	
	/**
	 * Gets the entity instance of the specified {@code Class} corresponding to 
	 * the specified key.
	 * This method implements "Consistent read". This method gets the entity as 
	 * the following procedure.
	 * 
    *
  • * Determines if the entity is locked ({@code Lock} entity is found). If the * entity has been locked, goes to the next step. If the entity has not been * locked, jumps to the last step. *
  • *
  • * Attempts to get {@code AppEngineGlobalTransaction} from {@code Lock}. If the * {@code AppEngineGlobalTransaction} is found, goes to the next step. If the * {@code AppEngineGlobalTransaction} is not found and transaction timeout has * been elapsed, this method determines the transaction had been failed * (rolled back), then deletes {@code AppEngineGlobalTransaction} from Google App * Engine Datastore and goes to the last step. If the {@code AppEngineGlobalTransaction} * is not found and transaction timeout has not been elapsed, this method * determines the entity is being modified under a transaction and throws * {@code ConcurrentModificationException}. *
  • *
  • * Gets the list of {@code Log} from {@code AppEngineGlobalTransaction} and * applies every operation to entities, then unlocks them. *
  • *
  • * Gets the entity from Google App Engine Datastore and checks the entity * is locked again. if the entity has been locked, this method throws * {@code ConcurrentModificationException}. Otherwise, this method returns * the entity. *
  • *
* * @param The type of entity. * @param clazz The {@code Class} of entity. * @param key The key corresponding to the entity you attempt to get. * @return The entity instance of the specified {@code Class} corresponding * to the specified key. * @throws ConcurrentModificationException If the entity has been modified * by other transaction. * @throws IndoubtException If the data consistency broken is detected when * the entity is get. */ @Override public E get(Class clazz, Object key) throws ConcurrentModificationException, IndoubtException { Key k = Keys.create(Translation.toKind(clazz), key); unlock(k); List entities = null; if ((local == null)) { entities = datastore.query(new Query(k)); } else { entities = datastore.query(local, new Query(k)); } for (Entity entity : entities) { if (entity.getKind().equals(Translation.LOCK_KIND)) { throw new ConcurrentModificationException( "Entity corresponding to [" + k + "] is processed under a transaction"); } } E object = Translation.toObject(clazz, entities, new HashMap(), datastore); entity = object; operation = Log.Operation.GET; return object; } private void unlock(Key key) { List entities = datastore.query(new Query(Translation.LOCK_KIND, key)); // If the entity corresponding to the specified key hasn't been locked, // this method ignores it. if (entities.size() == 0) { return; } Entity lockEntity = entities.get(0); Lock lock = Translation.toLock(lockEntity); // If transaction entities are not found, this method determines that the // entity corresponding to the specified key is processed under a // transaction when the current time within the deadline of datastore // operation. When the current time has exceeded the deadline of // datastore operation, this method determines that the another // transaction has failed and so just unlocks the entity. Key transactionKey = KeyFactory.stringToKey(lock.transaction()); List transactionEntities = new ArrayList(); if (datastore.get(transactionKey) == null) { if (System.currentTimeMillis() - lock.timestamp().getTime() <= datastore.deadline()) { throw new ConcurrentModificationException( "Entity corresponding to [" + key + "] is processed under a transaction"); } else { Transaction transaction = datastore.beginTransaction(); if (datastore.get(transaction, lockEntity.getKey()) == null) { logger.info("Entity locked by [" + lock.id() + "] has been rollbacked by another transaction"); transaction.rollback(); return; } datastore.delete(transaction, lockEntity.getKey()); transaction.commit(); return; } } else { transactionEntities.addAll(datastore.query(new Query(transactionKey))); } AppEngineGlobalTransaction transaction = Translation.toTransaction(transactionEntities, datastore); Key ancestor = Keys.ancestor(transactionKey); Log owner = null; Key ownerLockKey = null; int committed = 0; for (Log log : transaction.logs()) { Key entityKey = Keys.create( Translation.toKind(log.entity().getClass()), Entities.keyValue(log.entity())); Key lockKey = Keys.create(entityKey, Translation.LOCK_KIND, lock.id()); // The owner entity of transaction entities must be deleted after // every operation has been applied. Because this method cannot // apply any operation to entity without transaction entities. if (entityKey.equals(ancestor)) { owner = log; ownerLockKey = lockKey; continue; } List deletes = new ArrayList(); deletes.add(lockKey); // Applies operation to entity and unlocks it. try { apply(log.operation(), log.entity(), lockKey, deletes); log.state(State.COMMITTED); committed++; } catch (Exception e) { throwIndoubtException(e, transaction, committed); } } // At last, this method applies operation to the owner entity, unlocks // it and deletes transaction entities. if (owner != null) { List deletes = new ArrayList(); deletes.add(ownerLockKey); for (Entity delete : transactionEntities) { deletes.add(delete.getKey()); } try { apply(owner.operation(), owner.entity(), ownerLockKey, deletes); owner.state(State.COMMITTED); committed++; } catch (Exception e) { throwIndoubtException(e, transaction, committed); } } // Done! } private void apply(Log.Operation operation, Object entity, Key lock, List deletes) { Transaction transaction = datastore.beginTransaction(); if (datastore.get(transaction, lock) == null) { logger.info("Entity locked by [" + lock + "] has been applied by another transaction"); transaction.rollback(); return; } if (operation != Log.Operation.DELETE) { datastore.put(transaction, Translation.toEntities(entity)); } else { for (Entity e : Translation.toEntities(entity)) { deletes.add(e.getKey()); } } datastore.delete(transaction, deletes); transaction.commit(); } private void throwIndoubtException(Exception exception, AppEngineGlobalTransaction transaction, int committed) { logger.severe("Transaction [" + transaction.id() + "]: Failed to apply transaction log; " + "DATA CONSISTENCY BROKEN! PLEASE RECOVERY IMMEDIATELY " + "WITH THE FOLLOWING TRANSACTION DUMP :-<"); throw new IndoubtException(exception, transaction.id(), transaction.logs()); } /** * Puts the specified entity instance into the Google App Engine Datastore * newly. * * @param entity An entity instance. */ @Override public void put(Object entity) { if (entity == null) { throw new IllegalArgumentException("'entity' must not be [" + entity + "]"); } if (operation != null) { throw new IllegalStateException("Log [" + operation + "] -> [" + Log.Operation.PUT + "] is not allowed: This operation must be first"); } Key id = Keys.create(Translation.toKind(entity.getClass()), Entities.keyValue(entity)); Entity e = datastore.get(local, id); if (e != null) { throw new EntityExistsException(id); } this.entity = entity; operation = Log.Operation.PUT; } /** * Applies the specified entity's update to the Google App Engine Datastore. * * @param entity An entity instance to be updated to. */ @Override public void update(Object entity) { if (entity == null) { throw new IllegalArgumentException("'entity' must not be [" + entity + "]"); } if (operation != Log.Operation.GET) { throw new IllegalStateException("Log [" + operation + "] -> [" + Log.Operation.UPDATE + "] is not allowed: previous operation must be [" + Log.Operation.GET + "]"); } operation = Log.Operation.UPDATE; } /** * Deletes the specified entity from the Google App Engine Datastore. * * @param entity An entity instance to be deleted. */ @Override public void delete(Object entity) { if (entity == null) { throw new IllegalArgumentException("'entity' must not be [" + entity + "]"); } if (operation != Log.Operation.GET) { throw new IllegalStateException("Log [" + operation + "] -> [" + Log.Operation.DELETE + "] is not allowed: previous operation must be [" + Log.Operation.GET + "]"); } operation = Log.Operation.DELETE; } /** Allocates lock for the managing entity. */ @Override public void prepare() { List logs = global.logs(); Object parent = null; for (int i = logs.size() - 1; i >=0; i--) { if (logs.get(i).operation() != Log.Operation.GET) { parent = logs.get(i).entity(); break; } } Lock lock = new Lock(global.id(), KeyFactory.createKeyString( Keys.create(Translation.toKind(parent.getClass()), Entities.keyValue(parent)), Translation.TRANSACTION_KIND, global.id()), new Date()); datastore.put(local, Translation.toEntity(lock, Keys.create( Translation.toKind(entity.getClass()), Entities.keyValue(entity)))); local.commit(); this.lock = lock; } /** * Applies the transactional operation (WAL: Write Ahead Log) to the * managing entity and unlocks it. */ @Override public void commit() { List deletes = new ArrayList(); deletes.add(Translation.toEntity(lock, Keys.create( Translation.toKind(entity.getClass()), Entities.keyValue(entity))).getKey()); Key transactionKey = KeyFactory.stringToKey(lock.transaction()); if (Keys.ancestor(transactionKey).equals( Keys.create(Translation.toKind(entity.getClass()), Entities.keyValue(entity)))) { deletes.add(transactionKey); for (Entity entity : datastore.query( new Query(transactionKey).setKeysOnly())) { deletes.add(entity.getKey()); } } Key parent = Keys.create(Translation.toKind(entity.getClass()), Entities.keyValue(entity)); apply(operation, entity, Translation.toEntity(lock, parent).getKey(), deletes); } private Log.Operation operation; private Lock lock; /** * Returns the entity this {@code AppEngineResourceManager} manages. * * @return The entity this {@code AppEngineResourceManager} manages. */ @Override public Object entity() { return entity; } /** * Returns the {@code Transaction} this {@code AppEngineResourceManager} * manages. * * @return The {@code Transaction} this {@code AppEngineResourceManager} * manages. */ @Override public Transaction transaction() { return local; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy