/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.jena.sparql.transaction ;
import static org.junit.Assert.assertEquals ;
import static org.junit.Assert.fail ;
import java.util.concurrent.* ;
import java.util.concurrent.atomic.AtomicInteger ;
import org.apache.jena.atlas.iterator.Iter ;
import org.apache.jena.atlas.lib.Lib ;
import org.apache.jena.query.ReadWrite ;
import org.apache.jena.shared.JenaException ;
import org.apache.jena.sparql.core.DatasetGraph ;
import org.apache.jena.sparql.core.Quad ;
import org.apache.jena.sparql.sse.SSE ;
import org.apache.jena.system.ThreadAction ;
import org.apache.jena.system.ThreadTxn ;
import org.apache.jena.system.Txn ;
import org.apache.log4j.Level ;
import org.apache.log4j.Logger ;
import org.junit.After ;
import org.junit.Before ;
import org.junit.Test ;
/** Tests for transactions that start read and then promote to write */
public abstract class AbstractTestTransPromote {
// Currently,
// this feature is off and needs enabling via setPromotion.
// promotion is implicit when a write happens.
// See beforeClass / afterClass.
// Loggers.
private final Logger[] loggers ;
private Level[] levels ;
private boolean stdPromotion ;
private boolean stdReadCommitted ;
@Before
public void beforeLoggersNoWarnings() {
int N = loggers.length ;
levels = new Level[N] ;
for ( int i = 0 ; i < N ; i++ ) {
levels[i] = loggers[i].getLevel() ;
loggers[i].setLevel(Level.ERROR) ;
}
}
@After
public void afterResetLoggers() {
int N = loggers.length ;
for ( int i = 0 ; i < N ; i++ ) {
loggers[i].setLevel(levels[i]) ;
}
}
protected abstract void setPromotion(boolean b) ;
protected abstract boolean getPromotion() ;
protected abstract void setReadCommitted(boolean b) ;
protected abstract boolean getReadCommitted() ;
// The exact class used by exceptions of the system under test.
// TDB transctions are in the TDBException hierarchy
// so can't be JenaTransactionException.
protected abstract Class<? extends Exception> getTransactionExceptionClass() ;
@Before
public void before() {
stdPromotion = getPromotion() ;
stdReadCommitted = getReadCommitted() ;
setPromotion(true);
setReadCommitted(true);
}
@After
public void after() {
setPromotion(stdPromotion);
setReadCommitted(stdReadCommitted);
}
protected AbstractTestTransPromote(Logger[] loggers) {
this.loggers = loggers ;
}
private static Quad q1 = SSE.parseQuad("(_ :s :p1 1)") ;
private static Quad q2 = SSE.parseQuad("(_ :s :p2 2)") ;
private static Quad q3 = SSE.parseQuad("(_ :s :p3 3)") ;
protected abstract DatasetGraph create() ;
protected static void assertCount(long expected, DatasetGraph dsg) {
dsg.begin(ReadWrite.READ) ;
long x = Iter.count(dsg.find()) ;
dsg.end() ;
assertEquals(expected, x) ;
}
// "strict" = don't see intermedioate changes.
// "readCommitted" = do see
// Subclass / parameterized
@Test public void promote_snapshot_01() { run_01(false) ; }
@Test public void promote_readCommitted_01() { run_01(true) ; }
// READ-add
private void run_01(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
dsg.commit() ;
dsg.end() ;
}
@Test public void promote_snapshot_02() { run_02(false) ; }
@Test public void promote_readCommitted_02() { run_02(true) ; }
// Previous transaction then READ-add
private void run_02(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
dsg.begin(ReadWrite.READ) ;dsg.end() ;
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
dsg.commit() ;
dsg.end() ;
}
@Test public void promote_snapshot_03() { run_03(false) ; }
@Test public void promote_readCommitted_03() { run_03(true) ; }
private void run_03(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
dsg.begin(ReadWrite.WRITE) ;dsg.commit() ; dsg.end() ;
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
dsg.commit() ;
dsg.end() ;
}
@Test public void promote_snapshot_04() { run_04(false) ; }
@Test public void promote_readCommitted_04() { run_04(true) ; }
private void run_04(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
dsg.begin(ReadWrite.WRITE) ;dsg.abort() ; dsg.end() ;
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
dsg.commit() ;
dsg.end() ;
}
@Test public void promote_snapshot_05() { run_05(false) ; }
@Test public void promote_readCommitted_05() { run_05(true) ; }
private void run_05(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
// bad - forced abort.
// Causes a WARN.
//logger1.setLevel(Level.ERROR) ;
dsg.end() ;
//logger1.setLevel(level1) ;
assertCount(0, dsg) ;
}
@Test public void promote_snapshot_06() { run_06(false) ; }
@Test public void promote_readCommitted_06() { run_06(true) ; }
// Async writer after promotion.
private void run_06(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
AtomicInteger a = new AtomicInteger(0) ;
Semaphore sema = new Semaphore(0) ;
Thread t = new Thread(() -> {
sema.release() ;
Txn.executeWrite(dsg, () -> dsg.add(q3)) ;
sema.release() ;
}) ;
dsg.begin(ReadWrite.READ) ;
// Promote
dsg.add(q1) ;
t.start() ;
// First release.
sema.acquireUninterruptibly() ;
// Thread blocked.
dsg.add(q2) ;
dsg.commit() ;
dsg.end() ;
// Until thread exits.
sema.acquireUninterruptibly() ;
assertCount(3, dsg) ;
}
@Test public void promote_snapshot_07() { run_07(false) ; }
@Test public void promote_readCommitted_07() { run_07(true) ; }
// Async writer after promotion.
private void run_07(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
// Start long running reader.
ThreadAction tt = ThreadTxn.threadTxnRead(dsg, () -> {
long x = Iter.count(dsg.find()) ;
if ( x != 0 )
throw new RuntimeException() ;
}) ;
// Start R->W here
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
dsg.add(q2) ;
dsg.commit() ;
dsg.end() ;
tt.run() ;
}
@Test public void promote_snapshot_08() { run_08(false) ; }
@Test public void promote_readCommitted_08() { run_08(true) ; }
// Async writer after promotion trasnaction ends.
private void run_08(boolean allowReadCommitted) {
setReadCommitted(allowReadCommitted);
DatasetGraph dsg = create() ;
// Start R->W here
dsg.begin(ReadWrite.READ) ;
dsg.add(q1) ;
dsg.add(q2) ;
dsg.commit() ;
dsg.end() ;
Txn.executeRead(dsg, () -> {
long x = Iter.count(dsg.find()) ;
assertEquals(2, x) ;
}) ;
}
// Tests for XXX Read-committed yes/no (false = snapshot isolation, true = read committed),
// and whether the other transaction commits (true) or aborts (false).
@Test
public void promote_10() { promote_readCommit_txnCommit(true, true) ; }
@Test
public void promote_11() { promote_readCommit_txnCommit(true, false) ; }
@Test
public void promote_12() {
expect(()->promote_readCommit_txnCommit(false, true) ,
getTransactionExceptionClass()) ;
}
@SafeVarargs
private final void expect(Runnable runnable, Class<? extends Exception>...classes) {
try {
runnable.run();
fail("Exception expected") ;
} catch (Exception e) {
for ( Class<?> c : classes) {
if ( e.getClass().equals(c) )
return ;
}
throw e ;
}
}
@Test
public void promote_13() { promote_readCommit_txnCommit(false, false) ; }
private void promote_readCommit_txnCommit(boolean allowReadCommitted, boolean asyncCommit) {
setReadCommitted(allowReadCommitted) ;
DatasetGraph dsg = create() ;
ThreadAction tt = asyncCommit?
ThreadTxn.threadTxnWrite(dsg, () -> dsg.add(q3) ) :
ThreadTxn.threadTxnWriteAbort(dsg, () -> dsg.add(q3)) ;
dsg.begin(ReadWrite.READ) ;
// Other runs
tt.run() ;
// Can promote if readCommited
// Can't promote if not readCommited
dsg.add(q1) ;
if ( ! allowReadCommitted && asyncCommit )
fail("Should not be here") ;
assertEquals(asyncCommit, dsg.contains(q3)) ;
dsg.commit() ;
dsg.end() ;
//logger2.setLevel(level2);
}
// Active writer commits -> no promotion.
@Test
public void promote_active_writer_1() throws InterruptedException, ExecutionException {
expect(()->promote_active_writer(true) ,
getTransactionExceptionClass()) ;
}
// Active writer aborts -> promotion.
@Test
public void promote_active_writer_2() throws InterruptedException, ExecutionException {
// Active writer aborts -> promotion possible (but not implemented that way).
promote_active_writer(false) ;
}
private void promote_active_writer(boolean activeWriterCommit) {
ExecutorService executor = Executors.newFixedThreadPool(2) ;
try {
promote_clash_active_writer(executor, activeWriterCommit) ;
}
finally {
executor.shutdown() ;
}
}
private void promote_clash_active_writer(ExecutorService executor, boolean activeWriterCommit) {
setReadCommitted(false) ;
Semaphore semaActiveWriterStart = new Semaphore(0) ;
Semaphore semaActiveWriterContinue = new Semaphore(0) ;
Semaphore semaPromoteTxnStart = new Semaphore(0) ;
Semaphore semaPromoteTxnContinue = new Semaphore(0) ;
DatasetGraph dsg = create() ;
// The "active writer".
Callable<Object> activeWriter = ()->{
dsg.begin(ReadWrite.WRITE) ;
semaActiveWriterStart.release(1) ;
// (*1)
semaActiveWriterContinue.acquireUninterruptibly(1) ;
if ( activeWriterCommit )
dsg.commit() ;
else
dsg.abort();
dsg.end() ;
return null ;
} ;
Future<Object> activeWriterFuture = executor.submit(activeWriter) ;
// Advance "active writer" to (*1), inside a write transaction and waiting.
// The transaction has been created and started.
semaActiveWriterStart.acquireUninterruptibly();
Callable<JenaException> attemptedPromote = ()->{
dsg.begin(ReadWrite.READ) ;
semaPromoteTxnStart.release(1) ;
// (*2)
semaPromoteTxnContinue.acquireUninterruptibly();
try {
// (*3)
dsg.add(q1) ;
return null ;
} catch (JenaException e) {
Class<?> c = getTransactionExceptionClass() ;
if ( ! e.getClass().equals(c) )
throw e ;
return e ;
}
} ;
Future<JenaException> attemptedPromoteFuture = executor.submit(attemptedPromote) ;
// Advance "attempted promote" to (*2), inside a read transaction, before attempting a promoting write.
// The transaction has been created and started.
semaPromoteTxnStart.acquireUninterruptibly();
// Advance "attempted promote" allowing it to go (*3) where it blocks
// This may happen at any time - as soon as it does, the "attempted promote" blocks.
semaPromoteTxnContinue.release(1);
// I don't know of a better way to ensure "attempted promote" is blocked.
Lib.sleep(100) ;
// Let the active writer go.
semaActiveWriterContinue.release(1);
try {
// Collect the active writer.
activeWriterFuture.get();
// (Ideal) and the attempted promotion should advance if the active writer aborts.
JenaException e = attemptedPromoteFuture.get() ;
if ( e != null )
throw e ;
} catch (InterruptedException | ExecutionException e1) { throw new RuntimeException(e1) ; }
}
}