/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2014-2024 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import com.unboundid.directory.sdk.proxy.api.ServerAffinityProvider; import com.unboundid.directory.sdk.proxy.config.ServerAffinityProviderConfig; import com.unboundid.directory.sdk.proxy.types.BackendServer; import com.unboundid.directory.sdk.proxy.types.ProxyOperationContext; import com.unboundid.directory.sdk.proxy.types.ProxyServerContext; import com.unboundid.directory.sdk.proxy.types.ServerAffinity; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.BooleanValueArgument; /** * This class provides a simple example of a server affinity provider that * consistently routes requests from the same client connection to the same * backend server. This is similar to the client connection server affinity * provider shipped with the server. * <BR><BR> * For the first request received on a client connection, no affinity will have * been set and the load-balancing algorithm will be used to select which * backend server should be used, and then an affinity will be established to * that server. For subsequent requests, the affinity will be used, but if the * server to which the affinity is established is not available for some reason, * then a new affinity will be established to the server that was eventually * used to process the operation. * <BR><BR> * It has the following configuration arguments: * <UL> * <LI>allow-nonlocal-affinity -- Indicates whether to allow an affinity to * be set to a nonlocal server. If this is false, or if it is not set, * then an affinity will only be set for backend servers in the same * location as the Directory Proxy Server.</LI> * </UL> */ public final class ExampleServerAffinityProvider extends ServerAffinityProvider { /** * The name of the argument that will be used to indicate whether an affinity * should be set for a non-local server. */ private static final String ARG_NAME_ALLOW_NONLOCAL_AFFINITY = "allow-nonlocal-affinity"; /** * The name that will be used for the attachment that specifies the server * affinity. The attachment will be a map in which the key is the DN of the * config entry for the load-balancing algorithm and the value is a * ServerAffinity object. */ private static final String CONN_ATTACHMENT_NAME = ExampleServerAffinityProvider.class.getName() + ".affinity"; // Indicates whether to allow an affinity to be set for a nonlocal server. private volatile boolean allowNonLocalAffinity; // The time that the affinity information was last invalidated. private volatile Long lastAffinityInvalidateTime; // The current configuration for this provider. private volatile ServerAffinityProviderConfig config; // The server context for the server in which this extension is running. private ProxyServerContext serverContext; /** * Creates a new instance of this server affinity provider. All server * affinity provider implementations must include a default constructor, but * any initialization should generally be done in the * {@code initializeServerAffinityProvider} method. */ public ExampleServerAffinityProvider() { allowNonLocalAffinity = false; config = null; lastAffinityInvalidateTime = System.currentTimeMillis() - 1L; serverContext = null; } /** * Retrieves a human-readable name for this extension. * * @return A human-readable name for this extension. */ @Override() public String getExtensionName() { return "Example Server Affinity Provider"; } /** * Retrieves a human-readable description for this extension. Each element * of the array that is returned will be considered a separate paragraph in * generated documentation. * * @return A human-readable description for this extension, or {@code null} * or an empty array if no description should be available. */ @Override() public String[] getExtensionDescription() { return new String[] { "This server affinity provider serves as an example that may be used " + "to demonstrate the process for creating a third-party server " + "affinity provider. It will assign an affinity on a " + "per-client-connection basis, allowing the load-balancing " + "algorithm to select the backend server to use for the initial " + "request, and then establishing an affinity to that server that " + "will be used for subsequent requests received over the same " + "client connection." }; } /** * Updates the provided argument parser to define any configuration arguments * which may be used by this server affinity provider. The argument parser * may also be updated to define relationships between arguments (e.g., to * specify required, exclusive, or dependent argument sets). * * @param parser The argument parser to be updated with the configuration * arguments which may be used by this server affinity * provider. * * @throws ArgumentException If a problem is encountered while updating the * provided argument parser. */ @Override() public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { // Add an argument that allows you to indicate whether to establish an // affinity to a nonlocal server. final Character shortIdentifier = null; final String longIdentifier = ARG_NAME_ALLOW_NONLOCAL_AFFINITY; final boolean isRequired = false; final String placeholder = "{true|false}"; final String description = "Indicates whether an affinity may be " + "set to a nonlocal server."; final Boolean defaultValue = Boolean.FALSE; parser.addArgument(new BooleanValueArgument(shortIdentifier, longIdentifier, isRequired, placeholder, description, defaultValue)); } /** * Initializes this server affinity provider. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this server affinity * provider. * @param parser The argument parser which has been initialized from * the configuration for this server affinity provider. * * @throws LDAPException If a problem occurs while initializing this server * affinity provider. */ @Override() public void initializeServerAffinityProvider( final ProxyServerContext serverContext, final ServerAffinityProviderConfig config, final ArgumentParser parser) throws LDAPException { this.serverContext = serverContext; this.config = config; serverContext.debugInfo( "Beginning server affinity provider initialization"); // Determine whether to allow an affinity to be set for nonlocal servers. allowNonLocalAffinity = false; final BooleanValueArgument nonLocalArg = (BooleanValueArgument) parser.getNamedArgument(ARG_NAME_ALLOW_NONLOCAL_AFFINITY); if (nonLocalArg != null) { allowNonLocalAffinity = nonLocalArg.getValue(); } } /** * Indicates whether the configuration contained in the provided argument * parser represents a valid configuration for this extension. * * @param config The general configuration for this server * affinity provider. * @param parser The argument parser which has been initialized * with the proposed configuration. * @param unacceptableReasons A list that can be updated with reasons that * the proposed configuration is not acceptable. * * @return {@code true} if the proposed configuration is acceptable, or * {@code false} if not. */ @Override() public boolean isConfigurationAcceptable( final ServerAffinityProviderConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { // The configuration will always be considered acceptable. return true; } /** * Attempts to apply the configuration contained in the provided argument * parser. * * @param config The general configuration for this server * affinity provider. * @param parser The argument parser which has been * initialized with the new configuration. * @param adminActionsRequired A list that can be updated with information * about any administrative actions that may be * required before one or more of the * configuration changes will be applied. * @param messages A list that can be updated with information * about the result of applying the new * configuration. * * @return A result code that provides information about the result of * attempting to apply the configuration change. */ @Override() public ResultCode applyConfiguration( final ServerAffinityProviderConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { // Determine whether to allow an affinity to be set for nonlocal servers. final BooleanValueArgument nonLocalArg = (BooleanValueArgument) parser.getNamedArgument(ARG_NAME_ALLOW_NONLOCAL_AFFINITY); if (nonLocalArg != null) { allowNonLocalAffinity = nonLocalArg.getValue(); } else { allowNonLocalAffinity = false; } // Whenever the configuration changes, we'll invalidate any affinities that // are set. lastAffinityInvalidateTime = System.currentTimeMillis(); // Update the stored config object. this.config = config; return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this server affinity * provider is to be taken out of service. */ @Override() public void finalizeServerAffinityProvider() { // No finalization is required. } /** * Clears all affinity data associated with the provided list of * load-balancing algorithms. * * @param lbaDN The config entry DN of the load-balancing algorithm * for which to clear the affinity data. If this is * {@code null}, then all affinity data for all * load-balancing algorithms should be cleared. * @param backendServers The set of backend servers that are associated with * the specified load-balancing algorithm. */ @Override() public void clearAffinityData(final DN lbaDN, final Map<DN,BackendServer> backendServers) { // This implementation doesn't support clearing on a // per-load-balancing-algorithm basis. It only supports clearing everything // at once, so that's what we'll do. lastAffinityInvalidateTime = System.currentTimeMillis(); } /** * Indicates which backend server should be used for the provided operation. * It is generally recommended that this method only return a server if the * operation matches an affinity that has already been established (via a * previous call to the {@code updateAffinity} method). If no affinity has * been set, then it is recommended that this method return {@code null} to * allow the load-balancing algorithm to select an appropriate server instead. * * @param lbaDN The config entry DN of the load-balancing algorithm * for which to make the determination. * @param backendServers The set of backend servers from which the selection * may be made, indexed by the DN of the configuration * entry for each server. It will not be * {@code null}. * @param operation The operation to be processed. It will not be * {@code null}. * * @return The backend server for which an affinity is already established, * or {@code null} if the operation does not match any affinity that * has already been established and the appropriate backend server * should be selected by the load-balancing algorithm. */ @Override() @SuppressWarnings("unchecked") public ServerAffinity selectServer(final DN lbaDN, final Map<DN,BackendServer> backendServers, final ProxyOperationContext operation) { // See if the client connection has an attachment that specifies the // affinity assigned for that connection. If not, then return null to // indicate that there is no affinity. final Object attachment = operation.getClientContext().getAttachment(CONN_ATTACHMENT_NAME); if (attachment == null) { serverContext.debugInfo("ExampleServerAffinityProvider.selectServer " + "returning null because the connection doesn't have an affinity " + "attachment."); return null; } // Cast the attachment to the appropriate type of object for the attachment. // If this fails, then return null to indicate that there is no affinity. final Map<DN,ServerAffinity> affinityMap; try { // We need to do an unchecked cast here, which is why we had to annotate // the method with @SuppressWarnings. affinityMap = (Map<DN,ServerAffinity>) attachment; } catch (final Exception e) { serverContext.debugCaught(e); serverContext.debugInfo("ExampleServerAffinityProvider.selectServer " + "returning null because the connection's affinity attachment was " + "not a Map<DN,ServerAffinity>."); return null; } // If the affinity map doesn't have a value for the load-balancing algorithm // DN, then return null to indicate that there is no affinity. final ServerAffinity serverAffinity = affinityMap.get(lbaDN); if (serverAffinity == null) { serverContext.debugInfo("ExampleServerAffinityProvider.selectServer " + "returning null because the affinity map doesn't have any data " + "for load-balancing algorithm " + lbaDN); return null; } // Make sure that the affinity was created after the last invalidate time. if (serverAffinity.getAffinityEstablishedTime() <= lastAffinityInvalidateTime) { serverContext.debugInfo("ExampleServerAffinityProvider.selectServer " + "returning null because affinity " + serverAffinity + " was established before the time the affinity data was last " + "invalidated."); return null; } // The affinity is valid, so return it. return serverAffinity; } /** * Specifies the backend server that was used to process the provided * operation, which allows this affinity provider to establish or update any * necessary state information that could be used to select the same server * for "related" operations that may be processed in the future. * * @param operation The operation that was processed. * @param lbaDN The config entry DN of the load-balancing algorithm * with which the backend server is associated. * @param backendServer The backend server that was used to process the * operation. */ @Override() @SuppressWarnings("unchecked") public void updateAffinity(final ProxyOperationContext operation, final DN lbaDN, final BackendServer backendServer) { final DN backendServerDN = backendServer.getParsedConfigEntryDN(); // If the backend server isn't local, then make sure that it's OK to set an // affinity for a nonlocal server. if ((! backendServer.isLocal()) && (! allowNonLocalAffinity)) { serverContext.debugInfo("ExampleServerAffinityProvider.updateAffinity " + "not setting an affinity to non-local server " + backendServerDN); return; } // If there is an affinity attachment for the associated client connection, // then we'll reuse it. Otherwise, we'll create a new one and associate it // with the connection. Map<DN,ServerAffinity> affinityMap = null; try { final Object attachment = operation.getClientContext().getAttachment(CONN_ATTACHMENT_NAME); if (attachment != null) { affinityMap = (Map<DN,ServerAffinity>) attachment; } } catch (final Exception e) { serverContext.debugCaught(e); } if (affinityMap == null) { affinityMap = new ConcurrentHashMap<DN,ServerAffinity>(); operation.getClientContext().setAttachment(CONN_ATTACHMENT_NAME, affinityMap); } // We'll always set a new affinity even if there's already one set for the // same server. This will update the affinity established time so that // continued operations on the same connection will continue to go to the // same server. final ServerAffinity serverAffinity = new ServerAffinity(backendServer); affinityMap.put(lbaDN, new ServerAffinity(backendServer)); serverContext.debugInfo("Set affinity " + serverAffinity + " for load-balancing algorithm " + lbaDN); } /** * Retrieves a map containing examples of configurations that may be used for * this extension. The map key should be a list of sample arguments, and the * corresponding value should be a description of the behavior that will be * exhibited by the extension when used with that configuration. * * @return A map containing examples of configurations that may be used for * this extension. It may be {@code null} or empty if there should * not be any example argument sets. */ @Override() public Map<List<String>,String> getExamplesArgumentSets() { final LinkedHashMap<List<String>,String> exampleMap = new LinkedHashMap<List<String>,String>(1); exampleMap.put( Arrays.asList(ARG_NAME_ALLOW_NONLOCAL_AFFINITY + "=false"), "Indicates that this affinity provider should not set an affinity " + "to a nonlocal server."); return exampleMap; } /** * Appends a string representation of this LDAP health check to the provided * buffer. * * @param buffer The buffer to which the string representation should be * appended. */ @Override() public void toString(final StringBuilder buffer) { buffer.append("ExampleServerAffinityProvider(dn='"); buffer.append(config.getConfigObjectDN()); buffer.append("', allowNonlocalAffinity="); buffer.append(allowNonLocalAffinity); buffer.append(')'); } }