From bcfaaffa3751f8c7883e41c162ba4030fd9bd21a Mon Sep 17 00:00:00 2001
From: Jesse Morgan <jesse@jesterpm.net>
Date: Fri, 25 Oct 2013 01:36:27 +0000
Subject: Fixing ReplicatedRepository so that transactions may be entered when
 the master is unavailable as long as no changes are made to replicated
 storables.

---
 .../repo/replicated/ReadOnlyTransaction.java       |  91 ++++++++++
 .../repo/replicated/ReplicatedRepository.java      |  31 +++-
 .../repo/replicated/ReplicationTrigger.java        | 190 +++++++++++++--------
 3 files changed, 234 insertions(+), 78 deletions(-)
 create mode 100644 src/main/java/com/amazon/carbonado/repo/replicated/ReadOnlyTransaction.java

(limited to 'src/main/java/com/amazon/carbonado/repo')

diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReadOnlyTransaction.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReadOnlyTransaction.java
new file mode 100644
index 0000000..1666bf4
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReadOnlyTransaction.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2006-2013 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates.  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 com.amazon.carbonado.repo.replicated;
+
+import java.util.concurrent.TimeUnit;
+
+import com.amazon.carbonado.IsolationLevel;
+import com.amazon.carbonado.PersistException;
+import com.amazon.carbonado.Transaction;
+
+/**
+ * ReadOnlyTransaction wraps an another transaction. It's only function is to
+ * serve as a marker for the ReplicatedStorage triggers that no write
+ * operations are allowed.
+ *
+ * @author Jesse Morgan
+ */
+class ReadOnlyTransaction implements Transaction {
+    private final Transaction mTxn;
+
+    public ReadOnlyTransaction(Transaction txn) {
+        mTxn = txn;
+    }
+
+    @Override
+    public void commit() throws PersistException {
+        mTxn.commit();
+    }
+
+    @Override
+    public void exit() throws PersistException {
+        mTxn.exit();
+    }
+
+    @Override
+    public void setForUpdate(boolean forUpdate) {
+        mTxn.setForUpdate(forUpdate);
+    }
+
+    @Override
+    public boolean isForUpdate() {
+        return mTxn.isForUpdate();
+    }
+
+    @Override
+    public void setDesiredLockTimeout(int timeout, TimeUnit unit) {
+        mTxn.setDesiredLockTimeout(timeout, unit);
+    }
+
+    @Override
+    public IsolationLevel getIsolationLevel() {
+        return mTxn.getIsolationLevel();
+    }
+
+    @Override
+    public void detach() {
+        mTxn.detach();
+    }
+
+    @Override
+    public void attach() {
+        mTxn.attach();
+    }
+
+    @Override
+    public boolean preCommit() throws PersistException {
+        return mTxn.preCommit();
+    }
+
+    @Override
+    public String toString() {
+        return "ReadOnlyTransaction wrapping { " + mTxn.toString() + " }";
+    }
+}
+
diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java
index 6cf0d6c..cfe021d 100644
--- a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java
+++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java
@@ -70,6 +70,7 @@ import com.amazon.carbonado.spi.StoragePool;
 
 import com.amazon.carbonado.txn.TransactionPair;
 
+import com.amazon.carbonado.util.BelatedCreationException;
 import com.amazon.carbonado.util.Throttle;
 
 /**
@@ -243,18 +244,36 @@ class ReplicatedRepository
     }
 
     public Transaction enterTransaction() {
-        return new TransactionPair(mMasterRepository.enterTransaction(),
-                                   mReplicaRepository.enterTransaction());
+        Transaction master;
+        try {
+            master = mMasterRepository.enterTransaction();
+        } catch (BelatedCreationException e) {
+            return new ReadOnlyTransaction(mReplicaRepository.enterTransaction());
+        }
+
+        return new TransactionPair(master, mReplicaRepository.enterTransaction());
     }
 
     public Transaction enterTransaction(IsolationLevel level) {
-        return new TransactionPair(mMasterRepository.enterTransaction(level),
-                                   mReplicaRepository.enterTransaction(level));
+        Transaction master;
+        try {
+            master = mMasterRepository.enterTransaction(level);
+        } catch (BelatedCreationException e) {
+            return new ReadOnlyTransaction(mReplicaRepository.enterTransaction(level));
+        }
+
+        return new TransactionPair(master, mReplicaRepository.enterTransaction(level));
     }
 
     public Transaction enterTopTransaction(IsolationLevel level) {
-        return new TransactionPair(mMasterRepository.enterTopTransaction(level),
-                                   mReplicaRepository.enterTopTransaction(level));
+        Transaction master;
+        try {
+            master = mMasterRepository.enterTopTransaction(level);
+        } catch (BelatedCreationException e) {
+            return new ReadOnlyTransaction(mReplicaRepository.enterTopTransaction(level));
+        }
+
+        return new TransactionPair(master, mReplicaRepository.enterTopTransaction(level));
     }
 
     public IsolationLevel getTransactionIsolationLevel() {
diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java
index 4b01679..1b88e3e 100644
--- a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java
+++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java
@@ -83,99 +83,133 @@ class ReplicationTrigger<S extends Storable> extends Trigger<S> {
 
     @Override
     public Object beforeInsert(S replica) throws PersistException {
-        return beforeInsert(replica, false);
+        return beforeInsert(null, replica, false);
     }
 
     @Override
     public Object beforeTryInsert(S replica) throws PersistException {
-        return beforeInsert(replica, true);
+        return beforeInsert(null, replica, true);
     }
 
-    private Object beforeInsert(S replica, boolean forTry) throws PersistException {
-        final S master = mMasterStorage.prepare();
-        replica.copyAllProperties(master);
+    @Override
+    public Object beforeInsert(Transaction txn, S replica) throws PersistException {
+        return beforeInsert(txn, replica, false);
+    }
 
-        try {
-            if (forTry) {
-                if (!master.tryInsert()) {
-                    throw abortTry();
+    @Override
+    public Object beforeTryInsert(Transaction txn, S replica) throws PersistException {
+        return beforeInsert(txn, replica, true);
+    }
+
+    private Object beforeInsert(Transaction txn, S replica, boolean forTry) throws PersistException {
+        if (txn instanceof ReadOnlyTransaction) {
+            // This operation was intended to take place in a transaction, but
+            // the master repository was unavailable when the transaction was
+            // entered.
+                throw new PersistException("Current transaction is read-only.");
+            }
+
+            final S master = mMasterStorage.prepare();
+            replica.copyAllProperties(master);
+
+            try {
+                if (forTry) {
+                    if (!master.tryInsert()) {
+                        throw abortTry();
+                    }
+                } else {
+                    master.insert();
                 }
-            } else {
-                master.insert();
+            } catch (UniqueConstraintException e) {
+                // This may be caused by an inconsistency between replica and
+                // master. Here's one scenerio: user called tryLoad and saw the
+                // entry does not exist. So instead of calling update, he/she calls
+                // insert. If the master entry exists, then there is an
+                // inconsistency. The code below checks for this specific kind of
+                // error and repairs it by inserting a record in the replica.
+
+                // Here's another scenerio: Unique constraint was caused by an
+                // inconsistency with the values of the alternate keys. User
+                // expected alternate keys to have unique values, as indicated by
+                // replica.
+
+                repair(replica);
+
+                // Throw exception since we don't know what the user's intentions
+                // really are.
+                throw e;
             }
-        } catch (UniqueConstraintException e) {
-            // This may be caused by an inconsistency between replica and
-            // master. Here's one scenerio: user called tryLoad and saw the
-            // entry does not exist. So instead of calling update, he/she calls
-            // insert. If the master entry exists, then there is an
-            // inconsistency. The code below checks for this specific kind of
-            // error and repairs it by inserting a record in the replica.
 
-            // Here's another scenerio: Unique constraint was caused by an
-            // inconsistency with the values of the alternate keys. User
-            // expected alternate keys to have unique values, as indicated by
-            // replica.
+            // Master may have applied sequences to unitialized primary keys, so
+            // copy primary keys to replica. Mark properties as dirty to allow
+            // primary key to be changed.
+            replica.markPropertiesDirty();
 
-            repair(replica);
+            // Copy all properties in order to trigger constraints that
+            // master should have resolved.
+            master.copyAllProperties(replica);
 
-            // Throw exception since we don't know what the user's intentions
-            // really are.
-            throw e;
+            return null;
         }
 
-        // Master may have applied sequences to unitialized primary keys, so
-        // copy primary keys to replica. Mark properties as dirty to allow
-        // primary key to be changed.
-        replica.markPropertiesDirty();
+        @Override
+        public Object beforeUpdate(S replica) throws PersistException {
+            return beforeUpdate(null, replica, false);
+        }
 
-        // Copy all properties in order to trigger constraints that
-        // master should have resolved.
-        master.copyAllProperties(replica);
+        @Override
+        public Object beforeTryUpdate(S replica) throws PersistException {
+            return beforeUpdate(null, replica, true);
+        }
 
-        return null;
-    }
+        @Override
+        public Object beforeUpdate(Transaction txn, S replica) throws PersistException {
+            return beforeUpdate(txn, replica, false);
+        }
 
-    @Override
-    public Object beforeUpdate(S replica) throws PersistException {
-        return beforeUpdate(replica, false);
-    }
+        @Override
+        public Object beforeTryUpdate(Transaction txn, S replica) throws PersistException {
+            return beforeUpdate(txn, replica, true);
+        }
 
-    @Override
-    public Object beforeTryUpdate(S replica) throws PersistException {
-        return beforeUpdate(replica, true);
-    }
+        private Object beforeUpdate(Transaction txn, S replica, boolean forTry) throws PersistException {
+            if (txn instanceof ReadOnlyTransaction) {
+                // This operation was intended to take place in a transaction, but
+                // the master repository was unavailable when the transaction was
+                // entered.
+                throw new PersistException("Current transaction is read-only.");
+            }
 
-    private Object beforeUpdate(S replica, boolean forTry) throws PersistException {
-        final S master = mMasterStorage.prepare();
-        replica.copyPrimaryKeyProperties(master);
-        replica.copyVersionProperty(master);
-        replica.copyDirtyProperties(master);
+            final S master = mMasterStorage.prepare();
+            replica.copyPrimaryKeyProperties(master);
+            replica.copyVersionProperty(master);
+            replica.copyDirtyProperties(master);
 
-        try {
-            if (forTry) {
-                if (!master.tryUpdate()) {
-                    // Master record does not exist. To ensure consistency,
-                    // delete record from replica.
-                    if (tryDeleteReplica(replica)) {
-                        // Replica was inconsistent, but caller might be in a
-                        // transaction and rollback the repair. Run repair
-                        // again in separate thread to ensure it sticks.
-                        repair(replica);
-                    }
-                    throw abortTry();
-                }
-            } else {
-                try {
-                    master.update();
-                } catch (PersistNoneException e) {
-                    // Master record does not exist. To ensure consistency,
-                    // delete record from replica.
-                    if (tryDeleteReplica(replica)) {
-                        // Replica was inconsistent, but caller might be in a
-                        // transaction and rollback the repair. Run repair
-                        // again in separate thread to ensure it sticks.
-                        repair(replica);
+            try {
+                if (forTry) {
+                    if (!master.tryUpdate()) {
+                        // Master record does not exist. To ensure consistency,
+                        // delete record from replica.
+                        if (tryDeleteReplica(replica)) {
+                            // Replica was inconsistent, but caller might be in a
+                            // transaction and rollback the repair. Run repair
+                            // again in separate thread to ensure it sticks.
+                            repair(replica);
+                        }
+                        throw abortTry();
                     }
+                } else {
+                    try {
+                        master.update();
+                    } catch (PersistNoneException e) {
+                        // Master record does not exist. To ensure consistency,
+                        // delete record from replica.
+                        if (tryDeleteReplica(replica)) {
+                            // Replica was inconsistent, but caller might be in a
+                            // transaction and rollback the repair. Run repair
+                            // again in separate thread to ensure it sticks.
+                            repair(replica);
+                        }
                     throw e;
                 }
             }
@@ -199,6 +233,18 @@ class ReplicationTrigger<S extends Storable> extends Trigger<S> {
 
     @Override
     public Object beforeDelete(S replica) throws PersistException {
+        return beforeDelete(null, replica);
+    }
+
+    @Override
+    public Object beforeDelete(Transaction txn, S replica) throws PersistException {
+        if (txn instanceof ReadOnlyTransaction) {
+            // This operation was intended to take place in a transaction, but
+            // the master repository was unavailable when the transaction was
+            // entered.
+            throw new PersistException("Current transaction is read-only.");
+        }
+
         S master = mMasterStorage.prepare();
         replica.copyPrimaryKeyProperties(master);
 
-- 
cgit v1.2.3