/** * Copyright 2017 LinkedIn Corp. All rights reserved. * * Licensed 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. */ package com.github.ambry.store; import com.github.ambry.config.StoreConfig; import com.github.ambry.utils.Time; import com.github.ambry.utils.Utils; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Responsible for managing compaction of a {@link BlobStore}. */ class CompactionManager { static final String THREAD_NAME_PREFIX = "StoreCompactionThread-"; private final String mountPath; private final StoreConfig storeConfig; private final Time time; private final Collection<BlobStore> stores; private final CompactionExecutor compactionExecutor; private final StorageManagerMetrics metrics; private final CompactionPolicy compactionPolicy; private final Logger logger = LoggerFactory.getLogger(getClass()); private Thread compactionThread; /** * Creates a CompactionManager that handles scheduling and executing compaction. * @param mountPath the mount path of all the stores for which compaction may be executed. * @param storeConfig the {@link StoreConfig} that contains configurationd details. * @param stores the {@link Collection} of {@link BlobStore} that compaction needs to be executed for. * @param metrics the {@link StorageManagerMetrics} to use. * @param time the {@link Time} instance to use. */ CompactionManager(String mountPath, StoreConfig storeConfig, Collection<BlobStore> stores, StorageManagerMetrics metrics, Time time) { this.mountPath = mountPath; this.storeConfig = storeConfig; this.stores = stores; this.time = time; this.metrics = metrics; compactionExecutor = storeConfig.storeEnableCompaction ? new CompactionExecutor() : null; try { CompactionPolicyFactory compactionPolicyFactory = Utils.getObj(storeConfig.storeCompactionPolicyFactory, storeConfig, time); compactionPolicy = compactionPolicyFactory.getCompactionPolicy(); } catch (Exception e) { throw new IllegalStateException( "Error creating compaction policy using compactionPolicyFactory " + storeConfig.storeCompactionPolicyFactory); } } /** * Enables the compaction manager allowing it execute compactions if required. */ void enable() { if (compactionExecutor != null) { logger.info("Compaction thread started for {}", mountPath); compactionThread = Utils.newThread(THREAD_NAME_PREFIX + mountPath, compactionExecutor, true); compactionThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { logger.error("Thread {} threw exception", t, e); } }); compactionThread.start(); } } /** * Disables the compaction manager which disallows any new compactions. */ void disable() { if (compactionExecutor != null) { compactionExecutor.disable(); } } /** * Awaits the termination of all pending jobs of the compaction manager. */ void awaitTermination() { if (compactionExecutor != null && compactionThread != null) { try { compactionThread.join(2000); } catch (InterruptedException e) { metrics.compactionManagerTerminateErrorCount.inc(); logger.error("Compaction thread join wait for {} was interrupted", mountPath); } } } /** * @return {@code true} if the compaction thread is running. {@code false} otherwise. */ boolean isCompactionExecutorRunning() { return compactionExecutor != null && compactionExecutor.isRunning; } /* * Schedules the given {@code store} for compaction next. * @param store the {@link BlobStore} to compact. * @return {@code true} if the scheduling was successful. {@code false} if not. */ boolean scheduleNextForCompaction(BlobStore store) { return compactionExecutor != null && compactionExecutor.scheduleNextForCompaction(store); } /** /** * Get compaction details for a given {@link BlobStore} if any * @param blobStore the {@link BlobStore} for which compation details are requested * @return the {@link CompactionDetails} containing the details about log segments that needs to be compacted. * {@code null} if compaction is not required * @throws StoreException when {@link BlobStore} is not started */ private CompactionDetails getCompactionDetails(BlobStore blobStore) throws StoreException { return blobStore.getCompactionDetails(compactionPolicy); } /** * A {@link Runnable} that cycles through the stores and executes compaction if required. */ private class CompactionExecutor implements Runnable { private final Logger logger = LoggerFactory.getLogger(getClass()); private final ReentrantLock lock = new ReentrantLock(); private final Condition waitCondition = lock.newCondition(); private final Set<BlobStore> storesToSkip = new HashSet<>(); private final LinkedBlockingDeque<BlobStore> storesToCheck = new LinkedBlockingDeque<>(); private final long waitTimeMs = TimeUnit.HOURS.toMillis(storeConfig.storeCompactionCheckFrequencyInHours); private volatile boolean enabled = true; volatile boolean isRunning = false; /** * Starts by resuming any compactions that were left halfway. In steady state, it cycles through the stores at a * configurable frequency and runs compaction as required. */ @Override public void run() { isRunning = true; try { logger.info("Starting compaction thread for {}", mountPath); // complete any compactions in progress for (BlobStore store : stores) { logger.trace("{} being checked for resume", store); if (store.isStarted()) { logger.trace("{} is started and eligible for resume check", store); metrics.markCompactionStart(false); try { store.maybeResumeCompaction(); } catch (Exception e) { metrics.compactionErrorCount.inc(); logger.error("Compaction of store {} failed on resume. Continuing with the next store", store, e); storesToSkip.add(store); } finally { metrics.markCompactionStop(); } } } // continue to do compactions as required. long expectedNextCheckTime = time.milliseconds() + waitTimeMs; storesToCheck.addAll(stores); while (enabled) { try { while (enabled && storesToCheck.peek() != null) { BlobStore store = storesToCheck.poll(); logger.trace("{} being checked for compaction", store); boolean compactionStarted = false; try { if (store.isStarted() && !storesToSkip.contains(store)) { logger.trace("{} is started and eligible for compaction check", store); CompactionDetails details = getCompactionDetails(store); if (details != null) { logger.trace("Generated {} as details for {}", details, store); metrics.markCompactionStart(true); compactionStarted = true; store.compact(details); } } } catch (Exception e) { metrics.compactionErrorCount.inc(); logger.error("Compaction of store {} failed. Continuing with the next store", store, e); storesToSkip.add(store); } finally { if (compactionStarted) { metrics.markCompactionStop(); } } } lock.lock(); try { if (enabled) { if (storesToCheck.peek() == null) { long actualWaitTimeMs = expectedNextCheckTime - time.milliseconds(); logger.trace("Going to wait for {} ms in compaction thread at {}", actualWaitTimeMs, mountPath); time.await(waitCondition, actualWaitTimeMs); } if (time.milliseconds() >= expectedNextCheckTime) { expectedNextCheckTime = time.milliseconds() + waitTimeMs; storesToCheck.addAll(stores); } } } finally { lock.unlock(); } } catch (Exception e) { metrics.compactionExecutorErrorCount.inc(); logger.error("Compaction executor for {} encountered an error. Continuing", mountPath, e); } } } finally { isRunning = false; logger.info("Stopping compaction thread for {}", mountPath); } } /** * Disables the executor by disallowing scheduling of any new compaction jobs. */ void disable() { lock.lock(); try { enabled = false; waitCondition.signal(); } finally { lock.unlock(); } logger.info("Disabled compaction thread for {}", mountPath); } /** * Schedules the given {@code store} for compaction next. * @param store the {@link BlobStore} to compact. * @return {@code true} if the scheduling was successful. {@code false} if not. */ boolean scheduleNextForCompaction(BlobStore store) { if (!enabled || !store.isStarted() || storesToSkip.contains(store)) { return false; } lock.lock(); try { storesToCheck.addFirst(store); waitCondition.signal(); logger.info("Scheduled {} for compaction"); } finally { lock.unlock(); } return true; } } }