package org.corfudb.runtime.view; import lombok.Data; import lombok.Getter; import org.corfudb.AbstractCorfuTest; import org.corfudb.infrastructure.BaseServer; import org.corfudb.infrastructure.IServerRouter; import org.corfudb.infrastructure.LayoutServer; import org.corfudb.infrastructure.LogUnitServer; import org.corfudb.infrastructure.ManagementServer; import org.corfudb.infrastructure.SequencerServer; import org.corfudb.infrastructure.ServerContext; import org.corfudb.infrastructure.ServerContextBuilder; import org.corfudb.infrastructure.TestServerRouter; import org.corfudb.protocols.wireprotocol.CorfuMsgType; import org.corfudb.protocols.wireprotocol.LayoutBootstrapRequest; import org.corfudb.runtime.CorfuRuntime; import org.corfudb.runtime.clients.BaseClient; import org.corfudb.runtime.clients.IClientRouter; import org.corfudb.runtime.clients.LayoutClient; import org.corfudb.runtime.clients.LogUnitClient; import org.corfudb.runtime.clients.ManagementClient; import org.corfudb.runtime.clients.SequencerClient; import org.corfudb.runtime.clients.TestClientRouter; import org.corfudb.runtime.clients.TestRule; import org.junit.After; import org.junit.Before; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; /** * This class serves as a base class for most higher-level Corfu unit tests * providing several helper functions to reduce boilerplate code. * * For most tests, a CorfuRuntime can be obtained by calling getDefaultRuntime(). * This instantiates a single-node in-memory Corfu server at port 9000, already * bootstrapped. If getDefaultRuntime() is not called, then no servers are * started. * * For all other tests, servers can be started using the addServer(port, options) * function. The bootstrapAllServers(layout) function can be used to * bootstrap the servers with a specific layout. These servers can be referred * to by a CorfuRuntime using the "test:<port number>" convention. For example, * calling new CorfuRuntime("test:9000"); will connect a CorfuRuntime to the * test server at port 9000. * * To access servers, call the getLogUnit(port), getLayoutServer(port) and * getSequencer(port). This allows access to the server class public fields * and methods. * * In addition to simulating Corfu servers, this class also permits installing * special rules, which can be used to simulate failures or reorder messages. * To install, use the addClientRule(testRule) and addServerRule(testRule) * methods. * * Created by mwei on 12/22/15. */ public abstract class AbstractViewTest extends AbstractCorfuTest { /** The runtime generated by default, by getDefaultRuntime(). */ @Getter CorfuRuntime runtime; /** A map of the current test servers, by endpoint name */ final Map<String, TestServer> testServerMap = new ConcurrentHashMap<>(); /** A map of maps to endpoint->routers, mapped for each runtime instance captured */ final Map<CorfuRuntime, Map<String, TestClientRouter>> runtimeRouterMap = new ConcurrentHashMap<>(); /** Initialize the AbstractViewTest. */ public AbstractViewTest() { // Force all new CorfuRuntimes to override the getRouterFn CorfuRuntime.overrideGetRouterFunction = this::getRouterFunction; runtime = new CorfuRuntime(getDefaultEndpoint()); // Default number of times to read before hole filling to 0 // (most aggressive, to surface concurrency issues). runtime.getParameters().setHoleFillRetry(0); } /** Function for obtaining a router, given a runtime and an endpoint. * * @param runtime The CorfuRuntime to obtain a router for. * @param endpoint An endpoint string for the router. * @return */ private IClientRouter getRouterFunction(CorfuRuntime runtime, String endpoint) { runtimeRouterMap.putIfAbsent(runtime, new ConcurrentHashMap<>()); if (!endpoint.startsWith("test:")) { throw new RuntimeException("Unsupported endpoint in test: " + endpoint); } return runtimeRouterMap.get(runtime).computeIfAbsent(endpoint, x -> { TestClientRouter tcn = new TestClientRouter(testServerMap.get(endpoint).getServerRouter()); tcn.addClient(new BaseClient()) .addClient(new SequencerClient()) .addClient(new LayoutClient()) .addClient(new LogUnitClient()) .addClient(new ManagementClient()); return tcn; } ); } /** * Before each test, reset the tests. */ @Before public void resetTests() { testServerMap.clear(); runtime.parseConfigurationString(getDefaultConfigurationString()); // .setCacheDisabled(true); // Disable cache during unit tests to fully stress the system. runtime.getAddressSpaceView().resetCaches(); } @After public void cleanupBuffers() { testServerMap.values().stream().forEach(x -> { x.getLogUnitServer().shutdown(); x.getManagementServer().shutdown(); }); // Abort any active transactions... while (runtime.getObjectsView().TXActive()) { runtime.getObjectsView().TXAbort(); } } /** Add a server at a specific port, using the given configuration options. * * @param port The port to use. * @param config The configuration to use for the server. */ public void addServer(int port, Map<String, Object> config) { addServer(port, new ServerContext(config, new TestServerRouter(port))); } /** * Add a server to a specific port, using the given ServerContext. * @param port * @param serverContext */ public void addServer(int port, ServerContext serverContext) { new TestServer(serverContext).addToTest(port, this); } /** Add a default, in-memory unbootstrapped server at a specific port. * * @param port The port to use. */ public void addServer(int port) { new TestServer(new ServerContextBuilder().setSingle(false).setServerRouter(new TestServerRouter(port)).setPort(port).build()).addToTest(port, this); } /** Add a default, in-memory bootstrapped single node server at a specific port. * * @param port The port to use. */ public void addSingleServer(int port) { new TestServer(port).addToTest(port, this); } /** Get a instance of a test server, which provides access to the underlying components and server router. * * @param port The port of the test server to retrieve. * @return A test server instance. */ public TestServer getServer(int port) { return testServerMap.get("test:" + port); } /** Get a instance of a logging unit, given a port. * * @param port The port of the logging unit to retrieve. * @return A logging unit instance. */ public LogUnitServer getLogUnit(int port) { return getServer(port).getLogUnitServer(); } /** Get a instance of a sequencer, given a port. * * @param port The port of the sequencer to retrieve. * @return A sequencer instance. */ public SequencerServer getSequencer(int port) { return getServer(port).getSequencerServer(); } /** Get a instance of a layout server, given a port. * * @param port The port of the layout server to retrieve. * @return A layout server instance. */ public LayoutServer getLayoutServer(int port) { return getServer(port).getLayoutServer(); } /** * Get an instance of the management server, given a port * * @param port The port of the management server to retrieve * @return A management server instance. */ public ManagementServer getManagementServer(int port) { return getServer(port).getManagementServer(); } /** Get a instance of base server, given a port. * * @param port The port of the base server to retrieve. * @return A base server instance. */ public BaseServer getBaseServer(int port) { return getServer(port).getBaseServer(); } public IServerRouter getServerRouter(int port) { return getServer(port).getServerRouter(); } /** Bootstraps all servers with a particular layout. * * @param l The layout to bootstrap all servers with. */ public void bootstrapAllServers(Layout l) { testServerMap.entrySet().parallelStream() .forEach(e -> { e.getValue().layoutServer .handleMessage(CorfuMsgType.LAYOUT_BOOTSTRAP.payloadMsg(new LayoutBootstrapRequest(l)), null, e.getValue().serverRouter); e.getValue().managementServer .handleMessage(CorfuMsgType.MANAGEMENT_BOOTSTRAP_REQUEST.payloadMsg(l), null, e.getValue().serverRouter); }); } /** Get a default CorfuRuntime. The default CorfuRuntime is connected to a single-node * in-memory server at port 9000. * @return A default CorfuRuntime */ public CorfuRuntime getDefaultRuntime() { if (!testServerMap.containsKey(getEndpoint(SERVERS.PORT_0))) { addSingleServer(SERVERS.PORT_0); } return getRuntime().connect(); } /** * Create a runtime based on the provided layout. * @param l * @return */ public CorfuRuntime getRuntime(Layout l) { String cfg = l.getLayoutServers().stream().collect(Collectors.joining(",")); return new CorfuRuntime(cfg); } /** Clear installed rules for the default runtime. */ public void clearClientRules() { clearClientRules(getRuntime()); } /** Clear installed rules for a given runtime. * * @param r The runtime to clear rules for. */ public void clearClientRules(CorfuRuntime r) { runtimeRouterMap.get(r).values().forEach(x -> x.rules.clear()); } /** Add a rule for the default runtime. * * @param rule The rule to install */ public void addClientRule(TestRule rule) { addClientRule(getRuntime(), rule); } /** Add a rule for a particular runtime. * * @param r The runtime to install the rule to * @param rule The rule to install. */ public void addClientRule(CorfuRuntime r, TestRule rule) { runtimeRouterMap.get(r).values().forEach(x -> x.rules.add(rule)); } /** Add a rule to a particular router in a particular runtime. * * @param r The runtime to install the rule to * @param clientRouterEndpoint The Client router endpoint to install the rule to * @param rule The rule to install. */ public void addClientRule(CorfuRuntime r, String clientRouterEndpoint, TestRule rule) { runtimeRouterMap.get(r).get(clientRouterEndpoint).rules.add(rule); } /** Clear rules for a particular server. * * @param port The port of the server to clear rules for. */ public void clearServerRules(int port) { getServer(port).getServerRouter().rules.clear(); } /** Install a rule to a particular server. * * @param port The port of the server to install the rule to. * @param rule The rule to install. */ public void addServerRule(int port, TestRule rule) { getServer(port).getServerRouter().rules.add(rule); } /** The configuration string used for the default runtime. * * @return The configuration string used for the default runtime. */ public String getDefaultConfigurationString() { return getDefaultEndpoint(); } /** The default endpoint (single server) used for the default runtime. * * @return Returns the default endpoint. */ public String getDefaultEndpoint() { return getEndpoint(SERVERS.PORT_0); } /** Get the endpoint string, given a port number. * * @param port The port number to get an endpoint string for. * @return The endpoint string. */ public String getEndpoint(int port) { return "test:" + port; } // Private /** * This class holds instances of servers used for test. */ @Data private static class TestServer { ServerContext serverContext; BaseServer baseServer; SequencerServer sequencerServer; LayoutServer layoutServer; LogUnitServer logUnitServer; ManagementServer managementServer; IServerRouter serverRouter; int port; TestServer(Map<String, Object> optsMap) { this(new ServerContext(optsMap, new TestServerRouter())); } TestServer(ServerContext serverContext) { this.serverContext = serverContext; this.serverRouter = serverContext.getServerRouter(); this.baseServer = new BaseServer(); this.sequencerServer = new SequencerServer(serverContext); this.layoutServer = new LayoutServer(serverContext); this.logUnitServer = new LogUnitServer(serverContext); this.managementServer = new ManagementServer(serverContext); this.serverRouter.addServer(baseServer); this.serverRouter.addServer(sequencerServer); this.serverRouter.addServer(layoutServer); this.serverRouter.addServer(logUnitServer); this.serverRouter.addServer(managementServer); } TestServer(int port) { this(ServerContextBuilder.defaultContext(port).getServerConfig()); } void addToTest(int port, AbstractViewTest test) { if (test.testServerMap.putIfAbsent("test:" + port, this) != null) { throw new RuntimeException("Server already registered at port " + port); } } public TestServerRouter getServerRouter() { return (TestServerRouter) this.serverRouter; } } }