package io.eguan.dtx;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* 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.
* #L%
*/
import static com.hazelcast.core.LifecycleEvent.LifecycleState.SHUTDOWN;
import static com.hazelcast.core.LifecycleEvent.LifecycleState.SHUTTING_DOWN;
import static com.hazelcast.core.LifecycleEvent.LifecycleState.STARTED;
import static com.hazelcast.core.LifecycleEvent.LifecycleState.STARTING;
import static io.eguan.dtx.DtxResourceManagerState.LATE;
import static io.eguan.dtx.DtxResourceManagerState.POST_SYNC_PROCESSING;
import static io.eguan.dtx.DtxResourceManagerState.SYNCHRONIZING;
import static io.eguan.dtx.DtxResourceManagerState.UNDETERMINED;
import static io.eguan.dtx.DtxResourceManagerState.UNREGISTERED;
import static io.eguan.dtx.DtxResourceManagerState.UP_TO_DATE;
import static org.junit.Assert.assertTrue;
import io.eguan.dtx.DtxManager;
import io.eguan.dtx.DtxResourceManager;
import io.eguan.dtx.DtxResourceManagerState;
import io.eguan.dtx.events.DtxEvent;
import io.eguan.dtx.events.DtxResourceManagerEvent;
import io.eguan.dtx.events.HazelcastNodeEvent;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Multimap;
import com.google.common.collect.Table;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.hazelcast.core.LifecycleEvent.LifecycleState;
/**
* Common event listener implementations for testing purposes.
*
* @author oodrive
* @author pwehrle
*
*/
public final class DtxEventListeners {
private DtxEventListeners() {
throw new AssertionError("Not instantiable");
}
/**
* Superclass for all event listeners recording assertion errors within their handling methods.
*
*
*/
abstract static class ErrorCollectionDtxEvtListener {
private final List<AssertionError> assertErrors = new CopyOnWriteArrayList<AssertionError>();
/**
* Registers an {@link AssertionError} to be retrieved by {@link #getAssertErrors()} later.
*
* @param ae
* an {@link AssertionError} triggered during event analysis
* @param event
* the event triggering the error
*/
protected void registerAssertionError(final AssertionError ae, final DtxEvent<?> event) {
assertErrors.add(new AssertionError("Event caused assertion error; event=" + event, ae));
}
/**
* Gets the collected assertion errors.
*
* @return a list of {@link AssertionError}s
*/
final List<AssertionError> getAssertErrors() {
return assertErrors;
}
/**
* Checks for the current presence of assertion errors.
*
* @param targetLogger
* the {@link Logger} for printing detailed error information
* @throws AssertionError
* if any assertion errors are present
*/
final void checkForAssertErrors(final Logger targetLogger) throws AssertionError {
for (final AssertionError error : assertErrors) {
targetLogger.error("State transition assertion failed", error);
}
assertTrue("State transition assertion errors: " + assertErrors.size(), assertErrors.isEmpty());
}
}
/**
* Superclass for event listeners counting down a given {@link CountDownLatch}.
*
*
*/
abstract static class LatchCountingDtxEvtListener extends ErrorCollectionDtxEvtListener {
private static final Logger LOGGER = LoggerFactory.getLogger(LatchCountingDtxEvtListener.class);
private final CountDownLatch latch;
/**
* Internal constructor taking the target {@link CountDownLatch} as argument.
*
* @param latch
* a non-<code>null</code> {@link CountDownLatch}
*/
protected LatchCountingDtxEvtListener(final CountDownLatch latch) {
this.latch = Objects.requireNonNull(latch);
}
/**
* Counts down the latch submitted at construction.
*/
protected void countDownLatch() {
LOGGER.debug("Counting down latch; count=" + latch.getCount());
latch.countDown();
}
}
/**
* Event listener generating an {@link AssertionError} on receiving a predefined set of event types.
*
*
*/
static final class ErrorGeneratingEventListener extends ErrorCollectionDtxEvtListener {
private final List<Class<?>> eventClasses;
/**
* Constructs an instance for a given set of {@link DtxEvent} classes.
*
* @param targetEventClasses
* a set of target {@link Class classes}
*/
ErrorGeneratingEventListener(final Class<?>... targetEventClasses) {
this.eventClasses = Arrays.asList(targetEventClasses);
}
/**
* Intercepts all {@link DtxEvent}s.
*
* @param event
* the target {@link DtxEvent}
*/
@Subscribe
public final void eventOccurred(final DtxEvent<?> event) {
final Class<?> eventClass = event.getClass();
for (final Class<?> currClass : eventClasses) {
if (currClass.isAssignableFrom(eventClass)) {
registerAssertionError(new AssertionError("Event not supposed to occur; event=" + event
+ ", matching event class=" + currClass), event);
}
}
}
};
/**
* Event listener counting resource manager events.
*
*
*/
static final class ResMgrStateCountListener extends LatchCountingDtxEvtListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ResMgrStateCountListener.class);
private final UUID resMgrId;
/**
* Construct an instance for a given latch and an optional resource id.
*
* @param latch
* the target {@link CountDownLatch}
* @param resMgrId
* an optional {@link UUID}
*/
ResMgrStateCountListener(final CountDownLatch latch, final UUID resMgrId) {
super(latch);
this.resMgrId = resMgrId;
}
/**
* Handles all resource manager state changes and registers {@link AssertionError}s for non-authorized state
* transitions.
*
* @param event
* the target {@link DtxResourceManagerEvent}
*/
@Subscribe
public final void resMgrStateChanged(final DtxResourceManagerEvent event) {
final DtxResourceManagerState newState = event.getNewState();
final UUID resId = event.getResourceManagerId();
if ((resMgrId != null) && !resMgrId.equals(resId)) {
return;
}
final DtxResourceManagerState previousState = event.getPreviousState();
try {
// asserts expected state transitions
assertResMgrStateTransitions(previousState, newState);
}
catch (final AssertionError e) {
LOGGER.error("Assertion failed; resId=" + resId + ", previous=" + previousState + ", new=" + newState,
e);
registerAssertionError(e, event);
}
countDownLatch();
return;
}
}
/**
* Event listener counting Hazelcast cluster node events.
*
*
*/
static final class HazelcastNodeCountListener extends LatchCountingDtxEvtListener {
/**
* Constructs an instance with a given latch.
*
* @param latch
* the target {@link CountDownLatch}
*/
HazelcastNodeCountListener(final CountDownLatch latch) {
super(latch);
}
/**
* Handles all {@link HazelcastNodeEvent}s.
*
* @param event
* the triggered {@link HazelcastNodeEvent}
*/
@Subscribe
public final void nodeStateChanged(final HazelcastNodeEvent event) {
final LifecycleState previousState = event.getPreviousState();
final LifecycleState newState = event.getNewState();
// asserts expected state transitions
assertTrue(null != previousState || STARTING == newState || STARTED == newState);
assertTrue(STARTING != previousState || STARTED == newState);
assertTrue(SHUTTING_DOWN != previousState || SHUTDOWN == newState);
countDownLatch();
return;
}
}
/**
* Event listener checking all state transitions and counting down the latch once for each new transition to a
* target {@link DtxResourceManagerState}.
*
*
*/
static final class StateCountListener extends LatchCountingDtxEvtListener {
private static final Logger LOGGER = LoggerFactory.getLogger(SeparateStateCountListener.class);
private final Multimap<UUID, DtxManager> resMgrMap;
private final DtxResourceManagerState targetState;
/**
* Constructs an instance counting down the given latch for a given set of resource manager IDs.
*
* @param stateCountLatch
* the latch to count down on reaching the target {@link DtxResourceManagerState}
* @param targetState
* the {@link DtxResourceManagerState} to count down upon reaching
* @param resMgrMap
* a {@link Multimap} of resource manager IDs mapped to {@link DtxManager}s
*/
StateCountListener(final CountDownLatch stateCountLatch, final DtxResourceManagerState targetState,
final Multimap<UUID, DtxManager> resMgrMap) {
super(stateCountLatch);
this.resMgrMap = resMgrMap;
this.targetState = targetState;
}
/**
* Handles all events related to a {@link DtxResourceManager}'s state changes.
*
* @param event
* the triggered {@link DtxResourceManagerEvent}
*/
@Subscribe
@AllowConcurrentEvents
public final void resMgrStateChanged(final DtxResourceManagerEvent event) {
LOGGER.debug("State changed; event=" + event);
final DtxManager targetDtxManager = event.getSource();
final UUID targetResMgrId = event.getResourceManagerId();
if (!resMgrMap.get(targetResMgrId).contains(targetDtxManager)) {
return;
}
final DtxResourceManagerState oldState = event.getPreviousState();
final DtxResourceManagerState newState = event.getNewState();
try {
assertResMgrStateTransitions(oldState, newState);
}
catch (final AssertionError ae) {
registerAssertionError(ae, event);
}
if (targetState == newState) {
countDownLatch();
}
}
}
/**
* Event listener checking all state transitions and counting down latches associated to a resource manager and each
* {@link DtxManager} once for each new transition to a target {@link DtxResourceManagerState}.
*
*
*/
static final class SeparateStateCountListener extends ErrorCollectionDtxEvtListener {
private static final Logger LOGGER = LoggerFactory.getLogger(SeparateStateCountListener.class);
private final DtxResourceManagerState targetState;
private final Table<UUID, DtxManager, CountDownLatch> latchTable;
/**
* Constructs an instance counting down the given latch for a given set of resource manager IDs.
*
* @param latchTable
* a table assoicating a resource manager's {@link UUID} and a {@link DtxManager} to a target
* {@link CountDownLatch} to be counted down
* @param targetState
* the {@link DtxResourceManagerState} to count down upon reaching
*/
SeparateStateCountListener(final Table<UUID, DtxManager, CountDownLatch> latchTable,
final DtxResourceManagerState targetState) {
this.latchTable = latchTable;
this.targetState = targetState;
}
/**
* Handles all events related to a {@link DtxResourceManager}'s state changes.
*
* @param event
* the triggered {@link DtxResourceManagerEvent}
*/
@Subscribe
@AllowConcurrentEvents
public final void resMgrStateChanged(final DtxResourceManagerEvent event) {
LOGGER.debug("State changed; event=" + event);
final DtxManager targetDtxManager = event.getSource();
final UUID targetResMgrId = event.getResourceManagerId();
final DtxResourceManagerState oldState = event.getPreviousState();
final DtxResourceManagerState newState = event.getNewState();
try {
assertResMgrStateTransitions(oldState, newState);
}
catch (final AssertionError ae) {
registerAssertionError(ae, event);
}
final CountDownLatch targetLatch = latchTable.get(targetResMgrId, targetDtxManager);
if ((targetLatch != null) && (targetState == newState)) {
targetLatch.countDown();
}
}
}
/**
* Event listener checking all state transitions and counting down the latch once for each new transition to one of
* the target {@link DtxResourceManagerState}s.
*
*
*/
static final class StateSetCountListener extends LatchCountingDtxEvtListener {
private static final Logger LOGGER = LoggerFactory.getLogger(SeparateStateCountListener.class);
private final Multimap<UUID, DtxManager> resMgrMap;
private final List<DtxResourceManagerState> targetStates;
/**
* Constructs an instance counting down the given latch for a given set of resource manager IDs.
*
* @param stateCountLatch
* the latch to count down on reaching the target {@link DtxResourceManagerState}
* @param targetStates
* the {@link List} of {@link DtxResourceManagerState}s to count down upon reaching
* @param resMgrMap
* a {@link Multimap} of resource manager IDs mapped to {@link DtxManager}s
*/
StateSetCountListener(final CountDownLatch stateCountLatch, final List<DtxResourceManagerState> targetStates,
final Multimap<UUID, DtxManager> resMgrMap) {
super(stateCountLatch);
this.resMgrMap = resMgrMap;
this.targetStates = targetStates;
}
/**
* Handles all events related to a {@link DtxResourceManager}'s state changes.
*
* @param event
* the triggered {@link DtxResourceManagerEvent}
*/
@Subscribe
@AllowConcurrentEvents
public final void resMgrStateChanged(final DtxResourceManagerEvent event) {
LOGGER.debug("State changed; event=" + event);
final DtxManager targetDtxManager = event.getSource();
final UUID targetResMgrId = event.getResourceManagerId();
if (!resMgrMap.get(targetResMgrId).contains(targetDtxManager)) {
return;
}
final DtxResourceManagerState oldState = event.getPreviousState();
final DtxResourceManagerState newState = event.getNewState();
try {
assertResMgrStateTransitions(oldState, newState);
}
catch (final AssertionError ae) {
registerAssertionError(ae, event);
}
if (targetStates.contains(newState)) {
countDownLatch();
}
}
}
/**
* Runs a set of assertions covering legal state transitions.
*
* @param previousState
* the initial state
* @param newState
* the newly taken state to validate against possible transitions
* @throws AssertionError
* if an illegal state transition was detected
*/
private static final void assertResMgrStateTransitions(final DtxResourceManagerState previousState,
final DtxResourceManagerState newState) throws AssertionError {
final String errMsg = "Illegal state transition. previous state=" + previousState + ", new state=" + newState;
// UNREGISTERED -> UNDETERMINED
assertTrue(errMsg, UNREGISTERED != previousState || UNDETERMINED == newState);
// UNDETERMINED -> POST_SYNC_PROCESSING | LATE | UNREGISTERED
assertTrue(errMsg, UNDETERMINED != previousState || POST_SYNC_PROCESSING == newState || LATE == newState
|| UNREGISTERED == newState);
// POST_SYNC_PROCESSING -> UP_TO_DATE | UNDETERMINED | UNREGISTERED
assertTrue(errMsg, POST_SYNC_PROCESSING != previousState || UP_TO_DATE == newState || UNDETERMINED == newState
|| UNREGISTERED == newState);
// LATE -> SYNCHRONIZING | UNREGISTERED
assertTrue(errMsg, LATE != previousState || SYNCHRONIZING == newState || UNREGISTERED == newState);
// UP_TO_DATE -> LATE | UNDETERMINED | UNREGISTERED
assertTrue(errMsg, UP_TO_DATE != previousState || LATE == newState || UNDETERMINED == newState
|| UNREGISTERED == newState);
// SYNCHRONIZING -> UNDETERMINED | UNREGISTERED
assertTrue(errMsg, SYNCHRONIZING != previousState || UNDETERMINED == newState || UNREGISTERED == newState);
}
}