package nl.knaw.huygens.alexandria.storage; /* * #%L * alexandria-service * ======= * Copyright (C) 2015 - 2017 Huygens ING (KNAW) * ======= * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-3.0.html>. * #L% */ import static org.apache.tinkerpop.gremlin.process.traversal.P.lt; import java.io.IOException; import java.io.OutputStream; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.function.Supplier; import javax.inject.Singleton; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.structure.Element; import org.apache.tinkerpop.gremlin.structure.Graph; import org.apache.tinkerpop.gremlin.structure.Graph.Features.GraphFeatures; import org.apache.tinkerpop.gremlin.structure.T; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.apache.tinkerpop.gremlin.structure.io.IoCore; import org.apache.tinkerpop.gremlin.structure.io.graphml.GraphMLIo; import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONIo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import nl.knaw.huygens.alexandria.api.model.AlexandriaState; import nl.knaw.huygens.alexandria.storage.frames.IdentifiableVF; import nl.knaw.huygens.alexandria.storage.frames.ResourceVF; import nl.knaw.huygens.alexandria.storage.frames.VF; import peapod.FramedGraph; import peapod.FramedGraphTraversal; @Singleton public class Storage { private static Logger LOG = LoggerFactory.getLogger(Storage.class); public static final String IDENTIFIER_PROPERTY = "uuid"; private Graph graph; private FramedGraph framedGraph; private String dumpfile; private boolean supportsTransactions; private boolean supportsPersistence; private ThreadLocal<Boolean> transactionOpen; public Storage(final Graph graph) { setGraph(graph); } // - public methods - // // This is for the acceptancetest's benefit, so the tests kan start with a clear graph public void setGraph(final Graph graph) { this.graph = graph; this.framedGraph = new FramedGraph(graph, ResourceVF.class.getPackage()); final GraphFeatures graphFeatures = graph.features().graph(); this.supportsPersistence = graphFeatures.supportsPersistence(); this.supportsTransactions = graphFeatures.supportsTransactions(); } public boolean supportsTransactions() { return supportsTransactions; } public boolean supportsPersistence() { return supportsPersistence; } // framedGraph methods public <A> A runInTransaction(Supplier<A> supplier) { boolean inOpenTransaction = getTransactionIsOpen(); if (!inOpenTransaction) { startTransaction(); } try { A result = supplier.get(); if (!inOpenTransaction) { commitTransaction(); } return result; } catch (Exception e) { e.printStackTrace(); if (getTransactionIsOpen()) { rollbackTransaction(); } throw e; } } Boolean getTransactionIsOpen() { return getTransactionOpen().get(); } private ThreadLocal<Boolean> getTransactionOpen() { if (transactionOpen == null) { transactionOpen = ThreadLocal.withInitial(() -> false); } return transactionOpen; } public void runInTransaction(Runnable runner) { boolean startedInOpenTransaction = getTransactionIsOpen(); if (!startedInOpenTransaction) { startTransaction(); } try { runner.run(); if (!startedInOpenTransaction) { commitTransaction(); } } catch (Exception e) { e.printStackTrace(); if (getTransactionIsOpen()) { rollbackTransaction(); } throw e; } } public boolean existsVF(final Class<? extends VF> vfClass, final UUID uuid) { assertInTransaction(); assertClass(vfClass); return find(vfClass, uuid).tryNext().isPresent(); } public <A extends VF> A createVF(final Class<A> vfClass) { assertInTransaction(); assertClass(vfClass); return framedGraph.addVertex(vfClass); } public <A extends VF> Optional<A> readVF(final Class<A> vfClass, final UUID uuid) { assertInTransaction(); assertClass(vfClass); return firstOrEmpty(find(vfClass, uuid).toList()); } public <A extends IdentifiableVF> Optional<A> readVF(final Class<A> vfClass, final UUID uuid, final Integer revision) { assertInTransaction(); assertClass(vfClass); return firstOrEmpty(find(vfClass, uuid, revision).toList()); } public <A extends VF> FramedGraphTraversal<Object, A> find(Class<A> vfClass) { assertInTransaction(); assertClass(vfClass); return framedGraph.V(vfClass); } public GraphTraversal<Vertex, Vertex> getVertexTraversal(Object... vertexIds) { assertInTransaction(); return graph.traversal().V(vertexIds); } public GraphTraversal<Vertex, Vertex> getResourceVertexTraversal(Object... vertexIds) { return getVertexTraversal(vertexIds).has(T.label, "Resource"); } // graph methods public void removeExpiredTentatives(final Long threshold) { assertInTransaction(); getVertexTraversal()// .has("state", AlexandriaState.TENTATIVE.name())// .has("stateSince", lt(threshold))// .forEachRemaining(Element::remove); } public void removeVertexWithId(final String annotationBodyId) { assertInTransaction(); getVertexTraversal()// .has(IDENTIFIER_PROPERTY, annotationBodyId).next().remove(); } public Map<String, Object> getMetadata() { assertInTransaction(); Map<String, Object> metadata = Maps.newLinkedHashMap(); metadata.put("features", graph.features().toString().split(System.lineSeparator())); GraphTraversalSource traversal = graph.traversal(); metadata.put("vertices", count(traversal.V())); metadata.put("edges", count(traversal.E())); return metadata; } public void dumpToGraphSON(final OutputStream os) throws IOException { graph.io(GraphSONIo.build()).writer().create().writeGraph(os, graph); } public void dumpToGraphML(final OutputStream os) throws IOException { graph.io(new GraphMLIo.Builder()).writer().create().writeGraph(os, graph); } public void readGraph(DumpFormat format, String filename) throws IOException { nonGryoWarning(format); graph.io(format.builder).readGraph(filename); } public void writeGraph(DumpFormat format, String filename) throws IOException { nonGryoWarning(format); graph.io(format.builder).writeGraph(filename); } private void nonGryoWarning(DumpFormat format) { if (!DumpFormat.gryo.equals(format)) { LOG.warn("restoring from " + format.name() + " may lead to duplicate id errors, use gryo if possible"); } } public void loadFromDisk(final String file) { try { System.out.println("loading db from " + file + "..."); graph.io(IoCore.gryo()).readGraph(file); } catch (final IOException e) { throw new RuntimeException(e); } } public void saveToDisk(final String file) { if (file != null) { System.out.println("storing db to " + file + "..."); try { graph.io(IoCore.gryo()).writeGraph(file); } catch (final IOException e) { throw new RuntimeException(e); } } } public void setDumpFile(final String dumpfile) { this.dumpfile = dumpfile; } public void destroy() { // LOG.info("destroy called"); try { // LOG.info("closing graph {}", graph); graph.close(); // LOG.info("graph closed: {}", graph); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } // LOG.info("destroy done"); } public Vertex addVertex(Object... keyValues) { assertInTransaction(); return graph.addVertex(keyValues); } public <A extends VF> A frameVertex(Vertex v, Class<A> vfClass) { return framedGraph.frame(v, vfClass); } // - private methods - // private <A extends VF> FramedGraphTraversal<Object, A> find(final Class<A> vfClass, final UUID uuid) { return find(vfClass).has(IDENTIFIER_PROPERTY, uuid.toString()); } private <A extends VF> FramedGraphTraversal<Object, A> find(final Class<A> vfClass, final UUID uuid, final Integer revision) { return find(vfClass).has(IDENTIFIER_PROPERTY, uuid.toString() + "." + revision); } private String getDumpFile() { return dumpfile; } private <A> Optional<A> firstOrEmpty(final List<A> results) { return results.isEmpty() ? Optional.empty() : Optional.ofNullable(results.get(0)); } private Long count(GraphTraversal<?, ?> graphTraversal) { return graphTraversal.count().next(); } private void startTransaction() { assertTransactionIsClosed(); if (supportsTransactions()) { framedGraph.tx().rollback(); } setTransactionIsOpen(true); } void setTransactionIsOpen(Boolean b) { getTransactionOpen().set(b); } private void commitTransaction() { assertTransactionIsOpen(); if (supportsTransactions()) { tryCommitting(10); // framedGraph.tx().close(); } if (!supportsPersistence()) { saveToDisk(getDumpFile()); } setTransactionIsOpen(false); } private void tryCommitting(int count) { if (count > 1) { try { framedGraph.tx().commit(); } catch (Exception e) { // wait try { LOG.error("exception={}", e); Thread.sleep(500); } catch (InterruptedException ie) { ie.printStackTrace(); } // try again tryCommitting(count - 1); } } else { framedGraph.tx().commit(); } } private void rollbackTransaction() { assertTransactionIsOpen(); if (supportsTransactions()) { framedGraph.tx().rollback(); // framedGraph.tx().close(); } else { LOG.error("rollback called, but transactions are not supported by graph {}", graph); } setTransactionIsOpen(false); } private void assertInTransaction() { Preconditions.checkState(getTransactionIsOpen(), "We should be in an open transaction at this point, use runInTransaction()!"); } private void assertClass(final Class<? extends VF> clazz) { Preconditions.checkState(// clazz.getAnnotationsByType(peapod.annotations.Vertex.class).length > 0, // "Class " + clazz + " has no peapod @Vertex annotation, are you sure it's the correct class?"// ); } private void assertTransactionIsClosed() { Preconditions.checkState(!getTransactionIsOpen(), "We're already inside an open transaction!"); } private void assertTransactionIsOpen() { Preconditions.checkState(getTransactionIsOpen(), "We're not in an open transaction!"); } }