All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.druid.server.lookup.namespace.cache.CacheScheduler Maven / Gradle / Ivy

There is a newer version: 0.12.3
Show newest version
/*
 * Licensed to Metamarkets Group Inc. (Metamarkets) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Metamarkets licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package io.druid.server.lookup.namespace.cache;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.inject.Inject;
import io.druid.concurrent.ConcurrentAwaitableCounter;
import io.druid.java.util.emitter.service.ServiceEmitter;
import io.druid.java.util.emitter.service.ServiceMetricEvent;
import io.druid.guice.LazySingleton;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.query.lookup.namespace.CacheGenerator;
import io.druid.query.lookup.namespace.ExtractionNamespace;
import sun.misc.Cleaner;

import javax.annotation.Nullable;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Usage:
 * 
{@code
 * CacheScheduler.Entry entry = cacheScheduler.schedule(namespace); // or scheduleAndWait(namespace, timeout)
 * CacheState cacheState = entry.getCacheState();
 * // cacheState could be either NoCache or VersionedCache.
 * if (cacheState instanceof NoCache) {
 *   // the cache is not yet created, or already closed
 * } else {
 *   Map cache = ((VersionedCache) cacheState).getCache(); // use the cache
 *   // Although VersionedCache implements AutoCloseable, versionedCache shouldn't be manually closed
 *   // when obtained from entry.getCacheState(). If the namespace updates should be ceased completely,
 *   // entry.close() (see below) should be called, it will close the last VersionedCache as well.
 *   // On scheduled updates, outdated VersionedCaches are also closed automatically.
 * }
 * ...
 * entry.close(); // close the last VersionedCache and unschedule future updates
 * }
*/ @LazySingleton public final class CacheScheduler { private static final Logger log = new Logger(CacheScheduler.class); public final class Entry implements AutoCloseable { private final EntryImpl impl; private Entry(final T namespace, final CacheGenerator cacheGenerator) { impl = new EntryImpl<>(namespace, this, cacheGenerator); } /** * Returns the last cache state, either {@link NoCache} or {@link VersionedCache}. */ public CacheState getCacheState() { return impl.cacheStateHolder.get(); } /** * @return the entry's cache if it is already initialized and not yet closed * @throws IllegalStateException if the entry's cache is not yet initialized, or {@link #close()} has * already been called */ public Map getCache() { CacheState cacheState = getCacheState(); if (cacheState instanceof VersionedCache) { return ((VersionedCache) cacheState).getCache(); } else { throw new ISE("Cannot get cache: %s", cacheState); } } @VisibleForTesting Future getUpdaterFuture() { return impl.updaterFuture; } @VisibleForTesting public void awaitTotalUpdates(int totalUpdates) throws InterruptedException { impl.updateCounter.awaitCount(totalUpdates); } @VisibleForTesting void awaitNextUpdates(int nextUpdates) throws InterruptedException { impl.updateCounter.awaitNextIncrements(nextUpdates); } /** * Close the last {@link #getCacheState()}, if it is {@link VersionedCache}, and unschedule future updates. */ @Override public void close() { impl.close(); } @Override public String toString() { return impl.toString(); } } /** * This class effectively contains the whole state and most of the logic of {@link Entry}, need to be a separate class * because the Entry must not be referenced from the runnable executed in {@link #cacheManager}'s ExecutorService, * that would be a leak preventing the Entry to be collected by GC, and therefore {@link #entryCleaner} to be run by * the JVM. Also, {@link #entryCleaner} must not reference the Entry through it's Runnable hunk. */ public class EntryImpl implements AutoCloseable { private final T namespace; private final String asString; private final AtomicReference cacheStateHolder = new AtomicReference(NoCache.CACHE_NOT_INITIALIZED); private final Future updaterFuture; private final Cleaner entryCleaner; private final CacheGenerator cacheGenerator; private final ConcurrentAwaitableCounter updateCounter = new ConcurrentAwaitableCounter(); private final CountDownLatch startLatch = new CountDownLatch(1); private EntryImpl(final T namespace, final Entry entry, final CacheGenerator cacheGenerator) { try { this.namespace = namespace; this.asString = StringUtils.format("namespace [%s] : %s", namespace, super.toString()); this.updaterFuture = schedule(namespace); this.entryCleaner = createCleaner(entry); this.cacheGenerator = cacheGenerator; activeEntries.incrementAndGet(); } finally { startLatch.countDown(); } } private Cleaner createCleaner(Entry entry) { return Cleaner.create(entry, new Runnable() { @Override public void run() { closeFromCleaner(); } }); } private Future schedule(final T namespace) { final long updateMs = namespace.getPollMs(); Runnable command = new Runnable() { @Override public void run() { updateCache(); } }; if (updateMs > 0) { return cacheManager.scheduledExecutorService().scheduleAtFixedRate(command, 0, updateMs, TimeUnit.MILLISECONDS); } else { return cacheManager.scheduledExecutorService().schedule(command, 0, TimeUnit.MILLISECONDS); } } private void updateCache() { try { // Ensures visibility of the whole EntryImpl's state (fields and their state). startLatch.await(); CacheState currentCacheState = cacheStateHolder.get(); if (!Thread.currentThread().isInterrupted() && currentCacheState != NoCache.ENTRY_CLOSED) { final String currentVersion = currentVersionOrNull(currentCacheState); tryUpdateCache(currentVersion); } } catch (Throwable t) { try { close(); } catch (Exception e) { t.addSuppressed(e); } if (Thread.currentThread().isInterrupted() || t instanceof InterruptedException || t instanceof Error) { throw Throwables.propagate(t); } } } private void tryUpdateCache(String currentVersion) throws Exception { boolean updatedCacheSuccessfully = false; VersionedCache newVersionedCache = null; try { newVersionedCache = cacheGenerator.generateCache(namespace, this, currentVersion, CacheScheduler.this ); if (newVersionedCache != null) { CacheState previousCacheState = swapCacheState(newVersionedCache); if (previousCacheState != NoCache.ENTRY_CLOSED) { updatedCacheSuccessfully = true; if (previousCacheState instanceof VersionedCache) { ((VersionedCache) previousCacheState).close(); } log.debug("%s: the cache was successfully updated", this); } else { newVersionedCache.close(); log.debug("%s was closed while the cache was being updated, discarding the update", this); } } else { log.debug("%s: Version `%s` not updated, the cache is not updated", this, currentVersion); } } catch (Throwable t) { try { if (newVersionedCache != null && !updatedCacheSuccessfully) { newVersionedCache.close(); } log.error(t, "Failed to update %s", this); } catch (Exception e) { t.addSuppressed(e); } if (Thread.currentThread().isInterrupted() || t instanceof InterruptedException || t instanceof Error) { // propagate to the catch block in updateCache() throw t; } } } private String currentVersionOrNull(CacheState currentCacheState) { if (currentCacheState instanceof VersionedCache) { return ((VersionedCache) currentCacheState).version; } else { return null; } } private CacheState swapCacheState(VersionedCache newVersionedCache) { CacheState lastCacheState; // CAS loop do { lastCacheState = cacheStateHolder.get(); if (lastCacheState == NoCache.ENTRY_CLOSED) { return lastCacheState; } } while (!cacheStateHolder.compareAndSet(lastCacheState, newVersionedCache)); updateCounter.increment(); return lastCacheState; } @Override public void close() { if (!doClose(true)) { log.error("Cache for %s has already been closed", this); } // This Cleaner.clean() call effectively just removes the Cleaner from the internal linked list of all cleaners. // It will delegate to closeFromCleaner() which will be a no-op because cacheStateHolder is already set to // ENTRY_CLOSED. entryCleaner.clean(); } private void closeFromCleaner() { try { if (doClose(false)) { log.error("Entry.close() was not called, closed resources by the JVM"); } } catch (Throwable t) { try { log.error(t, "Error while closing %s", this); } catch (Exception e) { t.addSuppressed(e); } Throwables.propagateIfInstanceOf(t, Error.class); // Must not throw exceptions in the cleaner thread, run by the JVM. } } /** * @param calledManually true if called manually from {@link #close()}, false if called by the JVM via Cleaner * @return true if successfully closed, false if has already closed before */ private boolean doClose(boolean calledManually) { CacheState lastCacheState = cacheStateHolder.getAndSet(NoCache.ENTRY_CLOSED); if (lastCacheState != NoCache.ENTRY_CLOSED) { try { log.info("Closing %s", this); logExecutionError(); } // Logging (above) is not the main goal of the closing process, so try to cancel the updaterFuture even if // logging failed for whatever reason. finally { activeEntries.decrementAndGet(); updaterFuture.cancel(true); // If calledManually = false, i. e. called by the JVM via Cleaner.clean(), let the JVM close cache itself // via it's own Cleaner as well, when the cache becomes unreachable. Because when somebody forgets to call // entry.close(), it may be harmful to forcibly close the cache, which could still be used, at some // non-deterministic point of time. Cleaners are introduced to mitigate possible errors, not to escalate them. if (calledManually && lastCacheState instanceof VersionedCache) { ((VersionedCache) lastCacheState).cacheHandler.close(); } } return true; } else { return false; } } private void logExecutionError() { if (updaterFuture.isDone()) { try { updaterFuture.get(); } catch (ExecutionException ee) { log.error(ee.getCause(), "Error in %s", this); } catch (CancellationException ce) { log.error(ce, "Future for %s has already been cancelled", this); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException(ie); } } } @Override public String toString() { return asString; } } public interface CacheState {} public enum NoCache implements CacheState { CACHE_NOT_INITIALIZED, ENTRY_CLOSED } public final class VersionedCache implements CacheState, AutoCloseable { final String entryId; final CacheHandler cacheHandler; final String version; private VersionedCache(String entryId, String version) { this.entryId = entryId; this.cacheHandler = cacheManager.createCache(); this.version = version; } public Map getCache() { return cacheHandler.getCache(); } public String getVersion() { return version; } @Override public void close() { cacheHandler.close(); // Log statement after cacheHandler.close(), because logging may fail (e. g. in shutdown hooks) log.debug("Closed version [%s] of %s", version, entryId); } } private final Map, CacheGenerator> namespaceGeneratorMap; private final NamespaceExtractionCacheManager cacheManager; private final AtomicLong updatesStarted = new AtomicLong(0); private final AtomicInteger activeEntries = new AtomicInteger(); @Inject public CacheScheduler( final ServiceEmitter serviceEmitter, final Map, CacheGenerator> namespaceGeneratorMap, NamespaceExtractionCacheManager cacheManager ) { this.namespaceGeneratorMap = namespaceGeneratorMap; this.cacheManager = cacheManager; cacheManager.scheduledExecutorService().scheduleAtFixedRate( new Runnable() { long priorUpdatesStarted = 0L; @Override public void run() { try { final long tasks = updatesStarted.get(); serviceEmitter.emit( ServiceMetricEvent.builder() .build("namespace/deltaTasksStarted", tasks - priorUpdatesStarted) ); priorUpdatesStarted = tasks; } catch (Exception e) { log.error(e, "Error emitting namespace stats"); if (Thread.currentThread().isInterrupted()) { throw Throwables.propagate(e); } } } }, 1, 10, TimeUnit.MINUTES ); } /** * This method should be used from {@link CacheGenerator#generateCache} implementations, to obtain a {@link * VersionedCache} to be returned. * * @param entryId an object uniquely corresponding to the {@link CacheScheduler.Entry}, for which VersionedCache is * created * @param version version, associated with the cache */ public VersionedCache createVersionedCache(@Nullable EntryImpl entryId, String version) { updatesStarted.incrementAndGet(); return new VersionedCache(String.valueOf(entryId), version); } @VisibleForTesting long updatesStarted() { return updatesStarted.get(); } @VisibleForTesting public long getActiveEntries() { return activeEntries.get(); } @Nullable public Entry scheduleAndWait(ExtractionNamespace namespace, long waitForFirstRunMs) throws InterruptedException { final Entry entry = schedule(namespace); log.debug("Scheduled new %s", entry); boolean success = false; try { success = entry.impl.updateCounter.awaitFirstIncrement(waitForFirstRunMs, TimeUnit.MILLISECONDS); if (success) { return entry; } else { return null; } } finally { if (!success) { // ExecutionException's cause is logged in entry.close() entry.close(); log.error("CacheScheduler[%s] - problem during start or waiting for the first run", entry); } } } public Entry schedule(final T namespace) { final CacheGenerator generator = (CacheGenerator) namespaceGeneratorMap.get(namespace.getClass()); if (generator == null) { throw new ISE("Cannot find generator for namespace [%s]", namespace); } return new Entry<>(namespace, generator); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy