blob: d4dde3047eb5cbd0464e35d9a8ffb7fff62c0361 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 org.apache.drill.exec.store;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.apache.drill.common.config.ConfigConstants;
import org.apache.drill.common.exceptions.DrillRuntimeException;
import org.apache.drill.common.logical.FormatPluginConfig;
import org.apache.drill.common.logical.StoragePluginConfig;
import org.apache.drill.common.scanner.ClassPathScanner;
import org.apache.drill.exec.ExecConstants;
import org.apache.drill.exec.planner.logical.StoragePlugins;
import org.apache.drill.exec.store.dfs.FileSystemConfig;
import org.apache.drill.exec.util.ActionOnFile;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Loads the set of bootstrap plugin configurations for new systems.
* Also handles upgrades to new and existing systems. Each set is given
* by a Jackson-serialized JSON file with the list of plugin (configurations).
* <p>
* The files are given via indirections in the config system to allow easier
* testing when "non-stock" configurations.
*
* <h4>Bootstrap Files</h4>
*
* When a new system starts (boots), Drill populates the persistent store
* with a "starter" (bootstrap) set of storage plugin configurations as
* provided by each connector locator. The classic locator uses one or
* more bootstrap files, serialized to JSON, and visible at the root of the
* class path. Each plugin implementation, if packaged in its own project,
* can provide a bootstrap file ({@code bootstrap-storage-plugins.json}).
* <p>
* The core plugins share a single file in the exec module which has a list of
* storage plugins used as default on fresh start up. Then we have contrib module
* where some plugins and formats reside. We want them to be included in default
* plugins as well during fresh start up which is done by reading each bootstrap
* file in the classpath.
*
* <h4>Format Plugin Bootstrap</h4>
*
* All works fine for the plugins, since each plugin also has a
* {@code bootstrap-storage-plugins.json} loaded via this process. However, format
* plugins are defined <i>inside</i> storage plugins. This presents a problem when
* the format is defined outside of the core (such as in the contrib module). In
* this case, when new format was added, the developer had to modify the
* {@code bootstrap-storage-plugins.json} in exec module to ensure that the new format
* was included.
* <p>
* The {@code bootstrap-format-plugins.json} solves this problem: it allows non-core
* modules to include formats during bootstrap. The file must still indicate which
* plugin (config) to modify, and must list the same format for each supported file
* system plugin such as {@code dfs} and {@ cp}. Despite this limitation, the additional
* file makes the non-core format plugins independent of the core files.
* <p>
* The resulting bootstrap process is to 1) gather the storage plugins, and 2)
* merge any format bootstrap into these plugins.
*
* <h4>Upgrade Files</h4>
*
* Bootstrap files populate a new system. There are times when a user upgrades
* their Drill install and we would like to push out configurations for newly
* added plugins. This is done via the optional upgrade file.
* <p>
* If the system is new, configurations from the upgrade file replace
* (override) values from the bootstrap file. The net effect, bootstrap plus
* upgrades, are written into the persistent store.
* <p>
* If the system is upgraded, then the current technique uses a highly
* unreliable system: an upgrade file exists. Any upgrades are applied on top
* of the user's stored values. The upgrade file is then deleted to avoid
* applying the same upgrades multiple times. Not idea, but it is what it
* is for now.
*
* <h4>Format Plugins</h4>
*
* Earlier versions of this code provided support for format plugins. However,
* this code never worked. The original design of Drill has format plugins
* as a set of attributes of the file system plugin config. Additional work
* is needed to allow file system configs to share plugins. Such work is a
* good idea, so the ill-fated format plugin code still appears, but does
* nothing.
*
* <h4>Config Properties</h4>
*
* <dl>
* <dt>{@code ExecConstants.BOOTSTRAP_STORAGE_PLUGINS_FILE}
* ({@code drill.exec.storage.bootstrap.storage})</dt>
* <dd>The name of the bootstrap file, normally
* {@code bootstrap-storage-plugins.json}.</dd>
* <dt>{@code ExecConstants.UPGRADE_STORAGE_PLUGINS_FILE}
* ({@code drill.exec.storage.upgrade.storage})</dt>
* <dd>The name of the upgrade file, normally
* {@code storage-plugins-override.conf}. Unlike th bootstrap
* file, only one upgrade file can appear in the class path.
* The upgrade file is optional.</dd>
* </dl>
*/
public class PluginBootstrapLoaderImpl implements PluginBootstrapLoader {
private static final Logger logger = LoggerFactory.getLogger(PluginBootstrapLoaderImpl.class);
private final PluginRegistryContext context;
/**
* The file read the first time a Drillbit starts up, and deleted
* afterwards. A poor-man's way of handling upgrades. Will be
* non-null when an upgrade is needed, null thereafter until a
* new release.
*/
private URL pluginsOverrideFileUrl;
public PluginBootstrapLoaderImpl(PluginRegistryContext context) {
this.context = context;
}
@Override
public StoragePlugins bootstrapPlugins() throws IOException {
Map<String, URL> pluginURLMap = new HashMap<>();
StoragePlugins bootstrapPlugins = loadBootstrapPlugins(pluginURLMap);
// Upgrade the bootstrap plugins with any updates. Seems odd,
// but this is how Drill 1.17 works, so keeping the code.
// Uses the enabled status from the updates if different than
// the bootstrap version.
StoragePlugins updatedPlugins = updatedPlugins();
if (updatedPlugins != null) {
bootstrapPlugins.putAll(updatedPlugins);
}
applyFormatPlugins(bootstrapPlugins, pluginURLMap);
return bootstrapPlugins;
}
@VisibleForTesting
protected StoragePlugins loadBootstrapPlugins(Map<String, URL> pluginURLMap) throws IOException {
// bootstrap load the config since no plugins are stored.
String storageBootstrapFileName = context.config().getString(
ExecConstants.BOOTSTRAP_STORAGE_PLUGINS_FILE);
Set<URL> storageUrls = ClassPathScanner.forResource(storageBootstrapFileName, false);
if (storageUrls == null || storageUrls.isEmpty()) {
throw new IOException("Cannot find storage plugin boostrap file: " + storageBootstrapFileName);
}
logger.info("Loading the storage plugin configs from URLs {}.", storageUrls);
StoragePlugins bootstrapPlugins = new StoragePlugins();
for (URL url : storageUrls) {
try {
loadStoragePlugins(url, bootstrapPlugins, pluginURLMap);
} catch (IOException e) {
throw new IOException("Failed to load bootstrap plugins from " + url.toString(), e );
}
}
return bootstrapPlugins;
}
/**
* Given a set of bootstrap plugins, updated selected plugins with the list
* of format plugins. Since the plugins do not yet reside in the registry,
* they can be modified in place without making copies.
* <p>
* Only one module (core) should have defined the file system plugin
* configs. Only one module should define each format plugin. As a result,
* order should not matter when applying the format configs.
* @throws IOException for load failures
*/
private void applyFormatPlugins(StoragePlugins bootstrapPlugins,
Map<String, URL> pluginURLMap) throws IOException {
String formatBootstrapFileName = context.config().getString(
ExecConstants.BOOTSTRAP_FORMAT_PLUGINS_FILE);
Set<URL> formatUrls = ClassPathScanner.forResource(formatBootstrapFileName, false);
if (formatUrls == null) {
return;
}
for (URL url : formatUrls) {
logger.info("Loading format plugin configs from {}.", url);
loadFormatPlugins(url, bootstrapPlugins, pluginURLMap);
}
}
/**
* Get the new storage plugins from the
* {@link ConfigConstants#STORAGE_PLUGINS_OVERRIDE_CONF} file if it exists,
* null otherwise
*/
@Override
public StoragePlugins updatedPlugins() {
String upgradeFileName = context.config().getString(
ExecConstants.UPGRADE_STORAGE_PLUGINS_FILE);
Set<URL> urlSet = ClassPathScanner.forResource(upgradeFileName, false);
if (urlSet.isEmpty()) {
logger.trace(
"The {} file is absent. Proceed without updating of the storage plugins configs",
upgradeFileName);
return null;
}
if (urlSet.size() != 1) {
throw DrillRuntimeException.create(
"More than one %s file is placed in Drill's classpath: %s",
upgradeFileName, urlSet);
}
pluginsOverrideFileUrl = urlSet.iterator().next();
try (InputStream is = pluginsOverrideFileUrl.openStream();) {
return context.hoconMapper().readValue(is, StoragePlugins.class);
} catch (IOException e) {
logger.error("Failures are obtained while loading file: '{}'. Proceeding without update.",
upgradeFileName, e);
return null;
}
}
@Override
public void onUpgrade() {
if (pluginsOverrideFileUrl == null) {
return;
}
String fileAction = context.config().getString(
ExecConstants.ACTION_ON_STORAGE_PLUGINS_OVERRIDE_FILE);
Optional<ActionOnFile> actionOnFile = Arrays.stream(ActionOnFile.values())
.filter(action -> action.name().equalsIgnoreCase(fileAction))
.findFirst();
actionOnFile.ifPresent(action -> action.action(pluginsOverrideFileUrl));
// TODO: replace with ifPresentOrElse() once the project will be on Java9
if (!actionOnFile.isPresent()) {
logger.warn("Unknown value {} for {} boot option. Nothing will be done with file.",
fileAction, ExecConstants.ACTION_ON_STORAGE_PLUGINS_OVERRIDE_FILE);
}
}
/**
* Loads storage plugins from the given URL
*
* @param url
* URL to the storage plugins bootstrap file
* @param bootstrapPlugins
* a collection where the plugins should be loaded to
* @param pluginURLMap
* a map to store correspondence between storage plugins and
* bootstrap files in which they are defined. Used for logging
* @throws IOException
* if failed to retrieve a plugin from a bootstrap file
*/
private void loadStoragePlugins(URL url, StoragePlugins bootstrapPlugins,
Map<String, URL> pluginURLMap) throws IOException {
StoragePlugins plugins = getPluginsFromResource(url);
plugins.forEach(plugin -> {
StoragePluginConfig oldPluginConfig = bootstrapPlugins.putIfAbsent(plugin.getKey(), plugin.getValue());
if (oldPluginConfig != null) {
logger.warn("Duplicate plugin instance '[{}]' defined in [{}, {}], ignoring the later one.",
plugin.getKey(), pluginURLMap.get(plugin.getKey()), url);
} else {
pluginURLMap.put(plugin.getKey(), url);
}
});
}
/**
* Loads format plugins from the given URL and adds the formats to the
* specified storage plugins
*
* @param url
* URL to the format plugins bootstrap file
* @param bootstrapPlugins
* a collection with loaded storage plugins. New formats will be
* added to them
* @param pluginURLMap
* a map to store correspondence between storage plugins and
* bootstrap files in which they are defined. Used for logging
* @throws IOException
* if failed to retrieve a plugin from a bootstrap file
*/
private void loadFormatPlugins(URL url, StoragePlugins bootstrapPlugins,
Map<String, URL> pluginURLMap) throws IOException {
StoragePlugins plugins = getPluginsFromResource(url);
for (Entry<String, StoragePluginConfig> sourceEntry : plugins) {
String pluginName = sourceEntry.getKey();
StoragePluginConfig sourcePlugin = sourceEntry.getValue();
if (!(sourcePlugin instanceof FileSystemConfig)) {
logger.warn("Formats are only supported by File System plugins. Source name '{}' is of type '{}'.",
pluginName, sourcePlugin.getClass().getName());
continue;
}
StoragePluginConfig targetPlugin = bootstrapPlugins.getConfig(pluginName);
if (targetPlugin == null) {
logger.warn("No boostrap storage plugin matches the name '{}'", pluginName);
continue;
}
if (!(targetPlugin instanceof FileSystemConfig)) {
logger.warn("Formats are only supported by File System plugins. Source name '{}' " +
"is of type '{}' but the bootstrap plugin of that name is of type '{}.",
pluginName, sourcePlugin.getClass().getName(),
targetPlugin.getClass().getName());
continue;
}
FileSystemConfig targetFsConfig = (FileSystemConfig) targetPlugin;
FileSystemConfig sourceFsConfig = (FileSystemConfig) sourcePlugin;
sourceFsConfig.getFormats().forEach((formatName, formatValue) -> {
FormatPluginConfig oldPluginConfig = targetFsConfig.getFormats().putIfAbsent(formatName, formatValue);
if (oldPluginConfig != null) {
logger.warn("Duplicate format instance '{}' defined in '{}' and '{}', ignoring the later one.",
formatName, pluginURLMap.get(pluginName), url);
}
});
}
}
private StoragePlugins getPluginsFromResource(URL resource) throws IOException {
String pluginsData = Resources.toString(resource, StandardCharsets.UTF_8);
return context.hoconMapper().readValue(pluginsData, StoragePlugins.class);
}
}