blob: 2e69c7dbc02bab18b739a62f54c094c754ec5b02 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.sling.discovery.base.its;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import org.apache.sling.commons.testing.junit.categories.Slow;
import org.apache.sling.discovery.InstanceDescription;
import org.apache.sling.discovery.TopologyEvent;
import org.apache.sling.discovery.TopologyEvent.Type;
import org.apache.sling.discovery.TopologyEventListener;
import org.apache.sling.discovery.TopologyView;
import org.apache.sling.discovery.base.its.setup.VirtualInstance;
import org.apache.sling.discovery.base.its.setup.VirtualInstanceBuilder;
import org.apache.sling.testing.tools.retry.RetryLoop;
import org.apache.sling.testing.tools.retry.RetryLoop.Condition;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This base tests for discovery.impl and .oak simulates instances
* that are coming and going, have a slow repository (and other bad-weather
* scenarios), and make assumptions on both getTopology() to always be correct
* as well as handleTopologyEvent to be properly delivered to.
*/
public abstract class AbstractDiscoveryServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
class Tester implements TopologyEventListener, Runnable {
private final VirtualInstance instance;
private final Thread thread;
private TopologyEvent previousEvent;
private List<String> failures = new LinkedList<String>();
private volatile boolean stopped = false;
private final long pollingSleep;
private boolean running = true;
Tester(VirtualInstance instance, long pollingSleep) throws Throwable {
this.instance = instance;
this.pollingSleep = pollingSleep;
instance.bindTopologyEventListener(this);
thread = new Thread(this);
thread.setName("Tester-"+instance.getDebugName()+"-thread");
thread.setDaemon(true);
thread.start();
}
private synchronized void assertNoFailures() {
if (failures.size()==0) {
return;
}
fail("got "+failures.size()+" failures, the first one thereof: "+failures.get(0));
}
private synchronized boolean hasFailures() {
return failures.size()!=0;
}
private synchronized void asyncFail(String msg) {
failures.add(msg);
}
@Override
public void run() {
while(!stopped) {
try{
TopologyView topo = instance.getDiscoveryService().getTopology();
Thread.sleep(pollingSleep);
} catch(Throwable th) {
asyncFail("Got a Throwable: "+th);
return;
}
}
}
@Override
public void handleTopologyEvent(TopologyEvent event) {
if (hasFailures()) {
// then stop
return;
}
logger.info("handleTopologyEvent["+instance.getDebugName()+"]: "+event);
if (previousEvent == null) {
// then we're expecting a TOPOLOGY_INIT
if (event.getType()!=Type.TOPOLOGY_INIT) {
asyncFail("expected an INIT as the first, but got: "+event);
return;
}
} else if (previousEvent.getType() == Type.TOPOLOGY_CHANGED
|| previousEvent.getType() == Type.PROPERTIES_CHANGED) {
// then expecting a TOPOLOGY_CHANGING or PROPERTIES_CHANGED
if (event.getType()==Type.TOPOLOGY_CHANGING) {
// perfect
} else if (event.getType()==Type.PROPERTIES_CHANGED) {
// perfect
} else {
asyncFail("expected a CHANGING or PROPERTIES_CHANGED, but got: "+event);
return;
}
} else if (previousEvent.getType() == Type.TOPOLOGY_CHANGING) {
// then expecting a TOPOLOGY_CHANGED
if (event.getType()!=Type.TOPOLOGY_CHANGED) {
asyncFail("expected a CHANGED after CHANGING, but got: "+event);
return;
}
}
previousEvent = event;
}
public void shutdown() throws Exception {
stopped = true;
instance.stop();
running = false;
}
public void restart() throws Exception {
instance.startViewChecker((int) instance.getConfig().getConnectorPingInterval());
running = true;
}
public void pause() throws Throwable {
instance.stopViewChecker();
running = false;
}
}
List<Tester> testers = new LinkedList<Tester>();
public abstract VirtualInstanceBuilder newBuilder();
@Before
public void setUp() throws Exception {
testers.clear();
}
@After
public void tearDown() throws Exception {
for (Tester tester : testers) {
tester.shutdown();
}
}
Tester newInstance(String debugName, int interval, int timeout, long pollingSleep, VirtualInstance base) throws Throwable {
VirtualInstanceBuilder builder = newBuilder();
builder.setDebugName(debugName);
if (base == null) {
builder.newRepository("/var/discovery/testing/", true);
} else {
builder.useRepositoryOf(base);
}
builder.setConnectorPingInterval(interval);
builder.setConnectorPingTimeout(timeout);
builder.setMinEventDelay(1);
VirtualInstance instance = builder.build();
Tester t = new Tester(instance, pollingSleep);
testers.add(t);
instance.startViewChecker(interval);
return t;
}
private void assertStableTopology(Tester... instances) {
for (Tester tester : instances) {
logger.info("asserting tester: "+tester.instance.getDebugName());
TopologyEvent lastEvent = tester.previousEvent;
Type type = lastEvent.getType();
if (type == Type.TOPOLOGY_CHANGED || type == Type.TOPOLOGY_INIT) {
// fine
} else {
fail("wrong type, expected CHANGED or INIT, got: "+type);
}
assertNotNull(lastEvent.getNewView());
assertEquals(instances.length, lastEvent.getNewView().getInstances().size());
TopologyView t = tester.instance.getDiscoveryService().getTopology();
assertTrue(t.isCurrent());
assertEquals(instances.length, t.getInstances().size());
}
}
private boolean isStableTopology(Tester... instances) {
for (Tester tester : instances) {
TopologyEvent lastEvent = tester.previousEvent;
if (lastEvent == null) {
return false;
}
Type type = lastEvent.getType();
if (type == Type.TOPOLOGY_CHANGED || type == Type.TOPOLOGY_INIT) {
// fine
} else {
return false;
}
TopologyView newView = lastEvent.getNewView();
if (newView == null) {
return false;
}
if (instances.length != newView.getInstances().size()) {
return false;
}
TopologyView t = tester.instance.getDiscoveryService().getTopology();
if (!t.isCurrent()) {
return false;
}
if (instances.length != t.getInstances().size()) {
return false;
}
for (Tester t2 : instances) {
boolean foundMatch = false;
for (InstanceDescription id : t.getInstances()) {
if (t2.instance.getSlingId().equals(id.getSlingId())) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
return false;
}
}
}
return true;
}
@Test
public void testSingleInstance() throws Throwable {
logger.info("testSingleInstances: start");
Tester single = newInstance("single", 1, 5, 50, null);
single.instance.dumpRepo();
logger.info("testSingleInstances: starting retry loop (10sec max)");
startRetryLoop(testers, 10);
single.assertNoFailures();
assertStableTopology(single);
logger.info("testSingleInstances: end");
}
@Test
public void testTwoInstances() throws Throwable {
logger.info("testTwoInstances: start");
Tester i1 = newInstance("i1", 1, 10, 100, null);
Tester i2 = newInstance("i2", 1, 10, 100, i1.instance);
logger.info("testTwoInstances: starting retry loop (15sec max)");
startRetryLoop(testers, 15);
i1.instance.dumpRepo();
i1.assertNoFailures();
i2.assertNoFailures();
assertStableTopology(i1, i2);
logger.info("testTwoInstances: end");
}
@Test
public void testFiveInstances() throws Throwable {
logger.info("testFiveInstances: start");
Tester i1 = newInstance("i1", 1, 30, 250, null);
for(int i=2; i<=5; i++) {
Tester in = newInstance("i"+i, 1, 30, 250, i1.instance);
}
logger.info("testFiveInstances: starting retry loop (40sec max)");
startRetryLoop(testers, 40);
i1.instance.dumpRepo();
i1.assertNoFailures();
assertStableTopology(testers.toArray(new Tester[0]));
logger.info("testFiveInstances: end");
}
@Category(Slow.class) //TODO: this takes env 10sec
@Test
public void testTenInstances() throws Throwable {
logger.info("testTenInstances: start");
Tester i1 = newInstance("i1", 1, 30, 250, null);
for(int i=2; i<=10; i++) {
Tester in = newInstance("i"+i, 1, 30, 250, i1.instance);
}
logger.info("testTenInstances: starting retry loop (60sec max)");
startRetryLoop(testers, 60);
i1.instance.dumpRepo();
i1.assertNoFailures();
assertStableTopology(testers.toArray(new Tester[0]));
logger.info("testTenInstances: end");
}
@Category(Slow.class) //TODO: this takes env 15sec
@Test
public void testTwentyInstances() throws Throwable {
logger.info("testTwentyInstances: start");
Tester i1 = newInstance("i1", 1, 60, 1000, null);
for(int i=2; i<=20; i++) {
Tester in = newInstance("i"+i, 1, 60, 1000, i1.instance);
}
logger.info("testThirtyInstances: starting retry loop (80 sec max)");
startRetryLoop(testers, 80);
i1.instance.dumpRepo();
i1.assertNoFailures();
assertStableTopology(testers.toArray(new Tester[0]));
logger.info("testTwentyInstances: end");
}
@Category(Slow.class) //TODO: this takes env 40sec
@Test
public void testTwentyFourInstances() throws Throwable {
logger.info("testTwentyFourInstances: start");
Tester i1 = newInstance("i1", 4, 120, 1000, null);
for(int i=2; i<=24; i++) {
Tester in = newInstance("i"+i, 4, 120, 2000, i1.instance);
Thread.sleep(1000);
}
logger.info("testTwentyFourInstances: starting retry loop (180sec max)");
startRetryLoop(testers, 180);
i1.instance.dumpRepo();
i1.assertNoFailures();
assertStableTopology(testers.toArray(new Tester[0]));
logger.info("testTwentyFourInstances: end");
}
private void startRetryLoop(final List<Tester> testers, int retryTimeoutSeconds) {
startRetryLoop(retryTimeoutSeconds, testers.toArray(new Tester[0]));
}
private void startRetryLoop(int retryTimeoutSeconds, final Tester... testers) {
new RetryLoop(new Condition() {
@Override
public String getDescription() {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean isTrue() throws Exception {
if (!isStableTopology(testers)) {
return false;
}
for (Tester tester : testers) {
tester.assertNoFailures();
}
logger.info("retryLoop: declaring stable topology with "+testers.length);
return true;
}
}, retryTimeoutSeconds /*seconds*/, 1000/*millis*/);
}
@Category(Slow.class) //TODO: this takes env 120sec
@Test
public void testStartStopFiesta() throws Throwable {
final Tester[] instances = new Tester[8];
instances[0] = newInstance("i1", 1, 10, 1000, null);
for(int i=2; i<=8; i++) {
instances[i-1] = newInstance("i"+i, 1, 10, 1000, instances[0].instance);
Thread.sleep(600);
}
startRetryLoop(15, instances);
for (Tester tester : instances) {
tester.assertNoFailures();
}
assertStableTopology(testers.toArray(new Tester[0]));
Random r = new Random(123432141);
for(int i=0; i<10; i++) {
logger.info("testStartStopFiesta : loop "+i);
final List<Tester> alive = new LinkedList<Tester>();
int keepAlive = r.nextInt(instances.length);
for(int j=0; j<instances.length; j++) {
if (j == keepAlive || r.nextBoolean()) {
instances[j].restart();
alive.add(instances[j]);
} else {
instances[j].pause();
}
}
logger.info("testStartStopFiesta : loop "+i+", alive-cnt: "+alive.size());
startRetryLoop(alive, 30);
for (Tester tester : instances) {
tester.assertNoFailures();
}
assertStableTopology(alive.toArray(new Tester[0]));
}
}
}