/* * Copyright 2015-2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed 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.hawkular.inventory.base; import static org.hawkular.inventory.api.Action.created; import static org.hawkular.inventory.api.Action.deleted; import static org.hawkular.inventory.api.Relationships.Direction.both; import static org.hawkular.inventory.api.Relationships.Direction.outgoing; import static org.hawkular.inventory.api.Relationships.WellKnown.contains; import static org.hawkular.inventory.api.Relationships.WellKnown.defines; import static org.hawkular.inventory.api.Relationships.WellKnown.hasData; import java.util.HashSet; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import org.hawkular.inventory.api.Action; import org.hawkular.inventory.api.EntityNotFoundException; import org.hawkular.inventory.api.InventoryException; import org.hawkular.inventory.api.Log; import org.hawkular.inventory.api.Query; import org.hawkular.inventory.api.RelationAlreadyExistsException; import org.hawkular.inventory.api.RelationNotFoundException; import org.hawkular.inventory.api.Relationships; import org.hawkular.inventory.api.filters.Marker; import org.hawkular.inventory.api.filters.With; import org.hawkular.inventory.api.model.AbstractElement; import org.hawkular.inventory.api.model.Relationship; import org.hawkular.inventory.base.spi.CommitFailureException; import org.hawkular.inventory.base.spi.Discriminator; import org.hawkular.inventory.base.spi.ElementNotFoundException; import org.hawkular.inventory.base.spi.InconsistenStateException; import org.hawkular.inventory.paths.CanonicalPath; import org.hawkular.inventory.paths.Path; import org.hawkular.inventory.paths.RelativePath; import org.hawkular.inventory.paths.SegmentType; /** * @author Lukas Krejci * @since 0.2.0 */ final class Util { private static final Random rand = new Random(); private Util() { } public static <R, BE> R inTx(TraversalContext<BE, ?> context, TransactionPayload<R, BE> payload) { Transaction<BE> transaction = context.startTransaction(); Log.LOGGER.trace("Starting transaction: " + transaction); int maxFailures = context.getTransactionRetriesCount(); return onFailureRetry(context, transaction, payload, payload, maxFailures); } public static <R, BE> R inCommittableTx(TraversalContext<BE, ?> context, TransactionPayload.Committing<R, BE> payload) { Transaction.Committable<BE> tx = Transaction.Committable.from(context.startTransaction()); Log.LOGGER.trace("Starting self-committing transaction: " + tx); int maxFailures = context.getTransactionRetriesCount(); return onFailureRetry(context::startTransaction, tx, payload, payload, maxFailures); } public static <R, BE> R onFailureRetry(TraversalContext<BE, ?> ctx, Transaction<BE> tx, TransactionPayload<R, BE> firstPayload, TransactionPayload<R, BE> succeedingPayload, int maxFailures) { return onFailureRetry(ctx::startTransaction, Transaction.Committable.from(tx), TransactionPayload.Committing.committing(firstPayload), TransactionPayload.Committing.committing(succeedingPayload), maxFailures); } public static <R, BE> R onFailureRetry(Function<Transaction.PreCommit<BE>, Transaction<BE>> txCtor, Transaction.Committable<BE> tx, TransactionPayload.Committing<R, BE> firstPayload, TransactionPayload.Committing<R, BE> succeedingPayload, int maxFailures) { int failures = 0; Exception lastException; //this could be configurable, but let's just start with the hardcoded 300ms + a random bit //as the first retry wait time. int waitTime = 300 + rand.nextInt(150); do { try { try { R ret; if (failures == 0) { ret = firstPayload.run(tx); tx.registerCommittedPayload(firstPayload); } else { tx.getPreCommit().reset(); tx = Transaction.Committable.from(txCtor.apply(tx.getPreCommit())); ret = succeedingPayload.run(tx); tx.registerCommittedPayload(succeedingPayload); } return ret; } catch (Throwable t) { Log.LOGGER.dTransactionFailed(t.getMessage()); if (t instanceof InconsistenStateException || tx.requiresRollbackAfterFailure(t)) { tx.rollback(); } throw t; } } catch (CommitFailureException | InconsistenStateException e) { failures++; //if the backend fails the commit, we can retry Log.LOGGER.debugf(e, "Commit attempt %d/%d failed. Will wait for %d ms before retrying." + " The failure message was: %s", failures, maxFailures, waitTime, e.getMessage()); lastException = e; if (failures < maxFailures) { try { Thread.sleep(waitTime); } catch (InterruptedException ie) { Log.LOGGER.wInterruptedWhileWaitingForTransactionRetry(); //reset the interruption flag Thread.currentThread().interrupt(); //and jump out of the loop to throw the transaction failure exception break; } //double the wait time for the next attempt - the assumption is that if the competing transaction //takes a long time to complete, it probably is going to be really long. //We randomize the value a little bit so that competing transactions started at roughly same time //don't knock each other out easily. waitTime = waitTime * 2 + rand.nextInt(waitTime / 2); } } catch (RuntimeException e) { throw e; } catch (Exception e) { //an exception in the payload itself, not caused by a failed commit. We don't retry those... throw new InventoryException("Transaction payload failed.", e); } } while (failures < maxFailures); throw new TransactionFailureException(lastException, failures); } public static <BE> BE getSingle(Discriminator discriminator, Transaction<BE> backend, Query query, SegmentType entityType) { BE result = backend.querySingle(discriminator, query); if (result == null) { throw new EntityNotFoundException(entityType, Query.filters(query)); } return result; } public static <BE> EntityAndPendingNotifications<BE, Relationship> createAssociation(Discriminator discriminator, Transaction<BE> tx, BE source, String relationship, BE target, Map<String, Object> properties) { if (tx.hasRelationship(discriminator, source, target, relationship)) { throw new RelationAlreadyExistsException(relationship, Query.filters(Query.to(tx.extractCanonicalPath (source)))); } RelationshipRules.checkCreate(discriminator, tx, source, Relationships.Direction.outgoing, relationship, target); BE relationshipObject = tx.relate(discriminator, source, target, relationship, properties); Relationship ret = tx.convert(discriminator, relationshipObject, Relationship.class); return new EntityAndPendingNotifications<>(relationshipObject, ret, new Notification<>(ret, ret, created())); } public static <BE> EntityAndPendingNotifications<BE, Relationship> deleteAssociation(Discriminator discriminator, Transaction<BE> tx, Query sourceQuery, SegmentType sourceType, String relationship, BE target) { BE source = getSingle(discriminator, tx, sourceQuery, sourceType); BE relationshipObject; try { relationshipObject = tx.getRelationship(discriminator, source, target, relationship); } catch (ElementNotFoundException e) { throw new RelationNotFoundException(sourceType, relationship, Query.filters(sourceQuery), null, e); } RelationshipRules.checkDelete(discriminator, tx, source, Relationships.Direction.outgoing, relationship, target); Relationship ret = tx.convert(discriminator, relationshipObject, Relationship.class); tx.markDeleted(discriminator, relationshipObject); return new EntityAndPendingNotifications<>(relationshipObject, ret, new Notification<>(ret, ret, deleted())); } public static <BE> Relationship getAssociation(Discriminator discriminator, Transaction<BE> tx, Query sourceQuery, SegmentType sourceType, Query targetQuery, SegmentType targetType, String rel) { BE source = getSingle(discriminator, tx, sourceQuery, sourceType); BE target = getSingle(discriminator, tx, targetQuery, targetType); BE relationship; try { relationship = tx.getRelationship(discriminator, source, target, rel); } catch (ElementNotFoundException e) { throw new RelationNotFoundException(sourceType, rel, Query.filters(sourceQuery), null, null); } return tx.convert(discriminator, relationship, Relationship.class); } @SuppressWarnings("unchecked") public static Query queryTo(Query sourcePath, Path path) { if (path instanceof CanonicalPath) { return Query.to((CanonicalPath) path); } else { Query.SymmetricExtender extender = sourcePath.extend().path(); extender.with(With.relativePath(null, (RelativePath) path)); return extender.get(); } } /** * Tries to find an element at given path. * * @param tx current transaction in the traversal (if path is canonical, this is not used) * @param path either canonical or relative path of the element to find * @return the element * @throws ElementNotFoundException if the element is not found */ @SuppressWarnings("unchecked") public static <BE> BE find(Discriminator discriminator, Transaction<BE> tx, Query sourcePath, Path path) throws EntityNotFoundException { BE element; if (path.isCanonical()) { try { element = tx.find(discriminator, path.toCanonicalPath()); } catch (ElementNotFoundException e) { throw new EntityNotFoundException("Entity not found on path: " + path); } } else { Query query = queryTo(sourcePath, path); element = getSingle(discriminator, tx, query, null); } return element; } @SuppressWarnings("unchecked") public static Query extendTo(TraversalContext<?, ?> context, Path path) { if (path instanceof CanonicalPath) { return context.select().with(With.path((CanonicalPath) path)).get(); } else { Marker marker = Marker.next(); return context.sourcePath.extend().path().with(marker).with(context.selectCandidates) .with(With.relativePath(marker.getLabel(), (RelativePath) path)).get(); } } @SuppressWarnings("unchecked") public static <BE, E extends AbstractElement<?, U>, U extends AbstractElement.Update> void update( Discriminator discriminator, Class<E> entityClass, Transaction<BE> tx, Query entityQuery, U update, TransactionParticipant<BE, U> preUpdateCheck, BiConsumer<BE, Transaction<BE>> postUpdateCheck) { BE entity = tx.querySingle(discriminator, entityQuery); if (entity == null) { if (update instanceof Relationship.Update) { throw new RelationNotFoundException((String) null, Query.filters(entityQuery)); } else { throw new EntityNotFoundException(entityClass, Query.filters(entityQuery)); } } if (preUpdateCheck != null) { preUpdateCheck.execute(entity, update, tx); } E orig = tx.convert(discriminator, entity, entityClass); tx.update(discriminator, entity, update); if (postUpdateCheck != null) { postUpdateCheck.accept(entity, tx); } E updated = tx.convert(discriminator, entity, entityClass); tx.getPreCommit().addNotifications(new EntityAndPendingNotifications<>(entity, updated, new Action.Update<>(orig, update), Action.updated())); } @SuppressWarnings("unchecked") public static <BE, E extends AbstractElement<?, ?>> void delete(Discriminator discriminator, Class<E> entityClass, Transaction<BE> tx, Query entityQuery, BiConsumer<BE, Transaction<BE>> cleanupFunction, BiConsumer<BE, Transaction<BE>> postDelete, boolean eradicate) { BE entity = tx.querySingle(discriminator, entityQuery); if (entity == null) { if (entityClass.equals(Relationship.class)) { throw new RelationNotFoundException((String) null, Query.filters(entityQuery)); } else { throw new EntityNotFoundException(entityClass, Query.filters(entityQuery)); } } if (cleanupFunction != null) { cleanupFunction.accept(entity, tx); } Set<BE> verticesToDeleteThatDefineSomething = new HashSet<>(); Set<BE> dataToBeDeleted = new HashSet<>(); Set<BE> deleted = new HashSet<>(); Set<BE> deletedRels = new HashSet<>(); Discriminator excludingFastDeletes = discriminator.excludeDeletedInMillisecond(); Consumer<BE> categorizer = (e) -> { if (tx.hasRelationship(discriminator, e, outgoing, defines.name())) { verticesToDeleteThatDefineSomething.add(e); } else { deleted.add(e); } //not only the entity, but also its relationships are going to disappear deletedRels.addAll(tx.getRelationships(excludingFastDeletes, e, both)); tx.getRelationships(excludingFastDeletes, e, outgoing, hasData.name()).forEach(rel -> { dataToBeDeleted.add(tx.getRelationshipTarget(discriminator, rel)); }); }; categorizer.accept(entity); tx.getTransitiveClosureOver(excludingFastDeletes, entity, outgoing, contains.name()) .forEachRemaining(categorizer::accept); //we've gathered all entities to be deleted. Now record the notifications to be sent out when the transaction //commits. Consumer<BE> addNotification = be -> { AbstractElement<?, ?> e = tx.convert(discriminator, be, (Class<AbstractElement<?, ?>>) tx.extractType(be)); tx.getPreCommit().addNotifications(new EntityAndPendingNotifications<>(be, e, deleted())); }; deleted.stream().filter(o -> isRepresentableInAPI(tx, o)).forEach(addNotification); verticesToDeleteThatDefineSomething.stream().filter(o -> isRepresentableInAPI(tx, o)).forEach(addNotification); deletedRels.stream().filter(o -> isRepresentableInAPI(tx, o)).forEach(addNotification); //k, now we can delete them all... the order is not important anymore for (BE e : deleted) { if (eradicate) { tx.eradicate(e); } else { tx.markDeleted(discriminator, e); } } for (BE e : verticesToDeleteThatDefineSomething) { if (tx.hasRelationship(discriminator, e, outgoing, defines.name())) { //we avoid the convert() function here because it assumes the containing entities of the passed in //entity exist. This might not be true during the delete because the transitive closure "walks" the //entities from the "top" down the containment chain and the entities are immediately deleted. CanonicalPath rootPath = tx.extractCanonicalPath(entity); CanonicalPath definingPath = tx.extractCanonicalPath(e); throw new IllegalArgumentException("Could not delete entity '" + rootPath + "'. The entity '" + definingPath + "', which it (indirectly) contains, acts as a definition for some " + "entities that are not deleted along with it, which would leave them without a " + "definition. This is illegal."); } else { if (eradicate) { tx.eradicate(e); } else { tx.markDeleted(discriminator, e); } } } if (eradicate) { dataToBeDeleted.forEach(tx::deleteStructuredData); } if (postDelete != null) { postDelete.accept(entity, tx); } } /** * If the provided path is canonical, it is prefixed with the {@code canonicalPrefix} and returned. If the provided * path is relative, it is resolved against the {@code relativeOrigin} and then converted to a canonical path. * <p> * <p>The path can be partially untyped. * * @param path the string representation of a path (either canonical or relative) * @param canonicalPrefix the prefix to apply to a canonical path * @param relativeOrigin origin to resolve a relative path against * @param intendedFinalType the intended type of the final segment of the path * @return the canonical path represented by the provided path string * @see Path#fromPartiallyUntypedString(String, CanonicalPath, CanonicalPath, SegmentType) */ public static CanonicalPath canonicalize(String path, CanonicalPath canonicalPrefix, CanonicalPath relativeOrigin, SegmentType intendedFinalType) { Path p = Path.fromPartiallyUntypedString(path, canonicalPrefix, relativeOrigin, intendedFinalType); if (p instanceof RelativePath) { return ((RelativePath) p).applyTo(relativeOrigin); } else { return p.toCanonicalPath(); } } /** * Certain constructs in backend are not representable in API - such as the * {@link org.hawkular.inventory.api.Relationships.WellKnown#hasData} relationship. * * @param tx the context using which to access backend * @param entity the entity to decide on * @param <BE> the type of the backend entity * @return true if the entity can be represented in API results, false otherwise */ public static <BE> boolean isRepresentableInAPI(Transaction<BE> tx, BE entity) { if (tx.isBackendInternal(entity)) { return false; } if (Relationship.class.equals(tx.extractType(entity))) { if (Relationships.WellKnown.hasData.name().equals(tx.extractRelationshipName(entity))) { return false; } } return true; } public interface TransactionParticipant<BE, E> { void execute(BE entityRepresentation, E entity, Transaction<BE> transaction); } }