/* * Copyright Terracotta, Inc. * * 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.ehcache.clustered.client.internal; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import org.ehcache.clustered.client.internal.lock.VoltronReadWriteLockEntityClientService; import org.ehcache.clustered.client.internal.store.ClusterTierClientEntityService; import org.ehcache.clustered.lock.server.VoltronReadWriteLockServerEntityService; import org.ehcache.clustered.server.ClusterTierManagerServerEntityService; import org.ehcache.clustered.server.store.ClusterTierServerEntityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terracotta.connection.Connection; import org.terracotta.connection.ConnectionException; import org.terracotta.connection.ConnectionPropertyNames; import org.terracotta.connection.ConnectionService; import org.terracotta.connection.entity.Entity; import org.terracotta.connection.entity.EntityRef; import org.terracotta.entity.EntityClientService; import org.terracotta.entity.EntityMessage; import org.terracotta.entity.EntityResponse; import org.terracotta.entity.EntityServerService; import org.terracotta.entity.ServiceProvider; import org.terracotta.entity.ServiceProviderConfiguration; import org.terracotta.exception.EntityNotFoundException; import org.terracotta.exception.EntityNotProvidedException; import org.terracotta.exception.PermanentEntityException; import org.terracotta.offheapresource.OffHeapResourcesProvider; import org.terracotta.offheapresource.config.MemoryUnit; import org.terracotta.offheapresource.config.OffheapResourcesType; import org.terracotta.offheapresource.config.ResourceType; import org.terracotta.passthrough.IAsynchronousServerCrasher; import org.terracotta.passthrough.PassthroughConnection; import org.terracotta.passthrough.PassthroughServer; import org.terracotta.passthrough.PassthroughServerRegistry; import static org.mockito.Mockito.mock; /** * A {@link ConnectionService} implementation used to simulate Voltron server connections for unit testing purposes. * In common usage, this class: * <ol> * <li>is loaded once per JVM (potentially covering all unit tests in a Gradle test run)</li> * <li>is accessed through a * {@link org.terracotta.connection.ConnectionFactory#connect(URI, Properties) ConnectionFactory.connect(URI, Properties)} * method call * </li> * <li>is instantiated by {@link java.util.ServiceLoader} through an {@code Iterator} obtained over * configured {@link ConnectionService} classes * </li> * <li>is selected through calls to the {@link ConnectionService#handlesURI(URI)} method</li> * <li>once selected, is used to obtain a {@link Connection} through the * {@link ConnectionService#connect(URI, Properties)} method</li> * </ol> * Even though a unit test may make a direct reference to this class for configuration purposes, a * {@code ServiceLoader} provider-configuration file for {@code org.terracotta.connection.ConnectionService} * referring to this class must be available in the class path. * * <p> * For use, a unit test should define {@link org.junit.Before @Before} and {@link org.junit.After @After} * methods as in the following examples: * <pre><code> * @Before * public void definePassthroughServer() throws Exception { * UnitTestConnectionService.add(<i>CLUSTER_URI</i>, * new PassthroughServerBuilder() * .resource("primary-server-resource", 64, MemoryUnit.MB) * .resource("secondary-server-resource", 64, MemoryUnit.MB) * .build()); * } * * @After * public void removePassthroughServer() throws Exception { * UnitTestConnectionService.remove(<i>CLUSTER_URI</i>); * } * </code></pre> * * If your configuration uses no server resources, none need be defined. The {@link PassthroughServerBuilder} * can also add Voltron server & client services and service providers. * <p> * Tests needing direct access to a {@link Connection} can obtain a connection using the following: * <pre><code> * Connection connection = new UnitTestConnectionService().connect(<i>CLUSTER_URI</i>, new Properties()); * </code></pre> * after the server has been added to {@code UnitTestConnectionService}. Ideally, this connection should * be explicitly closed when no longer needed but {@link #remove} closes any remaining connections opened * through {@link #connect(URI, Properties)}. * * @see PassthroughServerBuilder */ public class UnitTestConnectionService implements ConnectionService { private static final Logger LOGGER = LoggerFactory.getLogger(UnitTestConnectionService.class); private static final Map<String, StripeDescriptor> STRIPES = new HashMap<String, StripeDescriptor>(); private static final Map<URI, ServerDescriptor> SERVERS = new HashMap<URI, ServerDescriptor>(); private static final String PASSTHROUGH = "passthrough"; /** * Adds a {@link PassthroughServer} if, and only if, a mapping for the {@code URI} supplied does not * already exist. The server is started as it is added. * * @param uri the {@code URI} for the server; only the <i>scheme</i>, <i>host</i>, and <i>port</i> * contribute to the server identification * @param server the {@code PassthroughServer} instance to use for connections to {@code uri} */ public static void add(URI uri, PassthroughServer server) { URI keyURI = createKey(uri); if (SERVERS.containsKey(keyURI)) { throw new AssertionError("Server at " + uri + " already provided; use remove() to remove"); } SERVERS.put(keyURI, new ServerDescriptor(server)); // TODO rework that better server.registerAsynchronousServerCrasher(mock(IAsynchronousServerCrasher.class)); server.start(true, false); LOGGER.info("Started PassthroughServer at {}", keyURI); } public static void addServerToStripe(String stripeName, PassthroughServer server) { if (STRIPES.get(stripeName) == null) { StripeDescriptor stripeDescriptor = new StripeDescriptor(); STRIPES.put(stripeName, stripeDescriptor); } STRIPES.get(stripeName).addServer(server); } public static void removeStripe(String stripeName) { StripeDescriptor stripeDescriptor = STRIPES.remove(stripeName); for (Connection connection : stripeDescriptor.getConnections()) { try { LOGGER.warn("Force close {}", formatConnectionId(connection)); connection.close(); } catch (IllegalStateException e) { // Ignored in case connection is already closed } catch (IOException e) { // Ignored } } stripeDescriptor.removeConnections(); } public static OffheapResourcesType getOffheapResourcesType(String resourceName, int size, MemoryUnit unit) { OffheapResourcesType resources = new OffheapResourcesType(); resources.getResource().add(getResource(resourceName, size, unit)); return resources; } private static ResourceType getResource(String resourceName, int size, MemoryUnit unit) { final ResourceType resource = new ResourceType(); resource.setName(resourceName); resource.setUnit(unit); resource.setValue(BigInteger.valueOf((long)size)); return resource; } /** * Adds a {@link PassthroughServer} if, and only if, a mapping for the URI supplied does not * already exist. The server is started as it is added. * * @param uri the URI, in string form, for the server; only the <i>scheme</i>, <i>host</i>, and <i>port</i> * contribute to the server identification * @param server the {@code PassthroughServer} instance to use for connections to {@code uri} */ public static void add(String uri, PassthroughServer server) { add(URI.create(uri), server); } /** * Removes the {@link PassthroughServer} previously associated with the {@code URI} provided. The * server is stopped as it is removed. In addition to stopping the server, all open {@link Connection} * instances created through the {@link #connect(URI, Properties)} method are closed; this is done to * clean up the threads started in support of each open connection. * * @param uri the {@code URI} for which the server is removed * * @return the removed {@code PassthroughServer} */ public static PassthroughServer remove(URI uri) { URI keyURI = createKey(uri); ServerDescriptor serverDescriptor = SERVERS.remove(keyURI); if (serverDescriptor != null) { for (Connection connection : serverDescriptor.getConnections().keySet()) { try { LOGGER.warn("Force close {}", formatConnectionId(connection)); connection.close(); } catch (AssertionError e) { // Ignored -- https://github.com/Terracotta-OSS/terracotta-apis/issues/102 } catch (IOException e) { // Ignored } } //open destroy connection. You need to make sure connection doesn't have any entities associated with it. PassthroughConnection connection = serverDescriptor.server.connectNewClient("destroy-connection"); for(Entry entry : serverDescriptor.knownEntities.entrySet()) { @SuppressWarnings("unchecked") Class<? extends Entity> type = (Class) entry.getKey(); List args = (List)entry.getValue(); Long version = (Long)args.get(0); String stringArg = (String)args.get(1); try { EntityRef entityRef = connection.getEntityRef(type, version, stringArg); entityRef.destroy(); } catch (EntityNotProvidedException ex) { LOGGER.error("Entity destroy failed (not provided???): ", ex); } catch (EntityNotFoundException ex) { LOGGER.error("Entity destroy failed: ", ex); } catch (PermanentEntityException ex) { LOGGER.error("Entity destroy failed (permanent???): ", ex); } } serverDescriptor.server.stop(); LOGGER.info("Stopped PassthroughServer at {}", keyURI); return serverDescriptor.server; } else { return null; } } /** * Removes the {@link PassthroughServer} previously associated with the URI provided. The server * is stopped as it is removed. * * @param uri the URI, in string form, for which the server is removed * * @return the removed {@code PassthroughServer} */ public static PassthroughServer remove(String uri) { return remove(URI.create(uri)); } /** * A builder for a new {@link PassthroughServer} instance. If no services are added using * {@link #serverEntityService(EntityServerService)} or {@link #clientEntityService(EntityClientService)}, * this builder defines the following services for each {@code PassthroughServer} built: * <ul> * <li>{@link ClusterTierManagerServerEntityService}</li> * <li>{@link ClusterTierManagerClientEntityService}</li> * <li>{@link VoltronReadWriteLockServerEntityService}</li> * <li>{@link VoltronReadWriteLockEntityClientService}</li> * </ul> */ @SuppressWarnings("unused") public static final class PassthroughServerBuilder { private final List<EntityServerService<?, ?>> serverEntityServices = new ArrayList<EntityServerService<?, ?>>(); private final List<EntityClientService<?, ?, ? extends EntityMessage, ? extends EntityResponse, Void>> clientEntityServices = new ArrayList<EntityClientService<?, ?, ? extends EntityMessage, ? extends EntityResponse, Void>>(); private final Map<ServiceProvider, ServiceProviderConfiguration> serviceProviders = new IdentityHashMap<ServiceProvider, ServiceProviderConfiguration>(); private final OffheapResourcesType resources = new OffheapResourcesType(); public PassthroughServerBuilder resource(String resourceName, int size, org.ehcache.config.units.MemoryUnit unit) { return this.resource(resourceName, size, convert(unit)); } private MemoryUnit convert(org.ehcache.config.units.MemoryUnit unit) { MemoryUnit convertedUnit; switch (unit) { case B: convertedUnit = MemoryUnit.B; break; case KB: convertedUnit = MemoryUnit.K_B; break; case MB: convertedUnit = MemoryUnit.MB; break; case GB: convertedUnit = MemoryUnit.GB; break; case TB: convertedUnit = MemoryUnit.TB; break; case PB: convertedUnit = MemoryUnit.PB; break; default: throw new UnsupportedOperationException("Unrecognized unit " + unit); } return convertedUnit; } private PassthroughServerBuilder resource(String resourceName, int size, MemoryUnit unit) { this.resources.getResource().add(getResource(resourceName, size, unit)); return this; } public PassthroughServerBuilder serviceProvider(ServiceProvider serviceProvider, ServiceProviderConfiguration configuration) { this.serviceProviders.put(serviceProvider, configuration); return this; } public PassthroughServerBuilder serverEntityService(EntityServerService<?, ?> service) { this.serverEntityServices.add(service); return this; } public PassthroughServerBuilder clientEntityService(EntityClientService<?, ?, ? extends EntityMessage, ? extends EntityResponse, Void> service) { this.clientEntityServices.add(service); return this; } public PassthroughServer build() { PassthroughServer newServer = new PassthroughServer(); /* * If services have been specified, don't establish the "defaults". */ if (serverEntityServices.isEmpty() && clientEntityServices.isEmpty()) { newServer.registerServerEntityService(new ClusterTierManagerServerEntityService()); newServer.registerClientEntityService(new ClusterTierManagerClientEntityService()); newServer.registerServerEntityService(new ClusterTierServerEntityService()); newServer.registerClientEntityService(new ClusterTierClientEntityService()); newServer.registerServerEntityService(new VoltronReadWriteLockServerEntityService()); newServer.registerClientEntityService(new VoltronReadWriteLockEntityClientService()); } for (EntityServerService<?, ?> service : serverEntityServices) { newServer.registerServerEntityService(service); } for (EntityClientService<?, ?, ? extends EntityMessage, ? extends EntityResponse, Void> service : clientEntityServices) { newServer.registerClientEntityService(service); } if (!this.resources.getResource().isEmpty()) { newServer.registerExtendedConfiguration(new OffHeapResourcesProvider(this.resources)); } for (Map.Entry<ServiceProvider, ServiceProviderConfiguration> entry : serviceProviders.entrySet()) { newServer.registerServiceProvider(entry.getKey(), entry.getValue()); } return newServer; } } public static Collection<Properties> getConnectionProperties(URI uri) { ServerDescriptor serverDescriptor = SERVERS.get(createKey(uri)); if (serverDescriptor != null) { return serverDescriptor.getConnections().values(); } else { return Collections.emptyList(); } } @Override public boolean handlesURI(URI uri) { if (PASSTHROUGH.equals(uri.getScheme())) { return STRIPES.containsKey(uri.getAuthority()); } checkURI(uri); return SERVERS.containsKey(uri); } @Override public Connection connect(URI uri, Properties properties) throws ConnectionException { if (PASSTHROUGH.equals(uri.getScheme())) { if(STRIPES.containsKey(uri.getAuthority())) { String serverName = uri.getHost(); PassthroughServer server = PassthroughServerRegistry.getSharedInstance().getServerForName(serverName); if(null != server) { String connectionName = properties.getProperty("connection.name"); if (null == connectionName) { connectionName = "Ehcache:ACTIVE-PASSIVE"; } Connection connection = server.connectNewClient(connectionName); STRIPES.get(uri.getAuthority()).add(connection); return connection; } } else { throw new IllegalArgumentException("UnitTestConnectionService failed to find stripe" + uri.getAuthority()); } } checkURI(uri); ServerDescriptor serverDescriptor = SERVERS.get(uri); if (serverDescriptor == null) { throw new IllegalArgumentException("No server available for " + uri); } String name = properties.getProperty(ConnectionPropertyNames.CONNECTION_NAME); if (name == null) { name = "Ehcache:UNKNOWN"; } Connection connection = serverDescriptor.server.connectNewClient(name); serverDescriptor.add(connection, properties); LOGGER.info("Client opened {} to PassthroughServer at {}", formatConnectionId(connection), uri); /* * Uses a Proxy around Connection so closed connections can be removed from the ServerDescriptor. */ return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] { Connection.class }, new ConnectionInvocationHandler(serverDescriptor, connection)); } /** * Ensures that the {@code URI} presented conforms to the value used to locate a server. * * @param requestURI the {@code URI} to check * * @throws IllegalArgumentException if the {@code URI} is not equal to the {@code URI} as reformed * by {@link #createKey(URI)} * * @see #checkURI(URI) */ private static void checkURI(URI requestURI) throws IllegalArgumentException { if (!requestURI.equals(createKey(requestURI))) { throw new IllegalArgumentException("Connection URI contains user-info, path, query, and/or fragment"); } } /** * Creates a "key" {@code URI} by dropping the <i>user-info</i>, <i>path</i>, <i>query</i>, and <i>fragment</i> * portions of the {@code URI}. * * @param requestURI the {@code URI} for which the key is to be generated * * @return a {@code URI} instance with the <i>user-info</i>, <i>path</i>, <i>query</i>, and <i>fragment</i> discarded */ private static URI createKey(URI requestURI) { try { URI keyURI = requestURI.parseServerAuthority(); return new URI(keyURI.getScheme(), null, keyURI.getHost(), keyURI.getPort(), null, null, null); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } private static final class StripeDescriptor { private final List<PassthroughServer> servers = new ArrayList<PassthroughServer>(); private final List<Connection> connections = new ArrayList<Connection>(); synchronized void addServer (PassthroughServer server) { servers.add(server); } synchronized List<Connection> getConnections() { return this.connections; } synchronized void add(Connection connection) { this.connections.add(connection); } synchronized void removeConnections() { this.connections.clear(); } } private static final class ServerDescriptor { private final PassthroughServer server; private final Map<Connection, Properties> connections = new IdentityHashMap<Connection, Properties>(); private final Map<Class<? extends Entity>, List<Object>> knownEntities = new HashMap<Class<? extends Entity>, List<Object>>(); ServerDescriptor(PassthroughServer server) { this.server = server; } synchronized Map<Connection, Properties> getConnections() { return new IdentityHashMap<Connection, Properties>(this.connections); } synchronized void add(Connection connection, Properties properties) { this.connections.put(connection, properties); } synchronized void remove(Connection connection) { this.connections.remove(connection); } public void addKnownEntity(Class<? extends Entity> arg, Object arg1, Object arg2) { List<Object> set = new ArrayList<Object>(); set.add(arg1); set.add(arg2); knownEntities.put(arg, set); } } /** * An {@link InvocationHandler} for a proxy over a {@link Connection} instance catch * connection closure for managing the server connection collection. */ private static final class ConnectionInvocationHandler implements InvocationHandler { private final ServerDescriptor serverDescriptor; private final Connection connection; ConnectionInvocationHandler(ServerDescriptor serverDescriptor, Connection connection) { this.serverDescriptor = serverDescriptor; this.connection = connection; } @Override @SuppressWarnings("unchecked") public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("close")) { serverDescriptor.remove(connection); LOGGER.info("Client closed {}", formatConnectionId(connection)); } if (method.getName().equals("getEntityRef")) { serverDescriptor.addKnownEntity((Class<? extends Entity>) args[0], args[1] ,args[2]); } try { return method.invoke(connection, args); } catch (InvocationTargetException e) { throw e.getCause(); } } } private static CharSequence formatConnectionId(Connection connection) { return connection.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(connection)); } }