/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.db.indexing; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.function.Consumer; import java.util.stream.Stream; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.WriteGraph; import org.simantics.db.common.request.IndexRoot; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.exception.DatabaseException; import org.simantics.db.indexing.internal.IndexChangedWriter; import org.simantics.db.layer0.adapter.GenericRelationIndex; import org.simantics.db.layer0.genericrelation.IndexedRelations; import org.simantics.db.layer0.internal.SimanticsInternal; import org.simantics.db.service.ServerInformation; import org.simantics.utils.FileUtils; import org.slf4j.LoggerFactory; /** * A facade for Simantics graph database index management facilities. * * @author Tuukka Lehtonen */ public final class DatabaseIndexing { private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(DatabaseIndexing.class); public static Path getIndexBaseLocation() { return Activator.getDefault().getIndexBaseFile(); } public static Path getIndexLocation(Session session, Resource relation, Resource input) { if (session == null) throw new NullPointerException("null session"); if (relation == null) throw new NullPointerException("null relation"); if (input == null) throw new NullPointerException("null input"); String dir = session.getService(ServerInformation.class).getDatabaseId() + "." + relation.getResourceId() + "." + input.getResourceId(); return getIndexBaseLocation().resolve(dir); } private static Path getAllDirtyFile() { return getIndexBaseLocation().resolve(".dirty"); } private static Path getChangedFile(Path indexPath) { return indexPath.resolve(".changed"); } public static void markAllDirty() throws IOException { Path indexBase = getIndexBaseLocation(); if (!Files.exists(indexBase) || !Files.isDirectory(indexBase)) return; if (LOGGER.isDebugEnabled()) LOGGER.debug("Marking all indexes dirty"); Path allDirtyFile = getAllDirtyFile(); if (!Files.exists(allDirtyFile)) { Files.createFile(allDirtyFile); FileUtils.sync(allDirtyFile); } } public static void clearAllDirty() throws IOException { if (LOGGER.isDebugEnabled()) LOGGER.debug("Clearing dirty state of all indexes"); Path indexBase = getIndexBaseLocation(); if (!Files.exists(indexBase) || !Files.isDirectory(indexBase)) return; forEachIndexPath(indexPath -> { Path p = getChangedFile(indexPath); try { FileUtils.delete(p); } catch (IOException e) { LOGGER.error("Could not delete {}", p.toAbsolutePath(), e); } }); FileUtils.delete(getAllDirtyFile()); } /** * Internal to indexing, invoked by {@link IndexedRelationsImpl} which * doesn't want to throw these exceptions forward. Just log it. * * @param indexPath */ static void markIndexChanged(Session session, Path indexPath) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Marking index dirty: " + indexPath); Path changedFile = getChangedFile(indexPath); try { // Mark change only once per DB session. if (getIndexChangedWriter(session).markDirty(changedFile)) { Files.createDirectories(indexPath); if (!Files.exists(changedFile)) { Files.createFile(changedFile); FileUtils.sync(changedFile); } else if (!Files.isRegularFile(changedFile)) { throw new FileAlreadyExistsException(changedFile.toString(), null, "index dirtyness indicator file already exists but it is not a regular file"); } } } catch (IOException e) { LOGGER.error("Could not mark index changed for indexPath={} and changedFile={}", indexPath.toAbsolutePath(), changedFile.toAbsolutePath()); } } private static IndexChangedWriter getIndexChangedWriter(Session session) { IndexChangedWriter writer = session.peekService(IndexChangedWriter.class); if (writer == null) { synchronized (IndexChangedWriter.class) { if (writer == null) session.registerService(IndexChangedWriter.class, writer = new IndexChangedWriter()); } } return writer; } public static void deleteAllIndexes() throws IOException { Path indexBase = DatabaseIndexing.getIndexBaseLocation(); ArrayList filter = new ArrayList<>(2); filter.add(getAllDirtyFile()); filter.add(indexBase); FileUtils.deleteWithFilter(indexBase, path -> !filter.contains(path)); FileUtils.delete(indexBase); } public static void deleteIndex(final Resource relation, final Resource modelPart) throws DatabaseException { SimanticsInternal.getSession().syncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { deleteIndex(graph, relation, modelPart); } }); } public static void deleteIndex(WriteGraph graph, final Resource relation, final Resource modelPart) throws DatabaseException { Resource model = graph.syncRequest(new IndexRoot(modelPart)); GenericRelationIndex index = graph.adapt(relation, GenericRelationIndex.class); IndexedRelations ir = graph.getService(IndexedRelations.class); // Deletes index files ir.reset(null, graph, relation, model); // Notifies DB listeners index.reset(graph, model); } public static void deleteIndex(Path indexPath) throws IOException { if (LOGGER.isDebugEnabled()) LOGGER.debug("Deleting index " + indexPath.toAbsolutePath()); ArrayList filter = new ArrayList<>(2); filter.add(getChangedFile(indexPath)); filter.add(indexPath); FileUtils.deleteWithFilter(indexPath, path -> !filter.contains(path)); FileUtils.delete(indexPath); } public static void validateIndexes() throws IOException { Path indexBase = getIndexBaseLocation(); if (LOGGER.isDebugEnabled()) LOGGER.debug("Validating indexes at " + indexBase); if (!Files.exists(indexBase)) return; if (!Files.isDirectory(indexBase)) { // Make sure that index-base is a valid directory if (LOGGER.isDebugEnabled()) LOGGER.debug(indexBase + " is not a directory! Removing it."); FileUtils.emptyDirectory(indexBase); Files.createDirectories(indexBase); return; } Path allDirtyFile = getAllDirtyFile(); if (Files.isRegularFile(allDirtyFile)) { if (LOGGER.isDebugEnabled()) LOGGER.debug("All indexes marked dirty, removing them."); deleteAllIndexes(); } else { forEachIndexPath(indexPath -> { Path changed = getChangedFile(indexPath); if (Files.isRegularFile(changed)) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Index is dirty, removing: " + indexPath); try { deleteIndex(indexPath); } catch (IOException e) { LOGGER.error("Could not delete index {}", indexPath.toAbsolutePath(), e); } } }); } } private static void forEachIndexPath(Consumer callback) throws IOException { try (Stream paths = Files.walk(getIndexBaseLocation(), 1).filter(Files::isDirectory)) { Iterator iter = paths.iterator(); while (iter.hasNext()) { Path p = iter.next(); callback.accept(p); } } } }