Synchronization with the indexes : refresh problem

Hi searchers,

I’am confronted to a problem that operate on a search screen. A classic problem I think.
The use case is as follows :

  1. The user do a search query with multiple criteria.
  2. The backend, through hibernate search, compute the results and return them to the view.
  3. The view result is displayed.

This normal scenario is ok. But another scenario is causing refresh problem :

  1. From 3), the user click on an action button in a single row result.
  2. The backend process the action, then the view refreshes itself by calling again the backend to compute the new search result.
  3. The problem here, it that the action process impacts the search result. That is to say, the action do a kind of database deletion, and so, the automatic indexing is only triggered by polling interval for updating the index. But this time is “too long”, because the view has already called the backend which is doing the query already BEFORE indexes are updated. If I do a Thread.sleep(2000) before the query, then the new result has taken into account the deletion (1 result less than at 2)).
    Another possibility to correct the sync problem is to set hibernate.search.automatic_indexing.synchronization.strategy = read-sync instead of default
    hibernate.search.automatic_indexing.synchronization.strategy = write sync

But I’am still not satisfied with this configuration because this use case is isolated, and I don’t want to impact the whole behavior of hibernate search only for this use case. Because, read-sync has Throughput to “Medium to worst”. Basically, we are losing in performance.

So I come to my work. I wish to change the behavior of hibernate.search.automatic_indexing.synchronization.strategy programmatically with a method like :

private void readSyncSynchronizationStrategy() {
        SearchSession searchSession = Search.session((EntityManager) sessionFactory.getCurrentSession());
        searchSession.automaticIndexingSynchronizationStrategy(AutomaticIndexingSynchronizationStrategy.readSync());
    }

The idea, is to turn the strategy to readSync BEFORE the transaction and turn back to writeSync (the default) AFTER the transaction commit (the commit trigger the automatic indexing)

Here is my code :

@Aspect
@Component
public class IndexationSynchronizationStrategy {

    private final SessionFactory sessionFactory;
    private final PlatformTransactionManager transactionManager;

    public IndexationSynchronizationStrategy(SessionFactory sessionFactory, PlatformTransactionManager transactionManager) {
        this.sessionFactory = sessionFactory;
        this.transactionManager = transactionManager;
    }

    @Around(value = "@annotation(com.fiducial.signature.annotation.IndexerReadSync)")
    public Object indexReadSync(ProceedingJoinPoint joinPoint) throws Throwable {
        final boolean[] rollbackOuterTrx = {false};
        final Object[] returnedObject = new Object[1];
        final TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        readSyncSynchronizationStrategy();
        transactionTemplate.execute(new TransactionCallback<>() {
            @Override
            public String doInTransaction(TransactionStatus status) {
                try {
                    returnedObject[0] = joinPoint.proceed();
                    return "done";
                } catch (Throwable e) {
                    rollbackOuterTrx[0] = true;
                    status.setRollbackOnly();
                    return "failed";
                }
            }
        });
        defaultSynchronizationStrategy();
        if (rollbackOuterTrx[0]) {
            // trigger rollback programmatically
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

        return returnedObject[0];
    }

    private void readSyncSynchronizationStrategy() {
        SearchSession searchSession = Search.session((EntityManager) sessionFactory.getCurrentSession());
        searchSession.automaticIndexingSynchronizationStrategy(AutomaticIndexingSynchronizationStrategy.sync());
    }

    private void defaultSynchronizationStrategy() {
        SearchSession searchSession = Search.session((EntityManager) sessionFactory.getCurrentSession());
        searchSession.automaticIndexingSynchronizationStrategy(AutomaticIndexingSynchronizationStrategy.writeSync());
    }

And the action method that causes the refresh problem :

@Service("gestionDepotBS")
public class GestionDepotBSImpl extends AllegoriaSecuredService implements GestionDepotBS {

@Override
	@IndexerReadSync
	public HttpStatus annulerDepotDac(Long id) {
		checkIdNotNull(id);
		Depot depot = depotDao.load(id);

		if (depot == null) {
			return HttpStatus.NOT_FOUND;
		}

		depot.setDateEtatRetour(new Date());
		depot.modifierEtatDepot(ChoixEtatDepot.ANNULER_PAR_UTILISATEUR);
		depot.setAnnule(Boolean.TRUE);
		depotDao.update(depot);
		return HttpStatus.OK;
	}
}

through the annotation @IndexerReadSync I trigger the switch/restore the strategy behavior, thanks to the aspect class IndexationSynchronizationStrategy . That’s the idea.
The original method called with joinPoint.proceed(); is done in a inner transaction because the automatic indexing is only triggered with commit. I “merged” the inner transaction to the outer transaction with the code

if (rollbackOuterTrx[0]) {
            // trigger rollback programmatically
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

because the orginal method is already called within a transaction (and I execute it in a inner transaction to provoke a commit statement)

But of course, all this to say that is doesn’t work more : at the screen, the refresh problem still occurs. The new result still contains the deleted single result. The sync strategy seems not working with my aspect ; which is well called (tested with breakpoint). Something is missing, but what ?

Thanks for your help.

Hello,

Sounds good, but you don’t really need to reset to defaults after commit: the strategy is set on the session only, and the session in your case will most likely be bound to the transaction, and will be closed on commit. So, you just don’t need to clean up anything: other sessions won’t be affected by your strategy change, and the only affected session won’t be used anymore after commit.

That’s probably your problem right there: I suspect your creating multiple transactions leads to multiple sessions being used for a single call, and they don’t share the same sync strategy. I’m not 100% sure, but since this transaction stuff looks complicated and (as explained above) is unnecessary, I would get rid of it.

What you need to do is:

  • Simplify your aspect to only call readSyncSynchronizationStrategy, before the method being intercepted. Remove all transaction-related tricks.
  • Make sure your aspect is applied inside the transaction interceptor. E.g. annotate the method with both @Transactional and @IndexerReadSync and somehow tell Spring that the transaction aspect should be applied before yours. A quick search on Google tells me annotating your aspect with @Order(1) should do the trick, or alternatively implementing Ordered.

Or just, you know… Call searchSession.automaticIndexingSynchronizationStrategy directly from the method, and don’t use custom annotations / aspects if they’re making your life harder :slight_smile:


As a side note, you probably want to use AutomaticIndexingSynchronizationStrategy.sync() rather than readSync(), so that you get a refresh and a commit. Technically they are probably equivalent right now with the Elasticsearch backend, since it always “commits” (kind of) and doesn’t provide an option for Hibernate Search to skip that, but that might not stay that way forever. (Also, read-sync is already skipping commits with the Lucene backend).

So it simplifies all the aspect’s implementation if strategy is bound to the session, indeed.
Then I did the final code according to your advice. The final scheme look like this :

Only the two added annotations :

@IndexerSyncStrategy
	@Transactional
	public HttpStatus annulerDepotDac(Long id) {
		checkIdNotNull(id);
		Depot depot = depotDao.load(id);

		if (depot == null) {
			return HttpStatus.NOT_FOUND;
		}

		depot.setDateEtatRetour(new Date());
		depot.modifierEtatDepot(ChoixEtatDepot.ANNULER_PAR_UTILISATEUR);
		depot.setAnnule(Boolean.TRUE);
		depotDao.update(depot);
		return HttpStatus.OK;
	}

The Aspect :

@Aspect
@Order(Ordered.LOWEST_PRECEDENCE)
@Component
public class IndexationSynchronizationStrategy {

    private final SessionFactory sessionFactory;

    public IndexationSynchronizationStrategy(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Before(value = "@annotation(com.fiducial.signature.annotation.IndexerSyncStrategy) && @annotation(javax.transaction.Transactional)")
    public void syncStrategy(JoinPoint jp) throws Throwable {
        SearchSession searchSession = Search.session((EntityManager) sessionFactory.getCurrentSession());
        searchSession.automaticIndexingSynchronizationStrategy(AutomaticIndexingSynchronizationStrategy.sync());
    }
}

The order of precedence :

@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 1)
public class HibernateConfiguration extends AbstractHibernateConfiguration {

By the way, @Transactional has the lowest precedence by default.
I did some test without setting any order on the 2 aspects (@Transactional and @IndexerSyncStrategy) and seems to work ; but I imagine without forces the order, the runtime order might be unpredictable.

You’re right but, since the application is multi-module, dependency is inevitable. The aspect is less code intrusive than a helper method (in addition it includes the dependency of sessionFactory)

Thank you.

1 Like