All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mongodb.internal.connection.BaseCluster Maven / Gradle / Ivy

/*
 * Copyright 2008-present MongoDB, 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 com.mongodb.internal.connection;

import com.mongodb.MongoClientException;
import com.mongodb.MongoException;
import com.mongodb.MongoIncompatibleDriverException;
import com.mongodb.MongoInterruptedException;
import com.mongodb.MongoOperationTimeoutException;
import com.mongodb.MongoTimeoutException;
import com.mongodb.ServerAddress;
import com.mongodb.UnixServerAddress;
import com.mongodb.connection.ClusterDescription;
import com.mongodb.connection.ClusterId;
import com.mongodb.connection.ClusterSettings;
import com.mongodb.connection.ClusterType;
import com.mongodb.connection.ServerDescription;
import com.mongodb.event.ClusterClosedEvent;
import com.mongodb.event.ClusterDescriptionChangedEvent;
import com.mongodb.event.ClusterListener;
import com.mongodb.event.ClusterOpeningEvent;
import com.mongodb.internal.TimeoutContext;
import com.mongodb.internal.VisibleForTesting;
import com.mongodb.internal.async.SingleResultCallback;
import com.mongodb.internal.connection.OperationContext.ServerDeprioritization;
import com.mongodb.internal.diagnostics.logging.Logger;
import com.mongodb.internal.diagnostics.logging.Loggers;
import com.mongodb.internal.logging.LogMessage;
import com.mongodb.internal.logging.LogMessage.Entry;
import com.mongodb.internal.logging.StructuredLogger;
import com.mongodb.internal.selector.AtMostTwoRandomServerSelector;
import com.mongodb.internal.selector.LatencyMinimizingServerSelector;
import com.mongodb.internal.selector.MinimumOperationCountServerSelector;
import com.mongodb.internal.time.Timeout;
import com.mongodb.lang.Nullable;
import com.mongodb.selector.CompositeServerSelector;
import com.mongodb.selector.ServerSelector;

import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;

import static com.mongodb.assertions.Assertions.assertNotNull;
import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.connection.ServerDescription.MAX_DRIVER_WIRE_VERSION;
import static com.mongodb.connection.ServerDescription.MIN_DRIVER_SERVER_VERSION;
import static com.mongodb.connection.ServerDescription.MIN_DRIVER_WIRE_VERSION;
import static com.mongodb.internal.Locks.withInterruptibleLock;
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
import static com.mongodb.internal.connection.EventHelper.wouldDescriptionsGenerateEquivalentEvents;
import static com.mongodb.internal.event.EventListenerHelper.singleClusterListener;
import static com.mongodb.internal.logging.LogMessage.Component.SERVER_SELECTION;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.FAILURE;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.OPERATION;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.OPERATION_ID;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.REMAINING_TIME_MS;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.SELECTOR;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.SERVER_HOST;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.SERVER_PORT;
import static com.mongodb.internal.logging.LogMessage.Entry.Name.TOPOLOGY_DESCRIPTION;
import static com.mongodb.internal.logging.LogMessage.Level.DEBUG;
import static com.mongodb.internal.logging.LogMessage.Level.INFO;
import static com.mongodb.internal.time.Timeout.ZeroSemantics.ZERO_DURATION_MEANS_EXPIRED;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.stream.Collectors.toList;

abstract class BaseCluster implements Cluster {
    private static final Logger LOGGER = Loggers.getLogger("cluster");
    private static final StructuredLogger STRUCTURED_LOGGER = new StructuredLogger("cluster");

    private final ReentrantLock lock = new ReentrantLock();
    private final AtomicReference phase = new AtomicReference<>(new CountDownLatch(1));
    private final ClusterableServerFactory serverFactory;
    private final ClusterId clusterId;
    private final ClusterSettings settings;
    private final ClusterListener clusterListener;
    private final Deque waitQueue = new ConcurrentLinkedDeque<>();
    private final ClusterClock clusterClock = new ClusterClock();
    private Thread waitQueueHandler;

    private volatile boolean isClosed;
    private volatile ClusterDescription description;

    BaseCluster(final ClusterId clusterId, final ClusterSettings settings, final ClusterableServerFactory serverFactory) {
        this.clusterId = notNull("clusterId", clusterId);
        this.settings = notNull("settings", settings);
        this.serverFactory = notNull("serverFactory", serverFactory);
        this.clusterListener = singleClusterListener(settings);
        clusterListener.clusterOpening(new ClusterOpeningEvent(clusterId));
        description = new ClusterDescription(settings.getMode(), ClusterType.UNKNOWN, Collections.emptyList(),
                settings, serverFactory.getSettings());
    }

    @Override
    public ClusterClock getClock() {
        return clusterClock;
    }

    @Override
    public ServerTuple selectServer(final ServerSelector serverSelector, final OperationContext operationContext) {
        isTrue("open", !isClosed());

        ServerDeprioritization serverDeprioritization = operationContext.getServerDeprioritization();
        boolean selectionWaitingLogged = false;
        Timeout computedServerSelectionTimeout = operationContext.getTimeoutContext().computeServerSelectionTimeout();
        logServerSelectionStarted(clusterId, operationContext.getId(), serverSelector, description);
        while (true) {
            CountDownLatch currentPhaseLatch = phase.get();
            ClusterDescription currentDescription = description;
            ServerTuple serverTuple = createCompleteSelectorAndSelectServer(
                    serverSelector, currentDescription, serverDeprioritization,
                    computedServerSelectionTimeout, operationContext.getTimeoutContext());

            if (!currentDescription.isCompatibleWithDriver()) {
                logAndThrowIncompatibleException(operationContext.getId(), serverSelector, currentDescription);
            }
            if (serverTuple != null) {
                ServerAddress serverAddress = serverTuple.getServerDescription().getAddress();
                logServerSelectionSucceeded(
                        clusterId,
                        operationContext.getId(),
                        serverAddress,
                        serverSelector,
                        currentDescription);
                serverDeprioritization.updateCandidate(serverAddress);
                return serverTuple;
            }
            computedServerSelectionTimeout.onExpired(() ->
                    logAndThrowTimeoutException(operationContext, serverSelector, currentDescription));

            if (!selectionWaitingLogged) {
                logServerSelectionWaiting(clusterId, operationContext.getId(), computedServerSelectionTimeout, serverSelector, currentDescription);
                selectionWaitingLogged = true;
            }
            connect();

            Timeout heartbeatLimitedTimeout = Timeout.earliest(
                    computedServerSelectionTimeout,
                    startMinWaitHeartbeatTimeout());

            heartbeatLimitedTimeout.awaitOn(currentPhaseLatch,
                    () -> format("waiting for a server that matches %s", serverSelector));
        }
    }

    @Override
    public void selectServerAsync(final ServerSelector serverSelector, final OperationContext operationContext,
            final SingleResultCallback callback) {
        isTrue("open", !isClosed());

        Timeout computedServerSelectionTimeout = operationContext.getTimeoutContext().computeServerSelectionTimeout();
        ServerSelectionRequest request = new ServerSelectionRequest(
                serverSelector, operationContext, computedServerSelectionTimeout, callback);

        CountDownLatch currentPhase = phase.get();
        ClusterDescription currentDescription = description;

        logServerSelectionStarted(
                clusterId,
                operationContext.getId(),
                serverSelector,
                currentDescription);

        if (!handleServerSelectionRequest(request, currentPhase, currentDescription)) {
            notifyWaitQueueHandler(request);
        }
    }

    public ClusterId getClusterId() {
        return clusterId;
    }

    public ClusterSettings getSettings() {
        return settings;
    }

    public ClusterableServerFactory getServerFactory() {
        return serverFactory;
    }

    protected abstract void connect();

    @Override
    public void close() {
        if (!isClosed()) {
            isClosed = true;
            phase.get().countDown();
            clusterListener.clusterClosed(new ClusterClosedEvent(clusterId));
            stopWaitQueueHandler();
        }
    }

    @Override
    public boolean isClosed() {
        return isClosed;
    }

    protected void updateDescription(final ClusterDescription newDescription) {
        withLock(() -> {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(format("Updating cluster description to  %s", newDescription.getShortDescription()));
            }

            description = newDescription;
            updatePhase();
        });
    }

    /**
     * Subclasses must ensure that this method is called in a way that events are delivered in a predictable order.
     * Typically, this means calling it while holding a lock that includes both updates to the cluster state and firing the event.
     */
    protected void fireChangeEvent(final ClusterDescription newDescription, final ClusterDescription previousDescription) {
        if (!wouldDescriptionsGenerateEquivalentEvents(newDescription, previousDescription)) {
             clusterListener.clusterDescriptionChanged(
                     new ClusterDescriptionChangedEvent(getClusterId(), newDescription, previousDescription));
        }
    }

    @Override
    public ClusterDescription getCurrentDescription() {
        return description;
    }

    @Override
    public void withLock(final Runnable action) {
        withInterruptibleLock(lock, action);
    }

    private void updatePhase() {
        withLock(() -> phase.getAndSet(new CountDownLatch(1)).countDown());
    }

    private Timeout startMinWaitHeartbeatTimeout() {
        long minHeartbeatFrequency = serverFactory.getSettings().getMinHeartbeatFrequency(NANOSECONDS);
        minHeartbeatFrequency = Math.max(0, minHeartbeatFrequency);
        return Timeout.expiresIn(minHeartbeatFrequency, NANOSECONDS, ZERO_DURATION_MEANS_EXPIRED);
    }

    private boolean handleServerSelectionRequest(
            final ServerSelectionRequest request, final CountDownLatch currentPhase,
            final ClusterDescription description) {

        try {
            OperationContext operationContext = request.getOperationContext();
            long operationId = operationContext.getId();
            if (currentPhase != request.phase) {
                CountDownLatch prevPhase = request.phase;
                request.phase = currentPhase;
                if (!description.isCompatibleWithDriver()) {
                    logAndThrowIncompatibleException(operationId, request.originalSelector, description);
                }


                ServerDeprioritization serverDeprioritization = request.operationContext.getServerDeprioritization();
                ServerTuple serverTuple = createCompleteSelectorAndSelectServer(
                        request.originalSelector,
                        description,
                        serverDeprioritization,
                        request.getTimeout(),
                        operationContext.getTimeoutContext());

                if (serverTuple != null) {
                    ServerAddress serverAddress = serverTuple.getServerDescription().getAddress();
                    logServerSelectionSucceeded(
                            clusterId,
                            operationId,
                            serverAddress,
                            request.originalSelector,
                            description);
                    serverDeprioritization.updateCandidate(serverAddress);
                    request.onResult(serverTuple, null);
                    return true;
                }
                if (prevPhase == null) {
                    logServerSelectionWaiting(
                            clusterId,
                            operationId,
                            request.getTimeout(),
                            request.originalSelector,
                            description);
                }
            }

            Timeout.onExistsAndExpired(request.getTimeout(), () -> {
                logAndThrowTimeoutException(operationContext, request.originalSelector, description);
            });
            return false;
        } catch (Exception e) {
            request.onResult(null, e);
            return true;
        }
    }

    @Nullable
    private ServerTuple createCompleteSelectorAndSelectServer(
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription,
            final ServerDeprioritization serverDeprioritization,
            final Timeout serverSelectionTimeout,
            final TimeoutContext timeoutContext) {
        return createCompleteSelectorAndSelectServer(
                serverSelector,
                clusterDescription,
                getServersSnapshot(serverSelectionTimeout, timeoutContext),
                serverDeprioritization,
                settings);
    }

    @Nullable
    @VisibleForTesting(otherwise = PRIVATE)
    static ServerTuple createCompleteSelectorAndSelectServer(
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription,
            final ServersSnapshot serversSnapshot,
            final ServerDeprioritization serverDeprioritization,
            final ClusterSettings settings) {
        ServerSelector completeServerSelector = getCompleteServerSelector(serverSelector, serverDeprioritization, serversSnapshot, settings);
        return completeServerSelector.select(clusterDescription)
                .stream()
                .map(serverDescription -> new ServerTuple(
                        assertNotNull(serversSnapshot.getServer(serverDescription.getAddress())),
                        serverDescription))
                .findAny()
                .orElse(null);
    }

    private static ServerSelector getCompleteServerSelector(
            final ServerSelector serverSelector,
            final ServerDeprioritization serverDeprioritization,
            final ServersSnapshot serversSnapshot,
            final ClusterSettings settings) {
        List selectors = Stream.of(
                getRaceConditionPreFilteringSelector(serversSnapshot),
                serverSelector,
                serverDeprioritization.getServerSelector(),
                settings.getServerSelector(), // may be null
                new LatencyMinimizingServerSelector(settings.getLocalThreshold(MILLISECONDS), MILLISECONDS),
                AtMostTwoRandomServerSelector.instance(),
                new MinimumOperationCountServerSelector(serversSnapshot)
        ).filter(Objects::nonNull).collect(toList());
        return new CompositeServerSelector(selectors);
    }

    private static ServerSelector getRaceConditionPreFilteringSelector(final ServersSnapshot serversSnapshot) {
        // The set of `Server`s maintained by the `Cluster` is updated concurrently with `clusterDescription` being read.
        // Additionally, that set of servers continues to be concurrently updated while `serverSelector` selects.
        // This race condition means that we are not guaranteed to observe all the servers from `clusterDescription`
        // among the `Server`s maintained by the `Cluster`.
        // To deal with this race condition, we take `serversSnapshot` of that set of `Server`s
        // (the snapshot itself does not have to be atomic) non-atomically with reading `clusterDescription`
        // (this means, `serversSnapshot` and `clusterDescription` are not guaranteed to be consistent with each other),
        // and do pre-filtering to make sure that the only `ServerDescription`s we may select,
        // are of those `Server`s that are known to both `clusterDescription` and `serversSnapshot`.
        return clusterDescription -> clusterDescription.getServerDescriptions()
                .stream()
                .filter(serverDescription -> serversSnapshot.containsServer(serverDescription.getAddress()))
                .collect(toList());
    }

    protected ClusterableServer createServer(final ServerAddress serverAddress) {
        return serverFactory.create(this, serverAddress);
    }

    private void logAndThrowIncompatibleException(
            final long operationId,
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription) {
        MongoIncompatibleDriverException exception = createIncompatibleException(clusterDescription);
        logServerSelectionFailed(clusterId, operationId, exception, serverSelector, clusterDescription);
        throw exception;
    }

    private MongoIncompatibleDriverException createIncompatibleException(final ClusterDescription curDescription) {
        String message;
        ServerDescription incompatibleServer = curDescription.findServerIncompatiblyOlderThanDriver();
        if (incompatibleServer != null) {
            message = format("Server at %s reports wire version %d, but this version of the driver requires at least %d (MongoDB %s).",
                    incompatibleServer.getAddress(), incompatibleServer.getMaxWireVersion(),
                    MIN_DRIVER_WIRE_VERSION, MIN_DRIVER_SERVER_VERSION);
        } else {
            incompatibleServer = curDescription.findServerIncompatiblyNewerThanDriver();
            if (incompatibleServer != null) {
                message = format("Server at %s requires wire version %d, but this version of the driver only supports up to %d.",
                        incompatibleServer.getAddress(), incompatibleServer.getMinWireVersion(), MAX_DRIVER_WIRE_VERSION);
            } else {
                throw new IllegalStateException("Server can't be both older than the driver and newer.");
            }
        }
        return new MongoIncompatibleDriverException(message, curDescription);
    }

    private void logAndThrowTimeoutException(
            final OperationContext operationContext,
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription) {
        String message = format(
                "Timed out while waiting for a server that matches %s. Client view of cluster state is %s",
                serverSelector, clusterDescription.getShortDescription());

        MongoTimeoutException exception = operationContext.getTimeoutContext().hasTimeoutMS()
                ? new MongoOperationTimeoutException(message) : new MongoTimeoutException(message);

        logServerSelectionFailed(clusterId, operationContext.getId(), exception, serverSelector, clusterDescription);
        throw exception;
    }

    private static final class ServerSelectionRequest {
        private final ServerSelector originalSelector;
        private final SingleResultCallback callback;
        private final OperationContext operationContext;
        private final Timeout timeout;
        private CountDownLatch phase;

        ServerSelectionRequest(
                final ServerSelector serverSelector,
                final OperationContext operationContext,
                final Timeout timeout,
                final SingleResultCallback callback) {
            this.originalSelector = serverSelector;
            this.operationContext = operationContext;
            this.timeout = timeout;
            this.callback = callback;
        }

        void onResult(@Nullable final ServerTuple serverTuple, @Nullable final Throwable t) {
            try {
                callback.onResult(serverTuple, t);
            } catch (Throwable tr) {
                // ignore
            }
        }

        Timeout getTimeout() {
            return timeout;
        }

        public OperationContext getOperationContext() {
            return operationContext;
        }
    }

    private void notifyWaitQueueHandler(final ServerSelectionRequest request) {
        withLock(() -> {
            if (isClosed) {
                return;
            }

            waitQueue.add(request);

            if (waitQueueHandler == null) {
                waitQueueHandler = new Thread(new WaitQueueHandler(), "cluster-" + clusterId.getValue());
                waitQueueHandler.setDaemon(true);
                waitQueueHandler.start();
            } else {
                updatePhase();
            }
        });
    }

    private void stopWaitQueueHandler() {
        withLock(() -> {
            if (waitQueueHandler != null) {
                waitQueueHandler.interrupt();
            }
        });
    }

    private final class WaitQueueHandler implements Runnable {

        WaitQueueHandler() {
        }

        public void run() {
            while (!isClosed) {
                CountDownLatch currentPhase = phase.get();
                ClusterDescription curDescription = description;

                Timeout timeout = Timeout.infinite();
                boolean someWaitersNotSatisfied = false;
                for (Iterator iter = waitQueue.iterator(); iter.hasNext();) {
                    ServerSelectionRequest currentRequest = iter.next();
                    if (handleServerSelectionRequest(currentRequest, currentPhase, curDescription)) {
                        iter.remove();
                    } else {
                        someWaitersNotSatisfied = true;
                        timeout = Timeout.earliest(
                                timeout,
                                currentRequest.getTimeout(),
                                startMinWaitHeartbeatTimeout());
                    }
                }

                if (someWaitersNotSatisfied) {
                    connect();
                }

                try {
                    timeout.awaitOn(currentPhase, () -> "ignored");
                } catch (MongoInterruptedException closed) {
                    // The cluster has been closed and the while loop will exit.
                }
            }
            // Notify all remaining waiters that a shutdown is in progress
            for (Iterator iter = waitQueue.iterator(); iter.hasNext();) {
                iter.next().onResult(null, new MongoClientException("Shutdown in progress"));
                iter.remove();
            }
        }
    }

    static void logServerSelectionStarted(
            final ClusterId clusterId,
            final long operationId,
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription) {
        if (STRUCTURED_LOGGER.isRequired(DEBUG, clusterId)) {
            STRUCTURED_LOGGER.log(new LogMessage(
                    SERVER_SELECTION, DEBUG, "Server selection started", clusterId,
                    asList(
                            new Entry(OPERATION, null),
                            new Entry(OPERATION_ID, operationId),
                            new Entry(SELECTOR, serverSelector.toString()),
                            new Entry(TOPOLOGY_DESCRIPTION, clusterDescription.getShortDescription())),
                    "Server selection started for operation[ {}] with ID {}. Selector: {}, topology description: {}"));
        }
    }

    private static void logServerSelectionWaiting(
            final ClusterId clusterId,
            final long operationId,
            final Timeout timeout,
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription) {
        if (STRUCTURED_LOGGER.isRequired(INFO, clusterId)) {
            STRUCTURED_LOGGER.log(new LogMessage(
                    SERVER_SELECTION, INFO, "Waiting for suitable server to become available", clusterId,
                    asList(
                            new Entry(OPERATION, null),
                            new Entry(OPERATION_ID, operationId),
                            timeout.call(MILLISECONDS,
                                    () -> new Entry(REMAINING_TIME_MS, "infinite"),
                                    (ms) -> new Entry(REMAINING_TIME_MS, ms),
                                    () -> new Entry(REMAINING_TIME_MS, 0L)),
                            new Entry(SELECTOR, serverSelector.toString()),
                            new Entry(TOPOLOGY_DESCRIPTION, clusterDescription.getShortDescription())),
                    "Waiting for server to become available for operation[ {}] with ID {}.[ Remaining time: {} ms.]"
                            + " Selector: {}, topology description: {}."));
        }
    }

    private static void logServerSelectionFailed(
            final ClusterId clusterId,
            final long operationId,
            final MongoException failure,
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription) {
        if (STRUCTURED_LOGGER.isRequired(DEBUG, clusterId)) {
            String failureDescription = failure instanceof MongoTimeoutException
                    // This hardcoded message guarantees that the `FAILURE` entry for `MongoTimeoutException` does not include
                    // any information that is specified via other entries, e.g., `SELECTOR` and `TOPOLOGY_DESCRIPTION`.
                    // The logging spec requires us to avoid such duplication of information.
                    ? MongoTimeoutException.class.getName() + ": Timed out while waiting for a suitable server"
                    : failure.toString();
            STRUCTURED_LOGGER.log(new LogMessage(
                    SERVER_SELECTION, DEBUG, "Server selection failed", clusterId,
                    asList(
                            new Entry(OPERATION, null),
                            new Entry(OPERATION_ID, operationId),
                            new Entry(FAILURE, failureDescription),
                            new Entry(SELECTOR, serverSelector.toString()),
                            new Entry(TOPOLOGY_DESCRIPTION, clusterDescription.getShortDescription())),
                    "Server selection failed for operation[ {}] with ID {}. Failure: {}. Selector: {}, topology description: {}"));
        }
    }

    static void logServerSelectionSucceeded(
            final ClusterId clusterId,
            final long operationId,
            final ServerAddress serverAddress,
            final ServerSelector serverSelector,
            final ClusterDescription clusterDescription) {
        if (STRUCTURED_LOGGER.isRequired(DEBUG, clusterId)) {
            STRUCTURED_LOGGER.log(new LogMessage(
                    SERVER_SELECTION, DEBUG, "Server selection succeeded", clusterId,
                    asList(
                            new Entry(OPERATION, null),
                            new Entry(OPERATION_ID, operationId),
                            new Entry(SERVER_HOST, serverAddress.getHost()),
                            new Entry(SERVER_PORT, serverAddress instanceof UnixServerAddress ? null : serverAddress.getPort()),
                            new Entry(SELECTOR, serverSelector.toString()),
                            new Entry(TOPOLOGY_DESCRIPTION, clusterDescription.getShortDescription())),
                    "Server selection succeeded for operation[ {}] with ID {}. Selected server: {}[:{}]."
                            + " Selector: {}, topology description: {}"));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy