[
https://issues.apache.org/jira/browse/OPENJPA-1702?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Heath Thomann resolved OPENJPA-1702.
------------------------------------
Resolution: Fixed
Fix Version/s: 2.1.0
> UnsupportedOperationException caused in BrokerImpl during transaction commit
> processing.
> ----------------------------------------------------------------------------------------
>
> Key: OPENJPA-1702
> URL: https://issues.apache.org/jira/browse/OPENJPA-1702
> Project: OpenJPA
> Issue Type: Bug
> Components: kernel
> Affects Versions: 1.2.0, 2.0.0
> Reporter: Heath Thomann
> Assignee: Heath Thomann
> Priority: Minor
> Fix For: 2.1.0
>
> Attachments: OPENJPA-1702-TEST.patch.txt
>
>
> For a given scenario, which will be described in detail below, an
> UnsupportedOperationException occurs as follows:
> [main] openjpa.Runtime - An exception occurred while ending the transaction.
> This exception will be re-thrown.
> <openjpa-1.2.3-SNAPSHOT-r422266:955388M nonfatal store error>
> org.apache.openjpa.util.StoreException: null
> at
> org.apache.openjpa.kernel.BrokerImpl.beforeCompletion(BrokerImpl.java:1853)
> at
> org.apache.openjpa.kernel.LocalManagedRuntime.commit(LocalManagedRuntime.java:81)
> at org.apache.openjpa.kernel.BrokerImpl.commit(BrokerImpl.java:1369)
> at
> org.apache.openjpa.kernel.DelegatingBroker.commit(DelegatingBroker.java:877)
> at
> org.apache.openjpa.persistence.EntityManagerImpl.commit(EntityManagerImpl.java:513)
> at hat.tests.TestUnsupportedOp.commitTx(TestUnsupportedOp.java:44)
> at hat.tests.TestUnsupportedOp.test(TestUnsupportedOp.java:90)
> at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
> at
> sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
> at
> sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
> at java.lang.reflect.Method.invoke(Method.java:592)
> at junit.framework.TestCase.runTest(TestCase.java:164)
> at junit.framework.TestCase.runBare(TestCase.java:130)
> at junit.framework.TestResult$1.protect(TestResult.java:110)
> at junit.framework.TestResult.runProtected(TestResult.java:128)
> at junit.framework.TestResult.run(TestResult.java:113)
> at junit.framework.TestCase.run(TestCase.java:120)
> at junit.framework.TestSuite.runTest(TestSuite.java:228)
> at junit.framework.TestSuite.run(TestSuite.java:223)
> at
> org.junit.internal.runners.OldTestClassRunner.run(OldTestClassRunner.java:35)
> at
> org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:46)
> at
> org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
> at
> org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
> at
> org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
> at
> org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
> at
> org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
> Caused by: java.lang.UnsupportedOperationException
> at java.util.AbstractCollection.add(AbstractCollection.java:216)
> at java.util.AbstractCollection.addAll(AbstractCollection.java:318)
> at
> org.apache.openjpa.kernel.BrokerImpl.flushTransAdditions(BrokerImpl.java:2103)
> at
> org.apache.openjpa.kernel.BrokerImpl.flushAdditions(BrokerImpl.java:2086)
> at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:2000)
> at org.apache.openjpa.kernel.BrokerImpl.flushSafe(BrokerImpl.java:1927)
> at
> org.apache.openjpa.kernel.BrokerImpl.beforeCompletion(BrokerImpl.java:1845)
> ... 25 more
> Using the stack trace, and some particulars about the code path, I've been
> able to recreate the UnsupportedOperationException. Let me first summarize
> what my test does, and then let me go into great details on how the issue
> occurs. My test does the following:
> 1) My "main" code simply begins a tran, performs a query, and commits the
> tran.
> 2) I've created a 'tran listener' (i.e. an impl of
> org.apache.openjpa.event.TransactionListener) and in that 'listener', method
> 'beforeCommit', I dirty the entity queried/found in #1.
> 3) After my 'beforeCommit' method returns, the UnsupportedOperationException
> is thrown.
> OK, that was the brief summary, for anyone else who cares to hear the gory
> details, lets dig in.....first, the exception stack shows the exception is
> hit here:
> Caused by: java.lang.UnsupportedOperationException
> at java.util.AbstractCollection.add(AbstractCollection.java:68)
> at java.util.AbstractCollection.addAll(AbstractCollection.java:87)
> at
> org.apache.openjpa.kernel.BrokerImpl.flushTransAdditions(BrokerImpl.java:2099)
>
> at
> org.apache.openjpa.kernel.BrokerImpl.flushAdditions(BrokerImpl.java:2086)
> at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:2000)
> So, lets look at the code around 'flush(BrokerImpl.java:2000)'. To follow is
> line 2000 (the last line) and a number of lines proceeding it:
> if ((_transEventManager.hasFlushListeners()
> || _transEventManager.hasEndListeners())
> && (flush || reason == FLUSH_COMMIT)) {
> // fire events
> mobjs = new ManagedObjectCollection(transactional);
> if (reason == FLUSH_COMMIT
> && _transEventManager.hasEndListeners()) {
> fireTransactionEvent(new TransactionEvent(this,
> TransactionEvent.BEFORE_COMMIT, mobjs,
> _persistedClss, _updatedClss, _deletedClss));
> flushAdditions(transactional, reason); <----- line
> 2000
> So, in order to get to this 'flushAdditions', you must have a 'listener'
> (i.e. an impl of org.apache.openjpa.event.TransactionListener). OK, with
> that said, keep this 'listener' idea in mind as we will come back to it.
> Continue to dig into the stack and going up two levels, we see that
> 'flushTransAdditions(BrokerImpl.java:2099)' looks like this:
> private boolean flushTransAdditions(Collection transactional, int reason)
> {
> if (_transAdditions == null || _transAdditions.isEmpty())
> return false;
> // keep local transactional list copy up to date
> transactional.addAll(_transAdditions); <----- line 2099
> There are two important things to note here:
> 1) 'transactional' is a 'Collection'.
> 2) the addAll will only be called depending on the state of '_transAdditions'.
> For #1, lets visit the javadoc for Collection.addAll and see why/when it
> throws the UnsupportedOperationException.....its states:
> * @throws UnsupportedOperationException if this collection does not
> * support the <tt>addAll</tt> method.
> So, we know that the 'Collection' is of a type which must not support addAll.
> This offers a clue and we should look to see at which points 'transactional'
> could be defined as a 'Collection' which doesn't support 'addAll'.
> 'transactional' is set in BrokerImpl at line 1946 which is here:
> Collection transactional = getTransactionalStates();
> If we look at 'getTransactionalStates()', we can see that the method could
> return a Collections.EMPTY_SET ('EmptySet'):
> protected Collection getTransactionalStates() {
> if (!hasTransactionalObjects())
> return Collections.EMPTY_SET;
> return _transCache.copy();
> }
> An 'EmptySet.addAll' eventually calls 'AbstractCollection.add' which
> blatantly throws an UnsupportedOperationException (plus, and
> Collections.EMPTY_SET is immutable, so we should be adding to it anyway).
> So, we know we must have a case where 'transactional' is an EmtpySet. One
> way this may occur is to only query objects as I've done in step #1 of my
> test (i.e. I never dirty anything in step #1).
> Next, #2 offers another clue in that we need to look at the case where
> '_transAdditions' is not null and not empty. If we look in BorkerImpl at the
> places where '_transAdditions' is set, we can see things are added to it in
> the 'setDirty' method. But, as we previously found, we are only querying
> objects, not making them dirty. So, how can we have 'transactional' be an
> EmptySet, yet '_transAdditions' not null or empty? One way is to go back to
> the 'listener' we discussed earlier and when the 'listener' is called, have
> it dirty an entity. In so doing, the 'setDirty' method will be called which
> will add elements to '_transAdditions' such that conditions are met to cause
> 'transactional.addAll' to be called in 'flushTransAdditions'. The ordering
> is basically like this:
> 1) 'transactional' is set to an EmptySet and the beginning of flush.
> 2) The 'listener' is called later on in flush which dirties an entity. This
> causes '_transAdditions' to not be null or empty.
> 3) After the 'listener' is called, flushTransAdditions is called where at
> which time 'addAll', and then 'add', is called on an
> EmptySet/AbstractCollection which returns the exception.
--
This message was sent by Atlassian JIRA
(v6.1.5#6160)