/* * 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 java.io.InputStream; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import org.hawkular.inventory.api.Configuration; import org.hawkular.inventory.api.EntityNotFoundException; import org.hawkular.inventory.api.Interest; import org.hawkular.inventory.api.Inventory; import org.hawkular.inventory.api.Query; import org.hawkular.inventory.api.Relationships; import org.hawkular.inventory.api.Tenants; import org.hawkular.inventory.api.TransactionFrame; import org.hawkular.inventory.api.filters.With; import org.hawkular.inventory.api.model.AbstractElement; import org.hawkular.inventory.api.model.Entity; import org.hawkular.inventory.api.model.Relationship; import org.hawkular.inventory.api.model.Tenant; import org.hawkular.inventory.api.paging.Page; import org.hawkular.inventory.api.paging.Pager; import org.hawkular.inventory.api.paging.TransformingPage; import org.hawkular.inventory.base.spi.CommitFailureException; import org.hawkular.inventory.base.spi.ElementNotFoundException; import org.hawkular.inventory.base.spi.InventoryBackend; import org.hawkular.inventory.paths.CanonicalPath; import rx.Observable; /** * An implementation of the {@link Inventory} that converts the API traversals into trees of filters that it then passes * for evaluation to a {@link InventoryBackend backend}. * * <p>This class is meant to be inherited by the implementation that should provide the initialization and cleanup * logic. * * @param <E> the type of the backend-specific class representing entities and relationships. * * @author Lukas Krejci * @since 0.1.0 */ public abstract class BaseInventory<E> implements Inventory { public static final Configuration.Property TRANSACTION_RETRIES = Configuration.Property.builder() .withPropertyNameAndSystemProperty("hawkular.inventory.transaction.retries") .withEnvironmentVariables("HAWKULAR_INVENTORY_TRANSACTION_RETRIES").build(); private InventoryBackend<E> backend; private final ObservableContext observableContext; private Configuration configuration; private TraversalContext<E, Tenant> tenantContext; private TraversalContext<E, Relationship> relationshipContext; private final TransactionConstructor<E> transactionConstructor; /** * This is a sort of copy constructor. * Can be used by subclasses when implementing the {@link #cloneWith(TransactionConstructor)} method. * * @param orig the original instance to copy stuff over from * @param backend if not null, then use this backend instead of the one used by {@code orig} * @param transactionConstructor if not null, then use this ctor instead of the one used by {@code orig} */ protected BaseInventory(BaseInventory<E> orig, InventoryBackend<E> backend, TransactionConstructor<E> transactionConstructor) { this.observableContext = orig.observableContext; this.configuration = orig.configuration; this.backend = backend == null ? orig.backend : backend; this.transactionConstructor = transactionConstructor == null ? orig.transactionConstructor : transactionConstructor; tenantContext = new TraversalContext<>(this, orig.tenantContext.declaredNow(), Query.empty(), Query.path().with(With.type(Tenant.class)).get(), this.backend, Tenant.class, configuration, observableContext, this.transactionConstructor); relationshipContext = new TraversalContext<>(this, orig.relationshipContext.declaredNow(), Query.empty(), Query.path().get(), this.backend, Relationship.class, configuration, observableContext, this.transactionConstructor); } protected BaseInventory() { observableContext = new ObservableContext(); transactionConstructor = TransactionConstructor.startInBackend(); } /** * Mainly here for testing purposes * @param txCtor transaction constructor to use - useful to supply some test-enabled impl */ protected BaseInventory(TransactionConstructor<E> txCtor) { observableContext = new ObservableContext(); transactionConstructor = txCtor; } /** * Clones this inventory with the exception of the transaction constructor. The returned inventory will be the * exact copy of this one but its transaction constructor will be set to the provided one. * * @param transactionCtor the transaction constructor to use. It has already been adapted using * {@link #adaptTransactionConstructor(TransactionConstructor)}. * * @return the cloned inventory */ protected abstract BaseInventory<E> cloneWith(TransactionConstructor<E> transactionCtor); /** * This is a hook used for unit testing. Using this we can keep track of how transaction constructors are used. * @param txCtor a potentially modified transaction constructor * @return a transaction constructor fit to use with this inventory impl. */ protected TransactionConstructor<E> adaptTransactionConstructor(TransactionConstructor<E> txCtor) { return txCtor; } @Override public final void initialize(Configuration configuration) { this.backend = doInitialize(configuration); tenantContext = new TraversalContext<>(this, null, Query.empty(), Query.path().with(With.type(Tenant.class)).get(), backend, Tenant.class, configuration, observableContext, transactionConstructor); relationshipContext = new TraversalContext<>(this, null, Query.empty(), Query.path().get(), backend, Relationship.class, configuration, observableContext, transactionConstructor); this.configuration = configuration; } @Override public TransactionFrame newTransactionFrame() { if (backend.isPreferringBigTransactions()) { //a full-blown transaction frame... we don't commit/rollback anything and postpone all that work to the //frame's commit rollback return new OneTxTransactionFrame(); } else { //the backend doesn't like big transactions... we just commit everything as it goes, essentially rendering //transaction frame useless.. return new ManyTxTransactionFrame(); } } BaseInventory<E> keepTransaction(Transaction<E> tx) { return cloneWith(adaptTransactionConstructor((b, p) -> { HidingPrecommit<E> precommit = new HidingPrecommit<>(); Runnable transferActionsAndNotifs = () -> { //transfer the resulting notifications and actions - they will be emitted once the "real" transaction //really successfully commits. precommit.getHiddenActions().forEach(tx.getPreCommit()::addAction); precommit.getHiddenNotifications().forEach(tx.getPreCommit()::addNotifications); }; return new BackendTransaction<>(new TransactionIgnoringBackend<>(tx.directAccess(), transferActionsAndNotifs), precommit); })); } /** * This method is called during {@link #initialize(Configuration)} and provides the instance of the backend * initialized from the configuration. * * @param configuration the configuration provided by the user * @return a backend implementation that will be used to access the backend store of the inventory */ protected abstract InventoryBackend<E> doInitialize(Configuration configuration); @Override public BaseInventory<E> at(Instant time) { BaseInventory<E> copy = cloneWith(transactionConstructor); copy.tenantContext = tenantContext.at(time); copy.relationshipContext = relationshipContext.at(time); return copy; } @Override public final void close() throws Exception { if (backend != null) { backend.close(); backend = null; } } @Override public Tenants.ReadWrite tenants() { return new BaseTenants.ReadWrite<>(tenantContext); } @Override public Relationships.Read relationships() { return new BaseRelationships.Read<>(relationshipContext); } /** * <b>WARNING</b>: This is not meant for general consumption but primarily for testing purposes. You can render * the inventory inconsistent and/or unusable with unwise modifications done directly through the backend. * * @return the backend this inventory is using for persistence and querying. */ public InventoryBackend<E> getBackend() { return backend; } @Override public boolean hasObservers(Interest<?, ?> interest) { return observableContext.isObserved(interest); } @Override public <C, V> Observable<C> observable(Interest<C, V> interest) { return observableContext.getObservableFor(interest); } @Override public InputStream getGraphSON(String tenantId) { return getBackend().getGraphSON(tenantContext.discriminator(), tenantId); } @Override public AbstractElement<?, ?> getElement(CanonicalPath path) { try { E element = getBackend().find(tenantContext.discriminator(), path); Class<?> type = getBackend().extractType(element); return (AbstractElement<?, ?>) getBackend().convert(tenantContext.discriminator(), element, type); } catch (ElementNotFoundException e) { throw new EntityNotFoundException("No element found on path: " + path.toString()); } } @Override public <T extends Entity<?, ?>> Iterator<T> getTransitiveClosureOver(CanonicalPath startingPoint, Relationships.Direction direction, Class<T> clazz, String... relationshipNames) { return getBackend().getTransitiveClosureOver(tenantContext.discriminator(), startingPoint, direction, clazz, relationshipNames); } @Override public Configuration getConfiguration() { return configuration; } @Override public <T extends AbstractElement> Page<T> execute(Query query, Class<T> requestedEntity, Pager pager) { InventoryBackend<E> tx = getBackend().startTransaction(); try { return new TransformingPage<T, T>(tx.query(tenantContext.discriminator(), query, pager, e -> backend.convert(tenantContext.discriminator(), e, requestedEntity), null), Function.identity()) { @Override public void close() { tx.rollback(); } }; } catch (Throwable t) { tx.rollback(); throw t; } } private static class TransactionIgnoringBackend<E> extends DelegatingInventoryBackend<E> { private final Runnable onCommit; public TransactionIgnoringBackend(InventoryBackend<E> backend, Runnable onCommit) { super(backend); this.onCommit = onCommit; } @Override public void commit() throws CommitFailureException { if (onCommit != null) { onCommit.run(); } } @Override public void rollback() { } @Override public InventoryBackend<E> startTransaction() { return this; } } private static final class HidingPrecommit<E> extends Transaction.PreCommit.Simple<E> { private HidingPrecommit() { } @Override public List<Consumer<Transaction<E>>> getActions() { return Collections.emptyList(); } @Override public List<EntityAndPendingNotifications<E, ?>> getFinalNotifications() { return Collections.emptyList(); } public List<EntityAndPendingNotifications<E, ?>> getHiddenNotifications() { return super.getFinalNotifications(); } public List<Consumer<Transaction<E>>> getHiddenActions() { return super.getActions(); } } private class OneTxTransactionFrame implements TransactionFrame { private InventoryBackend<E> activeBackend; private Transaction.PreCommit<E> activePrecommit; private List<TransactionPayload.Committing<?, E>> committedPayloads = new ArrayList<>(); private final TransactionConstructor<E> fakeTxCtor = (b, p) -> { InventoryBackend<E> realBackend; if (activeBackend == null) { activeBackend = b.startTransaction(); activePrecommit = p; } realBackend = activeBackend; BaseInventory.HidingPrecommit<E> txPrecommit = new BaseInventory.HidingPrecommit<>(); Runnable onCommit = () -> { //transfer the resulting notifications - they will be emitted once the "real" transaction //really successfully commits. txPrecommit.getHiddenActions().forEach(activePrecommit::addAction); txPrecommit.getHiddenNotifications().forEach(activePrecommit::addNotifications); }; return new BackendTransaction<E>(new TransactionIgnoringBackend<>(realBackend, onCommit), txPrecommit) { @Override public void registerCommittedPayload(TransactionPayload.Committing<?, E> committedPayload) { committedPayloads.add(committedPayload); } }; }; @Override public void commit() throws CommitException { Util.onFailureRetry(p -> new BackendTransaction<>(new TransactionIgnoringBackend<>(activeBackend, null), p), Transaction.Committable.from( adaptTransactionConstructor(fakeTxCtor) .construct(activeBackend, new BasePreCommit<>())), (TransactionPayload.Committing<Void, E>) tx -> { activePrecommit.initialize(boundInventory(), tx); activePrecommit.getActions().forEach(a -> a.accept(tx)); activeBackend.commit(); activePrecommit.getFinalNotifications().forEach(tenantContext::notifyAll); return null; }, tx -> { activePrecommit.reset(); for (TransactionPayload.Committing<?, E> p : committedPayloads) { //the payloads "think" they each run in a transaction... prepare //those for each of them Transaction<E> fakeTx = fakeTxCtor.construct(activeBackend, new Transaction.PreCommit.Simple<>()); fakeTx.getPreCommit().initialize(boundInventory(), fakeTx); p.run(fakeTx); } activePrecommit.initialize(boundInventory(), tx); activePrecommit.getActions().forEach(a -> a.accept(tx)); activeBackend.commit(); activePrecommit.getFinalNotifications().forEach(tenantContext::notifyAll); return null; }, relationshipContext.getTransactionRetriesCount()); } @Override public void rollback() { backend.rollback(); } @Override public Inventory boundInventory() { return cloneWith(adaptTransactionConstructor(fakeTxCtor)); } } private class ManyTxTransactionFrame implements TransactionFrame { private Transaction.PreCommit<E> activePrecommit; private final TransactionConstructor<E> notifsStashingTxCtor = (b, p) -> { if (activePrecommit == null) { activePrecommit = p; } BaseInventory.HidingPrecommit<E> hidingPrecommit = new BaseInventory.HidingPrecommit<>(); return new BackendTransaction<>(new DelegatingInventoryBackend<E>(backend.startTransaction()) { @Override public void commit() throws CommitFailureException { hidingPrecommit.getHiddenActions().forEach(activePrecommit::addAction); hidingPrecommit.getHiddenNotifications().forEach(activePrecommit::addNotifications); super.commit(); } }, hidingPrecommit); }; @Override public void commit() throws CommitException { //we need to start a new transaction for the actions to run in... The actual payloads are already //committed. //if there are no actions to commit, bail out quickly if (activePrecommit == null) { return; } Transaction<E> tx = null; try { tx = tenantContext.startTransaction(); activePrecommit.initialize(BaseInventory.this.keepTransaction(tx), tx); for(Consumer<Transaction<E>> action : activePrecommit.getActions()) { action.accept(tx); } tx.directAccess().commit(); } catch (Throwable t) { if (tx != null) { tx.directAccess().rollback(); } throw new CommitException(t); } activePrecommit.getFinalNotifications().forEach(tenantContext::notifyAll); } @Override public void rollback() { //This is a poor mans rollback... because the individual actions in this frame actually commit //on their own and we only stash away the actions and notifications to be sent, in the case of //rollback, we need to "complete" what's already committed by emitting the notifications. commit(); } @Override public Inventory boundInventory() { return cloneWith(adaptTransactionConstructor(notifsStashingTxCtor)); } } }