/*
* 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
*
*
* Copyright 2014-2016 UnboundID Corp.
*/
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(')');
}
}
|