/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.importer; import static com.google_voltpatches.common.base.Predicates.not; import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import org.voltcore.logging.VoltLogger; import org.voltdb.importer.formatter.FormatterBuilder; import com.google_voltpatches.common.base.Joiner; import com.google_voltpatches.common.base.Predicate; import com.google_voltpatches.common.collect.ImmutableMap; import com.google_voltpatches.common.collect.Maps; import com.google_voltpatches.common.util.concurrent.ListeningExecutorService; import com.google_voltpatches.common.util.concurrent.MoreExecutors; /** * This class is responsible for receiving notifications from the server and * starting or stopping the importer instances accordingly. There will be a * manager instance per importer type/bundle. */ public class ImporterLifeCycleManager implements ChannelChangeCallback { private final static VoltLogger s_logger = new VoltLogger("ImporterTypeManager"); public static final int MEDIUM_STACK_SIZE = 1024 * 512; private final AbstractImporterFactory m_factory; private ListeningExecutorService m_executorService; private ImmutableMap<URI, ImporterConfig> m_configs = ImmutableMap.of(); private AtomicReference<ImmutableMap<URI, AbstractImporter>> m_importers = new AtomicReference<>(ImmutableMap.<URI, AbstractImporter> of()); private volatile boolean m_stopping; private final AtomicBoolean m_starting = new AtomicBoolean(false); // Safe to keep reference here as there is only and it is not susceptible to catalog changes private final ChannelDistributer m_distributer; private final String m_distributerDesignation; public ImporterLifeCycleManager( AbstractImporterFactory factory, final ChannelDistributer distributer, String clusterTag) { m_factory = factory; m_distributer = distributer; m_distributerDesignation = m_factory.getTypeName() + "_" + clusterTag; } /** * This will be called for every importer configuration section for this importer type. * * @param props Properties defined in a configuration section for this importer */ public final void configure(Properties props, FormatterBuilder formatterBuilder) { Map<URI, ImporterConfig> configs = m_factory.createImporterConfigurations(props, formatterBuilder); m_configs = new ImmutableMap.Builder<URI, ImporterConfig>() .putAll(configs) .putAll(Maps.filterKeys(m_configs, not(in(configs.keySet())))) .build(); } public final int getConfigsCount() { return m_configs.size(); } /** * This method is used by the framework to indicate that the importers must be started now. * This implementation starts the required number of threads based on the number of resources * configured for this importer type. * <p>For importers that must be run on every site, this will also call * <code>accept()</code>. * For importers that must not be run on every site, this will register itself with the * resource distributer. */ public final void readyForData() { m_starting.compareAndSet(false, true); if (m_stopping) return; if (m_executorService != null) { // Should be caused by coding error. Generic RuntimeException is OK throw new RuntimeException("Importer has already been started and is running"); } if (m_configs.size()==0) { s_logger.info("No configured importers of " + m_factory.getTypeName() + " are ready to be started at this time"); return; } ThreadPoolExecutor tpe = new ThreadPoolExecutor( m_configs.size(), m_configs.size(), 5_000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), getThreadFactory(m_factory.getTypeName(), MEDIUM_STACK_SIZE) ); tpe.allowCoreThreadTimeOut(true); m_executorService = MoreExecutors.listeningDecorator(tpe); if (m_factory.isImporterRunEveryWhere()) { ImmutableMap.Builder<URI, AbstractImporter> builder = new ImmutableMap.Builder<>(); for (final ImporterConfig config : m_configs.values()) { AbstractImporter importer = m_factory.createImporter(config); builder.put(importer.getResourceID(), importer); } m_importers.set(builder.build()); startImporters(m_importers.get().values()); } else { m_importers.set(ImmutableMap.<URI, AbstractImporter> of()); m_distributer.registerCallback(m_distributerDesignation, this); m_distributer.registerChannels(m_distributerDesignation, m_configs.keySet()); } } private void startImporters(Collection<AbstractImporter> importers) { for (AbstractImporter importer : importers) { submitAccept(importer); } } /** * Callback method used by resource distributer to allocate/deallocate resources. * Stop will be called for resources that are removed from assignment list for this node. * Accept with be called in its own execution thread for resources that are added for this node. */ @Override public final void onChange(ImporterChannelAssignment assignment) { if (m_stopping && !assignment.getAdded().isEmpty()) { String msg = "Received an a channel assignment when the importer is stopping: " + assignment; s_logger.warn(msg); throw new IllegalStateException(msg); } if (m_stopping) { return; } ImmutableMap<URI, AbstractImporter> oldReference = m_importers.get(); ImmutableMap.Builder<URI, AbstractImporter> builder = new ImmutableMap.Builder<>(); builder.putAll(Maps.filterKeys(oldReference, notUriIn(assignment.getRemoved()))); List<AbstractImporter> toStop = new ArrayList<>(); List<String> missingRemovedURLs = new ArrayList<>(); List<String> missingAddedURLs = new ArrayList<>(); for (URI removed: assignment.getRemoved()) { if (m_configs.containsKey(removed)) { AbstractImporter importer = oldReference.get(removed); if (importer != null) { toStop.add(importer); } } else { missingRemovedURLs.add(removed.toString()); } } List<AbstractImporter> newImporters = new ArrayList<>(); for (final URI added: assignment.getAdded()) { if (m_configs.containsKey(added)) { AbstractImporter importer = m_factory.createImporter(m_configs.get(added)); newImporters.add(importer); builder.put(added, importer); } else { missingAddedURLs.add(added.toString()); } } if (!missingRemovedURLs.isEmpty() || !missingAddedURLs.isEmpty()) { s_logger.error("The source for Import has changed its configuration. Removed importer URL(s): (" + Joiner.on(", ").join(missingRemovedURLs) + "), added importer URL(s): (" + Joiner.on(", ").join(missingAddedURLs) + "). Pause and Resume the database to refresh the importer."); } ImmutableMap<URI, AbstractImporter> newReference = builder.build(); boolean success = m_importers.compareAndSet(oldReference, newReference); if (!m_stopping && success) { // Could fail if stop was called after we entered inside this method stopImporters(toStop); startImporters(newImporters); } } private final static Predicate<URI> notUriIn(final Set<URI> uris) { return new Predicate<URI>() { @Override final public boolean apply(URI uri) { return !uris.contains(uri); } }; } private void submitAccept(final AbstractImporter importer) { m_executorService.submit(() -> { try { final String thName = importer.getTaskThreadName(); if (thName != null) { Thread.currentThread().setName(thName); } importer.accept(); } catch(Throwable e) { s_logger.error( String.format("Error calling accept for importer %s", m_factory.getTypeName()), e); } }); } @Override public void onClusterStateChange(VersionedOperationMode mode) { if (s_logger.isDebugEnabled()) { s_logger.debug(m_factory.getTypeName() + ".onChange"); } } /** * This is called by the importer framework to stop importers. * All resources for this importer will be unregistered * from the resource distributer. */ public final void stop() { m_stopping = true; ImmutableMap<URI, AbstractImporter> oldReference; boolean success = false; do { // onChange also could set m_importers. Use while loop to pick up latest ref oldReference = m_importers.get(); success = m_importers.compareAndSet(oldReference, ImmutableMap.<URI, AbstractImporter> of()); } while (!success); if (!m_starting.get()) return; stopImporters(oldReference.values()); if (!m_factory.isImporterRunEveryWhere()) { m_distributer.registerChannels(m_distributerDesignation, Collections.<URI> emptySet()); m_distributer.unregisterCallback(m_distributerDesignation); } if (m_executorService != null) { m_executorService.shutdown(); try { m_executorService.awaitTermination(365, TimeUnit.DAYS); } catch (InterruptedException ex) { //Should never come here. s_logger.warn("Unexpected interrupted exception waiting for " + m_factory.getTypeName() + " to shutdown", ex); } } } private void stopImporters(Collection<AbstractImporter> importers) { for (AbstractImporter importer : importers) { try { importer.stopImporter(); } catch(Exception e) { s_logger.warn("Error trying to stop importer resource ID " + importer.getResourceID(), e); } } } private ThreadFactory getThreadFactory(final String groupName, final int stackSize) { final ThreadGroup group = new ThreadGroup(Thread.currentThread().getThreadGroup(), groupName); return new ThreadFactory() { private final AtomicLong m_createdThreadCount = new AtomicLong(0); @Override public synchronized Thread newThread(final Runnable r) { final String threadName = groupName + " - " + m_createdThreadCount.getAndIncrement(); Thread t = new Thread(group, r, threadName, stackSize); t.setDaemon(true); return t; } }; } public final static <T> Predicate<T> in(final Set<T> set) { return new Predicate<T>() { @Override public boolean apply(T m) { return set.contains(m); } }; } }