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

io.atomix.copycat.client.session.ClientSessionManager Maven / Gradle / Ivy

There is a newer version: 1.2.8
Show newest version
/*
 * Copyright 2015 the original author or authors.
 *
 * 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 io.atomix.copycat.client.session;

import io.atomix.catalyst.util.Assert;
import io.atomix.catalyst.concurrent.Scheduled;
import io.atomix.catalyst.concurrent.ThreadContext;
import io.atomix.copycat.client.ConnectionStrategy;
import io.atomix.copycat.client.util.ClientConnection;
import io.atomix.copycat.error.CopycatError;
import io.atomix.copycat.protocol.*;
import io.atomix.copycat.session.Session;

import java.net.ConnectException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;

/**
 * Client session manager.
 *
 * @author  open() {
    CompletableFuture future = new CompletableFuture<>();
    context.executor().execute(() -> register(new RegisterAttempt(1, future)));
    return future;
  }

  /**
   * Registers a session.
   */
  private void register(RegisterAttempt attempt) {
    state.getLogger().debug("Registering session: attempt {}", attempt.attempt);

    RegisterRequest request = RegisterRequest.builder()
      .withClient(state.getClientId())
      .build();

    state.getLogger().debug("Sending {}", request);
    connection.reset().send(request).whenComplete((response, error) -> {
      if (error == null) {
        state.getLogger().debug("Received {}", response);
        if (response.status() == Response.Status.OK) {
          interval = Duration.ofMillis(response.timeout()).dividedBy(2);
          connection.reset(response.leader(), response.members());
          state.setSessionId(response.session())
            .setState(Session.State.OPEN);
          state.getLogger().info("Registered session {}", response.session());
          attempt.complete();
          keepAlive();
        } else {
          strategy.attemptFailed(attempt);
        }
      } else {
        strategy.attemptFailed(attempt);
      }
    });
  }

  /**
   * Sends a keep-alive request to the cluster.
   */
  private void keepAlive() {
    keepAlive(true);
  }

  /**
   * Sends a keep-alive request to the cluster.
   */
  private void keepAlive(boolean retryOnFailure) {
    long sessionId = state.getSessionId();

    // If the current sessions state is unstable, reset the connection before sending a keep-alive.
    if (state.getState() == Session.State.UNSTABLE)
      connection.reset();

    KeepAliveRequest request = KeepAliveRequest.builder()
      .withSession(sessionId)
      .withCommandSequence(state.getCommandResponse())
      .withEventIndex(state.getEventIndex())
      .build();

    state.getLogger().debug("{} - Sending {}", sessionId, request);
    connection.send(request).whenComplete((response, error) -> {
      if (state.getState() != Session.State.CLOSED) {
        if (error == null) {
          state.getLogger().debug("{} - Received {}", sessionId, response);
          // If the request was successful, update the address selector and schedule the next keep-alive.
          if (response.status() == Response.Status.OK) {
            connection.reset(response.leader(), response.members());
            state.setState(Session.State.OPEN);
            scheduleKeepAlive();
          }
          // If the session is unknown, immediate expire the session.
          else if (response.error() == CopycatError.Type.UNKNOWN_SESSION_ERROR) {
            state.setState(Session.State.EXPIRED);
          }
          // If a leader is still set in the address selector, unset the leader and attempt to send another keep-alive.
          // This will ensure that the address selector selects all servers without filtering on the leader.
          else if (retryOnFailure && connection.leader() != null) {
            connection.reset(null, connection.servers());
            keepAlive(false);
          }
          // If no leader was set, set the session state to unstable and schedule another keep-alive.
          else {
            state.setState(Session.State.UNSTABLE);
            scheduleKeepAlive();
          }
        }
        // If a leader is still set in the address selector, unset the leader and attempt to send another keep-alive.
        // This will ensure that the address selector selects all servers without filtering on the leader.
        else if (retryOnFailure && connection.leader() != null) {
          connection.reset(null, connection.servers());
          keepAlive(false);
        }
        // If no leader was set, set the session state to unstable and schedule another keep-alive.
        else {
          state.setState(Session.State.UNSTABLE);
          scheduleKeepAlive();
        }
      }
    });
  }

  /**
   * Schedules a keep-alive request.
   */
  private void scheduleKeepAlive() {
    if (keepAlive != null)
      keepAlive.cancel();
    keepAlive = context.schedule(interval, () -> {
      keepAlive = null;
      keepAlive();
    });
  }

  /**
   * Closes the session manager.
   *
   * @return A completable future to be completed once the session manager is closed.
   */
  public CompletableFuture close() {
    if (state.getState() == Session.State.EXPIRED)
      return CompletableFuture.completedFuture(null);

    CompletableFuture future = new CompletableFuture<>();
    context.executor().execute(() -> {
      if (keepAlive != null)
        keepAlive.cancel();
      unregister(future);
    });
    return future;
  }

  /**
   * Unregisters the session.
   */
  private void unregister(CompletableFuture future) {
    unregister(true, future);
  }

  /**
   * Unregisters the session.
   *
   * @param future A completable future to be completed once the session is unregistered.
   */
  private void unregister(boolean retryOnFailure, CompletableFuture future) {
    long sessionId = state.getSessionId();
    state.getLogger().debug("Unregistering session: {}", sessionId);

    // If a keep-alive request is already pending, cancel it.
    if (keepAlive != null)
      keepAlive.cancel();

    // If the current sessions state is unstable, reset the connection before sending an unregister request.
    if (state.getState() == Session.State.UNSTABLE)
      connection.reset();

    UnregisterRequest request = UnregisterRequest.builder()
      .withSession(sessionId)
      .build();

    state.getLogger().debug("{} - Sending {}", sessionId, request);
    connection.send(request).whenComplete((response, error) -> {
      if (state.getState() != Session.State.CLOSED) {
        if (error == null) {
          state.getLogger().debug("{} - Received {}", sessionId, response);
          // If the request was successful, update the session state and complete the close future.
          if (response.status() == Response.Status.OK) {
            state.setState(Session.State.CLOSED);
            future.complete(null);
          }
          // If the session is unknown, immediate expire the session and complete the close future.
          else if (response.error() == CopycatError.Type.UNKNOWN_SESSION_ERROR) {
            state.setState(Session.State.EXPIRED);
            future.complete(null);
          }
          // If a leader is still set in the address selector, unset the leader and send another unregister attempt.
          // This will ensure that the address selector selects all servers without filtering on the leader.
          else if (retryOnFailure && connection.leader() != null) {
            connection.reset(null, connection.servers());
            unregister(false, future);
          }
          // If no leader was set, set the session state to unstable and schedule another unregister attempt.
          else {
            state.setState(Session.State.UNSTABLE);
            keepAlive = context.schedule(interval, () -> unregister(future));
          }
        }
        // If a leader is still set in the address selector, unset the leader and send another unregister attempt.
        // This will ensure that the address selector selects all servers without filtering on the leader.
        else if (retryOnFailure && connection.leader() != null) {
          connection.reset(null, connection.servers());
          unregister(false, future);
        }
        // If no leader was set, set the session state to unstable and schedule another unregister attempt.
        else {
          state.setState(Session.State.UNSTABLE);
          keepAlive = context.schedule(interval, () -> unregister(future));
        }
      }
    });
  }

  /**
   * Kills the client session manager.
   *
   * @return A completable future to be completed once the session manager is killed.
   */
  public CompletableFuture kill() {
    return CompletableFuture.runAsync(() -> {
      if (keepAlive != null)
        keepAlive.cancel();
      state.setState(Session.State.CLOSED);
    }, context.executor());
  }

  @Override
  public String toString() {
    return String.format("%s[session=%d]", getClass().getSimpleName(), state.getSessionId());
  }

  /**
   * Client session connection attempt.
   */
  private final class RegisterAttempt implements ConnectionStrategy.Attempt {
    private final int attempt;
    private final CompletableFuture future;

    private RegisterAttempt(int attempt, CompletableFuture future) {
      this.attempt = attempt;
      this.future = future;
    }

    @Override
    public int attempt() {
      return attempt;
    }

    /**
     * Completes the attempt successfully.
     */
    public void complete() {
      complete(null);
    }

    /**
     * Completes the attempt successfully.
     *
     * @param result The attempt result.
     */
    public void complete(Void result) {
      future.complete(result);
    }

    @Override
    public void fail() {
      future.completeExceptionally(new ConnectException("failed to register session"));
    }

    @Override
    public void fail(Throwable error) {
      future.completeExceptionally(error);
    }

    @Override
    public void retry() {
      state.getLogger().debug("Retrying session register attempt");
      register(new RegisterAttempt(attempt + 1, future));
    }

    @Override
    public void retry(Duration after) {
      state.getLogger().debug("Retrying session register attempt");
      context.schedule(after, () -> register(new RegisterAttempt(attempt + 1, future)));
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy