package org.infinispan.query.affinity; import static org.infinispan.factories.KnownComponentNames.ASYNC_OPERATIONS_EXECUTOR; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.search.similarities.Similarity; import org.hibernate.search.backend.BackendFactory; import org.hibernate.search.backend.IndexingMonitor; import org.hibernate.search.backend.LuceneWork; import org.hibernate.search.engine.service.spi.ServiceManager; import org.hibernate.search.indexes.spi.DirectoryBasedIndexManager; import org.hibernate.search.spi.SearchIntegrator; import org.hibernate.search.spi.WorkerBuildContext; import org.hibernate.search.store.DirectoryProvider; import org.infinispan.Cache; import org.infinispan.factories.ComponentRegistry; import org.infinispan.hibernate.search.spi.InfinispanDirectoryProvider; import org.infinispan.notifications.Listener; import org.infinispan.notifications.Listener.Observation; import org.infinispan.notifications.cachelistener.annotation.TopologyChanged; import org.infinispan.notifications.cachelistener.event.TopologyChangedEvent; import org.infinispan.query.backend.ComponentRegistryService; import org.infinispan.query.backend.KeyTransformationHandler; import org.infinispan.query.backend.QueryInterceptor; import org.infinispan.query.backend.TransactionHelper; import org.infinispan.query.logging.Log; import org.infinispan.remoting.rpc.RpcManager; import org.infinispan.remoting.transport.Address; import org.infinispan.remoting.transport.LocalModeAddress; import org.infinispan.util.logging.LogFactory; /** * {@link org.hibernate.search.indexes.spi.IndexManager} that splits the index into shards. * * @author gustavonalle * @since 8.2 */ @Listener(observation = Observation.POST) public class AffinityIndexManager extends DirectoryBasedIndexManager { private static final Log log = LogFactory.getLog(AffinityIndexManager.class, Log.class); private static final long POLL_WAIT = 1000L; private KeyTransformationHandler keyTransformationHandler; private Cache<?, ?> cache; private final ReadWriteLock flushLock = new ReentrantReadWriteLock(); private final Lock writeLock = flushLock.writeLock(); private final Lock readLock = flushLock.readLock(); private ShardAllocatorManager shardAllocatorManager; private TransactionHelper transactionHelper; private SearchIntegrator searchIntegrator; private String shardId; private boolean isAsync; private ShardAddress localShardAddress; private LuceneWorkDispatcher luceneWorkDispatcher; private WorkPartitioner workPartitioner; @Override public void initialize(String indexName, Properties properties, Similarity similarity, WorkerBuildContext buildContext) { ServiceManager serviceManager = buildContext.getServiceManager(); ComponentRegistryService componentRegistryService = serviceManager.requestService(ComponentRegistryService.class); ComponentRegistry componentRegistry = componentRegistryService.getComponentRegistry(); transactionHelper = new TransactionHelper(componentRegistry.getComponent(TransactionManager.class)); shardId = this.extractShardName(indexName); Transaction tx = transactionHelper.suspendTxIfExists(); try { super.initialize(indexName, properties, similarity, buildContext); } finally { transactionHelper.resume(tx); } RpcManager rpcManager = componentRegistry.getComponent(RpcManager.class); cache = componentRegistry.getComponent(Cache.class); keyTransformationHandler = componentRegistry.getComponent(QueryInterceptor.class).getKeyTransformationHandler(); shardAllocatorManager = componentRegistry.getComponent(ShardAllocatorManager.class); searchIntegrator = componentRegistry.getComponent(SearchIntegrator.class); isAsync = !BackendFactory.isConfiguredAsSync(properties); localShardAddress = new ShardAddress(shardId, rpcManager != null ? rpcManager.getAddress() : LocalModeAddress.INSTANCE); ExecutorService asyncExecutor = componentRegistry.getComponent(ExecutorService.class, ASYNC_OPERATIONS_EXECUTOR); luceneWorkDispatcher = new LuceneWorkDispatcher(this, rpcManager); workPartitioner = new WorkPartitioner(this, shardAllocatorManager); AffinityErrorHandler errorHandler = (AffinityErrorHandler) searchIntegrator.getErrorHandler(); errorHandler.initialize(rpcManager, asyncExecutor); cache.addListener(this); } private void handleOwnershipLost() { writeLock.lock(); try { log.debugf("Ownership of %s lost to '%s', closing index manager", this.getIndexName(), shardAllocatorManager.getOwner(String.valueOf(shardId))); this.flushAndReleaseResources(); } finally { writeLock.unlock(); } } @Override public void flushAndReleaseResources() { InfinispanDirectoryProvider directoryProvider = (InfinispanDirectoryProvider) this.getDirectoryProvider(); int activeDeleteTasks = directoryProvider.pendingDeleteTasks(); boolean wasInterrupted = false; long endTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(POLL_WAIT); while (activeDeleteTasks > 0 && endTime - System.nanoTime() > 0) { try { Thread.sleep(10); log.debugf("Waiting for pending delete tasks, remaining: %s", activeDeleteTasks); activeDeleteTasks = directoryProvider.pendingDeleteTasks(); } catch (InterruptedException ignored) { wasInterrupted = true; } } if (wasInterrupted) { Thread.currentThread().interrupt(); } log.debugf("Flushing directory provider at %s on %s", this.getIndexName(), localShardAddress); super.flushAndReleaseResources(); } Object stringToKey(String key) { return keyTransformationHandler.stringToKey(key, cache.getAdvancedCache().getClassLoader()); } Address getLockHolder(String indexName, String affinityId) { log.debugf("Getting lock holder for %s", indexName); Transaction tx = transactionHelper.suspendTxIfExists(); try { InfinispanDirectoryProvider directoryProvider = (InfinispanDirectoryProvider) this.getDirectoryProvider(); return directoryProvider.getLockOwner(indexName, Integer.valueOf(affinityId), IndexWriter.WRITE_LOCK_NAME); } finally { transactionHelper.resume(tx); } } Address getLockHolder() { return this.getLockHolder(this.getIndexName(), shardId); } @Override public void performOperations(List<LuceneWork> workList, IndexingMonitor monitor) { this.performOperations(workList, monitor, true, false); } private void checkOwnership() { log.debugf("Checking ownership at %s", localShardAddress); Address primaryOwner = shardAllocatorManager.getOwner(shardId); if (!localShardAddress.getAddress().equals(primaryOwner)) { log.debugf("%s is not owner of %s anymore, releasing resources", localShardAddress, this.getIndexName()); this.handleOwnershipLost(); } } void performOperations(List<LuceneWork> workList, IndexingMonitor monitor, boolean originLocal, boolean isRetry) { if (this.cache.getCacheConfiguration().clustering().cacheMode().isClustered()) { Map<ShardAddress, List<LuceneWork>> workByAddress = workPartitioner.partitionWorkByAddress(workList, originLocal, isRetry); readLock.lock(); try { log.debugf("Applying work @ %s, workMap is %s", localShardAddress, workByAddress); List<LuceneWork> localWork = workByAddress.get(localShardAddress); if (localWork != null && !localWork.isEmpty()) { log.debugf("About to apply local work %s (index %s) at %s", localWork, this.getIndexName(), localShardAddress); super.performOperations(localWork, monitor); log.debugf("Work %s applied at %s", localWork, localShardAddress); workByAddress.remove(localShardAddress); } } finally { readLock.unlock(); } workByAddress.entrySet().forEach(entry -> luceneWorkDispatcher.dispatch(entry.getValue(), entry.getKey(), originLocal)); if (isRetry || !originLocal) { this.checkOwnership(); } } else { super.performOperations(workList, monitor); } } private String extractShardName(String indexName) { int idx = indexName.lastIndexOf('.'); return idx == -1 ? "0" : indexName.substring(idx + 1); } @Override protected DirectoryProvider<?> createDirectoryProvider(String indexName, Properties cfg, WorkerBuildContext buildContext) { InfinispanDirectoryProvider directoryProvider = new InfinispanDirectoryProvider(Integer.valueOf(shardId)); directoryProvider.initialize(indexName, cfg, buildContext); return directoryProvider; } ShardAddress getLocalShardAddress() { return localShardAddress; } KeyTransformationHandler getKeyTransformationHandler() { return keyTransformationHandler; } String getCacheName() { return cache.getName(); } boolean isAsync() { return isAsync; } SearchIntegrator getSearchIntegrator() { return searchIntegrator; } @TopologyChanged @SuppressWarnings("unused") public void onTopologyChange(TopologyChangedEvent<?, ?> tce) { log.debugf("Topology changed notification for %s: %s", this.getIndexName(), tce); boolean ownershipChanged = shardAllocatorManager.isOwnershipChanged(tce, this.getIndexName()); log.debugf("Ownership changed? %s,", ownershipChanged); if (ownershipChanged) { this.handleOwnershipLost(); } } }