package net.notdot.bdbdatastore.server; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.notdot.protorpc.Disposable; import net.notdot.protorpc.RpcFailedError; import com.google.appengine.base.ApiBase; import com.google.appengine.base.ApiBase.Integer64Proto; import com.google.appengine.base.ApiBase.StringProto; import com.google.appengine.base.ApiBase.VoidProto; import com.google.appengine.datastore_v3.DatastoreV3; import com.google.appengine.datastore_v3.DatastoreV3.CompositeIndices; import com.google.appengine.datastore_v3.DatastoreV3.DeleteRequest; import com.google.appengine.datastore_v3.DatastoreV3.GetRequest; import com.google.appengine.datastore_v3.DatastoreV3.GetResponse; import com.google.appengine.datastore_v3.DatastoreV3.NextRequest; import com.google.appengine.datastore_v3.DatastoreV3.PutRequest; import com.google.appengine.datastore_v3.DatastoreV3.PutResponse; import com.google.appengine.datastore_v3.DatastoreV3.Query; import com.google.appengine.datastore_v3.DatastoreV3.QueryExplanation; import com.google.appengine.datastore_v3.DatastoreV3.QueryResult; import com.google.appengine.datastore_v3.DatastoreV3.Schema; import com.google.appengine.datastore_v3.DatastoreV3.GetResponse.Entity; import com.google.appengine.entity.Entity.CompositeIndex; import com.google.appengine.entity.Entity.EntityProto; import com.google.appengine.entity.Entity.Reference; import com.google.protobuf.RpcCallback; import com.google.protobuf.RpcController; import com.google.protobuf.Service; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.DeadlockException; import com.sleepycat.je.Transaction; public class DatastoreService extends com.google.appengine.datastore_v3.DatastoreV3.DatastoreService implements Service, Disposable { final Logger logger = LoggerFactory.getLogger(AppDatastore.class); protected Datastore datastore; protected long next_tx_id = 0; protected Map<DatastoreV3.Transaction,Transaction> transactions = new HashMap<DatastoreV3.Transaction,Transaction>(); protected long next_cursor_id = 0; protected Map<DatastoreV3.Cursor,AbstractDatastoreResultSet> cursors = new HashMap<DatastoreV3.Cursor,AbstractDatastoreResultSet>(); public DatastoreService(Datastore ds) { this.datastore = ds; } @Override protected void finalize() throws Throwable { this.close(); super.finalize(); } public void close() { try { // Clean up any outstanding transactions for(Transaction tx : this.transactions.values()) if(tx != null) tx.abort(); this.transactions.clear(); } catch(DatabaseException ex) { this.logger.error("Exception encountered while disposing DatastoreService", ex); } } protected Transaction getTransaction(DatastoreV3.Transaction handle, AppDatastore ds) { return getTransaction(handle, ds, false); } protected Transaction getTransaction(DatastoreV3.Transaction handle, AppDatastore ds, boolean createImplicitTx) { if(handle == null || !handle.hasHandle()) { // No handle - not in a transaction if(createImplicitTx) { try { return ds.newTransaction(); } catch(DatabaseException ex) { logger.error("Unable to create implicit transaction", ex); } } return null; } Transaction ret = transactions.get(handle); if(ret == null) { synchronized(transactions) { ret = transactions.get(handle); if(ret == null) { if(!transactions.containsKey(handle)) throw new RpcFailedError("Invalid transaction handle", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); if(ds == null) return null; try { ret = ds.newTransaction(); } catch(DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } transactions.put(handle, ret); } } } return ret; } protected void commitImplicitTransaction(DatastoreV3.Transaction handle, Transaction tx) { if((handle == null || !handle.hasHandle()) && tx != null) { try { tx.commit(); } catch(DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } } protected void rollbackImplicitTransaction(DatastoreV3.Transaction handle, Transaction tx) { if((handle == null || !handle.hasHandle()) && tx != null) { try { tx.abort(); } catch(DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } } @Override public void beginTransaction(RpcController controller, VoidProto request, RpcCallback<DatastoreV3.Transaction> done) { DatastoreV3.Transaction tx; synchronized(transactions) { tx = DatastoreV3.Transaction.newBuilder().setHandle(next_tx_id++).build(); //The actual transaction object is created on first use transactions.put(tx, null); } done.run(tx); } @Override public void commit(RpcController c, DatastoreV3.Transaction request, RpcCallback<VoidProto> done) { try { Transaction tx = this.getTransaction(request, null); if(tx != null) tx.commit(); done.run(VoidProto.getDefaultInstance()); this.transactions.remove(request); } catch(DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void count(RpcController controller, Query request, RpcCallback<Integer64Proto> done) { String app_id = request.getApp(); AppDatastore ds = this.datastore.getAppDatastore(app_id); try { AbstractDatastoreResultSet cursor = ds.executeQuery(request); int i = 0; cursor.openCursor(); try { while(cursor.read()) i++; } finally { cursor.closeCursor(); } done.run(ApiBase.Integer64Proto.newBuilder().setValue(i).build()); } catch (DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void createIndex(RpcController controller, CompositeIndex request, RpcCallback<Integer64Proto> done) { String app_id = request.getAppId(); AppDatastore ds = this.datastore.getAppDatastore(app_id); try { ds.addIndex(request, done); ds.saveCompositeIndexes(); } catch (Exception ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void delete(RpcController controller, DeleteRequest request, RpcCallback<VoidProto> done) { if(request.getKeyCount() == 0) { done.run(VoidProto.getDefaultInstance()); return; } String app_id = request.getKey(0).getApp(); AppDatastore ds = this.datastore.getAppDatastore(app_id); Transaction tx = null; try { tx = this.getTransaction(request.getTransaction(), ds, true); for(Reference ref : request.getKeyList()) { if(!ref.getApp().equals(app_id)) throw new RpcFailedError("All entities must have the same app_id", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); ds.delete(ref, tx); } this.commitImplicitTransaction(request.getTransaction(), tx); done.run(VoidProto.getDefaultInstance()); } catch(DeadlockException ex) { this.rollbackImplicitTransaction(request.getTransaction(), tx); throw new RpcFailedError("Operation was terminated to resolve a deadlock.", DatastoreV3.Error.ErrorCode.CONCURRENT_TRANSACTION.getNumber()); } catch(DatabaseException ex) { this.rollbackImplicitTransaction(request.getTransaction(), tx); throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void deleteCursor(RpcController controller, DatastoreV3.Cursor request, RpcCallback<VoidProto> done) { AbstractDatastoreResultSet cursor; synchronized(this.cursors) { cursor = this.cursors.get(request); if(cursor == null) throw new RpcFailedError("Invalid cursor", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); this.cursors.remove(request); } done.run(ApiBase.VoidProto.getDefaultInstance()); } @Override public void deleteIndex(RpcController controller, CompositeIndex request, RpcCallback<VoidProto> done) { String app_id = request.getAppId(); AppDatastore ds = this.datastore.getAppDatastore(app_id); try { if(!ds.deleteIndex(request)) throw new RpcFailedError(String.format("Could not delete index %d: Not found or still building.", request.getId()), DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); ds.saveCompositeIndexes(); done.run(ApiBase.VoidProto.getDefaultInstance()); } catch (Exception ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void explain(RpcController controller, Query request, RpcCallback<QueryExplanation> done) { throw new RpcFailedError("Operation not supported.", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); } @Override public void get(RpcController c, GetRequest request, RpcCallback<GetResponse> done) { GetResponse.Builder response = GetResponse.newBuilder(); if(request.getKeyCount() == 0) { done.run(response.build()); return; } String app_id = request.getKey(0).getApp(); AppDatastore ds = this.datastore.getAppDatastore(app_id); try { Transaction tx = this.getTransaction(request.getTransaction(), ds); for(Reference ref : request.getKeyList()) { if(!ref.getApp().equals(app_id)) { throw new RpcFailedError("All entities must have the same app_id", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); } Entity.Builder ent = Entity.newBuilder(); EntityProto entity = ds.get(ref, tx); if(entity != null) { ent.setEntity(entity); } response.addEntity(ent); } done.run(response.build()); } catch(DeadlockException ex) { throw new RpcFailedError("Operation was terminated to resolve a deadlock.", DatastoreV3.Error.ErrorCode.CONCURRENT_TRANSACTION.getNumber()); } catch(DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void getIndices(RpcController controller, StringProto request, RpcCallback<CompositeIndices> done) { AppDatastore ds = this.datastore.getAppDatastore(request.getValue()); DatastoreV3.CompositeIndices response = ds.getIndices(); done.run(response); } @Override public void getSchema(RpcController controller, StringProto request, RpcCallback<Schema> done) { AppDatastore ds = this.datastore.getAppDatastore(request.getValue()); try { done.run(ds.getSchema()); } catch (DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void next(RpcController controller, NextRequest request, RpcCallback<QueryResult> done) { QueryResult.Builder response = QueryResult.newBuilder(); AbstractDatastoreResultSet cursor = this.cursors.get(request.getCursor()); if(cursor == null) throw new RpcFailedError("Invalid cursor", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); try { response.setCursor(request.getCursor()); cursor.openCursor(); try { response.addAllResult(cursor.getNext(request.getCount())); } finally { cursor.closeCursor(); } response.setMoreResults(cursor.hasMore()); done.run(response.build()); } catch(DatabaseException ex) { throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void put(RpcController c, PutRequest request, RpcCallback<PutResponse> done) { PutResponse.Builder response = PutResponse.newBuilder(); if(request.getEntityCount() == 0) { done.run(response.build()); return; } String app_id = request.getEntity(0).getKey().getApp(); AppDatastore ds = this.datastore.getAppDatastore(app_id); Transaction tx = null; try { tx = this.getTransaction(request.getTransaction(), ds, true); for(EntityProto ent : request.getEntityList()) { if(!ent.getKey().getApp().equals(app_id)) { throw new RpcFailedError("All entities must have the same app_id", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); } response.addKey(ds.put(ent, tx)); } this.commitImplicitTransaction(request.getTransaction(), tx); done.run(response.build()); } catch(DeadlockException ex) { this.rollbackImplicitTransaction(request.getTransaction(), tx); throw new RpcFailedError("Operation was terminated to resolve a deadlock.", DatastoreV3.Error.ErrorCode.CONCURRENT_TRANSACTION.getNumber()); } catch(DatabaseException ex) { this.rollbackImplicitTransaction(request.getTransaction(), tx); throw new RpcFailedError(ex, DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void rollback(RpcController c, DatastoreV3.Transaction request, RpcCallback<VoidProto> done) { try { Transaction tx = this.getTransaction(request, null); if(tx != null) tx.abort(); done.run(VoidProto.getDefaultInstance()); this.transactions.remove(request); } catch(DatabaseException ex) { throw new RpcFailedError(ex.toString(), DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void runQuery(RpcController controller, Query request, RpcCallback<QueryResult> done) { if(!request.hasKind()) throw new RpcFailedError("All queries must specify a kind.", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); QueryResult.Builder response = QueryResult.newBuilder(); String app_id = request.getApp(); AppDatastore ds = this.datastore.getAppDatastore(app_id); try { AbstractDatastoreResultSet results = ds.executeQuery(request); if(results == null) throw new RpcFailedError(String.format("No index found to satisfy query %s", request), DatastoreV3.Error.ErrorCode.NEED_INDEX.getNumber()); DatastoreV3.Cursor dscursor; synchronized(this.cursors) { dscursor = DatastoreV3.Cursor.newBuilder().setCursor(next_cursor_id++).build(); } this.cursors.put(dscursor, results); response.setCursor(dscursor); response.setMoreResults(true); done.run(response.build()); } catch(DatabaseException ex) { throw new RpcFailedError(ex.toString(), DatastoreV3.Error.ErrorCode.INTERNAL_ERROR.getNumber()); } } @Override public void updateIndex(RpcController controller, CompositeIndex request, RpcCallback<VoidProto> done) { throw new RpcFailedError("Operation not supported.", DatastoreV3.Error.ErrorCode.BAD_REQUEST.getNumber()); } }