Package redis.client

Source Code of redis.client.RedisClientBase$SubscriptionsDispatcher

package redis.client;

import com.google.common.base.Charsets;
import com.google.common.primitives.SignedBytes;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import redis.Command;
import redis.RedisProtocol;
import redis.reply.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.Socket;
import java.util.Comparator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* The lowest layer that talks directly with the redis protocol.
* <p/>
* User: sam
* Date: 11/5/11
* Time: 10:24 PM
*/
public class RedisClientBase {
  private static final Comparator<byte[]> BYTES = SignedBytes.lexicographicalComparator();

  // Standard values for use with some commands
  public static final byte[] WEIGHTS = "WEIGHTS".getBytes();
  public static final byte[] WITHSCORES = "WITHSCORES".getBytes();
  public static final byte[] ALPHA = "ALPHA".getBytes();
  public static final byte[] LIMIT = "LIMIT".getBytes();
  public static final byte[] DESC = "DESC".getBytes();
  public static final byte[] BY = "BY".getBytes();
  public static final byte[] STORE = "STORE".getBytes();
  public static final byte[] GET = "GET".getBytes();

  // Needed for reconnection
  private final String host;
  private final int port;
  private int db = 0;
  private String passwd = null;

  // Single threaded pipelining
  private ListeningExecutorService es;
  protected RedisProtocol redisProtocol;
  private static final Pattern versionMatcher = Pattern.compile(
          "([0-9]+)\\.([0-9]+)(\\.([0-9]+))?");
  protected AtomicInteger pipelined = new AtomicInteger(0);
  protected int version = 9999999;

  protected RedisClientBase(String host, int port, int db, String passwd, ExecutorService executorService) throws RedisException {
    this.host = host;
    this.port = port;
    this.db = db;
    this.passwd = passwd;
    es = MoreExecutors.listeningDecorator(executorService);
    connect();
  }

  private boolean connect() throws RedisException {
    try {
      if (subscribed || tx) {
        return false;
      }
      redisProtocol = new RedisProtocol(new Socket(host, port));
      parseInfo();
      if (passwd != null)
        auth(passwd);
      if (db != 0)
        select(db);
      return true;
    } catch (IOException e) {
      throw new RedisException("Could not connect", e);
    } finally {
      subscribed = false;
      tx = false;
      retrying = false;
    }
  }

  private boolean parseAttempted;

  private void parseInfo() {
    if (parseAttempted) return; else parseAttempted = true;
    try {
      BulkReply info = (BulkReply) execute("INFO", new Command("INFO"));
      if (info != null && info.data() != null) {
        BufferedReader br = new BufferedReader(new StringReader(new String(info.data())));
        String line;
        while ((line = br.readLine()) != null) {
          int index = line.indexOf(':');
          if (index != -1) {
            String name = line.substring(0, index);
            String value = line.substring(index + 1);
            if ("redis_version".equals(name)) {
              this.version = parseVersion(value);
            }
          }
        }
      }
    } catch (RedisException re) {
      // Sadly no specific error code for this beyond the text
      if (re.getMessage().equals("ERR operation not permitted")) {
        // Server is either authenticated and we will try again when AUTH command is sent
        parseAttempted = false;
      } else { // or
        // is a non-standard redis implementation, e.g. Twemproxy
        // https://github.com/spullara/redis-protocol/issues/22
        connect();
      }
    } catch (Exception e) {
      // Ignore, don't try again
    }
  }

  public static int parseVersion(String value) {
    int version = 0;
    Matcher matcher = versionMatcher.matcher(value);
    if (matcher.matches()) {
      String major = matcher.group(1);
      String minor = matcher.group(2);
      String patch = matcher.group(4);
      version = 100 * Integer.parseInt(minor) + 10000 * Integer.parseInt(major);
      if (patch != null) {
        version += Integer.parseInt(patch);
      }
    }
    return version;
  }

  private Queue<SettableFuture<Reply>> txReplies = new ConcurrentLinkedQueue<SettableFuture<Reply>>();

  public synchronized ListenableFuture<? extends Reply> pipeline(String name, Command command) throws RedisException {
    if (subscribed) {
      throw new RedisException("You are subscribed and cannot create a pipeline");
    }
    try {
      redisProtocol.sendAsync(command);
    } catch (IOException e) {
      connect();
      throw new RedisException("Failed to execute: " + name, e);
    }
    pipelined.incrementAndGet();
    if (tx) {
      final SettableFuture<Reply> set = SettableFuture.create();
      es.submit(new Runnable() {
        @Override
        public void run() {
          try {
            Reply reply = redisProtocol.receiveAsync();
            if (reply instanceof ErrorReply) {
              set.setException(new RedisException(((ErrorReply) reply).data()));
            } else if (reply instanceof StatusReply) {
              if ("QUEUED".equals(((StatusReply) reply).data())) {
                txReplies.offer(set);
              }
            } else {
              set.set(reply);
            }
          } catch (IOException e) {
            throw new RedisException("Failed to receive queueing result");
          } finally {
            pipelined.decrementAndGet();
          }
        }
      });
      return set;
    } else {
      return es.submit(new Callable<Reply>() {
        @Override
        public Reply call() throws Exception {
          try {
            Reply reply = redisProtocol.receiveAsync();
            if (reply instanceof ErrorReply) {
              throw new RedisException(((ErrorReply) reply).data());
            }
            return reply;
          } finally {
            pipelined.decrementAndGet();
          }
        }
      });
    }
  }

  private boolean retrying = false;

  public synchronized Reply execute(String name, Command command) throws RedisException {
    if (tx) {
      throw new RedisException("Use the pipeline API when using transactions");
    }
    if (subscribed) {
      throw new RedisException("You are subscribed and must use the original pipeline to execute commands");
    }
    try {
      if (pipelined.get() == 0) {
        redisProtocol.sendAsync(command);
        Reply reply = redisProtocol.receiveAsync();
        if (reply instanceof ErrorReply) {
          throw new RedisException(((ErrorReply) reply).data());
        }
        return reply;
      } else {
        return pipeline(name, command).get();
      }
    } catch (IOException e) {
      if (!retrying && connect()) {
        retrying = true;
        execute(name, command);
      }
      throw new RedisException("I/O Failure: " + name, e);
    } catch (InterruptedException e) {
      throw new RedisException("Interrupted: " + name, e);
    } catch (ExecutionException e) {
      throw new RedisException("Failed to execute: " + name, e);
    }
  }

  public RedisProtocol getRedisProtocol() {
    return redisProtocol;
  }

  public void close() throws IOException {
    redisProtocol.close();
  }

  /**
   * Transaction support
   */

  private static final Command MULTI = new Command("MULTI".getBytes());
  private static final Command EXEC = new Command("EXEC".getBytes());
  private static final Command DISCARD = new Command("DISCARD".getBytes());

  private boolean tx;

  public synchronized StatusReply multi() {
    if (tx) {
      throw new RedisException("Already in a transaction");
    }
    if (subscribed) {
      throw new RedisException("You can only issue subscription commands once subscribed");
    }
    StatusReply multi = (StatusReply) execute("MULTI", MULTI);
    tx = true;
    return multi;
  }

  public StatusReply discard() {
    ListenableFuture<StatusReply> discard;
    synchronized (this) {
      if (subscribed) {
        throw new RedisException("You can only issue subscription commands once subscribed");
      }
      if (tx) {
        tx = false;
        discard = es.submit(new Callable<StatusReply>() {
          @Override
          public StatusReply call() {
            synchronized (RedisClientBase.this) {
              SettableFuture<Reply> txReply;
              while ((txReply = txReplies.poll()) != null) {
                txReply.setException(new RedisException("Discarded"));
              }
              return (StatusReply) execute("DISCARD", DISCARD);
            }
          }
        });
      } else {
        throw new RedisException("Not in a transaction");
      }
    }
    try {
      return discard.get();
    } catch (Exception e) {
      throw new RedisException("Failed to discard the transaction", e);
    }
  }

  public synchronized Future<Boolean> exec() {
    if (subscribed) {
      throw new RedisException("You can only issue subscription commands once subscribed");
    }
    if (tx) {
      tx = false;
      try {
        redisProtocol.sendAsync(EXEC);
        return es.submit(new Callable<Boolean>() {
          @Override
          public Boolean call() throws Exception {
            Reply maybeReply = redisProtocol.receiveAsync();
            if (maybeReply instanceof ErrorReply) {
              ErrorReply errorReply = (ErrorReply) maybeReply;
              throw new RedisException(errorReply.data());
            }
            MultiBulkReply execReply = (MultiBulkReply) maybeReply;
            if (execReply.data() == null) {
              for (SettableFuture<Reply> txReply : txReplies) {
                txReply.setException(new RedisException("Transaction failed"));
              }
              return false;
            }
            for (Reply reply : execReply.data()) {
              SettableFuture<Reply> poll = txReplies.poll();
              if (reply instanceof ErrorReply) {
                poll.setException(new RedisException((String) reply.data()));
              } else {
                poll.set(reply);
              }
            }
            return true;
          }
        });
      } catch (IOException e) {
        connect();
        throw new RedisException(e);
      }
    } else {
      throw new RedisException("Not in a transaction");
    }
  }

  private List<ReplyListener> replyListeners;
  private boolean subscribed;

  /**
   * Add a reply listener to this client for subscriptions.
   */
  public synchronized void addListener(ReplyListener replyListener) {
    if (replyListeners == null) {
      replyListeners = new CopyOnWriteArrayList<ReplyListener>();
    }
    replyListeners.add(replyListener);
  }

  /**
   * Remove a reply listener from this client.
   */
  public synchronized boolean removeListener(ReplyListener replyListener) {
    return replyListeners != null && replyListeners.remove(replyListener);
  }

  private static final byte[] MESSAGE = "message".getBytes();
  private static final byte[] PMESSAGE = "pmessage".getBytes();
  private static final byte[] SUBSCRIBE = "subscribe".getBytes();
  private static final byte[] UNSUBSCRIBE = "unsubscribe".getBytes();
  private static final byte[] PSUBSCRIBE = "psubscribe".getBytes();
  private static final byte[] PUNSUBSCRIBE = "punsubscribe".getBytes();

  /**
   * Subscribes the client to the specified channels.
   *
   * @param subscriptions
   */
  public synchronized void subscribe(Object... subscriptions) {
    subscribe();
    try {
      redisProtocol.sendAsync(new Command(SUBSCRIBE, subscriptions));
    } catch (IOException e) {
      connect();
      throw new RedisException("Failed to subscribe", e);
    }
  }

  /**
   * Subscribes the client to the specified patterns.
   *
   * @param subscriptions
   */
  public synchronized void psubscribe(Object... subscriptions) {
    subscribe();
    try {
      redisProtocol.sendAsync(new Command(PSUBSCRIBE, subscriptions));
    } catch (IOException e) {
      connect();
      throw new RedisException("Failed to subscribe", e);
    }
  }

  /**
   * Unsubscribes the client to the specified channels.
   *
   * @param subscriptions
   */
  public synchronized void unsubscribe(Object... subscriptions) {
    subscribe();
    try {
      redisProtocol.sendAsync(new Command(UNSUBSCRIBE, subscriptions));
    } catch (IOException e) {
      connect();
      throw new RedisException("Failed to subscribe", e);
    }
  }

  /**
   * Unsubscribes the client to the specified patterns.
   *
   * @param subscriptions
   */
  public synchronized void punsubscribe(Object... subscriptions) {
    subscribe();
    try {
      redisProtocol.sendAsync(new Command(PUNSUBSCRIBE, subscriptions));
    } catch (IOException e) {
      connect();
      throw new RedisException("Failed to subscribe", e);
    }
  }

  private void subscribe() {
    if (!subscribed) {
      subscribed = true;
      // Start up the listener, only subscription commands
      // are accepted past this point
      es.submit(new SubscriptionsDispatcher());
    }
  }

  protected static final String AUTH = "AUTH";
  protected static final byte[] AUTH_BYTES = AUTH.getBytes(Charsets.US_ASCII);

  /**
   * Authenticate to the server
   * Connection
   *
   * @param password0
   * @return StatusReply
   */
  public StatusReply auth(Object password0) throws RedisException {
    StatusReply statusReply = (StatusReply) execute(AUTH, new Command(AUTH_BYTES, password0));
    // Now that we are successful, parse the info
    parseInfo();
    return statusReply;
  }
 
  protected static final String SELECT = "SELECT";
  protected static final byte[] SELECT_BYTES = SELECT.getBytes(Charsets.US_ASCII);
  protected static final int SELECT_VERSION = parseVersion("1.0.0");

  /**
   * Change the selected database for the current connection
   * Connection
   *
   * @param index0
   * @return StatusReply
   */
  public StatusReply select(Object index0) throws RedisException {
    if (version < SELECT_VERSION) throw new RedisException("Server does not support SELECT");
    return (StatusReply) execute(SELECT, new Command(SELECT_BYTES, index0));
  }

  private class SubscriptionsDispatcher implements Runnable {
    @Override
    public void run() {
      try {
        while (true) {
          MultiBulkReply reply = (MultiBulkReply) redisProtocol.receiveAsync();
          Reply[] data = reply.data();
          if (data.length != 3 && data.length != 4) {
            throw new RedisException("Invalid subscription messsage");
          }
          for (ReplyListener replyListener : replyListeners) {
            byte[] type = (byte[]) data[0].data();
            byte[] data1 = (byte[]) data[1].data();
            Object data2 = data[2].data();
            switch (type.length) {
              case 7:
                if (BYTES.compare(type, MESSAGE) == 0) {
                  replyListener.message(data1, (byte[]) data2);
                  continue;
                }
              case 8:
                if (BYTES.compare(type, PMESSAGE) == 0) {
                  replyListener.pmessage(data1, (byte[]) data2, (byte[]) data[3].data());
                  continue;
                }
              case 9:
                if (BYTES.compare(type, SUBSCRIBE) == 0) {
                  replyListener.subscribed(data1, ((Number) data2).intValue());
                  continue;
                }
              case 10:
                if (BYTES.compare(type, PSUBSCRIBE) == 0) {
                  replyListener.psubscribed(data1, ((Number) data2).intValue());
                  continue;
                }
              case 11:
                if (BYTES.compare(type, UNSUBSCRIBE) == 0) {
                  replyListener.unsubscribed(data1, ((Number) data2).intValue());
                  continue;
                }
              case 12:
                if (BYTES.compare(type, PUNSUBSCRIBE) == 0) {
                  replyListener.punsubscribed(data1, ((Number) data2).intValue());
                  continue;
                }
            }
            close();
            throw new RedisException("Invalid subscription messsage");
          }
        }
      } catch (IOException e) {
        // Ignore, probably closed
      }
    }
  }
}
TOP

Related Classes of redis.client.RedisClientBase$SubscriptionsDispatcher

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.