/* * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html */ package org.opendaylight.openflowplugin.openflow.md.core.sal; import com.google.common.util.concurrent.ListeningExecutorService; import java.math.BigInteger; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; import org.mockito.stubbing.Answer; import org.opendaylight.controller.md.sal.binding.api.DataBroker; import org.opendaylight.controller.sal.binding.api.BindingAwareBroker.RoutedRpcRegistration; import org.opendaylight.controller.sal.binding.api.NotificationProviderService; import org.opendaylight.controller.sal.binding.api.RpcProviderRegistry; import org.opendaylight.openflowjava.protocol.api.connection.ConnectionAdapter; import org.opendaylight.openflowplugin.api.OFConstants; import org.opendaylight.openflowplugin.api.openflow.md.core.ConnectionConductor; import org.opendaylight.openflowplugin.api.openflow.md.core.NotificationEnqueuer; import org.opendaylight.openflowplugin.api.openflow.md.core.session.SessionContext; import org.opendaylight.openflowplugin.api.openflow.md.core.session.SwitchSessionKeyOF; import org.opendaylight.openflowplugin.openflow.md.core.ThreadPoolLoggingExecutor; import org.opendaylight.openflowplugin.openflow.md.core.sal.convertor.ConvertorManager; import org.opendaylight.openflowplugin.openflow.md.core.sal.convertor.ConvertorManagerFactory; import org.opendaylight.openflowplugin.openflow.md.core.session.OFSessionUtil; import org.opendaylight.yang.gen.v1.urn.opendaylight.openflow.common.types.rev130731.Capabilities; import org.opendaylight.yang.gen.v1.urn.opendaylight.openflow.protocol.rev130731.GetFeaturesOutput; import org.opendaylight.yang.gen.v1.urn.opendaylight.openflow.protocol.rev130731.GetFeaturesOutputBuilder; import org.opendaylight.yangtools.yang.binding.RpcService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Created by Martin Bobak mbobak@cisco.com on 9/22/14. */ @RunWith(MockitoJUnitRunner.class) public class ConcurrentSalRegistrationManagerTest { /** registration related action must end within this amount of seconds */ private static final int REGISTRATION_ACTION_TIMEOUT = 5; protected SalRegistrationManager registrationManager; protected static final Logger LOG = LoggerFactory.getLogger(ConcurrentSalRegistrationManagerTest.class); protected static final SwitchSessionKeyOF SWITCH_SESSION_KEY_OF = new SwitchSessionKeyOF(); private static final long THREAD_SLEEP_MILLIS = 100; private static final String DELAYED_THREAD = "DELAYED_THREAD"; private static final String NO_DELAY_THREAD = "NO_DELAY_THREAD"; private ThreadPoolCollectingExecutor taskExecutor; @Mock protected SessionContext context; @Mock private ConnectionConductor connectionConductor; @Mock private ListeningExecutorService rpcPool; @Mock private NotificationProviderService notificationProviderService; @Mock private RpcProviderRegistry rpcProviderRegistry; @Mock private DataBroker dataBroker; @Mock private NotificationEnqueuer notificationEnqueuer; @Mock private ConnectionAdapter connectionAdapter; private GetFeaturesOutput features; /** * prepare surrounding objects */ @Before public void setUp() { final ConvertorManager convertorManager = ConvertorManagerFactory.createDefaultManager(); registrationManager = new SalRegistrationManager(convertorManager); SWITCH_SESSION_KEY_OF.setDatapathId(BigInteger.ONE); Mockito.when(context.getNotificationEnqueuer()).thenReturn(notificationEnqueuer); // features mockery features = new GetFeaturesOutputBuilder() .setVersion(OFConstants.OFP_VERSION_1_3) .setDatapathId(BigInteger.valueOf(42)) .setCapabilities(new Capabilities(true, true, true, true, true, true, true)) .build(); Mockito.when(context.getFeatures()).thenReturn(features); Mockito.when(context.getPrimaryConductor()).thenReturn(connectionConductor); Mockito.when(context.getSessionKey()).thenReturn(SWITCH_SESSION_KEY_OF); Mockito.when(connectionConductor.getVersion()).thenReturn(OFConstants.OFP_VERSION_1_3); // provider context - registration responder Mockito.when(rpcProviderRegistry.addRoutedRpcImplementation(Matchers.<Class<RpcService>>any(), Matchers.any(RpcService.class))) .then(new Answer<RoutedRpcRegistration<?>>() { @Override public RoutedRpcRegistration<?> answer(InvocationOnMock invocation) { if (Thread.currentThread().getName().equals(DELAYED_THREAD)) { try { LOG.info(String.format("Will wait for %d millis", THREAD_SLEEP_MILLIS/10)); Thread.sleep(THREAD_SLEEP_MILLIS); } catch (InterruptedException e) { LOG.error("delaying of worker thread [{}] failed.", Thread.currentThread().getName(), e); } } Object[] args = invocation.getArguments(); RoutedRpcRegistration<RpcService> registration = Mockito.mock(RoutedRpcRegistration.class); Mockito.when(registration.getInstance()).thenReturn((RpcService) args[1]); return registration; } }); Mockito.when(connectionConductor.getConnectionAdapter()).thenReturn(connectionAdapter); Mockito.when(connectionAdapter.getRemoteAddress()).thenReturn(new InetSocketAddress("10.1.2.3", 4242)); taskExecutor = new ThreadPoolCollectingExecutor( 2, 2, 0, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(2), "junit"); registrationManager.setRpcProviderRegistry(rpcProviderRegistry); registrationManager.setDataService(dataBroker); registrationManager.setPublishService(notificationProviderService); registrationManager.init(); OFSessionUtil.getSessionManager().setRpcPool(rpcPool); } /** * clean up * @throws InterruptedException */ @After public void tearDown() throws InterruptedException { taskExecutor.shutdown(); taskExecutor.awaitTermination(1, TimeUnit.SECONDS); if (!taskExecutor.isTerminated()) { taskExecutor.shutdownNow(); } LOG.info("All tasks have finished."); LOG.info("amount of scheduled threads: {}, exited threads: {}, failed threads: {}", taskExecutor.getTaskCount(), taskExecutor.getThreadExitCounter(), taskExecutor.getFailLogbook().size()); for (String exitStatus : taskExecutor.getFailLogbook()) { LOG.debug(exitStatus); } OFSessionUtil.releaseSessionManager(); Assert.assertTrue("there should not be any failed threads in the pool", taskExecutor.getFailLogbook().isEmpty()); Assert.assertTrue("there should not be any living thread in the pool", taskExecutor.getActiveCount() == 0); } /** * Test method which verifies that session could not be invalidated while in creation. * @throws InterruptedException * @throws TimeoutException * @throws ExecutionException */ @Test public void testConcurrentRemoveSessionContext() throws InterruptedException, ExecutionException, TimeoutException { // run registrations Callable<Void> delayedThread = new Callable<Void>() { @Override public Void call() { LOG.info("Delayed session adding thread started."); Thread.currentThread().setName(DELAYED_THREAD); OFSessionUtil.getSessionManager().addSessionContext(SWITCH_SESSION_KEY_OF, context); LOG.info("Delayed session adding thread finished."); return null; } }; Callable<Void> noDelayThread = new Callable<Void>() { @Override public Void call() { LOG.info("Session removing thread started."); Thread.currentThread().setName(NO_DELAY_THREAD); OFSessionUtil.getSessionManager().invalidateSessionContext(SWITCH_SESSION_KEY_OF); LOG.info("Session removing thread finished."); return null; } }; Future<Void> addSessionResult = taskExecutor.submit(delayedThread); Future<Void> removeSessionResult = taskExecutor.submit(noDelayThread); addSessionResult.get(REGISTRATION_ACTION_TIMEOUT, TimeUnit.SECONDS); removeSessionResult.get(REGISTRATION_ACTION_TIMEOUT, TimeUnit.SECONDS); } private static class ThreadPoolCollectingExecutor extends ThreadPoolLoggingExecutor { private List<String> failLogbook; private int threadExitCounter = 0; /** * @param corePoolSize * @param maximumPoolSize * @param keepAliveTime * @param unit * @param workQueue * @param poolName */ public ThreadPoolCollectingExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, String poolName) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, poolName); failLogbook = Collections.synchronizedList(new ArrayList<String>()); } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); threadExitCounter ++; if (t != null) { failLogbook.add("job ["+r+"] exited with throwable:" + t.getMessage()); } } /** * @return the chronicles */ public List<String> getFailLogbook() { return failLogbook; } /** * @return the threadExitCounter */ public int getThreadExitCounter() { return threadExitCounter; } } }