/* * 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.api.test; import static org.hawkular.inventory.api.Action.created; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Collections; 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.Interest; import org.hawkular.inventory.api.Inventory; import org.hawkular.inventory.api.TransactionFrame; import org.hawkular.inventory.api.model.Tenant; import org.hawkular.inventory.base.BaseInventory; import org.hawkular.inventory.base.EntityAndPendingNotifications; import org.hawkular.inventory.base.Transaction; import org.hawkular.inventory.base.TransactionConstructor; import org.hawkular.inventory.base.spi.CommitFailureException; import org.hawkular.inventory.base.spi.InventoryBackend; import org.hawkular.inventory.paths.CanonicalPath; import org.hawkular.inventory.paths.SegmentType; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; /** * @author Lukas Krejci * @since 0.13.0 */ public class PreCommitActionsTest { @Test public void testPreCommitActionsInSingleActionTransaction() throws Exception { @SuppressWarnings("unchecked") InventoryBackend<String> backend = Mockito.mock(InventoryBackend.class); //holders for counters that we are going to be modifying in inner classes int[] payloadsExecuted = new int[1]; PrecommitTracker pct = new PrecommitTracker(); when(backend.startTransaction()).then(a -> { payloadsExecuted[0]++; Assert.assertEquals(0, pct.actionsObtained); return backend; }); when(backend.persist(any(), any(), any())).thenAnswer((args) -> args.getArguments()[1].toString()); commonBackendMocks(backend); doAnswer((args) -> { Assert.assertEquals(1, payloadsExecuted[0]); Assert.assertEquals(1, pct.actionsObtained); Assert.assertEquals(0, pct.finalNotificationsObtained); return null; }).when(backend).commit(); TestInventory inv = new TestInventory(backend, pct); inv.initialize(new Configuration(null, null, Collections.emptyMap())); inv.tenants().create(Tenant.Blueprint.builder().withId("asdf").build()); Assert.assertEquals(1, payloadsExecuted[0]); Assert.assertEquals(1, pct.actionsObtained); Assert.assertEquals(1, pct.finalNotificationsObtained); } @Test public void testPreCommitActionsInTransactionFrame() throws Exception { @SuppressWarnings("unchecked") InventoryBackend<String> backend = Mockito.mock(InventoryBackend.class); when(backend.startTransaction()).thenReturn(backend); //holders for counters that we are going to be modifying in inner classes int[] dataPersisted = new int[1]; int[] notifsSent = new int[1]; PrecommitTracker pct = new PrecommitTracker(); TestInventory inv = new TestInventory(backend, pct); inv.observable(Interest.in(Tenant.class).being(created())).subscribe(t -> notifsSent[0]++); //this way we check that the backend is actually contacted twice to persist the tenants, which would normally //cause 2 transactions to be committed. when(backend.persist(any(), any(), any())).thenAnswer((args) -> { dataPersisted[0]++; return args.getArguments()[1].toString(); }); commonBackendMocks(backend); //simulate the commit and check for correct behavior //noinspection Duplicates doAnswer((args) -> { Assert.assertEquals(2, dataPersisted[0]); //even though we had 2 calls to inventory (persisted 2 pieces of data) //the actions should be obtained only once - at the actual commit of the frame. Assert.assertEquals(1, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); return null; }).when(backend).commit(); inv.initialize(new Configuration(null, null, Collections.emptyMap())); TransactionFrame frame = inv.newTransactionFrame(); Inventory inv2 = frame.boundInventory(); //these two invocations do not really commit, because they're executed through an inventory that is bound to //a transaction frame. inv2.tenants().create(Tenant.Blueprint.builder().withId("asdf").build()); inv2.tenants().create(Tenant.Blueprint.builder().withId("asdf2").build()); //now commit the frame frame.commit(); Assert.assertEquals(2, dataPersisted[0]); Assert.assertEquals(1, pct.actionsObtained); Assert.assertEquals(2, notifsSent[0]); } @Test public void testPreCommitActionsInRetriedTransaction() throws Exception { @SuppressWarnings("unchecked") InventoryBackend<String> backend = Mockito.mock(InventoryBackend.class); when(backend.startTransaction()).thenReturn(backend); //holders for counters that we are going to be modifying in inner classes int[] dataPersisted = new int[1]; int[] notifsSent = new int[1]; PrecommitTracker pct = new PrecommitTracker(); TestInventory inv = new TestInventory(backend, pct); inv.observable(Interest.in(Tenant.class).being(created())).subscribe(t -> notifsSent[0]++); //noinspection Duplicates doAnswer((args) -> { Assert.assertEquals(1, dataPersisted[0]); Assert.assertEquals(1, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); throw new CommitFailureException(); }).doAnswer((args) -> { Assert.assertEquals(2, dataPersisted[0]); Assert.assertEquals(2, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); throw new CommitFailureException(); }).doAnswer((args) -> { Assert.assertEquals(3, dataPersisted[0]); Assert.assertEquals(3, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); return null; }).when(backend).commit(); when(backend.persist(any(), any(), any())).thenAnswer((args) -> { dataPersisted[0]++; return args.getArguments()[1].toString(); }); commonBackendMocks(backend); inv.initialize(new Configuration(null, null, Collections.emptyMap())); inv.tenants().create(Tenant.Blueprint.builder().withId("asdf").build()); Assert.assertEquals(3, dataPersisted[0]); Assert.assertEquals(3, pct.actionsObtained); Assert.assertEquals(1, notifsSent[0]); } @Test public void testPreCommitInRetriedTransactionFrame() throws Exception { @SuppressWarnings("unchecked") InventoryBackend<String> backend = Mockito.mock(InventoryBackend.class); when(backend.startTransaction()).thenReturn(backend); //holders for counters that we are going to be modifying in inner classes int[] dataPersisted = new int[1]; int[] notifsSent = new int[1]; PrecommitTracker pct = new PrecommitTracker(); TestInventory inv = new TestInventory(backend, pct); inv.observable(Interest.in(Tenant.class).being(created())).subscribe(t -> notifsSent[0]++); //noinspection Duplicates doAnswer((args) -> { Assert.assertEquals(2, dataPersisted[0]); Assert.assertEquals(1, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); throw new CommitFailureException(); }).doAnswer((args) -> { Assert.assertEquals(4, dataPersisted[0]); Assert.assertEquals(2, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); throw new CommitFailureException(); }).doAnswer((args) -> { Assert.assertEquals(6, dataPersisted[0]); Assert.assertEquals(3, pct.actionsObtained); Assert.assertEquals(0, notifsSent[0]); return null; }).when(backend).commit(); when(backend.persist(any(), any(), any())).thenAnswer((args) -> { dataPersisted[0]++; return args.getArguments()[1].toString(); }); commonBackendMocks(backend); inv.initialize(new Configuration(null, null, Collections.emptyMap())); TransactionFrame frame = inv.newTransactionFrame(); Inventory inv2 = frame.boundInventory(); inv2.tenants().create(Tenant.Blueprint.builder().withId("asdf").build()); inv2.tenants().create(Tenant.Blueprint.builder().withId("asdf2").build()); frame.commit(); Assert.assertEquals(6, dataPersisted[0]); Assert.assertEquals(3, pct.actionsObtained); Assert.assertEquals(2, notifsSent[0]); //check that we had exactly 3 commit attemps verify(backend, times(3)).commit(); } private void commonBackendMocks(InventoryBackend<String> backend) throws Exception { when(backend.isUniqueIndexSupported()).thenReturn(true); when(backend.isPreferringBigTransactions()).thenReturn(true); when(backend.extractId(any())).thenAnswer((args) -> CanonicalPath.fromString(args.getArguments()[0] .toString()).getSegment().getElementId()); when(backend.extractType(any())).thenAnswer(args -> { SegmentType t = CanonicalPath.fromString(args.getArgumentAt(0, String.class)).getSegment().getElementType(); return Inventory.types().bySegment(t).getElementType(); }); when(backend.find(any(), any())).thenAnswer(args -> args.getArgumentAt(1, CanonicalPath.class).toString()); when(backend.convert(any(), any(), any())).thenAnswer(args -> { Class<?> type = args.getArgumentAt(2, Class.class); String path = args.getArgumentAt(1, String.class); Constructor<?> ctor = type.getDeclaredConstructor(); ctor.setAccessible(true); Object entity = ctor.newInstance(); Field pathF = null; while (true) { try { pathF = type.getDeclaredField("path"); break; } catch (NoSuchFieldException e) { type = type.getSuperclass(); } } pathF.setAccessible(true); pathF.set(entity, CanonicalPath.fromString(path)); return entity; }); } private static final class PrecommitTracker implements Function<Transaction.PreCommit<String>, Transaction.PreCommit<String>> { int actionsObtained; int finalNotificationsObtained; int initialized; int reset; @Override public Transaction.PreCommit<String> apply(Transaction.PreCommit<String> pc) { return new Transaction.PreCommit<String>() { @Override public List<EntityAndPendingNotifications<String, ?>> getFinalNotifications() { finalNotificationsObtained++; return pc.getFinalNotifications(); } @Override public void initialize(Inventory inventory, Transaction<String> tx) { if (this.getClass() != pc.getClass()) { initialized++; } pc.initialize(inventory, tx); } @Override public void reset() { if (this.getClass() != pc.getClass()) { reset++; } pc.reset(); } @Override public void addAction(Consumer<Transaction<String>> action) { pc.addAction(action); } @Override public List<Consumer<Transaction<String>>> getActions() { if (this.getClass() != pc.getClass()) { actionsObtained++; } return pc.getActions(); } @Override public void addNotifications(EntityAndPendingNotifications<String, ?> element) { pc.addNotifications(element); } @Override public void addProcessedNotifications(EntityAndPendingNotifications<String, ?> element) { pc.addProcessedNotifications(element); } }; } } private static final class TestInventory extends BaseInventory<String> { private final InventoryBackend<String> backend; private final PrecommitTracker tracker; public TestInventory(InventoryBackend<String> backend, PrecommitTracker tracker) { super((b, p) -> { Transaction.PreCommit<String> realPrecommit = tracker.apply(p); return TransactionConstructor.<String>startInBackend().construct(b, realPrecommit); }); this.backend = backend; this.tracker = tracker; } private TestInventory(BaseInventory<String> orig, InventoryBackend<String> backend, TransactionConstructor<String> transactionConstructor, PrecommitTracker tracker) { super(orig, backend, transactionConstructor); this.backend = backend; this.tracker = tracker; } @Override protected BaseInventory<String> cloneWith(TransactionConstructor<String> transactionCtor) { return new TestInventory(this, backend, transactionCtor, tracker); } @Override protected TransactionConstructor<String> adaptTransactionConstructor(TransactionConstructor<String> txCtor) { return (b, p) -> { Transaction.PreCommit<String> realPrecommit = tracker.apply(p); return txCtor.construct(b, realPrecommit); }; } @Override protected InventoryBackend<String> doInitialize(Configuration configuration) { return backend; } } }