This Bugzilla instance is deprecated, and most Eclipse projects now use GitHub or Eclipse GitLab. Please see the deprecation plan for details.
Bug 256277 - Endless loop on @PrePersist
Summary: Endless loop on @PrePersist
Status: RESOLVED FIXED
Alias: None
Product: z_Archived
Classification: Eclipse Foundation
Component: Eclipselink (show other bugs)
Version: unspecified   Edit
Hardware: PC Windows XP
: P2 normal (vote)
Target Milestone: ---   Edit
Assignee: Nobody - feel free to take it CLA
QA Contact:
URL: http://wiki.eclipse.org/EclipseLink/B...
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2008-11-24 08:48 EST by Tom Eugelink CLA
Modified: 2022-06-09 10:29 EDT (History)
4 users (show)

See Also:


Attachments
UC1 - reproduction SE (25.42 KB, application/x-zip-compressed)
2009-01-05 16:24 EST, Michael OBrien CLA
no flags Details
UC2 - SE JPA example (no infinite loop - PrePersist persist entity does not have its own persist) (25.45 KB, application/x-zip-compressed)
2009-01-05 16:26 EST, Michael OBrien CLA
no flags Details
UC1 - reproduction SE code (nested flush() caused by read query on PrePersist) (21.96 KB, application/x-zip-compressed)
2009-01-11 21:21 EST, Michael OBrien CLA
no flags Details
eclipselink.jpa patch (pre-code-review) (6.50 KB, patch)
2009-01-11 22:19 EST, Michael OBrien CLA
no flags Details | Diff
eclipselink core patch (code review 1) (5.99 KB, patch)
2009-01-13 15:07 EST, Michael OBrien CLA
no flags Details | Diff
eclipselink core patch (code review 1) - missed saving RWUOW (5.98 KB, patch)
2009-01-13 15:17 EST, Michael OBrien CLA
no flags Details | Diff
eclipselink core patch (code review 1) - moved set for isWithinFlush within try/catch (6.14 KB, patch)
2009-01-13 18:07 EST, Michael OBrien CLA
no flags Details | Diff
eclipselink core patch (code review 1) - same as original patch but with 2 flag resets (in with/without commit changes) (6.26 KB, patch)
2009-01-13 19:42 EST, Michael OBrien CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Tom Eugelink CLA 2008-11-24 08:48:29 EST
Build ID: 1.0.1

Steps To Reproduce:
Execute a query during @PrePersist with flushmode set to the default value (auto).

Newsgroups, topic "@PrePersist endless loop", James' posting 2008-11-19 14:43
"Please also log a bug for the loop, if we are in the process of a flush, we should not attempt another flush, although if the query's flush mode is auto, we will have to throw an error"


More information:
When an entity is persisted some other entities must be created. Using the @PrePersist annotation and then start creating and persisting new entities. This results in an endless loop. 

Thread [AWT-EventQueue-1] (Suspended (breakpoint at line 78 in Batchtransferline))   
    Batchtransferline.preSaveHook() line: 78   
    Batchtransferline(AbstractBean).preSave() line: 230   
    GeneratedMethodAccessor81.invoke(Object, Object[]) line: not available   
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25   
    Method.invoke(Object, Object...) line: 597   
    PrivilegedAccessHelper.invokeMethod(Method, Object, Object[]) line: 344   
    EntityClassListener(EntityListener).invokeMethod(Method, Object, Object[], DescriptorEvent) line: 297   
    EntityClassListener.invokeMethod(String, DescriptorEvent) line: 64   
    EntityClassListener(EntityListener).preUpdateWithChanges(DescriptorEvent) line: 426   
    DescriptorEventManager.notifyListener(DescriptorEventListener, DescriptorEvent) line: 656   
    DescriptorEventManager.notifyEJB30Listeners(DescriptorEvent) line: 593   
    DescriptorEventManager.executeEvent(DescriptorEvent) line: 187   
    ExpressionQueryMechanism(DatabaseQueryMechanism).updateObjectForWriteWithChangeSet() line: 1083   
    UpdateObjectQuery.executeCommitWithChangeSet() line: 84   
    ExpressionQueryMechanism(DatabaseQueryMechanism).executeWriteWithChangeSet() line: 290   
    UpdateObjectQuery(WriteObjectQuery).executeDatabaseQuery() line: 58   
    UpdateObjectQuery(DatabaseQuery).execute(AbstractSession, AbstractRecord) line: 666   
    UpdateObjectQuery(DatabaseQuery).executeInUnitOfWork(UnitOfWorkImpl, AbstractRecord) line: 585   
    UpdateObjectQuery(ObjectLevelModifyQuery).executeInUnitOfWorkObjectLevelModifyQuery(UnitOfWorkImpl, AbstractRecord) line: 114   
    UpdateObjectQuery(ObjectLevelModifyQuery).executeInUnitOfWork(UnitOfWorkImpl, AbstractRecord) line: 86   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).internalExecuteQuery(DatabaseQuery, AbstractRecord) line: 2587   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery, AbstractRecord, int) line: 1178   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery, AbstractRecord) line: 1162   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery) line: 1108   
    CommitManager.commitChangedObjectsForClassWithChangeSet(UnitOfWorkChangeSet, String) line: 297   
    CommitManager.commitAllObjectsForClassWithChangeSet(UnitOfWorkChangeSet, Class) line: 228   
    CommitManager.commitAllObjectsWithChangeSet(UnitOfWorkChangeSet) line: 185   
    RepeatableWriteUnitOfWork(AbstractSession).writeAllObjectsWithChangeSet(UnitOfWorkChangeSet) line: 3129   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).commitToDatabase(boolean) line: 1241   
    RepeatableWriteUnitOfWork.commitToDatabase(boolean) line: 478   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).commitToDatabaseWithPreBuiltChangeSet(UnitOfWorkChangeSet, boolean) line: 1370   
    RepeatableWriteUnitOfWork.writeChanges() line: 300   
    EntityManagerImpl.flush() line: 384   
    EJBQueryImpl.performPreQueryFlush() line: 936   
    EJBQueryImpl.executeReadQuery() line: 378   
    EJBQueryImpl.getResultList() line: 517   
    Batch.findAllThatHaveAmountLeftOrderByBatchnr(Batchtype) line: 103   
    Batchcount.changeAllocation(BigInteger, Batchtransferline) line: 85   
    Batchtransferline.preSaveHook() line: 86   
    Batchtransferline(AbstractBean).preSave() line: 230   
    GeneratedMethodAccessor81.invoke(Object, Object[]) line: not available   
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25   
    Method.invoke(Object, Object...) line: 597   
    PrivilegedAccessHelper.invokeMethod(Method, Object, Object[]) line: 344   
    EntityClassListener(EntityListener).invokeMethod(Method, Object, Object[], DescriptorEvent) line: 297   
    EntityClassListener.invokeMethod(String, DescriptorEvent) line: 64   
    EntityClassListener(EntityListener).preUpdateWithChanges(DescriptorEvent) line: 426   
    DescriptorEventManager.notifyListener(DescriptorEventListener, DescriptorEvent) line: 656   
    DescriptorEventManager.notifyEJB30Listeners(DescriptorEvent) line: 593   
    DescriptorEventManager.executeEvent(DescriptorEvent) line: 187   
    ExpressionQueryMechanism(DatabaseQueryMechanism).updateObjectForWriteWithChangeSet() line: 1083   
    UpdateObjectQuery.executeCommitWithChangeSet() line: 84   
    ExpressionQueryMechanism(DatabaseQueryMechanism).executeWriteWithChangeSet() line: 290   
    UpdateObjectQuery(WriteObjectQuery).executeDatabaseQuery() line: 58   
    UpdateObjectQuery(DatabaseQuery).execute(AbstractSession, AbstractRecord) line: 666   
    UpdateObjectQuery(DatabaseQuery).executeInUnitOfWork(UnitOfWorkImpl, AbstractRecord) line: 585   
    UpdateObjectQuery(ObjectLevelModifyQuery).executeInUnitOfWorkObjectLevelModifyQuery(UnitOfWorkImpl, AbstractRecord) line: 114   
    UpdateObjectQuery(ObjectLevelModifyQuery).executeInUnitOfWork(UnitOfWorkImpl, AbstractRecord) line: 86   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).internalExecuteQuery(DatabaseQuery, AbstractRecord) line: 2587   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery, AbstractRecord, int) line: 1178   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery, AbstractRecord) line: 1162   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery) line: 1108   
    CommitManager.commitChangedObjectsForClassWithChangeSet(UnitOfWorkChangeSet, String) line: 297   
    CommitManager.commitAllObjectsForClassWithChangeSet(UnitOfWorkChangeSet, Class) line: 228   
    CommitManager.commitAllObjectsWithChangeSet(UnitOfWorkChangeSet) line: 185   
    RepeatableWriteUnitOfWork(AbstractSession).writeAllObjectsWithChangeSet(UnitOfWorkChangeSet) line: 3129   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).commitToDatabase(boolean) line: 1241   
    RepeatableWriteUnitOfWork.commitToDatabase(boolean) line: 478   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).commitToDatabaseWithPreBuiltChangeSet(UnitOfWorkChangeSet, boolean) line: 1370   
    RepeatableWriteUnitOfWork.writeChanges() line: 300   
    EntityManagerImpl.flush() line: 384   
    EJBQueryImpl.performPreQueryFlush() line: 936   
    EJBQueryImpl.executeReadQuery() line: 378   
    EJBQueryImpl.getResultList() line: 517   
    Batch.findAllThatHaveAmountLeftOrderByBatchnr(Batchtype) line: 103   
    Batchcount.changeAllocation(BigInteger, Batchtransferline) line: 85   
    Batchtransferline.preSaveHook() line: 86   
...
    Batchtransferline.preSaveHook() line: 86   
    Batchtransferline(AbstractBean).preSave() line: 230   
    GeneratedMethodAccessor81.invoke(Object, Object[]) line: not available   
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25   
    Method.invoke(Object, Object...) line: 597   
    PrivilegedAccessHelper.invokeMethod(Method, Object, Object[]) line: 344   
    EntityClassListener(EntityListener).invokeMethod(Method, Object, Object[], DescriptorEvent) line: 297   
    EntityClassListener.invokeMethod(String, DescriptorEvent) line: 64   
    EntityClassListener(EntityListener).preUpdateWithChanges(DescriptorEvent) line: 426   
    DescriptorEventManager.notifyListener(DescriptorEventListener, DescriptorEvent) line: 656   
    DescriptorEventManager.notifyEJB30Listeners(DescriptorEvent) line: 593   
    DescriptorEventManager.executeEvent(DescriptorEvent) line: 187   
    ExpressionQueryMechanism(DatabaseQueryMechanism).updateObjectForWriteWithChangeSet() line: 1083   
    UpdateObjectQuery.executeCommitWithChangeSet() line: 84   
    ExpressionQueryMechanism(DatabaseQueryMechanism).executeWriteWithChangeSet() line: 290   
    UpdateObjectQuery(WriteObjectQuery).executeDatabaseQuery() line: 58   
    UpdateObjectQuery(DatabaseQuery).execute(AbstractSession, AbstractRecord) line: 666   
    UpdateObjectQuery(DatabaseQuery).executeInUnitOfWork(UnitOfWorkImpl, AbstractRecord) line: 585   
    UpdateObjectQuery(ObjectLevelModifyQuery).executeInUnitOfWorkObjectLevelModifyQuery(UnitOfWorkImpl, AbstractRecord) line: 114   
    UpdateObjectQuery(ObjectLevelModifyQuery).executeInUnitOfWork(UnitOfWorkImpl, AbstractRecord) line: 86   
    RepeatableWriteUnitOfWork(UnitOfWorkImpl).internalExecuteQuery(DatabaseQuery, AbstractRecord) line: 2587   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery, AbstractRecord, int) line: 1178   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery, AbstractRecord) line: 1162   
    RepeatableWriteUnitOfWork(AbstractSession).executeQuery(DatabaseQuery) line: 1108   
    CommitManager.commitChangedObjectsForClassWithChangeSet(UnitOfWorkChangeSet, String) line: 297   
    CommitManager.commitAllObj
Comment 1 Michael OBrien CLA 2009-01-05 15:59:57 EST
I have reproduced this error using a nested persist() inside the @PrePersist callback method of the same entity being secondarily persisted.

The following prerequisites are required for reproduction.
- Run tests out of container on an SE persitence unit 
- At least two entities must be persisted to avoid performance optimization code for a single entity 
- Two different entity classes are required 
- Test must commit more than one object so that the non-performance else clause in CommitManager.commitAllObjectsWithChangeSet() is used 
- Test must perform a change on an existing object that is already persisted 
- Set FlushMode via entityManager.setFlushMode(FlushModeType.AUTO); 
- Set a @PrePersist method callback on an Entity that does not perform a secondary persist 
- Set a @PrePersist method callback on an Entity that performs a secondary persist 


Analysis document with reproduction, usecases and design details is being written to the following location.

http://wiki.eclipse.org/EclipseLink/Bugs/256277

Comment 2 Michael OBrien CLA 2009-01-05 16:24:56 EST
Created attachment 121567 [details]
UC1 - reproduction SE
Comment 3 Michael OBrien CLA 2009-01-05 16:26:39 EST
Created attachment 121568 [details]
UC2 - SE JPA example (no infinite loop - PrePersist persist entity does not have its own persist)
Comment 4 Gordon Yorke CLA 2009-01-06 10:16:00 EST
The core of this issue is the automatic flushing that occurs when a query is executed against an em.

em.commit()-> em.flush() -> prePersist event -> em.query -> em.flush()-> prePersist()

EclipseLink executes a flush on query execution to ensure that the query resolves against any uncommitted data.
Comment 5 Gordon Yorke CLA 2009-01-06 10:31:28 EST
The EM should track if it is already flushing and if it is then a query execution logs a warning (like: "EM is already flushing. Query will be executed without further changes being written to database.  If the query is conditional upon changed data the changes may not be reflected in the results.  Users should issue a flush() call upon completion of the dependent changes and prior to this flush() to ensure correct results")
and does not execute a subsequent flush().
Comment 6 Gordon Yorke CLA 2009-01-06 10:33:02 EST
To reproduce the Entity on which the prePersist exists should be an unregistered new object and be referenced by a managed Entity.
Comment 7 Michael OBrien CLA 2009-01-11 21:21:58 EST
Created attachment 122220 [details]
UC1 - reproduction SE code (nested flush() caused by read query on PrePersist)

See data model at
http://wiki.eclipse.org/EclipseLink/Bugs/256277#Data_Model
Comment 8 Michael OBrien CLA 2009-01-11 22:19:33 EST
Created attachment 122222 [details]
eclipselink.jpa patch (pre-code-review)

patch based on svn ref 3162
jpa lrg run with 1064,1,0 (expected existing 1 PostLoad failure)
Comment 9 Michael OBrien CLA 2009-01-13 15:07:58 EST
Created attachment 122453 [details]
eclipselink core patch (code review 1)

Code-reviewed by Tom and Gordon
Comment 10 Michael OBrien CLA 2009-01-13 15:17:36 EST
Created attachment 122455 [details]
eclipselink core patch (code review 1) - missed saving RWUOW

Created patch without saving a file in eclipse - ignore previously commented // return;
Comment 11 Michael OBrien CLA 2009-01-13 18:07:59 EST
Created attachment 122469 [details]
eclipselink core patch (code review 1) - moved set for isWithinFlush within try/catch

I moved the place where the flag was set so that it is inside the try/catch block.  We are then always setting the flag back to false in the finally block when we come back from a writeChanges() call that would have been nested.
There were 17 flush() failures on the JPA LRG with my previous patch because I was returning out before the try block with the flag set to true in some use cases.

Currently with this patch we match a clean view: 
JPA LRG: (1 expected PostClone failure)
    [junit] Tests run: 1065, Failures: 1, Errors: 0, Time elapsed: 806.286 sec
Foundation LRG: (2 expected getContainerAttributeName XML failures)
    [junit] Tests run: 5626, Failures: 0, Errors: 2, Time elapsed: 694.761 sec
Comment 12 Michael OBrien CLA 2009-01-13 19:42:54 EST
Created attachment 122486 [details]
eclipselink core patch (code review 1) - same as original patch but with 2 flag resets (in with/without commit changes)

  One more iteration, I sent the mail too soon and it turned out that I was setting the flag too late for my reproduction - which occurs in calculateChanges.
So we now set the flag before calculateChanges like before but clear it in two places instead of one.
- when we don't commit anything on an early return - just before the return
- when we commit changes - in the finally block

Details:
----------
The call to calculateChanges() can be recursive before we even get to commitToDatabaseWithPreBuiltChangeSet().
In this case we will commit in a PrePersist and then later fail on a second commit.
We therefore need to move the flag set before the try/catch block.

Thread [main] (Suspended (breakpoint at line 293 in RepeatableWriteUnitOfWork))	
	RepeatableWriteUnitOfWork.writeChanges() line: 293	

	EntityManagerImpl.flush() line: 534
	EJBQueryImpl.performPreQueryFlush() line: 1038	
	EJBQueryImpl.executeReadQuery() line: 347	
	EJBQueryImpl.getResultList() line: 592	
	Address.processQuery(EntityManager) line: 83	
	Address.prePersist() line: 73	
	NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]	
	NativeMethodAccessorImpl.invoke(Object, Object[]) line: 39	
	DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 25	
	Method.invoke(Object, Object...) line: 597	
	PrivilegedAccessHelper.invokeMethod(Method, Object, Object[]) line: 344	
	EntityClassListener(EntityListener).invokeMethod(Method, Object, Object[], DescriptorEvent) line: 297	
	EntityClassListener.invokeMethod(String, DescriptorEvent) line: 64	
	EntityClassListener(EntityListener).prePersist(DescriptorEvent) line: 412	
	DescriptorEventManager.notifyListener(DescriptorEventListener, DescriptorEvent) line: 650	
	DescriptorEventManager.notifyEJB30Listeners(DescriptorEvent) line: 593	
	DescriptorEventManager.executeEvent(DescriptorEvent) line: 187	
	RepeatableWriteUnitOfWork(UnitOfWorkImpl).registerNewObjectClone(Object, Object, ClassDescriptor) line: 3976	
	RepeatableWriteUnitOfWork(UnitOfWorkImpl).registerNotRegisteredNewObjectForPersist(Object, ClassDescriptor) line: 3955	
	RepeatableWriteUnitOfWork.registerNotRegisteredNewObjectForPersist(Object, ClassDescriptor) line: 356	
	RepeatableWriteUnitOfWork(UnitOfWorkImpl).discoverAndPersistUnregisteredNewObjects(Object, boolean, Map, Map, Map) line: 3857	
	OneToOneMapping(ObjectReferenceMapping).cascadeDiscoverAndPersistUnregisteredNewObjects(Object, Map, Map, Map, UnitOfWorkImpl) line: 713	
	ObjectBuilder.cascadeDiscoverAndPersistUnregisteredNewObjects(Object, Map, Map, Map, UnitOfWorkImpl) line: 1493	
	RepeatableWriteUnitOfWork(UnitOfWorkImpl).discoverAndPersistUnregisteredNewObjects(Object, boolean, Map, Map, Map) line: 3871	
	RepeatableWriteUnitOfWork.discoverUnregisteredNewObjects(Map, Map, Map, Map) line: 182	
	RepeatableWriteUnitOfWork(UnitOfWorkImpl).calculateChanges(Map, UnitOfWorkChangeSet, boolean) line: 613	
	RepeatableWriteUnitOfWork.writeChanges() line: 304	

	EntityManagerImpl.flush() line: 534	
	TestClient.prePersistQuery() line: 105	
	TestClient.main(String[]) line: 128	

Sequence: flush() calls flush() because of uncommitted changes
- flush
- flag is set
- calculateChanges called
- flush 2 has no effect
- changes committed
- flag cleared in finally block

Sequence: flush() calls flush() but no changes were uncommitted
- flush
- flag is set
- calculateChanges called
- flush 2 has no effect
- changes not done
- flag cleared before return
- we return early before the commit

Sequence: single flush() call with changes
- flush
- flag is set
- calculateChanges called
- changes committed
- flag cleared in finally block

Sequence: single flush() call with no changes
- flush
- flag is set
- calculateChanges called
- changes not done
- flag cleared before return
- we return early before the commit

Test results:
 My reproduction and JPA LRG and Core LRG OK
Comment 13 Michael OBrien CLA 2009-01-14 11:53:14 EST
    The following patch has been reviewed and checked in for bug# 256277

        http://bugs.eclipse.org/256277
        https://bugs.eclipse.org/bugs/attachment.cgi?id=122486&action=diff
        http://fisheye2.atlassian.com/changelog/eclipselink/?cs=3202

    Code-Review:
       Reviewed by Gordon and Tom - thank you

    Description:
       Currently flush() can be nested causing an infinite loop or DB errors on a double commit.
       We have introduced a boolean flag on RepeatableWriteUnitOfWork that is true while we are flushing and use this flag to stop any nested flushing from occurring.

       For a reproduction, we have a managed source entity that has some uncommitted changes.
       There is an unmanaged target entity that has a @PrePersist callback method that contains a query on the source .
       We do a final flush() or commit().
       The result is that the @PrePersist method causes it's own flush() within a flush() - we need to disallow this.
       Originally I placed the logic to break out of a nested flush at the EJBQueryImpl.executeReadQuery() level, however after a code review this was moved to a more generic location in RepeatableWriteUnitOfWork.writeChanges().


    Test Results: OK
       JPA LRG: (1 expected PostClone failure)
            [junit] Tests run: 1065, Failures: 1, Errors: 0, Time elapsed: 806.286 sec
       Foundation LRG: (2 expected getContainerAttributeName XML failures)
            [junit] Tests run: 5626, Failures: 0, Errors: 2, Time elapsed: 694.761 sec
   (reran under rev 3197)
       JPA LRG:
            [junit] Tests run: 1065, Failures: 1, Errors: 0, Time elapsed: 592.973 sec
       Core LRG:
            [junit] Tests run: 6646, Failures: 0, Errors: 0, Time elapsed: 882.218 sec

    SVN
       Merged into rev 3197
       Created rev 3202
Comment 14 Eclipse Webmaster CLA 2022-06-09 10:14:45 EDT
The Eclipselink project has moved to Github: https://github.com/eclipse-ee4j/eclipselink
Comment 15 Eclipse Webmaster CLA 2022-06-09 10:29:25 EDT
The Eclipselink project has moved to Github: https://github.com/eclipse-ee4j/eclipselink