/** * diqube: Distributed Query Base. * * Copyright (C) 2015 Bastian Gloeckle * * This file is part of diqube. * * diqube 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package org.diqube.server; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.Charset; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.inject.Inject; import org.diqube.context.AutoInstatiate; import org.diqube.data.table.TableShard; import org.diqube.loader.LoadException; import org.diqube.server.control.ControlFileFactory; import org.diqube.server.control.ControlFileUnloader; import org.diqube.util.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Manages the .control files that are provided to diqube at runtime and maintains corresponding .read and .failure * files. * * <p> * After the data of a control file has been loaded, a .ready file will be placed right next to it. The .ready file will * be removed when unloading the control file. * * <p> * Additionally, during operation of the server, a .failure file may be written by this class if an error was * encountered with this servers data of the table. In that case, the data will be undeployed automatically and the * failure file will contain details of the problem. * * <p> * The implementation is somewhat tightly connected to {@link NewDataWatcher}. * * @author Bastian Gloeckle */ @AutoInstatiate public class ControlFileManager { private static final Logger logger = LoggerFactory.getLogger(ControlFileManager.class); public static final String CONTROL_FILE_EXTENSION = ".control"; public static final String READY_FILE_EXTENSION = ".ready"; public static final String FAILURE_FILE_EXTENSION = ".failure"; @Inject private ControlFileFactory controlFileFactory; /** * Map from controlFile path to a pair of table name and a list of values of {@link TableShard#getLowestRowId()} of * the tableShards that were loaded from that file. */ private Map<String, Pair<String, List<Long>>> tableInfoByControlFilePath = new HashMap<>(); /** * Fully deploy a specific control file (if possible) and maintain .ready file. */ public synchronized void deployControlFile(File controlFile) { if (tableInfoByControlFilePath.containsKey(controlFile.getAbsolutePath())) { logger.info("Control file {} is loaded already. Skipping.", controlFile.getAbsolutePath()); return; } logger.info("Starting to load new table shard from control file {}.", controlFile.getAbsolutePath()); try { Pair<String, List<Long>> tableInfo = controlFileFactory.createControlFileLoader(controlFile).load(); tableInfoByControlFilePath.put(controlFile.getAbsolutePath(), tableInfo); logger.info("Data for table '{}' (with starting rowIds {}) loaded successfully from {}'", tableInfo.getLeft(), tableInfo.getRight(), controlFile.getAbsolutePath()); File failure = failureFile(controlFile); if (failure.exists()) if (!failure.delete()) logger.warn("Could not delete failure file {}", failureFile(controlFile)); // write ready file String content = LocalDateTime.now().toString(); try (FileOutputStream readyOS = new FileOutputStream(readyFile(controlFile))) { readyOS.write(content.getBytes(Charset.forName("UTF-8"))); } catch (IOException e) { logger.warn("Could not write ready file {}", readyFile(controlFile), e); } } catch (LoadException e) { logger.error("Could not load table shards from {}", controlFile.getAbsolutePath(), e); } } /** * Fully undeploy a specific control file and remove .ready file. This method is for a regular undeployment. */ public synchronized void undeployControlFile(File controlFile) { if (internalUndeployControlFile(controlFile, true)) { File readyFile = readyFile(controlFile); if (readyFile.exists()) if (!readyFile.delete()) logger.warn("Could not delete ready file {}", readyFile.getAbsolutePath()); File failure = failureFile(controlFile); if (failure.exists()) if (!failure.delete()) logger.warn("Could not delete failure file {}", failureFile(controlFile)); } } /** * Fully undeploy a specific control file and remove .ready file. * * @param handleMetadataChange * see {@link ControlFileUnloader#unload(boolean)} */ private boolean internalUndeployControlFile(File controlFile, boolean handleMetadataChange) { Pair<String, List<Long>> tableInfo = tableInfoByControlFilePath.get(controlFile.getAbsolutePath()); if (tableInfo == null) { logger.warn( "Could not resolve the table that data of control file {} was loaded to. Will not remove any in-memory data.", controlFile.getAbsolutePath()); return false; } controlFileFactory.createControlFileUnloader(controlFile, tableInfo).unload(handleMetadataChange); tableInfoByControlFilePath.remove(controlFile.getAbsolutePath()); return true; } /** * Undeploy all control files of a specific table because there was an error when processing the table - a .failure * file will be written. * * @param tableName * The table of which to undeploy all control files * @param errorDescription * The description of the error, will be written to the .failure file. * @param handleMetadataChange * see {@link ControlFileUnloader#unload(boolean)} */ public synchronized void undeployTableBecauseOfError(String tableName, String errorDescription, boolean handleMetadataChange) { List<File> controlFiles = new ArrayList<>(); for (Entry<String, Pair<String, List<Long>>> entry : tableInfoByControlFilePath.entrySet()) { if (entry.getValue().getLeft().equals(tableName)) { controlFiles.add(new File(entry.getKey())); } } for (File controlFile : controlFiles) { logger.info("Undeploying control file {} because of an error, writing {} file.", controlFile, FAILURE_FILE_EXTENSION); internalUndeployControlFile(controlFile, handleMetadataChange); File readyFile = readyFile(controlFile); if (readyFile.exists()) if (!readyFile.delete()) logger.warn("Could not delete ready file {}", readyFile.getAbsolutePath()); File failureFile = failureFile(controlFile); if (!failureFile.exists()) { try (FileOutputStream failureOS = new FileOutputStream(failureFile)) { failureOS.write(errorDescription.getBytes(Charset.forName("UTF-8"))); } catch (IOException e) { logger.warn("Could not write failure file {}", failureFile, e); } } } } /** * See {@link #undeployTableBecauseOfError(String, String)}, errorDescription is taken from given Throwable. */ public synchronized void undeployTableBecauseOfError(String tableName, Throwable t, boolean handleMetadataChange) { undeployTableBecauseOfError(tableName, t.getMessage(), handleMetadataChange); } /** * @return The ready file for a given control file. */ private File readyFile(File controlFile) { return new File(controlFile.getParentFile(), controlFile.getName().substring(0, controlFile.getName().length() - CONTROL_FILE_EXTENSION.length()) + READY_FILE_EXTENSION); } /** * @return The failure file for a given control file. */ private File failureFile(File controlFile) { return new File(controlFile.getParentFile(), controlFile.getName().substring(0, controlFile.getName().length() - CONTROL_FILE_EXTENSION.length()) + FAILURE_FILE_EXTENSION); } }