Thank you! On Mon, Mar 27, 2023 at 11:33 AM Kseniya Romanova <ksroman...@apache.org> wrote:
> Hi Alex! You can check if the preferred ASF Jira name is free and make a > request at https://selfserve.apache.org/jira-account.html > One of PMC members will approve right after this. > > Cheers, > Kseniya > > On Fri, Mar 24, 2023 at 10:24 PM Prigoreanu, Alexandru < > prigoreanu.alexan...@anteash.com> wrote: > >> hello everyone! please could you create a profile for me on jira so i can >> submit this issue there? thank you! >> >> On Wed, Mar 1, 2023 at 5:07 AM Prigoreanu, Alexandru < >> prigoreanu.alexan...@anteash.com> wrote: >> >> > Hello everyone! thank you for your time. >> > >> > - we have an entity PlaceImpl annotated @Cache(usage = >> > CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) >> > - we use Ignite's L2 Hibernate cache implementation through the >> > application property >> > >> spring.jpa.properties.hibernate.cache.region.factory_class=org.apache.ignite.cache.hibernate.HibernateRegionFactory >> > - we use ignite 2.14.0, ignite-hibernate-ext 5.3.0, hibernate 5.4.33 >> > - we have a complex transaction that creates a new PlaceImpl, saves it >> in >> > the database, and updates it. >> > - when the PlaceImpl is returned from the level 2 cache it does not >> > contain the latest data for some fields. let's say we expect that >> > PlaceImpl.description is not null while PlaceImpl.description is null. >> > >> > after a bit of debugging we got the following data: >> > - during the transaction the changes to PlaceImpl are flushed twice or >> > more: one in the middle of the transaction and the second one before the >> > transaction is committed. >> > - given the CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, this results >> in >> > 2 EntityUpdateAction for our PlaceImpl that was created (we don't talk >> > about inserts here) added to the collection of processes in >> > AfterTransactionCompletionProcessQueue.processes >> > - after the transaction is completed, on the processes of the >> > aforementioned list hibernate invokes doAfterTransactionCompletion >> > >> > public void afterTransactionCompletion(boolean success) { >> > while ( !processes.isEmpty() ) { >> > try { >> > processes.poll().doAfterTransactionCompletion( success, session ); >> > } >> > >> > - the first EntityUpdateAction contains an incomplete PlaceImpl that >> does >> > not yet have all the fields set >> > - the second EntityUpdateAction contains the complete PlaceImpl with all >> > the fields set >> > >> > - the first EntityUpdateAction.doAfterTransactionCompletion gets to >> > execute HibernateNonStrictAccessStrategy.afterUpdate: here ctx is not >> null >> > and the if ctx != null branch is executed and the incomplete PlaceImpl >> is >> > put in the level 2 cache >> > >> > @Override public boolean afterUpdate(Object key, Object val) { >> > WriteContext ctx = writeCtx.get(); >> > >> > >> > if (log.isDebugEnabled()) >> > log.debug("Put after update [cache=" + cache.name() + ", >> key=" + >> > key + ", val=" + val + ']'); >> > >> > >> > if (ctx != null) { >> > ctx.updated(key, val); >> > >> > >> > unlock(key); >> > >> > >> > return true; >> > } >> > >> > >> > return false; >> > } >> > >> > which invokes also unlock(key); >> > >> > @Override public void unlock(Object key) { >> > try { >> > WriteContext ctx = writeCtx.get(); >> > >> > >> > if (ctx != null && ctx.unlocked(key)) { >> > writeCtx.remove(); >> > >> > >> > ctx.updateCache(cache); >> > } >> > } >> > >> > catch (IgniteCheckedException e) { >> > throw convertException(e); >> > } >> > } >> > >> > that removes writeCtx from the current thread with writeCtx.remove(); >> > >> > - the second EntityUpdateAction.doAfterTransactionCompletion gets to >> > execute HibernateNonStrictAccessStrategy.afterUpdate: here ctx is null >> and >> > the if ctx != null branch is not executed, so the level 2 cache is never >> > updated with the latest changes in the PlaceImpl entity. >> > >> > we were able to have a minimal dummy text example of the transaction >> that >> > creates a PlaceImpl >> > >> > @Test >> > public void testPlaceImplCacheWorksWithFlush() throws Exception { >> > long[] placeId = new long [] {0L}; >> > doInTransaction(() -> { >> > PlaceImpl place = new PlaceImpl(); >> > entityManager.persist(place); >> > placeId[0] = place.getId(); >> > entityManager.flush(); >> > place.setName("NAME"); //set some place properties >> > entityManager.flush(); >> > place.setDescription("description"); //set some other place >> > properties >> > assertThat(place.getDescription(), Matchers.is("description")); >> > }); >> > //load place from the cache >> > Place place = placeImplRepository.findOne(placeId[0]); >> > assertThat(place.getName(), Matchers.is("NAME")); >> > //the following assertion fails >> > assertThat(place.getDescription(), Matchers.is("description")); >> > } >> > >> > we are aware the given example should not manually invoke flushes but in >> > our real transaction the flush is not manual, our code provokes >> > inadvertently autoFlushIfRequired that happens to flush also updates to >> our >> > new PlaceImpl entity >> > >> > what are your thoughts on the matter? >> > could this be a bug? >> > should we not use Ignite's hibernate level 2 cache implementation >> > HibernateRegionFactory when transactions update entities with multiple >> > flushes? >> > if you have any pointers on a solution we could also try to provide you >> a >> > pull request with the implementation. >> > >> > thank you for your time! >> > >> > Alex >> > >> >