hibernate-reactive icon indicating copy to clipboard operation
hibernate-reactive copied to clipboard

Non-reactive forceFlush in AbstractReactiveSaveEventListener.java:172

Open maresja1 opened this issue 3 years ago • 10 comments

There is a call to non-reactive forceFlush of SessionImpl that leads to UnsupportedOperationException from DefaultReactiveFlushEventListener:

	Caused by: java.util.concurrent.CompletionException: javax.persistence.PersistenceException: org.hibernate.HibernateException: java.lang.UnsupportedOperationException
		at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
		at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1194)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
		at org.hibernate.reactive.event.impl.AbstractReactiveSaveEventListener.reactiveSaveWithGeneratedId(AbstractReactiveSaveEventListener.java:123)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.saveTransientEntity(DefaultReactiveMergeEventListener.java:283)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.lambda$entityIsTransient$5(DefaultReactiveMergeEventListener.java:257)
		at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.entityIsTransient(DefaultReactiveMergeEventListener.java:257)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.lambda$entityIsDetached$11(DefaultReactiveMergeEventListener.java:364)
		at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.entityIsDetached(DefaultReactiveMergeEventListener.java:321)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.reactiveOnMerge(DefaultReactiveMergeEventListener.java:190)
		at org.hibernate.reactive.event.impl.DefaultReactiveMergeEventListener.reactiveOnMerge(DefaultReactiveMergeEventListener.java:90)
		at org.hibernate.event.service.internal.EventListenerGroupImpl.lambda$fireEventOnEachListener$0(EventListenerGroupImpl.java:133)
		at java.base/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2309)
		at java.base/java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:144)
		at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:133)
		at org.hibernate.reactive.session.impl.ReactiveSessionImpl.fireMerge(ReactiveSessionImpl.java:849)
		... 298 more
	Caused by: javax.persistence.PersistenceException: org.hibernate.HibernateException: java.lang.UnsupportedOperationException
		at app//org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
		at app//org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
		at app//org.hibernate.reactive.session.impl.ReactiveExceptionConverter.convert(ReactiveExceptionConverter.java:31)
		at app//org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1411)
		at app//org.hibernate.internal.SessionImpl.forceFlush(SessionImpl.java:1438)
		at app//org.hibernate.reactive.event.impl.AbstractReactiveSaveEventListener.reactivePerformSave(AbstractReactiveSaveEventListener.java:172)
		at app//org.hibernate.reactive.event.impl.AbstractReactiveSaveEventListener.lambda$reactiveSaveWithGeneratedId$0(AbstractReactiveSaveEventListener.java:123)
		at [email protected]/java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:1187)
		... 320 more
	Caused by: org.hibernate.HibernateException: java.lang.UnsupportedOperationException
		... 326 more
	Caused by: java.lang.UnsupportedOperationException
		at org.hibernate.reactive.event.impl.DefaultReactiveFlushEventListener.onFlush(DefaultReactiveFlushEventListener.java:63)
		at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
		at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407)
		... 324 more

maresja1 avatar Aug 19 '22 13:08 maresja1

maresja1 can you add a reproducer and/or test scenario with entities?

blafond avatar Aug 19 '22 13:08 blafond

@blafond I understand the request, but in my situation that would be pretty difficult. I thought that the call to forceFlush on SessionImpl is plainly wrong, as it results in UnsupportedOperationException from DefaultReactiveFlushEventListener.

In other words code from AbstractReactiveSaveEventListener ends up calling non-reactive flush, that ends up with expected exception from DefaultReactiveFlushEventListener.

I am working on experimental integration between Hibernate Reactive and Spring Data. In simple unit test it works fine, when trying to migrate existing non-reactive service to this integration I bumped into this exception. Reproducing it in a "clear" (non-Spring) environment would take long and might end up nowhere - as even this manifestation seems to be affected by reordering, in other words, the tests sometimes fail and sometimes pass.

Let it be noted, that this issue has a workaround and that is to flush reactively, manually after delete. As it is probably obvious from the code, the wrong kind of flush happens only when there is entity in the persistent context, which state is DELETED.

Feel free to ask for more details.

maresja1 avatar Aug 20 '22 15:08 maresja1

Yes, this is a bug. It shouldn't be possible to reach that code.

DavideD avatar Aug 30 '22 12:08 DavideD

Been digging around to try and find a proper test case or scenario.

The error is being thrown via the reactive AbstractReactiveSaveEventListener.reactivePerformSave() which basically contains the same code as ORM's AbstractSaveEventListener.performSave()

The only test in hibernate-core that enters that if( old != null ) check is via DetachedBagDelayedOperationTest.testCollectionWithQueuedOperationsOnRollback()

@DavideD, @gavinking Either of you know from the user's description how his use-case might be set up? else I'll try and recreate this ORM test in reactive.

blafond avatar Sep 06 '22 21:09 blafond

The only test in hibernate-core that enters that if( old != null ) check is via DetachedBagDelayedOperationTest.testCollectionWithQueuedOperationsOnRollback()

Sounds good, can't you adapt the same test for Hibernate Reactive?

DavideD avatar Sep 07 '22 08:09 DavideD

@DavideD, @gavinking Either of you know from the user's description how his use-case might be set up? else I'll try and recreate this ORM test in reactive.

At this point it doesn't really matter the exact use case. The call to the non reactive method is wrong anyway. So it's fine to adapt the test from ORM.

DavideD avatar Sep 07 '22 08:09 DavideD

After further testing in ORM, there are a number of tests that create a DELETED entity status where the entity is "old", however, I can't find a test/scenario where the "old" entity's status is DELETED (see: AbstractSaveEventListener.performSave())

So still have not been able to reproduce this in Reactive or find a test to port in ORM.

blafond avatar Sep 21 '22 17:09 blafond

@blafond, this should show the exception:

	@Test
	public void testForceFlushWithDelete(TestContext context) {
		final Flour rose1 = new Flour( 2, "Rose1", "1", "Full fragrance" );
		final Flour rose2 = new Flour( 2, "Rose2", "2", "Full fragrance" );

		test( context, getMutinySessionFactory()
				.withTransaction( session -> session
						.persist( rose1 )
						.call( () -> session.remove( rose1 ) )
						.call( () -> session.persist( rose2 ) )
				)
		);
	}

I've used the entity in ORMReactivePersistenceTest, but, please, find a better place for it (maybe you can adapt it for MutinySessionTest).

DavideD avatar Sep 21 '22 22:09 DavideD

I ran your in test with an ORM test in hibernate-core

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: org.hibernate.test.any.Person
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:120)
	at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
	at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:756)
	... 17 more

Your test ends up with something similar...

Caused by: org.hibernate.PersistentObjectException: detached entity passed to persist: org.hibernate.reactive.ORMForceFlushTest$Flour
	at app//org.hibernate.reactive.event.impl.DefaultReactivePersistEventListener.reactiveOnPersist(DefaultReactivePersistEventListener.java:121)
	at app//org.hibernate.reactive.event.impl.DefaultReactivePersistEventListener.reactiveOnPersist(DefaultReactivePersistEventListener.java:58)
	at app//org.hibernate.event.service.internal.EventListenerGroupImpl.lambda$fireEventOnEachListener$0(EventListenerGroupImpl.java:133)
	at org.hibernate.reactive.session.impl.ReactiveSessionImpl.firePersist(ReactiveSessionImpl.java:699)

The user's exception is thrown from Reactive is thrown from the same ORM class: EventListenerGroupImpl.fireEventOnEachListener()' but triggered via doFlush()`

blafond avatar Sep 22 '22 15:09 blafond

If you don't show me how you have adapted the test, I cannot help you

DavideD avatar Sep 22 '22 16:09 DavideD

@DavideD The commits above fixes the issue. I still need to refactor the reactivePerformSave() to simplify it.

blafond avatar Sep 23 '22 15:09 blafond