commit d76f5b46b5185fd66c61912c53be2c3da736cccd Author: Walter Oggioni Date: Sat Dec 28 09:02:28 2024 +0800 initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..be32e52 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,17 @@ +name: CI +on: + push: + tags: + - '*' +jobs: + build: + runs-on: hostinger + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Execute Gradle build + env: + PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} + run: ./gradlew build publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b6985c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..554f2b2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,107 @@ +plugins { + id 'java-library' + alias catalog.plugins.sambal + id 'maven-publish' +} + +allprojects { + group = 'net.woggioni' + version = project.currentTag.map { List tags -> + tags[0] + }.getOrElse(getProperty('xmemcached.version') + '-SNAPSHOT') + + pluginManager.withPlugin('java-library') { + + repositories { + mavenCentral() + } + + java { + withSourcesJar() + modularity.inferModulePath = true + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + } + + test { + + useJUnitPlatform() + systemProperty('junit.jupiter.execution.parallel.enabled', 'true') + systemProperty('junit.jupiter.execution.parallel.mode.classes.default', 'concurrent') + systemProperty('junit.jupiter.execution.parallel.mode.default = concurrent', 'concurrent') +// systemProperty('test.memcached.host', 'localhost') +// systemProperty('test.memcached.port', '1211') +// systemProperty('test.memcached.servers', 'localhost:1211') + + filter { + excludeTestsMatching "*IT" + } + } + } + + pluginManager.withPlugin(catalog.plugins.lombok.get().pluginId) { + lombok { + version = catalog.versions.lombok.get() + } + } + + pluginManager.withPlugin('maven-publish') { + + pluginManager.withPlugin('java-library') { + java { +// withJavadocJar() + withSourcesJar() + } + } + + publishing { + repositories { + maven { + name = "Gitea" + url = uri(getProperty('gitea.maven.url')) + + credentials(HttpHeaderCredentials) { + name = "Authorization" + value = "token ${System.getenv()["PUBLISHER_TOKEN"]}" + } + + authentication { + header(HttpHeaderAuthentication) + } + + } + } + publications { + maven(MavenPublication) { + from(components["java"]) + } + } + } + } +} + +dependencies { + implementation catalog.slf4j.api + // https://mvnrepository.com/artifact/org.springframework/spring-core + compileOnly group: 'org.springframework', name: 'spring-context', version: getProperty('spring.version') + // https://mvnrepository.com/artifact/org.springframework/spring-core + compileOnly group: 'org.springframework', name: 'spring-beans', version: getProperty('spring.version') + + // https://mvnrepository.com/artifact/org.easymock/easymock + testImplementation group: 'org.easymock', name: 'easymock', version: getProperty('easymock.version') + + testImplementation catalog.junit.jupiter.api + testRuntimeOnly catalog.junit.jupiter.engine + + // https://mvnrepository.com/artifact/org.springframework/spring-core + testImplementation group: 'org.springframework', name: 'spring-context', version: getProperty('spring.version') + // https://mvnrepository.com/artifact/org.springframework/spring-core + testImplementation group: 'org.springframework', name: 'spring-beans', version: getProperty('spring.version') + +// testImplementation platform(group: "org.testcontainers", name: "testcontainers-bom", version: "1.20.4") +// testImplementation group: "org.testcontainers", name: 'testcontainers' +// testImplementation group: 'org.testcontainers', name: 'junit-jupiter' + +} + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d82446a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true + +xmemcached.version = 2.4.9 + +lys.version = 2024.12.29 + +gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven + +spring.version=6.2.1 +easymock.version=5.5.0 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4ac3234 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,2 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cea7a79 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f3b75f3 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 +# +# https://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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..56719ae --- /dev/null +++ b/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + repositories { + maven { + url = getProperty('gitea.maven.url') + } + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + maven { + url = getProperty('gitea.maven.url') + content { + includeGroup 'com.lys' + } + } + } + versionCatalogs { + catalog { + from group: 'com.lys', name: 'lys-catalog', version: getProperty('lys.version') + } + } +} + +rootProject.name = 'xmemcached' diff --git a/src/assemble/distribution.xml b/src/assemble/distribution.xml new file mode 100644 index 0000000..f026b16 --- /dev/null +++ b/src/assemble/distribution.xml @@ -0,0 +1,47 @@ + + bin-with-dependencies + + tar.gz + + + + User_Guide_zh + /User_Guide_zh + + + User_Guide_en + /User_Guide_en + + + ${project.build.directory} + + + *.jar + + / + + + + + /lib + runtime + + xmemcached* + + + + + + README.md + / + + + NOTICE.txt + / + + + LICENSE.txt + / + + + \ No newline at end of file diff --git a/src/assemble/src.xml b/src/assemble/src.xml new file mode 100644 index 0000000..2190c70 --- /dev/null +++ b/src/assemble/src.xml @@ -0,0 +1,30 @@ + + src + + tar.gz + + + + ${project.basedir}/src + /src + + + + + README.md + / + + + pom.xml + / + + + NOTICE.txt + / + + + LICENSE.txt + / + + + \ No newline at end of file diff --git a/src/main/java/com/google/code/yanf4j/buffer/AbstractIoBuffer.java b/src/main/java/com/google/code/yanf4j/buffer/AbstractIoBuffer.java new file mode 100644 index 0000000..948aaba --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/AbstractIoBuffer.java @@ -0,0 +1,2494 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamClass; +import java.io.OutputStream; +import java.io.StreamCorruptedException; +import java.nio.BufferOverflowException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.nio.ShortBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.util.EnumSet; +import java.util.Set; + +/** + * A base implementation of {@link IoBuffer}. This implementation assumes that + * {@link IoBuffer#buf()} always returns a correct NIO {@link ByteBuffer} instance. Most + * implementations could extend this class and implement their own buffer management mechanism. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 748210 $, $Date: 2009-02-26 18:05:40 +0100 (Thu, 26 Feb 2009) $ + * @see IoBufferAllocator + */ +public abstract class AbstractIoBuffer extends IoBuffer { + /** Tells if a buffer has been created from an existing buffer */ + private final boolean derived; + + /** A flag set to true if the buffer can extend automatically */ + private boolean autoExpand; + + /** A flag set to true if the buffer can shrink automatically */ + private boolean autoShrink; + + /** Tells if a buffer can be expanded */ + private boolean recapacityAllowed = true; + + /** The minimum number of bytes the IoBuffer can hold */ + private int minimumCapacity; + + /** A mask for a byte */ + private static final long BYTE_MASK = 0xFFL; + + /** A mask for a short */ + private static final long SHORT_MASK = 0xFFFFL; + + /** A mask for an int */ + private static final long INT_MASK = 0xFFFFFFFFL; + + /** + * We don't have any access to Buffer.markValue(), so we need to track it down, which will cause + * small extra overhead. + */ + private int mark = -1; + + /** + * Creates a new parent buffer. + * + * @param allocator The allocator to use to create new buffers + * @param initialCapacity The initial buffer capacity when created + */ + protected AbstractIoBuffer(IoBufferAllocator allocator, int initialCapacity) { + setAllocator(allocator); + this.recapacityAllowed = true; + this.derived = false; + this.minimumCapacity = initialCapacity; + } + + /** + * Creates a new derived buffer. A derived buffer uses an existing buffer properties - the + * allocator and capacity -. + * + * @param parent The buffer we get the properties from + */ + protected AbstractIoBuffer(AbstractIoBuffer parent) { + setAllocator(parent.getAllocator()); + this.recapacityAllowed = false; + this.derived = true; + this.minimumCapacity = parent.minimumCapacity; + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isDirect() { + return buf().isDirect(); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isReadOnly() { + return buf().isReadOnly(); + } + + /** + * Sets the underlying NIO buffer instance. + * + * @param newBuf The buffer to store within this IoBuffer + */ + protected abstract void buf(ByteBuffer newBuf); + + /** + * {@inheritDoc} + */ + @Override + public final int minimumCapacity() { + return minimumCapacity; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer minimumCapacity(int minimumCapacity) { + if (minimumCapacity < 0) { + throw new IllegalArgumentException("minimumCapacity: " + minimumCapacity); + } + this.minimumCapacity = minimumCapacity; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final int capacity() { + return buf().capacity(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer capacity(int newCapacity) { + if (!recapacityAllowed) { + throw new IllegalStateException("Derived buffers and their parent can't be expanded."); + } + + // Allocate a new buffer and transfer all settings to it. + if (newCapacity > capacity()) { + // Expand: + // // Save the state. + int pos = position(); + int limit = limit(); + ByteOrder bo = order(); + + // // Reallocate. + ByteBuffer oldBuf = buf(); + ByteBuffer newBuf = getAllocator().allocateNioBuffer(newCapacity, isDirect()); + oldBuf.clear(); + newBuf.put(oldBuf); + buf(newBuf); + + // // Restore the state. + buf().limit(limit); + if (mark >= 0) { + buf().position(mark); + buf().mark(); + } + buf().position(pos); + buf().order(bo); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isAutoExpand() { + return autoExpand && recapacityAllowed; + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isAutoShrink() { + return autoShrink && recapacityAllowed; + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isDerived() { + return derived; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer setAutoExpand(boolean autoExpand) { + if (!recapacityAllowed) { + throw new IllegalStateException("Derived buffers and their parent can't be expanded."); + } + this.autoExpand = autoExpand; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer setAutoShrink(boolean autoShrink) { + if (!recapacityAllowed) { + throw new IllegalStateException("Derived buffers and their parent can't be shrinked."); + } + this.autoShrink = autoShrink; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer expand(int expectedRemaining) { + return expand(position(), expectedRemaining, false); + } + + private IoBuffer expand(int expectedRemaining, boolean autoExpand) { + return expand(position(), expectedRemaining, autoExpand); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer expand(int pos, int expectedRemaining) { + return expand(pos, expectedRemaining, false); + } + + private IoBuffer expand(int pos, int expectedRemaining, boolean autoExpand) { + if (!recapacityAllowed) { + throw new IllegalStateException("Derived buffers and their parent can't be expanded."); + } + + int end = pos + expectedRemaining; + int newCapacity; + if (autoExpand) { + newCapacity = IoBuffer.normalizeCapacity(end); + } else { + newCapacity = end; + } + if (newCapacity > capacity()) { + // The buffer needs expansion. + capacity(newCapacity); + } + + if (end > limit()) { + // We call limit() directly to prevent StackOverflowError + buf().limit(end); + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer shrink() { + + if (!recapacityAllowed) { + throw new IllegalStateException("Derived buffers and their parent can't be expanded."); + } + + int position = position(); + int capacity = capacity(); + int limit = limit(); + if (capacity == limit) { + return this; + } + + int newCapacity = capacity; + int minCapacity = Math.max(minimumCapacity, limit); + for (;;) { + if (newCapacity >>> 1 < minCapacity) { + break; + } + newCapacity >>>= 1; + } + + newCapacity = Math.max(minCapacity, newCapacity); + + if (newCapacity == capacity) { + return this; + } + + // Shrink and compact: + // // Save the state. + ByteOrder bo = order(); + + // // Reallocate. + ByteBuffer oldBuf = buf(); + ByteBuffer newBuf = getAllocator().allocateNioBuffer(newCapacity, isDirect()); + oldBuf.position(0); + oldBuf.limit(limit); + newBuf.put(oldBuf); + buf(newBuf); + + // // Restore the state. + buf().position(position); + buf().limit(limit); + buf().order(bo); + mark = -1; + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final int position() { + return buf().position(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer position(int newPosition) { + autoExpand(newPosition, 0); + buf().position(newPosition); + if (mark > newPosition) { + mark = -1; + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final int limit() { + return buf().limit(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer limit(int newLimit) { + autoExpand(newLimit, 0); + buf().limit(newLimit); + if (mark > newLimit) { + mark = -1; + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer mark() { + buf().mark(); + mark = position(); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final int markValue() { + return mark; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer reset() { + buf().reset(); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer clear() { + buf().clear(); + mark = -1; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer sweep() { + clear(); + return fillAndReset(remaining()); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer sweep(byte value) { + clear(); + return fillAndReset(value, remaining()); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer flip() { + buf().flip(); + mark = -1; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer rewind() { + buf().rewind(); + mark = -1; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final int remaining() { + return limit() - position(); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean hasRemaining() { + return limit() > position(); + } + + /** + * {@inheritDoc} + */ + @Override + public final byte get() { + return buf().get(); + } + + /** + * {@inheritDoc} + */ + @Override + public final short getUnsigned() { + return (short) (get() & 0xff); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer put(byte b) { + autoExpand(1); + buf().put(b); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final byte get(int index) { + return buf().get(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final short getUnsigned(int index) { + return (short) (get(index) & 0xff); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer put(int index, byte b) { + autoExpand(index, 1); + buf().put(index, b); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer get(byte[] dst, int offset, int length) { + buf().get(dst, offset, length); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer put(ByteBuffer src) { + autoExpand(src.remaining()); + buf().put(src); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer put(byte[] src, int offset, int length) { + autoExpand(length); + buf().put(src, offset, length); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer compact() { + int remaining = remaining(); + int capacity = capacity(); + + if (capacity == 0) { + return this; + } + + if (isAutoShrink() && remaining <= capacity >>> 2 && capacity > minimumCapacity) { + int newCapacity = capacity; + int minCapacity = Math.max(minimumCapacity, remaining << 1); + for (;;) { + if (newCapacity >>> 1 < minCapacity) { + break; + } + newCapacity >>>= 1; + } + + newCapacity = Math.max(minCapacity, newCapacity); + + if (newCapacity == capacity) { + return this; + } + + // Shrink and compact: + // // Save the state. + ByteOrder bo = order(); + + // // Sanity check. + if (remaining > newCapacity) { + throw new IllegalStateException( + "The amount of the remaining bytes is greater than " + "the new capacity."); + } + + // // Reallocate. + ByteBuffer oldBuf = buf(); + ByteBuffer newBuf = getAllocator().allocateNioBuffer(newCapacity, isDirect()); + newBuf.put(oldBuf); + buf(newBuf); + + // // Restore the state. + buf().order(bo); + } else { + buf().compact(); + } + mark = -1; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final ByteOrder order() { + return buf().order(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer order(ByteOrder bo) { + buf().order(bo); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final char getChar() { + return buf().getChar(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putChar(char value) { + autoExpand(2); + buf().putChar(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final char getChar(int index) { + return buf().getChar(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putChar(int index, char value) { + autoExpand(index, 2); + buf().putChar(index, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final CharBuffer asCharBuffer() { + return buf().asCharBuffer(); + } + + /** + * {@inheritDoc} + */ + @Override + public final short getShort() { + return buf().getShort(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putShort(short value) { + autoExpand(2); + buf().putShort(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final short getShort(int index) { + return buf().getShort(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putShort(int index, short value) { + autoExpand(index, 2); + buf().putShort(index, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final ShortBuffer asShortBuffer() { + return buf().asShortBuffer(); + } + + /** + * {@inheritDoc} + */ + @Override + public final int getInt() { + return buf().getInt(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putInt(int value) { + autoExpand(4); + buf().putInt(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final int getInt(int index) { + return buf().getInt(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putInt(int index, int value) { + autoExpand(index, 4); + buf().putInt(index, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final IntBuffer asIntBuffer() { + return buf().asIntBuffer(); + } + + /** + * {@inheritDoc} + */ + @Override + public final long getLong() { + return buf().getLong(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putLong(long value) { + autoExpand(8); + buf().putLong(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final long getLong(int index) { + return buf().getLong(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putLong(int index, long value) { + autoExpand(index, 8); + buf().putLong(index, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final LongBuffer asLongBuffer() { + return buf().asLongBuffer(); + } + + /** + * {@inheritDoc} + */ + @Override + public final float getFloat() { + return buf().getFloat(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putFloat(float value) { + autoExpand(4); + buf().putFloat(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final float getFloat(int index) { + return buf().getFloat(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putFloat(int index, float value) { + autoExpand(index, 4); + buf().putFloat(index, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final FloatBuffer asFloatBuffer() { + return buf().asFloatBuffer(); + } + + /** + * {@inheritDoc} + */ + @Override + public final double getDouble() { + return buf().getDouble(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putDouble(double value) { + autoExpand(8); + buf().putDouble(value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final double getDouble(int index) { + return buf().getDouble(index); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer putDouble(int index, double value) { + autoExpand(index, 8); + buf().putDouble(index, value); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public final DoubleBuffer asDoubleBuffer() { + return buf().asDoubleBuffer(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer asReadOnlyBuffer() { + recapacityAllowed = false; + return asReadOnlyBuffer0(); + } + + /** + * Implement this method to return the unexpandable read only version of this buffer. + */ + protected abstract IoBuffer asReadOnlyBuffer0(); + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer duplicate() { + recapacityAllowed = false; + return duplicate0(); + } + + /** + * Implement this method to return the unexpandable duplicate of this buffer. + */ + protected abstract IoBuffer duplicate0(); + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer slice() { + recapacityAllowed = false; + return slice0(); + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer getSlice(int index, int length) { + if (length < 0) { + throw new IllegalArgumentException("length: " + length); + } + + int limit = limit(); + + if (index > limit) { + throw new IllegalArgumentException("index: " + index); + } + + int endIndex = index + length; + + if (capacity() < endIndex) { + throw new IndexOutOfBoundsException( + "index + length (" + endIndex + ") is greater " + "than capacity (" + capacity() + ")."); + } + + clear(); + position(index); + limit(endIndex); + + IoBuffer slice = slice(); + position(index); + limit(limit); + return slice; + } + + /** + * {@inheritDoc} + */ + @Override + public final IoBuffer getSlice(int length) { + if (length < 0) { + throw new IllegalArgumentException("length: " + length); + } + int pos = position(); + int limit = limit(); + int nextPos = pos + length; + if (limit < nextPos) { + throw new IndexOutOfBoundsException( + "position + length (" + nextPos + ") is greater " + "than limit (" + limit + ")."); + } + + limit(pos + length); + IoBuffer slice = slice(); + position(nextPos); + limit(limit); + return slice; + } + + /** + * Implement this method to return the unexpandable slice of this buffer. + */ + protected abstract IoBuffer slice0(); + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + int h = 1; + int p = position(); + for (int i = limit() - 1; i >= p; i--) { + h = 31 * h + get(i); + } + return h; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof IoBuffer)) { + return false; + } + + IoBuffer that = (IoBuffer) o; + if (this.remaining() != that.remaining()) { + return false; + } + + int p = this.position(); + for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--) { + byte v1 = this.get(i); + byte v2 = that.get(j); + if (v1 != v2) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + public int compareTo(IoBuffer that) { + int n = this.position() + Math.min(this.remaining(), that.remaining()); + for (int i = this.position(), j = that.position(); i < n; i++, j++) { + byte v1 = this.get(i); + byte v2 = that.get(j); + if (v1 == v2) { + continue; + } + if (v1 < v2) { + return -1; + } + + return +1; + } + return this.remaining() - that.remaining(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + if (isDirect()) { + buf.append("DirectBuffer"); + } else { + buf.append("HeapBuffer"); + } + buf.append("[pos="); + buf.append(position()); + buf.append(" lim="); + buf.append(limit()); + buf.append(" cap="); + buf.append(capacity()); + buf.append(": "); + buf.append(getHexDump(16)); + buf.append(']'); + return buf.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer get(byte[] dst) { + return get(dst, 0, dst.length); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer put(IoBuffer src) { + return put(src.buf()); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer put(byte[] src) { + return put(src, 0, src.length); + } + + /** + * {@inheritDoc} + */ + @Override + public int getUnsignedShort() { + return getShort() & 0xffff; + } + + /** + * {@inheritDoc} + */ + @Override + public int getUnsignedShort(int index) { + return getShort(index) & 0xffff; + } + + /** + * {@inheritDoc} + */ + @Override + public long getUnsignedInt() { + return getInt() & 0xffffffffL; + } + + /** + * {@inheritDoc} + */ + @Override + public int getMediumInt() { + byte b1 = get(); + byte b2 = get(); + byte b3 = get(); + if (ByteOrder.BIG_ENDIAN.equals(order())) { + return getMediumInt(b1, b2, b3); + } else { + return getMediumInt(b3, b2, b1); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getUnsignedMediumInt() { + int b1 = getUnsigned(); + int b2 = getUnsigned(); + int b3 = getUnsigned(); + if (ByteOrder.BIG_ENDIAN.equals(order())) { + return b1 << 16 | b2 << 8 | b3; + } else { + return b3 << 16 | b2 << 8 | b1; + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getMediumInt(int index) { + byte b1 = get(index); + byte b2 = get(index + 1); + byte b3 = get(index + 2); + if (ByteOrder.BIG_ENDIAN.equals(order())) { + return getMediumInt(b1, b2, b3); + } else { + return getMediumInt(b3, b2, b1); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getUnsignedMediumInt(int index) { + int b1 = getUnsigned(index); + int b2 = getUnsigned(index + 1); + int b3 = getUnsigned(index + 2); + if (ByteOrder.BIG_ENDIAN.equals(order())) { + return b1 << 16 | b2 << 8 | b3; + } else { + return b3 << 16 | b2 << 8 | b1; + } + } + + /** + * {@inheritDoc} + */ + private int getMediumInt(byte b1, byte b2, byte b3) { + int ret = b1 << 16 & 0xff0000 | b2 << 8 & 0xff00 | b3 & 0xff; + // Check to see if the medium int is negative (high bit in b1 set) + if ((b1 & 0x80) == 0x80) { + // Make the the whole int negative + ret |= 0xff000000; + } + return ret; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putMediumInt(int value) { + byte b1 = (byte) (value >> 16); + byte b2 = (byte) (value >> 8); + byte b3 = (byte) value; + + if (ByteOrder.BIG_ENDIAN.equals(order())) { + put(b1).put(b2).put(b3); + } else { + put(b3).put(b2).put(b1); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putMediumInt(int index, int value) { + byte b1 = (byte) (value >> 16); + byte b2 = (byte) (value >> 8); + byte b3 = (byte) value; + + if (ByteOrder.BIG_ENDIAN.equals(order())) { + put(index, b1).put(index + 1, b2).put(index + 2, b3); + } else { + put(index, b3).put(index + 1, b2).put(index + 2, b1); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public long getUnsignedInt(int index) { + return getInt(index) & 0xffffffffL; + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream asInputStream() { + return new InputStream() { + @Override + public int available() { + return AbstractIoBuffer.this.remaining(); + } + + @Override + public synchronized void mark(int readlimit) { + AbstractIoBuffer.this.mark(); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public int read() { + if (AbstractIoBuffer.this.hasRemaining()) { + return AbstractIoBuffer.this.get() & 0xff; + } else { + return -1; + } + } + + @Override + public int read(byte[] b, int off, int len) { + int remaining = AbstractIoBuffer.this.remaining(); + if (remaining > 0) { + int readBytes = Math.min(remaining, len); + AbstractIoBuffer.this.get(b, off, readBytes); + return readBytes; + } else { + return -1; + } + } + + @Override + public synchronized void reset() { + AbstractIoBuffer.this.reset(); + } + + @Override + public long skip(long n) { + int bytes; + if (n > Integer.MAX_VALUE) { + bytes = AbstractIoBuffer.this.remaining(); + } else { + bytes = Math.min(AbstractIoBuffer.this.remaining(), (int) n); + } + AbstractIoBuffer.this.skip(bytes); + return bytes; + } + }; + } + + /** + * {@inheritDoc} + */ + @Override + public OutputStream asOutputStream() { + return new OutputStream() { + @Override + public void write(byte[] b, int off, int len) { + AbstractIoBuffer.this.put(b, off, len); + } + + @Override + public void write(int b) { + AbstractIoBuffer.this.put((byte) b); + } + }; + } + + /** + * {@inheritDoc} + */ + @Override + public String getHexDump() { + return this.getHexDump(Integer.MAX_VALUE); + } + + /** + * {@inheritDoc} + */ + @Override + public String getHexDump(int lengthLimit) { + return IoBufferHexDumper.getHexdump(this, lengthLimit); + } + + /** + * {@inheritDoc} + */ + @Override + public String getString(CharsetDecoder decoder) throws CharacterCodingException { + if (!hasRemaining()) { + return ""; + } + + boolean utf16 = decoder.charset().name().startsWith("UTF-16"); + + int oldPos = position(); + int oldLimit = limit(); + int end = -1; + int newPos; + + if (!utf16) { + end = indexOf((byte) 0x00); + if (end < 0) { + newPos = end = oldLimit; + } else { + newPos = end + 1; + } + } else { + int i = oldPos; + for (;;) { + boolean wasZero = get(i) == 0; + i++; + + if (i >= oldLimit) { + break; + } + + if (get(i) != 0) { + i++; + if (i >= oldLimit) { + break; + } else { + continue; + } + } + + if (wasZero) { + end = i - 1; + break; + } + } + + if (end < 0) { + newPos = end = oldPos + (oldLimit - oldPos & 0xFFFFFFFE); + } else { + if (end + 2 <= oldLimit) { + newPos = end + 2; + } else { + newPos = end; + } + } + } + + if (oldPos == end) { + position(newPos); + return ""; + } + + limit(end); + decoder.reset(); + + int expectedLength = (int) (remaining() * decoder.averageCharsPerByte()) + 1; + CharBuffer out = CharBuffer.allocate(expectedLength); + for (;;) { + CoderResult cr; + if (hasRemaining()) { + cr = decoder.decode(buf(), out, true); + } else { + cr = decoder.flush(out); + } + + if (cr.isUnderflow()) { + break; + } + + if (cr.isOverflow()) { + CharBuffer o = CharBuffer.allocate(out.capacity() + expectedLength); + out.flip(); + o.put(out); + out = o; + continue; + } + + if (cr.isError()) { + // Revert the buffer back to the previous state. + limit(oldLimit); + position(oldPos); + cr.throwException(); + } + } + + limit(oldLimit); + position(newPos); + return out.flip().toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getString(int fieldSize, CharsetDecoder decoder) throws CharacterCodingException { + checkFieldSize(fieldSize); + + if (fieldSize == 0) { + return ""; + } + + if (!hasRemaining()) { + return ""; + } + + boolean utf16 = decoder.charset().name().startsWith("UTF-16"); + + if (utf16 && (fieldSize & 1) != 0) { + throw new IllegalArgumentException("fieldSize is not even."); + } + + int oldPos = position(); + int oldLimit = limit(); + int end = oldPos + fieldSize; + + if (oldLimit < end) { + throw new BufferUnderflowException(); + } + + int i; + + if (!utf16) { + for (i = oldPos; i < end; i++) { + if (get(i) == 0) { + break; + } + } + + if (i == end) { + limit(end); + } else { + limit(i); + } + } else { + for (i = oldPos; i < end; i += 2) { + if (get(i) == 0 && get(i + 1) == 0) { + break; + } + } + + if (i == end) { + limit(end); + } else { + limit(i); + } + } + + if (!hasRemaining()) { + limit(oldLimit); + position(end); + return ""; + } + decoder.reset(); + + int expectedLength = (int) (remaining() * decoder.averageCharsPerByte()) + 1; + CharBuffer out = CharBuffer.allocate(expectedLength); + for (;;) { + CoderResult cr; + if (hasRemaining()) { + cr = decoder.decode(buf(), out, true); + } else { + cr = decoder.flush(out); + } + + if (cr.isUnderflow()) { + break; + } + + if (cr.isOverflow()) { + CharBuffer o = CharBuffer.allocate(out.capacity() + expectedLength); + out.flip(); + o.put(out); + out = o; + continue; + } + + if (cr.isError()) { + // Revert the buffer back to the previous state. + limit(oldLimit); + position(oldPos); + cr.throwException(); + } + } + + limit(oldLimit); + position(end); + return out.flip().toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putString(CharSequence val, CharsetEncoder encoder) + throws CharacterCodingException { + if (val.length() == 0) { + return this; + } + + CharBuffer in = CharBuffer.wrap(val); + encoder.reset(); + + int expandedState = 0; + + for (;;) { + CoderResult cr; + if (in.hasRemaining()) { + cr = encoder.encode(in, buf(), true); + } else { + cr = encoder.flush(buf()); + } + + if (cr.isUnderflow()) { + break; + } + if (cr.isOverflow()) { + if (isAutoExpand()) { + switch (expandedState) { + case 0: + autoExpand((int) Math.ceil(in.remaining() * encoder.averageBytesPerChar())); + expandedState++; + break; + case 1: + autoExpand((int) Math.ceil(in.remaining() * encoder.maxBytesPerChar())); + expandedState++; + break; + default: + throw new RuntimeException( + "Expanded by " + (int) Math.ceil(in.remaining() * encoder.maxBytesPerChar()) + + " but that wasn't enough for '" + val + "'"); + } + continue; + } + } else { + expandedState = 0; + } + cr.throwException(); + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putString(CharSequence val, int fieldSize, CharsetEncoder encoder) + throws CharacterCodingException { + checkFieldSize(fieldSize); + + if (fieldSize == 0) { + return this; + } + + autoExpand(fieldSize); + + boolean utf16 = encoder.charset().name().startsWith("UTF-16"); + + if (utf16 && (fieldSize & 1) != 0) { + throw new IllegalArgumentException("fieldSize is not even."); + } + + int oldLimit = limit(); + int end = position() + fieldSize; + + if (oldLimit < end) { + throw new BufferOverflowException(); + } + + if (val.length() == 0) { + if (!utf16) { + put((byte) 0x00); + } else { + put((byte) 0x00); + put((byte) 0x00); + } + position(end); + return this; + } + + CharBuffer in = CharBuffer.wrap(val); + limit(end); + encoder.reset(); + + for (;;) { + CoderResult cr; + if (in.hasRemaining()) { + cr = encoder.encode(in, buf(), true); + } else { + cr = encoder.flush(buf()); + } + + if (cr.isUnderflow() || cr.isOverflow()) { + break; + } + cr.throwException(); + } + + limit(oldLimit); + + if (position() < end) { + if (!utf16) { + put((byte) 0x00); + } else { + put((byte) 0x00); + put((byte) 0x00); + } + } + + position(end); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public String getPrefixedString(CharsetDecoder decoder) throws CharacterCodingException { + return getPrefixedString(2, decoder); + } + + /** + * Reads a string which has a length field before the actual encoded string, using the specified + * decoder and returns it. + * + * @param prefixLength the length of the length field (1, 2, or 4) + * @param decoder the decoder to use for decoding the string + * @return the prefixed string + * @throws CharacterCodingException when decoding fails + * @throws BufferUnderflowException when there is not enough data available + */ + @Override + public String getPrefixedString(int prefixLength, CharsetDecoder decoder) + throws CharacterCodingException { + if (!prefixedDataAvailable(prefixLength)) { + throw new BufferUnderflowException(); + } + + int fieldSize = 0; + + switch (prefixLength) { + case 1: + fieldSize = getUnsigned(); + break; + case 2: + fieldSize = getUnsignedShort(); + break; + case 4: + fieldSize = getInt(); + break; + } + + if (fieldSize == 0) { + return ""; + } + + boolean utf16 = decoder.charset().name().startsWith("UTF-16"); + + if (utf16 && (fieldSize & 1) != 0) { + throw new BufferDataException("fieldSize is not even for a UTF-16 string."); + } + + int oldLimit = limit(); + int end = position() + fieldSize; + + if (oldLimit < end) { + throw new BufferUnderflowException(); + } + + limit(end); + decoder.reset(); + + int expectedLength = (int) (remaining() * decoder.averageCharsPerByte()) + 1; + CharBuffer out = CharBuffer.allocate(expectedLength); + for (;;) { + CoderResult cr; + if (hasRemaining()) { + cr = decoder.decode(buf(), out, true); + } else { + cr = decoder.flush(out); + } + + if (cr.isUnderflow()) { + break; + } + + if (cr.isOverflow()) { + CharBuffer o = CharBuffer.allocate(out.capacity() + expectedLength); + out.flip(); + o.put(out); + out = o; + continue; + } + + cr.throwException(); + } + + limit(oldLimit); + position(end); + return out.flip().toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putPrefixedString(CharSequence in, CharsetEncoder encoder) + throws CharacterCodingException { + return putPrefixedString(in, 2, 0, encoder); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putPrefixedString(CharSequence in, int prefixLength, CharsetEncoder encoder) + throws CharacterCodingException { + return putPrefixedString(in, prefixLength, 0, encoder); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putPrefixedString(CharSequence in, int prefixLength, int padding, + CharsetEncoder encoder) throws CharacterCodingException { + return putPrefixedString(in, prefixLength, padding, (byte) 0, encoder); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putPrefixedString(CharSequence val, int prefixLength, int padding, byte padValue, + CharsetEncoder encoder) throws CharacterCodingException { + int maxLength; + switch (prefixLength) { + case 1: + maxLength = 255; + break; + case 2: + maxLength = 65535; + break; + case 4: + maxLength = Integer.MAX_VALUE; + break; + default: + throw new IllegalArgumentException("prefixLength: " + prefixLength); + } + + if (val.length() > maxLength) { + throw new IllegalArgumentException("The specified string is too long."); + } + if (val.length() == 0) { + switch (prefixLength) { + case 1: + put((byte) 0); + break; + case 2: + putShort((short) 0); + break; + case 4: + putInt(0); + break; + } + return this; + } + + int padMask; + switch (padding) { + case 0: + case 1: + padMask = 0; + break; + case 2: + padMask = 1; + break; + case 4: + padMask = 3; + break; + default: + throw new IllegalArgumentException("padding: " + padding); + } + + CharBuffer in = CharBuffer.wrap(val); + skip(prefixLength); // make a room for the length field + int oldPos = position(); + encoder.reset(); + + int expandedState = 0; + + for (;;) { + CoderResult cr; + if (in.hasRemaining()) { + cr = encoder.encode(in, buf(), true); + } else { + cr = encoder.flush(buf()); + } + + if (position() - oldPos > maxLength) { + throw new IllegalArgumentException("The specified string is too long."); + } + + if (cr.isUnderflow()) { + break; + } + if (cr.isOverflow()) { + if (isAutoExpand()) { + switch (expandedState) { + case 0: + autoExpand((int) Math.ceil(in.remaining() * encoder.averageBytesPerChar())); + expandedState++; + break; + case 1: + autoExpand((int) Math.ceil(in.remaining() * encoder.maxBytesPerChar())); + expandedState++; + break; + default: + throw new RuntimeException( + "Expanded by " + (int) Math.ceil(in.remaining() * encoder.maxBytesPerChar()) + + " but that wasn't enough for '" + val + "'"); + } + continue; + } + } else { + expandedState = 0; + } + cr.throwException(); + } + + // Write the length field + fill(padValue, padding - (position() - oldPos & padMask)); + int length = position() - oldPos; + switch (prefixLength) { + case 1: + put(oldPos - 1, (byte) length); + break; + case 2: + putShort(oldPos - 2, (short) length); + break; + case 4: + putInt(oldPos - 4, length); + break; + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public Object getObject() throws ClassNotFoundException { + return getObject(Thread.currentThread().getContextClassLoader()); + } + + /** + * {@inheritDoc} + */ + @Override + public Object getObject(final ClassLoader classLoader) throws ClassNotFoundException { + if (!prefixedDataAvailable(4)) { + throw new BufferUnderflowException(); + } + + int length = getInt(); + if (length <= 4) { + throw new BufferDataException("Object length should be greater than 4: " + length); + } + + int oldLimit = limit(); + limit(position() + length); + try { + ObjectInputStream in = new ObjectInputStream(asInputStream()) { + @Override + protected ObjectStreamClass readClassDescriptor() + throws IOException, ClassNotFoundException { + int type = read(); + if (type < 0) { + throw new EOFException(); + } + switch (type) { + case 0: // Primitive types + return super.readClassDescriptor(); + case 1: // Non-primitive types + String className = readUTF(); + Class clazz = Class.forName(className, true, classLoader); + return ObjectStreamClass.lookup(clazz); + default: + throw new StreamCorruptedException("Unexpected class descriptor type: " + type); + } + } + + @Override + protected Class resolveClass(ObjectStreamClass desc) + throws IOException, ClassNotFoundException { + String name = desc.getName(); + try { + return Class.forName(name, false, classLoader); + } catch (ClassNotFoundException ex) { + return super.resolveClass(desc); + } + } + }; + return in.readObject(); + } catch (IOException e) { + throw new BufferDataException(e); + } finally { + limit(oldLimit); + } + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putObject(Object o) { + int oldPos = position(); + skip(4); // Make a room for the length field. + try { + ObjectOutputStream out = new ObjectOutputStream(asOutputStream()) { + @Override + protected void writeClassDescriptor(ObjectStreamClass desc) throws IOException { + if (desc.forClass().isPrimitive()) { + write(0); + super.writeClassDescriptor(desc); + } else { + write(1); + writeUTF(desc.getName()); + } + } + }; + out.writeObject(o); + out.flush(); + } catch (IOException e) { + throw new BufferDataException(e); + } + + // Fill the length field + int newPos = position(); + position(oldPos); + putInt(newPos - oldPos - 4); + position(newPos); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean prefixedDataAvailable(int prefixLength) { + return prefixedDataAvailable(prefixLength, Integer.MAX_VALUE); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean prefixedDataAvailable(int prefixLength, int maxDataLength) { + if (remaining() < prefixLength) { + return false; + } + + int dataLength; + switch (prefixLength) { + case 1: + dataLength = getUnsigned(position()); + break; + case 2: + dataLength = getUnsignedShort(position()); + break; + case 4: + dataLength = getInt(position()); + break; + default: + throw new IllegalArgumentException("prefixLength: " + prefixLength); + } + + if (dataLength < 0 || dataLength > maxDataLength) { + throw new BufferDataException("dataLength: " + dataLength); + } + + return remaining() - prefixLength >= dataLength; + } + + /** + * {@inheritDoc} + */ + @Override + public int indexOf(byte b) { + if (hasArray()) { + int arrayOffset = arrayOffset(); + int beginPos = arrayOffset + position(); + int limit = arrayOffset + limit(); + byte[] array = array(); + + for (int i = beginPos; i < limit; i++) { + if (array[i] == b) { + return i - arrayOffset; + } + } + } else { + int beginPos = position(); + int limit = limit(); + + for (int i = beginPos; i < limit; i++) { + if (get(i) == b) { + return i; + } + } + } + + return -1; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer skip(int size) { + autoExpand(size); + return position(position() + size); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer fill(byte value, int size) { + autoExpand(size); + int q = size >>> 3; + int r = size & 7; + + if (q > 0) { + int intValue = value | value << 8 | value << 16 | value << 24; + long longValue = intValue; + longValue <<= 32; + longValue |= intValue; + + for (int i = q; i > 0; i--) { + putLong(longValue); + } + } + + q = r >>> 2; + r = r & 3; + + if (q > 0) { + int intValue = value | value << 8 | value << 16 | value << 24; + putInt(intValue); + } + + q = r >> 1; + r = r & 1; + + if (q > 0) { + short shortValue = (short) (value | value << 8); + putShort(shortValue); + } + + if (r > 0) { + put(value); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer fillAndReset(byte value, int size) { + autoExpand(size); + int pos = position(); + try { + fill(value, size); + } finally { + position(pos); + } + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer fill(int size) { + autoExpand(size); + int q = size >>> 3; + int r = size & 7; + + for (int i = q; i > 0; i--) { + putLong(0L); + } + + q = r >>> 2; + r = r & 3; + + if (q > 0) { + putInt(0); + } + + q = r >> 1; + r = r & 1; + + if (q > 0) { + putShort((short) 0); + } + + if (r > 0) { + put((byte) 0); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer fillAndReset(int size) { + autoExpand(size); + int pos = position(); + try { + fill(size); + } finally { + position(pos); + } + + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public > E getEnum(Class enumClass) { + return toEnum(enumClass, getUnsigned()); + } + + /** + * {@inheritDoc} + */ + @Override + public > E getEnum(int index, Class enumClass) { + return toEnum(enumClass, getUnsigned(index)); + } + + /** + * {@inheritDoc} + */ + @Override + public > E getEnumShort(Class enumClass) { + return toEnum(enumClass, getUnsignedShort()); + } + + /** + * {@inheritDoc} + */ + @Override + public > E getEnumShort(int index, Class enumClass) { + return toEnum(enumClass, getUnsignedShort(index)); + } + + /** + * {@inheritDoc} + */ + @Override + public > E getEnumInt(Class enumClass) { + return toEnum(enumClass, getInt()); + } + + /** + * {@inheritDoc} + */ + @Override + public > E getEnumInt(int index, Class enumClass) { + return toEnum(enumClass, getInt(index)); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putEnum(Enum e) { + if (e.ordinal() > BYTE_MASK) { + throw new IllegalArgumentException(enumConversionErrorMessage(e, "byte")); + } + return put((byte) e.ordinal()); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putEnum(int index, Enum e) { + if (e.ordinal() > BYTE_MASK) { + throw new IllegalArgumentException(enumConversionErrorMessage(e, "byte")); + } + return put(index, (byte) e.ordinal()); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putEnumShort(Enum e) { + if (e.ordinal() > SHORT_MASK) { + throw new IllegalArgumentException(enumConversionErrorMessage(e, "short")); + } + return putShort((short) e.ordinal()); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putEnumShort(int index, Enum e) { + if (e.ordinal() > SHORT_MASK) { + throw new IllegalArgumentException(enumConversionErrorMessage(e, "short")); + } + return putShort(index, (short) e.ordinal()); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putEnumInt(Enum e) { + return putInt(e.ordinal()); + } + + /** + * {@inheritDoc} + */ + @Override + public IoBuffer putEnumInt(int index, Enum e) { + return putInt(index, e.ordinal()); + } + + private E toEnum(Class enumClass, int i) { + E[] enumConstants = enumClass.getEnumConstants(); + if (i > enumConstants.length) { + throw new IndexOutOfBoundsException(String.format( + "%d is too large of an ordinal to convert to the enum %s", i, enumClass.getName())); + } + return enumConstants[i]; + } + + private String enumConversionErrorMessage(Enum e, String type) { + return String.format("%s.%s has an ordinal value too large for a %s", e.getClass().getName(), + e.name(), type); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSet(Class enumClass) { + return toEnumSet(enumClass, get() & BYTE_MASK); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSet(int index, Class enumClass) { + return toEnumSet(enumClass, get(index) & BYTE_MASK); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSetShort(Class enumClass) { + return toEnumSet(enumClass, getShort() & SHORT_MASK); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSetShort(int index, Class enumClass) { + return toEnumSet(enumClass, getShort(index) & SHORT_MASK); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSetInt(Class enumClass) { + return toEnumSet(enumClass, getInt() & INT_MASK); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSetInt(int index, Class enumClass) { + return toEnumSet(enumClass, getInt(index) & INT_MASK); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSetLong(Class enumClass) { + return toEnumSet(enumClass, getLong()); + } + + /** + * {@inheritDoc} + */ + @Override + public > EnumSet getEnumSetLong(int index, Class enumClass) { + return toEnumSet(enumClass, getLong(index)); + } + + private > EnumSet toEnumSet(Class clazz, long vector) { + EnumSet set = EnumSet.noneOf(clazz); + long mask = 1; + for (E e : clazz.getEnumConstants()) { + if ((mask & vector) == mask) { + set.add(e); + } + mask <<= 1; + } + return set; + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSet(Set set) { + long vector = toLong(set); + if ((vector & ~BYTE_MASK) != 0) { + throw new IllegalArgumentException("The enum set is too large to fit in a byte: " + set); + } + return put((byte) vector); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSet(int index, Set set) { + long vector = toLong(set); + if ((vector & ~BYTE_MASK) != 0) { + throw new IllegalArgumentException("The enum set is too large to fit in a byte: " + set); + } + return put(index, (byte) vector); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSetShort(Set set) { + long vector = toLong(set); + if ((vector & ~SHORT_MASK) != 0) { + throw new IllegalArgumentException("The enum set is too large to fit in a short: " + set); + } + return putShort((short) vector); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSetShort(int index, Set set) { + long vector = toLong(set); + if ((vector & ~SHORT_MASK) != 0) { + throw new IllegalArgumentException("The enum set is too large to fit in a short: " + set); + } + return putShort(index, (short) vector); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSetInt(Set set) { + long vector = toLong(set); + if ((vector & ~INT_MASK) != 0) { + throw new IllegalArgumentException("The enum set is too large to fit in an int: " + set); + } + return putInt((int) vector); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSetInt(int index, Set set) { + long vector = toLong(set); + if ((vector & ~INT_MASK) != 0) { + throw new IllegalArgumentException("The enum set is too large to fit in an int: " + set); + } + return putInt(index, (int) vector); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSetLong(Set set) { + return putLong(toLong(set)); + } + + /** + * {@inheritDoc} + */ + @Override + public > IoBuffer putEnumSetLong(int index, Set set) { + return putLong(index, toLong(set)); + } + + private > long toLong(Set set) { + long vector = 0; + for (E e : set) { + if (e.ordinal() >= Long.SIZE) { + throw new IllegalArgumentException( + "The enum set is too large to fit in a bit vector: " + set); + } + vector |= 1L << e.ordinal(); + } + return vector; + } + + /** + * This method forwards the call to {@link #expand(int)} only when autoExpand property is + * true. + */ + private IoBuffer autoExpand(int expectedRemaining) { + if (isAutoExpand()) { + expand(expectedRemaining, true); + } + return this; + } + + /** + * This method forwards the call to {@link #expand(int)} only when autoExpand property is + * true. + */ + private IoBuffer autoExpand(int pos, int expectedRemaining) { + if (isAutoExpand()) { + expand(pos, expectedRemaining, true); + } + return this; + } + + private static void checkFieldSize(int fieldSize) { + if (fieldSize < 0) { + throw new IllegalArgumentException("fieldSize cannot be negative: " + fieldSize); + } + } +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/BufferDataException.java b/src/main/java/com/google/code/yanf4j/buffer/BufferDataException.java new file mode 100644 index 0000000..fdd6639 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/BufferDataException.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +/** + * A {@link RuntimeException} which is thrown when the data the {@link IoBuffer} contains is + * corrupt. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + * + */ +public class BufferDataException extends RuntimeException { + private static final long serialVersionUID = -4138189188602563502L; + + public BufferDataException() { + super(); + } + + public BufferDataException(String message) { + super(message); + } + + public BufferDataException(String message, Throwable cause) { + super(message, cause); + } + + public BufferDataException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/CachedBufferAllocator.java b/src/main/java/com/google/code/yanf4j/buffer/CachedBufferAllocator.java new file mode 100644 index 0000000..8655ee8 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/CachedBufferAllocator.java @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import com.google.code.yanf4j.util.CircularQueue; + +/** + * An {@link IoBufferAllocator} that caches the buffers which are likely to be reused during + * auto-expansion of the buffers. + *

+ * In {@link SimpleBufferAllocator}, the underlying {@link ByteBuffer} of the {@link IoBuffer} is + * reallocated on its capacity change, which means the newly allocated bigger {@link ByteBuffer} + * replaces the old small {@link ByteBuffer} . Consequently, the old {@link ByteBuffer} is marked + * for garbage collection. + *

+ * It's not a problem in most cases as long as the capacity change doesn't happen frequently. + * However, once it happens too often, it burdens the VM and the cost of filling the newly allocated + * {@link ByteBuffer} with {@code NUL} surpass the cost of accessing the cache. In 2 dual-core + * Opteron Italy 270 processors, {@link CachedBufferAllocator} outperformed + * {@link SimpleBufferAllocator} in the following situation: + *

    + *
  • when a 32 bytes buffer is expanded 4 or more times,
  • + *
  • when a 64 bytes buffer is expanded 4 or more times,
  • + *
  • when a 128 bytes buffer is expanded 2 or more times,
  • + *
  • and when a 256 bytes or bigger buffer is expanded 1 or more times.
  • + *
+ * Please note the observation above is subject to change in a different environment. + *

+ * {@link CachedBufferAllocator} uses {@link ThreadLocal} to store the cached buffer, allocates + * buffers whose capacity is power of 2 only and provides performance advantage if + * {@link IoBuffer#free()} is called properly. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + */ +public class CachedBufferAllocator implements IoBufferAllocator { + + private static final int DEFAULT_MAX_POOL_SIZE = 8; + private static final int DEFAULT_MAX_CACHED_BUFFER_SIZE = 1 << 18; // 256KB + + private final int maxPoolSize; + private final int maxCachedBufferSize; + + private final ThreadLocal>> heapBuffers; + private final ThreadLocal>> directBuffers; + + /** + * Creates a new instance with the default parameters ({@literal #DEFAULT_MAX_POOL_SIZE} and + * {@literal #DEFAULT_MAX_CACHED_BUFFER_SIZE}). + */ + public CachedBufferAllocator() { + this(DEFAULT_MAX_POOL_SIZE, DEFAULT_MAX_CACHED_BUFFER_SIZE); + } + + /** + * Creates a new instance. + * + * @param maxPoolSize the maximum number of buffers with the same capacity per thread. 0 + * disables this limitation. + * @param maxCachedBufferSize the maximum capacity of a cached buffer. A buffer whose capacity is + * bigger than this value is not pooled. 0 disables this limitation. + */ + public CachedBufferAllocator(int maxPoolSize, int maxCachedBufferSize) { + if (maxPoolSize < 0) { + throw new IllegalArgumentException("maxPoolSize: " + maxPoolSize); + } + if (maxCachedBufferSize < 0) { + throw new IllegalArgumentException("maxCachedBufferSize: " + maxCachedBufferSize); + } + + this.maxPoolSize = maxPoolSize; + this.maxCachedBufferSize = maxCachedBufferSize; + + this.heapBuffers = new ThreadLocal>>() { + @Override + protected Map> initialValue() { + return newPoolMap(); + } + }; + this.directBuffers = new ThreadLocal>>() { + @Override + protected Map> initialValue() { + return newPoolMap(); + } + }; + } + + /** + * Returns the maximum number of buffers with the same capacity per thread. 0 means 'no + * limitation'. + */ + public int getMaxPoolSize() { + return this.maxPoolSize; + } + + /** + * Returns the maximum capacity of a cached buffer. A buffer whose capacity is bigger than this + * value is not pooled. 0 means 'no limitation'. + */ + public int getMaxCachedBufferSize() { + return this.maxCachedBufferSize; + } + + private Map> newPoolMap() { + Map> poolMap = new HashMap>(); + int poolSize = this.maxPoolSize == 0 ? DEFAULT_MAX_POOL_SIZE : this.maxPoolSize; + for (int i = 0; i < 31; i++) { + poolMap.put(1 << i, new CircularQueue(poolSize)); + } + poolMap.put(0, new CircularQueue(poolSize)); + poolMap.put(Integer.MAX_VALUE, new CircularQueue(poolSize)); + return poolMap; + } + + public IoBuffer allocate(int requestedCapacity, boolean direct) { + int actualCapacity = IoBuffer.normalizeCapacity(requestedCapacity); + IoBuffer buf; + if (this.maxCachedBufferSize != 0 && actualCapacity > this.maxCachedBufferSize) { + if (direct) { + buf = wrap(ByteBuffer.allocateDirect(actualCapacity)); + } else { + buf = wrap(ByteBuffer.allocate(actualCapacity)); + } + } else { + Queue pool; + if (direct) { + pool = this.directBuffers.get().get(actualCapacity); + } else { + pool = this.heapBuffers.get().get(actualCapacity); + } + + // Recycle if possible. + buf = pool.poll(); + if (buf != null) { + buf.clear(); + buf.setAutoExpand(false); + buf.order(ByteOrder.BIG_ENDIAN); + } else { + if (direct) { + buf = wrap(ByteBuffer.allocateDirect(actualCapacity)); + } else { + buf = wrap(ByteBuffer.allocate(actualCapacity)); + } + } + } + + buf.limit(requestedCapacity); + return buf; + } + + public ByteBuffer allocateNioBuffer(int capacity, boolean direct) { + return allocate(capacity, direct).buf(); + } + + public IoBuffer wrap(ByteBuffer nioBuffer) { + return new CachedBuffer(nioBuffer); + } + + public void dispose() {} + + private class CachedBuffer extends AbstractIoBuffer { + private final Thread ownerThread; + private ByteBuffer buf; + + protected CachedBuffer(ByteBuffer buf) { + super(CachedBufferAllocator.this, buf.capacity()); + this.ownerThread = Thread.currentThread(); + this.buf = buf; + buf.order(ByteOrder.BIG_ENDIAN); + } + + protected CachedBuffer(CachedBuffer parent, ByteBuffer buf) { + super(parent); + this.ownerThread = Thread.currentThread(); + this.buf = buf; + } + + @Override + public ByteBuffer buf() { + if (this.buf == null) { + throw new IllegalStateException("Buffer has been freed already."); + } + return this.buf; + } + + @Override + protected void buf(ByteBuffer buf) { + ByteBuffer oldBuf = this.buf; + this.buf = buf; + free(oldBuf); + } + + @Override + protected IoBuffer duplicate0() { + return new CachedBuffer(this, buf().duplicate()); + } + + @Override + protected IoBuffer slice0() { + return new CachedBuffer(this, buf().slice()); + } + + @Override + protected IoBuffer asReadOnlyBuffer0() { + return new CachedBuffer(this, buf().asReadOnlyBuffer()); + } + + @Override + public byte[] array() { + return buf().array(); + } + + @Override + public int arrayOffset() { + return buf().arrayOffset(); + } + + @Override + public boolean hasArray() { + return buf().hasArray(); + } + + @Override + public void free() { + free(this.buf); + this.buf = null; + } + + private void free(ByteBuffer oldBuf) { + if (oldBuf == null || oldBuf.capacity() > CachedBufferAllocator.this.maxCachedBufferSize + || oldBuf.isReadOnly() || isDerived() || Thread.currentThread() != this.ownerThread) { + return; + } + + // Add to the cache. + Queue pool; + if (oldBuf.isDirect()) { + pool = CachedBufferAllocator.this.directBuffers.get().get(oldBuf.capacity()); + } else { + pool = CachedBufferAllocator.this.heapBuffers.get().get(oldBuf.capacity()); + } + + if (pool == null) { + return; + } + + // Restrict the size of the pool to prevent OOM. + if (CachedBufferAllocator.this.maxPoolSize == 0 + || pool.size() < CachedBufferAllocator.this.maxPoolSize) { + pool.offer(new CachedBuffer(oldBuf)); + } + } + } +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/IoBuffer.java b/src/main/java/com/google/code/yanf4j/buffer/IoBuffer.java new file mode 100644 index 0000000..becf900 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/IoBuffer.java @@ -0,0 +1,1330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.nio.ReadOnlyBufferException; +import java.nio.ShortBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.util.EnumSet; +import java.util.Set; + +/** + * A byte buffer used by MINA applications. + *

+ * This is a replacement for {@link ByteBuffer}. Please refer to {@link ByteBuffer} documentation + * for preliminary usage. MINA does not use NIO {@link ByteBuffer} directly for two reasons: + *

    + *
  • + * It doesn't provide useful getters and putters such as fill, + * get/putString, and get/putAsciiInt() enough. + *
  • + *
  • + * It is difficult to write variable-length data due to its fixed capacity + *
  • + *
+ *

+ * + *

+ *

Allocation

+ *

+ * You can allocate a new heap buffer. + * + *

+ * IoBuffer buf = IoBuffer.allocate(1024, false);
+ * 
+ * + * you can also allocate a new direct buffer: + * + *
+ * IoBuffer buf = IoBuffer.allocate(1024, true);
+ * 
+ * + * or you can set the default buffer type. + * + *
+ * // Allocate heap buffer by default.
+ * IoBuffer.setUseDirectBuffer(false);
+ * // A new heap buffer is returned.
+ * IoBuffer buf = IoBuffer.allocate(1024);
+ * 
+ * + *

+ * + *

Wrapping existing NIO buffers and arrays

+ *

+ * This class provides a few wrap(...) methods that wraps any NIO buffers and byte arrays. + * + *

AutoExpand

+ *

+ * Writing variable-length data using NIO ByteBuffers is not really easy, and it is because + * its size is fixed. {@link IoBuffer} introduces autoExpand property. If + * autoExpand property is true, you never get {@link BufferOverflowException} or + * {@link IndexOutOfBoundsException} (except when index is negative). It automatically expands its + * capacity and limit value. For example: + * + *

+ * String greeting = messageBundle.getMessage("hello");
+ * IoBuffer buf = IoBuffer.allocate(16);
+ * // Turn on autoExpand (it is off by default)
+ * buf.setAutoExpand(true);
+ * buf.putString(greeting, utf8encoder);
+ * 
+ * + * The underlying {@link ByteBuffer} is reallocated by {@link IoBuffer} behind the scene if the + * encoded data is larger than 16 bytes in the example above. Its capacity will double, and its + * limit will increase to the last position the string is written. + *

+ * + *

AutoShrink

+ *

+ * You might also want to decrease the capacity of the buffer when most of the allocated memory area + * is not being used. {@link IoBuffer} provides autoShrink property to take care of this + * issue. If autoShrink is turned on, {@link IoBuffer} halves the capacity of the buffer + * when {@link #compact()} is invoked and only 1/4 or less of the current capacity is being used. + *

+ * You can also {@link #shrink()} method manually to shrink the capacity of the buffer. + *

+ * The underlying {@link ByteBuffer} is reallocated by {@link IoBuffer} behind the scene, and + * therefore {@link #buf()} will return a different {@link ByteBuffer} instance once capacity + * changes. Please also note {@link #compact()} or {@link #shrink()} will not decrease the capacity + * if the new capacity is less than the {@link #minimumCapacity()} of the buffer. + * + *

Derived Buffers

+ *

+ * Derived buffers are the buffers which were created by {@link #duplicate()}, {@link #slice()}, or + * {@link #asReadOnlyBuffer()}. They are useful especially when you broadcast the same messages to + * multiple {@link IoSession}s. Please note that the buffer derived from and its derived buffers are + * not both auto-expandable neither auto-shrinkable. Trying to call {@link #setAutoExpand(boolean)} + * or {@link #setAutoShrink(boolean)} with true parameter will raise an + * {@link IllegalStateException}. + *

+ * + *

Changing Buffer Allocation Policy

+ *

+ * {@link IoBufferAllocator} interface lets you override the default buffer management behavior. + * There are two allocators provided out-of-the-box: + *

    + *
  • {@link SimpleBufferAllocator} (default)
  • + *
  • {@link CachedBufferAllocator}
  • + *
+ * You can implement your own allocator and use it by calling + * {@link #setAllocator(IoBufferAllocator)}. + *

+ * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 748525 $, $Date: 2009-02-27 14:45:31 +0100 (Fri, 27 Feb 2009) $ + */ +public abstract class IoBuffer implements Comparable { + /** The allocator used to create new buffers */ + private static IoBufferAllocator allocator = new SimpleBufferAllocator(); + + /** A flag indicating which type of buffer we are using : heap or direct */ + private static boolean useDirectBuffer = false; + + /** + * Returns the allocator used by existing and new buffers + */ + public static IoBufferAllocator getAllocator() { + return allocator; + } + + /** + * Sets the allocator used by existing and new buffers + */ + public static void setAllocator(IoBufferAllocator newAllocator) { + if (newAllocator == null) { + throw new NullPointerException("allocator"); + } + + IoBufferAllocator oldAllocator = allocator; + + allocator = newAllocator; + + if (null != oldAllocator) { + oldAllocator.dispose(); + } + } + + /** + * Returns true if and only if a direct buffer is allocated by default when the type of + * the new buffer is not specified. The default value is false. + */ + public static boolean isUseDirectBuffer() { + return useDirectBuffer; + } + + /** + * Sets if a direct buffer should be allocated by default when the type of the new buffer is not + * specified. The default value is false. + */ + public static void setUseDirectBuffer(boolean useDirectBuffer) { + IoBuffer.useDirectBuffer = useDirectBuffer; + } + + /** + * Returns the direct or heap buffer which is capable to store the specified amount of bytes. + * + * @param capacity the capacity of the buffer + * + * @see #setUseDirectBuffer(boolean) + */ + public static IoBuffer allocate(int capacity) { + return allocate(capacity, useDirectBuffer); + } + + /** + * Returns the buffer which is capable of the specified size. + * + * @param capacity the capacity of the buffer + * @param direct true to get a direct buffer, false to get a heap buffer. + */ + public static IoBuffer allocate(int capacity, boolean direct) { + if (capacity < 0) { + throw new IllegalArgumentException("capacity: " + capacity); + } + + return allocator.allocate(capacity, direct); + } + + /** + * Wraps the specified NIO {@link ByteBuffer} into MINA buffer. + */ + public static IoBuffer wrap(ByteBuffer nioBuffer) { + return allocator.wrap(nioBuffer); + } + + /** + * Wraps the specified byte array into MINA heap buffer. + */ + public static IoBuffer wrap(byte[] byteArray) { + return wrap(ByteBuffer.wrap(byteArray)); + } + + /** + * Wraps the specified byte array into MINA heap buffer. + */ + public static IoBuffer wrap(byte[] byteArray, int offset, int length) { + return wrap(ByteBuffer.wrap(byteArray, offset, length)); + } + + /** + * Normalizes the specified capacity of the buffer to power of 2, which is often helpful for + * optimal memory usage and performance. If it is greater than or equal to + * {@link Integer#MAX_VALUE}, it returns {@link Integer#MAX_VALUE}. If it is zero, it returns + * zero. + */ + protected static int normalizeCapacity(int requestedCapacity) { + switch (requestedCapacity) { + case 0: + case 1 << 0: + case 1 << 1: + case 1 << 2: + case 1 << 3: + case 1 << 4: + case 1 << 5: + case 1 << 6: + case 1 << 7: + case 1 << 8: + case 1 << 9: + case 1 << 10: + case 1 << 11: + case 1 << 12: + case 1 << 13: + case 1 << 14: + case 1 << 15: + case 1 << 16: + case 1 << 17: + case 1 << 18: + case 1 << 19: + case 1 << 21: + case 1 << 22: + case 1 << 23: + case 1 << 24: + case 1 << 25: + case 1 << 26: + case 1 << 27: + case 1 << 28: + case 1 << 29: + case 1 << 30: + case Integer.MAX_VALUE: + return requestedCapacity; + } + + int newCapacity = 1; + while (newCapacity < requestedCapacity) { + newCapacity <<= 1; + if (newCapacity < 0) { + return Integer.MAX_VALUE; + } + } + return newCapacity; + } + + /** + * Creates a new instance. This is an empty constructor. + */ + protected IoBuffer() {} + + /** + * Declares this buffer and all its derived buffers are not used anymore so that it can be reused + * by some {@link IoBufferAllocator} implementations. It is not mandatory to call this method, but + * you might want to invoke this method for maximum performance. + */ + public abstract void free(); + + /** + * Returns the underlying NIO buffer instance. + */ + public abstract ByteBuffer buf(); + + /** + * @see ByteBuffer#isDirect() + */ + public abstract boolean isDirect(); + + /** + * returns true if and only if this buffer is derived from other buffer via + * {@link #duplicate()}, {@link #slice()} or {@link #asReadOnlyBuffer()}. + */ + public abstract boolean isDerived(); + + /** + * @see ByteBuffer#isReadOnly() + */ + public abstract boolean isReadOnly(); + + /** + * Returns the minimum capacity of this buffer which is used to determine the new capacity of the + * buffer shrunk by {@link #compact()} and {@link #shrink()} operation. The default value is the + * initial capacity of the buffer. + */ + public abstract int minimumCapacity(); + + /** + * Sets the minimum capacity of this buffer which is used to determine the new capacity of the + * buffer shrunk by {@link #compact()} and {@link #shrink()} operation. The default value is the + * initial capacity of the buffer. + */ + public abstract IoBuffer minimumCapacity(int minimumCapacity); + + /** + * @see ByteBuffer#capacity() + */ + public abstract int capacity(); + + /** + * Increases the capacity of this buffer. If the new capacity is less than or equal to the current + * capacity, this method returns silently. If the new capacity is greater than the current + * capacity, the buffer is reallocated while retaining the position, limit, mark and the content + * of the buffer. + */ + public abstract IoBuffer capacity(int newCapacity); + + /** + * Returns true if and only if autoExpand is turned on. + */ + public abstract boolean isAutoExpand(); + + /** + * Turns on or off autoExpand. + */ + public abstract IoBuffer setAutoExpand(boolean autoExpand); + + /** + * Returns true if and only if autoShrink is turned on. + */ + public abstract boolean isAutoShrink(); + + /** + * Turns on or off autoShrink. + */ + public abstract IoBuffer setAutoShrink(boolean autoShrink); + + /** + * Changes the capacity and limit of this buffer so this buffer get the specified + * expectedRemaining room from the current position. This method works even if you didn't + * set autoExpand to true. + */ + public abstract IoBuffer expand(int expectedRemaining); + + /** + * Changes the capacity and limit of this buffer so this buffer get the specified + * expectedRemaining room from the specified position. This method works even if + * you didn't set autoExpand to true. + */ + public abstract IoBuffer expand(int position, int expectedRemaining); + + /** + * Changes the capacity of this buffer so this buffer occupies as less memory as possible while + * retaining the position, limit and the buffer content between the position and limit. The + * capacity of the buffer never becomes less than {@link #minimumCapacity()}. The mark is + * discarded once the capacity changes. + */ + public abstract IoBuffer shrink(); + + /** + * @see java.nio.Buffer#position() + */ + public abstract int position(); + + /** + * @see java.nio.Buffer#position(int) + */ + public abstract IoBuffer position(int newPosition); + + /** + * @see java.nio.Buffer#limit() + */ + public abstract int limit(); + + /** + * @see java.nio.Buffer#limit(int) + */ + public abstract IoBuffer limit(int newLimit); + + /** + * @see java.nio.Buffer#mark() + */ + public abstract IoBuffer mark(); + + /** + * Returns the position of the current mark. This method returns -1 if no mark is set. + */ + public abstract int markValue(); + + /** + * @see java.nio.Buffer#reset() + */ + public abstract IoBuffer reset(); + + /** + * @see java.nio.Buffer#clear() + */ + public abstract IoBuffer clear(); + + /** + * Clears this buffer and fills its content with NUL. The position is set to zero, the + * limit is set to the capacity, and the mark is discarded. + */ + public abstract IoBuffer sweep(); + + /** + * double Clears this buffer and fills its content with value. The position is set to + * zero, the limit is set to the capacity, and the mark is discarded. + */ + public abstract IoBuffer sweep(byte value); + + /** + * @see java.nio.Buffer#flip() + */ + public abstract IoBuffer flip(); + + /** + * @see java.nio.Buffer#rewind() + */ + public abstract IoBuffer rewind(); + + /** + * @see java.nio.Buffer#remaining() + */ + public abstract int remaining(); + + /** + * @see java.nio.Buffer#hasRemaining() + */ + public abstract boolean hasRemaining(); + + /** + * @see ByteBuffer#duplicate() + */ + public abstract IoBuffer duplicate(); + + /** + * @see ByteBuffer#slice() + */ + public abstract IoBuffer slice(); + + /** + * @see ByteBuffer#asReadOnlyBuffer() + */ + public abstract IoBuffer asReadOnlyBuffer(); + + /** + * @see ByteBuffer#hasArray() + */ + public abstract boolean hasArray(); + + /** + * @see ByteBuffer#array() + */ + public abstract byte[] array(); + + /** + * @see ByteBuffer#arrayOffset() + */ + public abstract int arrayOffset(); + + /** + * @see ByteBuffer#get() + */ + public abstract byte get(); + + /** + * Reads one unsigned byte as a short integer. + */ + public abstract short getUnsigned(); + + /** + * @see ByteBuffer#put(byte) + */ + public abstract IoBuffer put(byte b); + + /** + * @see ByteBuffer#get(int) + */ + public abstract byte get(int index); + + /** + * Reads one byte as an unsigned short integer. + */ + public abstract short getUnsigned(int index); + + /** + * @see ByteBuffer#put(int, byte) + */ + public abstract IoBuffer put(int index, byte b); + + /** + * @see ByteBuffer#get(byte[], int, int) + */ + public abstract IoBuffer get(byte[] dst, int offset, int length); + + /** + * @see ByteBuffer#get(byte[]) + */ + public abstract IoBuffer get(byte[] dst); + + /** + * TODO document me. + */ + public abstract IoBuffer getSlice(int index, int length); + + /** + * TODO document me. + */ + public abstract IoBuffer getSlice(int length); + + /** + * Writes the content of the specified src into this buffer. + */ + public abstract IoBuffer put(ByteBuffer src); + + /** + * Writes the content of the specified src into this buffer. + */ + public abstract IoBuffer put(IoBuffer src); + + /** + * @see ByteBuffer#put(byte[], int, int) + */ + public abstract IoBuffer put(byte[] src, int offset, int length); + + /** + * @see ByteBuffer#put(byte[]) + */ + public abstract IoBuffer put(byte[] src); + + /** + * @see ByteBuffer#compact() + */ + public abstract IoBuffer compact(); + + /** + * @see ByteBuffer#order() + */ + public abstract ByteOrder order(); + + /** + * @see ByteBuffer#order(ByteOrder) + */ + public abstract IoBuffer order(ByteOrder bo); + + /** + * @see ByteBuffer#getChar() + */ + public abstract char getChar(); + + /** + * @see ByteBuffer#putChar(char) + */ + public abstract IoBuffer putChar(char value); + + /** + * @see ByteBuffer#getChar(int) + */ + public abstract char getChar(int index); + + /** + * @see ByteBuffer#putChar(int, char) + */ + public abstract IoBuffer putChar(int index, char value); + + /** + * @see ByteBuffer#asCharBuffer() + */ + public abstract CharBuffer asCharBuffer(); + + /** + * @see ByteBuffer#getShort() + */ + public abstract short getShort(); + + /** + * Reads two bytes unsigned integer. + */ + public abstract int getUnsignedShort(); + + /** + * @see ByteBuffer#putShort(short) + */ + public abstract IoBuffer putShort(short value); + + /** + * @see ByteBuffer#getShort() + */ + public abstract short getShort(int index); + + /** + * Reads two bytes unsigned integer. + */ + public abstract int getUnsignedShort(int index); + + /** + * @see ByteBuffer#putShort(int, short) + */ + public abstract IoBuffer putShort(int index, short value); + + /** + * @see ByteBuffer#asShortBuffer() + */ + public abstract ShortBuffer asShortBuffer(); + + /** + * @see ByteBuffer#getInt() + */ + public abstract int getInt(); + + /** + * Reads four bytes unsigned integer. + */ + public abstract long getUnsignedInt(); + + /** + * Relative get method for reading a medium int value. + * + *

+ * Reads the next three bytes at this buffer's current position, composing them into an int value + * according to the current byte order, and then increments the position by three. + *

+ * + * @return The medium int value at the buffer's current position + */ + public abstract int getMediumInt(); + + /** + * Relative get method for reading an unsigned medium int value. + * + *

+ * Reads the next three bytes at this buffer's current position, composing them into an int value + * according to the current byte order, and then increments the position by three. + *

+ * + * @return The unsigned medium int value at the buffer's current position + */ + public abstract int getUnsignedMediumInt(); + + /** + * Absolute get method for reading a medium int value. + * + *

+ * Reads the next three bytes at this buffer's current position, composing them into an int value + * according to the current byte order. + *

+ * + * @param index The index from which the medium int will be read + * @return The medium int value at the given index + * + * @throws IndexOutOfBoundsException If index is negative or not smaller than the + * buffer's limit + */ + public abstract int getMediumInt(int index); + + /** + * Absolute get method for reading an unsigned medium int value. + * + *

+ * Reads the next three bytes at this buffer's current position, composing them into an int value + * according to the current byte order. + *

+ * + * @param index The index from which the unsigned medium int will be read + * @return The unsigned medium int value at the given index + * + * @throws IndexOutOfBoundsException If index is negative or not smaller than the + * buffer's limit + */ + public abstract int getUnsignedMediumInt(int index); + + /** + * Relative put method for writing a medium int value. + * + *

+ * Writes three bytes containing the given int value, in the current byte order, into this buffer + * at the current position, and then increments the position by three. + *

+ * + * @param value The medium int value to be written + * + * @return This buffer + * + * @throws BufferOverflowException If there are fewer than three bytes remaining in this buffer + * + * @throws ReadOnlyBufferException If this buffer is read-only + */ + public abstract IoBuffer putMediumInt(int value); + + /** + * Absolute put method for writing a medium int value. + * + *

+ * Writes three bytes containing the given int value, in the current byte order, into this buffer + * at the given index. + *

+ * + * @param index The index at which the bytes will be written + * + * @param value The medium int value to be written + * + * @return This buffer + * + * @throws IndexOutOfBoundsException If index is negative or not smaller than the + * buffer's limit, minus three + * + * @throws ReadOnlyBufferException If this buffer is read-only + */ + public abstract IoBuffer putMediumInt(int index, int value); + + /** + * @see ByteBuffer#putInt(int) + */ + public abstract IoBuffer putInt(int value); + + /** + * @see ByteBuffer#getInt(int) + */ + public abstract int getInt(int index); + + /** + * Reads four bytes unsigned integer. + */ + public abstract long getUnsignedInt(int index); + + /** + * @see ByteBuffer#putInt(int, int) + */ + public abstract IoBuffer putInt(int index, int value); + + /** + * @see ByteBuffer#asIntBuffer() + */ + public abstract IntBuffer asIntBuffer(); + + /** + * @see ByteBuffer#getLong() + */ + public abstract long getLong(); + + /** + * @see ByteBuffer#putLong(int, long) + */ + public abstract IoBuffer putLong(long value); + + /** + * @see ByteBuffer#getLong(int) + */ + public abstract long getLong(int index); + + /** + * @see ByteBuffer#putLong(int, long) + */ + public abstract IoBuffer putLong(int index, long value); + + /** + * @see ByteBuffer#asLongBuffer() + */ + public abstract LongBuffer asLongBuffer(); + + /** + * @see ByteBuffer#getFloat() + */ + public abstract float getFloat(); + + /** + * @see ByteBuffer#putFloat(float) + */ + public abstract IoBuffer putFloat(float value); + + /** + * @see ByteBuffer#getFloat(int) + */ + public abstract float getFloat(int index); + + /** + * @see ByteBuffer#putFloat(int, float) + */ + public abstract IoBuffer putFloat(int index, float value); + + /** + * @see ByteBuffer#asFloatBuffer() + */ + public abstract FloatBuffer asFloatBuffer(); + + /** + * @see ByteBuffer#getDouble() + */ + public abstract double getDouble(); + + /** + * @see ByteBuffer#putDouble(double) + */ + public abstract IoBuffer putDouble(double value); + + /** + * @see ByteBuffer#getDouble(int) + */ + public abstract double getDouble(int index); + + /** + * @see ByteBuffer#putDouble(int, double) + */ + public abstract IoBuffer putDouble(int index, double value); + + /** + * @see ByteBuffer#asDoubleBuffer() + */ + public abstract DoubleBuffer asDoubleBuffer(); + + /** + * Returns an {@link InputStream} that reads the data from this buffer. {@link InputStream#read()} + * returns -1 if the buffer position reaches to the limit. + */ + public abstract InputStream asInputStream(); + + /** + * Returns an {@link OutputStream} that appends the data into this buffer. Please note that the + * {@link OutputStream#write(int)} will throw a {@link BufferOverflowException} instead of an + * {@link IOException} in case of buffer overflow. Please set autoExpand property by + * calling {@link #setAutoExpand(boolean)} to prevent the unexpected runtime exception. + */ + public abstract OutputStream asOutputStream(); + + /** + * Returns hexdump of this buffer. The data and pointer are not changed as a result of this method + * call. + * + * @return hexidecimal representation of this buffer + */ + public abstract String getHexDump(); + + /** + * Return hexdump of this buffer with limited length. + * + * @param lengthLimit The maximum number of bytes to dump from the current buffer position. + * @return hexidecimal representation of this buffer + */ + public abstract String getHexDump(int lengthLimit); + + // ////////////////////////////// + // String getters and putters // + // ////////////////////////////// + + /** + * Reads a NUL-terminated string from this buffer using the specified + * decoder and returns it. This method reads until the limit of this buffer if no + * NUL is found. + */ + public abstract String getString(CharsetDecoder decoder) throws CharacterCodingException; + + /** + * Reads a NUL-terminated string from this buffer using the specified + * decoder and returns it. + * + * @param fieldSize the maximum number of bytes to read + */ + public abstract String getString(int fieldSize, CharsetDecoder decoder) + throws CharacterCodingException; + + /** + * Writes the content of in into this buffer using the specified encoder + * . This method doesn't terminate string with NUL. You have to do it by yourself. + * + * @throws BufferOverflowException if the specified string doesn't fit + */ + public abstract IoBuffer putString(CharSequence val, CharsetEncoder encoder) + throws CharacterCodingException; + + /** + * Writes the content of in into this buffer as a NUL-terminated string + * using the specified encoder. + *

+ * If the charset name of the encoder is UTF-16, you cannot specify odd fieldSize, + * and this method will append two NULs as a terminator. + *

+ * Please note that this method doesn't terminate with NUL if the input string is + * longer than fieldSize. + * + * @param fieldSize the maximum number of bytes to write + */ + public abstract IoBuffer putString(CharSequence val, int fieldSize, CharsetEncoder encoder) + throws CharacterCodingException; + + /** + * Reads a string which has a 16-bit length field before the actual encoded string, using the + * specified decoder and returns it. This method is a shortcut for + * getPrefixedString(2, decoder). + */ + public abstract String getPrefixedString(CharsetDecoder decoder) throws CharacterCodingException; + + /** + * Reads a string which has a length field before the actual encoded string, using the specified + * decoder and returns it. + * + * @param prefixLength the length of the length field (1, 2, or 4) + */ + public abstract String getPrefixedString(int prefixLength, CharsetDecoder decoder) + throws CharacterCodingException; + + /** + * Writes the content of in into this buffer as a string which has a 16-bit length + * field before the actual encoded string, using the specified encoder. This method + * is a shortcut for putPrefixedString(in, 2, 0, encoder). + * + * @throws BufferOverflowException if the specified string doesn't fit + */ + public abstract IoBuffer putPrefixedString(CharSequence in, CharsetEncoder encoder) + throws CharacterCodingException; + + /** + * Writes the content of in into this buffer as a string which has a 16-bit length + * field before the actual encoded string, using the specified encoder. This method + * is a shortcut for putPrefixedString(in, prefixLength, 0, encoder). + * + * @param prefixLength the length of the length field (1, 2, or 4) + * + * @throws BufferOverflowException if the specified string doesn't fit + */ + public abstract IoBuffer putPrefixedString(CharSequence in, int prefixLength, + CharsetEncoder encoder) throws CharacterCodingException; + + /** + * Writes the content of in into this buffer as a string which has a 16-bit length + * field before the actual encoded string, using the specified encoder. This method + * is a shortcut for putPrefixedString(in, prefixLength, padding, ( byte ) 0, encoder) . + * + * @param prefixLength the length of the length field (1, 2, or 4) + * @param padding the number of padded NULs (1 (or 0), 2, or 4) + * + * @throws BufferOverflowException if the specified string doesn't fit + */ + public abstract IoBuffer putPrefixedString(CharSequence in, int prefixLength, int padding, + CharsetEncoder encoder) throws CharacterCodingException; + + /** + * Writes the content of in into this buffer as a string which has a 16-bit length + * field before the actual encoded string, using the specified encoder. + * + * @param prefixLength the length of the length field (1, 2, or 4) + * @param padding the number of padded bytes (1 (or 0), 2, or 4) + * @param padValue the value of padded bytes + * + * @throws BufferOverflowException if the specified string doesn't fit + */ + public abstract IoBuffer putPrefixedString(CharSequence val, int prefixLength, int padding, + byte padValue, CharsetEncoder encoder) throws CharacterCodingException; + + /** + * Reads a Java object from the buffer using the context {@link ClassLoader} of the current + * thread. + */ + public abstract Object getObject() throws ClassNotFoundException; + + /** + * Reads a Java object from the buffer using the specified classLoader. + */ + public abstract Object getObject(final ClassLoader classLoader) throws ClassNotFoundException; + + /** + * Writes the specified Java object to the buffer. + */ + public abstract IoBuffer putObject(Object o); + + /** + * Returns true if this buffer contains a data which has a data length as a prefix and + * the buffer has remaining data as enough as specified in the data length field. This method is + * identical with prefixedDataAvailable( prefixLength, Integer.MAX_VALUE ). Please not + * that using this method can allow DoS (Denial of Service) attack in case the remote peer sends + * too big data length value. It is recommended to use {@link #prefixedDataAvailable(int, int)} + * instead. + * + * @param prefixLength the length of the prefix field (1, 2, or 4) + * + * @throws IllegalArgumentException if prefixLength is wrong + * @throws BufferDataException if data length is negative + */ + public abstract boolean prefixedDataAvailable(int prefixLength); + + /** + * Returns true if this buffer contains a data which has a data length as a prefix and + * the buffer has remaining data as enough as specified in the data length field. + * + * @param prefixLength the length of the prefix field (1, 2, or 4) + * @param maxDataLength the allowed maximum of the read data length + * + * @throws IllegalArgumentException if prefixLength is wrong + * @throws BufferDataException if data length is negative or greater then maxDataLength + */ + public abstract boolean prefixedDataAvailable(int prefixLength, int maxDataLength); + + // /////////////////// + // IndexOf methods // + // /////////////////// + + /** + * Returns the first occurence position of the specified byte from the current position to the + * current limit. + * + * @return -1 if the specified byte is not found + */ + public abstract int indexOf(byte b); + + // //////////////////////// + // Skip or fill methods // + // //////////////////////// + + /** + * Forwards the position of this buffer as the specified size bytes. + */ + public abstract IoBuffer skip(int size); + + /** + * Fills this buffer with the specified value. This method moves buffer position forward. + */ + public abstract IoBuffer fill(byte value, int size); + + /** + * Fills this buffer with the specified value. This method does not change buffer position. + */ + public abstract IoBuffer fillAndReset(byte value, int size); + + /** + * Fills this buffer with NUL (0x00). This method moves buffer position forward. + */ + public abstract IoBuffer fill(int size); + + /** + * Fills this buffer with NUL (0x00). This method does not change buffer position. + */ + public abstract IoBuffer fillAndReset(int size); + + // //////////////////////// + // Enum methods // + // //////////////////////// + + /** + * Reads a byte from the buffer and returns the correlating enum constant defined by the specified + * enum type. + * + * @param The enum type to return + * @param enumClass The enum's class object + */ + public abstract > E getEnum(Class enumClass); + + /** + * Reads a byte from the buffer and returns the correlating enum constant defined by the specified + * enum type. + * + * @param The enum type to return + * @param index the index from which the byte will be read + * @param enumClass The enum's class object + */ + public abstract > E getEnum(int index, Class enumClass); + + /** + * Reads a short from the buffer and returns the correlating enum constant defined by the + * specified enum type. + * + * @param The enum type to return + * @param enumClass The enum's class object + */ + public abstract > E getEnumShort(Class enumClass); + + /** + * Reads a short from the buffer and returns the correlating enum constant defined by the + * specified enum type. + * + * @param The enum type to return + * @param index the index from which the bytes will be read + * @param enumClass The enum's class object + */ + public abstract > E getEnumShort(int index, Class enumClass); + + /** + * Reads an int from the buffer and returns the correlating enum constant defined by the specified + * enum type. + * + * @param The enum type to return + * @param enumClass The enum's class object + */ + public abstract > E getEnumInt(Class enumClass); + + /** + * Reads an int from the buffer and returns the correlating enum constant defined by the specified + * enum type. + * + * @param The enum type to return + * @param index the index from which the bytes will be read + * @param enumClass The enum's class object + */ + public abstract > E getEnumInt(int index, Class enumClass); + + /** + * Writes an enum's ordinal value to the buffer as a byte. + * + * @param e The enum to write to the buffer + */ + public abstract IoBuffer putEnum(Enum e); + + /** + * Writes an enum's ordinal value to the buffer as a byte. + * + * @param index The index at which the byte will be written + * @param e The enum to write to the buffer + */ + public abstract IoBuffer putEnum(int index, Enum e); + + /** + * Writes an enum's ordinal value to the buffer as a short. + * + * @param e The enum to write to the buffer + */ + public abstract IoBuffer putEnumShort(Enum e); + + /** + * Writes an enum's ordinal value to the buffer as a short. + * + * @param index The index at which the bytes will be written + * @param e The enum to write to the buffer + */ + public abstract IoBuffer putEnumShort(int index, Enum e); + + /** + * Writes an enum's ordinal value to the buffer as an integer. + * + * @param e The enum to write to the buffer + */ + public abstract IoBuffer putEnumInt(Enum e); + + /** + * Writes an enum's ordinal value to the buffer as an integer. + * + * @param index The index at which the bytes will be written + * @param e The enum to write to the buffer + */ + public abstract IoBuffer putEnumInt(int index, Enum e); + + // //////////////////////// + // EnumSet methods // + // //////////////////////// + + /** + * Reads a byte sized bit vector and converts it to an {@link EnumSet}. + * + *

+ * Each bit is mapped to a value in the specified enum. The least significant bit maps to the + * first entry in the specified enum and each subsequent bit maps to each subsequent bit as mapped + * to the subsequent enum value. + *

+ * + * @param the enum type + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSet(Class enumClass); + + /** + * Reads a byte sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param index the index from which the byte will be read + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSet(int index, Class enumClass); + + /** + * Reads a short sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSetShort(Class enumClass); + + /** + * Reads a short sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param index the index from which the bytes will be read + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSetShort(int index, Class enumClass); + + /** + * Reads an int sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSetInt(Class enumClass); + + /** + * Reads an int sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param index the index from which the bytes will be read + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSetInt(int index, Class enumClass); + + /** + * Reads a long sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSetLong(Class enumClass); + + /** + * Reads a long sized bit vector and converts it to an {@link EnumSet}. + * + * @see #getEnumSet(Class) + * @param the enum type + * @param index the index from which the bytes will be read + * @param enumClass the enum class used to create the EnumSet + * @return the EnumSet representation of the bit vector + */ + public abstract > EnumSet getEnumSetLong(int index, Class enumClass); + + /** + * Writes the specified {@link Set} to the buffer as a byte sized bit vector. + * + * @param the enum type of the Set + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSet(Set set); + + /** + * Writes the specified {@link Set} to the buffer as a byte sized bit vector. + * + * @param the enum type of the Set + * @param index the index at which the byte will be written + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSet(int index, Set set); + + /** + * Writes the specified {@link Set} to the buffer as a short sized bit vector. + * + * @param the enum type of the Set + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSetShort(Set set); + + /** + * Writes the specified {@link Set} to the buffer as a short sized bit vector. + * + * @param the enum type of the Set + * @param index the index at which the bytes will be written + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSetShort(int index, Set set); + + /** + * Writes the specified {@link Set} to the buffer as an int sized bit vector. + * + * @param the enum type of the Set + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSetInt(Set set); + + /** + * Writes the specified {@link Set} to the buffer as an int sized bit vector. + * + * @param the enum type of the Set + * @param index the index at which the bytes will be written + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSetInt(int index, Set set); + + /** + * Writes the specified {@link Set} to the buffer as a long sized bit vector. + * + * @param the enum type of the Set + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSetLong(Set set); + + /** + * Writes the specified {@link Set} to the buffer as a long sized bit vector. + * + * @param the enum type of the Set + * @param index the index at which the bytes will be written + * @param set the enum set to write to the buffer + */ + public abstract > IoBuffer putEnumSetLong(int index, Set set); +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/IoBufferAllocator.java b/src/main/java/com/google/code/yanf4j/buffer/IoBufferAllocator.java new file mode 100644 index 0000000..b4b8859 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/IoBufferAllocator.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +import java.nio.ByteBuffer; + +/** + * Allocates {@link IoBuffer}s and manages them. Please implement this interface if you need more + * advanced memory management scheme. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + */ +public interface IoBufferAllocator { + /** + * Returns the buffer which is capable of the specified size. + * + * @param capacity the capacity of the buffer + * @param direct true to get a direct buffer, false to get a heap buffer. + */ + IoBuffer allocate(int capacity, boolean direct); + + /** + * Returns the NIO buffer which is capable of the specified size. + * + * @param capacity the capacity of the buffer + * @param direct true to get a direct buffer, false to get a heap buffer. + */ + ByteBuffer allocateNioBuffer(int capacity, boolean direct); + + /** + * Wraps the specified NIO {@link ByteBuffer} into MINA buffer. + */ + IoBuffer wrap(ByteBuffer nioBuffer); + + /** + * Dispose of this allocator. + */ + void dispose(); +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/IoBufferHexDumper.java b/src/main/java/com/google/code/yanf4j/buffer/IoBufferHexDumper.java new file mode 100644 index 0000000..339b8bb --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/IoBufferHexDumper.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +/** + * Provides utility methods to dump an {@link IoBuffer} into a hex formatted string. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 686598 $, $Date: 2008-08-17 12:58:23 +0200 (Sun, 17 Aug 2008) $ + */ +class IoBufferHexDumper { + + /** + * The high digits lookup table. + */ + private static final byte[] highDigits; + + /** + * The low digits lookup table. + */ + private static final byte[] lowDigits; + + /** + * Initialize lookup tables. + */ + static { + final byte[] digits = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + int i; + byte[] high = new byte[256]; + byte[] low = new byte[256]; + + for (i = 0; i < 256; i++) { + high[i] = digits[i >>> 4]; + low[i] = digits[i & 0x0F]; + } + + highDigits = high; + lowDigits = low; + } + + /** + * Dumps an {@link IoBuffer} to a hex formatted string. + * + * @param in the buffer to dump + * @param lengthLimit the limit at which hex dumping will stop + * @return a hex formatted string representation of the in {@link Iobuffer}. + */ + public static String getHexdump(IoBuffer in, int lengthLimit) { + if (lengthLimit == 0) { + throw new IllegalArgumentException("lengthLimit: " + lengthLimit + " (expected: 1+)"); + } + + boolean truncate = in.remaining() > lengthLimit; + int size; + if (truncate) { + size = lengthLimit; + } else { + size = in.remaining(); + } + + if (size == 0) { + return "empty"; + } + + StringBuilder out = new StringBuilder(in.remaining() * 3 - 1); + + int mark = in.position(); + + // fill the first + int byteValue = in.get() & 0xFF; + out.append((char) highDigits[byteValue]); + out.append((char) lowDigits[byteValue]); + size--; + + // and the others, too + for (; size > 0; size--) { + out.append(' '); + byteValue = in.get() & 0xFF; + out.append((char) highDigits[byteValue]); + out.append((char) lowDigits[byteValue]); + } + + in.position(mark); + + if (truncate) { + out.append("..."); + } + + return out.toString(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/IoBufferWrapper.java b/src/main/java/com/google/code/yanf4j/buffer/IoBufferWrapper.java new file mode 100644 index 0000000..ed3d0da --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/IoBufferWrapper.java @@ -0,0 +1,891 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +import java.io.FilterOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.LongBuffer; +import java.nio.ShortBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.util.EnumSet; +import java.util.Set; + +/** + * A {@link IoBuffer} that wraps a buffer and proxies any operations to it. + *

+ * You can think this class like a {@link FilterOutputStream}. All operations are proxied by default + * so that you can extend this class and override existing operations selectively. You can introduce + * new operations, too. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + */ +public class IoBufferWrapper extends IoBuffer { + + /** + * The buffer proxied by this proxy. + */ + private final IoBuffer buf; + + /** + * Create a new instance. + * + * @param buf the buffer to be proxied + */ + protected IoBufferWrapper(IoBuffer buf) { + if (buf == null) { + throw new NullPointerException("buf"); + } + this.buf = buf; + } + + /** + * Returns the parent buffer that this buffer wrapped. + */ + public IoBuffer getParentBuffer() { + return buf; + } + + @Override + public boolean isDirect() { + return buf.isDirect(); + } + + @Override + public ByteBuffer buf() { + return buf.buf(); + } + + @Override + public int capacity() { + return buf.capacity(); + } + + @Override + public int position() { + return buf.position(); + } + + @Override + public IoBuffer position(int newPosition) { + buf.position(newPosition); + return this; + } + + @Override + public int limit() { + return buf.limit(); + } + + @Override + public IoBuffer limit(int newLimit) { + buf.limit(newLimit); + return this; + } + + @Override + public IoBuffer mark() { + buf.mark(); + return this; + } + + @Override + public IoBuffer reset() { + buf.reset(); + return this; + } + + @Override + public IoBuffer clear() { + buf.clear(); + return this; + } + + @Override + public IoBuffer sweep() { + buf.sweep(); + return this; + } + + @Override + public IoBuffer sweep(byte value) { + buf.sweep(value); + return this; + } + + @Override + public IoBuffer flip() { + buf.flip(); + return this; + } + + @Override + public IoBuffer rewind() { + buf.rewind(); + return this; + } + + @Override + public int remaining() { + return buf.remaining(); + } + + @Override + public boolean hasRemaining() { + return buf.hasRemaining(); + } + + @Override + public byte get() { + return buf.get(); + } + + @Override + public short getUnsigned() { + return buf.getUnsigned(); + } + + @Override + public IoBuffer put(byte b) { + buf.put(b); + return this; + } + + @Override + public byte get(int index) { + return buf.get(index); + } + + @Override + public short getUnsigned(int index) { + return buf.getUnsigned(index); + } + + @Override + public IoBuffer put(int index, byte b) { + buf.put(index, b); + return this; + } + + @Override + public IoBuffer get(byte[] dst, int offset, int length) { + buf.get(dst, offset, length); + return this; + } + + @Override + public IoBuffer getSlice(int index, int length) { + return buf.getSlice(index, length); + } + + @Override + public IoBuffer getSlice(int length) { + return buf.getSlice(length); + } + + @Override + public IoBuffer get(byte[] dst) { + buf.get(dst); + return this; + } + + @Override + public IoBuffer put(IoBuffer src) { + buf.put(src); + return this; + } + + @Override + public IoBuffer put(ByteBuffer src) { + buf.put(src); + return this; + } + + @Override + public IoBuffer put(byte[] src, int offset, int length) { + buf.put(src, offset, length); + return this; + } + + @Override + public IoBuffer put(byte[] src) { + buf.put(src); + return this; + } + + @Override + public IoBuffer compact() { + buf.compact(); + return this; + } + + @Override + public String toString() { + return buf.toString(); + } + + @Override + public int hashCode() { + return buf.hashCode(); + } + + @Override + public boolean equals(Object ob) { + return buf.equals(ob); + } + + public int compareTo(IoBuffer that) { + return buf.compareTo(that); + } + + @Override + public ByteOrder order() { + return buf.order(); + } + + @Override + public IoBuffer order(ByteOrder bo) { + buf.order(bo); + return this; + } + + @Override + public char getChar() { + return buf.getChar(); + } + + @Override + public IoBuffer putChar(char value) { + buf.putChar(value); + return this; + } + + @Override + public char getChar(int index) { + return buf.getChar(index); + } + + @Override + public IoBuffer putChar(int index, char value) { + buf.putChar(index, value); + return this; + } + + @Override + public CharBuffer asCharBuffer() { + return buf.asCharBuffer(); + } + + @Override + public short getShort() { + return buf.getShort(); + } + + @Override + public int getUnsignedShort() { + return buf.getUnsignedShort(); + } + + @Override + public IoBuffer putShort(short value) { + buf.putShort(value); + return this; + } + + @Override + public short getShort(int index) { + return buf.getShort(index); + } + + @Override + public int getUnsignedShort(int index) { + return buf.getUnsignedShort(index); + } + + @Override + public IoBuffer putShort(int index, short value) { + buf.putShort(index, value); + return this; + } + + @Override + public ShortBuffer asShortBuffer() { + return buf.asShortBuffer(); + } + + @Override + public int getInt() { + return buf.getInt(); + } + + @Override + public long getUnsignedInt() { + return buf.getUnsignedInt(); + } + + @Override + public IoBuffer putInt(int value) { + buf.putInt(value); + return this; + } + + @Override + public int getInt(int index) { + return buf.getInt(index); + } + + @Override + public long getUnsignedInt(int index) { + return buf.getUnsignedInt(index); + } + + @Override + public IoBuffer putInt(int index, int value) { + buf.putInt(index, value); + return this; + } + + @Override + public IntBuffer asIntBuffer() { + return buf.asIntBuffer(); + } + + @Override + public long getLong() { + return buf.getLong(); + } + + @Override + public IoBuffer putLong(long value) { + buf.putLong(value); + return this; + } + + @Override + public long getLong(int index) { + return buf.getLong(index); + } + + @Override + public IoBuffer putLong(int index, long value) { + buf.putLong(index, value); + return this; + } + + @Override + public LongBuffer asLongBuffer() { + return buf.asLongBuffer(); + } + + @Override + public float getFloat() { + return buf.getFloat(); + } + + @Override + public IoBuffer putFloat(float value) { + buf.putFloat(value); + return this; + } + + @Override + public float getFloat(int index) { + return buf.getFloat(index); + } + + @Override + public IoBuffer putFloat(int index, float value) { + buf.putFloat(index, value); + return this; + } + + @Override + public FloatBuffer asFloatBuffer() { + return buf.asFloatBuffer(); + } + + @Override + public double getDouble() { + return buf.getDouble(); + } + + @Override + public IoBuffer putDouble(double value) { + buf.putDouble(value); + return this; + } + + @Override + public double getDouble(int index) { + return buf.getDouble(index); + } + + @Override + public IoBuffer putDouble(int index, double value) { + buf.putDouble(index, value); + return this; + } + + @Override + public DoubleBuffer asDoubleBuffer() { + return buf.asDoubleBuffer(); + } + + @Override + public String getHexDump() { + return buf.getHexDump(); + } + + @Override + public String getString(int fieldSize, CharsetDecoder decoder) throws CharacterCodingException { + return buf.getString(fieldSize, decoder); + } + + @Override + public String getString(CharsetDecoder decoder) throws CharacterCodingException { + return buf.getString(decoder); + } + + @Override + public String getPrefixedString(CharsetDecoder decoder) throws CharacterCodingException { + return buf.getPrefixedString(decoder); + } + + @Override + public String getPrefixedString(int prefixLength, CharsetDecoder decoder) + throws CharacterCodingException { + return buf.getPrefixedString(prefixLength, decoder); + } + + @Override + public IoBuffer putString(CharSequence in, int fieldSize, CharsetEncoder encoder) + throws CharacterCodingException { + buf.putString(in, fieldSize, encoder); + return this; + } + + @Override + public IoBuffer putString(CharSequence in, CharsetEncoder encoder) + throws CharacterCodingException { + buf.putString(in, encoder); + return this; + } + + @Override + public IoBuffer putPrefixedString(CharSequence in, CharsetEncoder encoder) + throws CharacterCodingException { + buf.putPrefixedString(in, encoder); + return this; + } + + @Override + public IoBuffer putPrefixedString(CharSequence in, int prefixLength, CharsetEncoder encoder) + throws CharacterCodingException { + buf.putPrefixedString(in, prefixLength, encoder); + return this; + } + + @Override + public IoBuffer putPrefixedString(CharSequence in, int prefixLength, int padding, + CharsetEncoder encoder) throws CharacterCodingException { + buf.putPrefixedString(in, prefixLength, padding, encoder); + return this; + } + + @Override + public IoBuffer putPrefixedString(CharSequence in, int prefixLength, int padding, byte padValue, + CharsetEncoder encoder) throws CharacterCodingException { + buf.putPrefixedString(in, prefixLength, padding, padValue, encoder); + return this; + } + + @Override + public IoBuffer skip(int size) { + buf.skip(size); + return this; + } + + @Override + public IoBuffer fill(byte value, int size) { + buf.fill(value, size); + return this; + } + + @Override + public IoBuffer fillAndReset(byte value, int size) { + buf.fillAndReset(value, size); + return this; + } + + @Override + public IoBuffer fill(int size) { + buf.fill(size); + return this; + } + + @Override + public IoBuffer fillAndReset(int size) { + buf.fillAndReset(size); + return this; + } + + @Override + public boolean isAutoExpand() { + return buf.isAutoExpand(); + } + + @Override + public IoBuffer setAutoExpand(boolean autoExpand) { + buf.setAutoExpand(autoExpand); + return this; + } + + @Override + public IoBuffer expand(int pos, int expectedRemaining) { + buf.expand(pos, expectedRemaining); + return this; + } + + @Override + public IoBuffer expand(int expectedRemaining) { + buf.expand(expectedRemaining); + return this; + } + + @Override + public Object getObject() throws ClassNotFoundException { + return buf.getObject(); + } + + @Override + public Object getObject(ClassLoader classLoader) throws ClassNotFoundException { + return buf.getObject(classLoader); + } + + @Override + public IoBuffer putObject(Object o) { + buf.putObject(o); + return this; + } + + @Override + public InputStream asInputStream() { + return buf.asInputStream(); + } + + @Override + public OutputStream asOutputStream() { + return buf.asOutputStream(); + } + + @Override + public IoBuffer duplicate() { + return buf.duplicate(); + } + + @Override + public IoBuffer slice() { + return buf.slice(); + } + + @Override + public IoBuffer asReadOnlyBuffer() { + return buf.asReadOnlyBuffer(); + } + + @Override + public byte[] array() { + return buf.array(); + } + + @Override + public int arrayOffset() { + return buf.arrayOffset(); + } + + @Override + public int minimumCapacity() { + return buf.minimumCapacity(); + } + + @Override + public IoBuffer minimumCapacity(int minimumCapacity) { + buf.minimumCapacity(minimumCapacity); + return this; + } + + @Override + public IoBuffer capacity(int newCapacity) { + buf.capacity(newCapacity); + return this; + } + + @Override + public boolean isReadOnly() { + return buf.isReadOnly(); + } + + @Override + public int markValue() { + return buf.markValue(); + } + + @Override + public boolean hasArray() { + return buf.hasArray(); + } + + @Override + public void free() { + buf.free(); + } + + @Override + public boolean isDerived() { + return buf.isDerived(); + } + + @Override + public boolean isAutoShrink() { + return buf.isAutoShrink(); + } + + @Override + public IoBuffer setAutoShrink(boolean autoShrink) { + buf.setAutoShrink(autoShrink); + return this; + } + + @Override + public IoBuffer shrink() { + buf.shrink(); + return this; + } + + @Override + public int getMediumInt() { + return buf.getMediumInt(); + } + + @Override + public int getUnsignedMediumInt() { + return buf.getUnsignedMediumInt(); + } + + @Override + public int getMediumInt(int index) { + return buf.getMediumInt(index); + } + + @Override + public int getUnsignedMediumInt(int index) { + return buf.getUnsignedMediumInt(index); + } + + @Override + public IoBuffer putMediumInt(int value) { + buf.putMediumInt(value); + return this; + } + + @Override + public IoBuffer putMediumInt(int index, int value) { + buf.putMediumInt(index, value); + return this; + } + + @Override + public String getHexDump(int lengthLimit) { + return buf.getHexDump(lengthLimit); + } + + @Override + public boolean prefixedDataAvailable(int prefixLength) { + return buf.prefixedDataAvailable(prefixLength); + } + + @Override + public boolean prefixedDataAvailable(int prefixLength, int maxDataLength) { + return buf.prefixedDataAvailable(prefixLength, maxDataLength); + } + + @Override + public int indexOf(byte b) { + return buf.indexOf(b); + } + + @Override + public > E getEnum(Class enumClass) { + return buf.getEnum(enumClass); + } + + @Override + public > E getEnum(int index, Class enumClass) { + return buf.getEnum(index, enumClass); + } + + @Override + public > E getEnumShort(Class enumClass) { + return buf.getEnumShort(enumClass); + } + + @Override + public > E getEnumShort(int index, Class enumClass) { + return buf.getEnumShort(index, enumClass); + } + + @Override + public > E getEnumInt(Class enumClass) { + return buf.getEnumInt(enumClass); + } + + @Override + public > E getEnumInt(int index, Class enumClass) { + return buf.getEnumInt(index, enumClass); + } + + @Override + public IoBuffer putEnum(Enum e) { + buf.putEnum(e); + return this; + } + + @Override + public IoBuffer putEnum(int index, Enum e) { + buf.putEnum(index, e); + return this; + } + + @Override + public IoBuffer putEnumShort(Enum e) { + buf.putEnumShort(e); + return this; + } + + @Override + public IoBuffer putEnumShort(int index, Enum e) { + buf.putEnumShort(index, e); + return this; + } + + @Override + public IoBuffer putEnumInt(Enum e) { + buf.putEnumInt(e); + return this; + } + + @Override + public IoBuffer putEnumInt(int index, Enum e) { + buf.putEnumInt(index, e); + return this; + } + + @Override + public > EnumSet getEnumSet(Class enumClass) { + return buf.getEnumSet(enumClass); + } + + @Override + public > EnumSet getEnumSet(int index, Class enumClass) { + return buf.getEnumSet(index, enumClass); + } + + @Override + public > EnumSet getEnumSetShort(Class enumClass) { + return buf.getEnumSetShort(enumClass); + } + + @Override + public > EnumSet getEnumSetShort(int index, Class enumClass) { + return buf.getEnumSetShort(index, enumClass); + } + + @Override + public > EnumSet getEnumSetInt(Class enumClass) { + return buf.getEnumSetInt(enumClass); + } + + @Override + public > EnumSet getEnumSetInt(int index, Class enumClass) { + return buf.getEnumSetInt(index, enumClass); + } + + @Override + public > EnumSet getEnumSetLong(Class enumClass) { + return buf.getEnumSetLong(enumClass); + } + + @Override + public > EnumSet getEnumSetLong(int index, Class enumClass) { + return buf.getEnumSetLong(index, enumClass); + } + + @Override + public > IoBuffer putEnumSet(Set set) { + buf.putEnumSet(set); + return this; + } + + @Override + public > IoBuffer putEnumSet(int index, Set set) { + buf.putEnumSet(index, set); + return this; + } + + @Override + public > IoBuffer putEnumSetShort(Set set) { + buf.putEnumSetShort(set); + return this; + } + + @Override + public > IoBuffer putEnumSetShort(int index, Set set) { + buf.putEnumSetShort(index, set); + return this; + } + + @Override + public > IoBuffer putEnumSetInt(Set set) { + buf.putEnumSetInt(set); + return this; + } + + @Override + public > IoBuffer putEnumSetInt(int index, Set set) { + buf.putEnumSetInt(index, set); + return this; + } + + @Override + public > IoBuffer putEnumSetLong(Set set) { + buf.putEnumSetLong(set); + return this; + } + + @Override + public > IoBuffer putEnumSetLong(int index, Set set) { + buf.putEnumSetLong(index, set); + return this; + } +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/SimpleBufferAllocator.java b/src/main/java/com/google/code/yanf4j/buffer/SimpleBufferAllocator.java new file mode 100644 index 0000000..3fd6c93 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/SimpleBufferAllocator.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * A simplistic {@link IoBufferAllocator} which simply allocates a new buffer every time. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + */ +public class SimpleBufferAllocator implements IoBufferAllocator { + + public IoBuffer allocate(int capacity, boolean direct) { + return wrap(allocateNioBuffer(capacity, direct)); + } + + public ByteBuffer allocateNioBuffer(int capacity, boolean direct) { + ByteBuffer nioBuffer; + if (direct) { + nioBuffer = ByteBuffer.allocateDirect(capacity); + } else { + nioBuffer = ByteBuffer.allocate(capacity); + } + return nioBuffer; + } + + public IoBuffer wrap(ByteBuffer nioBuffer) { + return new SimpleBuffer(nioBuffer); + } + + public void dispose() {} + + private class SimpleBuffer extends AbstractIoBuffer { + private ByteBuffer buf; + + protected SimpleBuffer(ByteBuffer buf) { + super(SimpleBufferAllocator.this, buf.capacity()); + this.buf = buf; + buf.order(ByteOrder.BIG_ENDIAN); + } + + protected SimpleBuffer(SimpleBuffer parent, ByteBuffer buf) { + super(parent); + this.buf = buf; + } + + @Override + public ByteBuffer buf() { + return buf; + } + + @Override + protected void buf(ByteBuffer buf) { + this.buf = buf; + } + + @Override + protected IoBuffer duplicate0() { + return new SimpleBuffer(this, this.buf.duplicate()); + } + + @Override + protected IoBuffer slice0() { + return new SimpleBuffer(this, this.buf.slice()); + } + + @Override + protected IoBuffer asReadOnlyBuffer0() { + return new SimpleBuffer(this, this.buf.asReadOnlyBuffer()); + } + + @Override + public byte[] array() { + return buf.array(); + } + + @Override + public int arrayOffset() { + return buf.arrayOffset(); + } + + @Override + public boolean hasArray() { + return buf.hasArray(); + } + + @Override + public void free() {} + } +} diff --git a/src/main/java/com/google/code/yanf4j/buffer/package.html b/src/main/java/com/google/code/yanf4j/buffer/package.html new file mode 100644 index 0000000..bb70cb9 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/buffer/package.html @@ -0,0 +1,10 @@ + + + + + IoBuffer + + +

IoBuffer from mina

+ + \ No newline at end of file diff --git a/src/main/java/com/google/code/yanf4j/config/Configuration.java b/src/main/java/com/google/code/yanf4j/config/Configuration.java new file mode 100644 index 0000000..eb797e9 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/config/Configuration.java @@ -0,0 +1,191 @@ +/** + * Copyright [2009-2010] [dennis zhuang] 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.google.code.yanf4j.config; + +import net.rubyeye.xmemcached.impl.ReconnectRequest; +import java.util.concurrent.DelayQueue; +import com.google.code.yanf4j.util.SystemUtils; + +/** + * Networking configuration + * + * @author dennis + * + */ +public class Configuration { + + public static final String XMEMCACHED_SELECTOR_POOL_SIZE = "xmemcached.selector.pool.size"; + + /** + * Read buffer size per connection + */ + private int sessionReadBufferSize = 32 * 1024; + + /** + * Socket SO_TIMEOUT option + */ + private int soTimeout = 0; + + /** + * Thread count for processing WRITABLE event + */ + private int writeThreadCount = 0; + + /** + * Whether to enable statistics + */ + private boolean statisticsServer = false; + + /** + * Whether to handle read write concurrently,default is true + */ + private boolean handleReadWriteConcurrently = true; + + /** + * Thread coount for processing message dispatching + */ + private int dispatchMessageThreadCount = 0; + + /** + * THread count for processing READABLE event + */ + private int readThreadCount = 1; + + private int selectorPoolSize = + System.getProperty(XMEMCACHED_SELECTOR_POOL_SIZE) == null ? SystemUtils.getSystemThreadCount() + : Integer.parseInt(System.getProperty(XMEMCACHED_SELECTOR_POOL_SIZE)); + + /** + * Increasing buffer size per time + */ + public static final int DEFAULT_INCREASE_BUFF_SIZE = 32 * 1024; + + /** + * Max read buffer size for connection + */ + public final static int MAX_READ_BUFFER_SIZE = 128 * 1024; + + /** + * check session idle interval + */ + private long checkSessionTimeoutInterval = 1000L; + + public final int getWriteThreadCount() { + return this.writeThreadCount; + } + + public final int getDispatchMessageThreadCount() { + return this.dispatchMessageThreadCount; + } + + public final void setDispatchMessageThreadCount(int dispatchMessageThreadCount) { + this.dispatchMessageThreadCount = dispatchMessageThreadCount; + } + + public final void setWriteThreadCount(int writeThreadCount) { + this.writeThreadCount = writeThreadCount; + } + + private long sessionIdleTimeout = 5000L; + + /** + * @see setSessionIdleTimeout + * @return + */ + public final long getSessionIdleTimeout() { + return this.sessionIdleTimeout; + } + + public final void setSessionIdleTimeout(long sessionIdleTimeout) { + this.sessionIdleTimeout = sessionIdleTimeout; + } + + /** + * @see setSessionReadBufferSize + * @return + */ + public final int getSessionReadBufferSize() { + return this.sessionReadBufferSize; + } + + public final boolean isHandleReadWriteConcurrently() { + return this.handleReadWriteConcurrently; + } + + public final int getSoTimeout() { + return this.soTimeout; + } + + protected long statisticsInterval = 5 * 60 * 1000L; + + public final long getStatisticsInterval() { + return this.statisticsInterval; + } + + public final void setStatisticsInterval(long statisticsInterval) { + this.statisticsInterval = statisticsInterval; + } + + public final void setSoTimeout(int soTimeout) { + if (soTimeout < 0) { + throw new IllegalArgumentException("soTimeout<0"); + } + this.soTimeout = soTimeout; + } + + public final void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + this.handleReadWriteConcurrently = handleReadWriteConcurrently; + } + + public final void setSessionReadBufferSize(int tcpHandlerReadBufferSize) { + if (tcpHandlerReadBufferSize <= 0) { + throw new IllegalArgumentException("tcpHandlerReadBufferSize<=0"); + } + this.sessionReadBufferSize = tcpHandlerReadBufferSize; + } + + public final boolean isStatisticsServer() { + return this.statisticsServer; + } + + public final void setStatisticsServer(boolean statisticsServer) { + this.statisticsServer = statisticsServer; + } + + /** + * @see setReadThreadCount + * @return + */ + public final int getReadThreadCount() { + return this.readThreadCount; + } + + public final void setReadThreadCount(int readThreadCount) { + if (readThreadCount < 0) { + throw new IllegalArgumentException("readThreadCount<0"); + } + this.readThreadCount = readThreadCount; + } + + public void setCheckSessionTimeoutInterval(long checkSessionTimeoutInterval) { + this.checkSessionTimeoutInterval = checkSessionTimeoutInterval; + } + + public long getCheckSessionTimeoutInterval() { + return this.checkSessionTimeoutInterval; + } + + public void setSelectorPoolSize(int selectorPoolSize) { + this.selectorPoolSize = selectorPoolSize; + } + + public int getSelectorPoolSize() { + return selectorPoolSize; + } +} diff --git a/src/main/java/com/google/code/yanf4j/config/package.html b/src/main/java/com/google/code/yanf4j/config/package.html new file mode 100644 index 0000000..89574a3 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/config/package.html @@ -0,0 +1,10 @@ + + + + + Networking configuration + + +

Networking configuration

+ + \ No newline at end of file diff --git a/src/main/java/com/google/code/yanf4j/core/CodecFactory.java b/src/main/java/com/google/code/yanf4j/core/CodecFactory.java new file mode 100644 index 0000000..d15a0b0 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/CodecFactory.java @@ -0,0 +1,42 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * + * + * Codec factory + * + * @author boyan + * + */ +public interface CodecFactory { + + public interface Encoder { + public IoBuffer encode(Object message, Session session); + } + + public interface Decoder { + public Object decode(IoBuffer buff, Session session); + } + + public Encoder getEncoder(); + + public Decoder getDecoder(); +} diff --git a/src/main/java/com/google/code/yanf4j/core/Controller.java b/src/main/java/com/google/code/yanf4j/core/Controller.java new file mode 100644 index 0000000..d2c8ba6 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/Controller.java @@ -0,0 +1,96 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +import java.io.IOException; +import java.net.InetSocketAddress; +import com.google.code.yanf4j.statistics.Statistics; + +/** + * Networking Controller + * + * + * @author boyan + * + */ +public interface Controller { + + public abstract long getSessionTimeout(); + + public long getSessionIdleTimeout(); + + public void setSessionIdleTimeout(long sessionIdleTimeout); + + public abstract void setSessionTimeout(long sessionTimeout); + + public abstract int getSoTimeout(); + + public abstract void setSoTimeout(int timeout); + + public abstract void addStateListener(ControllerStateListener listener); + + public void removeStateListener(ControllerStateListener listener); + + public abstract boolean isHandleReadWriteConcurrently(); + + public abstract void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently); + + public abstract int getReadThreadCount(); + + public abstract void setReadThreadCount(int readThreadCount); + + public abstract Handler getHandler(); + + public abstract void setHandler(Handler handler); + + public abstract int getPort(); + + public abstract void start() throws IOException; + + public abstract boolean isStarted(); + + public abstract Statistics getStatistics(); + + public abstract CodecFactory getCodecFactory(); + + public abstract void setCodecFactory(CodecFactory codecFactory); + + public abstract void stop() throws IOException; + + public void setReceiveThroughputLimit(double receivePacketRate); + + public double getReceiveThroughputLimit(); + + public double getSendThroughputLimit(); + + public void setSendThroughputLimit(double sendThroughputLimit); + + public InetSocketAddress getLocalSocketAddress(); + + public void setLocalSocketAddress(InetSocketAddress inetAddress); + + public int getDispatchMessageThreadCount(); + + public void setDispatchMessageThreadCount(int dispatchMessageThreadPoolSize); + + public int getWriteThreadCount(); + + public void setWriteThreadCount(int writeThreadCount); + + public void setSocketOption(SocketOption socketOption, T value); + +} diff --git a/src/main/java/com/google/code/yanf4j/core/ControllerLifeCycle.java b/src/main/java/com/google/code/yanf4j/core/ControllerLifeCycle.java new file mode 100644 index 0000000..d4d40cd --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/ControllerLifeCycle.java @@ -0,0 +1,37 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * Controller lifecycle mark interface + * + * @author boyan + * + */ + +public interface ControllerLifeCycle { + + public void notifyReady(); + + public void notifyStarted(); + + public void notifyAllSessionClosed(); + + public void notifyException(Throwable t); + + public void notifyStopped(); +} diff --git a/src/main/java/com/google/code/yanf4j/core/ControllerStateListener.java b/src/main/java/com/google/code/yanf4j/core/ControllerStateListener.java new file mode 100644 index 0000000..31f6e69 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/ControllerStateListener.java @@ -0,0 +1,58 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * + * Controller state listener + * + * @author boyan + * + * @since 1.0, 2009-12-16 ����05:59:44 + */ +public interface ControllerStateListener { + + /** + * When controller is started + * + * @param controller + */ + public void onStarted(final Controller controller); + + /** + * When controller is ready + * + * @param controller + */ + public void onReady(final Controller controller); + + /** + * When all connections are closed + * + * @param controller + */ + public void onAllSessionClosed(final Controller controller); + + /** + * When controller has been stopped + * + * @param controller + */ + public void onStopped(final Controller controller); + + public void onException(final Controller controller, Throwable t); +} diff --git a/src/main/java/com/google/code/yanf4j/core/Dispatcher.java b/src/main/java/com/google/code/yanf4j/core/Dispatcher.java new file mode 100644 index 0000000..07c074a --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/Dispatcher.java @@ -0,0 +1,29 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * Dispatcher + * + * @author dennis + * + */ +public interface Dispatcher { + public void dispatch(Runnable r); + + public void stop(); +} diff --git a/src/main/java/com/google/code/yanf4j/core/EventType.java b/src/main/java/com/google/code/yanf4j/core/EventType.java new file mode 100644 index 0000000..697fea0 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/EventType.java @@ -0,0 +1,27 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * Event Type + * + * @author dennis + * + */ +public enum EventType { + REGISTER, READABLE, WRITEABLE, ENABLE_READ, ENABLE_WRITE, UNREGISTER, EXPIRED, IDLE, CONNECTED +} diff --git a/src/main/java/com/google/code/yanf4j/core/Handler.java b/src/main/java/com/google/code/yanf4j/core/Handler.java new file mode 100644 index 0000000..a89dfae --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/Handler.java @@ -0,0 +1,46 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * + * IO Event handler + * + * @author boyan + * + */ +public interface Handler { + + public void onSessionCreated(Session session); + + public void onSessionStarted(Session session); + + public void onSessionClosed(Session session); + + public void onMessageReceived(Session session, Object msg); + + public void onMessageSent(Session session, Object msg); + + public void onExceptionCaught(Session session, Throwable throwable); + + public void onSessionExpired(Session session); + + public void onSessionIdle(Session session); + + public void onSessionConnected(Session session); + +} diff --git a/src/main/java/com/google/code/yanf4j/core/Session.java b/src/main/java/com/google/code/yanf4j/core/Session.java new file mode 100644 index 0000000..f6f9504 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/Session.java @@ -0,0 +1,216 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteOrder; + +/** + * Abstract connection + * + * @author dennis + * + */ +public interface Session { + + public enum SessionStatus { + NULL, READING, WRITING, IDLE, INITIALIZE, CLOSING, CLOSED + } + + /** + * Start session + */ + public void start(); + + /** + * Write a message,if you don't care when the message is written + * + * @param packet + */ + public void write(Object packet); + + /** + * Check if session is closed + * + * @return + */ + public boolean isClosed(); + + /** + * Close session + */ + public void close(); + + /** + * Return the remote end's InetSocketAddress + * + * @return + */ + public InetSocketAddress getRemoteSocketAddress(); + + public InetAddress getLocalAddress(); + + /** + * Return true if using blocking write + * + * @return + */ + public boolean isUseBlockingWrite(); + + /** + * Set if using blocking write + * + * @param useBlockingWrite + */ + public void setUseBlockingWrite(boolean useBlockingWrite); + + /** + * Return true if using blocking read + * + * @return + */ + public boolean isUseBlockingRead(); + + public void setUseBlockingRead(boolean useBlockingRead); + + /** + * Flush the write queue,this method may be no effect if OP_WRITE is running. + */ + public void flush(); + + /** + * Return true if session is expired,session is expired beacause you set the sessionTimeout,if + * since session's last operation form now is over this vlaue,isExpired return true,and + * Handler.onExpired() will be invoked. + * + * @return + */ + public boolean isExpired(); + + /** + * Check if session is idle + * + * @return + */ + public boolean isIdle(); + + /** + * Return current encoder + * + * @return + */ + public CodecFactory.Encoder getEncoder(); + + /** + * Set encoder + * + * @param encoder + */ + public void setEncoder(CodecFactory.Encoder encoder); + + /** + * Return current decoder + * + * @return + */ + + public CodecFactory.Decoder getDecoder(); + + public void setDecoder(CodecFactory.Decoder decoder); + + /** + * Return true if allow handling read and write concurrently,default is true. + * + * @return + */ + public boolean isHandleReadWriteConcurrently(); + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently); + + /** + * Return the session read buffer's byte order,big end or little end. + * + * @return + */ + public ByteOrder getReadBufferByteOrder(); + + public void setReadBufferByteOrder(ByteOrder readBufferByteOrder); + + /** + * Set a attribute attched with this session + * + * @param key + * @param value + */ + public void setAttribute(String key, Object value); + + /** + * Remove attribute + * + * @param key + */ + public void removeAttribute(String key); + + /** + * Return attribute associated with key + * + * @param key + * @return + */ + public Object getAttribute(String key); + + /** + * Clear attributes + */ + public void clearAttributes(); + + /** + * Return the bytes in write queue,there bytes is in memory.Use this method to controll writing + * speed. + * + * @return + */ + public long getScheduleWritenBytes(); + + /** + * Return last operation timestamp,operation include read,write,idle + * + * @return + */ + public long getLastOperationTimeStamp(); + + /** + * return true if it is a loopback connection + * + * @return + */ + public boolean isLoopbackConnection(); + + public long getSessionIdleTimeout(); + + public void setSessionIdleTimeout(long sessionIdleTimeout); + + public long getSessionTimeout(); + + public void setSessionTimeout(long sessionTimeout); + + public Object setAttributeIfAbsent(String key, Object value); + + public Handler getHandler(); + +} diff --git a/src/main/java/com/google/code/yanf4j/core/SessionConfig.java b/src/main/java/com/google/code/yanf4j/core/SessionConfig.java new file mode 100644 index 0000000..97419f4 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/SessionConfig.java @@ -0,0 +1,51 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +import java.util.Queue; +import com.google.code.yanf4j.statistics.Statistics; + +/** + * Session configuration + * + * @author dennis + * + */ +public class SessionConfig { + public final Handler handler; + public final CodecFactory codecFactory; + public final Statistics statistics; + public final Queue queue; + public final Dispatcher dispatchMessageDispatcher; + public final boolean handleReadWriteConcurrently; + public final long sessionTimeout; + public final long sessionIdelTimeout; + + public SessionConfig(Handler handler, CodecFactory codecFactory, Statistics statistics, + Queue queue, Dispatcher dispatchMessageDispatcher, + boolean handleReadWriteConcurrently, long sessionTimeout, long sessionIdelTimeout) { + + this.handler = handler; + this.codecFactory = codecFactory; + this.statistics = statistics; + this.queue = queue; + this.dispatchMessageDispatcher = dispatchMessageDispatcher; + this.handleReadWriteConcurrently = handleReadWriteConcurrently; + this.sessionTimeout = sessionTimeout; + this.sessionIdelTimeout = sessionIdelTimeout; + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/SessionManager.java b/src/main/java/com/google/code/yanf4j/core/SessionManager.java new file mode 100644 index 0000000..d07c05b --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/SessionManager.java @@ -0,0 +1,39 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * Session manager + * + * @author dennis + * + */ +public interface SessionManager { + /** + * Register session to controller + * + * @param session + */ + public void registerSession(Session session); + + /** + * Unregister session + * + * @param session + */ + public void unregisterSession(Session session); +} diff --git a/src/main/java/com/google/code/yanf4j/core/SocketOption.java b/src/main/java/com/google/code/yanf4j/core/SocketOption.java new file mode 100644 index 0000000..262e1ab --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/SocketOption.java @@ -0,0 +1,79 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +/** + * Socket option + * + * @author dennis + * + * @param + */ +public class SocketOption { + + private final String name; + private final Class type; + + public SocketOption(String name, Class type) { + this.name = name; + this.type = type; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (this.name == null ? 0 : this.name.hashCode()); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SocketOption other = (SocketOption) obj; + if (this.name == null) { + if (other.name != null) { + return false; + } + } else if (!this.name.equals(other.name)) { + return false; + } + return true; + } + + public String name() { + return this.name; + } + + public Class type() { + return this.type; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/WriteMessage.java b/src/main/java/com/google/code/yanf4j/core/WriteMessage.java new file mode 100644 index 0000000..8a833f1 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/WriteMessage.java @@ -0,0 +1,42 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.impl.FutureImpl; + +/** + * Write message with a buffer + * + * @author dennis + * + */ +public interface WriteMessage { + + public void writing(); + + public boolean isWriting(); + + public IoBuffer getWriteBuffer(); + + public Object getMessage(); + + public void setWriteBuffer(IoBuffer buffers); + + public FutureImpl getWriteFuture(); + +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/AbstractController.java b/src/main/java/com/google/code/yanf4j/core/impl/AbstractController.java new file mode 100644 index 0000000..75db772 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/AbstractController.java @@ -0,0 +1,541 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ThreadPoolExecutor; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Controller; +import com.google.code.yanf4j.core.ControllerLifeCycle; +import com.google.code.yanf4j.core.ControllerStateListener; +import com.google.code.yanf4j.core.Dispatcher; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.SocketOption; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.statistics.Statistics; +import com.google.code.yanf4j.statistics.impl.DefaultStatistics; +import com.google.code.yanf4j.statistics.impl.SimpleStatistics; +import com.google.code.yanf4j.util.ConcurrentHashSet; +import com.google.code.yanf4j.util.DispatcherFactory; +import com.google.code.yanf4j.util.LinkedTransferQueue; + +/** + * Base controller + * + * @author dennis + * + */ +public abstract class AbstractController implements Controller, ControllerLifeCycle { + + protected Statistics statistics = new DefaultStatistics(); + protected long statisticsInterval; + + protected static final Logger log = LoggerFactory.getLogger(AbstractController.class); + /** + * controller state listener list + */ + protected CopyOnWriteArrayList stateListeners = + new CopyOnWriteArrayList(); + /** + * Event handler + */ + protected Handler handler; + /** + * Codec Factory + */ + + protected CodecFactory codecFactory; + /** + * Status + */ + protected volatile boolean started; + /** + * local bind address + */ + protected InetSocketAddress localSocketAddress; + /** + * Read event processing thread count + */ + protected int readThreadCount; + protected int writeThreadCount; + protected int dispatchMessageThreadCount; + protected Configuration configuration; + protected Dispatcher readEventDispatcher, dispatchMessageDispatcher, writeEventDispatcher; + protected long sessionTimeout; + protected boolean handleReadWriteConcurrently = true; + + protected int soTimeout; + + /** + * Socket options + */ + @SuppressWarnings("unchecked") + protected Map socketOptions = new HashMap(); + + @SuppressWarnings("unchecked") + public void setSocketOptions(Map socketOptions) { + if (socketOptions == null) { + throw new NullPointerException("Null socketOptions"); + } + this.socketOptions = socketOptions; + } + + /** + * Connected session set + */ + protected Set sessionSet = new ConcurrentHashSet(); + private Thread shutdownHookThread; + private volatile boolean isHutdownHookCalled = false; + + public final int getDispatchMessageThreadCount() { + return dispatchMessageThreadCount; + } + + public final void setDispatchMessageThreadCount(int dispatchMessageThreadPoolSize) { + if (started) { + throw new IllegalStateException("Controller is started"); + } + if (dispatchMessageThreadPoolSize < 0) { + throw new IllegalArgumentException("dispatchMessageThreadPoolSize<0"); + } + dispatchMessageThreadCount = dispatchMessageThreadPoolSize; + } + + public long getSessionIdleTimeout() { + return configuration.getSessionIdleTimeout(); + } + + /** + * Build write queue for session + * + * @return + */ + protected Queue buildQueue() { + return new LinkedTransferQueue(); + } + + public void setSessionIdleTimeout(long sessionIdleTimeout) { + configuration.setSessionIdleTimeout(sessionIdleTimeout); + + } + + public long getSessionTimeout() { + return sessionTimeout; + } + + public void setSessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public int getSoTimeout() { + return soTimeout; + } + + public void setSoTimeout(int timeout) { + soTimeout = timeout; + } + + public AbstractController() { + this(new Configuration(), null, null); + } + + public double getReceiveThroughputLimit() { + return statistics.getReceiveThroughputLimit(); + } + + public double getSendThroughputLimit() { + return statistics.getSendThroughputLimit(); + } + + public void setReceiveThroughputLimit(double receiveThroughputLimit) { + statistics.setReceiveThroughputLimit(receiveThroughputLimit); + + } + + public void setSendThroughputLimit(double sendThroughputLimit) { + statistics.setSendThroughputLimit(sendThroughputLimit); + } + + public AbstractController(Configuration configuration) { + this(configuration, null, null); + + } + + public AbstractController(Configuration configuration, CodecFactory codecFactory) { + this(configuration, null, codecFactory); + } + + public AbstractController(Configuration configuration, Handler handler, + CodecFactory codecFactory) { + init(configuration, handler, codecFactory); + } + + private synchronized void init(Configuration configuration, Handler handler, + CodecFactory codecFactory) { + setHandler(handler); + setCodecFactory(codecFactory); + setConfiguration(configuration); + setReadThreadCount(configuration.getReadThreadCount()); + setWriteThreadCount(configuration.getWriteThreadCount()); + setDispatchMessageThreadCount(configuration.getDispatchMessageThreadCount()); + setHandleReadWriteConcurrently(configuration.isHandleReadWriteConcurrently()); + setSoTimeout(configuration.getSoTimeout()); + setStatisticsConfig(configuration); + setReceiveThroughputLimit(-0.1d); + setStarted(false); + } + + void setStarted(boolean started) { + this.started = started; + } + + private void setStatisticsConfig(Configuration configuration) { + if (configuration.isStatisticsServer()) { + statistics = new SimpleStatistics(); + statisticsInterval = configuration.getStatisticsInterval(); + + } else { + statistics = new DefaultStatistics(); + statisticsInterval = -1; + } + } + + public Configuration getConfiguration() { + return configuration; + } + + public void setConfiguration(Configuration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("Null Configuration"); + } + this.configuration = configuration; + } + + public InetSocketAddress getLocalSocketAddress() { + return localSocketAddress; + } + + public void setLocalSocketAddress(InetSocketAddress inetSocketAddress) { + localSocketAddress = inetSocketAddress; + } + + public void onAccept(SelectionKey sk) throws IOException { + statistics.statisticsAccept(); + } + + public void onConnect(SelectionKey key) throws IOException { + throw new UnsupportedOperationException(); + } + + public void addStateListener(ControllerStateListener listener) { + stateListeners.add(listener); + } + + public void removeStateListener(ControllerStateListener listener) { + stateListeners.remove(listener); + } + + public boolean isHandleReadWriteConcurrently() { + return handleReadWriteConcurrently; + } + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + this.handleReadWriteConcurrently = handleReadWriteConcurrently; + } + + public int getReadThreadCount() { + return readThreadCount; + } + + public void setReadThreadCount(int readThreadCount) { + if (started) { + throw new IllegalStateException(); + } + if (readThreadCount < 0) { + throw new IllegalArgumentException("readThreadCount<0"); + } + this.readThreadCount = readThreadCount; + } + + public final int getWriteThreadCount() { + return writeThreadCount; + } + + public final void setWriteThreadCount(int writeThreadCount) { + if (started) { + throw new IllegalStateException(); + } + if (writeThreadCount < 0) { + throw new IllegalArgumentException("readThreadCount<0"); + } + this.writeThreadCount = writeThreadCount; + } + + public Handler getHandler() { + return handler; + } + + public void setHandler(Handler handler) { + if (started) { + throw new IllegalStateException("The Controller have started"); + } + this.handler = handler; + } + + public int getPort() { + if (localSocketAddress != null) { + return localSocketAddress.getPort(); + } + throw new NullPointerException("Controller is not binded"); + } + + public synchronized void start() throws IOException { + if (isStarted()) { + return; + } + if (getHandler() == null) { + throw new IOException("The handler is null"); + } + if (getCodecFactory() == null) { + setCodecFactory(new ByteBufferCodecFactory()); + } + setStarted(true); + setReadEventDispatcher(DispatcherFactory.newDispatcher(getReadThreadCount(), + new ThreadPoolExecutor.CallerRunsPolicy(), "xmemcached-read-thread")); + setWriteEventDispatcher(DispatcherFactory.newDispatcher(getWriteThreadCount(), + new ThreadPoolExecutor.CallerRunsPolicy(), "xmemcached-write-thread")); + setDispatchMessageDispatcher(DispatcherFactory.newDispatcher(getDispatchMessageThreadCount(), + new ThreadPoolExecutor.CallerRunsPolicy(), "xmemcached-dispatch-thread")); + startStatistics(); + start0(); + notifyStarted(); + if (AddrUtil.isEnableShutDownHook()) { + shutdownHookThread = new Thread() { + @Override + public void run() { + try { + isHutdownHookCalled = true; + AbstractController.this.stop(); + } catch (IOException e) { + log.error("Stop controller fail", e); + } + } + }; + Runtime.getRuntime().addShutdownHook(shutdownHookThread); + } + log.info("The Controller started at " + localSocketAddress + " ..."); + } + + protected abstract void start0() throws IOException; + + void setDispatchMessageDispatcher(Dispatcher dispatcher) { + Dispatcher oldDispatcher = dispatchMessageDispatcher; + dispatchMessageDispatcher = dispatcher; + if (oldDispatcher != null) { + oldDispatcher.stop(); + } + } + + Dispatcher getReadEventDispatcher() { + return readEventDispatcher; + } + + void setReadEventDispatcher(Dispatcher dispatcher) { + Dispatcher oldDispatcher = readEventDispatcher; + readEventDispatcher = dispatcher; + if (oldDispatcher != null) { + oldDispatcher.stop(); + } + } + + void setWriteEventDispatcher(Dispatcher dispatcher) { + Dispatcher oldDispatcher = writeEventDispatcher; + writeEventDispatcher = dispatcher; + if (oldDispatcher != null) { + oldDispatcher.stop(); + } + } + + private final void startStatistics() { + statistics.start(); + } + + public void notifyStarted() { + for (ControllerStateListener stateListener : stateListeners) { + stateListener.onStarted(this); + } + } + + public boolean isStarted() { + return started; + } + + public final Statistics getStatistics() { + return statistics; + } + + public final CodecFactory getCodecFactory() { + return codecFactory; + } + + public final void setCodecFactory(CodecFactory codecFactory) { + this.codecFactory = codecFactory; + } + + public void notifyReady() { + for (ControllerStateListener stateListener : stateListeners) { + stateListener.onReady(this); + } + } + + public final synchronized void unregisterSession(Session session) { + sessionSet.remove(session); + if (sessionSet.size() == 0) { + notifyAllSessionClosed(); + notifyAll(); + } + } + + public void checkStatisticsForRestart() { + if (statisticsInterval > 0 + && System.currentTimeMillis() - statistics.getStartedTime() > statisticsInterval * 1000) { + statistics.restart(); + } + } + + public final synchronized void registerSession(Session session) { + if (started) { + sessionSet.add(session); + } else { + session.close(); + } + + } + + public void stop() throws IOException { + synchronized (this) { + if (!isStarted()) { + return; + } + setStarted(false); + } + for (Session session : sessionSet) { + session.close(); + } + stopStatistics(); + stopDispatcher(); + sessionSet.clear(); + notifyStopped(); + clearStateListeners(); + stop0(); + if (AddrUtil.isEnableShutDownHook() && shutdownHookThread != null && !isHutdownHookCalled) { + Runtime.getRuntime().removeShutdownHook(shutdownHookThread); + } + log.info("Controller has been stopped."); + } + + protected abstract void stop0() throws IOException; + + private final void stopDispatcher() { + if (readEventDispatcher != null) { + readEventDispatcher.stop(); + } + if (dispatchMessageDispatcher != null) { + dispatchMessageDispatcher.stop(); + } + if (writeEventDispatcher != null) { + writeEventDispatcher.stop(); + } + } + + private final void stopStatistics() { + statistics.stop(); + } + + private final void clearStateListeners() { + stateListeners.clear(); + } + + public final void notifyException(Throwable t) { + for (ControllerStateListener stateListener : stateListeners) { + stateListener.onException(this, t); + } + } + + public final void notifyStopped() { + for (ControllerStateListener stateListener : stateListeners) { + stateListener.onStopped(this); + } + } + + public final void notifyAllSessionClosed() { + for (ControllerStateListener stateListener : stateListeners) { + stateListener.onAllSessionClosed(this); + } + } + + public Set getSessionSet() { + return Collections.unmodifiableSet(sessionSet); + } + + public void setSocketOption(SocketOption socketOption, T value) { + if (socketOption == null) { + throw new NullPointerException("Null socketOption"); + } + if (value == null) { + throw new NullPointerException("Null value"); + } + if (!socketOption.type().equals(value.getClass())) { + throw new IllegalArgumentException("Expected " + socketOption.type().getSimpleName() + + " value,but givend " + value.getClass().getSimpleName()); + } + socketOptions.put(socketOption, value); + } + + @SuppressWarnings("unchecked") + public T getSocketOption(SocketOption socketOption) { + return (T) socketOptions.get(socketOption); + } + + /** + * Bind localhost address + * + * @param inetSocketAddress + * @throws IOException + */ + public void bind(InetSocketAddress inetSocketAddress) throws IOException { + if (inetSocketAddress == null) { + throw new IllegalArgumentException("Null inetSocketAddress"); + } + setLocalSocketAddress(inetSocketAddress); + start(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/AbstractSession.java b/src/main/java/com/google/code/yanf4j/core/impl/AbstractSession.java new file mode 100644 index 0000000..d6546d8 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/AbstractSession.java @@ -0,0 +1,439 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Dispatcher; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.SessionConfig; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.statistics.Statistics; +import com.google.code.yanf4j.util.LinkedTransferQueue; + +/** + * Base connection + * + * @author dennis + * + */ +public abstract class AbstractSession implements Session { + + protected IoBuffer readBuffer; + protected static final Logger log = LoggerFactory.getLogger(AbstractSession.class); + + protected final ConcurrentHashMap attributes = + new ConcurrentHashMap(); + + protected Queue writeQueue; + + protected long sessionIdleTimeout; + + protected long sessionTimeout; + + public long getSessionIdleTimeout() { + return sessionIdleTimeout; + } + + public void setSessionIdleTimeout(long sessionIdleTimeout) { + this.sessionIdleTimeout = sessionIdleTimeout; + } + + public long getSessionTimeout() { + return sessionTimeout; + } + + public void setSessionTimeout(long sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public Queue getWriteQueue() { + return writeQueue; + } + + public Statistics getStatistics() { + return statistics; + } + + public Handler getHandler() { + return handler; + } + + public Dispatcher getDispatchMessageDispatcher() { + return dispatchMessageDispatcher; + } + + public ReentrantLock getWriteLock() { + return writeLock; + } + + protected CodecFactory.Encoder encoder; + protected CodecFactory.Decoder decoder; + + protected volatile boolean closed; + + protected Statistics statistics; + + protected Handler handler; + + protected boolean loopback; + + public AtomicLong lastOperationTimeStamp = new AtomicLong(0); + + protected AtomicLong scheduleWritenBytes = new AtomicLong(0); + + protected final Dispatcher dispatchMessageDispatcher; + protected boolean useBlockingWrite = false; + protected boolean useBlockingRead = true; + protected boolean handleReadWriteConcurrently = true; + + public abstract void decode(); + + public void updateTimeStamp() { + lastOperationTimeStamp.set(System.currentTimeMillis()); + } + + public long getLastOperationTimeStamp() { + return lastOperationTimeStamp.get(); + } + + public final boolean isHandleReadWriteConcurrently() { + return handleReadWriteConcurrently; + } + + public final void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + this.handleReadWriteConcurrently = handleReadWriteConcurrently; + } + + public long getScheduleWritenBytes() { + return scheduleWritenBytes.get(); + } + + public CodecFactory.Encoder getEncoder() { + return encoder; + } + + public void setEncoder(CodecFactory.Encoder encoder) { + this.encoder = encoder; + } + + public CodecFactory.Decoder getDecoder() { + return decoder; + } + + public IoBuffer getReadBuffer() { + return readBuffer; + } + + public void setReadBuffer(IoBuffer readBuffer) { + this.readBuffer = readBuffer; + } + + public void setDecoder(CodecFactory.Decoder decoder) { + this.decoder = decoder; + } + + public final ByteOrder getReadBufferByteOrder() { + if (readBuffer == null) { + throw new IllegalStateException(); + } + return readBuffer.order(); + } + + public final void setReadBufferByteOrder(ByteOrder readBufferByteOrder) { + if (readBuffer == null) { + throw new NullPointerException("Null ReadBuffer"); + } + readBuffer.order(readBufferByteOrder); + } + + // synchronized,prevent reactors invoking this method concurrently. + protected synchronized void onIdle() { + try { + // check twice + if (isIdle()) { + updateTimeStamp(); + handler.onSessionIdle(this); + } + } catch (Throwable e) { + onException(e); + } + } + + protected void onConnected() { + try { + handler.onSessionConnected(this); + } catch (Throwable e) { + onException(e); + } + } + + public void onExpired() { + try { + if (isExpired()) { + handler.onSessionExpired(this); + } + } catch (Throwable e) { + onException(e); + } + } + + protected abstract WriteMessage wrapMessage(Object msg, Future writeFuture); + + /** + * Pre-Process WriteMessage before writing to channel + * + * @param writeMessage + * @return + */ + protected WriteMessage preprocessWriteMessage(WriteMessage writeMessage) { + return writeMessage; + } + + protected void dispatchReceivedMessage(final Object message) { + if (dispatchMessageDispatcher == null) { + long start = -1; + if (statistics != null && statistics.isStatistics()) { + start = System.currentTimeMillis(); + } + onMessage(message, this); + if (start != -1) { + statistics.statisticsProcess(System.currentTimeMillis() - start); + } + } else { + + dispatchMessageDispatcher.dispatch(new Runnable() { + public void run() { + long start = -1; + if (statistics != null && statistics.isStatistics()) { + start = System.currentTimeMillis(); + } + onMessage(message, AbstractSession.this); + if (start != -1) { + statistics.statisticsProcess(System.currentTimeMillis() - start); + } + } + + }); + } + + } + + private void onMessage(final Object message, Session session) { + try { + handler.onMessageReceived(session, message); + } catch (Throwable e) { + onException(e); + } + } + + public final boolean isClosed() { + return closed; + } + + public final void setClosed(boolean closed) { + this.closed = closed; + } + + public void close() { + synchronized (this) { + if (isClosed()) { + return; + } + setClosed(true); + } + try { + closeChannel(); + clearAttributes(); + log.debug("session closed"); + } catch (IOException e) { + onException(e); + log.error("Close session error", e); + } finally { + onClosed(); + } + } + + protected abstract void closeChannel() throws IOException; + + public void onException(Throwable e) { + handler.onExceptionCaught(this, e); + } + + protected void onClosed() { + try { + handler.onSessionClosed(this); + } catch (Throwable e) { + onException(e); + } + } + + public void setAttribute(String key, Object value) { + attributes.put(key, value); + } + + public Object setAttributeIfAbsent(String key, Object value) { + return attributes.putIfAbsent(key, value); + } + + public void removeAttribute(String key) { + attributes.remove(key); + } + + public Object getAttribute(String key) { + return attributes.get(key); + } + + public void clearAttributes() { + attributes.clear(); + } + + public synchronized void start() { + log.debug("session started"); + onStarted(); + start0(); + } + + protected abstract void start0(); + + protected void onStarted() { + try { + handler.onSessionStarted(this); + } catch (Throwable e) { + onException(e); + } + } + + protected ReentrantLock writeLock = new ReentrantLock(); + + protected AtomicReference currentMessage = + new LinkedTransferQueue.PaddedAtomicReference(null); + + static final class FailFuture implements Future { + + public boolean cancel(boolean mayInterruptIfRunning) { + return Boolean.FALSE; + } + + public Boolean get() throws InterruptedException, ExecutionException { + return Boolean.FALSE; + } + + public Boolean get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return Boolean.FALSE; + } + + public boolean isCancelled() { + return false; + } + + public boolean isDone() { + return true; + } + + } + + public void write(Object packet) { + if (closed) { + return; + } + WriteMessage message = wrapMessage(packet, null); + scheduleWritenBytes.addAndGet(message.getWriteBuffer().remaining()); + writeFromUserCode(message); + } + + public abstract void writeFromUserCode(WriteMessage message); + + public final boolean isLoopbackConnection() { + return loopback; + } + + public boolean isUseBlockingWrite() { + return useBlockingWrite; + } + + public void setUseBlockingWrite(boolean useBlockingWrite) { + this.useBlockingWrite = useBlockingWrite; + } + + public boolean isUseBlockingRead() { + return useBlockingRead; + } + + public void setUseBlockingRead(boolean useBlockingRead) { + this.useBlockingRead = useBlockingRead; + } + + public void clearWriteQueue() { + writeQueue.clear(); + } + + public boolean isExpired() { + return false; + } + + public boolean isIdle() { + long lastOpTimestamp = getLastOperationTimeStamp(); + return lastOpTimestamp > 0 && System.currentTimeMillis() - lastOpTimestamp > sessionIdleTimeout; + } + + public AbstractSession(SessionConfig sessionConfig) { + super(); + lastOperationTimeStamp.set(System.currentTimeMillis()); + statistics = sessionConfig.statistics; + handler = sessionConfig.handler; + writeQueue = sessionConfig.queue; + encoder = sessionConfig.codecFactory.getEncoder(); + decoder = sessionConfig.codecFactory.getDecoder(); + dispatchMessageDispatcher = sessionConfig.dispatchMessageDispatcher; + handleReadWriteConcurrently = sessionConfig.handleReadWriteConcurrently; + sessionTimeout = sessionConfig.sessionTimeout; + sessionIdleTimeout = sessionConfig.sessionIdelTimeout; + } + + public long transferTo(long position, long count, FileChannel target) throws IOException { + throw new UnsupportedOperationException(); + } + + public long transferFrom(long position, long count, FileChannel source) throws IOException { + throw new UnsupportedOperationException(); + } + + protected void onCreated() { + try { + handler.onSessionCreated(this); + } catch (Throwable e) { + onException(e); + } + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/ByteBufferCodecFactory.java b/src/main/java/com/google/code/yanf4j/core/impl/ByteBufferCodecFactory.java new file mode 100644 index 0000000..4be66a7 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/ByteBufferCodecFactory.java @@ -0,0 +1,96 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Session; + +/** + * Default codec factory + * + * @author dennis + * + */ +public class ByteBufferCodecFactory implements CodecFactory { + static final IoBuffer EMPTY_BUFFER = IoBuffer.allocate(0); + + private boolean direct; + + public ByteBufferCodecFactory() { + this(false); + } + + public ByteBufferCodecFactory(boolean direct) { + super(); + this.direct = direct; + this.encoder = new ByteBufferEncoder(); + this.decoder = new ByteBufferDecoder(); + } + + public class ByteBufferDecoder implements Decoder { + + public Object decode(IoBuffer buff, Session session) { + if (buff == null) { + return null; + } + if (buff.remaining() == 0) { + return EMPTY_BUFFER; + } + byte[] bytes = new byte[buff.remaining()]; + buff.get(bytes); + IoBuffer result = IoBuffer.allocate(bytes.length, ByteBufferCodecFactory.this.direct); + result.put(bytes); + result.flip(); + return result; + } + + } + + private Decoder decoder; + + public Decoder getDecoder() { + return this.decoder; + } + + public class ByteBufferEncoder implements Encoder { + + public IoBuffer encode(Object message, Session session) { + final IoBuffer msgBuffer = (IoBuffer) message; + if (msgBuffer == null) { + return null; + } + if (msgBuffer.remaining() == 0) { + return EMPTY_BUFFER; + } + byte[] bytes = new byte[msgBuffer.remaining()]; + msgBuffer.get(bytes); + IoBuffer result = IoBuffer.allocate(bytes.length, ByteBufferCodecFactory.this.direct); + result.put(bytes); + result.flip(); + return result; + } + + } + + private Encoder encoder; + + public Encoder getEncoder() { + return this.encoder; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/FutureImpl.java b/src/main/java/com/google/code/yanf4j/core/impl/FutureImpl.java new file mode 100644 index 0000000..59e44fe --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/FutureImpl.java @@ -0,0 +1,170 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Simple {@link Future} implementation, which uses synchronization {@link Object} to synchronize + * during the lifecycle. + * + * @see Future + * + * @author Alexey Stashok + */ +public class FutureImpl implements Future { + + private final Object sync; + + private boolean isDone; + + private boolean isCancelled; + private Throwable failure; + + protected R result; + + public FutureImpl() { + this(new Object()); + } + + public FutureImpl(Object sync) { + this.sync = sync; + } + + /** + * Get current result value without any blocking. + * + * @return current result value without any blocking. + */ + public R getResult() { + synchronized (this.sync) { + return this.result; + } + } + + /** + * Set the result value and notify about operation completion. + * + * @param result the result value + */ + public void setResult(R result) { + synchronized (this.sync) { + this.result = result; + notifyHaveResult(); + } + } + + /** + * {@inheritDoc} + */ + public boolean cancel(boolean mayInterruptIfRunning) { + synchronized (this.sync) { + this.isCancelled = true; + notifyHaveResult(); + return true; + } + } + + /** + * {@inheritDoc} + */ + public boolean isCancelled() { + synchronized (this.sync) { + return this.isCancelled; + } + } + + /** + * {@inheritDoc} + */ + public boolean isDone() { + synchronized (this.sync) { + return this.isDone; + } + } + + /** + * {@inheritDoc} + */ + public R get() throws InterruptedException, ExecutionException { + synchronized (this.sync) { + for (;;) { + if (this.isDone) { + if (this.isCancelled) { + throw new CancellationException(); + } else if (this.failure != null) { + throw new ExecutionException(this.failure); + } else if (this.result != null) { + return this.result; + } + } + + this.sync.wait(); + } + } + } + + /** + * {@inheritDoc} + */ + public R get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + long startTime = System.currentTimeMillis(); + long timeoutMillis = TimeUnit.MILLISECONDS.convert(timeout, unit); + synchronized (this.sync) { + for (;;) { + if (this.isDone) { + if (this.isCancelled) { + throw new CancellationException(); + } else if (this.failure != null) { + throw new ExecutionException(this.failure); + } else if (this.result != null) { + return this.result; + } + } else if (System.currentTimeMillis() - startTime > timeoutMillis) { + throw new TimeoutException(); + } + + this.sync.wait(timeoutMillis); + } + } + } + + /** + * Notify about the failure, occured during asynchronous operation execution. + * + * @param failure + */ + public void failure(Throwable failure) { + synchronized (this.sync) { + this.failure = failure; + notifyHaveResult(); + } + } + + /** + * Notify blocked listeners threads about operation completion. + */ + protected void notifyHaveResult() { + this.isDone = true; + this.sync.notifyAll(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/FutureLockImpl.java b/src/main/java/com/google/code/yanf4j/core/impl/FutureLockImpl.java new file mode 100644 index 0000000..94da40f --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/FutureLockImpl.java @@ -0,0 +1,206 @@ +/* + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2007-2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU General Public License + * Version 2 only ("GPL") or the Common Development and Distribution License("CDDL") (collectively, + * the "License"). You may not use this file except in compliance with the License. You can obtain a + * copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html or + * glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific language governing + * permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each file and include the + * License file at glassfish/bootstrap/legal/LICENSE.txt. Sun designates this particular file as + * subject to the "Classpath" exception as provided by Sun in the GPL Version 2 section of the + * License file that accompanied this code. If applicable, add the following below the License + * Header, with the fields enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * If you wish your version of this file to be governed by only the CDDL or only the GPL Version 2, + * indicate your decision by adding "[Contributor] elects to include this software in this + * distribution under the [CDDL or GPL Version 2] license." If you don't indicate a single choice of + * license, a recipient has the option to distribute your version of this file under either the + * CDDL, the GPL Version 2 or to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL Version 2 license, then the + * option applies only if the new code is made subject to such option by the copyright holder. + */ + +package com.google.code.yanf4j.core.impl; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Simple {@link Future} implementation, which uses {@link ReentrantLock} to synchronize during the + * lifecycle. + * + * @see Future + * @see ReentrantLock + * + * @author Alexey Stashok + */ +public class FutureLockImpl implements Future { + + private final ReentrantLock lock; + + private boolean isDone; + + private CountDownLatch latch; + + private boolean isCancelled; + private Throwable failure; + + protected R result; + + public FutureLockImpl() { + this(new ReentrantLock()); + } + + public FutureLockImpl(ReentrantLock lock) { + this.lock = lock; + latch = new CountDownLatch(1); + } + + /** + * Get current result value without any blocking. + * + * @return current result value without any blocking. + */ + public R getResult() { + try { + lock.lock(); + return result; + } finally { + lock.unlock(); + } + } + + /** + * Set the result value and notify about operation completion. + * + * @param result the result value + */ + public void setResult(R result) { + try { + lock.lock(); + this.result = result; + notifyHaveResult(); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public boolean cancel(boolean mayInterruptIfRunning) { + try { + lock.lock(); + isCancelled = true; + notifyHaveResult(); + return true; + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public boolean isCancelled() { + try { + lock.lock(); + return isCancelled; + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public boolean isDone() { + try { + lock.lock(); + return isDone; + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public R get() throws InterruptedException, ExecutionException { + latch.await(); + + try { + lock.lock(); + if (isCancelled) { + throw new CancellationException(); + } else if (failure != null) { + throw new ExecutionException(failure); + } + + return result; + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public R get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + boolean isTimeOut = !latch.await(timeout, unit); + try { + lock.lock(); + if (!isTimeOut) { + if (isCancelled) { + throw new CancellationException(); + } else if (failure != null) { + throw new ExecutionException(failure); + } + + return result; + } else { + throw new TimeoutException(); + } + } finally { + lock.unlock(); + } + } + + /** + * Notify about the failure, occured during asynchronous operation execution. + * + * @param failure + */ + public void failure(Throwable failure) { + try { + lock.lock(); + this.failure = failure; + notifyHaveResult(); + } finally { + lock.unlock(); + } + } + + /** + * Notify blocked listeners threads about operation completion. + */ + protected void notifyHaveResult() { + isDone = true; + latch.countDown(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/HandlerAdapter.java b/src/main/java/com/google/code/yanf4j/core/impl/HandlerAdapter.java new file mode 100644 index 0000000..0d575e2 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/HandlerAdapter.java @@ -0,0 +1,71 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; + +/** + * IO Handler adapter + * + * + * + * @author boyan + * + */ +public class HandlerAdapter implements Handler { + private static final Logger log = LoggerFactory.getLogger(HandlerAdapter.class); + + public void onExceptionCaught(Session session, Throwable throwable) { + + } + + public void onMessageSent(Session session, Object message) { + + } + + public void onSessionConnected(Session session) { + + } + + public void onSessionStarted(Session session) { + + } + + public void onSessionCreated(Session session) { + + } + + public void onSessionClosed(Session session) { + + } + + public void onMessageReceived(Session session, Object message) { + + } + + public void onSessionIdle(Session session) { + + } + + public void onSessionExpired(Session session) { + log.warn("Session(" + session.getRemoteSocketAddress() + ") is expired."); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/PoolDispatcher.java b/src/main/java/com/google/code/yanf4j/core/impl/PoolDispatcher.java new file mode 100644 index 0000000..14998bb --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/PoolDispatcher.java @@ -0,0 +1,74 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import com.google.code.yanf4j.core.Dispatcher; +import com.google.code.yanf4j.util.WorkerThreadFactory; + +/** + * + * + * + * Pool dispatcher,wrap a threadpool. + * + * @author dennis + * + */ +public class PoolDispatcher implements Dispatcher { + public static final int DEFAULT_POOL_QUEUE_SIZE_FACTOR = 1000; + public static final float DEFAULT_MAX_POOL_SIZE_FACTOR = 1.25f; + private ThreadPoolExecutor threadPool; + + public PoolDispatcher(int poolSize) { + this(poolSize, 60, TimeUnit.SECONDS, new ThreadPoolExecutor.AbortPolicy(), "pool-dispatcher"); + } + + public PoolDispatcher(int poolSize, long keepAliveTime, TimeUnit unit, + RejectedExecutionHandler rejectedExecutionHandler, String prefix) { + this(poolSize, DEFAULT_POOL_QUEUE_SIZE_FACTOR, DEFAULT_MAX_POOL_SIZE_FACTOR, keepAliveTime, + unit, rejectedExecutionHandler, prefix); + } + + public PoolDispatcher(int poolSize, int poolQueueSizeFactor, float maxPoolSizeFactor, + long keepAliveTime, TimeUnit unit, RejectedExecutionHandler rejectedExecutionHandler, + String prefix) { + this.threadPool = new ThreadPoolExecutor(poolSize, (int) (maxPoolSizeFactor * poolSize), + keepAliveTime, unit, new ArrayBlockingQueue(poolSize * poolQueueSizeFactor), + new WorkerThreadFactory(prefix)); + this.threadPool.setRejectedExecutionHandler(rejectedExecutionHandler); + } + + public final void dispatch(Runnable r) { + if (!this.threadPool.isShutdown()) { + this.threadPool.execute(r); + } + } + + public void stop() { + this.threadPool.shutdown(); + try { + this.threadPool.awaitTermination(1000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/StandardSocketOption.java b/src/main/java/com/google/code/yanf4j/core/impl/StandardSocketOption.java new file mode 100644 index 0000000..9ae4868 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/StandardSocketOption.java @@ -0,0 +1,200 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import java.net.ServerSocket; +import java.net.Socket; +import com.google.code.yanf4j.core.SocketOption; + +/** + * Standard socket options + * + * @author dennis + * + */ +public class StandardSocketOption { + + /** + * Keep connection alive. + * + *

+ * The value of this socket option is a {@code Boolean} that represents whether the option is + * enabled or disabled. When the {@code SO_KEEPALIVE} option is enabled the operating system may + * use a keep-alive mechanism to periodically probe the other end of a connection when + * the connection is otherwise idle. The exact semantics of the keep alive mechanism is system + * dependent and therefore unspecified. + * + *

+ * The initial value of this socket option is {@code FALSE}. The socket option may be enabled or + * disabled at any time. + * + * @see RFC 1122 * Requirements for Internet + * Hosts -- Communication Layers< /a> + * @see Socket#setKeepAlive + */ + public static final SocketOption SO_KEEPALIVE = + new SocketOption("SO_KEEPALIVE", Boolean.class); + /** + * The size of the socket send buffer. + * + *

+ * The value of this socket option is an {@code Integer} that is the size of the socket send + * buffer in bytes. The socket send buffer is an output buffer used by the networking + * implementation. It may need to be increased for high-volume connections. The value of the + * socket option is a hint to the implementation to size the buffer and the actual size + * may differ. The socket option can be queried to retrieve the actual size. + * + *

+ * For datagram-oriented sockets, the size of the send buffer may limit the size of the datagrams + * that may be sent by the socket. Whether datagrams larger than the buffer size are sent or + * discarded is system dependent. + * + *

+ * The initial/default size of the socket send buffer and the range of allowable values is system + * dependent although a negative size is not allowed. An attempt to set the socket send buffer to + * larger than its maximum size causes it to be set to its maximum size. + * + *

+ * An implementation allows this socket option to be set before the socket is bound or connected. + * Whether an implementation allows the socket send buffer to be changed after the socket is bound + * is system dependent. + * + * @see Socket#setSendBufferSize + */ + public static final SocketOption SO_SNDBUF = + new SocketOption("SO_SNDBUF", Integer.class); + /** + * The size of the socket receive buffer. + * + *

+ * The value of this socket option is an {@code Integer} that is the size of the socket receive + * buffer in bytes. The socket receive buffer is an input buffer used by the networking + * implementation. It may need to be increased for high-volume connections or decreased to limit + * the possible backlog of incoming data. The value of the socket option is a hint to the + * implementation to size the buffer and the actual size may differ. + * + *

+ * For datagram-oriented sockets, the size of the receive buffer may limit the size of the + * datagrams that can be received. Whether datagrams larger than the buffer size can be received + * is system dependent. Increasing the socket receive buffer may be important for cases where + * datagrams arrive in bursts faster than they can be processed. + * + *

+ * In the case of stream-oriented sockets and the TCP/IP protocol, the size of the socket receive + * buffer may be used when advertising the size of the TCP receive window to the remote peer. + * + *

+ * The initial/default size of the socket receive buffer and the range of allowable values is + * system dependent although a negative size is not allowed. An attempt to set the socket receive + * buffer to larger than its maximum size causes it to be set to its maximum size. + * + *

+ * An implementation allows this socket option to be set before the socket is bound or connected. + * Whether an implementation allows the socket receive buffer to be changed after the socket is + * bound is system dependent. + * + * @see RFC 1323: TCP * Extensions for High + * Performance< /a> + * @see Socket#setReceiveBufferSize + * @see ServerSocket#setReceiveBufferSize + */ + public static final SocketOption SO_RCVBUF = + new SocketOption("SO_RCVBUF", Integer.class); + /** + * Re-use address. + * + *

+ * The value of this socket option is a {@code Boolean} that represents whether the option is + * enabled or disabled. The exact semantics of this socket option are socket type and system + * dependent. + * + *

+ * In the case of stream-oriented sockets, this socket option will usually determine whether the + * socket can be bound to a socket address when a previous connection involving that socket + * address is in the TIME_WAIT state. On implementations where the semantics differ, and + * the socket option is not required to be enabled in order to bind the socket when a previous + * connection is in this state, then the implementation may choose to ignore this option. + * + *

+ * For datagram-oriented sockets the socket option is used to allow multiple programs bind to the + * same address. This option should be enabled when the socket is to be used for Internet Protocol + * (IP) multicasting. + * + *

+ * An implementation allows this socket option to be set before the socket is bound or connected. + * Changing the value of this socket option after the socket is bound has no effect. The default + * value of this socket option is system dependent. + * + * @see RFC 793: * Transmission Control + * Protocol< /a> + * @see ServerSocket#setReuseAddress + */ + public static final SocketOption SO_REUSEADDR = + new SocketOption("SO_REUSEADDR", Boolean.class); + /** + * Linger on close if data is present. + * + *

+ * The value of this socket option is an {@code Integer} that controls the action taken when + * unsent data is queued on the socket and a method to close the socket is invoked. If the value + * of the socket option is zero or greater, then it represents a timeout value, in seconds, known + * as the linger interval. The linger interval is the timeout for the {@code close} + * method to block while the operating system attempts to transmit the unsent data or it decides + * that it is unable to transmit the data. If the value of the socket option is less than zero + * then the option is disabled. In that case the {@code close} method does not wait until unsent + * data is transmitted; if possible the operating system will transmit any unsent data before the + * connection is closed. + * + *

+ * This socket option is intended for use with sockets that are configured in + * {@link java.nio.channels.SelectableChannel#isBlocking() blocking} mode only. The behavior of + * the {@code close} method when this option is enabled on a non-blocking socket is not defined. + * + *

+ * The initial value of this socket option is a negative value, meaning that the option is + * disabled. The option may be enabled, or the linger interval changed, at any time. The maximum + * value of the linger interval is system dependent. Setting the linger interval to a value that + * is greater than its maximum value causes the linger interval to be set to its maximum value. + * + * @see Socket#setSoLinger + */ + public static final SocketOption SO_LINGER = + new SocketOption("SO_LINGER", Integer.class); + /** + * Disable the Nagle algorithm. + * + *

+ * The value of this socket option is a {@code Boolean} that represents whether the option is + * enabled or disabled. The socket option is specific to stream-oriented sockets using the TCP/IP + * protocol. TCP/IP uses an algorithm known as The Nagle Algorithm to coalesce short + * segments and improve network efficiency. + * + *

+ * The default value of this socket option is {@code FALSE}. The socket option should only be + * enabled in cases where it is known that the coalescing impacts performance. The socket option + * may be enabled at any time. In other words, the Nagle Algorithm can be disabled. Once the + * option is enabled, it is system dependent whether it can be subsequently disabled. If it + * cannot, then invoking the {@code setOption} method to disable the option has no effect. + * + * @see RFC 1122: * Requirements for Internet + * Hosts -- Communication Layers< /a> + * @see Socket#setTcpNoDelay + */ + public static final SocketOption TCP_NODELAY = + new SocketOption("TCP_NODELAY", Boolean.class); + +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/TextLineCodecFactory.java b/src/main/java/com/google/code/yanf4j/core/impl/TextLineCodecFactory.java new file mode 100644 index 0000000..9237363 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/TextLineCodecFactory.java @@ -0,0 +1,117 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.util.ByteBufferMatcher; +import com.google.code.yanf4j.util.ShiftAndByteBufferMatcher; + +/** + * Text line codec factory + * + * @author dennis + * + */ +public class TextLineCodecFactory implements CodecFactory { + + public static final IoBuffer SPLIT = IoBuffer.wrap("\r\n".getBytes()); + + private static final ByteBufferMatcher SPLIT_PATTERN = new ShiftAndByteBufferMatcher(SPLIT); + + public static final String DEFAULT_CHARSET_NAME = "utf-8"; + + private Charset charset; + + public TextLineCodecFactory() { + this.charset = Charset.forName(DEFAULT_CHARSET_NAME); + } + + public TextLineCodecFactory(String charsetName) { + this.charset = Charset.forName(charsetName); + } + + class StringDecoder implements Decoder { + public Object decode(IoBuffer buffer, Session session) { + String result = null; + int index = SPLIT_PATTERN.matchFirst(buffer); + if (index >= 0) { + int limit = buffer.limit(); + buffer.limit(index); + CharBuffer charBuffer = TextLineCodecFactory.this.charset.decode(buffer.buf()); + result = charBuffer.toString(); + buffer.limit(limit); + buffer.position(index + SPLIT.remaining()); + + } + return result; + } + } + + private Decoder decoder = new StringDecoder(); + + public Decoder getDecoder() { + return this.decoder; + + } + + class StringEncoder implements Encoder { + public IoBuffer encode(Object msg, Session session) { + if (msg == null) { + return null; + } + String message = (String) msg; + ByteBuffer buff = TextLineCodecFactory.this.charset.encode(message); + byte[] bs = new byte[buff.remaining() + SPLIT.remaining()]; + int len = buff.remaining(); + System.arraycopy(buff.array(), buff.position(), bs, 0, len); + bs[len] = 13; // \r + bs[len + 1] = 10; // \n + IoBuffer resultBuffer = IoBuffer.wrap(bs); + + return resultBuffer; + } + } + + private Encoder encoder = new StringEncoder(); + + public Encoder getEncoder() { + return this.encoder; + } + + // public static void main(String args[]) { + // TextLineCodecFactory codecFactory = new TextLineCodecFactory(); + // Encoder encoder = codecFactory.getEncoder(); + // long sum = 0; + // for (int i = 0; i < 100000; i++) { + // sum += encoder.encode("hello", null).remaining(); + // } + // + // long start = System.currentTimeMillis(); + // + // for (int i = 0; i < 10000000; i++) { + // sum += encoder.encode("hello", null).remaining(); + // } + // long cost = System.currentTimeMillis() - start; + // System.out.println("sum=" + sum + ",cost = " + cost + " ms."); + // } + +} diff --git a/src/main/java/com/google/code/yanf4j/core/impl/WriteMessageImpl.java b/src/main/java/com/google/code/yanf4j/core/impl/WriteMessageImpl.java new file mode 100644 index 0000000..4a5e01a --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/impl/WriteMessageImpl.java @@ -0,0 +1,77 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.core.impl; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.WriteMessage; + +/** + * Write message implementation with a buffer + * + * @author dennis + * + */ +public class WriteMessageImpl implements WriteMessage { + + protected Object message; + + protected IoBuffer buffer; + + protected FutureImpl writeFuture; + + protected boolean writing; + + public final void writing() { + this.writing = true; + } + + public final boolean isWriting() { + return this.writing; + } + + public WriteMessageImpl(Object message, FutureImpl writeFuture) { + this.message = message; + this.writeFuture = writeFuture; + } + + /* + * (non-Javadoc) + * + * @see com.google.code.yanf4j.nio.IWriteMessage#getBuffers() + */ + public synchronized final IoBuffer getWriteBuffer() { + return this.buffer; + } + + public synchronized final void setWriteBuffer(IoBuffer buffers) { + this.buffer = buffers; + + } + + public final FutureImpl getWriteFuture() { + return this.writeFuture; + } + + /* + * (non-Javadoc) + * + * @see com.google.code.yanf4j.nio.IWriteMessage#getMessage() + */ + public final Object getMessage() { + return this.message; + } +} diff --git a/src/main/java/com/google/code/yanf4j/core/package.html b/src/main/java/com/google/code/yanf4j/core/package.html new file mode 100644 index 0000000..ea258c4 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/core/package.html @@ -0,0 +1,10 @@ + + + + + Networking core package + + +

Networking core package

+ + \ No newline at end of file diff --git a/src/main/java/com/google/code/yanf4j/nio/NioSession.java b/src/main/java/com/google/code/yanf4j/nio/NioSession.java new file mode 100644 index 0000000..4799375 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/NioSession.java @@ -0,0 +1,52 @@ +/** + * Copyright [2009-2010] [dennis zhuang] 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.google.code.yanf4j.nio; + +import java.nio.channels.SelectableChannel; +import java.nio.channels.Selector; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.Session; + +/** + * Nio connection + * + * @author dennis + * + */ +public interface NioSession extends Session { + /** + * When io event occured + * + * @param event + * @param selector + */ + public void onEvent(EventType event, Selector selector); + + /** + * Enable read event + * + * @param selector + */ + public void enableRead(Selector selector); + + /** + * Enable write event + * + * @param selector + */ + public void enableWrite(Selector selector); + + /** + * return the channel for this connection + * + * @return + */ + + public SelectableChannel channel(); +} diff --git a/src/main/java/com/google/code/yanf4j/nio/NioSessionConfig.java b/src/main/java/com/google/code/yanf4j/nio/NioSessionConfig.java new file mode 100644 index 0000000..ccaa81d --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/NioSessionConfig.java @@ -0,0 +1,50 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.nio; + +import java.nio.channels.SelectableChannel; +import java.util.Queue; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Dispatcher; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.SessionConfig; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.nio.impl.SelectorManager; +import com.google.code.yanf4j.statistics.Statistics; + +/** + * Nio session configuration + * + * @author dennis + * + */ +public class NioSessionConfig extends SessionConfig { + + public final SelectableChannel selectableChannel; + public final SelectorManager selectorManager; + + public NioSessionConfig(SelectableChannel sc, Handler handler, SelectorManager reactor, + CodecFactory codecFactory, Statistics statistics, Queue queue, + Dispatcher dispatchMessageDispatcher, boolean handleReadWriteConcurrently, + long sessionTimeout, long sessionIdleTimeout) { + super(handler, codecFactory, statistics, queue, dispatchMessageDispatcher, + handleReadWriteConcurrently, sessionTimeout, sessionIdleTimeout); + this.selectableChannel = sc; + this.selectorManager = reactor; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/nio/SelectionKeyHandler.java b/src/main/java/com/google/code/yanf4j/nio/SelectionKeyHandler.java new file mode 100644 index 0000000..ef62bff --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/SelectionKeyHandler.java @@ -0,0 +1,33 @@ +package com.google.code.yanf4j.nio; + +/** + * Copyright [2008-2009] [dennis zhuang] 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 + */ +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; + +/** + * SelectionKey handler + * + * @author dennis + * + */ +public interface SelectionKeyHandler { + public void onAccept(SelectionKey sk) throws IOException; + + public void closeSelectionKey(SelectionKey key); + + public void onWrite(SelectionKey key); + + public void onRead(SelectionKey key); + + public void onConnect(SelectionKey key) throws IOException; + + public void closeChannel(Selector selector) throws IOException; +} diff --git a/src/main/java/com/google/code/yanf4j/nio/TCPController.java b/src/main/java/com/google/code/yanf4j/nio/TCPController.java new file mode 100644 index 0000000..38ad8fc --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/TCPController.java @@ -0,0 +1,165 @@ +/** + * Copyright [2008-2009] [dennis zhuang] 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.google.code.yanf4j.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.StandardSocketOption; +import com.google.code.yanf4j.nio.impl.SocketChannelController; + +/** + * Controller for tcp server + * + * @author dennis + */ +public class TCPController extends SocketChannelController { + + private ServerSocketChannel serverSocketChannel; + + /** + * Accept backlog queue size + */ + private int backlog = 500; // default 500 + + public int getBacklog() { + return this.backlog; + } + + public void setBacklog(int backlog) { + if (isStarted()) { + throw new IllegalStateException(); + } + if (backlog < 0) { + throw new IllegalArgumentException("backlog<0"); + } + this.backlog = backlog; + } + + public TCPController() { + super(); + } + + public TCPController(Configuration configuration) { + super(configuration, null, null); + + } + + public TCPController(Configuration configuration, CodecFactory codecFactory) { + super(configuration, null, codecFactory); + } + + public TCPController(Configuration configuration, Handler handler, CodecFactory codecFactory) { + super(configuration, handler, codecFactory); + } + + private int connectionTime, latency, bandwidth; + + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + this.connectionTime = connectionTime; + this.latency = latency; + this.bandwidth = bandwidth; + } + + @Override + protected void doStart() throws IOException { + this.serverSocketChannel = ServerSocketChannel.open(); + this.serverSocketChannel.socket().setSoTimeout(this.soTimeout); + if (this.connectionTime != 0 || this.latency != 0 || this.bandwidth != 0) { + this.serverSocketChannel.socket().setPerformancePreferences(this.connectionTime, this.latency, + this.bandwidth); + } + this.serverSocketChannel.configureBlocking(false); + + if (this.socketOptions.get(StandardSocketOption.SO_REUSEADDR) != null) { + this.serverSocketChannel.socket().setReuseAddress(StandardSocketOption.SO_REUSEADDR.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_REUSEADDR))); + } + if (this.socketOptions.get(StandardSocketOption.SO_RCVBUF) != null) { + this.serverSocketChannel.socket().setReceiveBufferSize(StandardSocketOption.SO_RCVBUF.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_RCVBUF))); + + } + if (this.localSocketAddress != null) { + this.serverSocketChannel.socket().bind(this.localSocketAddress, this.backlog); + } else { + this.serverSocketChannel.socket().bind(new InetSocketAddress("localhost", 0), this.backlog); + } + setLocalSocketAddress( + (InetSocketAddress) this.serverSocketChannel.socket().getLocalSocketAddress()); + this.selectorManager.registerChannel(this.serverSocketChannel, SelectionKey.OP_ACCEPT, null); + } + + @Override + public void onAccept(SelectionKey selectionKey) throws IOException { + // �������رգ�ȡ������ + if (!this.serverSocketChannel.isOpen()) { + selectionKey.cancel(); + return; + } + SocketChannel sc = null; + try { + sc = this.serverSocketChannel.accept(); + if (sc != null) { + configureSocketChannel(sc); + Session session = buildSession(sc); + // enable read + this.selectorManager.registerSession(session, EventType.ENABLE_READ); + session.start(); + super.onAccept(selectionKey); // for statistics + } else { + log.debug("Accept fail"); + } + } catch (IOException e) { + closeAcceptChannel(selectionKey, sc); + log.error("Accept connection error", e); + notifyException(e); + } + } + + /** + * + * @param sk + * @param sc + * @throws IOException + * @throws SocketException + */ + private void closeAcceptChannel(SelectionKey sk, SocketChannel sc) + throws IOException, SocketException { + if (sk != null) { + sk.cancel(); + } + if (sc != null) { + sc.socket().setSoLinger(true, 0); // await TIME_WAIT status + sc.socket().shutdownOutput(); + sc.close(); + } + } + + public void closeChannel(Selector selector) throws IOException { + if (this.serverSocketChannel != null) { + this.serverSocketChannel.close(); + } + } + + public void unbind() throws IOException { + stop(); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/nio/impl/AbstractNioSession.java b/src/main/java/com/google/code/yanf4j/nio/impl/AbstractNioSession.java new file mode 100644 index 0000000..d2488ce --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/impl/AbstractNioSession.java @@ -0,0 +1,343 @@ +package com.google.code.yanf4j.nio.impl; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.core.impl.AbstractSession; +import com.google.code.yanf4j.nio.NioSession; +import com.google.code.yanf4j.nio.NioSessionConfig; +import com.google.code.yanf4j.util.SelectorFactory; + +/** + * Abstract nio session + * + * @author dennis + * + */ +public abstract class AbstractNioSession extends AbstractSession implements NioSession { + + public SelectableChannel channel() { + return selectableChannel; + } + + protected SelectorManager selectorManager; + protected SelectableChannel selectableChannel; + + public AbstractNioSession(NioSessionConfig sessionConfig) { + super(sessionConfig); + selectorManager = sessionConfig.selectorManager; + selectableChannel = sessionConfig.selectableChannel; + } + + public final void enableRead(Selector selector) { + SelectionKey key = selectableChannel.keyFor(selector); + if (key != null && key.isValid()) { + interestRead(key); + } else { + try { + selectableChannel.register(selector, SelectionKey.OP_READ, this); + } catch (ClosedChannelException e) { + // ignore + } catch (CancelledKeyException e) { + // ignore + } + } + } + + private void interestRead(SelectionKey key) { + if (key.attachment() == null) { + key.attach(this); + } + key.interestOps(key.interestOps() | SelectionKey.OP_READ); + } + + @Override + protected void start0() { + registerSession(); + } + + public InetAddress getLocalAddress() { + return ((SocketChannel) selectableChannel).socket().getLocalAddress(); + } + + protected abstract Object writeToChannel(WriteMessage msg) + throws ClosedChannelException, IOException; + + protected void onWrite(SelectionKey key) { + boolean isLockedByMe = false; + if (currentMessage.get() == null) { + // get next message + WriteMessage nextMessage = writeQueue.peek(); + if (nextMessage != null && writeLock.tryLock()) { + if (!writeQueue.isEmpty() && currentMessage.compareAndSet(null, nextMessage)) { + writeQueue.remove(); + } + } else { + return; + } + } else if (!writeLock.tryLock()) { + return; + } + + isLockedByMe = true; + WriteMessage currentMessage = null; + + final long maxWritten = readBuffer.capacity() + readBuffer.capacity() >>> 1; + try { + long written = 0; + while (this.currentMessage.get() != null) { + currentMessage = this.currentMessage.get(); + currentMessage = preprocessWriteMessage(currentMessage); + this.currentMessage.set(currentMessage); + long before = this.currentMessage.get().getWriteBuffer().remaining(); + Object writeResult = null; + + if (written < maxWritten) { + writeResult = writeToChannel(currentMessage); + written += this.currentMessage.get().getWriteBuffer().remaining() - before; + } else { + // wait for next time to write + } + // write complete + if (writeResult != null) { + this.currentMessage.set(writeQueue.poll()); + handler.onMessageSent(this, currentMessage.getMessage()); + // try to get next message + if (this.currentMessage.get() == null) { + if (isLockedByMe) { + isLockedByMe = false; + writeLock.unlock(); + } + // get next message + WriteMessage nextMessage = writeQueue.peek(); + if (nextMessage != null && writeLock.tryLock()) { + isLockedByMe = true; + if (!writeQueue.isEmpty() && this.currentMessage.compareAndSet(null, nextMessage)) { + writeQueue.remove(); + } + continue; + } else { + break; + } + } + } else { + // does't write complete + if (isLockedByMe) { + isLockedByMe = false; + writeLock.unlock(); + } + // register OP_WRITE event + selectorManager.registerSession(this, EventType.ENABLE_WRITE); + break; + } + } + } catch (IOException e) { + handler.onExceptionCaught(this, e); + if (currentMessage != null && currentMessage.getWriteFuture() != null) { + currentMessage.getWriteFuture().failure(e); + } + if (isLockedByMe) { + isLockedByMe = false; + writeLock.unlock(); + } + close(); + } finally { + if (isLockedByMe) { + writeLock.unlock(); + } + } + } + + public final void enableWrite(Selector selector) { + SelectionKey key = selectableChannel.keyFor(selector); + if (key != null && key.isValid()) { + interestWrite(key); + } else { + try { + selectableChannel.register(selector, SelectionKey.OP_WRITE, this); + } catch (ClosedChannelException e) { + // ignore + } catch (CancelledKeyException e) { + // ignore + } + } + } + + private void interestWrite(SelectionKey key) { + if (key.attachment() == null) { + key.attach(this); + } + key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); + } + + protected void onRead(SelectionKey key) { + readFromBuffer(); + } + + protected abstract void readFromBuffer(); + + @Override + protected void closeChannel() throws IOException { + flush0(); + unregisterSession(); + unregisterChannel(); + } + + protected final void unregisterChannel() throws IOException { + writeLock.lock(); + try { + if (getAttribute(SelectorManager.REACTOR_ATTRIBUTE) != null) { + ((Reactor) getAttribute(SelectorManager.REACTOR_ATTRIBUTE)) + .unregisterChannel(selectableChannel); + } + if (selectableChannel.isOpen()) { + selectableChannel.close(); + } + } finally { + writeLock.unlock(); + } + } + + protected final void registerSession() { + selectorManager.registerSession(this, EventType.REGISTER); + } + + protected void unregisterSession() { + selectorManager.registerSession(this, EventType.UNREGISTER); + } + + @Override + public void writeFromUserCode(WriteMessage message) { + if (schduleWriteMessage(message)) { + return; + } + onWrite(null); + } + + protected boolean schduleWriteMessage(WriteMessage writeMessage) { + boolean offered = writeQueue.offer(writeMessage); + assert offered; + final Reactor reactor = selectorManager.getReactorFromSession(this); + if (Thread.currentThread() != reactor) { + selectorManager.registerSession(this, EventType.ENABLE_WRITE); + return true; + } + return false; + } + + public void flush() { + if (isClosed()) { + return; + } + flush0(); + } + + protected final void flush0() { + SelectionKey tmpKey = null; + Selector writeSelector = null; + int attempts = 0; + try { + while (true) { + if (writeSelector == null) { + writeSelector = SelectorFactory.getSelector(); + if (writeSelector == null) { + return; + } + tmpKey = selectableChannel.register(writeSelector, SelectionKey.OP_WRITE); + } + if (writeSelector.select(1000) == 0) { + attempts++; + if (attempts > 2) { + return; + } + } else { + break; + } + } + onWrite(selectableChannel.keyFor(writeSelector)); + } catch (ClosedChannelException cce) { + onException(cce); + log.error("Flush error", cce); + close(); + } catch (IOException ioe) { + onException(ioe); + log.error("Flush error", ioe); + close(); + } finally { + if (tmpKey != null) { + // Cancel the key. + tmpKey.cancel(); + tmpKey = null; + } + if (writeSelector != null) { + try { + writeSelector.selectNow(); + } catch (IOException e) { + log.error("Temp selector selectNow error", e); + } + // return selector + SelectorFactory.returnSelector(writeSelector); + } + } + } + + protected final long doRealWrite(SelectableChannel channel, IoBuffer buffer) throws IOException { + if (log.isDebugEnabled()) { + StringBuffer bufMsg = new StringBuffer("send buffers:\n[\n"); + final ByteBuffer buff = buffer.buf(); + bufMsg.append(" buffer:position=").append(buff.position()).append(",limit=") + .append(buff.limit()).append(",capacity=").append(buff.capacity()).append("\n"); + + bufMsg.append("]"); + log.debug(bufMsg.toString()); + } + return ((WritableByteChannel) channel).write(buffer.buf()); + } + + /** + * �ɷ�IO�¼� + */ + public final void onEvent(EventType event, Selector selector) { + if (isClosed()) { + return; + } + SelectionKey key = selectableChannel.keyFor(selector); + + switch (event) { + case EXPIRED: + onExpired(); + break; + case WRITEABLE: + onWrite(key); + break; + case READABLE: + onRead(key); + break; + case ENABLE_WRITE: + enableWrite(selector); + break; + case ENABLE_READ: + enableRead(selector); + break; + case IDLE: + onIdle(); + break; + case CONNECTED: + onConnected(); + break; + default: + log.error("Unknown event:" + event.name()); + break; + } + } +} diff --git a/src/main/java/com/google/code/yanf4j/nio/impl/NioController.java b/src/main/java/com/google/code/yanf4j/nio/impl/NioController.java new file mode 100644 index 0000000..8d60c52 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/impl/NioController.java @@ -0,0 +1,226 @@ +/** + * Copyright [2009-2010] [dennis zhuang] 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.google.code.yanf4j.nio.impl; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.util.Queue; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.core.impl.AbstractController; +import com.google.code.yanf4j.nio.NioSessionConfig; +import com.google.code.yanf4j.nio.SelectionKeyHandler; +import com.google.code.yanf4j.util.SystemUtils; + +/** + * Base nio controller + * + * @author dennis + * + */ +public abstract class NioController extends AbstractController implements SelectionKeyHandler { + + protected SelectorManager selectorManager; + + /** + * Reactor count + */ + protected int selectorPoolSize = SystemUtils.getSystemThreadCount(); + + /** + * @see setSelectorPoolSize + * @return + */ + public int getSelectorPoolSize() { + return this.selectorPoolSize; + } + + public void setSelectorPoolSize(int selectorPoolSize) { + if (isStarted()) { + throw new IllegalStateException("Controller has been started"); + } + this.selectorPoolSize = selectorPoolSize; + } + + public NioController() { + super(); + } + + public NioController(Configuration configuration, CodecFactory codecFactory) { + super(configuration, codecFactory); + } + + public NioController(Configuration configuration, Handler handler, CodecFactory codecFactory) { + super(configuration, handler, codecFactory); + } + + public NioController(Configuration configuration) { + super(configuration); + } + + /** + * Write task + * + * @author dennis + * + */ + private final class WriteTask implements Runnable { + private final SelectionKey key; + + private WriteTask(SelectionKey key) { + this.key = key; + } + + public final void run() { + dispatchWriteEvent(this.key); + } + } + + /** + * Read task + * + * @author dennis + * + */ + private final class ReadTask implements Runnable { + private final SelectionKey key; + + private ReadTask(SelectionKey key) { + this.key = key; + } + + public final void run() { + dispatchReadEvent(this.key); + } + } + + public final SelectorManager getSelectorManager() { + return this.selectorManager; + } + + @Override + protected void start0() throws IOException { + try { + initialSelectorManager(); + doStart(); + } catch (IOException e) { + log.error("Start server error", e); + notifyException(e); + stop(); + throw e; + } + + } + + /** + * Start selector manager + * + * @throws IOException + */ + protected void initialSelectorManager() throws IOException { + if (this.selectorManager == null) { + this.selectorManager = new SelectorManager(this.selectorPoolSize, this, this.configuration); + this.selectorManager.start(); + } + } + + /** + * Inner startup + * + * @throws IOException + */ + protected abstract void doStart() throws IOException; + + /** + * Read event occured + */ + public void onRead(SelectionKey key) { + if (this.readEventDispatcher == null) { + dispatchReadEvent(key); + } else { + this.readEventDispatcher.dispatch(new ReadTask(key)); + } + } + + /** + * Writable event occured + */ + public void onWrite(final SelectionKey key) { + if (this.writeEventDispatcher == null) { + dispatchWriteEvent(key); + } else { + this.writeEventDispatcher.dispatch(new WriteTask(key)); + } + } + + /** + * Cancel selection key + */ + public void closeSelectionKey(SelectionKey key) { + if (key.attachment() instanceof Session) { + Session session = (Session) key.attachment(); + if (session != null) { + session.close(); + } + } + } + + /** + * Dispatch read event + * + * @param key + * @return + */ + protected abstract void dispatchReadEvent(final SelectionKey key); + + /** + * Dispatch write event + * + * @param key + * @return + */ + protected abstract void dispatchWriteEvent(final SelectionKey key); + + @Override + protected void stop0() throws IOException { + if (this.selectorManager == null || !this.selectorManager.isStarted()) { + return; + } + this.selectorManager.stop(); + } + + public synchronized void bind(int port) throws IOException { + if (isStarted()) { + throw new IllegalStateException("Server has been bind to " + getLocalSocketAddress()); + } + bind(new InetSocketAddress(port)); + } + + /** + * Build nio session config + * + * @param sc + * @param queue + * @return + */ + protected final NioSessionConfig buildSessionConfig(SelectableChannel sc, + Queue queue) { + final NioSessionConfig sessionConfig = + new NioSessionConfig(sc, getHandler(), this.selectorManager, getCodecFactory(), + getStatistics(), queue, this.dispatchMessageDispatcher, isHandleReadWriteConcurrently(), + this.sessionTimeout, this.configuration.getSessionIdleTimeout()); + return sessionConfig; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/nio/impl/NioTCPSession.java b/src/main/java/com/google/code/yanf4j/nio/impl/NioTCPSession.java new file mode 100644 index 0000000..b47575e --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/impl/NioTCPSession.java @@ -0,0 +1,327 @@ +package com.google.code.yanf4j.nio.impl; + +/** + * Copyright [2008-2009] [dennis zhuang] 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 + */ +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.concurrent.Future; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.core.impl.FutureImpl; +import com.google.code.yanf4j.core.impl.WriteMessageImpl; +import com.google.code.yanf4j.nio.NioSessionConfig; +import com.google.code.yanf4j.util.ByteBufferUtils; +import com.google.code.yanf4j.util.SelectorFactory; + +/** + * Nio tcp connection + * + * @author dennis + * + */ +public class NioTCPSession extends AbstractNioSession { + private InetSocketAddress remoteAddress; + + @Override + public final boolean isExpired() { + if (log.isDebugEnabled()) { + log.debug("sessionTimeout=" + this.sessionTimeout + ",this.timestamp=" + + this.lastOperationTimeStamp.get() + ",current=" + System.currentTimeMillis()); + } + return this.sessionTimeout <= 0 ? false + : System.currentTimeMillis() - this.lastOperationTimeStamp.get() >= this.sessionTimeout; + } + + public NioTCPSession(NioSessionConfig sessionConfig, int readRecvBufferSize) { + super(sessionConfig); + if (this.selectableChannel != null && this.getRemoteSocketAddress() != null) { + this.loopback = this.getRemoteSocketAddress().getAddress().isLoopbackAddress(); + } + this.setReadBuffer(IoBuffer.allocate(readRecvBufferSize)); + this.onCreated(); + } + + @Override + protected Object writeToChannel(WriteMessage message) throws IOException { + if (message.getWriteFuture() != null && !message.isWriting() + && message.getWriteFuture().isCancelled()) { + return message.getMessage(); + } + if (message.getWriteBuffer() == null) { + if (message.getWriteFuture() != null) { + message.getWriteFuture().setResult(Boolean.TRUE); + } + return message.getMessage(); + } + IoBuffer writeBuffer = message.getWriteBuffer(); + // begin writing + message.writing(); + if (this.useBlockingWrite) { + return this.blockingWrite(this.selectableChannel, message, writeBuffer); + } else { + while (true) { + long n = this.doRealWrite(this.selectableChannel, writeBuffer); + if (n > 0) { + this.statistics.statisticsWrite(n); + this.scheduleWritenBytes.addAndGet(0 - n); + } + if (writeBuffer == null || !writeBuffer.hasRemaining()) { + if (message.getWriteFuture() != null) { + message.getWriteFuture().setResult(Boolean.TRUE); + } + return message.getMessage(); + } else if (n == 0) { + // have more data, but the buffer is full, + // wait next time to write + return null; + } + } + } + + } + + public InetSocketAddress getRemoteSocketAddress() { + if (this.remoteAddress == null) { + this.remoteAddress = (InetSocketAddress) ((SocketChannel) this.selectableChannel).socket() + .getRemoteSocketAddress(); + } + return this.remoteAddress; + } + + /** + * Blocking write using temp selector + * + * @param channel + * @param message + * @param writeBuffer + * @return + * @throws IOException + * @throws ClosedChannelException + */ + protected final Object blockingWrite(SelectableChannel channel, WriteMessage message, + IoBuffer writeBuffer) throws IOException, ClosedChannelException { + SelectionKey tmpKey = null; + Selector writeSelector = null; + int attempts = 0; + int bytesProduced = 0; + try { + while (writeBuffer.hasRemaining()) { + long len = this.doRealWrite(channel, writeBuffer); + if (len > 0) { + attempts = 0; + bytesProduced += len; + this.statistics.statisticsWrite(len); + } else { + attempts++; + if (writeSelector == null) { + writeSelector = SelectorFactory.getSelector(); + if (writeSelector == null) { + // Continue using the main one. + continue; + } + tmpKey = channel.register(writeSelector, SelectionKey.OP_WRITE); + } + if (writeSelector.select(1000) == 0) { + if (attempts > 2) { + throw new IOException("Client disconnected"); + } + } + } + } + if (!writeBuffer.hasRemaining() && message.getWriteFuture() != null) { + message.getWriteFuture().setResult(Boolean.TRUE); + } + } finally { + if (tmpKey != null) { + tmpKey.cancel(); + tmpKey = null; + } + if (writeSelector != null) { + // Cancel the key. + writeSelector.selectNow(); + SelectorFactory.returnSelector(writeSelector); + } + } + this.scheduleWritenBytes.addAndGet(0 - bytesProduced); + return message.getMessage(); + } + + @Override + protected WriteMessage wrapMessage(Object msg, Future writeFuture) { + WriteMessage message = new WriteMessageImpl(msg, (FutureImpl) writeFuture); + if (message.getWriteBuffer() == null) { + message.setWriteBuffer(this.encoder.encode(message.getMessage(), this)); + } + return message; + } + + @Override + protected void readFromBuffer() { + if (!this.readBuffer.hasRemaining()) { + if (this.readBuffer.capacity() < Configuration.MAX_READ_BUFFER_SIZE) { + this.readBuffer = + IoBuffer.wrap(ByteBufferUtils.increaseBufferCapatity(this.readBuffer.buf())); + } else { + // buffer's capacity is greater than maxium + return; + } + } + if (this.closed) { + return; + } + int n = -1; + int readCount = 0; + try { + while ((n = ((ReadableByteChannel) this.selectableChannel).read(this.readBuffer.buf())) > 0) { + readCount += n; + } + if (readCount > 0) { + decodeAndDispatch(); + } else if (readCount == 0 + && !((SocketChannel) this.selectableChannel).socket().isInputShutdown() + && this.useBlockingRead) { + n = this.blockingRead(); + if (n > 0) { + readCount += n; + } + } + if (n < 0) { // Connection closed + this.close(); + } else { + this.selectorManager.registerSession(this, EventType.ENABLE_READ); + } + if (log.isDebugEnabled()) { + log.debug("read " + readCount + " bytes from channel"); + } + } catch (ClosedChannelException e) { + // ignore exception + this.close(); + } catch (Throwable e) { + this.onException(e); + this.close(); + } + } + + private void decodeAndDispatch() { + updateTimeStamp(); + this.readBuffer.flip(); + this.decode(); + this.readBuffer.compact(); + } + + /** + * Blocking read using temp selector + * + * @return + * @throws ClosedChannelException + * @throws IOException + */ + protected final int blockingRead() throws ClosedChannelException, IOException { + int n = 0; + int readCount = 0; + Selector readSelector = SelectorFactory.getSelector(); + SelectionKey tmpKey = null; + try { + if (this.selectableChannel.isOpen()) { + tmpKey = this.selectableChannel.register(readSelector, 0); + tmpKey.interestOps(tmpKey.interestOps() | SelectionKey.OP_READ); + int code = readSelector.select(500); + tmpKey.interestOps(tmpKey.interestOps() & ~SelectionKey.OP_READ); + if (code > 0) { + do { + n = ((ReadableByteChannel) this.selectableChannel).read(this.readBuffer.buf()); + readCount += n; + if (log.isDebugEnabled()) { + log.debug("use temp selector read " + n + " bytes"); + } + } while (n > 0 && this.readBuffer.hasRemaining()); + if (readCount > 0) { + decodeAndDispatch(); + } + } + } + } finally { + if (tmpKey != null) { + tmpKey.cancel(); + tmpKey = null; + } + if (readSelector != null) { + // Cancel the key. + readSelector.selectNow(); + SelectorFactory.returnSelector(readSelector); + } + } + return readCount; + } + + /** + * Decode buffer + */ + @Override + public void decode() { + Object message; + int size = this.readBuffer.remaining(); + while (this.readBuffer.hasRemaining()) { + try { + message = this.decoder.decode(this.readBuffer, this); + if (message == null) { + break; + } else { + if (this.statistics.isStatistics()) { + this.statistics.statisticsRead(size - this.readBuffer.remaining()); + size = this.readBuffer.remaining(); + } + } + this.dispatchReceivedMessage(message); + } catch (Exception e) { + this.onException(e); + log.error("Decode error", e); + super.close(); + break; + } + } + } + + public Socket socket() { + return ((SocketChannel) this.selectableChannel).socket(); + } + + @Override + protected final void closeChannel() throws IOException { + this.flush0(); + // try to close output first + Socket socket = ((SocketChannel) this.selectableChannel).socket(); + try { + if (!socket.isClosed() && !socket.isOutputShutdown()) { + socket.shutdownOutput(); + } + if (!socket.isClosed() && !socket.isInputShutdown()) { + socket.shutdownInput(); + } + } catch (Exception e) { + } + try { + socket.close(); + } catch (Exception e) { + + } + this.unregisterSession(); + this.unregisterChannel(); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/nio/impl/Reactor.java b/src/main/java/com/google/code/yanf4j/nio/impl/Reactor.java new file mode 100644 index 0000000..f427d37 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/impl/Reactor.java @@ -0,0 +1,544 @@ +/* + * + */ + +package com.google.code.yanf4j.nio.impl; + +import java.io.IOException; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.Date; +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.nio.NioSession; +import com.google.code.yanf4j.util.SystemUtils; + +/** + * Reactor pattern + * + * @author dennis + * + */ +public final class Reactor extends Thread { + /** + * JVM bug threshold + */ + public static final int JVMBUG_THRESHHOLD = + Integer.getInteger("com.googlecode.yanf4j.nio.JVMBUG_THRESHHOLD", 128); + public static final int JVMBUG_THRESHHOLD2 = JVMBUG_THRESHHOLD * 2; + public static final int JVMBUG_THRESHHOLD1 = (JVMBUG_THRESHHOLD2 + JVMBUG_THRESHHOLD) / 2; + public static final int DEFAULT_WAIT = 1000; + + private static final Logger log = LoggerFactory.getLogger("remoting"); + + private boolean jvmBug0; + private boolean jvmBug1; + + private final int reactorIndex; + + private final SelectorManager selectorManager; + + private final AtomicInteger jvmBug = new AtomicInteger(0); + + private long lastJVMBug; + + private Selector selector; + + private final NioController controller; + + private final Configuration configuration; + + static public class PaddingAtomicBoolean extends AtomicBoolean { + + /** + * + */ + private static final long serialVersionUID = 5227571972657902891L; + public int p1; + public long p2, p3, p4, p5, p6, p7, p8; + + PaddingAtomicBoolean(boolean v) { + super(v); + } + } + + private final AtomicBoolean wakenUp = new PaddingAtomicBoolean(false); + + public static class RegisterEvent { + SelectableChannel channel; + int ops; + EventType eventType; + Object attachment; + Session session; + + public RegisterEvent(SelectableChannel channel, int ops, Object attachment) { + super(); + this.channel = channel; + this.ops = ops; + this.attachment = attachment; + } + + public RegisterEvent(Session session, EventType eventType) { + super(); + this.session = session; + this.eventType = eventType; + } + } + + private Queue register; + + private final Lock gate = new ReentrantLock(); + + private int selectTries = 0; + + private long nextTimeout = 0; + + private long lastCheckTimestamp = 0L; + + Reactor(SelectorManager selectorManager, Configuration configuration, int index) + throws IOException { + super(); + reactorIndex = index; + this.register = (Queue) SystemUtils.createTransferQueue(); + this.selectorManager = selectorManager; + controller = selectorManager.getController(); + selector = SystemUtils.openSelector(); + this.configuration = configuration; + setName("Xmemcached-Reactor-" + index); + setDaemon(true); + } + + public final Selector getSelector() { + return selector; + } + + public int getReactorIndex() { + return reactorIndex; + } + + @Override + public void run() { + selectorManager.notifyReady(); + while (selectorManager.isStarted() && selector.isOpen()) { + try { + beforeSelect(); + wakenUp.set(false); + long before = -1; + // Wether to look jvm bug + if (SystemUtils.isLinuxPlatform() && !SystemUtils.isAfterJava6u4Version()) { + before = System.currentTimeMillis(); + } + long wait = DEFAULT_WAIT; + if (nextTimeout > 0) { + wait = nextTimeout; + } + int selected = selector.select(wait); + if (selected == 0) { + if (before != -1) { + lookJVMBug(before, selected, wait); + } + selectTries++; + // check timeout and idle + nextTimeout = checkSessionTimeout(); + continue; + } else { + selectTries = 0; + } + + } catch (ClosedSelectorException e) { + break; + } catch (IOException e) { + log.error("Reactor select error", e); + if (selector.isOpen()) { + continue; + } else { + break; + } + } + Set selectedKeys = selector.selectedKeys(); + gate.lock(); + try { + postSelect(selectedKeys, selector.keys()); + dispatchEvent(selectedKeys); + } finally { + gate.unlock(); + } + } + if (selector != null) { + if (selector.isOpen()) { + try { + controller.closeChannel(selector); + selector.close(); + } catch (IOException e) { + controller.notifyException(e); + log.error("stop reactor error", e); + } + } + } + + } + + /** + * Look jvm bug + * + * @param before + * @param selected + * @param wait + * @return + * @throws IOException + */ + private boolean lookJVMBug(long before, int selected, long wait) throws IOException { + boolean seeing = false; + long now = System.currentTimeMillis(); + + if (JVMBUG_THRESHHOLD > 0 && selected == 0 && wait > JVMBUG_THRESHHOLD + && now - before < wait / 4 && !wakenUp.get() /* waken up */ + && !Thread.currentThread().isInterrupted()/* Interrupted */) { + jvmBug.incrementAndGet(); + if (jvmBug.get() >= JVMBUG_THRESHHOLD2) { + gate.lock(); + try { + lastJVMBug = now; + log.warn("JVM bug occured at " + new Date(lastJVMBug) + + ",http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933,reactIndex=" + + reactorIndex); + if (jvmBug1) { + log.debug("seeing JVM BUG(s) - recreating selector,reactIndex=" + reactorIndex); + } else { + jvmBug1 = true; + log.info("seeing JVM BUG(s) - recreating selector,reactIndex=" + reactorIndex); + } + seeing = true; + final Selector new_selector = SystemUtils.openSelector(); + + for (SelectionKey k : selector.keys()) { + if (!k.isValid() || k.interestOps() == 0) { + continue; + } + + final SelectableChannel channel = k.channel(); + final Object attachment = k.attachment(); + + channel.register(new_selector, k.interestOps(), attachment); + } + + selector.close(); + selector = new_selector; + + } finally { + gate.unlock(); + } + jvmBug.set(0); + + } else if (jvmBug.get() == JVMBUG_THRESHHOLD || jvmBug.get() == JVMBUG_THRESHHOLD1) { + if (jvmBug0) { + log.debug("seeing JVM BUG(s) - cancelling interestOps==0,reactIndex=" + reactorIndex); + } else { + jvmBug0 = true; + log.info("seeing JVM BUG(s) - cancelling interestOps==0,reactIndex=" + reactorIndex); + } + gate.lock(); + seeing = true; + try { + for (SelectionKey k : selector.keys()) { + if (k.isValid() && k.interestOps() == 0) { + k.cancel(); + } + } + } finally { + gate.unlock(); + } + } + } else { + jvmBug.set(0); + } + return seeing; + } + + /** + * Dispatch selected event + * + * @param selectedKeySet + */ + public final void dispatchEvent(Set selectedKeySet) { + Iterator it = selectedKeySet.iterator(); + boolean skipOpRead = false; + while (it.hasNext()) { + SelectionKey key = it.next(); + it.remove(); + if (!key.isValid()) { + if (key.attachment() != null) { + controller.closeSelectionKey(key); + } else { + key.cancel(); + } + continue; + } + try { + if (key.isValid() && key.isAcceptable()) { + controller.onAccept(key); + continue; + } + if (key.isValid() && (key.readyOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { + // Remove write interest + key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE); + controller.onWrite(key); + if (!controller.isHandleReadWriteConcurrently()) { + skipOpRead = true; + } + } + if (!skipOpRead && key.isValid() + && (key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { + key.interestOps(key.interestOps() & ~SelectionKey.OP_READ); + if (!controller.getStatistics().isReceiveOverFlow()) { + // Remove read interest + controller.onRead(key); + } else { + key.interestOps(key.interestOps() | SelectionKey.OP_READ); + } + + } + if ((key.readyOps() & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT) { + controller.onConnect(key); + } + + } catch (CancelledKeyException e) { + // ignore + } catch (RejectedExecutionException e) { + + if (key.attachment() instanceof AbstractNioSession) { + ((AbstractNioSession) key.attachment()).onException(e); + } + controller.notifyException(e); + if (selector.isOpen()) { + continue; + } else { + break; + } + } catch (Exception e) { + if (key.attachment() instanceof AbstractNioSession) { + ((AbstractNioSession) key.attachment()).onException(e); + } + controller.closeSelectionKey(key); + controller.notifyException(e); + log.error("Reactor dispatch events error", e); + if (selector.isOpen()) { + continue; + } else { + break; + } + } + } + } + + final void unregisterChannel(SelectableChannel channel) throws IOException { + Selector selector = this.selector; + if (selector != null) { + if (channel != null) { + SelectionKey key = channel.keyFor(selector); + if (key != null) { + key.cancel(); + } + } + } + wakeup(); + } + + /** + * Check session timeout or idle + * + * @return + */ + private final long checkSessionTimeout() { + long nextTimeout = 0; + if (configuration.getCheckSessionTimeoutInterval() > 0) { + gate.lock(); + try { + if (isNeedCheckSessionIdleTimeout()) { + nextTimeout = configuration.getCheckSessionTimeoutInterval(); + for (SelectionKey key : selector.keys()) { + + if (key.attachment() != null) { + long n = checkExpiredIdle(key, getSessionFromAttchment(key)); + nextTimeout = n < nextTimeout ? n : nextTimeout; + } + } + selectTries = 0; + lastCheckTimestamp = System.currentTimeMillis(); + } + } finally { + gate.unlock(); + } + } + return nextTimeout; + } + + private boolean isNeedCheckSessionIdleTimeout() { + return selectTries * 1000 >= configuration.getCheckSessionTimeoutInterval() + || System.currentTimeMillis() - this.lastCheckTimestamp >= configuration + .getCheckSessionTimeoutInterval(); + } + + private final Session getSessionFromAttchment(SelectionKey key) { + if (key.attachment() instanceof Session) { + return (Session) key.attachment(); + } + return null; + } + + public final void registerSession(Session session, EventType event) { + final Selector selector = this.selector; + if (isReactorThread() && selector != null) { + dispatchSessionEvent(session, event); + } else { + register.offer(new RegisterEvent(session, event)); + wakeup(); + } + } + + private final boolean isReactorThread() { + return Thread.currentThread() == this; + } + + final void beforeSelect() { + controller.checkStatisticsForRestart(); + processRegister(); + } + + private final void processRegister() { + RegisterEvent event = null; + while ((event = register.poll()) != null) { + if (event.session != null) { + dispatchSessionEvent(event.session, event.eventType); + } else { + registerChannelNow(event.channel, event.ops, event.attachment); + } + } + } + + Configuration getConfiguration() { + return configuration; + } + + private final void dispatchSessionEvent(Session session, EventType event) { + if (session.isClosed() && event != EventType.UNREGISTER) { + return; + } + switch (event) { + case REGISTER: + controller.registerSession(session); + break; + case UNREGISTER: + controller.unregisterSession(session); + break; + default: + ((NioSession) session).onEvent(event, selector); + break; + } + } + + public final void postSelect(Set selectedKeys, Set allKeys) { + if (controller.getSessionTimeout() > 0 || controller.getSessionIdleTimeout() > 0) { + if (isNeedCheckSessionIdleTimeout()) { + for (SelectionKey key : allKeys) { + if (!selectedKeys.contains(key)) { + if (key.attachment() != null) { + checkExpiredIdle(key, getSessionFromAttchment(key)); + } + } + } + lastCheckTimestamp = System.currentTimeMillis(); + } + } + } + + private long checkExpiredIdle(SelectionKey key, Session session) { + if (session == null) { + return 0; + } + long nextTimeout = 0; + boolean expired = false; + if (controller.getSessionTimeout() > 0) { + expired = checkExpired(key, session); + nextTimeout = controller.getSessionTimeout(); + } + if (controller.getSessionIdleTimeout() > 0 && !expired) { + checkIdle(session); + nextTimeout = controller.getSessionIdleTimeout(); + } + return nextTimeout; + } + + private final void checkIdle(Session session) { + if (controller.getSessionIdleTimeout() > 0) { + if (session.isIdle()) { + ((NioSession) session).onEvent(EventType.IDLE, selector); + } + } + } + + private final boolean checkExpired(SelectionKey key, Session session) { + if (session != null && session.isExpired()) { + ((NioSession) session).onEvent(EventType.EXPIRED, selector); + controller.closeSelectionKey(key); + return true; + } + return false; + } + + public final void registerChannel(SelectableChannel channel, int ops, Object attachment) { + if (isReactorThread()) { + registerChannelNow(channel, ops, attachment); + } else { + register.offer(new RegisterEvent(channel, ops, attachment)); + wakeup(); + } + + } + + private void registerChannelNow(SelectableChannel channel, int ops, Object attachment) { + if (channel.isOpen()) { + gate.lock(); + try { + channel.register(selector, ops, attachment); + + } catch (ClosedChannelException e) { + log.error("Register channel error", e); + controller.notifyException(e); + } finally { + gate.unlock(); + } + } + } + + final void wakeup() { + if (wakenUp.compareAndSet(false, true)) { + final Selector selector = this.selector; + if (selector != null) { + selector.wakeup(); + } + } + } + + final void selectNow() throws IOException { + final Selector selector = this.selector; + if (selector != null) { + selector.selectNow(); + } + } +} diff --git a/src/main/java/com/google/code/yanf4j/nio/impl/SelectorManager.java b/src/main/java/com/google/code/yanf4j/nio/impl/SelectorManager.java new file mode 100644 index 0000000..e6b9cbe --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/impl/SelectorManager.java @@ -0,0 +1,189 @@ +package com.google.code.yanf4j.nio.impl; + +import java.io.IOException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.Session; + +/** + * Selector manager + * + * @author dennis + * + */ +public class SelectorManager { + private final Reactor[] reactorSet; + private final AtomicInteger sets = new AtomicInteger(0); + private final NioController controller; + private final int dividend; + + /** + * Reactor count which are ready + */ + private int reactorReadyCount; + + public SelectorManager(int selectorPoolSize, NioController controller, Configuration conf) + throws IOException { + if (selectorPoolSize <= 0) { + throw new IllegalArgumentException("selectorPoolSize<=0"); + } + log.info("Creating " + selectorPoolSize + " reactors..."); + reactorSet = new Reactor[selectorPoolSize]; + this.controller = controller; + for (int i = 0; i < selectorPoolSize; i++) { + reactorSet[i] = new Reactor(this, conf, i); + } + dividend = reactorSet.length - 1; + } + + private volatile boolean started; + + public int getSelectorCount() { + return reactorSet == null ? 0 : reactorSet.length; + } + + public synchronized void start() { + if (started) { + return; + } + started = true; + for (Reactor reactor : reactorSet) { + reactor.start(); + } + } + + Reactor getReactorFromSession(Session session) { + Reactor reactor = (Reactor) session.getAttribute(REACTOR_ATTRIBUTE); + + if (reactor == null) { + reactor = nextReactor(); + final Reactor oldReactor = (Reactor) session.setAttributeIfAbsent(REACTOR_ATTRIBUTE, reactor); + if (oldReactor != null) { + reactor = oldReactor; + } + } + return reactor; + } + + /** + * Find reactor by index + * + * @param index + * @return + */ + public Reactor getReactorByIndex(int index) { + if (index < 0 || index > reactorSet.length - 1) { + throw new ArrayIndexOutOfBoundsException(); + } + return reactorSet[index]; + } + + public synchronized void stop() { + if (!started) { + return; + } + started = false; + for (Reactor reactor : reactorSet) { + reactor.interrupt(); + } + } + + public static final String REACTOR_ATTRIBUTE = System.currentTimeMillis() + "_Reactor_Attribute"; + + /** + * Register channel + * + * @param channel + * @param ops + * @param attachment + * @return + */ + public final Reactor registerChannel(SelectableChannel channel, int ops, Object attachment) { + awaitReady(); + int index = 0; + // Accept event used index 0 reactor + if (ops == SelectionKey.OP_ACCEPT || ops == SelectionKey.OP_CONNECT) { + index = 0; + } else { + index = sets.incrementAndGet() % dividend + 1; + } + final Reactor reactor = reactorSet[index]; + reactor.registerChannel(channel, ops, attachment); + return reactor; + + } + + void awaitReady() { + synchronized (this) { + while (!started || reactorReadyCount != reactorSet.length) { + try { + this.wait(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt();// reset interrupt status + } + } + } + } + + /** + * Get next reactor + * + * @return + */ + public final Reactor nextReactor() { + if (dividend > 0) { + return reactorSet[sets.incrementAndGet() % dividend + 1]; + } else { + return reactorSet[0]; + } + } + + /** + * Register session + * + * @param session + * @param event + */ + public final void registerSession(Session session, EventType event) { + if (session.isClosed() && event != EventType.UNREGISTER) { + return; + } + Reactor reactor = (Reactor) session.getAttribute(REACTOR_ATTRIBUTE); + + if (reactor == null) { + reactor = nextReactor(); + final Reactor oldReactor = (Reactor) session.setAttributeIfAbsent(REACTOR_ATTRIBUTE, reactor); + if (oldReactor != null) { + reactor = oldReactor; + } + } + reactor.registerSession(session, event); + } + + public NioController getController() { + return controller; + } + + /** + * Notify all reactor have been ready + */ + synchronized void notifyReady() { + reactorReadyCount++; + if (reactorReadyCount == reactorSet.length) { + controller.notifyReady(); + notifyAll(); + } + + } + + private static final Logger log = LoggerFactory.getLogger(SelectorManager.class); + + public final boolean isStarted() { + return started; + } +} diff --git a/src/main/java/com/google/code/yanf4j/nio/impl/SocketChannelController.java b/src/main/java/com/google/code/yanf4j/nio/impl/SocketChannelController.java new file mode 100644 index 0000000..cd30b85 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/impl/SocketChannelController.java @@ -0,0 +1,115 @@ +package com.google.code.yanf4j.nio.impl; + +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.Queue; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.core.impl.StandardSocketOption; +import com.google.code.yanf4j.nio.NioSession; +import com.google.code.yanf4j.nio.NioSessionConfig; + +/** + * Nio tcp socket controller + * + * @author dennis + * + */ +public abstract class SocketChannelController extends NioController { + + protected boolean soLingerOn = false; + + public void setSoLinger(boolean on, int value) { + this.soLingerOn = on; + this.socketOptions.put(StandardSocketOption.SO_LINGER, value); + } + + public SocketChannelController() { + super(); + } + + public SocketChannelController(Configuration configuration) { + super(configuration, null, null); + + } + + public SocketChannelController(Configuration configuration, CodecFactory codecFactory) { + super(configuration, null, codecFactory); + } + + public SocketChannelController(Configuration configuration, Handler handler, + CodecFactory codecFactory) { + super(configuration, handler, codecFactory); + } + + @Override + protected final void dispatchReadEvent(SelectionKey key) { + Session session = (Session) key.attachment(); + if (session != null) { + ((NioSession) session).onEvent(EventType.READABLE, key.selector()); + } else { + log.warn("Could not find session for readable event,maybe it is closed"); + } + } + + @Override + protected final void dispatchWriteEvent(SelectionKey key) { + Session session = (Session) key.attachment(); + if (session != null) { + ((NioSession) session).onEvent(EventType.WRITEABLE, key.selector()); + } else { + log.warn("Could not find session for writable event,maybe it is closed"); + } + + } + + protected NioSession buildSession(SocketChannel sc) { + Queue queue = buildQueue(); + NioSessionConfig sessionConfig = buildSessionConfig(sc, queue); + NioSession session = + new NioTCPSession(sessionConfig, this.configuration.getSessionReadBufferSize()); + return session; + } + + /** + * Confiure socket channel + * + * @param sc + * @throws IOException + */ + protected final void configureSocketChannel(SocketChannel sc) throws IOException { + sc.socket().setSoTimeout(this.soTimeout); + sc.configureBlocking(false); + if (this.socketOptions.get(StandardSocketOption.SO_REUSEADDR) != null) { + sc.socket().setReuseAddress(StandardSocketOption.SO_REUSEADDR.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_REUSEADDR))); + } + if (this.socketOptions.get(StandardSocketOption.SO_SNDBUF) != null) { + sc.socket().setSendBufferSize(StandardSocketOption.SO_SNDBUF.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_SNDBUF))); + } + if (this.socketOptions.get(StandardSocketOption.SO_KEEPALIVE) != null) { + sc.socket().setKeepAlive(StandardSocketOption.SO_KEEPALIVE.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_KEEPALIVE))); + } + if (this.socketOptions.get(StandardSocketOption.SO_LINGER) != null) { + sc.socket().setSoLinger(this.soLingerOn, StandardSocketOption.SO_LINGER.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_LINGER))); + } + if (this.socketOptions.get(StandardSocketOption.SO_RCVBUF) != null) { + sc.socket().setReceiveBufferSize(StandardSocketOption.SO_RCVBUF.type() + .cast(this.socketOptions.get(StandardSocketOption.SO_RCVBUF))); + + } + if (this.socketOptions.get(StandardSocketOption.TCP_NODELAY) != null) { + sc.socket().setTcpNoDelay(StandardSocketOption.TCP_NODELAY.type() + .cast(this.socketOptions.get(StandardSocketOption.TCP_NODELAY))); + } + } + +} diff --git a/src/main/java/com/google/code/yanf4j/nio/package.html b/src/main/java/com/google/code/yanf4j/nio/package.html new file mode 100644 index 0000000..59a13f9 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/nio/package.html @@ -0,0 +1,10 @@ + + + + + Nio implementation + + +

Nio implementation

+ + \ No newline at end of file diff --git a/src/main/java/com/google/code/yanf4j/statistics/Statistics.java b/src/main/java/com/google/code/yanf4j/statistics/Statistics.java new file mode 100644 index 0000000..099cff2 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/statistics/Statistics.java @@ -0,0 +1,95 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.statistics; + +/** + * Statistics + * + * @author dennis + * + */ +public interface Statistics { + + public void start(); + + public void stop(); + + public double getReceiveBytesPerSecond(); + + public double getSendBytesPerSecond(); + + public abstract void statisticsProcess(long n); + + public abstract long getProcessedMessageCount(); + + public abstract double getProcessedMessageAverageTime(); + + public abstract void statisticsRead(long n); + + public abstract void statisticsWrite(long n); + + public abstract long getRecvMessageCount(); + + public abstract long getRecvMessageTotalSize(); + + public abstract long getRecvMessageAverageSize(); + + public abstract long getWriteMessageTotalSize(); + + public abstract long getWriteMessageCount(); + + public abstract long getWriteMessageAverageSize(); + + public abstract double getRecvMessageCountPerSecond(); + + public abstract double getWriteMessageCountPerSecond(); + + public void statisticsAccept(); + + public double getAcceptCountPerSecond(); + + public long getStartedTime(); + + public void reset(); + + public void restart(); + + public boolean isStatistics(); + + public void setReceiveThroughputLimit(double receiveThroughputLimit); + + /** + * Check session if receive bytes per second is over flow controll + * + * @return + */ + public boolean isReceiveOverFlow(); + + /** + * Check session if receive bytes per second is over flow controll + * + * @return + */ + public boolean isSendOverFlow(); + + public double getSendThroughputLimit(); + + public void setSendThroughputLimit(double sendThroughputLimit); + + public double getReceiveThroughputLimit(); + +} diff --git a/src/main/java/com/google/code/yanf4j/statistics/impl/DefaultStatistics.java b/src/main/java/com/google/code/yanf4j/statistics/impl/DefaultStatistics.java new file mode 100644 index 0000000..4701ddb --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/statistics/impl/DefaultStatistics.java @@ -0,0 +1,148 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.statistics.impl; + +import com.google.code.yanf4j.statistics.Statistics; + +/** + * Default statistics implementation + * + * @author dennis + * + */ +public class DefaultStatistics implements Statistics { + public void start() { + + } + + public double getSendBytesPerSecond() { + return 0; + } + + public double getReceiveBytesPerSecond() { + return 0; + } + + public boolean isStatistics() { + return false; + } + + public long getStartedTime() { + return 0; + } + + public void reset() { + + } + + public void restart() { + + } + + public double getProcessedMessageAverageTime() { + return 0; + } + + public long getProcessedMessageCount() { + return 0; + } + + public void statisticsProcess(long n) { + + } + + public void stop() { + + } + + public long getRecvMessageCount() { + + return 0; + } + + public long getRecvMessageTotalSize() { + + return 0; + } + + public long getRecvMessageAverageSize() { + + return 0; + } + + public double getRecvMessageCountPerSecond() { + + return 0; + } + + public long getWriteMessageCount() { + + return 0; + } + + public long getWriteMessageTotalSize() { + + return 0; + } + + public long getWriteMessageAverageSize() { + + return 0; + } + + public void statisticsRead(long n) { + + } + + public void statisticsWrite(long n) { + + } + + public double getWriteMessageCountPerSecond() { + + return 0; + } + + public double getAcceptCountPerSecond() { + return 0; + } + + public void statisticsAccept() { + + } + + public void setReceiveThroughputLimit(double receivePacketRate) {} + + public boolean isReceiveOverFlow() { + return false; + } + + public boolean isSendOverFlow() { + return false; + } + + public double getSendThroughputLimit() { + return -1.0; + } + + public void setSendThroughputLimit(double sendThroughputLimit) {} + + public final double getReceiveThroughputLimit() { + return -1.0; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/statistics/impl/SimpleStatistics.java b/src/main/java/com/google/code/yanf4j/statistics/impl/SimpleStatistics.java new file mode 100644 index 0000000..bd5ad80 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/statistics/impl/SimpleStatistics.java @@ -0,0 +1,237 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.statistics.impl; + +import java.util.concurrent.atomic.AtomicLong; +import com.google.code.yanf4j.statistics.Statistics; + +/** + * A simple statistics implementation + * + * @author dennis + * + */ +public class SimpleStatistics implements Statistics { + private boolean started = false; + + public boolean isStatistics() { + return this.started; + } + + public synchronized void reset() { + if (this.started) { + throw new IllegalStateException(); + } + this.startTime = this.stopTime = -1; + this.recvMessageCount.set(0); + this.recvMessageTotalSize.set(0); + this.writeMessageCount.set(0); + this.writeMessageTotalSize.set(0); + this.processMessageCount.set(0); + this.processMessageTotalTime.set(0); + this.acceptCount.set(0); + } + + private double receiveThroughputLimit = -1.0; // receive bytes per second + private double sendThroughputLimit = -1.0; // send bytes per second + + public void setReceiveThroughputLimit(double receivePacketRate) { + this.receiveThroughputLimit = receivePacketRate; + } + + /** + * Check session if receive bytes per second is over flow controll + * + * @return + */ + public boolean isReceiveOverFlow() { + if (getReceiveThroughputLimit() < 0.0000f) { + return false; + } + return getReceiveBytesPerSecond() > getReceiveThroughputLimit(); + } + + /** + * Check session if receive bytes per second is over flow controll + * + * @return + */ + public boolean isSendOverFlow() { + if (getSendThroughputLimit() < 0.0000f) { + return false; + } + return getSendBytesPerSecond() > getSendThroughputLimit(); + } + + public double getSendThroughputLimit() { + return this.sendThroughputLimit; + } + + public void setSendThroughputLimit(double sendThroughputLimit) { + this.sendThroughputLimit = sendThroughputLimit; + } + + public final double getReceiveThroughputLimit() { + return this.receiveThroughputLimit; + } + + public synchronized void restart() { + stop(); + reset(); + start(); + } + + private long startTime, stopTime = -1; + + private AtomicLong recvMessageCount = new AtomicLong(); + + private AtomicLong recvMessageTotalSize = new AtomicLong(); + + private AtomicLong writeMessageCount = new AtomicLong(); + + private AtomicLong writeMessageTotalSize = new AtomicLong(); + + private AtomicLong processMessageCount = new AtomicLong(); + + private AtomicLong acceptCount = new AtomicLong(); + + private AtomicLong processMessageTotalTime = new AtomicLong(); + + public long getStartedTime() { + return this.startTime; + } + + public double getProcessedMessageAverageTime() { + return this.processMessageCount.get() == 0 ? 0 + : (double) this.processMessageTotalTime.get() / this.processMessageCount.get(); + } + + public long getProcessedMessageCount() { + return this.processMessageCount.get(); + } + + public void statisticsProcess(long n) { + if (!this.started) { + return; + } + if (n < 0) { + return; + } + this.processMessageTotalTime.addAndGet(n); + this.processMessageCount.incrementAndGet(); + } + + public SimpleStatistics() { + + } + + public synchronized void start() { + this.startTime = System.currentTimeMillis(); + this.started = true; + } + + public synchronized void stop() { + this.stopTime = System.currentTimeMillis(); + this.started = false; + } + + public void statisticsRead(long n) { + if (!this.started) { + return; + } + if (n <= 0) { + return; + } + this.recvMessageCount.incrementAndGet(); + this.recvMessageTotalSize.addAndGet(n); + } + + public long getRecvMessageCount() { + return this.recvMessageCount.get(); + } + + public long getRecvMessageTotalSize() { + return this.recvMessageTotalSize.get(); + } + + public long getWriteMessageCount() { + return this.writeMessageCount.get(); + } + + public long getWriteMessageTotalSize() { + return this.writeMessageTotalSize.get(); + } + + public void statisticsWrite(long n) { + if (!this.started) { + return; + } + if (n <= 0) { + return; + } + this.writeMessageCount.incrementAndGet(); + this.writeMessageTotalSize.addAndGet(n); + } + + public long getRecvMessageAverageSize() { + return this.recvMessageCount.get() == 0 ? 0 + : this.recvMessageTotalSize.get() / this.recvMessageCount.get(); + } + + public double getRecvMessageCountPerSecond() { + long duration = (this.stopTime == -1) ? (System.currentTimeMillis() - this.startTime) + : (this.stopTime - this.startTime); + return duration == 0 ? 0 : (double) this.recvMessageCount.get() * 1000 / duration; + } + + public double getWriteMessageCountPerSecond() { + long duration = (this.stopTime == -1) ? (System.currentTimeMillis() - this.startTime) + : (this.stopTime - this.startTime); + return duration == 0 ? 0 : (double) this.writeMessageCount.get() * 1000 / duration; + } + + public long getWriteMessageAverageSize() { + return this.writeMessageCount.get() == 0 ? 0 + : this.writeMessageTotalSize.get() / this.writeMessageCount.get(); + } + + public double getAcceptCountPerSecond() { + long duration = (this.stopTime == -1) ? (System.currentTimeMillis() - this.startTime) + : (this.stopTime - this.startTime); + return duration == 0 ? 0 : (double) this.acceptCount.get() * 1000 / duration; + } + + public double getReceiveBytesPerSecond() { + long duration = (this.stopTime == -1) ? (System.currentTimeMillis() - this.startTime) + : (this.stopTime - this.startTime); + return duration == 0 ? 0 : (double) this.recvMessageTotalSize.get() * 1000 / duration; + } + + public double getSendBytesPerSecond() { + long duration = (this.stopTime == -1) ? (System.currentTimeMillis() - this.startTime) + : (this.stopTime - this.startTime); + return duration == 0 ? 0 : (double) this.writeMessageTotalSize.get() * 1000 / duration; + } + + public void statisticsAccept() { + if (!this.started) { + return; + } + this.acceptCount.incrementAndGet(); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/statistics/package.html b/src/main/java/com/google/code/yanf4j/statistics/package.html new file mode 100644 index 0000000..3604b72 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/statistics/package.html @@ -0,0 +1,10 @@ + + + + + Statistics + + +

Networking statistics

+ + \ No newline at end of file diff --git a/src/main/java/com/google/code/yanf4j/util/ByteBufferMatcher.java b/src/main/java/com/google/code/yanf4j/util/ByteBufferMatcher.java new file mode 100644 index 0000000..74705b1 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/ByteBufferMatcher.java @@ -0,0 +1,34 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.util.List; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * ByteBuffer matcher + * + * @author dennis + * + */ +public interface ByteBufferMatcher { + + public int matchFirst(IoBuffer buffer); + + public List matchAll(final IoBuffer buffer); + +} diff --git a/src/main/java/com/google/code/yanf4j/util/ByteBufferUtils.java b/src/main/java/com/google/code/yanf4j/util/ByteBufferUtils.java new file mode 100644 index 0000000..f06907a --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/ByteBufferUtils.java @@ -0,0 +1,197 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +/* + * Copyright 2004-2005 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. + */ +/** + * 来自于cindy2.4的工具类,做了简化和新增 + */ +import java.nio.ByteBuffer; +import com.google.code.yanf4j.config.Configuration; + +public class ByteBufferUtils { + /** + * + * @param byteBuffer + * @return * + */ + public static final ByteBuffer increaseBufferCapatity(ByteBuffer byteBuffer) { + + if (byteBuffer == null) { + throw new IllegalArgumentException("buffer is null"); + } + if (Configuration.DEFAULT_INCREASE_BUFF_SIZE < 0) { + throw new IllegalArgumentException("size less than 0"); + } + + int capacity = byteBuffer.capacity() + Configuration.DEFAULT_INCREASE_BUFF_SIZE; + if (capacity < 0) { + throw new IllegalArgumentException("capacity can't be negative"); + } + ByteBuffer result = (byteBuffer.isDirect() ? ByteBuffer.allocateDirect(capacity) + : ByteBuffer.allocate(capacity)); + result.order(byteBuffer.order()); + byteBuffer.flip(); + result.put(byteBuffer); + return result; + } + + public static final void flip(ByteBuffer[] buffers) { + if (buffers == null) { + return; + } + for (ByteBuffer buffer : buffers) { + if (buffer != null) { + buffer.flip(); + } + } + } + + public static final ByteBuffer gather(ByteBuffer[] buffers) { + if (buffers == null || buffers.length == 0) { + return null; + } + ByteBuffer result = ByteBuffer.allocate(remaining(buffers)); + result.order(buffers[0].order()); + for (int i = 0; i < buffers.length; i++) { + if (buffers[i] != null) { + result.put(buffers[i]); + } + } + result.flip(); + return result; + } + + public static final int remaining(ByteBuffer[] buffers) { + if (buffers == null) { + return 0; + } + int remaining = 0; + for (int i = 0; i < buffers.length; i++) { + if (buffers[i] != null) { + remaining += buffers[i].remaining(); + } + } + return remaining; + } + + public static final void clear(ByteBuffer[] buffers) { + if (buffers == null) { + return; + } + for (ByteBuffer buffer : buffers) { + if (buffer != null) { + buffer.clear(); + } + } + } + + public static final String toHex(byte b) { + return ("" + "0123456789ABCDEF".charAt(0xf & b >> 4) + "0123456789ABCDEF".charAt(b & 0xf)); + } + + public static final int indexOf(ByteBuffer buffer, ByteBuffer pattern) { + if (pattern == null || buffer == null) { + return -1; + } + int n = buffer.remaining(); + int m = pattern.remaining(); + int patternPos = pattern.position(); + int bufferPos = buffer.position(); + if (n < m) { + return -1; + } + for (int s = 0; s <= n - m; s++) { + boolean match = true; + for (int i = 0; i < m; i++) { + if (buffer.get(s + i + bufferPos) != pattern.get(patternPos + i)) { + match = false; + break; + } + } + if (match) { + return (bufferPos + s); + } + } + return -1; + } + + public static final int indexOf(ByteBuffer buffer, ByteBuffer pattern, int offset) { + if (offset < 0) { + throw new IllegalArgumentException("offset must be greater than 0"); + } + if (pattern == null || buffer == null) { + return -1; + } + int patternPos = pattern.position(); + int n = buffer.remaining(); + int m = pattern.remaining(); + if (n < m) { + return -1; + } + if (offset < buffer.position() || offset > buffer.limit()) { + return -1; + } + for (int s = 0; s <= n - m; s++) { + boolean match = true; + for (int i = 0; i < m; i++) { + if (buffer.get(s + i + offset) != pattern.get(patternPos + i)) { + match = false; + break; + } + } + if (match) { + return (offset + s); + } + } + return -1; + } + + /** + * 查看ByteBuffer数组是否还有剩余 + * + * @param buffers ByteBuffers + * @return have remaining + */ + public static final boolean hasRemaining(ByteBuffer[] buffers) { + if (buffers == null) { + return false; + } + for (int i = 0; i < buffers.length; i++) { + if (buffers[i] != null && buffers[i].hasRemaining()) { + return true; + } + } + return false; + } + + public static final int uByte(byte b) { + return b & 0xFF; + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/CircularQueue.java b/src/main/java/com/google/code/yanf4j/util/CircularQueue.java new file mode 100644 index 0000000..2ab100f --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/CircularQueue.java @@ -0,0 +1,342 @@ +package com.google.code.yanf4j.util; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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. + */ + +import java.io.Serializable; +import java.util.AbstractList; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Queue; + +/** + * A circular queue from mina + * + * @author dennis + * + * @param + */ +public class CircularQueue extends AbstractList implements List, Queue, Serializable { + + private static final long serialVersionUID = 3993421269224511264L; + + private static final int DEFAULT_CAPACITY = 4; + + private final int initialCapacity; + // XXX: This volatile keyword here is a workaround for SUN Java Compiler + // bug, + // which produces buggy byte code. I don't event know why adding a volatile + // fixes the problem. Eclipse Java Compiler seems to produce correct byte + // code. + private volatile Object[] items; + private int mask; + private int first = 0; + private int last = 0; + private boolean full; + private int shrinkThreshold; + + /** + * Construct a new, empty queue. + */ + public CircularQueue() { + this(DEFAULT_CAPACITY); + } + + public CircularQueue(int initialCapacity) { + int actualCapacity = normalizeCapacity(initialCapacity); + this.items = new Object[actualCapacity]; + this.mask = actualCapacity - 1; + this.initialCapacity = actualCapacity; + this.shrinkThreshold = 0; + } + + private static int normalizeCapacity(int initialCapacity) { + int actualCapacity = 1; + while (actualCapacity < initialCapacity) { + actualCapacity <<= 1; + if (actualCapacity < 0) { + actualCapacity = 1 << 30; + break; + } + } + return actualCapacity; + } + + /** + * Returns the capacity of this queue. + */ + public int capacity() { + return this.items.length; + } + + @Override + public void clear() { + if (!isEmpty()) { + Arrays.fill(this.items, null); + this.first = 0; + this.last = 0; + this.full = false; + shrinkIfNeeded(); + } + } + + @SuppressWarnings("unchecked") + public E poll() { + if (isEmpty()) { + return null; + } + + Object ret = this.items[this.first]; + this.items[this.first] = null; + decreaseSize(); + + if (this.first == this.last) { + this.first = this.last = 0; + } + + shrinkIfNeeded(); + return (E) ret; + } + + public boolean offer(E item) { + if (item == null) { + throw new NullPointerException("item"); + } + expandIfNeeded(); + this.items[this.last] = item; + increaseSize(); + return true; + } + + @SuppressWarnings("unchecked") + public E peek() { + if (isEmpty()) { + return null; + } + + return (E) this.items[this.first]; + } + + @SuppressWarnings("unchecked") + @Override + public E get(int idx) { + checkIndex(idx); + return (E) this.items[getRealIndex(idx)]; + } + + @Override + public boolean isEmpty() { + return (this.first == this.last) && !this.full; + } + + @Override + public int size() { + if (this.full) { + return capacity(); + } + + if (this.last >= this.first) { + return this.last - this.first; + } else { + return this.last - this.first + capacity(); + } + } + + @Override + public String toString() { + return "first=" + this.first + ", last=" + this.last + ", size=" + size() + ", mask = " + + this.mask; + } + + private void checkIndex(int idx) { + if (idx < 0 || idx >= size()) { + throw new IndexOutOfBoundsException(String.valueOf(idx)); + } + } + + private int getRealIndex(int idx) { + return (this.first + idx) & this.mask; + } + + private void increaseSize() { + this.last = (this.last + 1) & this.mask; + this.full = this.first == this.last; + } + + private void decreaseSize() { + this.first = (this.first + 1) & this.mask; + this.full = false; + } + + private void expandIfNeeded() { + if (this.full) { + // expand queue + final int oldLen = this.items.length; + final int newLen = oldLen << 1; + Object[] tmp = new Object[newLen]; + + if (this.first < this.last) { + System.arraycopy(this.items, this.first, tmp, 0, this.last - this.first); + } else { + System.arraycopy(this.items, this.first, tmp, 0, oldLen - this.first); + System.arraycopy(this.items, 0, tmp, oldLen - this.first, this.last); + } + + this.first = 0; + this.last = oldLen; + this.items = tmp; + this.mask = tmp.length - 1; + if (newLen >>> 3 > this.initialCapacity) { + this.shrinkThreshold = newLen >>> 3; + } + } + } + + private void shrinkIfNeeded() { + int size = size(); + if (size <= this.shrinkThreshold) { + // shrink queue + final int oldLen = this.items.length; + int newLen = normalizeCapacity(size); + if (size == newLen) { + newLen <<= 1; + } + + if (newLen >= oldLen) { + return; + } + + if (newLen < this.initialCapacity) { + if (oldLen == this.initialCapacity) { + return; + } else { + newLen = this.initialCapacity; + } + } + + Object[] tmp = new Object[newLen]; + + // Copy only when there's something to copy. + if (size > 0) { + if (this.first < this.last) { + System.arraycopy(this.items, this.first, tmp, 0, this.last - this.first); + } else { + System.arraycopy(this.items, this.first, tmp, 0, oldLen - this.first); + System.arraycopy(this.items, 0, tmp, oldLen - this.first, this.last); + } + } + + this.first = 0; + this.last = size; + this.items = tmp; + this.mask = tmp.length - 1; + this.shrinkThreshold = 0; + } + } + + @Override + public boolean add(E o) { + return offer(o); + } + + @SuppressWarnings("unchecked") + @Override + public E set(int idx, E o) { + checkIndex(idx); + + int realIdx = getRealIndex(idx); + Object old = this.items[realIdx]; + this.items[realIdx] = o; + return (E) old; + } + + @Override + public void add(int idx, E o) { + if (idx == size()) { + offer(o); + return; + } + + checkIndex(idx); + expandIfNeeded(); + + int realIdx = getRealIndex(idx); + + // Make a room for a new element. + if (this.first < this.last) { + System.arraycopy(this.items, realIdx, this.items, realIdx + 1, this.last - realIdx); + } else { + if (realIdx >= this.first) { + System.arraycopy(this.items, 0, this.items, 1, this.last); + this.items[0] = this.items[this.items.length - 1]; + System.arraycopy(this.items, realIdx, this.items, realIdx + 1, + this.items.length - realIdx - 1); + } else { + System.arraycopy(this.items, realIdx, this.items, realIdx + 1, this.last - realIdx); + } + } + + this.items[realIdx] = o; + increaseSize(); + } + + @SuppressWarnings("unchecked") + @Override + public E remove(int idx) { + if (idx == 0) { + return poll(); + } + + checkIndex(idx); + + int realIdx = getRealIndex(idx); + Object removed = this.items[realIdx]; + + // Remove a room for the removed element. + if (this.first < this.last) { + System.arraycopy(this.items, this.first, this.items, this.first + 1, realIdx - this.first); + } else { + if (realIdx >= this.first) { + System.arraycopy(this.items, this.first, this.items, this.first + 1, realIdx - this.first); + } else { + System.arraycopy(this.items, 0, this.items, 1, realIdx); + this.items[0] = this.items[this.items.length - 1]; + System.arraycopy(this.items, this.first, this.items, this.first + 1, + this.items.length - this.first - 1); + } + } + + this.items[this.first] = null; + decreaseSize(); + + shrinkIfNeeded(); + return (E) removed; + } + + public E remove() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + return poll(); + } + + public E element() { + if (isEmpty()) { + throw new NoSuchElementException(); + } + return peek(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/ConcurrentHashSet.java b/src/main/java/com/google/code/yanf4j/util/ConcurrentHashSet.java new file mode 100644 index 0000000..f0ca04c --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/ConcurrentHashSet.java @@ -0,0 +1,47 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A {@link ConcurrentHashMap}-backed {@link Set}. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 597692 $, $Date: 2007-11-23 08:56:32 -0700 (Fri, 23 Nov 2007) $ + */ +public class ConcurrentHashSet extends MapBackedSet { + + private static final long serialVersionUID = 8518578988740277828L; + + public ConcurrentHashSet() { + super(new ConcurrentHashMap()); + } + + public ConcurrentHashSet(Collection c) { + super(new ConcurrentHashMap(), c); + } + + @Override + public boolean add(E o) { + Boolean answer = ((ConcurrentMap) map).putIfAbsent(o, Boolean.TRUE); + return answer == null; + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/DispatcherFactory.java b/src/main/java/com/google/code/yanf4j/util/DispatcherFactory.java new file mode 100644 index 0000000..ef04f42 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/DispatcherFactory.java @@ -0,0 +1,22 @@ +package com.google.code.yanf4j.util; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.TimeUnit; +import com.google.code.yanf4j.core.impl.PoolDispatcher; + +/** + * Dispatcher Factory + * + * @author dennis + * + */ +public class DispatcherFactory { + public static com.google.code.yanf4j.core.Dispatcher newDispatcher(int size, + RejectedExecutionHandler rejectedExecutionHandler, String prefix) { + if (size > 0) { + return new PoolDispatcher(size, 60, TimeUnit.SECONDS, rejectedExecutionHandler, prefix); + } else { + return null; + } + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/LinkedTransferQueue.java b/src/main/java/com/google/code/yanf4j/util/LinkedTransferQueue.java new file mode 100644 index 0000000..4399ffc --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/LinkedTransferQueue.java @@ -0,0 +1,780 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU General Public License + * Version 2 only ("GPL") or the Common Development and Distribution License("CDDL") (collectively, + * the "License"). You may not use this file except in compliance with the License. You can obtain a + * copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html or + * glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific language governing + * permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each file and include the + * License file at glassfish/bootstrap/legal/LICENSE.txt. Sun designates this particular file as + * subject to the "Classpath" exception as provided by Sun in the GPL Version 2 section of the + * License file that accompanied this code. If applicable, add the following below the License + * Header, with the fields enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * If you wish your version of this file to be governed by only the CDDL or only the GPL Version 2, + * indicate your decision by adding "[Contributor] elects to include this software in this + * distribution under the [CDDL or GPL Version 2] license." If you don't indicate a single choice of + * license, a recipient has the option to distribute your version of this file under either the + * CDDL, the GPL Version 2 or to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL Version 2 license, then the + * option applies only if the new code is made subject to such option by the copyright holder. + */ + +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the + * public domain, as explained at http://creativecommons.org/licenses/publicdomain + */ + +package com.google.code.yanf4j.util; + +import java.util.AbstractQueue; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.locks.LockSupport; + +/** + * An unbounded TransferQueue based on linked nodes. This queue orders elements FIFO + * (first-in-first-out) with respect to any given producer. The head of the queue is that + * element that has been on the queue the longest time for some producer. The tail of the + * queue is that element that has been on the queue the shortest time for some producer. + * + *

+ * Beware that, unlike in most collections, the size method is NOT a constant-time + * operation. Because of the asynchronous nature of these queues, determining the current number of + * elements requires a traversal of the elements. + * + *

+ * This class and its iterator implement all of the optional methods of the + * {@link Collection} and {@link Iterator} interfaces. + * + *

+ * Memory consistency effects: As with other concurrent collections, actions in a thread prior to + * placing an object into a {@code LinkedTransferQueue} + * happen-before actions subsequent to + * the access or removal of that element from the {@code LinkedTransferQueue} in another thread. + * + * @author Doug Lea + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Trustin Lee (tlee@redhat.com) + * + * @param the type of elements held in this collection + * + */ +public class LinkedTransferQueue extends AbstractQueue implements BlockingQueue { + + /* + * This class extends the approach used in FIFO-mode SynchronousQueues. See the internal + * documentation, as well as the PPoPP 2006 paper "Scalable Synchronous Queues" by Scherer, Lea & + * Scott (http://www.cs.rice.edu/~wns1/papers/2006-PPoPP-SQ.pdf) + * + * The main extension is to provide different Wait modes for the main "xfer" method that puts or + * takes items. These don't impact the basic dual-queue logic, but instead control whether or how + * threads block upon insertion of request or data nodes into the dual queue. It also uses + * slightly different conventions for tracking whether nodes are off-list or cancelled. + */ + + // Wait modes for xfer method + private static final int NOWAIT = 0; + private static final int TIMEOUT = 1; + private static final int WAIT = 2; + + /** The number of CPUs, for spin control */ + private static final int NCPUS = Runtime.getRuntime().availableProcessors(); + + /** + * The number of times to spin before blocking in timed waits. The value is empirically derived -- + * it works well across a variety of processors and OSes. Empirically, the best value seems not to + * vary with number of CPUs (beyond 2) so is just a constant. + */ + private static final int maxTimedSpins = NCPUS < 2 ? 0 : 32; + + /** + * The number of times to spin before blocking in untimed waits. This is greater than timed value + * because untimed waits spin faster since they don't need to check times on each spin. + */ + private static final int maxUntimedSpins = maxTimedSpins * 16; + + /** + * The number of nanoseconds for which it is faster to spin rather than to use timed park. A rough + * estimate suffices. + */ + private static final long spinForTimeoutThreshold = 1000L; + + /** + * Node class for LinkedTransferQueue. Opportunistically subclasses from AtomicReference to + * represent item. Uses Object, not E, to allow setting item to "this" after use, to avoid garbage + * retention. Similarly, setting the next field to this is used as sentinel that node is off list. + */ + private static final class QNode extends AtomicReference { + private static final long serialVersionUID = 5925596372370723938L; + + transient volatile QNode next; + transient volatile Thread waiter; // to control park/unpark + final boolean isData; + public long p1, p2, p3, p4, p5, p6, p7; + + QNode(Object item, boolean isData) { + super(item); + this.isData = isData; + } + + private static final AtomicReferenceFieldUpdater nextUpdater; + static { + AtomicReferenceFieldUpdater tmp = null; + try { + tmp = AtomicReferenceFieldUpdater.newUpdater(QNode.class, QNode.class, "next"); + + // Test if AtomicReferenceFieldUpdater is really working. + QNode testNode = new QNode(null, false); + tmp.set(testNode, testNode); + if (testNode.next != testNode) { + // Not set as expected - fall back to the safe mode. + throw new Exception(); + } + } catch (Throwable t) { + // Running in a restricted environment with a security manager. + tmp = null; + } + nextUpdater = tmp; + } + + boolean casNext(QNode cmp, QNode val) { + if (nextUpdater != null) { + return nextUpdater.compareAndSet(this, cmp, val); + } else { + return alternativeCasNext(cmp, val); + } + } + + private synchronized boolean alternativeCasNext(QNode cmp, QNode val) { + if (this.next == cmp) { + this.next = val; + return true; + } else { + return false; + } + } + } + + /** + * Padded version of AtomicReference used for head, tail and cleanMe, to alleviate contention + * across threads CASing one vs the other. + */ + public static final class PaddedAtomicReference extends AtomicReference { + private static final long serialVersionUID = 4684288940772921317L; + + // enough padding for 64bytes with 4byte refs + @SuppressWarnings("unused") + public Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; + + public PaddedAtomicReference(T r) { + super(r); + } + } + + /** head of the queue */ + private final PaddedAtomicReference head; + /** tail of the queue */ + private final PaddedAtomicReference tail; + + /** + * Reference to a cancelled node that might not yet have been unlinked from queue because it was + * the last inserted node when it cancelled. + */ + private final PaddedAtomicReference cleanMe; + + /** + * Tries to cas nh as new head; if successful, unlink old head's next node to avoid garbage + * retention. + */ + private boolean advanceHead(QNode h, QNode nh) { + if (h == this.head.get() && this.head.compareAndSet(h, nh)) { + h.next = h; // forget old next + return true; + } + return false; + } + + /** + * Puts or takes an item. Used for most queue operations (except poll() and tryTransfer()). See + * the similar code in SynchronousQueue for detailed explanation. + * + * @param e the item or if null, signifies that this is a take + * @param mode the wait mode: NOWAIT, TIMEOUT, WAIT + * @param nanos timeout in nanosecs, used only if mode is TIMEOUT + * @return an item, or null on failure + */ + private Object xfer(Object e, int mode, long nanos) { + boolean isData = e != null; + QNode s = null; + final PaddedAtomicReference head = this.head; + final PaddedAtomicReference tail = this.tail; + + for (;;) { + QNode t = tail.get(); + QNode h = head.get(); + + if (t != null && (t == h || t.isData == isData)) { + if (s == null) { + s = new QNode(e, isData); + } + QNode last = t.next; + if (last != null) { + if (t == tail.get()) { + tail.compareAndSet(t, last); + } + } else if (t.casNext(null, s)) { + tail.compareAndSet(t, s); + return awaitFulfill(t, s, e, mode, nanos); + } + } + + else if (h != null) { + QNode first = h.next; + if (t == tail.get() && first != null && advanceHead(h, first)) { + Object x = first.get(); + if (x != first && first.compareAndSet(x, e)) { + LockSupport.unpark(first.waiter); + return isData ? e : x; + } + } + } + } + } + + /** + * Version of xfer for poll() and tryTransfer, which simplifies control paths both here and in + * xfer + */ + private Object fulfill(Object e) { + boolean isData = e != null; + final PaddedAtomicReference head = this.head; + final PaddedAtomicReference tail = this.tail; + + for (;;) { + QNode t = tail.get(); + QNode h = head.get(); + + if (t != null && (t == h || t.isData == isData)) { + QNode last = t.next; + if (t == tail.get()) { + if (last != null) { + tail.compareAndSet(t, last); + } else { + return null; + } + } + } else if (h != null) { + QNode first = h.next; + if (t == tail.get() && first != null && advanceHead(h, first)) { + Object x = first.get(); + if (x != first && first.compareAndSet(x, e)) { + LockSupport.unpark(first.waiter); + return isData ? e : x; + } + } + } + } + } + + /** + * Spins/blocks until node s is fulfilled or caller gives up, depending on wait mode. + * + * @param pred the predecessor of waiting node + * @param s the waiting node + * @param e the comparison value for checking match + * @param mode mode + * @param nanos timeout value + * @return matched item, or s if cancelled + */ + private Object awaitFulfill(QNode pred, QNode s, Object e, int mode, long nanos) { + if (mode == NOWAIT) { + return null; + } + + long lastTime = mode == TIMEOUT ? System.nanoTime() : 0; + Thread w = Thread.currentThread(); + int spins = -1; // set to desired spin count below + for (;;) { + if (w.isInterrupted()) { + s.compareAndSet(e, s); + } + Object x = s.get(); + if (x != e) { // Node was matched or cancelled + advanceHead(pred, s); // unlink if head + if (x == s) { // was cancelled + clean(pred, s); + return null; + } else if (x != null) { + s.set(s); // avoid garbage retention + return x; + } else { + return e; + } + } + if (mode == TIMEOUT) { + long now = System.nanoTime(); + nanos -= now - lastTime; + lastTime = now; + if (nanos <= 0) { + s.compareAndSet(e, s); // try to cancel + continue; + } + } + if (spins < 0) { + QNode h = this.head.get(); // only spin if at head + spins = h != null && h.next == s ? (mode == TIMEOUT ? maxTimedSpins : maxUntimedSpins) : 0; + } + if (spins > 0) { + --spins; + } else if (s.waiter == null) { + s.waiter = w; + } else if (mode != TIMEOUT) { + // LockSupport.park(this); + LockSupport.park(); // allows run on java5 + s.waiter = null; + spins = -1; + } else if (nanos > spinForTimeoutThreshold) { + // LockSupport.parkNanos(this, nanos); + LockSupport.parkNanos(nanos); + s.waiter = null; + spins = -1; + } + } + } + + /** + * Returns validated tail for use in cleaning methods + */ + private QNode getValidatedTail() { + for (;;) { + QNode h = this.head.get(); + QNode first = h.next; + if (first != null && first.next == first) { // help advance + advanceHead(h, first); + continue; + } + QNode t = this.tail.get(); + QNode last = t.next; + if (t == this.tail.get()) { + if (last != null) { + this.tail.compareAndSet(t, last); // help advance + } else { + return t; + } + } + } + } + + /** + * Gets rid of cancelled node s with original predecessor pred. + * + * @param pred predecessor of cancelled node + * @param s the cancelled node + */ + void clean(QNode pred, QNode s) { + Thread w = s.waiter; + if (w != null) { // Wake up thread + s.waiter = null; + if (w != Thread.currentThread()) { + LockSupport.unpark(w); + } + } + /* + * At any given time, exactly one node on list cannot be deleted -- the last inserted node. To + * accommodate this, if we cannot delete s, we save its predecessor as "cleanMe", processing the + * previously saved version first. At least one of node s or the node previously saved can + * always be processed, so this always terminates. + */ + while (pred.next == s) { + QNode oldpred = reclean(); // First, help get rid of cleanMe + QNode t = getValidatedTail(); + if (s != t) { // If not tail, try to unsplice + QNode sn = s.next; // s.next == s means s already off list + if (sn == s || pred.casNext(s, sn)) { + break; + } + } else if (oldpred == pred || // Already saved + oldpred == null && this.cleanMe.compareAndSet(null, pred)) { + break; // Postpone cleaning + } + } + } + + /** + * Tries to unsplice the cancelled node held in cleanMe that was previously uncleanable because it + * was at tail. + * + * @return current cleanMe node (or null) + */ + private QNode reclean() { + /* + * cleanMe is, or at one time was, predecessor of cancelled node s that was the tail so could + * not be unspliced. If s is no longer the tail, try to unsplice if necessary and make cleanMe + * slot available. This differs from similar code in clean() because we must check that pred + * still points to a cancelled node that must be unspliced -- if not, we can (must) clear + * cleanMe without unsplicing. This can loop only due to contention on casNext or clearing + * cleanMe. + */ + QNode pred; + while ((pred = this.cleanMe.get()) != null) { + QNode t = getValidatedTail(); + QNode s = pred.next; + if (s != t) { + QNode sn; + if (s == null || s == pred || s.get() != s || (sn = s.next) == s || pred.casNext(s, sn)) { + this.cleanMe.compareAndSet(pred, null); + } + } else { + break; + } + } + return pred; + } + + @SuppressWarnings("unchecked") + E cast(Object e) { + return (E) e; + } + + /** + * Creates an initially empty LinkedTransferQueue. + */ + public LinkedTransferQueue() { + QNode dummy = new QNode(null, false); + this.head = new PaddedAtomicReference(dummy); + this.tail = new PaddedAtomicReference(dummy); + this.cleanMe = new PaddedAtomicReference(null); + } + + /** + * Creates a LinkedTransferQueue initially containing the elements of the given + * collection, added in traversal order of the collection's iterator. + * + * @param c the collection of elements to initially contain + * @throws NullPointerException if the specified collection or any of its elements are null + */ + public LinkedTransferQueue(Collection c) { + this(); + addAll(c); + } + + public void put(E e) throws InterruptedException { + if (e == null) { + throw new NullPointerException(); + } + if (Thread.interrupted()) { + throw new InterruptedException(); + } + xfer(e, NOWAIT, 0); + } + + public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { + if (e == null) { + throw new NullPointerException(); + } + if (Thread.interrupted()) { + throw new InterruptedException(); + } + xfer(e, NOWAIT, 0); + return true; + } + + public boolean offer(E e) { + if (e == null) { + throw new NullPointerException(); + } + xfer(e, NOWAIT, 0); + return true; + } + + public void transfer(E e) throws InterruptedException { + if (e == null) { + throw new NullPointerException(); + } + if (xfer(e, WAIT, 0) == null) { + Thread.interrupted(); + throw new InterruptedException(); + } + } + + public boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException { + if (e == null) { + throw new NullPointerException(); + } + if (xfer(e, TIMEOUT, unit.toNanos(timeout)) != null) { + return true; + } + if (!Thread.interrupted()) { + return false; + } + throw new InterruptedException(); + } + + public boolean tryTransfer(E e) { + if (e == null) { + throw new NullPointerException(); + } + return fulfill(e) != null; + } + + public E take() throws InterruptedException { + Object e = xfer(null, WAIT, 0); + if (e != null) { + return cast(e); + } + Thread.interrupted(); + throw new InterruptedException(); + } + + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + Object e = xfer(null, TIMEOUT, unit.toNanos(timeout)); + if (e != null || !Thread.interrupted()) { + return cast(e); + } + throw new InterruptedException(); + } + + public E poll() { + return cast(fulfill(null)); + } + + public int drainTo(Collection c) { + if (c == null) { + throw new NullPointerException(); + } + if (c == this) { + throw new IllegalArgumentException(); + } + int n = 0; + E e; + while ((e = poll()) != null) { + c.add(e); + ++n; + } + return n; + } + + public int drainTo(Collection c, int maxElements) { + if (c == null) { + throw new NullPointerException(); + } + if (c == this) { + throw new IllegalArgumentException(); + } + int n = 0; + E e; + while (n < maxElements && (e = poll()) != null) { + c.add(e); + ++n; + } + return n; + } + + // Traversal-based methods + + /** + * Return head after performing any outstanding helping steps + */ + QNode traversalHead() { + for (;;) { + QNode t = this.tail.get(); + QNode h = this.head.get(); + if (h != null && t != null) { + QNode last = t.next; + QNode first = h.next; + if (t == this.tail.get()) { + if (last != null) { + this.tail.compareAndSet(t, last); + } else if (first != null) { + Object x = first.get(); + if (x == first) { + advanceHead(h, first); + } else { + return h; + } + } else { + return h; + } + } + } + } + } + + @Override + public Iterator iterator() { + return new Itr(); + } + + /** + * Iterators. Basic strategy is to traverse list, treating non-data (i.e., request) nodes as + * terminating list. Once a valid data node is found, the item is cached so that the next call to + * next() will return it even if subsequently removed. + */ + class Itr implements Iterator { + QNode nextNode; // Next node to return next + QNode currentNode; // last returned node, for remove() + QNode prevNode; // predecessor of last returned node + E nextItem; // Cache of next item, once commited to in next + + Itr() { + this.nextNode = traversalHead(); + advance(); + } + + E advance() { + this.prevNode = this.currentNode; + this.currentNode = this.nextNode; + E x = this.nextItem; + + QNode p = this.nextNode.next; + for (;;) { + if (p == null || !p.isData) { + this.nextNode = null; + this.nextItem = null; + return x; + } + Object item = p.get(); + if (item != p && item != null) { + this.nextNode = p; + this.nextItem = cast(item); + return x; + } + this.prevNode = p; + p = p.next; + } + } + + public boolean hasNext() { + return this.nextNode != null; + } + + public E next() { + if (this.nextNode == null) { + throw new NoSuchElementException(); + } + return advance(); + } + + public void remove() { + QNode p = this.currentNode; + QNode prev = this.prevNode; + if (prev == null || p == null) { + throw new IllegalStateException(); + } + Object x = p.get(); + if (x != null && x != p && p.compareAndSet(x, p)) { + clean(prev, p); + } + } + } + + public E peek() { + for (;;) { + QNode h = traversalHead(); + QNode p = h.next; + if (p == null) { + return null; + } + Object x = p.get(); + if (p != x) { + if (!p.isData) { + return null; + } + if (x != null) { + return cast(x); + } + } + } + } + + @Override + public boolean isEmpty() { + for (;;) { + QNode h = traversalHead(); + QNode p = h.next; + if (p == null) { + return true; + } + Object x = p.get(); + if (p != x) { + if (!p.isData) { + return true; + } + if (x != null) { + return false; + } + } + } + } + + public boolean hasWaitingConsumer() { + for (;;) { + QNode h = traversalHead(); + QNode p = h.next; + if (p == null) { + return false; + } + Object x = p.get(); + if (p != x) { + return !p.isData; + } + } + } + + /** + * Returns the number of elements in this queue. If this queue contains more than + * Integer.MAX_VALUE elements, returns Integer.MAX_VALUE. + * + *

+ * Beware that, unlike in most collections, this method is NOT a constant-time operation. + * Because of the asynchronous nature of these queues, determining the current number of elements + * requires an O(n) traversal. + * + * @return the number of elements in this queue + */ + @Override + public int size() { + int count = 0; + QNode h = traversalHead(); + for (QNode p = h.next; p != null && p.isData; p = p.next) { + Object x = p.get(); + if (x != null && x != p) { + if (++count == Integer.MAX_VALUE) { + break; + } + } + } + return count; + } + + public int getWaitingConsumerCount() { + int count = 0; + QNode h = traversalHead(); + for (QNode p = h.next; p != null && !p.isData; p = p.next) { + if (p.get() == null) { + if (++count == Integer.MAX_VALUE) { + break; + } + } + } + return count; + } + + public int remainingCapacity() { + return Integer.MAX_VALUE; + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/MapBackedSet.java b/src/main/java/com/google/code/yanf4j/util/MapBackedSet.java new file mode 100644 index 0000000..8200a95 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/MapBackedSet.java @@ -0,0 +1,60 @@ +package com.google.code.yanf4j.util; + +import java.io.Serializable; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * A {@link Map}-backed {@link Set}. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 597692 $, $Date: 2007-11-23 08:56:32 -0700 (Fri, 23 Nov 2007) $ + */ +public class MapBackedSet extends AbstractSet implements Serializable { + + private static final long serialVersionUID = -8347878570391674042L; + + protected final Map map; + + public MapBackedSet(Map map) { + this.map = map; + } + + public MapBackedSet(Map map, Collection c) { + this.map = map; + addAll(c); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean contains(Object o) { + return map.containsKey(o); + } + + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } + + @Override + public boolean add(E o) { + return map.put(o, Boolean.TRUE) == null; + } + + @Override + public boolean remove(Object o) { + return map.remove(o) != null; + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/PropertyUtils.java b/src/main/java/com/google/code/yanf4j/util/PropertyUtils.java new file mode 100644 index 0000000..935d52e --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/PropertyUtils.java @@ -0,0 +1,52 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.util.Properties; + +/** + * java.util.Property utils + * + * @author dennis + * + */ +public class PropertyUtils { + + public static int getPropertyAsInteger(Properties props, String propName) { + return Integer.parseInt(PropertyUtils.getProperty(props, propName)); + } + + public static String getProperty(Properties props, String name) { + return props.getProperty(name).trim(); + } + + public static boolean getPropertyAsBoolean(Properties props, String name) { + return Boolean.valueOf(getProperty(props, name)); + } + + public static long getPropertyAsLong(Properties props, String name) { + return Long.parseLong(getProperty(props, name)); + } + + public static short getPropertyAsShort(Properties props, String name) { + return Short.parseShort(getProperty(props, name)); + } + + public static byte getPropertyAsByte(Properties props, String name) { + return Byte.parseByte(getProperty(props, name)); + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/ResourcesUtils.java b/src/main/java/com/google/code/yanf4j/util/ResourcesUtils.java new file mode 100644 index 0000000..63d4a90 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/ResourcesUtils.java @@ -0,0 +1,208 @@ +/** + * Copyright [2008] [dennis zhuang] 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.google.code.yanf4j.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.Properties; + +/** + * Resource utils + * + * @author dennis + * + */ +public class ResourcesUtils extends Object { + + /** */ + /** + * Returns the URL of the resource on the classpath + * + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static URL getResourceURL(String resource) throws IOException { + URL url = null; + ClassLoader loader = ResourcesUtils.class.getClassLoader(); + if (loader != null) { + url = loader.getResource(resource); + } + if (url == null) { + url = ClassLoader.getSystemResource(resource); + } + if (url == null) { + throw new IOException("Could not find resource " + resource); + } + return url; + } + + /** */ + /** + * Returns the URL of the resource on the classpath + * + * @param loader The classloader used to load the resource + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static URL getResourceURL(ClassLoader loader, String resource) throws IOException { + URL url = null; + if (loader != null) { + url = loader.getResource(resource); + } + if (url == null) { + url = ClassLoader.getSystemResource(resource); + } + if (url == null) { + throw new IOException("Could not find resource " + resource); + } + return url; + } + + /** */ + /** + * Returns a resource on the classpath as a Stream object + * + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static InputStream getResourceAsStream(String resource) throws IOException { + InputStream in = null; + ClassLoader loader = ResourcesUtils.class.getClassLoader(); + if (loader != null) { + in = loader.getResourceAsStream(resource); + } + if (in == null) { + in = ClassLoader.getSystemResourceAsStream(resource); + } + if (in == null) { + throw new IOException("Could not find resource " + resource); + } + return in; + } + + /** */ + /** + * Returns a resource on the classpath as a Stream object + * + * @param loader The classloader used to load the resource + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static InputStream getResourceAsStream(ClassLoader loader, String resource) + throws IOException { + InputStream in = null; + if (loader != null) { + in = loader.getResourceAsStream(resource); + } + if (in == null) { + in = ClassLoader.getSystemResourceAsStream(resource); + } + if (in == null) { + throw new IOException("Could not find resource " + resource); + } + return in; + } + + /** */ + /** + * Returns a resource on the classpath as a Properties object + * + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static Properties getResourceAsProperties(String resource) throws IOException { + Properties props = new Properties(); + InputStream in = null; + String propfile = resource; + in = getResourceAsStream(propfile); + props.load(in); + in.close(); + return props; + } + + /** */ + /** + * Returns a resource on the classpath as a Properties object + * + * @param loader The classloader used to load the resource + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static Properties getResourceAsProperties(ClassLoader loader, String resource) + throws IOException { + Properties props = new Properties(); + InputStream in = null; + String propfile = resource; + in = getResourceAsStream(loader, propfile); + props.load(in); + in.close(); + return props; + } + + /** */ + /** + * Returns a resource on the classpath as a Reader object + * + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static InputStreamReader getResourceAsReader(String resource) throws IOException { + return new InputStreamReader(getResourceAsStream(resource)); + } + + /** */ + /** + * Returns a resource on the classpath as a Reader object + * + * @param loader The classloader used to load the resource + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static Reader getResourceAsReader(ClassLoader loader, String resource) throws IOException { + return new InputStreamReader(getResourceAsStream(loader, resource)); + } + + /** */ + /** + * Returns a resource on the classpath as a File object + * + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static File getResourceAsFile(String resource) throws IOException { + return new File(getResourceURL(resource).getFile()); + } + + /** */ + /** + * Returns a resource on the classpath as a File object + * + * @param loader The classloader used to load the resource + * @param resource The resource to find + * @throws IOException If the resource cannot be found or read + * @return The resource + */ + public static File getResourceAsFile(ClassLoader loader, String resource) throws IOException { + return new File(getResourceURL(loader, resource).getFile()); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/util/SelectorFactory.java b/src/main/java/com/google/code/yanf4j/util/SelectorFactory.java new file mode 100644 index 0000000..cd8076f --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/SelectorFactory.java @@ -0,0 +1,158 @@ +/* + * The contents of this file are subject to the terms of the Common Development and Distribution + * License (the License). You may not use this file except in compliance with the License. + * + * You can obtain a copy of the license at https://glassfish.dev.java.net/public/CDDLv1.0.html or + * glassfish/bootstrap/legal/CDDLv1.0.txt. See the License for the specific language governing + * permissions and limitations under the License. + * + * When distributing Covered Code, include this CDDL Header Notice in each file and include the + * License file at glassfish/bootstrap/legal/CDDLv1.0.txt. If applicable, add the following below + * the CDDL Header, with the fields enclosed by brackets [] replaced by you own identifying + * information: "Portions Copyrighted [year] [name of copyright owner]" + * + * Copyright 2007 Sun Microsystems, Inc. All rights reserved. + */ +package com.google.code.yanf4j.util; + +import java.io.IOException; +import java.nio.channels.Selector; +import java.util.EmptyStackException; +import java.util.Stack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Temp selector factory,come from grizzly + * + * @author dennis zhuang + */ +public class SelectorFactory { + + public static final int DEFAULT_MAX_SELECTORS = 20; + + private static final Logger logger = LoggerFactory.getLogger(SelectorFactory.class); + /** + * The timeout before we exit. + */ + public final static long timeout = 5000; + + /** + * The number of Selector to create. + */ + private static int maxSelectors; + + /** + * Cache of Selector + */ + private final static Stack selectors = new Stack(); + + /** + * Creates the Selector + */ + static { + try { + setMaxSelectors(DEFAULT_MAX_SELECTORS); + } catch (IOException ex) { + logger.warn("SelectorFactory initializing Selector pool", ex); + } + } + + /** + * Set max selector pool size. + * + * @param size max pool size + */ + public final static void setMaxSelectors(int size) throws IOException { + synchronized (selectors) { + if (size < maxSelectors) { + reduce(size); + } else if (size > maxSelectors) { + grow(size); + } + + maxSelectors = size; + } + } + + /** + * Returns max selector pool size + * + * @return max pool size + */ + public final static int getMaxSelectors() { + return maxSelectors; + } + + /** + * Get a exclusive Selector + * + * @return Selector + */ + public final static Selector getSelector() { + synchronized (selectors) { + Selector s = null; + try { + if (selectors.size() != 0) { + s = selectors.pop(); + } + } catch (EmptyStackException ex) { + } + + int attempts = 0; + try { + while (s == null && attempts < 2) { + selectors.wait(timeout); + try { + if (selectors.size() != 0) { + s = selectors.pop(); + } + } catch (EmptyStackException ex) { + break; + } + attempts++; + } + } catch (InterruptedException ex) { + } + return s; + } + } + + /** + * Return the Selector to the cache + * + * @param s Selector + */ + public final static void returnSelector(Selector s) { + synchronized (selectors) { + selectors.push(s); + if (selectors.size() == 1) { + selectors.notify(); + } + } + } + + /** + * Increase Selector pool size + */ + private static void grow(int size) throws IOException { + for (int i = 0; i < size - maxSelectors; i++) { + selectors.add(Selector.open()); + } + } + + /** + * Decrease Selector pool size + */ + private static void reduce(int size) { + for (int i = 0; i < maxSelectors - size; i++) { + try { + Selector selector = selectors.pop(); + selector.close(); + } catch (IOException e) { + logger.error("SelectorFactory.reduce", e); + } + } + } + +} diff --git a/src/main/java/com/google/code/yanf4j/util/ShiftAndByteBufferMatcher.java b/src/main/java/com/google/code/yanf4j/util/ShiftAndByteBufferMatcher.java new file mode 100644 index 0000000..07b284c --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/ShiftAndByteBufferMatcher.java @@ -0,0 +1,94 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.util.ArrayList; +import java.util.List; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * ByteBuffer matcher based on shift-and algorithm + * + * @author dennis + * + */ +public class ShiftAndByteBufferMatcher implements ByteBufferMatcher { + + private int[] b; + private int mask; + + private int patternLimit; + private int patternPos; + private int patternLen; + + public ShiftAndByteBufferMatcher(IoBuffer pat) { + if (pat == null || pat.remaining() == 0) { + throw new IllegalArgumentException("blank buffer"); + } + this.patternLimit = pat.limit(); + this.patternPos = pat.position(); + this.patternLen = pat.remaining(); + preprocess(pat); + this.mask = 1 << (this.patternLen - 1); + } + + /** + * Ԥ���� + * + * @param pat + */ + private void preprocess(IoBuffer pat) { + this.b = new int[256]; + for (int i = this.patternPos; i < this.patternLimit; i++) { + int p = ByteBufferUtils.uByte(pat.get(i)); + this.b[p] = this.b[p] | (1 << i); + } + } + + public final List matchAll(IoBuffer buffer) { + List matches = new ArrayList(); + int bufferLimit = buffer.limit(); + int d = 0; + for (int pos = buffer.position(); pos < bufferLimit; pos++) { + d <<= 1; + d |= 1; + d &= this.b[ByteBufferUtils.uByte(buffer.get(pos))]; + if ((d & this.mask) != 0) { + matches.add(pos - this.patternLen + 1); + } + } + return matches; + } + + public final int matchFirst(IoBuffer buffer) { + if (buffer == null) { + return -1; + } + int bufferLimit = buffer.limit(); + int d = 0; + for (int pos = buffer.position(); pos < bufferLimit; pos++) { + d <<= 1; + d |= 1; + d &= this.b[buffer.get(pos) & 0XFF]; + if ((d & this.mask) != 0) { + return pos - this.patternLen + 1; + } + } + return -1; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/util/ShiftOrByteBufferMatcher.java b/src/main/java/com/google/code/yanf4j/util/ShiftOrByteBufferMatcher.java new file mode 100644 index 0000000..94f737b --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/ShiftOrByteBufferMatcher.java @@ -0,0 +1,93 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.util.ArrayList; +import java.util.List; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * ByteBuffer matcher based on shift-or algorithm + * + * @author dennis + * + */ +public class ShiftOrByteBufferMatcher implements ByteBufferMatcher { + + private int[] b; + private int lim; + + private int patternLen; + + public ShiftOrByteBufferMatcher(IoBuffer pat) { + if (pat == null || pat.remaining() == 0) { + throw new IllegalArgumentException("blank buffer"); + } + this.patternLen = pat.remaining(); + preprocess(pat); + } + + /** + * Ԥ���� + * + * @param pat + */ + private void preprocess(IoBuffer pat) { + this.b = new int[256]; + this.lim = 0; + for (int i = 0; i < 256; i++) { + this.b[i] = ~0; + + } + for (int i = 0, j = 1; i < this.patternLen; i++, j <<= 1) { + this.b[ByteBufferUtils.uByte(pat.get(i))] &= ~j; + this.lim |= j; + } + this.lim = ~(this.lim >> 1); + + } + + public final List matchAll(IoBuffer buffer) { + List matches = new ArrayList(); + int bufferLimit = buffer.limit(); + int state = ~0; + for (int pos = buffer.position(); pos < bufferLimit; pos++) { + state <<= 1; + state |= this.b[ByteBufferUtils.uByte(buffer.get(pos))]; + if (state < this.lim) { + matches.add(pos - this.patternLen + 1); + } + } + return matches; + } + + public final int matchFirst(IoBuffer buffer) { + if (buffer == null) { + return -1; + } + int bufferLimit = buffer.limit(); + int state = ~0; + for (int pos = buffer.position(); pos < bufferLimit; pos++) { + state = (state <<= 1) | this.b[ByteBufferUtils.uByte(buffer.get(pos))]; + if (state < this.lim) { + return pos - this.patternLen + 1; + } + } + return -1; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/util/SimpleQueue.java b/src/main/java/com/google/code/yanf4j/util/SimpleQueue.java new file mode 100644 index 0000000..1759687 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/SimpleQueue.java @@ -0,0 +1,45 @@ +package com.google.code.yanf4j.util; + +import java.util.Iterator; +import java.util.LinkedList; + +/** + * Simple queue. All methods are thread-safe. + * + * @author dennis zhuang + */ +public class SimpleQueue extends java.util.AbstractQueue { + + protected final LinkedList list; + + public SimpleQueue(int initializeCapacity) { + this.list = new LinkedList(); + } + + public SimpleQueue() { + this(100); + } + + public synchronized boolean offer(T e) { + return this.list.add(e); + } + + public synchronized T peek() { + return this.list.peek(); + } + + public synchronized T poll() { + return this.list.poll(); + } + + @Override + public Iterator iterator() { + return this.list.iterator(); + } + + @Override + public int size() { + return this.list.size(); + } + +} diff --git a/src/main/java/com/google/code/yanf4j/util/SystemUtils.java b/src/main/java/com/google/code/yanf4j/util/SystemUtils.java new file mode 100644 index 0000000..422b3a3 --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/SystemUtils.java @@ -0,0 +1,149 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.channels.Selector; +import java.nio.channels.spi.SelectorProvider; +import java.util.Queue; +import java.util.Random; + +/** + * System utils + * + * @author dennis + * + */ +public final class SystemUtils { + + private SystemUtils() { + + } + + public static final String OS_NAME = System.getProperty("os.name"); + + private static boolean isLinuxPlatform = false; + + static { + if (OS_NAME != null && OS_NAME.toLowerCase().indexOf("linux") >= 0) { + isLinuxPlatform = true; + } + } + public static final String JAVA_VERSION = System.getProperty("java.version"); + private static boolean isAfterJava6u4Version = false; + static { + if (JAVA_VERSION != null) { + // java4 or java5 + if (JAVA_VERSION.indexOf("1.4.") >= 0 || JAVA_VERSION.indexOf("1.5.") >= 0) { + isAfterJava6u4Version = false; + } else if (JAVA_VERSION.indexOf("1.6.") >= 0) { + int index = JAVA_VERSION.indexOf("_"); + if (index > 0) { + String subVersionStr = JAVA_VERSION.substring(index + 1); + if (subVersionStr != null && subVersionStr.length() > 0) { + try { + int subVersion = Integer.parseInt(subVersionStr); + if (subVersion >= 4) { + isAfterJava6u4Version = true; + } + } catch (NumberFormatException e) { + + } + } + } + // after java6 + } else { + isAfterJava6u4Version = true; + } + } + } + + public static final boolean isLinuxPlatform() { + return isLinuxPlatform; + } + + public static final boolean isAfterJava6u4Version() { + return isAfterJava6u4Version; + } + + public static void main(String[] args) { + System.out.println(isAfterJava6u4Version()); + } + + public static final int getSystemThreadCount() { + int cpus = getCpuProcessorCount(); + if (cpus <= 8) { + return cpus; + } else { + return 8 + (cpus - 8) * 5 / 8; + } + } + + public static final int getCpuProcessorCount() { + return Runtime.getRuntime().availableProcessors(); + } + + public static final Selector openSelector() throws IOException { + Selector result = null; + // check if it is linux os + if (SystemUtils.isLinuxPlatform()) { + try { + Class providerClazz = Class.forName("sun.nio.ch.EPollSelectorProvider"); + if (providerClazz != null) { + try { + Method method = providerClazz.getMethod("provider"); + if (method != null) { + SelectorProvider selectorProvider = (SelectorProvider) method.invoke(null); + if (selectorProvider != null) { + result = selectorProvider.openSelector(); + } + } + } catch (Exception e) { + // ignore + } + } + } catch (Exception e) { + // ignore + } + } + if (result == null) { + result = Selector.open(); + } + return result; + + } + + public static final String getRawAddress(InetSocketAddress inetSocketAddress) { + InetAddress address = inetSocketAddress.getAddress(); + return address != null ? address.getHostAddress() : inetSocketAddress.getHostName(); + } + + public static final Queue createTransferQueue() { + try { + return (Queue) Class.forName("java.util.concurrent.LinkedTransferQueue").newInstance(); + } catch (Exception e) { + return new LinkedTransferQueue(); + } + } + + public static Random createRandom() { + return new Random(); + } +} diff --git a/src/main/java/com/google/code/yanf4j/util/TransferQueue.java b/src/main/java/com/google/code/yanf4j/util/TransferQueue.java new file mode 100644 index 0000000..e8bba8b --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/TransferQueue.java @@ -0,0 +1,101 @@ +/* + * Written by Doug Lea with assistance from members of JCP JSR-166 Expert Group and released to the + * public domain, as explained at http://creativecommons.org/licenses/publicdomain + */ +package com.google.code.yanf4j.util; + +import java.util.concurrent.*; + +/** + * A {@link BlockingQueue} in which producers may wait for consumers to receive elements. A + * {@code TransferQueue} may be useful for example in message passing applications in which + * producers sometimes (using method {@code transfer}) await receipt of elements by consumers + * invoking {@code take} or {@code poll}, while at other times enqueue elements (via method + * {@code put}) without waiting for receipt. Non-blocking and time-out versions of + * {@code tryTransfer} are also available. A TransferQueue may also be queried via + * {@code hasWaitingConsumer} whether there are any threads waiting for items, which is a converse + * analogy to a {@code peek} operation. + * + *

+ * Like any {@code BlockingQueue}, a {@code TransferQueue} may be capacity bounded. If so, an + * attempted {@code transfer} operation may initially block waiting for available space, and/or + * subsequently block waiting for reception by a consumer. Note that in a queue with zero capacity, + * such as {@link SynchronousQueue}, {@code put} and {@code transfer} are effectively synonymous. + * + *

+ * This interface is a member of the + * Java Collections Framework. + * + * @since 1.7 + * @author Doug Lea + * @param the type of elements held in this collection + */ +public interface TransferQueue extends BlockingQueue { + /** + * Transfers the specified element if there exists a consumer already waiting to receive it, + * otherwise returning {@code false} without enqueuing the element. + * + * @param e the element to transfer + * @return {@code true} if the element was transferred, else {@code false} + * @throws ClassCastException if the class of the specified element prevents it from being added + * to this queue + * @throws NullPointerException if the specified element is null + * @throws IllegalArgumentException if some property of the specified element prevents it from + * being added to this queue + */ + boolean tryTransfer(E e); + + /** + * Inserts the specified element into this queue, waiting if necessary for space to become + * available and the element to be dequeued by a consumer invoking {@code take} or {@code poll}. + * + * @param e the element to transfer + * @throws InterruptedException if interrupted while waiting, in which case the element is not + * enqueued. + * @throws ClassCastException if the class of the specified element prevents it from being added + * to this queue + * @throws NullPointerException if the specified element is null + * @throws IllegalArgumentException if some property of the specified element prevents it from + * being added to this queue + */ + void transfer(E e) throws InterruptedException; + + /** + * Inserts the specified element into this queue, waiting up to the specified wait time if + * necessary for space to become available and the element to be dequeued by a consumer invoking + * {@code take} or {@code poll}. + * + * @param e the element to transfer + * @param timeout how long to wait before giving up, in units of {@code unit} + * @param unit a {@code TimeUnit} determining how to interpret the {@code timeout} parameter + * @return {@code true} if successful, or {@code false} if the specified waiting time elapses + * before completion, in which case the element is not enqueued. + * @throws InterruptedException if interrupted while waiting, in which case the element is not + * enqueued. + * @throws ClassCastException if the class of the specified element prevents it from being added + * to this queue + * @throws NullPointerException if the specified element is null + * @throws IllegalArgumentException if some property of the specified element prevents it from + * being added to this queue + */ + boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedException; + + /** + * Returns {@code true} if there is at least one consumer waiting to dequeue an element via + * {@code take} or {@code poll}. The return value represents a momentary state of affairs. + * + * @return {@code true} if there is at least one waiting consumer + */ + boolean hasWaitingConsumer(); + + /** + * Returns an estimate of the number of consumers waiting to dequeue elements via {@code take} or + * {@code poll}. The return value is an approximation of a momentary state of affairs, that may be + * inaccurate if consumers have completed or given up waiting. The value may be useful for + * monitoring and heuristics, but not for synchronization control. Implementations of this method + * are likely to be noticeably slower than those for {@link #hasWaitingConsumer}. + * + * @return the number of consumers waiting to dequeue elements + */ + int getWaitingConsumerCount(); +} diff --git a/src/main/java/com/google/code/yanf4j/util/WorkerThreadFactory.java b/src/main/java/com/google/code/yanf4j/util/WorkerThreadFactory.java new file mode 100644 index 0000000..2046c0c --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/WorkerThreadFactory.java @@ -0,0 +1,65 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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.google.code.yanf4j.util; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Thread factory for worker thread + * + * @author dennis + * + */ +public class WorkerThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + public WorkerThreadFactory(ThreadGroup group, String prefix) { + if (group == null) { + SecurityManager s = System.getSecurityManager(); + this.group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + } else { + this.group = group; + } + if (prefix == null) { + this.namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; + } else { + this.namePrefix = prefix + "-" + poolNumber.getAndIncrement() + "-thread-"; + } + } + + public WorkerThreadFactory(String prefix) { + this(null, prefix); + } + + public WorkerThreadFactory() { + this(null, null); + } + + public Thread newThread(Runnable r) { + Thread t = new Thread(this.group, r, this.namePrefix + this.threadNumber.getAndIncrement(), 0); + t.setDaemon(true); + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + +} diff --git a/src/main/java/com/google/code/yanf4j/util/package.html b/src/main/java/com/google/code/yanf4j/util/package.html new file mode 100644 index 0000000..3ef728d --- /dev/null +++ b/src/main/java/com/google/code/yanf4j/util/package.html @@ -0,0 +1,10 @@ + + + + + Yanf4j utilities + + +

Yanf4j utilities

+ + \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..812d3ef --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,11 @@ +module com.googlecode.xmemcached { + requires org.slf4j; + requires static java.security.sasl; + requires static java.management; + requires static java.rmi; + requires static spring.beans; + requires static spring.context; + + exports net.rubyeye.xmemcached; + exports net.rubyeye.xmemcached.exception; +} diff --git a/src/main/java/net/rubyeye/xmemcached/CASOperation.java b/src/main/java/net/rubyeye/xmemcached/CASOperation.java new file mode 100644 index 0000000..19af7f7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/CASOperation.java @@ -0,0 +1,33 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +/** + * CAS operation,encapsulate gets and cas commands,and supports retry times. + * + * @author dennis + * + */ +public interface CASOperation { + /** + * Max retry times,If retry times is great than this value,xmemcached will throw TimeoutException + * + * @return + */ + public int getMaxTries(); + + /** + * Return the new value which you want to cas + * + * @param currentCAS + * @param currentValue + * @return expected new value + */ + public T getNewValue(long currentCAS, T currentValue); +} diff --git a/src/main/java/net/rubyeye/xmemcached/CommandFactory.java b/src/main/java/net/rubyeye/xmemcached/CommandFactory.java new file mode 100644 index 0000000..57ac4e9 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/CommandFactory.java @@ -0,0 +1,294 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.Protocol; + +@SuppressWarnings("unchecked") +public interface CommandFactory { + + /** + * set command factory's buffer allocator + * + * @since 1.2.0 + * @param bufferAllocator + */ + public void setBufferAllocator(BufferAllocator bufferAllocator); + + /** + * create a delete command + * + * @param key + * @param time + * @return + */ + public Command createDeleteCommand(final String key, final byte[] keyBytes, final int time, + long cas, boolean noreply); + + /** + * create a version command + * + * @return + */ + public Command createVersionCommand(CountDownLatch latch, InetSocketAddress server); + + /** + * create a flush_all command + * + * @return + */ + public Command createFlushAllCommand(CountDownLatch latch, int delay, boolean noreply); + + /** + * create a stats command + * + * @return + */ + public Command createStatsCommand(InetSocketAddress server, CountDownLatch latch, + String itemName); + + /** + * create a get/gets command + * + * @param key + * @param keyBytes + * @param cmdType 命令类型 + * @param transcoder TODO + * @param cmdBytes 命令的字节数组,如"get".getBytes() + * @return + */ + + public Command createGetCommand(final String key, final byte[] keyBytes, + final CommandType cmdType, Transcoder transcoder); + + /** + * Create a multi-get command + * + * @param + * @param keys + * @param latch + * @param result + * @param cmdBytes + * @param cmdType + * @param transcoder + * @return + */ + public Command createGetMultiCommand(Collection keys, CountDownLatch latch, + CommandType cmdType, Transcoder transcoder); + + /** + * create a incr/decr command + * + * @param key + * @param keyBytes + * @param delta + * @param initial + * @param expTime + * @param cmdType + * @param noreply + * @return + */ + public Command createIncrDecrCommand(final String key, final byte[] keyBytes, final long delta, + long initial, int expTime, CommandType cmdType, boolean noreply); + + /** + * Create a cas command + * + * @param key + * @param keyBytes + * @param exp + * @param value + * @param cas + * @param noreply + * @param transcoder + * @return + */ + public Command createCASCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, long cas, boolean noreply, Transcoder transcoder); + + /** + * Create a set command + * + * @param key + * @param keyBytes + * @param exp + * @param value + * @param noreply + * @param transcoder + * @return + */ + public Command createSetCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, boolean noreply, Transcoder transcoder); + + /** + * create a add command + * + * @param key + * @param keyBytes + * @param exp + * @param value + * @param noreply + * @param transcoder + * @return + */ + public Command createAddCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, boolean noreply, Transcoder transcoder); + + /** + * create a replace command + * + * @param key + * @param keyBytes + * @param exp + * @param value + * @param noreply + * @param transcoder + * @return + */ + public Command createReplaceCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, boolean noreply, Transcoder transcoder); + + /** + * create a append command + * + * @param key + * @param keyBytes + * @param value + * @param noreply + * @param transcoder + * @return + */ + public Command createAppendCommand(final String key, final byte[] keyBytes, final Object value, + boolean noreply, Transcoder transcoder); + + /** + * Create a prepend command + * + * @param key + * @param keyBytes + * @param value + * @param noreply + * @param transcoder + * @return + */ + public Command createPrependCommand(final String key, final byte[] keyBytes, final Object value, + boolean noreply, Transcoder transcoder); + + /** + * Create a verbosity command + * + * @param latch + * @param level + * @param noreply + * @return + */ + public Command createVerbosityCommand(CountDownLatch latch, int level, boolean noreply); + + /** + * Create a command for listing authentication mechanisms + * + * @param latch + * @return + */ + public Command createAuthListMechanismsCommand(CountDownLatch latch); + + /** + * Create command for starting authentication + * + * @param mechanism + * @param latch + * @param authData + * @return + */ + public Command createAuthStartCommand(String mechanism, CountDownLatch latch, byte[] authData); + + /** + * Create a command for stepping authentication + * + * @param mechanism + * @param latch + * @param authData + * @return + */ + public Command createAuthStepCommand(String mechanism, CountDownLatch latch, byte[] authData); + + /** + * create a quit command + * + * @return + */ + public Command createQuitCommand(); + + /** + * Create a touch command + * + * @since 1.3.3 + * @param key + * @param keyBytes + * @param latch TODO + * @param exp + * @param noreply + * @return + */ + public Command createTouchCommand(final String key, final byte[] keyBytes, CountDownLatch latch, + int exp, boolean noreply); + + /** + * Create a get-and-touch command + * + * @since 1.3.3 + * @param key + * @param keyBytes + * @param latch TODO + * @param exp + * @param noreply + * @return + */ + public Command createGetAndTouchCommand(final String key, final byte[] keyBytes, + CountDownLatch latch, int exp, boolean noreply); + + /** + * Create a Auto discovery config command, only supports Cache Engine Version 1.4.14 or Higher. + * This method works the same for both AWS and GCP Auto Discovery. + * + * @see Adding + * AWS Auto Discovery To Your Client Library + * @see Adding + * GCP Auto Discovery To Your Client Library + * @param subCommand + * @param key + * @return + */ + public Command createAutoDiscoveryCacheConfigCommand(String subCommand, String key); + + /** + * Get this client's protocol version + * + * @return + */ + public Protocol getProtocol(); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/Counter.java b/src/main/java/net/rubyeye/xmemcached/Counter.java new file mode 100644 index 0000000..913766e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/Counter.java @@ -0,0 +1,149 @@ +package net.rubyeye.xmemcached; + +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.exception.MemcachedClientException; +import net.rubyeye.xmemcached.exception.MemcachedException; + +/** + * Counter,encapsulate the incr/decr methods. + * + * @author dennis + * + */ +public final class Counter { + private final MemcachedClient memcachedClient; + private final String key; + private final long initialValue; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.key == null) ? 0 : this.key.hashCode()); + result = + prime * result + ((this.memcachedClient == null) ? 0 : this.memcachedClient.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Counter other = (Counter) obj; + if (this.key == null) { + if (other.key != null) { + return false; + } + } else if (!this.key.equals(other.key)) { + return false; + } + if (this.memcachedClient == null) { + if (other.memcachedClient != null) { + return false; + } + } else if (!this.memcachedClient.equals(other.memcachedClient)) { + return false; + } + return true; + } + + public final String getKey() { + return this.key; + } + + /** + * Get current value + * + * @return + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public long get() throws MemcachedException, InterruptedException, TimeoutException { + Object result = this.memcachedClient.get(this.key); + if (result == null) { + throw new MemcachedClientException("key is not existed."); + } else { + if (result instanceof Long) + return (Long) result; + else + return Long.valueOf(((String) result).trim()); + } + } + + /** + * Set counter's value to expected. + * + * @param value + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public void set(long value) throws MemcachedException, InterruptedException, TimeoutException { + this.memcachedClient.set(this.key, 0, String.valueOf(value)); + } + + public Counter(MemcachedClient memcachedClient, String key, long initialValue) { + super(); + this.memcachedClient = memcachedClient; + this.key = key; + this.initialValue = initialValue; + try { + this.memcachedClient.add(key, 0, String.valueOf(this.initialValue)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + throw new IllegalStateException("Initialize counter failed", e); + } + } + + /** + * Increase value by one + * + * @return + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public long incrementAndGet() throws MemcachedException, InterruptedException, TimeoutException { + return this.memcachedClient.incr(this.key, 1, this.initialValue); + } + + /** + * Decrease value by one + * + * @return + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public long decrementAndGet() throws MemcachedException, InterruptedException, TimeoutException { + return this.memcachedClient.decr(this.key, 1, this.initialValue); + } + + /** + * Add value and get the result + * + * @param delta + * @return + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public long addAndGet(long delta) + throws MemcachedException, InterruptedException, TimeoutException { + if (delta >= 0) { + return this.memcachedClient.incr(this.key, delta, this.initialValue); + } else { + return this.memcachedClient.decr(this.key, -delta, this.initialValue); + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/FlowControl.java b/src/main/java/net/rubyeye/xmemcached/FlowControl.java new file mode 100644 index 0000000..0fa971e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/FlowControl.java @@ -0,0 +1,37 @@ +package net.rubyeye.xmemcached; + +import java.util.concurrent.Semaphore; + +/** + * Flow control for noreply operations. + * + * @author dennis + * @since 1.3.8 + * + */ +public class FlowControl { + private Semaphore permits; + private int max; + + public FlowControl(int permits) { + super(); + this.max = permits; + this.permits = new Semaphore(permits); + } + + public int max() { + return this.max; + } + + public int permits() { + return this.permits.availablePermits(); + } + + public boolean aquire() { + return this.permits.tryAcquire(); + } + + public void release() { + this.permits.release(); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/GetsResponse.java b/src/main/java/net/rubyeye/xmemcached/GetsResponse.java new file mode 100644 index 0000000..396e414 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/GetsResponse.java @@ -0,0 +1,75 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +/** + * Response for gets command.It's a value object. + * + * @author dennis + * + */ +public final class GetsResponse { + private final long cas; + private final T value; + + public GetsResponse(final long cas, final T value) { + super(); + this.cas = cas; + this.value = value; + } + + public long getCas() { + return this.cas; + } + + public T getValue() { + return this.value; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (this.cas ^ (this.cas >>> 32)); + result = prime * result + ((this.value == null) ? 0 : this.value.hashCode()); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + GetsResponse other = (GetsResponse) obj; + if (this.cas != other.cas) { + return false; + } + if (this.value == null) { + if (other.value != null) { + return false; + } + } else if (!this.value.equals(other.value)) { + return false; + } + return true; + } + + @Override + public String toString() { + return "GetsResponse[cas=" + this.cas + ",value=" + this.value + "]"; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/HashAlgorithm.java b/src/main/java/net/rubyeye/xmemcached/HashAlgorithm.java new file mode 100644 index 0000000..e25f3a2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/HashAlgorithm.java @@ -0,0 +1,223 @@ +package net.rubyeye.xmemcached; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.zip.CRC32; +import net.rubyeye.xmemcached.exception.MemcachedClientException; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Known hashing algorithms for locating a server for a key. Note that all hash algorithms return + * 64-bits of hash, but only the lower 32-bits are significant. This allows a positive 32-bit number + * to be returned for all cases. + */ +public enum HashAlgorithm { + + /** + * Native hash (String.hashCode()). + */ + NATIVE_HASH, + /** + * CRC32_HASH as used by the perl API. This will be more consistent both across multiple API users + * as well as java versions, but is mostly likely significantly slower. + */ + CRC32_HASH, + /** + * FNV hashes are designed to be fast while maintaining a low collision rate. The FNV speed allows + * one to quickly hash lots of data while maintaining a reasonable collision rate. + * + * @see http://www.isthe.com/chongo/tech/comp/fnv/ + * @see http://en.wikipedia.org/wiki/Fowler_Noll_Vo_hash + */ + FNV1_64_HASH, + /** + * Variation of FNV. + */ + FNV1A_64_HASH, + /** + * 32-bit FNV1. + */ + FNV1_32_HASH, + /** + * 32-bit FNV1a. + */ + FNV1A_32_HASH, + /** + * MD5-based hash algorithm used by ketama. + */ + KETAMA_HASH, + + /** + * From mysql source + */ + MYSQL_HASH, + + ELF_HASH, + + RS_HASH, + + /** + * From lua source,it is used for long key + */ + LUA_HASH, + + ELECTION_HASH, + /** + * The Jenkins One-at-a-time hash ,please see http://www.burtleburtle.net/bob/hash/doobs.html + */ + ONE_AT_A_TIME; + + private static final long FNV_64_INIT = 0xcbf29ce484222325L; + private static final long FNV_64_PRIME = 0x100000001b3L; + + private static final long FNV_32_INIT = 2166136261L; + private static final long FNV_32_PRIME = 16777619; + + /** + * Compute the hash for the given key. + * + * @return a positive integer hash + */ + public long hash(final String k) { + long rv = 0; + switch (this) { + case NATIVE_HASH: + rv = k.hashCode(); + break; + case CRC32_HASH: + // return (crc32(shift) >> 16) & 0x7fff; + CRC32 crc32 = new CRC32(); + crc32.update(ByteUtils.getBytes(k)); + rv = crc32.getValue() >> 16 & 0x7fff; + break; + case FNV1_64_HASH: { + // Thanks to pierre@demartines.com for the pointer + rv = FNV_64_INIT; + int len = k.length(); + for (int i = 0; i < len; i++) { + rv *= FNV_64_PRIME; + rv ^= k.charAt(i); + } + } + break; + case FNV1A_64_HASH: { + rv = FNV_64_INIT; + int len = k.length(); + for (int i = 0; i < len; i++) { + rv ^= k.charAt(i); + rv *= FNV_64_PRIME; + } + } + break; + case FNV1_32_HASH: { + rv = FNV_32_INIT; + int len = k.length(); + for (int i = 0; i < len; i++) { + rv *= FNV_32_PRIME; + rv ^= k.charAt(i); + } + } + break; + case FNV1A_32_HASH: { + rv = FNV_32_INIT; + int len = k.length(); + for (int i = 0; i < len; i++) { + rv ^= k.charAt(i); + rv *= FNV_32_PRIME; + } + } + break; + case ELECTION_HASH: + case KETAMA_HASH: + byte[] bKey = computeMd5(k); + rv = (long) (bKey[3] & 0xFF) << 24 | (long) (bKey[2] & 0xFF) << 16 + | (long) (bKey[1] & 0xFF) << 8 | bKey[0] & 0xFF; + break; + + case MYSQL_HASH: + int nr2 = 4; + for (int i = 0; i < k.length(); i++) { + rv ^= ((rv & 63) + nr2) * k.charAt(i) + (rv << 8); + nr2 += 3; + } + break; + case ELF_HASH: + long x = 0; + for (int i = 0; i < k.length(); i++) { + rv = (rv << 4) + k.charAt(i); + if ((x = rv & 0xF0000000L) != 0) { + rv ^= x >> 24; + rv &= ~x; + } + } + rv = rv & 0x7FFFFFFF; + break; + case RS_HASH: + long b = 378551; + long a = 63689; + for (int i = 0; i < k.length(); i++) { + rv = rv * a + k.charAt(i); + a *= b; + } + rv = rv & 0x7FFFFFFF; + break; + case LUA_HASH: + int step = (k.length() >> 5) + 1; + rv = k.length(); + for (int len = k.length(); len >= step; len -= step) { + rv = rv ^ (rv << 5) + (rv >> 2) + k.charAt(len - 1); + } + break; + case ONE_AT_A_TIME: + try { + int hash = 0; + for (byte bt : k.getBytes("utf-8")) { + hash += (bt & 0xFF); + hash += (hash << 10); + hash ^= (hash >>> 6); + } + hash += (hash << 3); + hash ^= (hash >>> 11); + hash += (hash << 15); + rv = hash; + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Hash function error", e); + } + break; + default: + assert false; + } + + return rv & 0xffffffffL; /* Convert to unsigned 32-bits */ + } + + private static ThreadLocal md5Local = new ThreadLocal(); + + /** + * Get the md5 of the given key. + */ + public static byte[] computeMd5(String k) { + MessageDigest md5 = md5Local.get(); + if (md5 == null) { + try { + md5 = MessageDigest.getInstance("MD5"); + md5Local.set(md5); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + } + md5.reset(); + md5.update(ByteUtils.getBytes(k)); + return md5.digest(); + } + + // public static void main(String[] args) { + // HashAlgorithm alg=HashAlgorithm.CRC32_HASH; + // long h=0; + // long start=System.currentTimeMillis(); + // for(int i=0;i<100000;i++) + // h=alg.hash("MYSQL_HASH"); + // System.out.println(System.currentTimeMillis()-start); + // } +} diff --git a/src/main/java/net/rubyeye/xmemcached/KeyIterator.java b/src/main/java/net/rubyeye/xmemcached/KeyIterator.java new file mode 100644 index 0000000..cd3620d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/KeyIterator.java @@ -0,0 +1,61 @@ +package net.rubyeye.xmemcached; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.exception.MemcachedException; + +/** + * Key Iterator for memcached,use 'stats items' and 'stats cachedump' to iterate all keys,it is + * inefficient and not thread-safe.The 'stats cachedump" has length limitation,then iterator could + * not visit all keys if you have many keys.
+ *

+ * Note: memcached 1.6.x will remove cachedump stats,so this feature will be + * invalid in memcached 1.6.x + *

+ * + * @deprecated memcached 1.6.x will remove cachedump stats command,so this feature will be removed + * in the future + * + * @author dennis + * + */ +@Deprecated +public interface KeyIterator { + /** + * Get next key,if iterator has reached the end,throw ArrayIndexOutOfBoundsException + * + * @return + * @throws ArrayIndexOutOfBoundsException + * ,MemcachedException,TimeoutException,InterruptedException + * + */ + public String next() throws MemcachedException, TimeoutException, InterruptedException; + + /** + * Check if the iterator has more keys. + * + * @return + */ + public boolean hasNext(); + + /** + * Close this iterator when you don't need it any more.It is not mandatory to call this method, + * but you might want to invoke this method for maximum performance. + */ + public void close(); + + /** + * Get current iterator's memcached server address + * + * @return + */ + public InetSocketAddress getServerAddress(); + + /** + * Set operation timeout,default is 1000 MILLISECONDS. + * + * @param opTimeout + */ + public void setOpTimeout(long opTimeout); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/KeyProvider.java b/src/main/java/net/rubyeye/xmemcached/KeyProvider.java new file mode 100644 index 0000000..947df6f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/KeyProvider.java @@ -0,0 +1,17 @@ +package net.rubyeye.xmemcached; + +/** + * Key provider to pre-process keys before sending to memcached. + * + * @author dennis + * @since 1.3.8 + */ +public interface KeyProvider { + /** + * Processes key and returns a new key. + * + * @param key + * @return + */ + public String process(String key); +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedClient.java b/src/main/java/net/rubyeye/xmemcached/MemcachedClient.java new file mode 100644 index 0000000..ec771d9 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedClient.java @@ -0,0 +1,1754 @@ +package net.rubyeye.xmemcached; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.impl.ReconnectRequest; +import net.rubyeye.xmemcached.networking.Connector; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.Protocol; + +/** + * The memcached client's interface + * + * @author dennis + * + */ +public interface MemcachedClient { + + /** + * Default thread number for reading nio's receive buffer and dispatch commands.Recommend users to + * set it equal or less to the memcached server's number on linux platform,keep default on + * windows.Default is 0. + */ + public static final int DEFAULT_READ_THREAD_COUNT = 0; + + /** + * Default TCP keeplive option,which is true + */ + public static final boolean DEFAULT_TCP_KEEPLIVE = true; + /** + * Default connect timeout,1 minutes + */ + public static final int DEFAULT_CONNECT_TIMEOUT = 60000; + /** + * Default socket's send buffer size,8k + */ + public static final int DEFAULT_TCP_SEND_BUFF_SIZE = 32 * 1024; + /** + * Disable nagle algorithm by default + * + */ + public static final boolean DEFAULT_TCP_NO_DELAY = true; + /** + * Default session read buffer size,16k + */ + public static final int DEFAULT_SESSION_READ_BUFF_SIZE = 128 * 1024; + /** + * Default socket's receive buffer size,16k + */ + public static final int DEFAULT_TCP_RECV_BUFF_SIZE = 64 * 1024; + /** + * Default operation timeout,if the operation is not returned in 5 second,throw TimeoutException. + */ + public static final long DEFAULT_OP_TIMEOUT = 5000L; + /** + * With java nio,there is only one connection to a memcached.In a high concurrent enviroment,you + * may want to pool memcached clients.But a xmemcached client has to start a reactor thread and + * some thread pools,if you create too many clients,the cost is very large. Xmemcached supports + * connection pool instreadof client pool.you can create more connections to one or more memcached + * servers,and these connections share the same reactor and thread pools,it will reduce the cost + * of system.Default pool size is 1. + */ + public static final int DEFAULT_CONNECTION_POOL_SIZE = 1; + + /** + * Default session idle timeout,if session is idle,xmemcached will do a heartbeat action to check + * if connection is alive. + */ + public static final int DEFAULT_SESSION_IDLE_TIMEOUT = 5000; + + /** + * Default heal session interval in milliseconds. + */ + public static final long DEFAULT_HEAL_SESSION_INTERVAL = 2000; + + int MAX_QUEUED_NOPS = 40000; + int DYNAMIC_MAX_QUEUED_NOPS = + (int) (MAX_QUEUED_NOPS * (Runtime.getRuntime().maxMemory() / 1024.0 / 1024.0 / 1024.0)); + + /** + * Default max queued noreply operations number.It is calcuated dynamically based on your jvm + * maximum memory. + * + * @since 1.3.8 + */ + public static final int DEFAULT_MAX_QUEUED_NOPS = + DYNAMIC_MAX_QUEUED_NOPS > MAX_QUEUED_NOPS ? MAX_QUEUED_NOPS : DYNAMIC_MAX_QUEUED_NOPS; + + /** + * Maximum number of timeout exception for close connection. + * + * @since 1.4.0 + */ + public static final int DEFAULT_MAX_TIMEOUTEXCEPTION_THRESHOLD = 1000; + + /** + * Set the merge factor,this factor determins how many 'get' commands would be merge to one + * multi-get command.default is 150 + * + * @param mergeFactor + */ + public void setMergeFactor(final int mergeFactor); + + /** + * Get the connect timeout + * + */ + public long getConnectTimeout(); + + /** + * Set the connect timeout,default is 1 minutes + * + * @param connectTimeout + */ + public void setConnectTimeout(long connectTimeout); + + /** + * return the session manager + * + * @return + */ + public Connector getConnector(); + + /** + * Enable/Disable merge many get commands to one multi-get command.true is to enable it,false is + * to disable it.Default is true.Recommend users to enable it. + * + * @param optimizeGet + */ + public void setOptimizeGet(final boolean optimizeGet); + + /** + * Enable/Disable merge many command's buffers to one big buffer fit socket's send buffer + * size.Default is true.Recommend true. + * + * @param optimizeMergeBuffer + */ + public void setOptimizeMergeBuffer(final boolean optimizeMergeBuffer); + + /** + * @return + */ + public boolean isShutdown(); + + /** + * Aadd a memcached server,the thread call this method will be blocked until the connecting + * operations completed(success or fail) + * + * @param server host string + * @param port port number + */ + public void addServer(final String server, final int port) throws IOException; + + /** + * Add a memcached server,the thread call this method will be blocked until the connecting + * operations completed(success or fail) + * + * @param inetSocketAddress memcached server's socket address + */ + public void addServer(final InetSocketAddress inetSocketAddress) throws IOException; + + /** + * Add many memcached servers.You can call this method through JMX or program + * + * @param host String like [host1]:[port1] [host2]:[port2] ... + */ + public void addServer(String hostList) throws IOException; + + /** + * Get current server list.You can call this method through JMX or program + */ + public List getServersDescription(); + + /** + * Remove many memcached server + * + * @param host String like [host1]:[port1] [host2]:[port2] ... + */ + public void removeServer(String hostList); + + /** + * Remove memcached server with the exact given address. + * + * @param address Resolved server address + */ + public void removeServer(InetSocketAddress address); + + /** + * Set the nio's ByteBuffer Allocator,use SimpleBufferAllocator by default. + * + * + * @param bufferAllocator + * @return + */ + @Deprecated + public void setBufferAllocator(final BufferAllocator bufferAllocator); + + /** + * Get value by key + * + * @param + * @param key Key + * @param timeout Operation timeout,if the method is not returned in this time,throw + * TimeoutException + * @param transcoder The value's transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public T get(final String key, final long timeout, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + public T get(final String key, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + public T get(final String key, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + public T get(final String key) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Just like get,But it return a GetsResponse,include cas value for cas update. + * + * @param + * @param key key + * @param timeout operation timeout + * @param transcoder + * + * @return GetsResponse + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public GetsResponse gets(final String key, final long timeout, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #gets(String, long, Transcoder) + * @param + * @param key + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public GetsResponse gets(final String key) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #gets(String, long, Transcoder) + * @param + * @param key + * @param timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public GetsResponse gets(final String key, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #gets(String, long, Transcoder) + * @param + * @param key + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + @SuppressWarnings("unchecked") + public GetsResponse gets(final String key, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Bulk get items + * + * @param + * @param keyCollections key collection + * @param opTimeout opTimeout + * @param transcoder Value transcoder + * @return Exists items map + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map get(final Collection keyCollections, final long opTimeout, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #get(Collection, long, Transcoder) + * @param + * @param keyCollections + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map get(final Collection keyCollections, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #get(Collection, long, Transcoder) + * @param + * @param keyCollections + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map get(final Collection keyCollections) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #get(Collection, long, Transcoder) + * @param + * @param keyCollections + * @param timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map get(final Collection keyCollections, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Bulk gets items + * + * @param + * @param keyCollections key collection + * @param opTime Operation timeout + * @param transcoder Value transcoder + * @return Exists GetsResponse map + * @see net.rubyeye.xmemcached.GetsResponse + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map> gets(final Collection keyCollections, + final long opTime, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #gets(Collection, long, Transcoder) + * @param + * @param keyCollections + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map> gets(final Collection keyCollections) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #gets(Collection, long, Transcoder) + * @param + * @param keyCollections + * @param timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map> gets(final Collection keyCollections, + final long timeout) throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #gets(Collection, long, Transcoder) + * @param + * @param keyCollections + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map> gets(final Collection keyCollections, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Store key-value item to memcached + * + * @param + * @param key stored key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value stored data + * @param transcoder transocder + * @param timeout operation timeout,in milliseconds + * @return boolean result + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean set(final String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #set(String, int, Object, Transcoder, long) + */ + public boolean set(final String key, final int exp, final Object value) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #set(String, int, Object, Transcoder, long) + */ + public boolean set(final String key, final int exp, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #set(String, int, Object, Transcoder, long) + */ + public boolean set(final String key, final int exp, final T value, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Store key-value item to memcached,doesn't wait for reply + * + * @param + * @param key stored key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value stored data + * @param transcoder transocder + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void setWithNoReply(final String key, final int exp, final Object value) + throws InterruptedException, MemcachedException; + + /** + * @see #setWithNoReply(String, int, Object, Transcoder) + * @param + * @param key + * @param exp + * @param value + * @param transcoder + * @throws InterruptedException + * @throws MemcachedException + */ + public void setWithNoReply(final String key, final int exp, final T value, + final Transcoder transcoder) throws InterruptedException, MemcachedException; + + /** + * Add key-value item to memcached, success only when the key is not exists in memcached. + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value + * @param transcoder + * @param timeout + * @return boolean result + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean add(final String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #add(String, int, Object, Transcoder, long) + * @param key + * @param exp + * @param value + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean add(final String key, final int exp, final Object value) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #add(String, int, Object, Transcoder, long) + * @param key + * @param exp + * @param value + * @param timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean add(final String key, final int exp, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #add(String, int, Object, Transcoder, long) + * + * @param + * @param key + * @param exp + * @param value + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean add(final String key, final int exp, final T value, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Add key-value item to memcached, success only when the key is not exists in memcached.This + * method doesn't wait for reply. + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + + public void addWithNoReply(final String key, final int exp, final Object value) + throws InterruptedException, MemcachedException; + + /** + * @see #addWithNoReply(String, int, Object, Transcoder) + * @param + * @param key + * @param exp + * @param value + * @param transcoder + * @throws InterruptedException + * @throws MemcachedException + */ + public void addWithNoReply(final String key, final int exp, final T value, + final Transcoder transcoder) throws InterruptedException, MemcachedException; + + /** + * Replace the key's data item in memcached,success only when the key's data item is exists in + * memcached.This method will wait for reply from server. + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value + * @param transcoder + * @param timeout + * @return boolean result + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean replace(final String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #replace(String, int, Object, Transcoder, long) + * @param key + * @param exp + * @param value + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean replace(final String key, final int exp, final Object value) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #replace(String, int, Object, Transcoder, long) + * @param key + * @param exp + * @param value + * @param timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean replace(final String key, final int exp, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #replace(String, int, Object, Transcoder, long) + * @param + * @param key + * @param exp + * @param value + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean replace(final String key, final int exp, final T value, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Replace the key's data item in memcached,success only when the key's data item is exists in + * memcached.This method doesn't wait for reply from server. + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value + * @param transcoder + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void replaceWithNoReply(final String key, final int exp, final Object value) + throws InterruptedException, MemcachedException; + + /** + * @see #replaceWithNoReply(String, int, Object, Transcoder) + * @param + * @param key + * @param exp + * @param value + * @param transcoder + * @throws InterruptedException + * @throws MemcachedException + */ + public void replaceWithNoReply(final String key, final int exp, final T value, + final Transcoder transcoder) throws InterruptedException, MemcachedException; + + /** + * @see #append(String, Object, long) + * @param key + * @param value + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean append(final String key, final Object value) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Append value to key's data item,this method will wait for reply + * + * @param key + * @param value + * @param timeout + * @return boolean result + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean append(final String key, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Append value to key's data item,this method doesn't wait for reply. + * + * @param key + * @param value + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void appendWithNoReply(final String key, final Object value) + throws InterruptedException, MemcachedException; + + /** + * @see #prepend(String, Object, long) + * @param key + * @param value + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean prepend(final String key, final Object value) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Prepend value to key's data item in memcached.This method doesn't wait for reply. + * + * @param key + * @param value + * @return boolean result + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean prepend(final String key, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Prepend value to key's data item in memcached.This method doesn't wait for reply. + * + * @param key + * @param value + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void prependWithNoReply(final String key, final Object value) + throws InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, Object, Transcoder, long, long) + * @param key + * @param exp + * @param value + * @param cas + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, final Object value, final long cas) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Cas is a check and set operation which means "store this data but only if no one else has + * updated since I last fetched it." + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param value + * @param transcoder + * @param timeout + * @param cas + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, final T value, + final Transcoder transcoder, final long timeout, final long cas) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, Object, Transcoder, long, long) + * @param key + * @param exp + * @param value + * @param timeout + * @param cas + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, final Object value, final long timeout, + final long cas) throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, Object, Transcoder, long, long) + * @param + * @param key + * @param exp + * @param value + * @param transcoder + * @param cas + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, final T value, + final Transcoder transcoder, final long cas) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Cas is a check and set operation which means "store this data but only if no one else has + * updated since I last fetched it." + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param operation CASOperation + * @param transcoder object transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, final CASOperation operation, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * cas is a check and set operation which means "store this data but only if no one else has + * updated since I last fetched it." + * + * @param + * @param key + * @param exp An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a + * unix timestamp of an exact date. + * @param getsReponse gets method's result + * @param operation CASOperation + * @param transcoder + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, GetsResponse getsReponse, + final CASOperation operation, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, GetsResponse, CASOperation, Transcoder) + * @param + * @param key + * @param exp + * @param getsReponse + * @param operation + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, GetsResponse getsReponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, GetsResponse, CASOperation, Transcoder) + * @param + * @param key + * @param getsResponse + * @param operation + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, GetsResponse getsResponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, GetsResponse, CASOperation, Transcoder) + * @param + * @param key + * @param exp + * @param operation + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final int exp, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #cas(String, int, GetsResponse, CASOperation, Transcoder) + * @param + * @param key + * @param operation + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean cas(final String key, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * + * @param + * @param key + * @param getsResponse + * @param operation + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void casWithNoReply(final String key, GetsResponse getsResponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * cas noreply + * + * @param + * @param key + * @param exp + * @param getsReponse + * @param operation + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void casWithNoReply(final String key, final int exp, GetsResponse getsReponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #casWithNoReply(String, int, GetsResponse, CASOperation) + * @param + * @param key + * @param exp + * @param operation + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void casWithNoReply(final String key, final int exp, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see #casWithNoReply(String, int, GetsResponse, CASOperation) + * @param + * @param key + * @param operation + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void casWithNoReply(final String key, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Delete key's data item from memcached.It it is not exists,return false.
+ * time is the amount of time in seconds (or Unix time until
+ * which) the client wishes the server to refuse "add" and "replace"
+ * commands with this key. For this amount of item, the item is put into a
+ * delete queue, which means that it won't possible to retrieve it by the
+ * "get" command, but "add" and "replace" command with this key will also
+ * fail (the "set" command will succeed, however). After the time passes,
+ * the item is finally deleted from server memory.
+ * Note: This method is deprecated,because memcached 1.4.0 remove the optional argument + * "time".You can still use this method on old version,but is not recommended. + * + * @param key + * @param time + * @throws InterruptedException + * @throws MemcachedException + */ + @Deprecated + public boolean delete(final String key, final int time) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Delete key's date item from memcached + * + * @param key + * @param opTimeout Operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + * @since 1.3.2 + */ + public boolean delete(final String key, long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Delete key's date item from memcached only if its cas value is the same as what was read. + * + * @param key + * @cas cas on delete to make sure the key is deleted only if its value is same as what was read. + * @param opTimeout Operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + * @since 1.3.2 + */ + public boolean delete(final String key, long cas, long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Set a new expiration time for an existing item + * + * @param key item's key + * @param exp New expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as + * a unix timestamp of an exact date. + * @param opTimeout operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean touch(final String key, int exp, long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Set a new expiration time for an existing item,using default opTimeout second. + * + * @param key item's key + * @param exp New expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as + * a unix timestamp of an exact date. + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public boolean touch(final String key, int exp) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Get item and set a new expiration time for it + * + * @param + * @param key item's key + * @param newExp New expiration time, in seconds. Can be up to 30 days. After 30 days, is treated + * as a unix timestamp of an exact date. + * @param opTimeout operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public T getAndTouch(final String key, int newExp, long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Get item and set a new expiration time for it,using default opTimeout + * + * @param + * @param key + * @param newExp + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public T getAndTouch(final String key, int newExp) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Bulk get items and touch them + * + * @param + * @param keyExpMap A map,key is item's key,and value is a new expiration time for the item. + * @param opTimeout operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + // public Map getAndTouch(Map keyExpMap, + // long opTimeout) throws TimeoutException, InterruptedException, + // MemcachedException; + + /** + * Bulk get items and touch them,using default opTimeout + * + * @see #getAndTouch(Map, long) + * @param + * @param keyExpMap + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + // public Map getAndTouch(Map keyExpMap) + // throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Get all connected memcached servers's version. + * + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public Map getVersions() + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * "incr" are used to change data for some item in-place, incrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for incr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @return the new value of the item's data, after the increment operation was carried out. + * @param key + * @param num + * @throws InterruptedException + * @throws MemcachedException + */ + public long incr(final String key, final long delta) + throws TimeoutException, InterruptedException, MemcachedException; + + public long incr(final String key, final long delta, final long initValue) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * "incr" are used to change data for some item in-place, incrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for incr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @param key key + * @param num increment + * @param initValue initValue if the data is not exists. + * @param timeout operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public long incr(final String key, final long delta, final long initValue, long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * "decr" are used to change data for some item in-place, decrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for decr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @return the new value of the item's data, after the decrement operation was carried out. + * @param key + * @param num + * @throws InterruptedException + * @throws MemcachedException + */ + public long decr(final String key, final long delta) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * @see decr + * @param key + * @param num + * @param initValue + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public long decr(final String key, final long delta, long initValue) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * "decr" are used to change data for some item in-place, decrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for decr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @param key The key + * @param num The increment + * @param initValue The initial value if the data is not exists. + * @param timeout Operation timeout + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public long decr(final String key, final long delta, long initValue, long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Make All connected memcached's data item invalid + * + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void flushAll() throws TimeoutException, InterruptedException, MemcachedException; + + public void flushAllWithNoReply() throws InterruptedException, MemcachedException; + + /** + * Make All connected memcached's data item invalid + * + * @param timeout operation timeout + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void flushAll(long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Invalidate all existing items immediately + * + * @param address Target memcached server + * @param timeout operation timeout + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void flushAll(InetSocketAddress address) + throws MemcachedException, InterruptedException, TimeoutException; + + public void flushAllWithNoReply(InetSocketAddress address) + throws MemcachedException, InterruptedException; + + public void flushAll(InetSocketAddress address, long timeout) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * This method is deprecated,please use flushAll(InetSocketAddress) instead. + * + * @deprecated + * @param host + * + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + @Deprecated + public void flushAll(String host) + throws TimeoutException, InterruptedException, MemcachedException; + + public Map stats(InetSocketAddress address) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * �ョ��瑰������emcached server缁��淇℃� + * + * @param address ����板� + * @param timeout ���瓒�� + * @return + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public Map stats(InetSocketAddress address, long timeout) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * Get stats from all memcached servers + * + * @param timeout + * @return server->item->value map + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public Map> getStats(long timeout) + throws MemcachedException, InterruptedException, TimeoutException; + + public Map> getStats() + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * Get special item stats. "stats items" for example + * + * @param item + * @return + */ + public Map> getStatsByItem(String itemName) + throws MemcachedException, InterruptedException, TimeoutException;; + + public void shutdown() throws IOException; + + public boolean delete(final String key) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * return default transcoder,default is SerializingTranscoder + * + * @return + */ + @SuppressWarnings("unchecked") + public Transcoder getTranscoder(); + + /** + * set transcoder + * + * @param transcoder + */ + @SuppressWarnings("unchecked") + public void setTranscoder(final Transcoder transcoder); + + public Map> getStatsByItem(String itemName, long timeout) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * get operation timeout setting + * + * @return + */ + public long getOpTimeout(); + + /** + * set operation timeout,default is one second. + * + * @param opTimeout + */ + public void setOpTimeout(long opTimeout); + + public Map getVersions(long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Returns available memcached servers list.This method is drepcated,please use + * getAvailableServers instead. + * + * @see #getAvailableServers() + * @return + */ + @Deprecated + public Collection getAvaliableServers(); + + /** + * Returns available memcached servers list. + * + * @return A available server collection + */ + public Collection getAvailableServers(); + + /** + * add a memcached server to MemcachedClient + * + * @param server + * @param port + * @param weight + * @throws IOException + */ + public void addServer(final String server, final int port, int weight) throws IOException; + + public void addServer(final InetSocketAddress inetSocketAddress, int weight) throws IOException; + + /** + * Delete key's data item from memcached.This method doesn't wait for reply. This method does not + * work on memcached 1.3 or later version.See + * i s s u + * e 3
+ * Note: This method is deprecated,because memcached 1.4.0 remove the optional argument + * "time".You can still use this method on old version,but is not recommended. + * + * @param key + * @param time + * @throws InterruptedException + * @throws MemcachedException + */ + @Deprecated + public void deleteWithNoReply(final String key, final int time) + throws InterruptedException, MemcachedException; + + public void deleteWithNoReply(final String key) throws InterruptedException, MemcachedException; + + /** + * "incr" are used to change data for some item in-place, incrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for incr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @param key + * @param num + * @throws InterruptedException + * @throws MemcachedException + */ + public void incrWithNoReply(final String key, final long delta) + throws InterruptedException, MemcachedException; + + /** + * "decr" are used to change data for some item in-place, decrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for decr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @param key + * @param num + * @throws InterruptedException + * @throws MemcachedException + */ + public void decrWithNoReply(final String key, final long delta) + throws InterruptedException, MemcachedException; + + /** + * Set the verbosity level of the memcached's logging output.This method will wait for reply. + * + * @param address + * @param level logging level + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public void setLoggingLevelVerbosity(InetSocketAddress address, int level) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Set the verbosity level of the memcached's logging output.This method doesn't wait for reply + * from server + * + * @param address memcached server address + * @param level logging level + * @throws InterruptedException + * @throws MemcachedException + */ + public void setLoggingLevelVerbosityWithNoReply(InetSocketAddress address, int level) + throws InterruptedException, MemcachedException; + + /** + * Add a memcached client listener + * + * @param listener + */ + public void addStateListener(MemcachedClientStateListener listener); + + /** + * Remove a memcached client listener + * + * @param listener + */ + public void removeStateListener(MemcachedClientStateListener listener); + + /** + * Get all current state listeners + * + * @return + */ + public Collection getStateListeners(); + + public void flushAllWithNoReply(int exptime) throws InterruptedException, MemcachedException; + + public void flushAll(int exptime, long timeout) + throws TimeoutException, InterruptedException, MemcachedException; + + public void flushAllWithNoReply(InetSocketAddress address, int exptime) + throws MemcachedException, InterruptedException; + + public void flushAll(InetSocketAddress address, long timeout, int exptime) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * If the memcached dump or network error cause connection closed,xmemcached would try to heal the + * connection.The interval between reconnections is 2 seconds by default. You can change that + * value by this method. + * + * @param healConnectionInterval MILLISECONDS + */ + public void setHealSessionInterval(long healConnectionInterval); + + /** + * If the memcached dump or network error cause connection closed,xmemcached would try to heal the + * connection.You can disable this behaviour by using this method:
+ * client.setEnableHealSession(false);
+ * The default value is true. + * + * @param enableHealSession + * @since 1.3.9 + */ + public void setEnableHealSession(boolean enableHealSession); + + /** + * Return the default heal session interval in milliseconds + * + * @return + */ + public long getHealSessionInterval(); + + public Protocol getProtocol(); + + /** + * Store all primitive type as string,defualt is false. + */ + public void setPrimitiveAsString(boolean primitiveAsString); + + /** + * In a high concurrent enviroment,you may want to pool memcached clients.But a xmemcached client + * has to start a reactor thread and some thread pools,if you create too many clients,the cost is + * very large. Xmemcached supports connection pool instreadof client pool.you can create more + * connections to one or more memcached servers,and these connections share the same reactor and + * thread pools,it will reduce the cost of system. + * + * @param poolSize pool size,default is one,every memcached has only one connection. + */ + public void setConnectionPoolSize(int poolSize); + + /** + * Whether to enable heart beat + * + * @param enableHeartBeat if true,then enable heartbeat,true by default + */ + public void setEnableHeartBeat(boolean enableHeartBeat); + + /** + * Enables/disables sanitizing keys by URLEncoding. + * + * @param sanitizeKey if true, then URLEncode all keys + */ + public void setSanitizeKeys(boolean sanitizeKey); + + public boolean isSanitizeKeys(); + + /** + * Get counter for key,and if the key's value is not set,then set it with 0. + * + * @param key + * @return + */ + public Counter getCounter(String key); + + /** + * Get counter for key,and if the key's value is not set,then set it with initial value. + * + * @param key + * @param initialValue + * @return + */ + public Counter getCounter(String key, long initialValue); + + /** + * Get key iterator for special memcached server.You must known that the iterator is a snapshot + * for memcached all keys,it is not real-time.The 'stats cachedump" has length limitation,so + * iterator could not visit all keys if you have many keys.Your application should not be + * dependent on this feature. + * + * @deprecated memcached 1.6.x will remove cachedump stats command,so this method will be removed + * in the future + * @param address + * @return + */ + @Deprecated + public KeyIterator getKeyIterator(InetSocketAddress address) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * Configure auth info + * + * @param map Auth info map,key is memcached server address,and value is the auth info for the + * key. + */ + public void setAuthInfoMap(Map map); + + /** + * return current all auth info + * + * @return Auth info map,key is memcached server address,and value is the auth info for the key. + */ + public Map getAuthInfoMap(); + + /** + * Retruns the AuthInfo for all server strings (hostname:port) + * + * @return A map of AuthInfos for server strings (hostname:port) + */ + public Map getAuthInfoStringMap(); + + /** + * "incr" are used to change data for some item in-place, incrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for incr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @param key + * @param delta + * @param initValue the initial value to be added when value is not found + * @param timeout + * @param exp the initial vlaue expire time, in seconds. Can be up to 30 days. After 30 days, is + * treated as a unix timestamp of an exact date. + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + long decr(String key, long delta, long initValue, long timeout, int exp) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * "incr" are used to change data for some item in-place, incrementing it. The data for the item + * is treated as decimal representation of a 64-bit unsigned integer. If the current data value + * does not conform to such a representation, the commands behave as if the value were 0. Also, + * the item must already exist for incr to work; these commands won't pretend that a non-existent + * key exists with value 0; instead, it will fail.This method doesn't wait for reply. + * + * @param key key + * @param delta increment delta + * @param initValue the initial value to be added when value is not found + * @param timeout operation timeout + * @param exp the initial vlaue expire time, in seconds. Can be up to 30 days. After 30 days, is + * treated as a unix timestamp of an exact date. + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + long incr(String key, long delta, long initValue, long timeout, int exp) + throws TimeoutException, InterruptedException, MemcachedException; + + /** + * Return the cache instance name + * + * @return + */ + public String getName(); + + /** + * Set cache instance name + * + * @param name + */ + public void setName(String name); + + /** + * Returns reconnecting task queue,the queue is thread-safe and 'weakly consistent',but maybe you + * should not modify it at all. + * + * @return The reconnecting task queue,if the client has not been started,returns null. + */ + public Queue getReconnectRequestQueue(); + + /** + * Configure wheather to set client in failure mode.If set it to true,that means you want to + * configure client in failure mode. Failure mode is that when a memcached server is down,it would + * not taken from the server list but marked as unavailable,and then further requests to this + * server will be transformed to standby node if configured or throw an exception until it comes + * back up. + * + * @param failureMode true is to configure client in failure mode. + */ + public void setFailureMode(boolean failureMode); + + /** + * Returns if client is in failure mode. + * + * @return + */ + public boolean isFailureMode(); + + /** + * Set a key provider for pre-processing keys before sending them to memcached. + * + * @since 1.3.8 + * @param keyProvider + */ + public void setKeyProvider(KeyProvider keyProvider); + + /** + * Returns maximum number of timeout exception for closing connection. + * + * @return + */ + public int getTimeoutExceptionThreshold(); + + /** + * Set maximum number of timeout exception for closing connection.You can set it to be a large + * value to disable this feature. + * + * @see #DEFAULT_MAX_TIMEOUTEXCEPTION_THRESHOLD + * @param timeoutExceptionThreshold + */ + public void setTimeoutExceptionThreshold(int timeoutExceptionThreshold); + + /** + * Invalidate all namespace under the namespace using the default operation timeout. + * + * @since 1.4.2 + * @param ns the namespace + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public abstract void invalidateNamespace(String ns) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * Invalidate all items under the namespace. + * + * @since 1.4.2 + * @param ns the namespace + * @param opTimeout operation timeout in milliseconds. + * @throws MemcachedException + * @throws InterruptedException + * @throws TimeoutException + */ + public void invalidateNamespace(String ns, long opTimeout) + throws MemcachedException, InterruptedException, TimeoutException; + + /** + * Remove current namespace set for this memcached client.It must begin with + * {@link #beginWithNamespace(String)} method. + * + * @see #beginWithNamespace(String) + */ + public void endWithNamespace(); + + /** + * set current namespace for following operations with memcached client.It must be ended with + * {@link #endWithNamespace()} method.For example: + * + *
+   * memcachedClient.beginWithNamespace(userId);
+   * try {
+   *   memcachedClient.set("username", 0, username);
+   *   memcachedClient.set("email", 0, email);
+   * } finally {
+   *   memcachedClient.endWithNamespace();
+   * }
+   * 
+ * + * @see #endWithNamespace() + * @see #withNamespace(String, MemcachedClientCallable) + * @param ns + */ + public void beginWithNamespace(String ns); + + /** + * With the namespae to do something with current memcached client.All operations with memcached + * client done in callable will be under the namespace. {@link #beginWithNamespace(String)} and + * {@link #endWithNamespace()} will called around automatically. For example: + * + *
+   *   memcachedClient.withNamespace(userId,new MemcachedClientCallable{
+   *     public Void call(MemcachedClient client) throws MemcachedException,
+   * 	InterruptedException, TimeoutException{
+   *      client.set("username",0,username);
+   *      client.set("email",0,email);
+   *      return null;
+   *     }
+   *   });
+   *   //invalidate all items under the namespace.
+   *   memcachedClient.invalidateNamespace(userId);
+   * 
+ * + * @since 1.4.2 + * @param ns + * @param callable + * @see #beginWithNamespace(String) + * @see #endWithNamespace() + * @return + */ + public T withNamespace(String ns, MemcachedClientCallable callable) + throws MemcachedException, InterruptedException, TimeoutException; + +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedClientBuilder.java b/src/main/java/net/rubyeye/xmemcached/MemcachedClientBuilder.java new file mode 100644 index 0000000..68f6d5d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedClientBuilder.java @@ -0,0 +1,294 @@ +package net.rubyeye.xmemcached; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.SocketOption; + +/** + * Builder pattern.Configure XmemcachedClient's options,then build it + * + * @author dennis + * + */ +public interface MemcachedClientBuilder { + + /** + * + * @return net.rubyeye.xmemcached.MemcachedSessionLocator + */ + public MemcachedSessionLocator getSessionLocator(); + + /** + * Set the XmemcachedClient's session locator.Use ArrayMemcachedSessionLocator by default.If you + * want to choose consistent hash strategy,set it to KetamaMemcachedSessionLocator + * + * @param sessionLocator + */ + public void setSessionLocator(MemcachedSessionLocator sessionLocator); + + /** + * + * @return net.rubyeye.xmemcached.MemcachedSessionComparator + */ + public MemcachedSessionComparator getSessionComparator(); + + /** + * Set the XmemcachedClient's session comparator.Use IndexMemcachedSessionComparator by default. + * + * @param sessionComparator + */ + public void setSessionComparator(MemcachedSessionComparator sessionComparator); + + public BufferAllocator getBufferAllocator(); + + /** + * Set nio ByteBuffer's allocator.Use SimpleBufferAllocator by default.You can choose + * CachedBufferAllocator. + * + * @param bufferAllocator + */ + public void setBufferAllocator(BufferAllocator bufferAllocator); + + /** + * Return the default networking's configuration,you can change them. + * + * @return + */ + public Configuration getConfiguration(); + + /** + * Set the XmemcachedClient's networking configuration(reuseAddr,receiveBufferSize,tcpDelay etc.) + * + * @param configuration + */ + public void setConfiguration(Configuration configuration); + + /** + * Build MemcachedClient by current options. + * + * @return + * @throws IOException + */ + public MemcachedClient build() throws IOException; + + /** + * In a high concurrent enviroment,you may want to pool memcached clients.But a xmemcached client + * has to start a reactor thread and some thread pools,if you create too many clients,the cost is + * very large. Xmemcached supports connection pool instead of client pool.you can create more + * connections to one or more memcached servers,and these connections share the same reactor and + * thread pools,it will reduce the cost of system. + * + * @param poolSize pool size,default is 1 + */ + public void setConnectionPoolSize(int poolSize); + + /** + * Set xmemcached's transcoder,it is used for seriailizing + * + * @return + */ + @SuppressWarnings("unchecked") + public Transcoder getTranscoder(); + + @SuppressWarnings("unchecked") + public void setTranscoder(Transcoder transcoder); + + /** + * get xmemcached's command factory + * + * @return + */ + public CommandFactory getCommandFactory(); + + /** + * Add a state listener + * + * @param stateListener + */ + public void addStateListener(MemcachedClientStateListener stateListener); + + /** + * Remove a state listener + * + * @param stateListener + */ + public void removeStateListener(MemcachedClientStateListener stateListener); + + /** + * Set state listeners,replace current list + * + * @param stateListeners + */ + public void setStateListeners(List stateListeners); + + /** + * set xmemcached's command factory.Default is TextCommandFactory,which implements memcached text + * protocol. + * + * @param commandFactory + */ + public void setCommandFactory(CommandFactory commandFactory); + + /** + * Set tcp socket option + * + * @param socketOption + * @param value + */ + @SuppressWarnings("unchecked") + public void setSocketOption(SocketOption socketOption, Object value); + + /** + * Get all tcp socket options + * + * @return + */ + @SuppressWarnings("unchecked") + public Map getSocketOptions(); + + /** + * Configure auth info + * + * @param map Auth info map,key is memcached server address,and value is the auth info for the + * key. + */ + public void setAuthInfoMap(Map map); + + /** + * return current all auth info + * + * @return Auth info map,key is memcached server address,and value is the auth info for the key. + */ + public Map getAuthInfoMap(); + + /** + * Add auth info for memcached server + * + * @param address + * @param authInfo + */ + public void addAuthInfo(InetSocketAddress address, AuthInfo authInfo); + + /** + * Remove auth info for memcached server + * + * @param address + */ + public void removeAuthInfo(InetSocketAddress address); + + /** + * Return the cache instance name + * + * @return + */ + public String getName(); + + /** + * Set cache instance name + * + * @param name + */ + public void setName(String name); + + /** + * Configure wheather to set client in failure mode.If set it to true,that means you want to + * configure client in failure mode. Failure mode is that when a memcached server is down,it would + * not taken from the server list but marked as unavailable,and then further requests to this + * server will be transformed to standby node if configured or throw an exception until it comes + * back up. + * + * @param failureMode true is to configure client in failure mode. + */ + public void setFailureMode(boolean failureMode); + + /** + * Returns if client is in failure mode. + * + * @return + */ + public boolean isFailureMode(); + + /** + * Returns connect timeout in milliseconds + * + * @return connect timeout + */ + public long getConnectTimeout(); + + /** + * Set connect timeout in milliseconds + * + * @see net.rubyeye.xmemcached.MemcachedClient#DEFAULT_CONNECT_TIMEOUT + * + * @param connectTimeout + */ + public void setConnectTimeout(long connectTimeout); + + /** + * Enables/disables sanitizing keys by URLEncoding. + * + * @param sanitizeKey if true, then URLEncode all keys + */ + public void setSanitizeKeys(boolean sanitizeKeys); + + /** + * Set a key provider for pre-processing keys before sending them to memcached. + * + * @since 1.3.8 + * @param keyProvider + */ + public void setKeyProvider(KeyProvider keyProvider); + + /** + * Set max queued noreply operations number + * + * @see MemcachedClient#DEFAULT_MAX_QUEUED_NOPS + * @param maxQueuedNoReplyOperations + * @since 1.3.8 + */ + public void setMaxQueuedNoReplyOperations(int maxQueuedNoReplyOperations); + + /** + * If the memcached dump or network error cause connection closed,xmemcached would try to heal the + * connection.The interval between reconnections is 2 seconds by default. You can change that + * value by this method. + * + * @since 1.3.9 + * @param healConnectionInterval MILLISECONDS + */ + public void setHealSessionInterval(long healConnectionInterval); + + /** + * If the memcached dump or network error cause connection closed,xmemcached would try to heal the + * connection.You can disable this behaviour by using this method:
+ * client.setEnableHealSession(false);
+ * The default value is true. + * + * @param enableHealSession + * @since 1.3.9 + */ + public void setEnableHealSession(boolean enableHealSession); + + /** + * Set default operation timeout. + * + * @param opTimeout Operation timeout value in milliseconds. + * @since 1.4.1 + */ + public void setOpTimeout(long opTimeout); + + /** + * Returns the default operation timeout in milliseconds. + * + * @since 1.4.1 + * @return + */ + public long getOpTimeout(); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedClientCallable.java b/src/main/java/net/rubyeye/xmemcached/MemcachedClientCallable.java new file mode 100644 index 0000000..643e163 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedClientCallable.java @@ -0,0 +1,30 @@ +package net.rubyeye.xmemcached; + +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.exception.MemcachedException; + +/** + * MemcachedClient callable when using namespace in xmemcached.For example: + * + *
+ *   memcachedClient.withNamespace(userId,new MemcachedClientCallable{
+ *     public Void call(MemcachedClient client) throws MemcachedException,
+ * 			InterruptedException, TimeoutException{
+ *      client.set("username",0,username);
+ *      client.set("email",0,email);
+ *      return null;
+ *     }
+ *   }); 
+ *   //invalidate all items under the namespace.
+ *   memcachedClient.invalidateNamespace(userId);
+ * 
+ * + * @author dennis + * @see MemcachedClient#withNamespace(String, MemcachedClientCallable) + * @since 1.4.2 + * @param + */ +public interface MemcachedClientCallable { + public T call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException; +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedClientStateListener.java b/src/main/java/net/rubyeye/xmemcached/MemcachedClientStateListener.java new file mode 100644 index 0000000..f22c022 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedClientStateListener.java @@ -0,0 +1,59 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +import java.net.InetSocketAddress; + +/** + * MemcachedClient state listener.When client startup,shutdown,connected to a memcached server or + * disconnected happened,client will notify the listener instance which implemented this + * interface.Please don't do any operations which may block in these callback methods. + * + * @author dennis + * + */ +public interface MemcachedClientStateListener { + /** + * After client is started. + * + * @param memcachedClient + */ + public void onStarted(MemcachedClient memcachedClient); + + /** + * After client is shutdown. + * + * @param memcachedClient + */ + public void onShutDown(MemcachedClient memcachedClient); + + /** + * After a memcached server is connected,don't do any operations may block here. + * + * @param memcachedClient + * @param inetSocketAddress + */ + public void onConnected(MemcachedClient memcachedClient, InetSocketAddress inetSocketAddress); + + /** + * After a memcached server is disconnected,don't do any operations may block here. + * + * @param memcachedClient + * @param inetSocketAddress + */ + public void onDisconnected(MemcachedClient memcachedClient, InetSocketAddress inetSocketAddress); + + /** + * When exceptions occur + * + * @param memcachedClient + * @param throwable + */ + public void onException(MemcachedClient memcachedClient, Throwable throwable); +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedOptimizer.java b/src/main/java/net/rubyeye/xmemcached/MemcachedOptimizer.java new file mode 100644 index 0000000..b843542 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedOptimizer.java @@ -0,0 +1,35 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +import java.util.Queue; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.Command; + +/** + * xmemcached Optimizer + * + * @author dennis + */ +public interface MemcachedOptimizer { + @SuppressWarnings("unchecked") + Command optimize(final Command currentCommand, final Queue writeQueue, + final Queue executingCmds, int sendBufferSize); + + @Deprecated + public void setBufferAllocator(BufferAllocator bufferAllocator); +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedSessionComparator.java b/src/main/java/net/rubyeye/xmemcached/MemcachedSessionComparator.java new file mode 100644 index 0000000..d21e279 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedSessionComparator.java @@ -0,0 +1,28 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +import java.util.Comparator; +import com.google.code.yanf4j.core.Session; + +/** + * Session comparator. + * + * @author Jungsub Shin + * + */ +public interface MemcachedSessionComparator extends Comparator { + /** + * Returns a session by special key. + * + * @param key + * @return + */ + public int compare(Session o1, Session o2); +} diff --git a/src/main/java/net/rubyeye/xmemcached/MemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/MemcachedSessionLocator.java new file mode 100644 index 0000000..a26bc58 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/MemcachedSessionLocator.java @@ -0,0 +1,42 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +import java.util.Collection; +import com.google.code.yanf4j.core.Session; + +/** + * Session locator.Find session by key. + * + * @author dennis + * + */ +public interface MemcachedSessionLocator { + /** + * Returns a session by special key. + * + * @param key + * @return + */ + public Session getSessionByKey(final String key); + + /** + * Update sessions when session was added or removed. + * + * @param list The newer sessions + */ + public void updateSessions(final Collection list); + + /** + * Configure failure mode + * + * @param failureMode true is using failure mode + */ + public void setFailureMode(boolean failureMode); +} diff --git a/src/main/java/net/rubyeye/xmemcached/XMemcachedClient.java b/src/main/java/net/rubyeye/xmemcached/XMemcachedClient.java new file mode 100644 index 0000000..81ac1c9 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/XMemcachedClient.java @@ -0,0 +1,2669 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.SocketOption; +import com.google.code.yanf4j.util.SystemUtils; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.codec.MemcachedCodecFactory; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.exception.NoValueException; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.ClosedMemcachedTCPSession; +import net.rubyeye.xmemcached.impl.DefaultKeyProvider; +import net.rubyeye.xmemcached.impl.IndexMemcachedSessionComparator; +import net.rubyeye.xmemcached.impl.KeyIteratorImpl; +import net.rubyeye.xmemcached.impl.MemcachedClientStateListenerAdapter; +import net.rubyeye.xmemcached.impl.MemcachedConnector; +import net.rubyeye.xmemcached.impl.MemcachedHandler; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.impl.ReconnectRequest; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.monitor.MemcachedClientNameHolder; +import net.rubyeye.xmemcached.monitor.XMemcachedMbeanServer; +import net.rubyeye.xmemcached.networking.Connector; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import net.rubyeye.xmemcached.utils.Protocol; + +/** + * Memcached Client for connecting to memcached server and do operations. + * + * @author dennis(killme2008@gmail.com) + * + */ +public class XMemcachedClient implements XMemcachedClientMBean, MemcachedClient { + + private static final Logger log = LoggerFactory.getLogger(XMemcachedClient.class); + protected MemcachedSessionLocator sessionLocator; + protected MemcachedSessionComparator sessionComparator; + private volatile boolean shutdown; + protected MemcachedConnector connector; + @SuppressWarnings("unchecked") + private Transcoder transcoder; + private boolean sanitizeKeys; + private MemcachedHandler memcachedHandler; + protected CommandFactory commandFactory; + protected long opTimeout = DEFAULT_OP_TIMEOUT; + private long connectTimeout = DEFAULT_CONNECT_TIMEOUT; + protected int connectionPoolSize = DEFAULT_CONNECTION_POOL_SIZE; + protected int maxQueuedNoReplyOperations = DEFAULT_MAX_QUEUED_NOPS; + protected boolean resolveInetAddresses = true; + + protected final AtomicInteger serverOrderCount = new AtomicInteger(); + + private Map authInfoMap = new HashMap(); + private Map authInfoStringMap = new HashMap(); + + private String name; // cache name + + private boolean failureMode; + + private int timeoutExceptionThreshold = DEFAULT_MAX_TIMEOUTEXCEPTION_THRESHOLD; + + private final CopyOnWriteArrayList stateListenerAdapters = + new CopyOnWriteArrayList(); + private Thread shutdownHookThread; + + private volatile boolean isHutdownHookCalled = false; + // key provider for pre-processing keys before sending them to memcached + // added by dennis,2012-07-14 + private KeyProvider keyProvider = DefaultKeyProvider.INSTANCE; + /** + * namespace thread local. + */ + public static final ThreadLocal NAMESPACE_LOCAL = new ThreadLocal(); + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#setMergeFactor(int) + */ + public final void setMergeFactor(final int mergeFactor) { + if (mergeFactor < 0) { + throw new IllegalArgumentException("mergeFactor<0"); + } + this.connector.setMergeFactor(mergeFactor); + } + + public int getTimeoutExceptionThreshold() { + return this.timeoutExceptionThreshold; + } + + public void setTimeoutExceptionThreshold(final int timeoutExceptionThreshold) { + if (timeoutExceptionThreshold <= 0) { + throw new IllegalArgumentException( + "Illegal timeoutExceptionThreshold value " + timeoutExceptionThreshold); + } + if (timeoutExceptionThreshold < 100) { + log.warn( + "Too small timeoutExceptionThreshold value may cause connections disconnect/reconnect frequently."); + } + this.timeoutExceptionThreshold = timeoutExceptionThreshold; + } + + public T withNamespace(final String ns, final MemcachedClientCallable callable) + throws MemcachedException, InterruptedException, TimeoutException { + beginWithNamespace(ns); + try { + return callable.call(this); + } finally { + endWithNamespace(); + } + } + + public void endWithNamespace() { + NAMESPACE_LOCAL.remove(); + } + + public void beginWithNamespace(final String ns) { + if (ns == null || ns.trim().length() == 0) { + throw new IllegalArgumentException("Blank namespace"); + } + if (NAMESPACE_LOCAL.get() != null) { + throw new IllegalStateException("Previous namespace wasn't ended."); + } + NAMESPACE_LOCAL.set(ns); + } + + public KeyProvider getKeyProvider() { + return this.keyProvider; + } + + public void setKeyProvider(final KeyProvider keyProvider) { + if (keyProvider == null) { + throw new IllegalArgumentException("Null key provider"); + } + this.keyProvider = keyProvider; + } + + public final MemcachedSessionLocator getSessionLocator() { + return this.sessionLocator; + } + + public final MemcachedSessionComparator getSessionComparator() { + return this.sessionComparator; + } + + public final CommandFactory getCommandFactory() { + return this.commandFactory; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#getConnectTimeout() + */ + public long getConnectTimeout() { + return this.connectTimeout; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#setConnectTimeout(long) + */ + public void setConnectTimeout(final long connectTimeout) { + if (connectTimeout < 0) { + throw new IllegalArgumentException("connectTimeout<0"); + } + this.connectTimeout = connectTimeout; + } + + public void setEnableHeartBeat(final boolean enableHeartBeat) { + this.memcachedHandler.setEnableHeartBeat(enableHeartBeat); + } + + /** + * get operation timeout setting + * + * @return + */ + public final long getOpTimeout() { + return this.opTimeout; + } + + /** + * set operation timeout,default is one second. + * + * @param opTimeout + */ + public final void setOpTimeout(final long opTimeout) { + if (opTimeout < 0) { + throw new IllegalArgumentException("opTimeout<0"); + } + this.opTimeout = opTimeout; + } + + public void setHealSessionInterval(final long healConnectionInterval) { + if (healConnectionInterval <= 0) { + throw new IllegalArgumentException("Invalid heal session interval:" + healConnectionInterval); + } + if (null != this.connector) { + this.connector.setHealSessionInterval(healConnectionInterval); + } else { + throw new IllegalStateException("The client hasn't been started"); + } + } + + public long getHealSessionInterval() { + if (null != this.connector) { + return this.connector.getHealSessionInterval(); + } + return -1L; + } + + public Map getAuthInfoMap() { + return this.authInfoMap; + } + + public void setAuthInfoMap(final Map map) { + this.authInfoMap = map; + this.authInfoStringMap = new HashMap(); + for (Map.Entry entry : map.entrySet()) { + String server = AddrUtil.getServerString(entry.getKey()); + this.authInfoStringMap.put(server, entry.getValue()); + } + } + + public Map getAuthInfoStringMap() { + return this.authInfoStringMap; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#getConnector() + */ + public final Connector getConnector() { + return this.connector; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#setOptimizeMergeBuffer(boolean) + */ + public final void setOptimizeMergeBuffer(final boolean optimizeMergeBuffer) { + this.connector.setOptimizeMergeBuffer(optimizeMergeBuffer); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#isShutdown() + */ + public final boolean isShutdown() { + return this.shutdown; + } + + @SuppressWarnings("unchecked") + private final GetsResponse gets0(final String key, final byte[] keyBytes, + final Transcoder transcoder) + throws MemcachedException, TimeoutException, InterruptedException { + GetsResponse result = (GetsResponse) this.fetch0(key, keyBytes, CommandType.GETS_ONE, + this.opTimeout, transcoder); + return result; + } + + protected final Session sendCommand(final Command cmd) throws MemcachedException { + if (this.shutdown) { + throw new MemcachedException("Xmemcached is stopped"); + } + return this.connector.send(cmd); + } + + /** + * XMemcached constructor,default weight is 1 + * + * @param server �����P + * @param port ����ㄧ��� + * @throws IOException + */ + public XMemcachedClient(final String server, final int port) throws IOException { + this(server, port, 1); + } + + /** + * XMemcached constructor + * + * @param host server host + * @param port server port + * @param weight server weight + * @throws IOException + */ + public XMemcachedClient(final String host, final int port, final int weight) throws IOException { + super(); + if (weight <= 0) { + throw new IllegalArgumentException("weight<=0"); + } + checkServerPort(host, port); + buildConnector(new ArrayMemcachedSessionLocator(), new IndexMemcachedSessionComparator(), + new SimpleBufferAllocator(), XMemcachedClientBuilder.getDefaultConfiguration(), + XMemcachedClientBuilder.getDefaultSocketOptions(), new TextCommandFactory(), + new SerializingTranscoder()); + start0(); + connect(new InetSocketAddressWrapper(newSocketAddress(host, port), + this.serverOrderCount.incrementAndGet(), weight, null, this.resolveInetAddresses)); + } + + protected InetSocketAddress newSocketAddress(final String server, final int port) { + return new InetSocketAddress(server, port); + } + + private void checkServerPort(final String server, final int port) { + if (server == null || server.length() == 0) { + throw new IllegalArgumentException(); + } + if (port <= 0) { + throw new IllegalArgumentException(); + } + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#addServer(java.lang.String, int) + */ + public final void addServer(final String server, final int port) throws IOException { + this.addServer(server, port, 1); + } + + /** + * add a memcached server to MemcachedClient + * + * @param server + * @param port + * @param weight + * @throws IOException + */ + public final void addServer(final String server, final int port, final int weight) + throws IOException { + if (weight <= 0) { + throw new IllegalArgumentException("weight<=0"); + } + checkServerPort(server, port); + connect(new InetSocketAddressWrapper(newSocketAddress(server, port), + this.serverOrderCount.incrementAndGet(), weight, null, this.resolveInetAddresses)); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#addServer(java.net. InetSocketAddress ) + */ + public final void addServer(final InetSocketAddress inetSocketAddress) throws IOException { + this.addServer(inetSocketAddress, 1); + } + + public final void addServer(final InetSocketAddress inetSocketAddress, final int weight) + throws IOException { + if (inetSocketAddress == null) { + throw new IllegalArgumentException("Null InetSocketAddress"); + } + if (weight <= 0) { + throw new IllegalArgumentException("weight<=0"); + } + connect(new InetSocketAddressWrapper(inetSocketAddress, this.serverOrderCount.incrementAndGet(), + weight, null, this.resolveInetAddresses)); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#addServer(java.lang.String) + */ + public final void addServer(final String hostList) throws IOException { + Map addresses = AddrUtil.getAddressMap(hostList); + if (addresses != null && addresses.size() > 0) { + for (Map.Entry entry : addresses.entrySet()) { + final InetSocketAddress mainNodeAddr = entry.getKey(); + final InetSocketAddress standbyNodeAddr = entry.getValue(); + connect(new InetSocketAddressWrapper(mainNodeAddr, this.serverOrderCount.incrementAndGet(), + 1, null, this.resolveInetAddresses)); + if (standbyNodeAddr != null) { + connect(new InetSocketAddressWrapper(standbyNodeAddr, + this.serverOrderCount.incrementAndGet(), 1, mainNodeAddr, this.resolveInetAddresses)); + } + } + } + } + + public void addOneServerWithWeight(final String server, final int weight) throws IOException { + Map addresses = AddrUtil.getAddressMap(server); + if (addresses == null) { + throw new IllegalArgumentException("Null Server"); + } + if (addresses.size() != 1) { + throw new IllegalArgumentException("Please add one server at one time"); + } + if (weight <= 0) { + throw new IllegalArgumentException("weight<=0"); + } + if (addresses != null && addresses.size() > 0) { + for (Map.Entry entry : addresses.entrySet()) { + final InetSocketAddress mainNodeAddr = entry.getKey(); + final InetSocketAddress standbyNodeAddr = entry.getValue(); + connect(new InetSocketAddressWrapper(mainNodeAddr, this.serverOrderCount.incrementAndGet(), + 1, null, this.resolveInetAddresses)); + if (standbyNodeAddr != null) { + connect(new InetSocketAddressWrapper(standbyNodeAddr, + this.serverOrderCount.incrementAndGet(), 1, mainNodeAddr, this.resolveInetAddresses)); + } + } + } + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#getServersDescription() + */ + public final List getServersDescription() { + final List result = new ArrayList(); + for (Session session : this.connector.getSessionSet()) { + InetSocketAddress socketAddress = session.getRemoteSocketAddress(); + int weight = ((MemcachedSession) session).getInetSocketAddressWrapper().getWeight(); + result.add(SystemUtils.getRawAddress(socketAddress) + ":" + socketAddress.getPort() + + "(weight=" + weight + ")"); + } + return result; + } + + public final void setServerWeight(final String server, final int weight) { + InetSocketAddress socketAddress = AddrUtil.getOneAddress(server); + Queue sessionQueue = this.connector.getSessionByAddress(socketAddress); + if (sessionQueue == null) { + throw new IllegalArgumentException("There is no server " + server); + } + for (Session session : sessionQueue) { + if (session != null) { + ((MemcachedTCPSession) session).getInetSocketAddressWrapper().setWeight(weight); + } + } + this.connector.updateSessions(); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#removeServer(java.lang.String) + */ + public final void removeServer(final String hostList) { + List addresses = AddrUtil.getAddresses(hostList); + if (addresses != null && addresses.size() > 0) { + for (InetSocketAddress address : addresses) { + removeServer(address); + } + + } + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#removeServer(java.net.InetSocketAddress) + */ + public void removeServer(final InetSocketAddress address) { + // Close main sessions + Queue sessionQueue = this.connector.getSessionByAddress(address); + if (sessionQueue != null) { + for (Session session : sessionQueue) { + if (session != null) { + // Disable auto reconnection + ((MemcachedSession) session).setAllowReconnect(false); + // Close connection + ((MemcachedSession) session).quit(); + } + } + } + // Close standby sessions + List standBySession = this.connector.getStandbySessionListByMainNodeAddr(address); + if (standBySession != null) { + for (Session session : standBySession) { + if (session != null) { + this.connector.removeReconnectRequest(session.getRemoteSocketAddress()); + // Disable auto reconnection + ((MemcachedSession) session).setAllowReconnect(false); + // Close connection + ((MemcachedSession) session).quit(); + } + } + } + this.connector.removeReconnectRequest(address); + } + + protected void connect(final InetSocketAddressWrapper inetSocketAddressWrapper) + throws IOException { + // creat connection pool + InetSocketAddress inetSocketAddress = inetSocketAddressWrapper.getInetSocketAddress(); + if (this.connectionPoolSize > 1) { + log.warn( + "You are using connection pool for xmemcached client,it's not recommended unless you have test it that it can boost performance in your app."); + } + for (int i = 0; i < this.connectionPoolSize; i++) { + Future future = null; + boolean connected = false; + Throwable throwable = null; + try { + future = this.connector.connect(inetSocketAddressWrapper); + + if (!future.get(this.connectTimeout, TimeUnit.MILLISECONDS)) { + log.error("connect to " + SystemUtils.getRawAddress(inetSocketAddress) + ":" + + inetSocketAddress.getPort() + " fail"); + } else { + connected = true; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throwable = e; + log.error("connect to " + SystemUtils.getRawAddress(inetSocketAddress) + ":" + + inetSocketAddress.getPort() + " error", e); + } catch (TimeoutException e) { + throwable = e; + log.error("connect to " + SystemUtils.getRawAddress(inetSocketAddress) + ":" + + inetSocketAddress.getPort() + " timeout", e); + } catch (Exception e) { + throwable = e; + log.error("connect to " + SystemUtils.getRawAddress(inetSocketAddress) + ":" + + inetSocketAddress.getPort() + " error", e); + } + // If it is not connected,it will be added to waiting queue for + // reconnecting. + if (!connected) { + if (future != null) { + future.cancel(true); + } + // If we use failure mode, add a mock session at first + if (this.failureMode) { + this.connector.addSession(new ClosedMemcachedTCPSession(inetSocketAddressWrapper)); + } + this.connector.addToWatingQueue( + new ReconnectRequest(inetSocketAddressWrapper, 0, getHealSessionInterval())); + log.error("Connect to " + SystemUtils.getRawAddress(inetSocketAddress) + ":" + + inetSocketAddress.getPort() + " fail", throwable); + // throw new IOException(throwable); + } + } + } + + @SuppressWarnings("unchecked") + private final Object fetch0(final String key, final byte[] keyBytes, + final CommandType cmdType, final long timeout, Transcoder transcoder) + throws InterruptedException, TimeoutException, MemcachedException { + final Command command = + this.commandFactory.createGetCommand(key, keyBytes, cmdType, this.transcoder); + latchWait(command, timeout, sendCommand(command)); + command.getIoBuffer().free(); // free buffer + checkException(command); + CachedData data = (CachedData) command.getResult(); + if (data == null) { + return null; + } + if (transcoder == null) { + transcoder = this.transcoder; + } + if (cmdType == CommandType.GETS_ONE) { + return new GetsResponse(data.getCas(), transcoder.decode(data)); + } else { + return transcoder.decode(data); + } + } + + private final void start0() throws IOException { + registerMBean(); + startConnector(); + MemcachedClientNameHolder.clear(); + } + + private final void startConnector() throws IOException { + if (this.shutdown) { + this.shutdown = false; + this.connector.start(); + this.memcachedHandler.start(); + if (AddrUtil.isEnableShutDownHook()) { + this.shutdownHookThread = new Thread() { + @Override + public void run() { + try { + XMemcachedClient.this.isHutdownHookCalled = true; + XMemcachedClient.this.shutdown(); + } catch (IOException e) { + log.error("Shutdown XMemcachedClient error", e); + } + } + }; + Runtime.getRuntime().addShutdownHook(this.shutdownHookThread); + } + } + } + + /** + * Set max queued noreply operations number + * + * @param maxQueuedNoReplyOperations + */ + void setMaxQueuedNoReplyOperations(final int maxQueuedNoReplyOperations) { + if (maxQueuedNoReplyOperations <= 1) { + throw new IllegalArgumentException("maxQueuedNoReplyOperations<=1"); + } + this.maxQueuedNoReplyOperations = maxQueuedNoReplyOperations; + } + + @SuppressWarnings("unchecked") + private void buildConnector(MemcachedSessionLocator locator, + MemcachedSessionComparator comparator, BufferAllocator bufferAllocator, + Configuration configuration, final Map socketOptions, + CommandFactory commandFactory, Transcoder transcoder) { + if (locator == null) { + locator = new ArrayMemcachedSessionLocator(); + } + if (comparator == null) { + comparator = new IndexMemcachedSessionComparator(); + } + if (bufferAllocator == null) { + bufferAllocator = new SimpleBufferAllocator(); + } + if (configuration == null) { + configuration = XMemcachedClientBuilder.getDefaultConfiguration(); + } + if (transcoder == null) { + transcoder = new SerializingTranscoder(); + } + if (commandFactory == null) { + commandFactory = new TextCommandFactory(); + } + if (this.name == null) { + this.name = "MemcachedClient-" + Constants.MEMCACHED_CLIENT_COUNTER.getAndIncrement(); + MemcachedClientNameHolder.setName(this.name); + } + this.commandFactory = commandFactory; + ByteUtils.setProtocol(this.commandFactory.getProtocol()); + log.info("XMemcachedClient is using " + this.commandFactory.getProtocol().name() + " protocol"); + this.commandFactory.setBufferAllocator(bufferAllocator); + this.shutdown = true; + this.transcoder = transcoder; + this.sessionLocator = locator; + this.sessionComparator = comparator; + this.connector = + newConnector(bufferAllocator, configuration, this.sessionLocator, this.sessionComparator, + this.commandFactory, this.connectionPoolSize, this.maxQueuedNoReplyOperations); + this.memcachedHandler = new MemcachedHandler(this); + this.connector.setHandler(this.memcachedHandler); + this.connector.setCodecFactory(new MemcachedCodecFactory()); + this.connector.setSessionTimeout(-1); + this.connector.setSocketOptions(socketOptions); + if (isFailureMode()) { + log.info("XMemcachedClient in failure mode."); + } + this.connector.setFailureMode(this.failureMode); + this.sessionLocator.setFailureMode(this.failureMode); + } + + protected MemcachedConnector newConnector(final BufferAllocator bufferAllocator, + final Configuration configuration, final MemcachedSessionLocator memcachedSessionLocator, + final MemcachedSessionComparator memcachedSessionComparator, + final CommandFactory commandFactory, final int poolSize, + final int maxQueuedNoReplyOperations) { + // make sure dispatch message thread count is zero + configuration.setDispatchMessageThreadCount(0); + return new MemcachedConnector(configuration, memcachedSessionLocator, + memcachedSessionComparator, bufferAllocator, commandFactory, poolSize, + maxQueuedNoReplyOperations); + } + + private final void registerMBean() { + if (this.shutdown) { + XMemcachedMbeanServer.getInstance().registMBean(this, this.getClass().getPackage().getName() + + ":type=" + this.getClass().getSimpleName() + "-" + MemcachedClientNameHolder.getName()); + } + } + + public void setOptimizeGet(final boolean optimizeGet) { + this.connector.setOptimizeGet(optimizeGet); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#setBufferAllocator(net.rubyeye + * .xmemcached.buffer.BufferAllocator) + */ + public final void setBufferAllocator(final BufferAllocator bufferAllocator) { + this.connector.setBufferAllocator(bufferAllocator); + } + + /** + * XMemcached Constructor. + * + * @param inetSocketAddress + * @param weight + * @throws IOException + */ + public XMemcachedClient(final InetSocketAddress inetSocketAddress, final int weight, + final CommandFactory cmdFactory) throws IOException { + super(); + if (inetSocketAddress == null) { + throw new IllegalArgumentException("Null InetSocketAddress"); + + } + if (cmdFactory == null) { + throw new IllegalArgumentException("Null command factory."); + } + if (weight <= 0) { + throw new IllegalArgumentException("weight<=0"); + } + buildConnector(new ArrayMemcachedSessionLocator(), new IndexMemcachedSessionComparator(), + new SimpleBufferAllocator(), XMemcachedClientBuilder.getDefaultConfiguration(), + XMemcachedClientBuilder.getDefaultSocketOptions(), cmdFactory, new SerializingTranscoder()); + start0(); + connect(new InetSocketAddressWrapper(inetSocketAddress, this.serverOrderCount.incrementAndGet(), + weight, null, this.resolveInetAddresses)); + } + + public XMemcachedClient(final InetSocketAddress inetSocketAddress, final int weight) + throws IOException { + this(inetSocketAddress, weight, new TextCommandFactory()); + } + + public XMemcachedClient(final InetSocketAddress inetSocketAddress) throws IOException { + this(inetSocketAddress, 1); + } + + public XMemcachedClient() throws IOException { + super(); + buildConnector(new ArrayMemcachedSessionLocator(), new IndexMemcachedSessionComparator(), + new SimpleBufferAllocator(), XMemcachedClientBuilder.getDefaultConfiguration(), + XMemcachedClientBuilder.getDefaultSocketOptions(), new TextCommandFactory(), + new SerializingTranscoder()); + start0(); + } + + /** + * XMemcachedClient constructor.Every server's weight is one by default. You should not new client + * instance by this method, use MemcachedClientBuilder instead. + * + * @param locator + * @param comparator + * @param allocator + * @param conf + * @param commandFactory + * @param transcoder + * @param addressList + * @param stateListeners + * @throws IOException + */ + @SuppressWarnings("unchecked") + public XMemcachedClient(final MemcachedSessionLocator locator, + final MemcachedSessionComparator comparator, final BufferAllocator allocator, + final Configuration conf, final Map socketOptions, + final CommandFactory commandFactory, final Transcoder transcoder, + final Map addressMap, + final List stateListeners, + final Map map, final int poolSize, final long connectTimeout, + final String name, final boolean failureMode, final boolean resolveInetAddresses) + throws IOException { + super(); + setConnectTimeout(connectTimeout); + setFailureMode(failureMode); + setName(name); + optimiezeSetReadThreadCount(conf, addressMap == null ? 0 : addressMap.size()); + buildConnector(locator, comparator, allocator, conf, socketOptions, commandFactory, transcoder); + if (stateListeners != null) { + for (MemcachedClientStateListener stateListener : stateListeners) { + addStateListener(stateListener); + } + } + setAuthInfoMap(map); + setConnectionPoolSize(poolSize); + this.resolveInetAddresses = resolveInetAddresses; + start0(); + if (addressMap != null) { + for (Map.Entry entry : addressMap.entrySet()) { + final InetSocketAddress mainNodeAddr = entry.getKey(); + final InetSocketAddress standbyNodeAddr = entry.getValue(); + connect(new InetSocketAddressWrapper(mainNodeAddr, this.serverOrderCount.incrementAndGet(), + 1, null, this.resolveInetAddresses)); + if (standbyNodeAddr != null) { + connect(new InetSocketAddressWrapper(standbyNodeAddr, + this.serverOrderCount.incrementAndGet(), 1, mainNodeAddr, this.resolveInetAddresses)); + } + } + } + } + + /** + * XMemcachedClient constructor. + * + * @param locator + * @param comparator + * @param allocator + * @param conf + * @param commandFactory + * @param transcoder + * @param addressList + * @param weights + * @param stateListeners weight array for address list + * @throws IOException + */ + @SuppressWarnings("unchecked") + XMemcachedClient(final MemcachedSessionLocator locator, + final MemcachedSessionComparator comparator, final BufferAllocator allocator, + final Configuration conf, final Map socketOptions, + final CommandFactory commandFactory, final Transcoder transcoder, + final Map addressMap, final int[] weights, + final List stateListeners, + final Map infoMap, final int poolSize, final long connectTimeout, + final String name, final boolean failureMode, final boolean resolveInetAddresses) + throws IOException { + super(); + setConnectTimeout(connectTimeout); + setFailureMode(failureMode); + setName(name); + if (weights == null && addressMap != null) { + throw new IllegalArgumentException("Null weights"); + } + if (weights != null && addressMap == null) { + throw new IllegalArgumentException("Null addressList"); + } + + if (weights != null) { + for (int weight : weights) { + if (weight <= 0) { + throw new IllegalArgumentException("Some weights<=0"); + } + } + } + if (weights != null && addressMap != null && weights.length < addressMap.size()) { + throw new IllegalArgumentException("weights.length is less than addressList.size()"); + } + optimiezeSetReadThreadCount(conf, addressMap == null ? 0 : addressMap.size()); + buildConnector(locator, comparator, allocator, conf, socketOptions, commandFactory, transcoder); + if (stateListeners != null) { + for (MemcachedClientStateListener stateListener : stateListeners) { + addStateListener(stateListener); + } + } + setAuthInfoMap(infoMap); + setConnectionPoolSize(poolSize); + this.resolveInetAddresses = resolveInetAddresses; + start0(); + if (addressMap != null && weights != null) { + int i = 0; + for (Map.Entry entry : addressMap.entrySet()) { + final InetSocketAddress mainNodeAddr = entry.getKey(); + final InetSocketAddress standbyNodeAddr = entry.getValue(); + connect(new InetSocketAddressWrapper(mainNodeAddr, this.serverOrderCount.incrementAndGet(), + weights[i], null, this.resolveInetAddresses)); + if (standbyNodeAddr != null) { + connect( + new InetSocketAddressWrapper(standbyNodeAddr, this.serverOrderCount.incrementAndGet(), + weights[i], mainNodeAddr, this.resolveInetAddresses)); + } + i++; + } + } + } + + private final void optimiezeSetReadThreadCount(final Configuration conf, final int addressCount) { + if (conf != null && addressCount > 1) { + if (!isWindowsPlatform() && conf.getReadThreadCount() == DEFAULT_READ_THREAD_COUNT) { + int threadCount = SystemUtils.getSystemThreadCount(); + conf.setReadThreadCount(addressCount > threadCount ? threadCount : addressCount); + } + } + } + + private final boolean isWindowsPlatform() { + String osName = System.getProperty("os.name"); + if (osName != null && osName.toLowerCase().indexOf("windows") >= 0) { + return true; + } else { + return false; + } + } + + /** + * XMemcached Constructor.Every server's weight is one by default. + * + * @param addressList + * @throws IOException + */ + public XMemcachedClient(final List addressList) throws IOException { + this(addressList, new TextCommandFactory()); + } + + /** + * XMemcached Constructor.Every server's weight is one by default. + * + * @param cmdFactory command factory + * @param addressList memcached server socket address list. + * @throws IOException + */ + public XMemcachedClient(final List addressList, + final CommandFactory cmdFactory) throws IOException { + super(); + if (cmdFactory == null) { + throw new IllegalArgumentException("Null command factory."); + } + if (addressList == null || addressList.isEmpty()) { + throw new IllegalArgumentException("Empty address list"); + } + BufferAllocator simpleBufferAllocator = new SimpleBufferAllocator(); + buildConnector(new ArrayMemcachedSessionLocator(), new IndexMemcachedSessionComparator(), + simpleBufferAllocator, XMemcachedClientBuilder.getDefaultConfiguration(), + XMemcachedClientBuilder.getDefaultSocketOptions(), cmdFactory, new SerializingTranscoder()); + start0(); + for (InetSocketAddress inetSocketAddress : addressList) { + connect(new InetSocketAddressWrapper(inetSocketAddress, + this.serverOrderCount.incrementAndGet(), 1, null, this.resolveInetAddresses)); + + } + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.lang.String, long, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + @SuppressWarnings("unchecked") + public final T get(final String key, final long timeout, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return (T) this.get0(key, timeout, CommandType.GET_ONE, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.lang.String, long) + */ + @SuppressWarnings("unchecked") + public final T get(final String key, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + return (T) this.get(key, timeout, this.transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.lang.String, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final T get(final String key, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.get(key, this.opTimeout, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.lang.String) + */ + @SuppressWarnings("unchecked") + public final T get(final String key) + throws TimeoutException, InterruptedException, MemcachedException { + return (T) this.get(key, this.opTimeout); + } + + private Object get0(String key, final long timeout, final CommandType cmdType, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + return this.fetch0(key, keyBytes, cmdType, timeout, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.lang.String, long, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + @SuppressWarnings("unchecked") + public final GetsResponse gets(final String key, final long timeout, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return (GetsResponse) this.get0(key, timeout, CommandType.GETS_ONE, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.lang.String) + */ + public final GetsResponse gets(final String key) + throws TimeoutException, InterruptedException, MemcachedException { + return this.gets(key, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.lang.String, long) + */ + @SuppressWarnings("unchecked") + public final GetsResponse gets(final String key, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + return this.gets(key, timeout, this.transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.lang.String, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + @SuppressWarnings("unchecked") + public final GetsResponse gets(final String key, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.gets(key, this.opTimeout, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.util.Collection, long, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final Map get(final Collection keyCollections, final long timeout, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.getMulti0(keyCollections, timeout, CommandType.GET_MANY, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.util.Collection, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final Map get(final Collection keyCollections, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.getMulti0(keyCollections, this.opTimeout, CommandType.GET_MANY, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.util.Collection) + */ + public final Map get(final Collection keyCollections) + throws TimeoutException, InterruptedException, MemcachedException { + return this.get(keyCollections, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#get(java.util.Collection, long) + */ + @SuppressWarnings("unchecked") + public final Map get(final Collection keyCollections, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + return this.get(keyCollections, timeout, this.transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.util.Collection, long, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + @SuppressWarnings("unchecked") + public final Map> gets(final Collection keyCollections, + final long timeout, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return (Map>) this.getMulti0(keyCollections, timeout, + CommandType.GETS_MANY, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.util.Collection) + */ + public final Map> gets(final Collection keyCollections) + throws TimeoutException, InterruptedException, MemcachedException { + return this.gets(keyCollections, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.util.Collection, long) + */ + @SuppressWarnings("unchecked") + public final Map> gets(final Collection keyCollections, + final long timeout) throws TimeoutException, InterruptedException, MemcachedException { + return this.gets(keyCollections, timeout, this.transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#gets(java.util.Collection, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final Map> gets(final Collection keyCollections, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.gets(keyCollections, this.opTimeout, transcoder); + } + + private final Map getMulti0(final Collection keys, final long timeout, + final CommandType cmdType, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + if (keys == null || keys.size() == 0) { + return null; + } + Collection keyCollections = new ArrayList(keys.size()); + for (String key : keys) { + keyCollections.add(preProcessKey(key)); + } + final CountDownLatch latch; + final List commands; + if (this.connector.getSessionSet().size() <= 1) { + commands = new ArrayList(1); + latch = new CountDownLatch(1); + commands.add(this.sendGetMultiCommand(keyCollections, latch, cmdType, transcoder)); + + } else { + Collection> catalogKeys = catalogKeys(keyCollections); + commands = new ArrayList(catalogKeys.size()); + latch = new CountDownLatch(catalogKeys.size()); + for (List catalogKeyCollection : catalogKeys) { + commands.add(this.sendGetMultiCommand(catalogKeyCollection, latch, cmdType, transcoder)); + } + } + if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { + for (Command getCmd : commands) { + getCmd.cancel(); + } + throw new TimeoutException("Timed out waiting for operation"); + } + return this.reduceResult(cmdType, transcoder, commands); + } + + @SuppressWarnings("unchecked") + private Map reduceResult(final CommandType cmdType, final Transcoder transcoder, + final List commands) + throws MemcachedException, InterruptedException, TimeoutException { + final Map result = new HashMap(commands.size()); + for (Command getCmd : commands) { + getCmd.getIoBuffer().free(); + checkException(getCmd); + Map map = (Map) getCmd.getResult(); + if (cmdType == CommandType.GET_MANY) { + Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + String decodeKey = decodeKey(entry.getKey()); + if (decodeKey != null) { + result.put(decodeKey, transcoder.decode(entry.getValue())); + } + } + + } else { + Iterator> it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + GetsResponse getsResponse = + new GetsResponse(entry.getValue().getCas(), transcoder.decode(entry.getValue())); + String decodeKey = decodeKey(entry.getKey()); + if (decodeKey != null) { + result.put(decodeKey, (T) getsResponse); + } + } + + } + + } + return result; + } + + /** + * Hash key to servers + * + * @param keyCollections + * @return + */ + private final Collection> catalogKeys(final Collection keyCollections) { + final Map> catalogMap = new HashMap>(); + + for (String key : keyCollections) { + Session index = this.sessionLocator.getSessionByKey(key); + List tmpKeys = catalogMap.get(index); + if (tmpKeys == null) { + tmpKeys = new ArrayList(10); + catalogMap.put(index, tmpKeys); + } + tmpKeys.add(key); + } + + Collection> catalogKeys = catalogMap.values(); + return catalogKeys; + } + + private final Command sendGetMultiCommand(final Collection keys, + final CountDownLatch latch, final CommandType cmdType, final Transcoder transcoder) + throws InterruptedException, TimeoutException, MemcachedException { + final Command command = + this.commandFactory.createGetMultiCommand(keys, latch, cmdType, transcoder); + sendCommand(command); + return command; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#set(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder, long) + */ + public final boolean set(String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, exp, value); + return this.sendStoreCommand( + this.commandFactory.createSetCommand(key, keyBytes, exp, value, false, transcoder), + timeout); + } + + @SuppressWarnings("unchecked") + public void setWithNoReply(final String key, final int exp, final Object value) + throws InterruptedException, MemcachedException { + this.setWithNoReply(key, exp, value, this.transcoder); + } + + public void setWithNoReply(String key, final int exp, final T value, + final Transcoder transcoder) throws InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, exp, value); + try { + this.sendStoreCommand( + this.commandFactory.createSetCommand(key, keyBytes, exp, value, true, transcoder), + this.opTimeout); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + private final byte[] checkStoreArguments(final String key, final int exp, final T value) { + byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + if (value == null) { + throw new IllegalArgumentException("value could not be null"); + } + if (exp < 0) { + throw new IllegalArgumentException("Expire time must be greater than or equal to 0"); + } + return keyBytes; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#set(java.lang.String, int, java.lang.Object) + */ + public final boolean set(final String key, final int exp, final Object value) + throws TimeoutException, InterruptedException, MemcachedException { + return this.set(key, exp, value, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#set(java.lang.String, int, java.lang.Object, long) + */ + @SuppressWarnings("unchecked") + public final boolean set(final String key, final int exp, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + return this.set(key, exp, value, this.transcoder, timeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#set(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final boolean set(final String key, final int exp, final T value, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.set(key, exp, value, transcoder, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#add(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder, long) + */ + public final boolean add(String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return this.add0(key, exp, value, transcoder, timeout); + } + + private boolean add0(final String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws InterruptedException, TimeoutException, MemcachedException { + byte[] keyBytes = this.checkStoreArguments(key, exp, value); + return this.sendStoreCommand( + this.commandFactory.createAddCommand(key, keyBytes, exp, value, false, transcoder), + timeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#add(java.lang.String, int, java.lang.Object) + */ + public final boolean add(final String key, final int exp, final Object value) + throws TimeoutException, InterruptedException, MemcachedException { + return this.add(key, exp, value, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#add(java.lang.String, int, java.lang.Object, long) + */ + @SuppressWarnings("unchecked") + public final boolean add(final String key, final int exp, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + return this.add(key, exp, value, this.transcoder, timeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#add(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final boolean add(final String key, final int exp, final T value, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.add(key, exp, value, transcoder, this.opTimeout); + } + + @SuppressWarnings("unchecked") + public void addWithNoReply(final String key, final int exp, final Object value) + throws InterruptedException, MemcachedException { + this.addWithNoReply(key, exp, value, this.transcoder); + + } + + public void addWithNoReply(String key, final int exp, final T value, + final Transcoder transcoder) throws InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, exp, value); + try { + this.sendStoreCommand( + this.commandFactory.createAddCommand(key, keyBytes, exp, value, true, transcoder), + this.opTimeout); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + + } + + @SuppressWarnings("unchecked") + public void replaceWithNoReply(final String key, final int exp, final Object value) + throws InterruptedException, MemcachedException { + this.replaceWithNoReply(key, exp, value, this.transcoder); + + } + + public void replaceWithNoReply(String key, final int exp, final T value, + final Transcoder transcoder) throws InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, exp, value); + try { + this.sendStoreCommand( + this.commandFactory.createReplaceCommand(key, keyBytes, exp, value, true, transcoder), + this.opTimeout); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#replace(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder, long) + */ + public final boolean replace(String key, final int exp, final T value, + final Transcoder transcoder, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, exp, value); + return this.sendStoreCommand( + this.commandFactory.createReplaceCommand(key, keyBytes, exp, value, false, transcoder), + timeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#replace(java.lang.String, int, java.lang.Object) + */ + public final boolean replace(final String key, final int exp, final Object value) + throws TimeoutException, InterruptedException, MemcachedException { + return this.replace(key, exp, value, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#replace(java.lang.String, int, java.lang.Object, + * long) + */ + @SuppressWarnings("unchecked") + public final boolean replace(final String key, final int exp, final Object value, + final long timeout) throws TimeoutException, InterruptedException, MemcachedException { + return this.replace(key, exp, value, this.transcoder, timeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#replace(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final boolean replace(final String key, final int exp, final T value, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + return this.replace(key, exp, value, transcoder, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#append(java.lang.String, java.lang.Object) + */ + public final boolean append(final String key, final Object value) + throws TimeoutException, InterruptedException, MemcachedException { + return this.append(key, value, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#append(java.lang.String, java.lang.Object, long) + */ + public final boolean append(String key, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, 0, value); + return this.sendStoreCommand( + this.commandFactory.createAppendCommand(key, keyBytes, value, false, this.transcoder), + timeout); + } + + public void appendWithNoReply(String key, final Object value) + throws InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, 0, value); + try { + this.sendStoreCommand( + this.commandFactory.createAppendCommand(key, keyBytes, value, true, this.transcoder), + this.opTimeout); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#prepend(java.lang.String, java.lang.Object) + */ + public final boolean prepend(final String key, final Object value) + throws TimeoutException, InterruptedException, MemcachedException { + return this.prepend(key, value, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#prepend(java.lang.String, java.lang.Object, long) + */ + public final boolean prepend(String key, final Object value, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, 0, value); + return this.sendStoreCommand( + this.commandFactory.createPrependCommand(key, keyBytes, value, false, this.transcoder), + timeout); + } + + public void prependWithNoReply(String key, final Object value) + throws InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, 0, value); + try { + this.sendStoreCommand( + this.commandFactory.createPrependCommand(key, keyBytes, value, true, this.transcoder), + this.opTimeout); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, java.lang.Object, long) + */ + public final boolean cas(final String key, final int exp, final Object value, final long cas) + throws TimeoutException, InterruptedException, MemcachedException { + return this.cas(key, exp, value, this.opTimeout, cas); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder, long, long) + */ + public final boolean cas(String key, final int exp, final T value, + final Transcoder transcoder, final long timeout, final long cas) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = this.checkStoreArguments(key, 0, value); + return this.sendStoreCommand( + this.commandFactory.createCASCommand(key, keyBytes, exp, value, cas, false, transcoder), + timeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, java.lang.Object, long, + * long) + */ + @SuppressWarnings("unchecked") + public final boolean cas(final String key, final int exp, final Object value, final long timeout, + final long cas) throws TimeoutException, InterruptedException, MemcachedException { + return this.cas(key, exp, value, this.transcoder, timeout, cas); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, T, + * net.rubyeye.xmemcached.transcoders.Transcoder, long) + */ + public final boolean cas(final String key, final int exp, final T value, + final Transcoder transcoder, final long cas) + throws TimeoutException, InterruptedException, MemcachedException { + return this.cas(key, exp, value, transcoder, this.opTimeout, cas); + } + + private final boolean cas0(final String key, final int exp, + final GetsResponse getsResponse, final CASOperation operation, + final Transcoder transcoder, final byte[] keyBytes, final boolean noreply) + throws TimeoutException, InterruptedException, MemcachedException { + if (operation == null) { + throw new IllegalArgumentException("CASOperation could not be null"); + } + if (operation.getMaxTries() < 0) { + throw new IllegalArgumentException("max tries must be greater than 0"); + } + int tryCount = 0; + GetsResponse result = getsResponse; + if (result == null) { + throw new NoValueException("Null GetsResponse for key=" + key); + } + while (tryCount <= operation.getMaxTries() && result != null + && !this.sendStoreCommand(this.commandFactory.createCASCommand(key, keyBytes, exp, + operation.getNewValue(result.getCas(), result.getValue()), result.getCas(), noreply, + transcoder), this.opTimeout) + && !noreply) { + tryCount++; + result = this.gets0(key, keyBytes, transcoder); + if (result == null) { + throw new NoValueException("could not gets the value for Key=" + key + " for cas"); + } + if (tryCount > operation.getMaxTries()) { + throw new TimeoutException("CAS try times is greater than max"); + } + } + return true; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, + * net.rubyeye.xmemcached.CASOperation, net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final boolean cas(String key, final int exp, final CASOperation operation, + final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + GetsResponse result = this.gets0(key, keyBytes, transcoder); + return this.cas0(key, exp, result, operation, transcoder, keyBytes, false); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, + * net.rubyeye.xmemcached.GetsResponse, net.rubyeye.xmemcached.CASOperation, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final boolean cas(String key, final int exp, final GetsResponse getsReponse, + final CASOperation operation, final Transcoder transcoder) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + return this.cas0(key, exp, getsReponse, operation, transcoder, keyBytes, false); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, + * net.rubyeye.xmemcached.GetsResponse, net.rubyeye.xmemcached.CASOperation) + */ + @SuppressWarnings("unchecked") + public final boolean cas(final String key, final int exp, final GetsResponse getsReponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + + return this.cas(key, exp, getsReponse, operation, this.transcoder); + } + + public void casWithNoReply(final String key, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + this.casWithNoReply(key, 0, operation); + } + + public void casWithNoReply(final String key, final GetsResponse getsResponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + this.casWithNoReply(key, 0, getsResponse, operation); + + } + + @SuppressWarnings("unchecked") + public void casWithNoReply(String key, final int exp, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = ByteUtils.getBytes(key); + GetsResponse result = this.gets0(key, keyBytes, this.transcoder); + this.casWithNoReply(key, exp, result, operation); + + } + + @SuppressWarnings("unchecked") + public void casWithNoReply(String key, final int exp, final GetsResponse getsReponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + this.cas0(key, exp, getsReponse, operation, this.transcoder, keyBytes, true); + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, + * net.rubyeye.xmemcached.GetsResponse, net.rubyeye.xmemcached.CASOperation) + */ + public final boolean cas(final String key, final GetsResponse getsReponse, + final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + return this.cas(key, 0, getsReponse, operation); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, int, + * net.rubyeye.xmemcached.CASOperation) + */ + @SuppressWarnings("unchecked") + public final boolean cas(final String key, final int exp, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + return this.cas(key, exp, operation, this.transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#cas(java.lang.String, + * net.rubyeye.xmemcached.CASOperation) + */ + public final boolean cas(final String key, final CASOperation operation) + throws TimeoutException, InterruptedException, MemcachedException { + return this.cas(key, 0, operation); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#delete(java.lang.String, int) + */ + public final boolean delete(final String key, final int time) + throws TimeoutException, InterruptedException, MemcachedException { + return delete0(key, time, 0, false, this.opTimeout); + } + + public boolean delete(final String key, final long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException { + return delete0(key, 0, 0, false, opTimeout); + } + + public boolean delete(final String key, final long cas, final long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException { + return delete0(key, 0, cas, false, opTimeout); + } + + /** + * Delete key's data item from memcached.This method doesn't wait for reply + * + * @param key + * @param time + * @throws InterruptedException + * @throws MemcachedException + */ + public final void deleteWithNoReply(final String key, final int time) + throws InterruptedException, MemcachedException { + try { + delete0(key, time, 0, true, this.opTimeout); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + public final void deleteWithNoReply(final String key) + throws InterruptedException, MemcachedException { + this.deleteWithNoReply(key, 0); + } + + private boolean delete0(String key, final int time, final long cas, final boolean noreply, + final long opTimeout) throws MemcachedException, InterruptedException, TimeoutException { + key = preProcessKey(key); + final byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + final Command command = + this.commandFactory.createDeleteCommand(key, keyBytes, time, cas, noreply); + final Session session = sendCommand(command); + if (!command.isNoreply()) { + latchWait(command, opTimeout, session); + command.getIoBuffer().free(); + checkException(command); + if (command.getResult() == null) { + throw new MemcachedException("Operation fail,may be caused by networking or timeout"); + } + } else { + return false; + } + return (Boolean) command.getResult(); + } + + protected void checkException(final Command command) throws MemcachedException { + if (command.getException() != null) { + if (command.getException() instanceof MemcachedException) { + throw (MemcachedException) command.getException(); + } else { + throw new MemcachedException(command.getException()); + } + } + } + + public boolean touch(String key, final int exp, final long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + final byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + CountDownLatch latch = new CountDownLatch(1); + final Command command = + this.commandFactory.createTouchCommand(key, keyBytes, latch, exp, false); + latchWait(command, opTimeout, sendCommand(command)); + command.getIoBuffer().free(); + checkException(command); + if (command.getResult() == null) { + throw new MemcachedException("Operation fail,may be caused by networking or timeout"); + } + return (Boolean) command.getResult(); + } + + public boolean touch(final String key, final int exp) + throws TimeoutException, InterruptedException, MemcachedException { + return this.touch(key, exp, this.opTimeout); + } + + @SuppressWarnings("unchecked") + public T getAndTouch(String key, final int newExp, final long opTimeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + final byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + CountDownLatch latch = new CountDownLatch(1); + final Command command = + this.commandFactory.createGetAndTouchCommand(key, keyBytes, latch, newExp, false); + latchWait(command, opTimeout, sendCommand(command)); + command.getIoBuffer().free(); + checkException(command); + CachedData data = (CachedData) command.getResult(); + if (data == null) { + return null; + } + return (T) this.transcoder.decode(data); + } + + @SuppressWarnings("unchecked") + public T getAndTouch(final String key, final int newExp) + throws TimeoutException, InterruptedException, MemcachedException { + return (T) this.getAndTouch(key, newExp, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#incr(java.lang.String, int) + */ + public final long incr(String key, final long delta) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, 0, CommandType.INCR, false, this.opTimeout, 0); + } + + public long incr(String key, final long delta, final long initValue) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, initValue, CommandType.INCR, false, this.opTimeout, 0); + } + + public long incr(String key, final long delta, final long initValue, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, initValue, CommandType.INCR, false, timeout, 0); + } + + public long incr(String key, final long delta, final long initValue, final long timeout, + final int exp) throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, initValue, CommandType.INCR, false, timeout, exp); + } + + public final void incrWithNoReply(String key, final long delta) + throws InterruptedException, MemcachedException { + key = preProcessKey(key); + try { + sendIncrOrDecrCommand(key, delta, 0, CommandType.INCR, true, this.opTimeout, 0); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + public final void decrWithNoReply(String key, final long delta) + throws InterruptedException, MemcachedException { + key = preProcessKey(key); + try { + sendIncrOrDecrCommand(key, delta, 0, CommandType.DECR, true, this.opTimeout, 0); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#decr(java.lang.String, int) + */ + public final long decr(String key, final long delta) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, 0, CommandType.DECR, false, this.opTimeout, 0); + } + + public long decr(String key, final long delta, final long initValue) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, initValue, CommandType.DECR, false, this.opTimeout, 0); + } + + public long decr(String key, final long delta, final long initValue, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, initValue, CommandType.DECR, false, timeout, 0); + } + + public long decr(String key, final long delta, final long initValue, final long timeout, + final int exp) throws TimeoutException, InterruptedException, MemcachedException { + key = preProcessKey(key); + return sendIncrOrDecrCommand(key, delta, initValue, CommandType.DECR, false, timeout, exp); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#flushAll() + */ + public final void flushAll() throws TimeoutException, InterruptedException, MemcachedException { + this.flushAll(this.opTimeout); + } + + public void flushAllWithNoReply() throws InterruptedException, MemcachedException { + try { + flushAllMemcachedServers(this.opTimeout, true, 0); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + public void flushAllWithNoReply(final int exptime) + throws InterruptedException, MemcachedException { + try { + flushAllMemcachedServers(this.opTimeout, true, exptime); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + public void flushAllWithNoReply(final InetSocketAddress address) + throws MemcachedException, InterruptedException { + try { + flushSpecialMemcachedServer(address, this.opTimeout, true, 0); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + public void flushAllWithNoReply(final InetSocketAddress address, final int exptime) + throws MemcachedException, InterruptedException { + try { + flushSpecialMemcachedServer(address, this.opTimeout, true, exptime); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + } + + public final void flushAll(final int exptime, final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + flushAllMemcachedServers(timeout, false, exptime); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#flushAll(long) + */ + public final void flushAll(final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + flushAllMemcachedServers(timeout, false, 0); + } + + private void flushAllMemcachedServers(final long timeout, final boolean noreply, + final int exptime) throws MemcachedException, InterruptedException, TimeoutException { + final Collection sessions = this.connector.getSessionSet(); + CountDownLatch latch = new CountDownLatch(sessions.size()); + List commands = new ArrayList(sessions.size()); + for (Session session : sessions) { + if (session != null && !session.isClosed()) { + Command command = this.commandFactory.createFlushAllCommand(latch, exptime, noreply); + + session.write(command); + } else { + latch.countDown(); + } + } + if (!noreply) { + if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { + for (Command cmd : commands) { + cmd.cancel(); + } + throw new TimeoutException("Timed out waiting for operation"); + } + } + } + + public void setLoggingLevelVerbosity(final InetSocketAddress address, final int level) + throws TimeoutException, InterruptedException, MemcachedException { + setMemcachedLoggingLevel(address, level, false); + + } + + private void setMemcachedLoggingLevel(final InetSocketAddress address, final int level, + final boolean noreply) throws MemcachedException, InterruptedException, TimeoutException { + if (address == null) { + throw new IllegalArgumentException("Null adderss"); + } + CountDownLatch latch = new CountDownLatch(1); + + Queue sessionQueue = this.connector.getSessionByAddress(address); + if (sessionQueue == null || sessionQueue.peek() == null) { + throw new MemcachedException( + "could not find session for " + SystemUtils.getRawAddress(address) + ":" + + address.getPort() + ",maybe it have not been connected"); + } + + Command command = this.commandFactory.createVerbosityCommand(latch, level, noreply); + final Session session = sessionQueue.peek(); + session.write(command); + if (!noreply) { + latchWait(command, this.opTimeout, session); + } + } + + public void setLoggingLevelVerbosityWithNoReply(final InetSocketAddress address, final int level) + throws InterruptedException, MemcachedException { + try { + setMemcachedLoggingLevel(address, level, true); + } catch (TimeoutException e) { + throw new MemcachedException(e); + } + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#flushAll(java.net. InetSocketAddress ) + */ + public final void flushAll(final InetSocketAddress address) + throws MemcachedException, InterruptedException, TimeoutException { + this.flushAll(address, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#flushAll(java.net. InetSocketAddress , long) + */ + public final void flushAll(final InetSocketAddress address, final long timeout) + throws MemcachedException, InterruptedException, TimeoutException { + flushSpecialMemcachedServer(address, timeout, false, 0); + } + + public final void flushAll(final InetSocketAddress address, final long timeout, final int exptime) + throws MemcachedException, InterruptedException, TimeoutException { + flushSpecialMemcachedServer(address, timeout, false, exptime); + } + + private void flushSpecialMemcachedServer(final InetSocketAddress address, final long timeout, + final boolean noreply, final int exptime) + throws MemcachedException, InterruptedException, TimeoutException { + if (address == null) { + throw new IllegalArgumentException("Null adderss"); + } + CountDownLatch latch = new CountDownLatch(1); + + Queue sessionQueue = this.connector.getSessionByAddress(address); + if (sessionQueue == null || sessionQueue.peek() == null) { + throw new MemcachedException( + "could not find session for " + SystemUtils.getRawAddress(address) + ":" + + address.getPort() + ",maybe it have not been connected"); + } + Command command = this.commandFactory.createFlushAllCommand(latch, exptime, noreply); + final Session session = sessionQueue.peek(); + session.write(command); + if (!noreply) { + latchWait(command, timeout, session); + } + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#flushAll(java.lang.String) + */ + public final void flushAll(final String host) + throws TimeoutException, InterruptedException, MemcachedException { + this.flushAll(AddrUtil.getOneAddress(host), this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#stats(java.net.InetSocketAddress) + */ + public final Map stats(final InetSocketAddress address) + throws MemcachedException, InterruptedException, TimeoutException { + return this.stats(address, this.opTimeout); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#stats(java.net.InetSocketAddress, long) + */ + @SuppressWarnings("unchecked") + public final Map stats(final InetSocketAddress address, final long timeout) + throws MemcachedException, InterruptedException, TimeoutException { + if (address == null) { + throw new IllegalArgumentException("Null inetSocketAddress"); + } + CountDownLatch latch = new CountDownLatch(1); + + Queue sessionQueue = this.connector.getSessionByAddress(address); + if (sessionQueue == null || sessionQueue.peek() == null) { + throw new MemcachedException( + "could not find session for " + SystemUtils.getRawAddress(address) + ":" + + address.getPort() + ",maybe it have not been connected"); + } + Command command = this.commandFactory.createStatsCommand(address, latch, null); + final Session session = sessionQueue.peek(); + session.write(command); + latchWait(command, timeout, session); + return (Map) command.getResult(); + } + + public final Map> getStats() + throws MemcachedException, InterruptedException, TimeoutException { + return this.getStats(this.opTimeout); + } + + public final Map> getStatsByItem(final String itemName) + throws MemcachedException, InterruptedException, TimeoutException { + return this.getStatsByItem(itemName, this.opTimeout); + } + + @SuppressWarnings("unchecked") + public final Map> getStatsByItem(final String itemName, + final long timeout) throws MemcachedException, InterruptedException, TimeoutException { + final Set sessionSet = this.connector.getSessionSet(); + final Map> collectResult = + new HashMap>(); + if (sessionSet.size() == 0) { + return collectResult; + } + final CountDownLatch latch = new CountDownLatch(sessionSet.size()); + List commands = new ArrayList(sessionSet.size()); + for (Session session : sessionSet) { + Command command = + this.commandFactory.createStatsCommand(session.getRemoteSocketAddress(), latch, itemName); + + session.write(command); + commands.add(command); + + } + if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { + for (Command command : commands) { + command.cancel(); + } + throw new TimeoutException("Timed out waiting for operation"); + } + for (Command command : commands) { + checkException(command); + collectResult.put(((ServerAddressAware) command).getServer(), + (Map) command.getResult()); + } + return collectResult; + } + + public final Map getVersions() + throws TimeoutException, InterruptedException, MemcachedException { + return this.getVersions(this.opTimeout); + } + + public final Map getVersions(final long timeout) + throws TimeoutException, InterruptedException, MemcachedException { + final Set sessionSet = this.connector.getSessionSet(); + Map collectResult = new HashMap(); + if (sessionSet.size() == 0) { + return collectResult; + } + final CountDownLatch latch = new CountDownLatch(sessionSet.size()); + List commands = new ArrayList(sessionSet.size()); + for (Session session : sessionSet) { + Command command = + this.commandFactory.createVersionCommand(latch, session.getRemoteSocketAddress()); + session.write(command); + commands.add(command); + + } + + if (!latch.await(timeout, TimeUnit.MILLISECONDS)) { + for (Command command : commands) { + command.cancel(); + } + throw new TimeoutException("Timed out waiting for operation"); + } + for (Command command : commands) { + checkException(command); + collectResult.put(((ServerAddressAware) command).getServer(), (String) command.getResult()); + } + return collectResult; + } + + public Map> getStats(final long timeout) + throws MemcachedException, InterruptedException, TimeoutException { + return this.getStatsByItem(null, timeout); + } + + /** + * For subclass override. + */ + protected void shutdown0() { + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#shutdown() + */ + public final void shutdown() throws IOException { + if (this.shutdown) { + return; + } + shutdown0(); + this.shutdown = true; + this.connector.shuttingDown(); + this.connector.quitAllSessions(); + this.connector.stop(); + this.memcachedHandler.stop(); + XMemcachedMbeanServer.getInstance().shutdown(); + if (AddrUtil.isEnableShutDownHook() && !this.isHutdownHookCalled) { + try { + Runtime.getRuntime().removeShutdownHook(this.shutdownHookThread); + } catch (Exception e) { + // ignore; + } + } + } + + private long sendIncrOrDecrCommand(final String key, final long delta, final long initValue, + final CommandType cmdType, final boolean noreply, final long operationTimeout, final int exp) + throws InterruptedException, TimeoutException, MemcachedException { + final byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + final Command command = this.commandFactory.createIncrDecrCommand(key, keyBytes, delta, + initValue, exp, cmdType, noreply); + final Session session = sendCommand(command); + if (!command.isNoreply()) { + latchWait(command, operationTimeout, session); + command.getIoBuffer().free(); + checkException(command); + if (command.getResult() == null) { + throw new MemcachedException("Operation fail,may be caused by networking or timeout"); + } + final Object result = command.getResult(); + if (result instanceof String) { + if (((String) result).equals("NOT_FOUND")) { + if (this.add0(key, exp, String.valueOf(initValue), this.transcoder, this.opTimeout)) { + return initValue; + } else { + return sendIncrOrDecrCommand(key, delta, initValue, cmdType, noreply, operationTimeout, + exp); + } + } else { + throw new MemcachedException( + "Unknown result type for incr/decr:" + result.getClass() + ",result=" + result); + } + } else { + return (Long) command.getResult(); + } + } else { + return -1; + } + } + + public void setConnectionPoolSize(final int poolSize) { + if (!this.shutdown && getAvaliableServers().size() > 0) { + throw new IllegalStateException("Xmemcached client has been started"); + } + if (poolSize <= 0) { + throw new IllegalArgumentException("poolSize<=0"); + } + this.connectionPoolSize = poolSize; + this.connector.setConnectionPoolSize(poolSize); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#delete(java.lang.String) + */ + public final boolean delete(final String key) + throws TimeoutException, InterruptedException, MemcachedException { + return this.delete(key, 0); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#getTranscoder() + */ + @SuppressWarnings("unchecked") + public final Transcoder getTranscoder() { + return this.transcoder; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClient#setTranscoder(net.rubyeye. xmemcached + * .transcoders.Transcoder) + */ + @SuppressWarnings("unchecked") + public final void setTranscoder(final Transcoder transcoder) { + this.transcoder = transcoder; + } + + private final boolean sendStoreCommand(final Command command, final long timeout) + throws InterruptedException, TimeoutException, MemcachedException { + + final Session session = sendCommand(command); + if (!command.isNoreply()) { + latchWait(command, timeout, session); + command.getIoBuffer().free(); + checkException(command); + if (command.getResult() == null) { + throw new MemcachedException("Operation fail,may be caused by networking or timeout"); + } + } else { + return false; + } + return (Boolean) command.getResult(); + } + + private static final String CONTINUOUS_TIMEOUT_COUNTER = "ContinuousTimeouts"; + + protected void latchWait(final Command cmd, final long timeout, final Session session) + throws InterruptedException, TimeoutException { + if (cmd.getLatch().await(timeout, TimeUnit.MILLISECONDS)) { + AtomicInteger counter = getContinuousTimeoutCounter(session); + // reset counter. + if (counter.get() > 0) { + counter.set(0); + } + } else { + cmd.cancel(); + AtomicInteger counter = getContinuousTimeoutCounter(session); + if (counter.incrementAndGet() > this.timeoutExceptionThreshold) { + log.warn(session + " exceeded continuous timeout threshold,we will close it."); + try { + // reset counter. + counter.set(0); + session.close(); + } catch (Exception e) { + // ignore it. + } + } + throw new TimeoutException("Timed out(" + timeout + + " milliseconds) waiting for operation while connected to " + session); + } + } + + private AtomicInteger getContinuousTimeoutCounter(final Session session) { + AtomicInteger counter = (AtomicInteger) session.getAttribute(CONTINUOUS_TIMEOUT_COUNTER); + if (counter == null) { + counter = new AtomicInteger(0); + AtomicInteger oldCounter = + (AtomicInteger) session.setAttributeIfAbsent(CONTINUOUS_TIMEOUT_COUNTER, counter); + if (oldCounter != null) { + counter = oldCounter; + } + } + return counter; + } + + /** + * Use getAvailableServers() instead + * + * @deprecated + * @see MemcachedClient#getAvailableServers() + */ + @Deprecated + public final Collection getAvaliableServers() { + return getAvailableServers(); + } + + public Collection getAvailableServers() { + Set sessionSet = this.connector.getSessionSet(); + Set result = new HashSet(); + for (Session session : sessionSet) { + result.add(session.getRemoteSocketAddress()); + } + return Collections.unmodifiableSet(result); + } + + public final int getConnectionSizeBySocketAddress(final InetSocketAddress address) { + Queue sessionList = this.connector.getSessionByAddress(address); + return sessionList == null ? 0 : sessionList.size(); + } + + public void addStateListener(final MemcachedClientStateListener listener) { + MemcachedClientStateListenerAdapter adapter = + new MemcachedClientStateListenerAdapter(listener, this); + this.stateListenerAdapters.add(adapter); + this.connector.addStateListener(adapter); + } + + public Collection getStateListeners() { + final List result = + new ArrayList(this.stateListenerAdapters.size()); + for (MemcachedClientStateListenerAdapter adapter : this.stateListenerAdapters) { + result.add(adapter.getMemcachedClientStateListener()); + } + return result; + } + + public void setPrimitiveAsString(final boolean primitiveAsString) { + this.transcoder.setPrimitiveAsString(primitiveAsString); + } + + public void removeStateListener(final MemcachedClientStateListener listener) { + for (MemcachedClientStateListenerAdapter adapter : this.stateListenerAdapters) { + if (adapter.getMemcachedClientStateListener().equals(listener)) { + this.stateListenerAdapters.remove(adapter); + this.connector.removeStateListener(adapter); + } + } + } + + public Protocol getProtocol() { + return this.commandFactory.getProtocol(); + } + + public boolean isSanitizeKeys() { + return this.sanitizeKeys; + } + + public void setSanitizeKeys(final boolean sanitizeKeys) { + this.sanitizeKeys = sanitizeKeys; + } + + private String decodeKey(String key) + throws MemcachedException, InterruptedException, TimeoutException { + try { + key = this.sanitizeKeys ? URLDecoder.decode(key, "UTF-8") : key; + } catch (UnsupportedEncodingException e) { + throw new MemcachedException("Unsupport encoding utf-8 when decodeKey", e); + } + String ns = NAMESPACE_LOCAL.get(); + if (ns != null && ns.trim().length() > 0) { + String nsValue = getNamespace(ns); + try { + if (nsValue != null && key.startsWith(nsValue)) { + // The extra length of ':' + key = key.substring(nsValue.length() + 1); + } else { + return null; + } + } catch (Exception e) { + throw new MemcachedException("Exception occured when decode key.", e); + } + } + return key; + } + + private String preProcessKey(String key) throws MemcachedException, InterruptedException { + key = this.keyProvider.process(key); + try { + key = this.sanitizeKeys ? URLEncoder.encode(key, "UTF-8") : key; + } catch (UnsupportedEncodingException e) { + throw new MemcachedException("Unsupport encoding utf-8 when sanitize key", e); + } + String ns = NAMESPACE_LOCAL.get(); + if (ns != null && ns.trim().length() > 0) { + try { + key = getNamespace(ns) + ":" + key; + } catch (TimeoutException e) { + throw new MemcachedException("Timeout occured when gettting namespace value.", e); + } + } + return key; + } + + public void invalidateNamespace(final String ns, final long opTimeout) + throws MemcachedException, InterruptedException, TimeoutException { + String key = this.keyProvider.process(getNSKey(ns)); + sendIncrOrDecrCommand(key, 1, System.nanoTime(), CommandType.INCR, false, opTimeout, 0); + } + + public void invalidateNamespace(final String ns) + throws MemcachedException, InterruptedException, TimeoutException { + this.invalidateNamespace(ns, this.opTimeout); + } + + /** + * Returns the real namespace of ns. + * + * @param ns + * @return + * @throws TimeoutException + * @throws InterruptedException + * @throws MemcachedException + */ + public String getNamespace(final String ns) + throws TimeoutException, InterruptedException, MemcachedException { + String key = this.keyProvider.process(getNSKey(ns)); + byte[] keyBytes = ByteUtils.getBytes(key); + ByteUtils.checkKey(keyBytes); + Object item = this.fetch0(key, keyBytes, CommandType.GET_ONE, this.opTimeout, this.transcoder); + while (item == null) { + item = String.valueOf(System.nanoTime()); + boolean added = this.add0(key, 0, item, this.transcoder, this.opTimeout); + if (!added) { + item = this.fetch0(key, keyBytes, CommandType.GET_ONE, this.opTimeout, this.transcoder); + } + } + String namespace = item.toString(); + if (!ByteUtils.isNumber(namespace)) { + throw new IllegalStateException( + "Namespace key already has value.The key is:" + key + ",and the value is:" + namespace); + } + return namespace; + } + + private String getNSKey(final String ns) { + String key = "namespace:" + ns; + return key; + } + + public Counter getCounter(final String key, final long initialValue) { + return new Counter(this, key, initialValue); + } + + public Counter getCounter(final String key) { + return new Counter(this, key, 0); + } + + /** + * @deprecated memcached 1.6.x will remove cachedump stats command,so this method will be removed + * in the future + */ + @Deprecated + @SuppressWarnings("unchecked") + public KeyIterator getKeyIterator(final InetSocketAddress address) + throws MemcachedException, TimeoutException, InterruptedException { + if (address == null) { + throw new IllegalArgumentException("null address"); + } + Queue sessions = this.connector.getSessionByAddress(address); + if (sessions == null || sessions.size() == 0) { + throw new MemcachedException( + "The special memcached server has not been connected," + address); + } + Session session = sessions.peek(); + CountDownLatch latch = new CountDownLatch(1); + Command command = + this.commandFactory.createStatsCommand(session.getRemoteSocketAddress(), latch, "items"); + session.write(command); + if (!latch.await(5000, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Operation timeout"); + } + if (command.getException() != null) { + if (command.getException() instanceof MemcachedException) { + throw (MemcachedException) command.getException(); + } else { + throw new MemcachedException("stats items failed", command.getException()); + } + } + Map result = (Map) command.getResult(); + LinkedList itemNumberList = new LinkedList(); + for (Map.Entry entry : result.entrySet()) { + final String key = entry.getKey(); + final String[] keys = key.split(":"); + if (keys.length == 3 && keys[2].equals("number") && keys[0].equals("items")) { + // has items,then add it to itemNumberList + if (Integer.parseInt(entry.getValue()) > 0) { + itemNumberList.add(Integer.parseInt(keys[1])); + } + } + } + return new KeyIteratorImpl(itemNumberList, this, address); + } + + public void setEnableHealSession(final boolean enableHealSession) { + if (this.connector != null) { + this.connector.setEnableHealSession(enableHealSession); + } else { + throw new IllegalStateException("The client has not been started."); + } + } + + public void setFailureMode(final boolean failureMode) { + this.failureMode = failureMode; + if (this.sessionLocator != null) { + this.sessionLocator.setFailureMode(failureMode); + } + if (this.connector != null) { + this.connector.setFailureMode(failureMode); + } + } + + public boolean isFailureMode() { + return this.failureMode; + } + + public Queue getReconnectRequestQueue() { + return this.connector != null ? this.connector.getReconnectRequestQueue() : null; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/XMemcachedClientBuilder.java b/src/main/java/net/rubyeye/xmemcached/XMemcachedClientBuilder.java new file mode 100644 index 0000000..95fd109 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/XMemcachedClientBuilder.java @@ -0,0 +1,465 @@ +package net.rubyeye.xmemcached; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.DefaultKeyProvider; +import net.rubyeye.xmemcached.impl.IndexMemcachedSessionComparator; +import net.rubyeye.xmemcached.impl.RandomMemcachedSessionLocaltor; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.utils.Protocol; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.SocketOption; +import com.google.code.yanf4j.core.impl.StandardSocketOption; + +/** + * Builder pattern.Configure XmemcachedClient's options,then build it + * + * @author dennis + * + */ +public class XMemcachedClientBuilder implements MemcachedClientBuilder { + + private static final Logger log = LoggerFactory.getLogger(XMemcachedClientBuilder.class); + + protected MemcachedSessionLocator sessionLocator = new ArrayMemcachedSessionLocator(); + protected MemcachedSessionComparator sessionComparator = new IndexMemcachedSessionComparator(); + protected BufferAllocator bufferAllocator = new SimpleBufferAllocator(); + protected Configuration configuration = getDefaultConfiguration(); + protected Map addressMap = + new LinkedHashMap(); + + protected int[] weights; + + protected long connectTimeout = MemcachedClient.DEFAULT_CONNECT_TIMEOUT; + + protected int connectionPoolSize = MemcachedClient.DEFAULT_CONNECTION_POOL_SIZE; + + @SuppressWarnings("unchecked") + protected final Map socketOptions = getDefaultSocketOptions(); + + protected List stateListeners = + new ArrayList(); + + protected Map authInfoMap = + new HashMap(); + + protected String name; + + protected boolean failureMode; + + protected boolean sanitizeKeys; + + protected KeyProvider keyProvider = DefaultKeyProvider.INSTANCE; + + protected int maxQueuedNoReplyOperations = MemcachedClient.DEFAULT_MAX_QUEUED_NOPS; + + protected long healSessionInterval = MemcachedClient.DEFAULT_HEAL_SESSION_INTERVAL; + + protected boolean enableHealSession = true; + + protected long opTimeout = MemcachedClient.DEFAULT_OP_TIMEOUT; + + protected boolean resolveInetAddresses = true; + + public boolean isResolveInetAddresses() { + return resolveInetAddresses; + } + + public void setResolveInetAddresses(boolean resolveInetAddresses) { + this.resolveInetAddresses = resolveInetAddresses; + } + + public void doNotResolveInetAddresses() { + this.resolveInetAddresses = false; + } + + public long getOpTimeout() { + return opTimeout; + } + + public void setOpTimeout(long opTimeout) { + if (opTimeout <= 0) + throw new IllegalArgumentException("Invalid opTimeout value:" + opTimeout); + this.opTimeout = opTimeout; + } + + public int getMaxQueuedNoReplyOperations() { + return maxQueuedNoReplyOperations; + } + + public long getHealSessionInterval() { + return healSessionInterval; + } + + public void setHealSessionInterval(long healSessionInterval) { + this.healSessionInterval = healSessionInterval; + } + + public boolean isEnableHealSession() { + return enableHealSession; + } + + public void setEnableHealSession(boolean enableHealSession) { + this.enableHealSession = enableHealSession; + } + + /** + * Set max queued noreply operations number + * + * @see MemcachedClient#DEFAULT_MAX_QUEUED_NOPS + * @param maxQueuedNoReplyOperations + * @since 1.3.8 + */ + public void setMaxQueuedNoReplyOperations(int maxQueuedNoReplyOperations) { + this.maxQueuedNoReplyOperations = maxQueuedNoReplyOperations; + } + + public void setSanitizeKeys(boolean sanitizeKeys) { + this.sanitizeKeys = sanitizeKeys; + } + + public void addStateListener(MemcachedClientStateListener stateListener) { + this.stateListeners.add(stateListener); + } + + @SuppressWarnings("unchecked") + public void setSocketOption(SocketOption socketOption, Object value) { + if (socketOption == null) { + throw new NullPointerException("Null socketOption"); + } + if (value == null) { + throw new NullPointerException("Null value"); + } + if (!socketOption.type().equals(value.getClass())) { + throw new IllegalArgumentException("Expected " + socketOption.type().getSimpleName() + + " value,but givend " + value.getClass().getSimpleName()); + } + this.socketOptions.put(socketOption, value); + } + + @SuppressWarnings("unchecked") + public Map getSocketOptions() { + return this.socketOptions; + } + + public final void setConnectionPoolSize(int poolSize) { + if (this.connectionPoolSize <= 0) { + throw new IllegalArgumentException("poolSize<=0"); + } + this.connectionPoolSize = poolSize; + } + + public void removeStateListener(MemcachedClientStateListener stateListener) { + this.stateListeners.remove(stateListener); + } + + public long getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(long connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public void setStateListeners(List stateListeners) { + if (stateListeners == null) { + throw new IllegalArgumentException("Null state listeners"); + } + this.stateListeners = stateListeners; + } + + protected CommandFactory commandFactory = new TextCommandFactory(); + + @SuppressWarnings("unchecked") + public static final Map getDefaultSocketOptions() { + Map map = new HashMap(); + map.put(StandardSocketOption.TCP_NODELAY, MemcachedClient.DEFAULT_TCP_NO_DELAY); + map.put(StandardSocketOption.SO_RCVBUF, MemcachedClient.DEFAULT_TCP_RECV_BUFF_SIZE); + map.put(StandardSocketOption.SO_KEEPALIVE, MemcachedClient.DEFAULT_TCP_KEEPLIVE); + map.put(StandardSocketOption.SO_SNDBUF, MemcachedClient.DEFAULT_TCP_SEND_BUFF_SIZE); + map.put(StandardSocketOption.SO_LINGER, 0); + map.put(StandardSocketOption.SO_REUSEADDR, true); + return map; + } + + public static final Configuration getDefaultConfiguration() { + final Configuration configuration = new Configuration(); + configuration.setSessionReadBufferSize(MemcachedClient.DEFAULT_SESSION_READ_BUFF_SIZE); + configuration.setReadThreadCount(MemcachedClient.DEFAULT_READ_THREAD_COUNT); + configuration.setSessionIdleTimeout(MemcachedClient.DEFAULT_SESSION_IDLE_TIMEOUT); + configuration.setWriteThreadCount(0); + return configuration; + } + + public boolean isFailureMode() { + return this.failureMode; + } + + public void setFailureMode(boolean failureMode) { + this.failureMode = failureMode; + } + + public final CommandFactory getCommandFactory() { + return this.commandFactory; + } + + public final void setCommandFactory(CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @SuppressWarnings({"rawtypes"}) + protected Transcoder transcoder = new SerializingTranscoder(); + + public XMemcachedClientBuilder(String addressList) { + this(AddrUtil.getAddresses(addressList)); + } + + public XMemcachedClientBuilder(List addressList) { + if (addressList != null) { + for (InetSocketAddress addr : addressList) { + this.addressMap.put(addr, null); + } + } + } + + public XMemcachedClientBuilder(List addressList, int[] weights) { + if (addressList != null) { + for (InetSocketAddress addr : addressList) { + this.addressMap.put(addr, null); + } + } + this.weights = weights; + } + + public XMemcachedClientBuilder(Map addressMap) { + this.addressMap = addressMap; + } + + public XMemcachedClientBuilder(Map addressMap, + int[] weights) { + this.addressMap = addressMap; + this.weights = weights; + } + + public XMemcachedClientBuilder() { + this((Map) null); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#getSessionLocator() + */ + public MemcachedSessionLocator getSessionLocator() { + return this.sessionLocator; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setSessionLocator(net. rubyeye + * .xmemcached.MemcachedSessionLocator) + */ + public void setSessionLocator(MemcachedSessionLocator sessionLocator) { + if (sessionLocator == null) { + throw new IllegalArgumentException("Null SessionLocator"); + } + this.sessionLocator = sessionLocator; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#getSessionComparator() + */ + public MemcachedSessionComparator getSessionComparator() { + return this.sessionComparator; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setSessionComparator(net. rubyeye + * .xmemcached.MemcachedSessionComparator) + */ + public void setSessionComparator(MemcachedSessionComparator sessionComparator) { + if (sessionComparator == null) { + throw new IllegalArgumentException("Null SessionComparator"); + } + this.sessionComparator = sessionComparator; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#getBufferAllocator() + */ + public BufferAllocator getBufferAllocator() { + return this.bufferAllocator; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setBufferAllocator(net. + * rubyeye.xmemcached.buffer.BufferAllocator) + */ + public void setBufferAllocator(BufferAllocator bufferAllocator) { + if (bufferAllocator == null) { + throw new IllegalArgumentException("Null bufferAllocator"); + } + this.bufferAllocator = bufferAllocator; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#getConfiguration() + */ + public Configuration getConfiguration() { + return this.configuration; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setConfiguration(com.google + * .code.yanf4j.config.Configuration) + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#build() + */ + public MemcachedClient build() throws IOException { + XMemcachedClient memcachedClient; + // kestrel protocol use random session locator. + if (this.commandFactory.getProtocol() == Protocol.Kestrel) { + if (!(this.sessionLocator instanceof RandomMemcachedSessionLocaltor)) { + log.warn( + "Recommend to use `net.rubyeye.xmemcached.impl.RandomMemcachedSessionLocaltor` as session locator for kestrel protocol."); + } + } + if (this.weights == null) { + memcachedClient = + new XMemcachedClient(this.sessionLocator, this.sessionComparator, this.bufferAllocator, + this.configuration, this.socketOptions, this.commandFactory, this.transcoder, + this.addressMap, this.stateListeners, this.authInfoMap, this.connectionPoolSize, + this.connectTimeout, this.name, this.failureMode, this.resolveInetAddresses); + + } else { + if (this.addressMap == null) { + throw new IllegalArgumentException("Null Address map"); + } + if (this.addressMap.size() > this.weights.length) { + throw new IllegalArgumentException("Weights Array's length is less than server's number"); + } + memcachedClient = new XMemcachedClient(this.sessionLocator, this.sessionComparator, + this.bufferAllocator, this.configuration, this.socketOptions, this.commandFactory, + this.transcoder, this.addressMap, this.weights, this.stateListeners, this.authInfoMap, + this.connectionPoolSize, this.connectTimeout, this.name, this.failureMode, + this.resolveInetAddresses); + } + this.configureClient(memcachedClient); + return memcachedClient; + } + + protected void configureClient(XMemcachedClient memcachedClient) { + if (this.commandFactory.getProtocol() == Protocol.Kestrel) { + memcachedClient.setOptimizeGet(false); + } + memcachedClient.setConnectTimeout(connectTimeout); + memcachedClient.setSanitizeKeys(sanitizeKeys); + memcachedClient.setKeyProvider(this.keyProvider); + memcachedClient.setOpTimeout(this.opTimeout); + memcachedClient.setHealSessionInterval(this.healSessionInterval); + memcachedClient.setEnableHealSession(this.enableHealSession); + memcachedClient.setMaxQueuedNoReplyOperations(this.maxQueuedNoReplyOperations); + } + + @SuppressWarnings("rawtypes") + public Transcoder getTranscoder() { + return this.transcoder; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setTranscoder(transcoder) + */ + public void setTranscoder(Transcoder transcoder) { + if (transcoder == null) { + throw new IllegalArgumentException("Null Transcoder"); + } + this.transcoder = transcoder; + } + + public Map getAuthInfoMap() { + return this.authInfoMap; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setKeyProvider() + */ + public void setKeyProvider(KeyProvider keyProvider) { + if (keyProvider == null) + throw new IllegalArgumentException("null key provider"); + this.keyProvider = keyProvider; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#addAuthInfo() + */ + public void addAuthInfo(InetSocketAddress address, AuthInfo authInfo) { + this.authInfoMap.put(address, authInfo); + } + + public void removeAuthInfo(InetSocketAddress address) { + this.authInfoMap.remove(address); + } + + public void setAuthInfoMap(Map authInfoMap) { + this.authInfoMap = authInfoMap; + } + + public String getName() { + return this.name; + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.MemcachedClientBuilder#setName() + */ + public void setName(String name) { + this.name = name; + + } + + public void setSelectorPoolSize(int selectorPoolSize) { + getConfiguration().setSelectorPoolSize(selectorPoolSize); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/XMemcachedClientMBean.java b/src/main/java/net/rubyeye/xmemcached/XMemcachedClientMBean.java new file mode 100644 index 0000000..2375b88 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/XMemcachedClientMBean.java @@ -0,0 +1,61 @@ +package net.rubyeye.xmemcached; + +import java.io.IOException; +import java.util.List; + +/** + * XMemcachedClientMBean.It is used for JMX to add/remove memcached server. + * + * @author dennis + * + */ +public interface XMemcachedClientMBean { + + /** + * Add memcached servers + * + * @param host a String in the form of "[host1]:[port1],[host2]:[port2] + * [host3]:[port3],[host4]:[port4]" + */ + public void addServer(String hostList) throws IOException; + + /** + * Add a memcached server + * + * @param server a String in the form of "[host1]:[port1],[host2]:[port2]" + * @param weight server's weight + */ + public void addOneServerWithWeight(String server, int weight) throws IOException; + + /** + * Remove memcached servers + * + * @param host a string in the form of "[host1]:[port1],[host2]:[port2] + * [host3]:[port3],[host4]:[port4]" + */ + public void removeServer(String hostList); + + /** + * Get all connected memcached servers + * + * @return a list of string,every string is in the form of "[host1]:[port1](weight=num1) + * [host2]:[port2](weight=num1)" + */ + public List getServersDescription(); + + /** + * Set a memcached server's weight + * + * @param server + * @param weight + */ + public void setServerWeight(String server, int weight); + + /** + * Return the cache instance name + * + * @return + */ + public String getName(); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/auth/AuthInfo.java b/src/main/java/net/rubyeye/xmemcached/auth/AuthInfo.java new file mode 100644 index 0000000..7083667 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/auth/AuthInfo.java @@ -0,0 +1,85 @@ +package net.rubyeye.xmemcached.auth; + +import javax.security.auth.callback.CallbackHandler; + +/** + * Authentication infomation for a memcached server + * + * @author dennis + * + */ +public class AuthInfo { + private final CallbackHandler callbackHandler; + private final String[] mechanisms; + private final int maxAttempts = + Integer.parseInt(System.getProperty("net.rubyeye.xmemcached.auth_max_attempts", "-1")); + private int attempts; + + public synchronized boolean isValid() { + return this.attempts <= this.maxAttempts || this.maxAttempts < 0; + } + + public synchronized boolean isFirstTime() { + return this.attempts == 0; + } + + public synchronized void increaseAttempts() { + this.attempts++; + } + + public AuthInfo(CallbackHandler callbackHandler, String[] mechanisms) { + super(); + this.callbackHandler = callbackHandler; + this.mechanisms = mechanisms; + } + + public int getMaxAttempts() { + return maxAttempts; + } + + /** + * Get a typical auth descriptor for PLAIN auth with the given username and password. + * + * @param u the username + * @param p the password + * + * @return an AuthInfo + */ + public static AuthInfo plain(String username, String password) { + return new AuthInfo(new PlainCallbackHandler(username, password), new String[] {"PLAIN"}); + } + + /** + * Get a typical auth descriptor for CRAM-MD5 auth with the given username and password. + * + * @param u the username + * @param p the password + * + * @return an AuthInfo + */ + public static AuthInfo cramMD5(String username, String password) { + return new AuthInfo(new PlainCallbackHandler(username, password), new String[] {"CRAM-MD5"}); + } + + /** + * Get a typical auth descriptor for CRAM-MD5 or PLAIN auth with the given username and password. + * + * @param u the username + * @param p the password + * + * @return an AuthInfo + */ + public static AuthInfo typical(String username, String password) { + return new AuthInfo(new PlainCallbackHandler(username, password), + new String[] {"CRAM-MD5", "PLAIN"}); + } + + public CallbackHandler getCallbackHandler() { + return callbackHandler; + } + + public String[] getMechanisms() { + return mechanisms; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/auth/AuthMemcachedConnectListener.java b/src/main/java/net/rubyeye/xmemcached/auth/AuthMemcachedConnectListener.java new file mode 100644 index 0000000..12df72d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/auth/AuthMemcachedConnectListener.java @@ -0,0 +1,42 @@ +package net.rubyeye.xmemcached.auth; + +import java.net.InetSocketAddress; +import java.util.Map; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.XMemcachedClient; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.networking.MemcachedSessionConnectListener; +import net.rubyeye.xmemcached.utils.AddrUtil; + +/** + * Client state listener for auth + * + * @author dennis + * + */ +public class AuthMemcachedConnectListener implements MemcachedSessionConnectListener { + + public void onConnect(MemcachedSession session, MemcachedClient client) { + MemcachedTCPSession tcpSession = (MemcachedTCPSession) session; + Map authInfoMap = client.getAuthInfoStringMap(); + if (authInfoMap != null) { + AuthInfo authInfo = + authInfoMap.get(AddrUtil.getServerString(tcpSession.getRemoteSocketAddress())); + if (authInfo != null) { + XMemcachedClient xMemcachedClient = (XMemcachedClient) client; + AuthTask task = new AuthTask(authInfo, xMemcachedClient.getCommandFactory(), tcpSession); + task.start(); + // First time,try to wait + if (authInfo.isFirstTime()) { + try { + task.join(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/auth/AuthTask.java b/src/main/java/net/rubyeye/xmemcached/auth/AuthTask.java new file mode 100644 index 0000000..e4b85d2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/auth/AuthTask.java @@ -0,0 +1,149 @@ +package net.rubyeye.xmemcached.auth; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.BaseBinaryCommand; +import net.rubyeye.xmemcached.command.binary.ResponseStatus; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.MemcachedClient; + +/** + * Authentication task + * + * @author dennis + * + */ +public class AuthTask extends Thread { + private final AuthInfo authInfo; + private final CommandFactory commandFactory; + private MemcachedTCPSession memcachedTCPSession; + public static final byte[] EMPTY_BYTES = new byte[0]; + static final Logger log = LoggerFactory.getLogger(AuthTask.class); + private SaslClient saslClient; + + public AuthTask(AuthInfo authInfo, CommandFactory commandFactory, + MemcachedTCPSession memcachedTCPSession) { + super(); + this.authInfo = authInfo; + this.commandFactory = commandFactory; + this.memcachedTCPSession = memcachedTCPSession; + } + + public void run() { + if (this.authInfo.isValid()) { + doAuth(); + this.authInfo.increaseAttempts(); + } + } + + private void doAuth() { + try { + final AtomicBoolean done = new AtomicBoolean(false); + Command command = startAuth(); + + while (!done.get()) { + // wait previous command response + waitCommand(command, done); + // process response + ResponseStatus responseStatus = ((BaseBinaryCommand) command).getResponseStatus(); + switch (responseStatus) { + case NO_ERROR: + done.set(true); + log.info("Authentication to " + this.memcachedTCPSession.getRemoteSocketAddress() + + " successfully"); + break; + case AUTH_REQUIRED: + log.error( + "Authentication failed to " + this.memcachedTCPSession.getRemoteSocketAddress()); + log.warn("Reopen connection to " + this.memcachedTCPSession.getRemoteSocketAddress() + + ",beacause auth fail"); + this.memcachedTCPSession.setAuthFailed(true); + + // It it is not first time ,try to sleep 1 second + if (!this.authInfo.isFirstTime()) { + Thread.sleep(1000); + } + this.memcachedTCPSession.close(); + done.set(true); + break; + case FUTHER_AUTH_REQUIRED: + String result = String.valueOf(command.getResult()); + byte[] response = saslClient.evaluateChallenge(ByteUtils.getBytes(result)); + CountDownLatch latch = new CountDownLatch(1); + command = commandFactory.createAuthStepCommand(saslClient.getMechanismName(), latch, + response); + if (!this.memcachedTCPSession.isClosed()) + this.memcachedTCPSession.write(command); + else { + log.error("Authentication fail,because the connection has been closed"); + throw new RuntimeException("Authentication fai,connection has been close"); + } + + break; + default: + log.error( + "Authentication failed to " + this.memcachedTCPSession.getRemoteSocketAddress() + + ",response status=" + responseStatus); + command = startAuth(); + break; + + } + + } + } catch (Exception e) { + log.error("Create saslClient error", e); + } finally { + destroySaslClient(); + } + } + + private void destroySaslClient() { + if (saslClient != null) { + try { + saslClient.dispose(); + } catch (SaslException e) { + log.error("Dispose saslClient error", e); + } + this.saslClient = null; + } + } + + private Command startAuth() throws SaslException { + // destroy previous client. + destroySaslClient(); + + this.saslClient = Sasl.createSaslClient(authInfo.getMechanisms(), null, "memcached", + memcachedTCPSession.getRemoteSocketAddress().toString(), null, + this.authInfo.getCallbackHandler()); + byte[] response = + saslClient.hasInitialResponse() ? saslClient.evaluateChallenge(EMPTY_BYTES) : EMPTY_BYTES; + CountDownLatch latch = new CountDownLatch(1); + Command command = + this.commandFactory.createAuthStartCommand(saslClient.getMechanismName(), latch, response); + if (!this.memcachedTCPSession.isClosed()) + this.memcachedTCPSession.write(command); + else { + log.error("Authentication fail,because the connection has been closed"); + throw new RuntimeException("Authentication fai,connection has been close"); + } + return command; + } + + private void waitCommand(Command cmd, AtomicBoolean done) { + try { + cmd.getLatch().await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + done.set(true); + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/auth/PlainCallbackHandler.java b/src/main/java/net/rubyeye/xmemcached/auth/PlainCallbackHandler.java new file mode 100644 index 0000000..7669f75 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/auth/PlainCallbackHandler.java @@ -0,0 +1,38 @@ +package net.rubyeye.xmemcached.auth; + +import java.io.IOException; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +/** + * A callback handler for name/password authentication + * + * @author dennis + * + */ +public class PlainCallbackHandler implements CallbackHandler { + private String username; + private String password; + + public PlainCallbackHandler(String username, String password) { + super(); + this.username = username; + this.password = password; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(this.username); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(password.toCharArray()); + } else + throw new UnsupportedCallbackException(callback); + } + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/auth/package.html b/src/main/java/net/rubyeye/xmemcached/auth/package.html new file mode 100644 index 0000000..9bdce10 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/auth/package.html @@ -0,0 +1,10 @@ + + + + + SASL supporting for memcached 1.4.3 or later + + +

Memcached 1.4.3 or later version has supported SASL authentication,these classes are used for that

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryCacheClient.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryCacheClient.java new file mode 100644 index 0000000..25e0c47 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryCacheClient.java @@ -0,0 +1,290 @@ +package net.rubyeye.xmemcached.autodiscovery; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.SocketOption; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.MemcachedClientStateListener; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.XMemcachedClient; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.IndexMemcachedSessionComparator; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; + +/** + * Auto Discovery Client. + * + * @since 2.3.0 + * @author dennis + * + */ +public class AutoDiscoveryCacheClient extends XMemcachedClient implements ConfigUpdateListener { + + private static final Logger log = LoggerFactory.getLogger(AutoDiscoveryCacheClient.class); + + private boolean firstTimeUpdate = true; + + private List configAddrs = new ArrayList(); + + public synchronized void onUpdate(final ClusterConfiguration config) { + + if (this.firstTimeUpdate) { + this.firstTimeUpdate = false; + removeConfigAddrs(); + } + + List oldList = this.currentClusterConfiguration != null + ? this.currentClusterConfiguration.getNodeList() : Collections.EMPTY_LIST; + List newList = config.getNodeList(); + + List addNodes = new ArrayList(); + List removeNodes = new ArrayList(); + + for (CacheNode node : newList) { + if (!oldList.contains(node)) { + addNodes.add(node); + } + } + + for (CacheNode node : oldList) { + if (!newList.contains(node)) { + removeNodes.add(node); + } + } + + // Begin to update server list + for (CacheNode node : addNodes) { + try { + connect(new InetSocketAddressWrapper(node.getInetSocketAddress(), + this.configPoller.getCacheNodeOrder(node), 1, null, this.resolveInetAddresses)); + } catch (IOException e) { + log.error("Connect to " + node + "failed.", e); + } + } + + for (CacheNode node : removeNodes) { + try { + this.removeServer(node.getInetSocketAddress()); + } catch (Exception e) { + log.error("Remove " + node + " failed."); + } + } + + this.currentClusterConfiguration = config; + } + + private void removeConfigAddrs() { + for (InetSocketAddress configAddr : this.configAddrs) { + this.removeServer(configAddr); + while (getConnector().getSessionByAddress(configAddr) != null + && getConnector().getSessionByAddress(configAddr).size() > 0) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + private final ConfigurationPoller configPoller; + + /** + * Default elasticcache configuration poll interval, it's one minute. + */ + public static final long DEFAULT_POLL_CONFIG_INTERVAL_MS = 60000; + + /** + * Construct an AutoDiscoveryCacheClient instance with one config address and default poll interval. + * + * @since 2.3.0 + * @param addr config server address. + * @throws IOException + */ + public AutoDiscoveryCacheClient(final InetSocketAddress addr) throws IOException { + this(addr, DEFAULT_POLL_CONFIG_INTERVAL_MS); + } + + /** + * Construct an AutoDiscoveryCacheClient instance with one config address and poll interval. + * + * @since 2.3.0 + * @param addr config server address. + * @param pollConfigIntervalMills config poll interval in milliseconds. + * @throws IOException + */ + public AutoDiscoveryCacheClient(final InetSocketAddress addr, final long pollConfigIntervalMills) + throws IOException { + this(addr, pollConfigIntervalMills, new TextCommandFactory()); + } + + public AutoDiscoveryCacheClient(final InetSocketAddress addr, final long pollConfigIntervalMills, + final CommandFactory cmdFactory) throws IOException { + this(asList(addr), pollConfigIntervalMills, cmdFactory); + } + + private static List asList(final InetSocketAddress addr) { + List addrs = new ArrayList(); + addrs.add(addr); + return addrs; + } + + /** + * Construct an AutoDiscoveryCacheClient instance with config server addresses and default config + * poll interval. + * + * @since 2.3.0 + * @param addrs config server list. + * @throws IOException + */ + public AutoDiscoveryCacheClient(final List addrs) throws IOException { + this(addrs, DEFAULT_POLL_CONFIG_INTERVAL_MS); + } + + /** + * Construct an AutoDiscoveryCacheClient instance with config server addresses. + * + * @since 2.3.0 + * @param addrs + * @param pollConfigIntervalMills + * @throws IOException + */ + public AutoDiscoveryCacheClient(final List addrs, + final long pollConfigIntervalMills) throws IOException { + this(addrs, pollConfigIntervalMills, new TextCommandFactory()); + } + + /** + * Construct an AutoDiscoveryCacheClient instance with config server addresses. + * + * @since 2.3.0 + * @param addrs config server list. + * @param pollConfigIntervalMills config poll interval in milliseconds. + * @param commandFactory protocol command factory. + * @throws IOException + */ + @SuppressWarnings("unchecked") + public AutoDiscoveryCacheClient(final List addrs, + final long pollConfigIntervalMills, final CommandFactory commandFactory) throws IOException { + this(new ArrayMemcachedSessionLocator(), new IndexMemcachedSessionComparator(), + new SimpleBufferAllocator(), XMemcachedClientBuilder.getDefaultConfiguration(), + XMemcachedClientBuilder.getDefaultSocketOptions(), new TextCommandFactory(), + new SerializingTranscoder(), Collections.EMPTY_LIST, Collections.EMPTY_MAP, 1, + XMemcachedClient.DEFAULT_CONNECT_TIMEOUT, null, true, true, addrs, pollConfigIntervalMills); + + } + + private static Map getAddressMapFromConfigAddrs( + final List configAddrs) { + Map m = + new HashMap(); + for (InetSocketAddress addr : configAddrs) { + m.put(addr, null); + } + return m; + } + + AutoDiscoveryCacheClient(final MemcachedSessionLocator locator, + final MemcachedSessionComparator comparator, final BufferAllocator allocator, + final Configuration conf, final Map socketOptions, + final CommandFactory commandFactory, final Transcoder transcoder, + final List stateListeners, + final Map map, final int poolSize, final long connectTimeout, + final String name, final boolean failureMode, final boolean resolveInetAddresses, + final List configAddrs, final long pollConfigIntervalMills) + throws IOException { + super(locator, comparator, allocator, conf, socketOptions, commandFactory, transcoder, + getAddressMapFromConfigAddrs(configAddrs), stateListeners, map, poolSize, connectTimeout, + name, failureMode, resolveInetAddresses); + if (pollConfigIntervalMills <= 0) { + throw new IllegalArgumentException("Invalid pollConfigIntervalMills value."); + } + // Use failure mode by default. + this.commandFactory = commandFactory; + setFailureMode(true); + this.configAddrs = configAddrs; + this.configPoller = new ConfigurationPoller(this, pollConfigIntervalMills); + // Run at once to get config at startup. + // It will call onUpdate in the same thread. + this.configPoller.run(); + if (this.currentClusterConfiguration == null) { + throw new IllegalStateException( + "Retrieve ElasticCache config from `" + configAddrs.toString() + "` failed."); + } + this.configPoller.start(); + } + + private volatile ClusterConfiguration currentClusterConfiguration; + + /** + * Get cluster config from cache node by network command. + * + * @return + */ + public ClusterConfiguration getConfig() + throws MemcachedException, InterruptedException, TimeoutException { + return this.getConfig("cluster"); + } + + /** + * Get config by key from cache node by network command. + * + * @since 2.3.0 + * @return clusetr config. + */ + public ClusterConfiguration getConfig(final String key) + throws MemcachedException, InterruptedException, TimeoutException { + Command cmd = this.commandFactory.createAutoDiscoveryCacheConfigCommand("get", key); + final Session session = sendCommand(cmd); + latchWait(cmd, this.opTimeout, session); + cmd.getIoBuffer().free(); + checkException(cmd); + String result = (String) cmd.getResult(); + if (result == null) { + throw new MemcachedException("Operation fail,may be caused by networking or timeout"); + } + return AutoDiscoveryUtils.parseConfiguration(result); + } + + @Override + protected void shutdown0() { + super.shutdown0(); + if (this.configPoller != null) { + try { + this.configPoller.stop(); + } catch (Exception e) { + // ignore + } + } + } + + /** + * Get the current using configuration in memory. + * + * @since 2.3.0 + * @return current cluster config. + */ + public ClusterConfiguration getCurrentConfig() { + return this.currentClusterConfiguration; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryCacheClientBuilder.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryCacheClientBuilder.java new file mode 100644 index 0000000..86f5787 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryCacheClientBuilder.java @@ -0,0 +1,109 @@ +package net.rubyeye.xmemcached.autodiscovery; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.utils.AddrUtil; + +/** + * AutoDiscoveryCacheClient builder. + * + * @author dennis + * + */ +public class AutoDiscoveryCacheClientBuilder extends XMemcachedClientBuilder { + + /** + * Returns pollConfigIntervalMs. + * + * @return + */ + public long getPollConfigIntervalMs() { + return pollConfigIntervalMs; + } + + /** + * Set poll config interval in milliseconds. + * + * @param pollConfigIntervalMs + */ + public void setPollConfigIntervalMs(long pollConfigIntervalMs) { + this.pollConfigIntervalMs = pollConfigIntervalMs; + } + + /** + * Returns initial ElasticCache server addresses. + * + * @return + */ + public List getConfigAddrs() { + return configAddrs; + } + + /** + * Set initial ElasticCache server addresses. + * + * @param configAddrs + */ + public void setConfigAddrs(List configAddrs) { + this.configAddrs = configAddrs; + } + + private List configAddrs; + + private long pollConfigIntervalMs = AutoDiscoveryCacheClient.DEFAULT_POLL_CONFIG_INTERVAL_MS; + + /** + * Create a builder with an initial ElasticCache server list string in the form of "host:port + * host2:port". + * + * @param serverList server list string in the form of "host:port host2:port" + */ + public AutoDiscoveryCacheClientBuilder(String serverList) { + this(AddrUtil.getAddresses(serverList)); + } + + /** + * Create a builder with an initial ElasticCache server. + * + * @param addr + */ + public AutoDiscoveryCacheClientBuilder(InetSocketAddress addr) { + this(asList(addr)); + } + + private static List asList(InetSocketAddress addr) { + List ret = new ArrayList(); + ret.add(addr); + return ret; + } + + /** + * Create a builder with initial ElasticCache server addresses. + * + * @param configAddrs + */ + public AutoDiscoveryCacheClientBuilder(List configAddrs) { + super(configAddrs); + this.configAddrs = configAddrs; + } + + /** + * Returns a new instanceof AutoDiscoveryCacheClient. + */ + @Override + public AutoDiscoveryCacheClient build() throws IOException { + + AutoDiscoveryCacheClient memcachedClient = new AutoDiscoveryCacheClient(this.sessionLocator, + this.sessionComparator, this.bufferAllocator, this.configuration, this.socketOptions, + this.commandFactory, this.transcoder, this.stateListeners, this.authInfoMap, + this.connectionPoolSize, this.connectTimeout, this.name, this.failureMode, + this.resolveInetAddresses, configAddrs, this.pollConfigIntervalMs); + this.configureClient(memcachedClient); + + return memcachedClient; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryUtils.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryUtils.java new file mode 100644 index 0000000..a2615f2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/AutoDiscoveryUtils.java @@ -0,0 +1,59 @@ +package net.rubyeye.xmemcached.autodiscovery; + +import java.util.ArrayList; +import java.util.List; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Auto discovery get config command + * + * @author dennis + * + */ +public class AutoDiscoveryUtils { + + private static final String DELIMITER = "|"; + + /** + * Parse response string to ClusterConfiguration instance. + * + * @param line + * @return + */ + public static ClusterConfiguration parseConfiguration(String line) { + String[] lines = line.trim().split("(?:\\r?\\n)"); + if (lines.length < 2) { + throw new IllegalArgumentException("Incorrect config response:" + line); + } + String configversion = lines[0]; + String nodeListStr = lines[1]; + if (!ByteUtils.isNumber(configversion)) { + throw new IllegalArgumentException( + "Invalid configversion: " + configversion + ", it should be a number."); + } + String[] nodeStrs = nodeListStr.split("(?:\\s)+"); + int version = Integer.parseInt(configversion); + List nodeList = new ArrayList(nodeStrs.length); + for (String nodeStr : nodeStrs) { + if (nodeStr.equals("")) { + continue; + } + + int firstDelimiter = nodeStr.indexOf(DELIMITER); + int secondDelimiter = nodeStr.lastIndexOf(DELIMITER); + if (firstDelimiter < 1 || firstDelimiter == secondDelimiter) { + throw new IllegalArgumentException( + "Invalid server ''" + nodeStr + "'' in response: " + line); + } + String hostName = nodeStr.substring(0, firstDelimiter).trim(); + String ipAddress = nodeStr.substring(firstDelimiter + 1, secondDelimiter).trim(); + String portNum = nodeStr.substring(secondDelimiter + 1).trim(); + int port = Integer.parseInt(portNum); + nodeList.add(new CacheNode(hostName, ipAddress, port)); + } + + return new ClusterConfiguration(version, nodeList); + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/CacheNode.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/CacheNode.java new file mode 100644 index 0000000..627b59a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/CacheNode.java @@ -0,0 +1,91 @@ +package net.rubyeye.xmemcached.autodiscovery; + +import java.io.Serializable; +import java.net.InetSocketAddress; + +/** + * Auto Discovery Node information. + * + * @author dennis + * + */ +public class CacheNode implements Serializable { + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((hostName == null) ? 0 : hostName.hashCode()); + result = prime * result + port; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CacheNode other = (CacheNode) obj; + if (hostName == null) { + if (other.hostName != null) + return false; + } else if (!hostName.equals(other.hostName)) + return false; + if (port != other.port) + return false; + return true; + } + + private static final long serialVersionUID = -2999058612548153786L; + + public String getHostName() { + return hostName; + } + + public void setHostName(String hostName) { + this.hostName = hostName; + } + + public InetSocketAddress getInetSocketAddress() { + return new InetSocketAddress(hostName, port); + } + + public String getIpAddress() { + return ipAddress; + } + + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String toString() { + return "[" + this.hostName + "|" + this.ipAddress + "|" + this.port + "]"; + } + + public String getCacheKey() { + return this.hostName + ":" + this.port; + } + + private String hostName; + private String ipAddress; + private int port; + + public CacheNode(String hostName, String ipAddress, int port) { + super(); + this.hostName = hostName; + this.ipAddress = ipAddress; + this.port = port; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/ClusterConfiguration.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/ClusterConfiguration.java new file mode 100644 index 0000000..0a8b72e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/ClusterConfiguration.java @@ -0,0 +1,52 @@ +package net.rubyeye.xmemcached.autodiscovery; + +import java.io.Serializable; +import java.util.List; + +/** + * Cluster configuration retrieved from ElasticCache. + * + * @author dennis + * + */ +public class ClusterConfiguration implements Serializable { + + private static final long serialVersionUID = 6809891639636689050L; + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public List getNodeList() { + return nodeList; + } + + public void setNodeList(List nodeList) { + this.nodeList = nodeList; + } + + private int version; + private List nodeList; + + public ClusterConfiguration(int version, List nodeList) { + super(); + this.version = version; + this.nodeList = nodeList; + } + + public ClusterConfiguration() { + super(); + } + + public String toString() { + StringBuilder nodeList = new StringBuilder("{ Version: " + version + ", CacheNode List: "); + nodeList.append(this.nodeList); + nodeList.append("}"); + + return nodeList.toString(); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/ConfigUpdateListener.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/ConfigUpdateListener.java new file mode 100644 index 0000000..229b9cf --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/ConfigUpdateListener.java @@ -0,0 +1,17 @@ +package net.rubyeye.xmemcached.autodiscovery; + +/** + * Auto Discovery config update event listener. + * + * @author dennis + * + */ +public interface ConfigUpdateListener { + + /** + * Called when config is changed. + * + * @param config the new config + */ + public void onUpdate(ClusterConfiguration config); +} diff --git a/src/main/java/net/rubyeye/xmemcached/autodiscovery/ConfigurationPoller.java b/src/main/java/net/rubyeye/xmemcached/autodiscovery/ConfigurationPoller.java new file mode 100644 index 0000000..73b105b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/autodiscovery/ConfigurationPoller.java @@ -0,0 +1,110 @@ +package net.rubyeye.xmemcached.autodiscovery; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Auto Discovery configuration poller + * + * @author dennis + * + */ +public class ConfigurationPoller implements Runnable { + + /** + * Return current ClusterConfigration. + * + * @return + */ + public ClusterConfiguration getClusterConfiguration() { + return clusterConfigration; + } + + private final AtomicInteger serverOrderCounter = new AtomicInteger(0); + + private Map ordersMap = new HashMap(); + + public synchronized int getCacheNodeOrder(CacheNode node) { + Integer order = this.ordersMap.get(node.getCacheKey()); + if (order != null) { + return order; + } + order = this.serverOrderCounter.incrementAndGet(); + this.ordersMap.put(node.getCacheKey(), order); + return order; + } + + public synchronized void removeCacheNodeOrder(CacheNode node) { + this.ordersMap.remove(node.getCacheKey()); + } + + private static final Logger log = LoggerFactory.getLogger(ConfigurationPoller.class); + + private final AutoDiscoveryCacheClient client; + + private final long pollIntervalMills; + + private ScheduledExecutorService scheduledExecutorService; + + private volatile ClusterConfiguration clusterConfigration = null; + + public ConfigurationPoller(AutoDiscoveryCacheClient client, long pollIntervalMills) { + super(); + this.client = client; + this.pollIntervalMills = pollIntervalMills; + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "AutoDiscoveryCacheConfigPoller"); + t.setDaemon(true); + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + }); + } + + public void start() { + this.scheduledExecutorService.scheduleWithFixedDelay(this, this.pollIntervalMills, + this.pollIntervalMills, TimeUnit.MILLISECONDS); + } + + public void stop() { + this.scheduledExecutorService.shutdown(); + } + + public void run() { + try { + ClusterConfiguration newConfig = this.client.getConfig(); + if (newConfig != null) { + ClusterConfiguration currentConfig = this.clusterConfigration; + if (currentConfig == null) { + this.clusterConfigration = newConfig; + } else { + if (newConfig.getVersion() < currentConfig.getVersion()) { + log.warn("Ignored new config from ElasticCache node, it's too old, current version is: " + + currentConfig.getVersion() + ", but the new version is: " + + newConfig.getVersion()); + return; + } else { + this.clusterConfigration = newConfig; + } + } + log.info("Retrieved new config from ElasticCache node: " + this.clusterConfigration); + this.client.onUpdate(newConfig); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("Poll config from ElasticCache node failed", e); + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/aws/AWSElasticCacheClient.java b/src/main/java/net/rubyeye/xmemcached/aws/AWSElasticCacheClient.java new file mode 100644 index 0000000..0ab7b93 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/aws/AWSElasticCacheClient.java @@ -0,0 +1,295 @@ +package net.rubyeye.xmemcached.aws; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.autodiscovery.CacheNode; +import net.rubyeye.xmemcached.autodiscovery.ClusterConfiguration; +import net.rubyeye.xmemcached.autodiscovery.ConfigUpdateListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.SocketOption; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.MemcachedClientStateListener; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.XMemcachedClient; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.IndexMemcachedSessionComparator; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; + +/** + * AWS ElasticCache Client. + * + * @since 2.3.0 + * @author dennis + * @see net.rubyeye.xmemcached.autodiscovery.AutoDiscoveryCacheClient + * + */ +@Deprecated +public class AWSElasticCacheClient extends XMemcachedClient implements ConfigUpdateListener { + + private static final Logger log = LoggerFactory.getLogger(AWSElasticCacheClient.class); + + private boolean firstTimeUpdate = true; + + private List configAddrs = new ArrayList(); + + public synchronized void onUpdate(final ClusterConfiguration config) { + + if (this.firstTimeUpdate) { + this.firstTimeUpdate = false; + removeConfigAddrs(); + } + + List oldList = this.currentClusterConfiguration != null + ? this.currentClusterConfiguration.getNodeList() : Collections.EMPTY_LIST; + List newList = config.getNodeList(); + + List addNodes = new ArrayList(); + List removeNodes = new ArrayList(); + + for (CacheNode node : newList) { + if (!oldList.contains(node)) { + addNodes.add(node); + } + } + + for (CacheNode node : oldList) { + if (!newList.contains(node)) { + removeNodes.add(node); + } + } + + // Begin to update server list + for (CacheNode node : addNodes) { + try { + connect(new InetSocketAddressWrapper(node.getInetSocketAddress(), + this.configPoller.getCacheNodeOrder(node), 1, null, this.resolveInetAddresses)); + } catch (IOException e) { + log.error("Connect to " + node + "failed.", e); + } + } + + for (CacheNode node : removeNodes) { + try { + this.removeServer(node.getInetSocketAddress()); + } catch (Exception e) { + log.error("Remove " + node + " failed."); + } + } + + this.currentClusterConfiguration = config; + } + + private void removeConfigAddrs() { + for (InetSocketAddress configAddr : this.configAddrs) { + this.removeServer(configAddr); + while (getConnector().getSessionByAddress(configAddr) != null + && getConnector().getSessionByAddress(configAddr).size() > 0) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + + private final ConfigurationPoller configPoller; + + /** + * Default elasticcache configuration poll interval, it's one minute. + */ + public static final long DEFAULT_POLL_CONFIG_INTERVAL_MS = 60000; + + /** + * Construct an AWSElasticCacheClient instance with one config address and default poll interval. + * + * @since 2.3.0 + * @param addr config server address. + * @throws IOException + */ + public AWSElasticCacheClient(final InetSocketAddress addr) throws IOException { + this(addr, DEFAULT_POLL_CONFIG_INTERVAL_MS); + } + + /** + * Construct an AWSElasticCacheClient instance with one config address and poll interval. + * + * @since 2.3.0 + * @param addr config server address. + * @param pollConfigIntervalMills config poll interval in milliseconds. + * @throws IOException + */ + public AWSElasticCacheClient(final InetSocketAddress addr, final long pollConfigIntervalMills) + throws IOException { + this(addr, pollConfigIntervalMills, new TextCommandFactory()); + } + + public AWSElasticCacheClient(final InetSocketAddress addr, final long pollConfigIntervalMills, + final CommandFactory cmdFactory) throws IOException { + this(asList(addr), pollConfigIntervalMills, cmdFactory); + } + + private static List asList(final InetSocketAddress addr) { + List addrs = new ArrayList(); + addrs.add(addr); + return addrs; + } + + /** + * Construct an AWSElasticCacheClient instance with config server addresses and default config + * poll interval. + * + * @since 2.3.0 + * @param addrs config server list. + * @throws IOException + */ + public AWSElasticCacheClient(final List addrs) throws IOException { + this(addrs, DEFAULT_POLL_CONFIG_INTERVAL_MS); + } + + /** + * Construct an AWSElasticCacheClient instance with config server addresses. + * + * @since 2.3.0 + * @param addrs + * @param pollConfigIntervalMills + * @throws IOException + */ + public AWSElasticCacheClient(final List addrs, + final long pollConfigIntervalMills) throws IOException { + this(addrs, pollConfigIntervalMills, new TextCommandFactory()); + } + + /** + * Construct an AWSElasticCacheClient instance with config server addresses. + * + * @since 2.3.0 + * @param addrs config server list. + * @param pollConfigIntervalMills config poll interval in milliseconds. + * @param commandFactory protocol command factory. + * @throws IOException + */ + @SuppressWarnings("unchecked") + public AWSElasticCacheClient(final List addrs, + final long pollConfigIntervalMills, final CommandFactory commandFactory) throws IOException { + this(new ArrayMemcachedSessionLocator(), new IndexMemcachedSessionComparator(), + new SimpleBufferAllocator(), XMemcachedClientBuilder.getDefaultConfiguration(), + XMemcachedClientBuilder.getDefaultSocketOptions(), new TextCommandFactory(), + new SerializingTranscoder(), Collections.EMPTY_LIST, Collections.EMPTY_MAP, 1, + XMemcachedClient.DEFAULT_CONNECT_TIMEOUT, null, true, true, addrs, pollConfigIntervalMills); + + } + + private static Map getAddressMapFromConfigAddrs( + final List configAddrs) { + Map m = + new HashMap(); + for (InetSocketAddress addr : configAddrs) { + m.put(addr, null); + } + return m; + } + + AWSElasticCacheClient(final MemcachedSessionLocator locator, + final MemcachedSessionComparator comparator, final BufferAllocator allocator, + final Configuration conf, final Map socketOptions, + final CommandFactory commandFactory, final Transcoder transcoder, + final List stateListeners, + final Map map, final int poolSize, final long connectTimeout, + final String name, final boolean failureMode, final boolean resolveInetAddresses, + final List configAddrs, final long pollConfigIntervalMills) + throws IOException { + super(locator, comparator, allocator, conf, socketOptions, commandFactory, transcoder, + getAddressMapFromConfigAddrs(configAddrs), stateListeners, map, poolSize, connectTimeout, + name, failureMode, resolveInetAddresses); + if (pollConfigIntervalMills <= 0) { + throw new IllegalArgumentException("Invalid pollConfigIntervalMills value."); + } + // Use failure mode by default. + this.commandFactory = commandFactory; + setFailureMode(true); + this.configAddrs = configAddrs; + this.configPoller = new ConfigurationPoller(this, pollConfigIntervalMills); + // Run at once to get config at startup. + // It will call onUpdate in the same thread. + this.configPoller.run(); + if (this.currentClusterConfiguration == null) { + throw new IllegalStateException( + "Retrieve ElasticCache config from `" + configAddrs.toString() + "` failed."); + } + this.configPoller.start(); + } + + private volatile ClusterConfiguration currentClusterConfiguration; + + /** + * Get cluster config from cache node by network command. + * + * @return + */ + public ClusterConfiguration getConfig() + throws MemcachedException, InterruptedException, TimeoutException { + return this.getConfig("cluster"); + } + + /** + * Get config by key from cache node by network command. + * + * @since 2.3.0 + * @return clusetr config. + */ + public ClusterConfiguration getConfig(final String key) + throws MemcachedException, InterruptedException, TimeoutException { + Command cmd = this.commandFactory.createAutoDiscoveryCacheConfigCommand("get", key); + final Session session = sendCommand(cmd); + latchWait(cmd, this.opTimeout, session); + cmd.getIoBuffer().free(); + checkException(cmd); + String result = (String) cmd.getResult(); + if (result == null) { + throw new MemcachedException("Operation fail,may be caused by networking or timeout"); + } + return AWSUtils.parseConfiguration(result); + } + + @Override + protected void shutdown0() { + super.shutdown0(); + if (this.configPoller != null) { + try { + this.configPoller.stop(); + } catch (Exception e) { + // ignore + } + } + } + + /** + * Get the current using configuration in memory. + * + * @since 2.3.0 + * @return current cluster config. + */ + public ClusterConfiguration getCurrentConfig() { + return this.currentClusterConfiguration; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/aws/AWSElasticCacheClientBuilder.java b/src/main/java/net/rubyeye/xmemcached/aws/AWSElasticCacheClientBuilder.java new file mode 100644 index 0000000..09c88cb --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/aws/AWSElasticCacheClientBuilder.java @@ -0,0 +1,111 @@ +package net.rubyeye.xmemcached.aws; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.utils.AddrUtil; + +/** + * AWSElasticCacheClient builder. + * + * @author dennis + * @see net.rubyeye.xmemcached.autodiscovery.AutoDiscoveryCacheClientBuilder + * + */ +@Deprecated +public class AWSElasticCacheClientBuilder extends XMemcachedClientBuilder { + + /** + * Returns pollConfigIntervalMs. + * + * @return + */ + public long getPollConfigIntervalMs() { + return pollConfigIntervalMs; + } + + /** + * Set poll config interval in milliseconds. + * + * @param pollConfigIntervalMs + */ + public void setPollConfigIntervalMs(long pollConfigIntervalMs) { + this.pollConfigIntervalMs = pollConfigIntervalMs; + } + + /** + * Returns initial ElasticCache server addresses. + * + * @return + */ + public List getConfigAddrs() { + return configAddrs; + } + + /** + * Set initial ElasticCache server addresses. + * + * @param configAddrs + */ + public void setConfigAddrs(List configAddrs) { + this.configAddrs = configAddrs; + } + + private List configAddrs; + + private long pollConfigIntervalMs = AWSElasticCacheClient.DEFAULT_POLL_CONFIG_INTERVAL_MS; + + /** + * Create a builder with an initial ElasticCache server list string in the form of "host:port + * host2:port". + * + * @param serverList server list string in the form of "host:port host2:port" + */ + public AWSElasticCacheClientBuilder(String serverList) { + this(AddrUtil.getAddresses(serverList)); + } + + /** + * Create a builder with an initial ElasticCache server. + * + * @param addr + */ + public AWSElasticCacheClientBuilder(InetSocketAddress addr) { + this(asList(addr)); + } + + private static List asList(InetSocketAddress addr) { + List ret = new ArrayList(); + ret.add(addr); + return ret; + } + + /** + * Create a builder with initial ElasticCache server addresses. + * + * @param configAddrs + */ + public AWSElasticCacheClientBuilder(List configAddrs) { + super(configAddrs); + this.configAddrs = configAddrs; + } + + /** + * Returns a new instanceof AWSElasticCacheClient. + */ + @Override + public AWSElasticCacheClient build() throws IOException { + + AWSElasticCacheClient memcachedClient = new AWSElasticCacheClient(this.sessionLocator, + this.sessionComparator, this.bufferAllocator, this.configuration, this.socketOptions, + this.commandFactory, this.transcoder, this.stateListeners, this.authInfoMap, + this.connectionPoolSize, this.connectTimeout, this.name, this.failureMode, + this.resolveInetAddresses, configAddrs, this.pollConfigIntervalMs); + this.configureClient(memcachedClient); + + return memcachedClient; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/aws/AWSUtils.java b/src/main/java/net/rubyeye/xmemcached/aws/AWSUtils.java new file mode 100644 index 0000000..a9a9770 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/aws/AWSUtils.java @@ -0,0 +1,62 @@ +package net.rubyeye.xmemcached.aws; + +import java.util.ArrayList; +import java.util.List; +import net.rubyeye.xmemcached.autodiscovery.CacheNode; +import net.rubyeye.xmemcached.autodiscovery.ClusterConfiguration; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * AWS get config command + * + * @author dennis + * + */ +@Deprecated +public class AWSUtils { + + private static final String DELIMITER = "|"; + + /** + * Parse response string to ClusterConfiguration instance. + * + * @param line + * @return + */ + public static ClusterConfiguration parseConfiguration(String line) { + String[] lines = line.trim().split("(?:\\r?\\n)"); + if (lines.length < 2) { + throw new IllegalArgumentException("Incorrect config response:" + line); + } + String configversion = lines[0]; + String nodeListStr = lines[1]; + if (!ByteUtils.isNumber(configversion)) { + throw new IllegalArgumentException( + "Invalid configversion: " + configversion + ", it should be a number."); + } + String[] nodeStrs = nodeListStr.split("(?:\\s)+"); + int version = Integer.parseInt(configversion); + List nodeList = new ArrayList(nodeStrs.length); + for (String nodeStr : nodeStrs) { + if (nodeStr.equals("")) { + continue; + } + + int firstDelimiter = nodeStr.indexOf(DELIMITER); + int secondDelimiter = nodeStr.lastIndexOf(DELIMITER); + if (firstDelimiter < 1 || firstDelimiter == secondDelimiter) { + throw new IllegalArgumentException( + "Invalid server ''" + nodeStr + "'' in response: " + line); + } + String hostName = nodeStr.substring(0, firstDelimiter).trim(); + String ipAddress = nodeStr.substring(firstDelimiter + 1, secondDelimiter).trim(); + String portNum = nodeStr.substring(secondDelimiter + 1).trim(); + int port = Integer.parseInt(portNum); + nodeList.add(new CacheNode(hostName, ipAddress, port)); + } + + return new ClusterConfiguration(version, nodeList); + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/aws/ConfigurationPoller.java b/src/main/java/net/rubyeye/xmemcached/aws/ConfigurationPoller.java new file mode 100644 index 0000000..5f4b4d7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/aws/ConfigurationPoller.java @@ -0,0 +1,113 @@ +package net.rubyeye.xmemcached.aws; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import net.rubyeye.xmemcached.autodiscovery.CacheNode; +import net.rubyeye.xmemcached.autodiscovery.ClusterConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * AWS ElastiCache configuration poller + * + * @author dennis + * + */ +@Deprecated +public class ConfigurationPoller implements Runnable { + + /** + * Return current ClusterConfigration. + * + * @return + */ + public ClusterConfiguration getClusterConfiguration() { + return clusterConfigration; + } + + private final AtomicInteger serverOrderCounter = new AtomicInteger(0); + + private Map ordersMap = new HashMap(); + + public synchronized int getCacheNodeOrder(CacheNode node) { + Integer order = this.ordersMap.get(node.getCacheKey()); + if (order != null) { + return order; + } + order = this.serverOrderCounter.incrementAndGet(); + this.ordersMap.put(node.getCacheKey(), order); + return order; + } + + public synchronized void removeCacheNodeOrder(CacheNode node) { + this.ordersMap.remove(node.getCacheKey()); + } + + private static final Logger log = LoggerFactory.getLogger(ConfigurationPoller.class); + + private final AWSElasticCacheClient client; + + private final long pollIntervalMills; + + private ScheduledExecutorService scheduledExecutorService; + + private volatile ClusterConfiguration clusterConfigration = null; + + public ConfigurationPoller(AWSElasticCacheClient client, long pollIntervalMills) { + super(); + this.client = client; + this.pollIntervalMills = pollIntervalMills; + this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "AWSElasticCacheConfigPoller"); + t.setDaemon(true); + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + }); + } + + public void start() { + this.scheduledExecutorService.scheduleWithFixedDelay(this, this.pollIntervalMills, + this.pollIntervalMills, TimeUnit.MILLISECONDS); + } + + public void stop() { + this.scheduledExecutorService.shutdown(); + } + + public void run() { + try { + ClusterConfiguration newConfig = this.client.getConfig(); + if (newConfig != null) { + ClusterConfiguration currentConfig = this.clusterConfigration; + if (currentConfig == null) { + this.clusterConfigration = newConfig; + } else { + if (newConfig.getVersion() < currentConfig.getVersion()) { + log.warn("Ignored new config from ElasticCache node, it's too old, current version is: " + + currentConfig.getVersion() + ", but the new version is: " + + newConfig.getVersion()); + return; + } else { + this.clusterConfigration = newConfig; + } + } + log.info("Retrieved new config from ElasticCache node: " + this.clusterConfigration); + this.client.onUpdate(newConfig); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error("Poll config from ElasticCache node failed", e); + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/buffer/BufferAllocator.java b/src/main/java/net/rubyeye/xmemcached/buffer/BufferAllocator.java new file mode 100644 index 0000000..8093a0a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/buffer/BufferAllocator.java @@ -0,0 +1,25 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.buffer; + +import java.nio.ByteBuffer; + +/** + * IoBuffer allocator + * + * @author dennis(killme2008@gmail.com) + */ +@Deprecated +public interface BufferAllocator { + public IoBuffer allocate(int capacity); + + public IoBuffer wrap(ByteBuffer byteBuffer); + + public void dispose(); +} diff --git a/src/main/java/net/rubyeye/xmemcached/buffer/CachedBufferAllocator.java b/src/main/java/net/rubyeye/xmemcached/buffer/CachedBufferAllocator.java new file mode 100644 index 0000000..03094d7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/buffer/CachedBufferAllocator.java @@ -0,0 +1,267 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; +import java.util.Queue; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.util.CircularQueue; + +/** + * Cached IoBuffer allocator,cached buffer in ThreadLocal. + * + * @author dennis + * + */ +@Deprecated +public class CachedBufferAllocator implements BufferAllocator { + + private static final int DEFAULT_MAX_POOL_SIZE = 8; + private static final int DEFAULT_MAX_CACHED_BUFFER_SIZE = 1 << 18; // 256KB + private final int maxPoolSize; + private final int maxCachedBufferSize; + private final ThreadLocal>> heapBuffers; + private final IoBuffer EMPTY_IO_BUFFER = + new CachedBufferAllocator.CachedIoBuffer(ByteBuffer.allocate(0)); + + /** + * Creates a new instance with the default parameters ({@literal #DEFAULT_MAX_POOL_SIZE} and + * {@literal #DEFAULT_MAX_CACHED_BUFFER_SIZE}). + */ + public CachedBufferAllocator() { + this(DEFAULT_MAX_POOL_SIZE, DEFAULT_MAX_CACHED_BUFFER_SIZE); + } + + /** + * Creates a new instance. + * + * @param maxPoolSize the maximum number of buffers with the same capacity per thread. 0 + * disables this limitation. + * @param maxCachedBufferSize the maximum capacity of a cached buffer. A buffer whose capacity is + * bigger than this value is not pooled. 0 disables this limitation. + */ + public CachedBufferAllocator(int maxPoolSize, int maxCachedBufferSize) { + if (maxPoolSize < 0) { + throw new IllegalArgumentException("maxPoolSize: " + maxPoolSize); + } + if (maxCachedBufferSize < 0) { + throw new IllegalArgumentException("maxCachedBufferSize: " + maxCachedBufferSize); + } + + this.maxPoolSize = maxPoolSize; + this.maxCachedBufferSize = maxCachedBufferSize; + + this.heapBuffers = new ThreadLocal>>() { + + @Override + protected Map> initialValue() { + return newPoolMap(); + } + }; + } + + public int getMaxPoolSize() { + return this.maxPoolSize; + } + + public int getMaxCachedBufferSize() { + return this.maxCachedBufferSize; + } + + /** + * 初始化缓冲池 + * + * @return + */ + private Map> newPoolMap() { + Map> poolMap = new HashMap>(); + int poolSize = this.maxPoolSize == 0 ? DEFAULT_MAX_POOL_SIZE : this.maxPoolSize; + for (int i = 0; i < 31; i++) { + poolMap.put(1 << i, new CircularQueue(poolSize)); + } + poolMap.put(0, new CircularQueue(poolSize)); + poolMap.put(Integer.MAX_VALUE, new CircularQueue(poolSize)); + return poolMap; + } + + public final IoBuffer allocate(int requestedCapacity) { + if (requestedCapacity == 0) { + return this.EMPTY_IO_BUFFER; + } + // 圆整requestedCapacity到2的x次方 + int actualCapacity = ByteUtils.normalizeCapacity(requestedCapacity); + IoBuffer buf; + if (this.maxCachedBufferSize != 0 && actualCapacity > this.maxCachedBufferSize) { + buf = wrap(ByteBuffer.allocate(actualCapacity)); + } else { + Queue pool; + pool = this.heapBuffers.get().get(actualCapacity); + // 从池中取 + buf = pool.poll(); + if (buf != null) { + buf.clear(); + } else { + buf = wrap(ByteBuffer.allocate(actualCapacity)); + } + } + buf.limit(requestedCapacity); + return buf; + } + + public final IoBuffer wrap(ByteBuffer nioBuffer) { + return new CachedIoBuffer(nioBuffer); + } + + public void dispose() { + this.heapBuffers.remove(); + } + + public static BufferAllocator newInstance() { + return new CachedBufferAllocator(); + } + + public static BufferAllocator newInstance(int maxPoolSize, int maxCachedBufferSize) { + return new CachedBufferAllocator(maxPoolSize, maxCachedBufferSize); + } + + public class CachedIoBuffer implements IoBuffer { + + Thread ownerThread; // 所分配的线程 + ByteBuffer origBuffer; + + public CachedIoBuffer(ByteBuffer origBuffer) { + super(); + this.ownerThread = Thread.currentThread(); + this.origBuffer = origBuffer; + } + + public void putInt(int i) { + this.origBuffer.putInt(i); + + } + + public void putShort(short s) { + this.origBuffer.putShort(s); + } + + public ByteOrder order() { + return this.origBuffer.order(); + } + + public boolean isDirect() { + return this.origBuffer.isDirect(); + } + + public void order(ByteOrder byteOrder) { + this.origBuffer.order(byteOrder); + } + + public void putLong(long l) { + this.origBuffer.putLong(l); + + } + + public final void free() { + if (this.origBuffer == null + || this.origBuffer.capacity() > CachedBufferAllocator.this.maxCachedBufferSize + || Thread.currentThread() != this.ownerThread) { + return; + } + + // Add to the cache. + Queue pool; + pool = CachedBufferAllocator.this.heapBuffers.get().get(this.origBuffer.capacity()); + if (pool == null) { + return; + } + // 防止OOM + if (CachedBufferAllocator.this.maxPoolSize == 0 + || pool.size() < CachedBufferAllocator.this.maxPoolSize) { + pool.offer(new CachedIoBuffer(this.origBuffer)); + } + this.origBuffer = null; + + } + + public final ByteBuffer[] getByteBuffers() { + return new ByteBuffer[] {this.origBuffer}; + } + + public final void put(byte[] bytes) { + this.origBuffer.put(bytes); + } + + public final int capacity() { + return this.origBuffer.capacity(); + } + + public final void clear() { + this.origBuffer.clear(); + } + + public final void reset() { + this.origBuffer.reset(); + } + + public final int remaining() { + return this.origBuffer.remaining(); + } + + public final int position() { + return this.origBuffer.position(); + } + + public final void mark() { + this.origBuffer.mark(); + } + + public final int limit() { + return this.origBuffer.limit(); + } + + public final boolean hasRemaining() { + return this.origBuffer.hasRemaining(); + } + + public final void flip() { + this.origBuffer.flip(); + } + + public final void put(byte b) { + this.origBuffer.put(b); + } + + public final void put(ByteBuffer buff) { + this.origBuffer.put(buff); + } + + public final ByteBuffer getByteBuffer() { + return this.origBuffer; + } + + public final void limit(int limit) { + this.origBuffer.limit(limit); + } + + public final void position(int pos) { + this.origBuffer.position(pos); + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/buffer/IoBuffer.java b/src/main/java/net/rubyeye/xmemcached/buffer/IoBuffer.java new file mode 100644 index 0000000..3b8c03b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/buffer/IoBuffer.java @@ -0,0 +1,68 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * ByteBuffer wrapper + * + * @author dennis + * + */ +@Deprecated +public interface IoBuffer { + + int capacity(); + + void clear(); + + void flip(); + + void free(); + + ByteBuffer getByteBuffer(); + + ByteBuffer[] getByteBuffers(); + + boolean hasRemaining(); + + int limit(); + + void limit(int limit); + + void mark(); + + int position(); + + void position(int pos); + + void put(ByteBuffer buff); + + void put(byte b); + + void putShort(short s); + + void putInt(int i); + + void putLong(long l); + + void put(byte[] bytes); + + int remaining(); + + void reset(); + + boolean isDirect(); + + void order(ByteOrder byteOrder); + + ByteOrder order(); +} diff --git a/src/main/java/net/rubyeye/xmemcached/buffer/SimpleBufferAllocator.java b/src/main/java/net/rubyeye/xmemcached/buffer/SimpleBufferAllocator.java new file mode 100644 index 0000000..364c69f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/buffer/SimpleBufferAllocator.java @@ -0,0 +1,42 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.buffer; + +import java.nio.ByteBuffer; + +/** + * Simple IoBuffer allocator,allcate a new heap ByteBuffer each time. + * + * @author dennis + * + */ +@Deprecated +public class SimpleBufferAllocator implements BufferAllocator { + + public static final IoBuffer EMPTY_IOBUFFER = new SimpleIoBuffer(ByteBuffer.allocate(0)); + + public final IoBuffer allocate(int capacity) { + if (capacity == 0) { + return EMPTY_IOBUFFER; + } else { + return wrap(ByteBuffer.allocate(capacity)); + } + } + + public final void dispose() {} + + public final static BufferAllocator newInstance() { + return new SimpleBufferAllocator(); + } + + public final IoBuffer wrap(ByteBuffer byteBuffer) { + return new SimpleIoBuffer(byteBuffer); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/buffer/SimpleIoBuffer.java b/src/main/java/net/rubyeye/xmemcached/buffer/SimpleIoBuffer.java new file mode 100644 index 0000000..177aa5c --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/buffer/SimpleIoBuffer.java @@ -0,0 +1,120 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.buffer; + +/** + * Simpe ByteBuffer Wrapper + */ +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +@Deprecated +public class SimpleIoBuffer implements IoBuffer { + + protected ByteBuffer origBuffer; + + public SimpleIoBuffer(ByteBuffer origBuffer) { + this.origBuffer = origBuffer; + } + + public final void free() { + this.origBuffer = null; + } + + public final ByteBuffer[] getByteBuffers() { + return new ByteBuffer[] {this.origBuffer}; + } + + public final void put(byte[] bytes) { + this.origBuffer.put(bytes); + } + + public final int capacity() { + return this.origBuffer.capacity(); + } + + public void putInt(int i) { + this.origBuffer.putInt(i); + + } + + public void putShort(short s) { + this.origBuffer.putShort(s); + } + + public final void clear() { + this.origBuffer.clear(); + } + + public final void reset() { + this.origBuffer.reset(); + } + + public final int remaining() { + return this.origBuffer.remaining(); + } + + public final int position() { + return this.origBuffer.position(); + } + + public final void mark() { + this.origBuffer.mark(); + } + + public final int limit() { + return this.origBuffer.limit(); + } + + public final boolean hasRemaining() { + return this.origBuffer.hasRemaining(); + } + + public final void flip() { + this.origBuffer.flip(); + } + + public final void put(byte b) { + this.origBuffer.put(b); + } + + public final void put(ByteBuffer buff) { + this.origBuffer.put(buff); + } + + public final ByteBuffer getByteBuffer() { + return this.origBuffer; + } + + public final void limit(int limit) { + this.origBuffer.limit(limit); + } + + public final void position(int pos) { + this.origBuffer.position(pos); + } + + public void order(ByteOrder byteOrder) { + this.origBuffer.order(byteOrder); + } + + public boolean isDirect() { + return this.origBuffer.isDirect(); + } + + public ByteOrder order() { + return this.origBuffer.order(); + } + + public void putLong(long l) { + this.origBuffer.putLong(l); + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/buffer/package.html b/src/main/java/net/rubyeye/xmemcached/buffer/package.html new file mode 100644 index 0000000..d8dd160 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/buffer/package.html @@ -0,0 +1,10 @@ + + + + + ByteBuffer Wrapper(Deprecated) + + +

ByteBuffer Wrapper,all classes of this package has been deprecated.

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/codec/MemcachedCodecFactory.java b/src/main/java/net/rubyeye/xmemcached/codec/MemcachedCodecFactory.java new file mode 100644 index 0000000..8c1107e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/codec/MemcachedCodecFactory.java @@ -0,0 +1,48 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.codec; + +import com.google.code.yanf4j.core.CodecFactory; + +/** + * Memcached protocol codec factory + * + * @author dennis + * + * @param + */ +public class MemcachedCodecFactory implements CodecFactory { + + private final MemcachedEncoder encoder; + + private final MemcachedDecoder decoder; + + public MemcachedCodecFactory() { + super(); + this.encoder = new MemcachedEncoder(); + this.decoder = new MemcachedDecoder(); + } + + /** + * return the memcached protocol decoder + */ + + public final CodecFactory.Decoder getDecoder() { + return this.decoder; + + } + + /** + * return the memcached protocol encoder + */ + + public final CodecFactory.Encoder getEncoder() { + return this.encoder; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/codec/MemcachedDecoder.java b/src/main/java/net/rubyeye/xmemcached/codec/MemcachedDecoder.java new file mode 100644 index 0000000..0095053 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/codec/MemcachedDecoder.java @@ -0,0 +1,62 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.codec; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.CodecFactory.Decoder; +import com.google.code.yanf4j.util.ByteBufferMatcher; +import com.google.code.yanf4j.util.ShiftAndByteBufferMatcher; + +/** + * Memcached protocol decoder + * + * @author dennis + * + */ +public class MemcachedDecoder implements Decoder { + + public static final Logger log = LoggerFactory.getLogger(MemcachedDecoder.class); + + public MemcachedDecoder() { + super(); + } + + /** + * shift-and algorithm for ByteBuffer's match + */ + public static final ByteBufferMatcher SPLIT_MATCHER = + new ShiftAndByteBufferMatcher(IoBuffer.wrap(ByteUtils.SPLIT)); + + public Object decode(IoBuffer buffer, Session origSession) { + MemcachedTCPSession session = (MemcachedTCPSession) origSession; + if (session.getCurrentCommand() != null) { + return decode0(buffer, session); + } else { + session.takeCurrentCommand(); + if (session.getCurrentCommand() == null) + return null; + return decode0(buffer, session); + } + } + + private Object decode0(IoBuffer buffer, MemcachedTCPSession session) { + if (session.getCurrentCommand().decode(session, buffer.buf())) { + final Command command = session.getCurrentCommand(); + session.setCurrentCommand(null); + return command; + } + return null; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/codec/MemcachedEncoder.java b/src/main/java/net/rubyeye/xmemcached/codec/MemcachedEncoder.java new file mode 100644 index 0000000..cc22e87 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/codec/MemcachedEncoder.java @@ -0,0 +1,28 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.codec; + +import net.rubyeye.xmemcached.command.Command; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.CodecFactory.Encoder; + +/** + * memcached protocol encoder + * + * @author dennis + * + */ +public class MemcachedEncoder implements Encoder { + + public IoBuffer encode(Object message, Session session) { + return ((Command) message).getIoBuffer(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/codec/package.html b/src/main/java/net/rubyeye/xmemcached/codec/package.html new file mode 100644 index 0000000..fd47a91 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/codec/package.html @@ -0,0 +1,10 @@ + + + + + Memcached protocol codec + + +

Memcached protocol codec

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/command/AssocCommandAware.java b/src/main/java/net/rubyeye/xmemcached/command/AssocCommandAware.java new file mode 100644 index 0000000..dbcbcee --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/AssocCommandAware.java @@ -0,0 +1,15 @@ +package net.rubyeye.xmemcached.command; + +import java.util.List; + +/** + * Assoc commands aware interface.Association commands mean that commands has the same key. + * + * @author dennis + * + */ +public interface AssocCommandAware { + public List getAssocCommands(); + + public void setAssocCommands(List assocCommands); +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/BinaryCommandFactory.java b/src/main/java/net/rubyeye/xmemcached/command/BinaryCommandFactory.java new file mode 100644 index 0000000..73418fd --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/BinaryCommandFactory.java @@ -0,0 +1,194 @@ +package net.rubyeye.xmemcached.command; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.binary.BinaryAutoDiscoveryCacheConfigCommand; +import net.rubyeye.xmemcached.command.binary.BinaryAppendPrependCommand; +import net.rubyeye.xmemcached.command.binary.BinaryAuthListMechanismsCommand; +import net.rubyeye.xmemcached.command.binary.BinaryAuthStartCommand; +import net.rubyeye.xmemcached.command.binary.BinaryAuthStepCommand; +import net.rubyeye.xmemcached.command.binary.BinaryCASCommand; +import net.rubyeye.xmemcached.command.binary.BinaryDeleteCommand; +import net.rubyeye.xmemcached.command.binary.BinaryFlushAllCommand; +import net.rubyeye.xmemcached.command.binary.BinaryGetAndTouchCommand; +import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; +import net.rubyeye.xmemcached.command.binary.BinaryGetMultiCommand; +import net.rubyeye.xmemcached.command.binary.BinaryIncrDecrCommand; +import net.rubyeye.xmemcached.command.binary.BinaryQuitCommand; +import net.rubyeye.xmemcached.command.binary.BinaryStatsCommand; +import net.rubyeye.xmemcached.command.binary.BinaryStoreCommand; +import net.rubyeye.xmemcached.command.binary.BinaryTouchCommand; +import net.rubyeye.xmemcached.command.binary.BinaryVerbosityCommand; +import net.rubyeye.xmemcached.command.binary.BinaryVersionCommand; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.Protocol; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Binary protocol command factory + * + * @author dennis + * @since 1.2.0 + */ +@SuppressWarnings("unchecked") +public class BinaryCommandFactory implements CommandFactory { + + public Command createAutoDiscoveryCacheConfigCommand(String subCommand, String key) { + return new BinaryAutoDiscoveryCacheConfigCommand(new CountDownLatch(1), subCommand, key); + } + + private BufferAllocator bufferAllocator = new SimpleBufferAllocator(); + + public void setBufferAllocator(BufferAllocator bufferAllocator) { + this.bufferAllocator = bufferAllocator; + } + + public Command createAddCommand(String key, byte[] keyBytes, int exp, Object value, + boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, exp, value, CommandType.ADD, noreply, transcoder); + } + + public Command createAppendCommand(String key, byte[] keyBytes, Object value, boolean noreply, + Transcoder transcoder) { + return new BinaryAppendPrependCommand(key, keyBytes, CommandType.APPEND, new CountDownLatch(1), + 0, 0, value, noreply, transcoder); + } + + public Command createCASCommand(String key, byte[] keyBytes, int exp, Object value, long cas, + boolean noreply, Transcoder transcoder) { + return new BinaryCASCommand(key, keyBytes, CommandType.CAS, new CountDownLatch(1), exp, cas, + value, noreply, transcoder); + } + + public Command createDeleteCommand(String key, byte[] keyBytes, int time, long cas, + boolean noreply) { + return new BinaryDeleteCommand(key, keyBytes, cas, CommandType.DELETE, new CountDownLatch(1), + noreply); + } + + public Command createFlushAllCommand(CountDownLatch latch, int delay, boolean noreply) { + return new BinaryFlushAllCommand(latch, delay, noreply); + } + + public Command createGetCommand(String key, byte[] keyBytes, CommandType cmdType, + Transcoder transcoder) { + return new BinaryGetCommand(key, keyBytes, cmdType, new CountDownLatch(1), OpCode.GET, false); + } + + public Command createGetMultiCommand(Collection keys, CountDownLatch latch, + CommandType cmdType, Transcoder transcoder) { + Iterator it = keys.iterator(); + String key = null; + List bufferList = + new ArrayList(); + int totalLength = 0; + while (it.hasNext()) { + key = it.next(); + if (it.hasNext()) { + // first n-1 send getq command + Command command = new BinaryGetCommand(key, ByteUtils.getBytes(key), cmdType, null, + OpCode.GET_KEY_QUIETLY, true); + command.encode(); + totalLength += command.getIoBuffer().remaining(); + bufferList.add(command.getIoBuffer()); + } + } + // last key,create a get command + Command lastCommand = new BinaryGetCommand(key, ByteUtils.getBytes(key), cmdType, + new CountDownLatch(1), OpCode.GET_KEY, false); + lastCommand.encode(); + bufferList.add(lastCommand.getIoBuffer()); + totalLength += lastCommand.getIoBuffer().remaining(); + + IoBuffer mergedBuffer = IoBuffer.allocate(totalLength); + for (IoBuffer buffer : bufferList) { + mergedBuffer.put(buffer.buf()); + } + mergedBuffer.flip(); + Command resultCommand = new BinaryGetMultiCommand(key, cmdType, latch); + resultCommand.setIoBuffer(mergedBuffer); + return resultCommand; + } + + public Command createIncrDecrCommand(String key, byte[] keyBytes, long amount, long initial, + int expTime, CommandType cmdType, boolean noreply) { + return new BinaryIncrDecrCommand(key, keyBytes, amount, initial, expTime, cmdType, noreply); + } + + public Command createPrependCommand(String key, byte[] keyBytes, Object value, boolean noreply, + Transcoder transcoder) { + return new BinaryAppendPrependCommand(key, keyBytes, CommandType.PREPEND, new CountDownLatch(1), + 0, 0, value, noreply, transcoder); + } + + public Command createReplaceCommand(String key, byte[] keyBytes, int exp, Object value, + boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, exp, value, CommandType.REPLACE, noreply, + transcoder); + } + + final Command createStoreCommand(String key, byte[] keyBytes, int exp, Object value, + CommandType cmdType, boolean noreply, Transcoder transcoder) { + return new BinaryStoreCommand(key, keyBytes, cmdType, new CountDownLatch(1), exp, -1, value, + noreply, transcoder); + } + + public Command createSetCommand(String key, byte[] keyBytes, int exp, Object value, + boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, exp, value, CommandType.SET, noreply, transcoder); + } + + public Command createStatsCommand(InetSocketAddress server, CountDownLatch latch, + String itemName) { + return new BinaryStatsCommand(server, latch, itemName); + } + + public Command createVerbosityCommand(CountDownLatch latch, int level, boolean noreply) { + return new BinaryVerbosityCommand(latch, level, noreply); + } + + public Command createVersionCommand(CountDownLatch latch, InetSocketAddress server) { + return new BinaryVersionCommand(latch, server); + } + + public Command createAuthListMechanismsCommand(CountDownLatch latch) { + return new BinaryAuthListMechanismsCommand(latch); + } + + public Command createAuthStartCommand(String mechanism, CountDownLatch latch, byte[] authData) { + return new BinaryAuthStartCommand(mechanism, ByteUtils.getBytes(mechanism), latch, authData); + } + + public Command createAuthStepCommand(String mechanism, CountDownLatch latch, byte[] authData) { + return new BinaryAuthStepCommand(mechanism, ByteUtils.getBytes(mechanism), latch, authData); + } + + public Command createGetAndTouchCommand(String key, byte[] keyBytes, CountDownLatch latch, + int exp, boolean noreply) { + return new BinaryGetAndTouchCommand(key, keyBytes, noreply ? CommandType.GATQ : CommandType.GAT, + latch, exp, noreply); + } + + public Command createTouchCommand(String key, byte[] keyBytes, CountDownLatch latch, int exp, + boolean noreply) { + return new BinaryTouchCommand(key, keyBytes, CommandType.TOUCH, latch, exp, noreply); + } + + public Command createQuitCommand() { + return new BinaryQuitCommand(); + } + + public Protocol getProtocol() { + return Protocol.Binary; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/Command.java b/src/main/java/net/rubyeye/xmemcached/command/Command.java new file mode 100644 index 0000000..acc61a2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/Command.java @@ -0,0 +1,309 @@ +/** + * Copyright [2009-2010] [dennis zhuang] 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 net.rubyeye.xmemcached.command; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.exception.MemcachedClientException; +import net.rubyeye.xmemcached.exception.MemcachedDecodeException; +import net.rubyeye.xmemcached.exception.MemcachedServerException; +import net.rubyeye.xmemcached.exception.UnknownCommandException; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.core.impl.FutureImpl; + +/** + * Abstract Memcached Command + * + * @author dennis + * + */ +public abstract class Command implements WriteMessage { + + public static final byte REQUEST_MAGIC_NUMBER = (byte) (0x80 & 0xFF); + + public static final byte RESPONSE_MAGIC_NUMBER = (byte) (0x81 & 0xFF); + + private boolean added; + + public boolean isAdded() { + return added; + } + + public void setAdded(boolean added) { + this.added = added; + } + + public final Object getMessage() { + return this; + } + + public synchronized final com.google.code.yanf4j.buffer.IoBuffer getWriteBuffer() { + return getIoBuffer(); + } + + public void setWriteBuffer(com.google.code.yanf4j.buffer.IoBuffer buffers) { + // throw new UnsupportedOperationException(); + } + + protected String key; + protected byte[] keyBytes; + protected Object result; + protected CountDownLatch latch; + protected CommandType commandType; + protected Exception exception; + protected IoBuffer ioBuffer; + protected volatile boolean cancel; + protected OperationStatus status; + protected int mergeCount = -1; + private int copiedMergeCount = mergeCount; + @SuppressWarnings("unchecked") + protected Transcoder transcoder; + protected boolean noreply; + protected FutureImpl writeFuture; + + public final byte[] getKeyBytes() { + return keyBytes; + } + + public final void setKeyBytes(byte[] keyBytes) { + this.keyBytes = keyBytes; + } + + public void setCommandType(final CommandType commandType) { + this.commandType = commandType; + } + + public int getMergeCount() { + return mergeCount; + } + + @SuppressWarnings("unchecked") + public Transcoder getTranscoder() { + return transcoder; + } + + @SuppressWarnings("unchecked") + public void setTranscoder(Transcoder transcoder) { + this.transcoder = transcoder; + } + + public void setMergeCount(final int mergetCount) { + mergeCount = mergetCount; + this.copiedMergeCount = mergetCount; + } + + public int getCopiedMergeCount() { + return copiedMergeCount; + } + + public Command() { + super(); + status = OperationStatus.SENDING; + } + + public Command(String key, byte[] keyBytes, CountDownLatch latch) { + super(); + this.key = key; + this.keyBytes = keyBytes; + status = OperationStatus.SENDING; + this.latch = latch; + } + + public Command(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch) { + super(); + this.key = key; + this.keyBytes = keyBytes; + status = OperationStatus.SENDING; + this.latch = latch; + commandType = cmdType; + } + + public Command(final CommandType cmdType) { + commandType = cmdType; + status = OperationStatus.SENDING; + } + + public Command(final CommandType cmdType, final CountDownLatch latch) { + commandType = cmdType; + this.latch = latch; + status = OperationStatus.SENDING; + } + + public Command(final String key, final CommandType commandType, final CountDownLatch latch) { + super(); + this.key = key; + this.commandType = commandType; + this.latch = latch; + status = OperationStatus.SENDING; + } + + public OperationStatus getStatus() { + return status; + } + + public final void setStatus(OperationStatus status) { + this.status = status; + } + + public final void setIoBuffer(IoBuffer ioBuffer) { + this.ioBuffer = ioBuffer; + } + + public Exception getException() { + return exception; + } + + public void setException(Exception throwable) { + exception = throwable; + } + + public final String getKey() { + return key; + } + + public final void setKey(String key) { + this.key = key; + } + + public final Object getResult() { + return result; + } + + public final void setResult(Object result) { + this.result = result; + } + + public final IoBuffer getIoBuffer() { + return ioBuffer; + } + + @Override + public String toString() { + try { + return new String(ioBuffer.buf().array(), "utf-8"); + } catch (UnsupportedEncodingException e) { + } + return "[error]"; + } + + public boolean isCancel() { + return status == OperationStatus.SENDING && cancel; + } + + public final void cancel() { + cancel = true; + if (ioBuffer != null) { + ioBuffer.free(); + } + } + + public final CountDownLatch getLatch() { + return latch; + } + + public final void countDownLatch() { + if (latch != null) { + latch.countDown(); + if (latch.getCount() == 0) { + status = OperationStatus.DONE; + } + } + } + + public final CommandType getCommandType() { + return commandType; + } + + public final void setLatch(CountDownLatch latch) { + this.latch = latch; + } + + public abstract void encode(); + + public abstract boolean decode(MemcachedTCPSession session, ByteBuffer buffer); + + protected final void decodeError(String msg, Throwable e) { + throw new MemcachedDecodeException( + msg == null ? "decode error,session will be closed,key=" + this.key : msg, e); + } + + protected final void decodeError() { + throw new MemcachedDecodeException("decode error,session will be closed,key=" + this.key); + } + + protected final boolean decodeError(String line) { + if (line.startsWith("ERROR")) { + String[] splits = line.split("ERROR"); + String errorMsg = splits.length >= 2 ? splits[1] : "Unknow command " + getCommandType(); + setException(new UnknownCommandException( + "Response error,error message:" + errorMsg + ",key=" + this.key)); + countDownLatch(); + return true; + } else if (line.startsWith("CLIENT_ERROR")) { + setException(new MemcachedClientException(getErrorMsg(line, "Unknown Client Error"))); + this.countDownLatch(); + return true; + } else if (line.startsWith("SERVER_ERROR")) { + setException(new MemcachedServerException(getErrorMsg(line, "Unknown Server Error"))); + this.countDownLatch(); + return true; + } else { + throw new MemcachedDecodeException( + "Decode error,session will be closed,key=" + this.key + ",server returns=" + line); + } + + } + + protected final boolean decodeError(Session session, ByteBuffer buffer) { + String line = ByteUtils.nextLine(buffer); + if (line == null) { + return false; + } else { + return decodeError(line); + } + } + + private String getErrorMsg(String line, String defaultMsg) { + int index = line.indexOf(" "); + String errorMsg = index > 0 ? line.substring(index + 1) : defaultMsg; + errorMsg += ",key=" + this.key; + return errorMsg; + } + + public final boolean isNoreply() { + return noreply; + } + + public final void setNoreply(boolean noreply) { + this.noreply = noreply; + } + + public FutureImpl getWriteFuture() { + return writeFuture; + } + + public final void setWriteFuture(FutureImpl writeFuture) { + this.writeFuture = writeFuture; + } + + public final boolean isWriting() { + return true; + } + + public final void writing() { + // do nothing + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/CommandType.java b/src/main/java/net/rubyeye/xmemcached/command/CommandType.java new file mode 100644 index 0000000..dde9321 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/CommandType.java @@ -0,0 +1,15 @@ +package net.rubyeye.xmemcached.command; + +/** + * Command Type for memcached protocol. + * + * @author dennis + * + */ +public enum CommandType { + + NOOP, STATS, FLUSH_ALL, GET_ONE, GET_MANY, SET, REPLACE, ADD, EXCEPTION, // + DELETE, VERSION, QUIT, INCR, DECR, GETS_ONE, GETS_MANY, CAS, APPEND, PREPEND, // + GET_HIT, GET_MISS, VERBOSITY, AUTH_LIST, AUTH_START, AUTH_STEP, TOUCH, GAT, GATQ, SET_MANY, // + AUTO_DISCOVERY_CONFIG; +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/KestrelCommandFactory.java b/src/main/java/net/rubyeye/xmemcached/command/KestrelCommandFactory.java new file mode 100644 index 0000000..6fa743a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/KestrelCommandFactory.java @@ -0,0 +1,138 @@ +package net.rubyeye.xmemcached.command; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.kestrel.KestrelDeleteCommand; +import net.rubyeye.xmemcached.command.kestrel.KestrelFlushAllCommand; +import net.rubyeye.xmemcached.command.kestrel.KestrelGetCommand; +import net.rubyeye.xmemcached.command.kestrel.KestrelSetCommand; +import net.rubyeye.xmemcached.command.text.TextQuitCommand; +import net.rubyeye.xmemcached.command.text.TextStatsCommand; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.Protocol; + +/** + * Kestrel is a message queue written in scala by robey(http://github.com/robey/kestrel).It's + * protocol use memcached text protocol,so you can use any memcached clients to talk with it.But + * it's protocol implementation is not all compatible with memcached standard protocol,So xmemcached + * supply this command factory for it. + * + * @author dennis + * + */ +@SuppressWarnings("unchecked") +public class KestrelCommandFactory implements CommandFactory { + + public Command createAutoDiscoveryCacheConfigCommand(String subCommand, String key) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createAddCommand(String key, byte[] keyBytes, int exp, Object value, + boolean noreply, Transcoder transcoder) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createAppendCommand(String key, byte[] keyBytes, Object value, boolean noreply, + Transcoder transcoder) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createCASCommand(String key, byte[] keyBytes, int exp, Object value, long cas, + boolean noreply, Transcoder transcoder) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createDeleteCommand(String key, byte[] keyBytes, int time, long cas, + boolean noreply) { + return new KestrelDeleteCommand(key, keyBytes, -1, new CountDownLatch(1), noreply); + } + + public Command createFlushAllCommand(CountDownLatch latch, int delay, boolean noreply) { + return new KestrelFlushAllCommand(latch, delay, noreply); + } + + public Command createGetCommand(String key, byte[] keyBytes, CommandType cmdType, + Transcoder transcoder) { + return new KestrelGetCommand(key, keyBytes, cmdType, new CountDownLatch(1), transcoder); + } + + public Command createGetMultiCommand(Collection keys, CountDownLatch latch, + CommandType cmdType, Transcoder transcoder) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createIncrDecrCommand(String key, byte[] keyBytes, long amount, long initial, + int expTime, CommandType cmdType, boolean noreply) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createPrependCommand(String key, byte[] keyBytes, Object value, boolean noreply, + Transcoder transcoder) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createReplaceCommand(String key, byte[] keyBytes, int exp, Object value, + boolean noreply, Transcoder transcoder) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createSetCommand(String key, byte[] keyBytes, int exp, Object value, + boolean noreply, Transcoder transcoder) { + return new KestrelSetCommand(key, keyBytes, CommandType.SET, new CountDownLatch(1), exp, -1, + value, noreply, transcoder); + } + + public Command createStatsCommand(InetSocketAddress server, CountDownLatch latch, + String itemName) { + if (itemName != null) { + throw new UnsupportedOperationException("Kestrel doesn't support 'stats itemName'"); + } + return new TextStatsCommand(server, latch, null); + } + + public Command createVerbosityCommand(CountDownLatch latch, int level, boolean noreply) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createVersionCommand(CountDownLatch latch, InetSocketAddress server) { + throw new UnsupportedOperationException("Kestrel doesn't support this operation"); + } + + public Command createQuitCommand() { + return new TextQuitCommand(); + } + + public Protocol getProtocol() { + return Protocol.Kestrel; + } + + public Command createAuthListMechanismsCommand(CountDownLatch latch) { + throw new UnsupportedOperationException("Kestrel doesn't support SASL"); + } + + public Command createAuthStartCommand(String mechanism, CountDownLatch latch, byte[] authData) { + throw new UnsupportedOperationException("Kestrel doesn't support SASL"); + } + + public Command createAuthStepCommand(String mechanism, CountDownLatch latch, byte[] authData) { + throw new UnsupportedOperationException("Kestrel doesn't support SASL"); + } + + public Command createGetAndTouchCommand(String key, byte[] keyBytes, CountDownLatch latch, + int exp, boolean noreply) { + throw new UnsupportedOperationException("GAT is only supported by binary protocol"); + } + + public Command createTouchCommand(String key, byte[] keyBytes, CountDownLatch latch, int exp, + boolean noreply) { + throw new UnsupportedOperationException("Touch is only supported by binary protocol"); + } + + public void setBufferAllocator(BufferAllocator bufferAllocator) { + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/MapReturnValueAware.java b/src/main/java/net/rubyeye/xmemcached/command/MapReturnValueAware.java new file mode 100644 index 0000000..3cb632f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/MapReturnValueAware.java @@ -0,0 +1,16 @@ +package net.rubyeye.xmemcached.command; + +import java.util.Map; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Command which implement this interface,it's return value is a map + * + * @author dennis + * + */ +public interface MapReturnValueAware { + + public abstract Map getReturnValues(); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/MergeCommandsAware.java b/src/main/java/net/rubyeye/xmemcached/command/MergeCommandsAware.java new file mode 100644 index 0000000..8ddb02c --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/MergeCommandsAware.java @@ -0,0 +1,18 @@ +package net.rubyeye.xmemcached.command; + +import java.util.Map; + +/** + * Merge commands aware interface.Merge commands mean that merge get commands to a bulk-get + * commands. + * + * @author boyan + * + */ +public interface MergeCommandsAware { + + public Map getMergeCommands(); + + public void setMergeCommands(Map mergeCommands); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/OperationStatus.java b/src/main/java/net/rubyeye/xmemcached/command/OperationStatus.java new file mode 100644 index 0000000..2678013 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/OperationStatus.java @@ -0,0 +1,11 @@ +package net.rubyeye.xmemcached.command; + +/** + * Command status. + * + * @author dennis + * + */ +public enum OperationStatus { + SENDING, WRITING, SENT, PROCESSING, DONE, CANCEL; +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/ServerAddressAware.java b/src/main/java/net/rubyeye/xmemcached/command/ServerAddressAware.java new file mode 100644 index 0000000..fb919b9 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/ServerAddressAware.java @@ -0,0 +1,20 @@ +package net.rubyeye.xmemcached.command; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +/** + * Server address aware interface.Command which implement this interface have these methods to + * getter/setter memcached's InetSocketAddress. + * + * @author boyan + * + */ +public interface ServerAddressAware { + public static final ByteBuffer VERSION = ByteBuffer.wrap("version\r\n".getBytes()); + + public InetSocketAddress getServer(); + + public void setServer(InetSocketAddress server); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/StoreCommand.java b/src/main/java/net/rubyeye/xmemcached/command/StoreCommand.java new file mode 100644 index 0000000..ca5ae9a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/StoreCommand.java @@ -0,0 +1,14 @@ +package net.rubyeye.xmemcached.command; + +/** + * A store command interface for STORE commands such as SET,ADD + * + * @author apple + * + */ +public interface StoreCommand { + + public void setValue(Object value); + + public Object getValue(); +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/TextCommandFactory.java b/src/main/java/net/rubyeye/xmemcached/command/TextCommandFactory.java new file mode 100644 index 0000000..4b04fd8 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/TextCommandFactory.java @@ -0,0 +1,232 @@ +package net.rubyeye.xmemcached.command; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.text.TextAutoDiscoveryCacheConfigCommand; +import net.rubyeye.xmemcached.command.text.TextCASCommand; +import net.rubyeye.xmemcached.command.text.TextDeleteCommand; +import net.rubyeye.xmemcached.command.text.TextFlushAllCommand; +import net.rubyeye.xmemcached.command.text.TextGetMultiCommand; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; +import net.rubyeye.xmemcached.command.text.TextIncrDecrCommand; +import net.rubyeye.xmemcached.command.text.TextQuitCommand; +import net.rubyeye.xmemcached.command.text.TextStatsCachedumpCommand; +import net.rubyeye.xmemcached.command.text.TextStatsCommand; +import net.rubyeye.xmemcached.command.text.TextStoreCommand; +import net.rubyeye.xmemcached.command.text.TextTouchCommand; +import net.rubyeye.xmemcached.command.text.TextVerbosityCommand; +import net.rubyeye.xmemcached.command.text.TextVersionCommand; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.Protocol; + +/** + * Command Factory for creating text protocol commands. + * + * @author dennis + * + */ +public class TextCommandFactory implements CommandFactory { + + public Command createAutoDiscoveryCacheConfigCommand(String subCommand, String key) { + return new TextAutoDiscoveryCacheConfigCommand(new CountDownLatch(1), subCommand, key); + } + + public void setBufferAllocator(BufferAllocator bufferAllocator) { + + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createDeleteCommand(java.lang. String , byte[], int) + */ + public final Command createDeleteCommand(final String key, final byte[] keyBytes, final int time, + long cas, boolean noreply) { + return new TextDeleteCommand(key, keyBytes, time, new CountDownLatch(1), noreply); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createVersionCommand() + */ + public final Command createVersionCommand(CountDownLatch latch, InetSocketAddress server) { + return new TextVersionCommand(latch, server); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createFlushAllCommand(java.util + * .concurrent.CountDownLatch) + */ + public final Command createFlushAllCommand(CountDownLatch latch, int exptime, boolean noreply) { + return new TextFlushAllCommand(latch, exptime, noreply); + } + + /** + * Create verbosity command + * + * @param latch + * @param level + * @param noreply + * @return + */ + public final Command createVerbosityCommand(CountDownLatch latch, int level, boolean noreply) { + return new TextVerbosityCommand(latch, level, noreply); + } + + /* + * (non-Javadoc) + * + * @seenet.rubyeye.xmemcached.CommandFactory#createStatsCommand(java.net.InetSocketAddress, + * java.util.concurrent.CountDownLatch) + */ + public final Command createStatsCommand(InetSocketAddress server, CountDownLatch latch, + String itemName) { + return new TextStatsCommand(server, latch, itemName); + } + + /* + * (non-Javadoc) + * + * @seenet.rubyeye.xmemcached.CommandFactory#createStatsCachedumpCommand(java.net.InetSocketAddress, + * java.util.concurrent.CountDownLatch, int, int) + */ + public final Command createStatsCachedumpCommand(InetSocketAddress server, CountDownLatch latch, + int slabId, int limit) { + return new TextStatsCachedumpCommand(server, latch, slabId, limit); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createStoreCommand(java.lang.String , byte[], int, + * java.lang.Object, net.rubyeye.xmemcached.command.CommandType, java.lang.String, long, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + @SuppressWarnings("unchecked") + public final Command createCASCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, long cas, boolean noreply, Transcoder transcoder) { + return new TextCASCommand(key, keyBytes, CommandType.CAS, new CountDownLatch(1), exp, cas, + value, noreply, transcoder); + } + + @SuppressWarnings("unchecked") + public final Command createSetCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, exp, value, CommandType.SET, noreply, transcoder); + } + + @SuppressWarnings("unchecked") + public final Command createAddCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, exp, value, CommandType.ADD, noreply, transcoder); + } + + @SuppressWarnings("unchecked") + public final Command createReplaceCommand(final String key, final byte[] keyBytes, final int exp, + final Object value, boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, exp, value, CommandType.REPLACE, noreply, + transcoder); + } + + @SuppressWarnings("unchecked") + public final Command createAppendCommand(final String key, final byte[] keyBytes, + final Object value, boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, 0, value, CommandType.APPEND, noreply, + transcoder); + } + + @SuppressWarnings("unchecked") + public final Command createPrependCommand(final String key, final byte[] keyBytes, + final Object value, boolean noreply, Transcoder transcoder) { + return this.createStoreCommand(key, keyBytes, 0, value, CommandType.PREPEND, noreply, + transcoder); + } + + @SuppressWarnings("unchecked") + final Command createStoreCommand(String key, byte[] keyBytes, int exp, Object value, + CommandType cmdType, boolean noreply, Transcoder transcoder) { + return new TextStoreCommand(key, keyBytes, cmdType, new CountDownLatch(1), exp, -1, value, + noreply, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createGetCommand(java.lang.String, byte[], + * net.rubyeye.xmemcached.command.CommandType) + */ + @SuppressWarnings("unchecked") + public final Command createGetCommand(final String key, final byte[] keyBytes, + final CommandType cmdType, Transcoder transcoder) { + return new TextGetOneCommand(key, keyBytes, cmdType, new CountDownLatch(1)); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createGetMultiCommand(java.util .Collection, + * java.util.concurrent.CountDownLatch, net.rubyeye.xmemcached.command.CommandType, + * net.rubyeye.xmemcached.transcoders.Transcoder) + */ + public final Command createGetMultiCommand(Collection keys, CountDownLatch latch, + CommandType cmdType, Transcoder transcoder) { + StringBuilder sb = new StringBuilder(keys.size() * 5); + for (String tmpKey : keys) { + ByteUtils.checkKey(tmpKey); + sb.append(tmpKey).append(" "); + } + String gatherKey = sb.toString(); + byte[] keyBytes = ByteUtils.getBytes(gatherKey.substring(0, gatherKey.length() - 1)); + return new TextGetMultiCommand(keys.iterator().next(), keyBytes, cmdType, latch, transcoder); + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.CommandFactory#createIncrDecrCommand(java.lang .String, byte[], + * int, net.rubyeye.xmemcached.command.CommandType) + */ + public final Command createIncrDecrCommand(final String key, final byte[] keyBytes, + final long amount, long initial, int exptime, CommandType cmdType, boolean noreply) { + return new TextIncrDecrCommand(key, keyBytes, cmdType, new CountDownLatch(1), amount, initial, + noreply); + } + + public Command createAuthListMechanismsCommand(CountDownLatch latch) { + throw new UnsupportedOperationException("SASL is only supported by binary protocol"); + } + + public Command createAuthStartCommand(String mechanism, CountDownLatch latch, byte[] authData) { + throw new UnsupportedOperationException("SASL is only supported by binary protocol"); + } + + public Command createAuthStepCommand(String mechanism, CountDownLatch latch, byte[] authData) { + throw new UnsupportedOperationException("SASL is only supported by binary protocol"); + } + + public Command createGetAndTouchCommand(String key, byte[] keyBytes, CountDownLatch latch, + int exp, boolean noreply) { + throw new UnsupportedOperationException("GAT is only supported by binary protocol"); + } + + public Command createTouchCommand(String key, byte[] keyBytes, CountDownLatch latch, int exp, + boolean noreply) { + return new TextTouchCommand(key, keyBytes, CommandType.TOUCH, latch, exp, noreply); + } + + public Command createQuitCommand() { + return new TextQuitCommand(); + } + + public Protocol getProtocol() { + return Protocol.Text; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/VerbosityCommand.java b/src/main/java/net/rubyeye/xmemcached/command/VerbosityCommand.java new file mode 100644 index 0000000..48f9d47 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/VerbosityCommand.java @@ -0,0 +1,30 @@ +package net.rubyeye.xmemcached.command; + +import java.util.concurrent.CountDownLatch; + +/** + * Abstract verbosity command for text protocol + * + * @author dennis + * + */ +public abstract class VerbosityCommand extends Command { + + protected int level; + + public final int getLevel() { + return level; + } + + public final void setLevel(int logLevel) { + this.level = logLevel; + } + + public VerbosityCommand(CountDownLatch latch, int level, boolean noreply) { + super(CommandType.VERBOSITY, latch); + this.level = level; + this.key = "[verbosity]"; + this.noreply = noreply; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BaseBinaryCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BaseBinaryCommand.java new file mode 100644 index 0000000..eba737a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BaseBinaryCommand.java @@ -0,0 +1,407 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import com.google.code.yanf4j.buffer.IoBuffer; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.StoreCommand; +import net.rubyeye.xmemcached.exception.MemcachedDecodeException; +import net.rubyeye.xmemcached.exception.MemcachedServerException; +import net.rubyeye.xmemcached.exception.UnknownCommandException; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.OpaqueGenerater; + +/** + * Base Binary command. + * + * @author dennis + * + */ +public abstract class BaseBinaryCommand extends Command implements StoreCommand { + static final short DEFAULT_VBUCKET_ID = 0; + protected int expTime; + protected long cas; + protected Object value; + + protected OpCode opCode; + protected BinaryDecodeStatus decodeStatus = BinaryDecodeStatus.NONE; + protected int responseKeyLength, responseExtrasLength, responseTotalBodyLength; + protected ResponseStatus responseStatus; + protected int opaque; + protected short vbucketId = DEFAULT_VBUCKET_ID; + + @SuppressWarnings("unchecked") + public BaseBinaryCommand(final String key, final byte[] keyBytes, final CommandType cmdType, + final CountDownLatch latch, final int exp, final long cas, final Object value, + final boolean noreply, final Transcoder transcoder) { + super(key, keyBytes, cmdType, latch); + this.expTime = exp; + this.cas = cas; + this.value = value; + this.noreply = noreply; + this.transcoder = transcoder; + } + + public final int getExpTime() { + return this.expTime; + } + + public final void setExpTime(final int exp) { + this.expTime = exp; + } + + public final long getCas() { + return this.cas; + } + + public int getOpaque() { + return this.opaque; + } + + public void setOpaque(final int opaque) { + this.opaque = opaque; + } + + public final void setCas(final long cas) { + this.cas = cas; + } + + public final Object getValue() { + return this.value; + } + + public final void setValue(final Object value) { + this.value = value; + } + + @Override + @SuppressWarnings("unchecked") + public final Transcoder getTranscoder() { + return this.transcoder; + } + + @Override + @SuppressWarnings("unchecked") + public final void setTranscoder(final Transcoder transcoder) { + this.transcoder = transcoder; + } + + @Override + public boolean decode(final MemcachedTCPSession session, final ByteBuffer buffer) { + while (true) { + LABEL: switch (this.decodeStatus) { + case NONE: + if (buffer.remaining() < 24) { + return false; + } else { + readHeader(buffer); + } + continue; + case READ_EXTRAS: + if (readExtras(buffer, this.responseExtrasLength)) { + this.decodeStatus = BinaryDecodeStatus.READ_KEY; + continue; + } else { + return false; + } + case READ_KEY: + if (readKey(buffer, this.responseKeyLength)) { + this.decodeStatus = BinaryDecodeStatus.READ_VALUE; + continue; + } else { + return false; + } + case READ_VALUE: + if (this.responseStatus == null || this.responseStatus == ResponseStatus.NO_ERROR) { + if (readValue(buffer, this.responseTotalBodyLength, this.responseKeyLength, + this.responseExtrasLength)) { + this.decodeStatus = BinaryDecodeStatus.DONE; + continue; + } else { + return false; + } + } else { + // Ignore error message + if (ByteUtils.stepBuffer(buffer, this.responseTotalBodyLength - this.responseKeyLength + - this.responseExtrasLength)) { + this.decodeStatus = BinaryDecodeStatus.DONE; + continue; + } else { + return false; + } + } + case DONE: + if (finish()) { + return true; + } else { + // Do not finish,continue to decode + this.decodeStatus = BinaryDecodeStatus.NONE; + break LABEL; + } + case IGNORE: + buffer.reset(); + return true; + } + } + } + + protected boolean finish() { + if (this.result == null) { + if (this.responseStatus == ResponseStatus.NO_ERROR) { + setResult(Boolean.TRUE); + } else { + setResult(Boolean.FALSE); + } + } + countDownLatch(); + return true; + } + + protected void readHeader(final ByteBuffer buffer) { + markBuffer(buffer); + readMagicNumber(buffer); + if (!readOpCode(buffer)) { + this.decodeStatus = BinaryDecodeStatus.IGNORE; + return; + } + readKeyLength(buffer); + readExtrasLength(buffer); + readDataType(buffer); + readStatus(buffer); + readBodyLength(buffer); + if (!readOpaque(buffer)) { + this.decodeStatus = BinaryDecodeStatus.IGNORE; + return; + } + this.decodeStatus = BinaryDecodeStatus.READ_EXTRAS; + readCAS(buffer); + + } + + private void markBuffer(final ByteBuffer buffer) { + buffer.mark(); + } + + protected boolean readOpaque(final ByteBuffer buffer) { + if (this.noreply) { + int returnOpaque = buffer.getInt(); + if (returnOpaque != this.opaque) { + return false; + } + } else { + ByteUtils.stepBuffer(buffer, 4); + } + return true; + } + + protected long readCAS(final ByteBuffer buffer) { + ByteUtils.stepBuffer(buffer, 8); + return 0; + } + + protected boolean readKey(final ByteBuffer buffer, final int keyLength) { + return ByteUtils.stepBuffer(buffer, keyLength); + } + + protected boolean readValue(final ByteBuffer buffer, final int bodyLength, final int keyLength, + final int extrasLength) { + return ByteUtils.stepBuffer(buffer, bodyLength - keyLength - extrasLength); + } + + protected boolean readExtras(final ByteBuffer buffer, final int extrasLength) { + return ByteUtils.stepBuffer(buffer, extrasLength); + } + + private int readBodyLength(final ByteBuffer buffer) { + this.responseTotalBodyLength = buffer.getInt(); + return this.responseTotalBodyLength; + } + + protected void readStatus(final ByteBuffer buffer) { + this.responseStatus = ResponseStatus.parseShort(buffer.getShort()); + switch (this.responseStatus) { + case NOT_SUPPORTED: + case UNKNOWN_COMMAND: + setException(new UnknownCommandException()); + break; + case AUTH_REQUIRED: + case FUTHER_AUTH_REQUIRED: + case VALUE_TOO_BIG: + case INVALID_ARGUMENTS: + case INC_DEC_NON_NUM: + case BELONGS_TO_ANOTHER_SRV: + case AUTH_ERROR: + case OUT_OF_MEMORY: + case INTERNAL_ERROR: + case BUSY: + case TEMP_FAILURE: + setException(new MemcachedServerException(this.responseStatus.errorMessage())); + break; + } + + } + + public final OpCode getOpCode() { + return this.opCode; + } + + public final void setOpCode(final OpCode opCode) { + this.opCode = opCode; + } + + public final ResponseStatus getResponseStatus() { + return this.responseStatus; + } + + public final void setResponseStatus(final ResponseStatus responseStatus) { + this.responseStatus = responseStatus; + } + + private int readKeyLength(final ByteBuffer buffer) { + this.responseKeyLength = buffer.getShort(); + return this.responseKeyLength; + } + + private int readExtrasLength(final ByteBuffer buffer) { + this.responseExtrasLength = buffer.get(); + return this.responseExtrasLength; + } + + private byte readDataType(final ByteBuffer buffer) { + return buffer.get(); + } + + protected boolean readOpCode(final ByteBuffer buffer) { + byte op = buffer.get(); + if (op != this.opCode.fieldValue()) { + if (this.noreply) { + return false; + } else { + throw new MemcachedDecodeException("Not a proper " + this.opCode.name() + " response"); + } + } + return true; + } + + private void readMagicNumber(final ByteBuffer buffer) { + byte magic = buffer.get(); + + if (magic != RESPONSE_MAGIC_NUMBER) { + throw new MemcachedDecodeException("Not a proper response"); + } + } + + /** + * Set,add,replace protocol's extras length + */ + static final byte EXTRAS_LENGTH = (byte) 8; + + @Override + @SuppressWarnings("unchecked") + public void encode() { + CachedData data = null; + if (this.transcoder != null) { + data = this.transcoder.encode(this.value); + } + // header+key+value+extras + int length = 24 + getKeyLength() + getValueLength(data) + getExtrasLength(); + + this.ioBuffer = IoBuffer.allocate(length); + fillHeader(data); + fillExtras(data); + fillKey(); + fillValue(data); + + this.ioBuffer.flip(); + + } + + protected void fillValue(final CachedData data) { + this.ioBuffer.put(data.getData()); + } + + protected void fillKey() { + this.ioBuffer.put(this.keyBytes); + } + + protected void fillExtras(final CachedData data) { + this.ioBuffer.putInt(data.getFlag()); + this.ioBuffer.putInt(this.expTime); + } + + private void fillHeader(final CachedData data) { + byte[] bs = new byte[24]; + bs[0] = REQUEST_MAGIC_NUMBER; + bs[1] = this.opCode.fieldValue(); + short keyLen = getKeyLength(); + bs[2] = ByteUtils.short1(keyLen); + bs[3] = ByteUtils.short0(keyLen); + bs[4] = getExtrasLength(); + // dataType,always zero bs[5]=0; + + bs[6] = ByteUtils.short1(this.vbucketId); + bs[7] = ByteUtils.short0(this.vbucketId); + // body len + int bodyLen = getExtrasLength() + getKeyLength() + getValueLength(data); + bs[8] = ByteUtils.int3(bodyLen); + bs[9] = ByteUtils.int2(bodyLen); + bs[10] = ByteUtils.int1(bodyLen); + bs[11] = ByteUtils.int0(bodyLen); + // Opaque + if (this.noreply) { + this.opaque = OpaqueGenerater.getInstance().getNextValue(); + } + bs[12] = ByteUtils.int3(this.opaque); + bs[13] = ByteUtils.int2(this.opaque); + bs[14] = ByteUtils.int1(this.opaque); + bs[15] = ByteUtils.int0(this.opaque); + // cas + long casValue = getCasValue(); + bs[16] = ByteUtils.long7(casValue); + bs[17] = ByteUtils.long6(casValue); + bs[18] = ByteUtils.long5(casValue); + bs[19] = ByteUtils.long4(casValue); + bs[20] = ByteUtils.long3(casValue); + bs[21] = ByteUtils.long2(casValue); + bs[22] = ByteUtils.long1(casValue); + bs[23] = ByteUtils.long0(casValue); + this.ioBuffer.put(bs); + } + + protected long getCasValue() { + return 0L; + } + + protected int getValueLength(final CachedData data) { + return data.getData().length; + } + + protected short getKeyLength() { + return (short) this.keyBytes.length; + } + + protected byte getExtrasLength() { + return EXTRAS_LENGTH; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAppendPrependCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAppendPrependCommand.java new file mode 100644 index 0000000..61abc8e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAppendPrependCommand.java @@ -0,0 +1,60 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.exception.UnknownCommandException; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.Transcoder; + +/** + * Binary protocol for append,prepend + * + * @author dennis + * + */ +@SuppressWarnings("unchecked") +public class BinaryAppendPrependCommand extends BaseBinaryCommand { + + public BinaryAppendPrependCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, int exp, long cas, Object value, boolean noreply, + Transcoder transcoder) { + super(key, keyBytes, cmdType, latch, exp, cas, value, noreply, transcoder); + switch (cmdType) { + case APPEND: + this.opCode = noreply ? OpCode.APPEND_QUIETLY : OpCode.APPEND; + break; + case PREPEND: + this.opCode = noreply ? OpCode.PREPEND_QUIETLY : OpCode.PREPEND; + break; + default: + throw new UnknownCommandException("Not a append or prepend command:" + cmdType.name()); + } + } + + @Override + protected void fillExtras(CachedData data) { + // no extras + } + + @Override + protected byte getExtrasLength() { + return 0; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthListMechanismsCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthListMechanismsCommand.java new file mode 100644 index 0000000..815a793 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthListMechanismsCommand.java @@ -0,0 +1,63 @@ +package net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * List auth mechanisms command + * + * @author dennis + * + */ +public class BinaryAuthListMechanismsCommand extends BaseBinaryCommand { + + public BinaryAuthListMechanismsCommand(CountDownLatch latch) { + super(null, null, CommandType.AUTH_LIST, latch, 0, 0, null, false, null); + this.opCode = OpCode.AUTH_LIST_MECHANISMS; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + int valueLength = bodyLength - keyLength - extrasLength; + if (buffer.remaining() < valueLength) { + return false; + } + byte[] bytes = new byte[valueLength]; + buffer.get(bytes); + setResult(new String(bytes)); + countDownLatch(); + return true; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected void fillKey() { + // must not have key + } + + @Override + protected short getKeyLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthStartCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthStartCommand.java new file mode 100644 index 0000000..ee84a78 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthStartCommand.java @@ -0,0 +1,60 @@ +package net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Auth start command + * + * @author dennis + * + */ +public class BinaryAuthStartCommand extends BaseBinaryCommand { + + public BinaryAuthStartCommand(String mechanism, byte[] keyBytes, CountDownLatch latch, + byte[] authData) { + super(mechanism, keyBytes, CommandType.AUTH_START, latch, 0, 0, authData, false, null); + this.opCode = OpCode.AUTH_START; + + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + if (this.value != null) + this.ioBuffer.put((byte[]) this.value); + } + + @Override + protected int getValueLength(CachedData data) { + if (this.value == null) + return 0; + else + return ((byte[]) this.value).length; + } + + @Override + protected byte getExtrasLength() { + return (byte) 0; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + int valueLength = bodyLength - keyLength - extrasLength; + if (buffer.remaining() < valueLength) { + return false; + } + byte[] bytes = new byte[valueLength]; + buffer.get(bytes); + setResult(new String(bytes)); + countDownLatch(); + return true; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthStepCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthStepCommand.java new file mode 100644 index 0000000..ca41f55 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAuthStepCommand.java @@ -0,0 +1,59 @@ +package net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Auth step command + * + * @author dennis + * + */ +public class BinaryAuthStepCommand extends BaseBinaryCommand { + + public BinaryAuthStepCommand(String mechanism, byte[] keyBytes, CountDownLatch latch, + byte[] authData) { + super(mechanism, keyBytes, CommandType.AUTH_STEP, latch, 0, 0, authData, false, null); + this.opCode = OpCode.AUTH_STEP; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + if (this.value != null) + this.ioBuffer.put((byte[]) this.value); + } + + @Override + protected int getValueLength(CachedData data) { + if (this.value == null) + return 0; + else + return ((byte[]) this.value).length; + } + + @Override + protected byte getExtrasLength() { + return (byte) 0; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + int valueLength = bodyLength - keyLength - extrasLength; + if (buffer.remaining() < valueLength) { + return false; + } + byte[] bytes = new byte[valueLength]; + buffer.get(bytes); + setResult(new String(bytes)); + countDownLatch(); + return true; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAutoDiscoveryCacheConfigCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAutoDiscoveryCacheConfigCommand.java new file mode 100644 index 0000000..d6080b3 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryAutoDiscoveryCacheConfigCommand.java @@ -0,0 +1,80 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Auto Discovery config command + * + * @author dennis + * + */ +public class BinaryAutoDiscoveryCacheConfigCommand extends BaseBinaryCommand { + + public BinaryAutoDiscoveryCacheConfigCommand(final CountDownLatch latch, String subCommand, + String key) { + super(key, ByteUtils.getBytes(key), CommandType.AUTO_DISCOVERY_CONFIG, latch, 0, 0, latch, + false, null); + this.commandType = CommandType.AUTO_DISCOVERY_CONFIG; + if (subCommand.equals("get")) { + this.opCode = OpCode.CONFIG_GET; + } else if (subCommand.equals("set")) { + this.opCode = OpCode.CONFIG_SET; + } else if (subCommand.equals("delete")) { + this.opCode = OpCode.CONFIG_DEL; + } + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + int valueLength = bodyLength - keyLength - extrasLength; + if (buffer.remaining() < valueLength) { + return false; + } + byte[] bytes = new byte[valueLength]; + buffer.get(bytes); + setResult(new String(bytes)); + countDownLatch(); + return true; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryCASCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryCASCommand.java new file mode 100644 index 0000000..b56badb --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryCASCommand.java @@ -0,0 +1,50 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.Transcoder; + +/** + * CAS binary protocol implementation + * + * @author dennis + * + */ +@SuppressWarnings("unchecked") +public class BinaryCASCommand extends BaseBinaryCommand { + + public BinaryCASCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int exp, long cas, Object value, boolean noreply, Transcoder transcoder) { + super(key, keyBytes, cmdType, latch, exp, cas, value, noreply, transcoder); + switch (cmdType) { + case CAS: + this.opCode = noreply ? OpCode.SET_QUIETLY : OpCode.SET; + break; + default: + throw new IllegalArgumentException("Unknow cas command type:" + cmdType); + } + + } + + @Override + protected long getCasValue() { + return this.cas; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryDecodeStatus.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryDecodeStatus.java new file mode 100644 index 0000000..b9eb99d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryDecodeStatus.java @@ -0,0 +1,27 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +/** + * Binary protocol decode status. + * + * @author dennis + * + */ +public enum BinaryDecodeStatus { + NONE, READ_EXTRAS, READ_KEY, READ_VALUE, DONE, IGNORE; +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryDeleteCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryDeleteCommand.java new file mode 100644 index 0000000..a95f1f4 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryDeleteCommand.java @@ -0,0 +1,79 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Binary delete command + * + * @author boyan + * + */ +public class BinaryDeleteCommand extends BaseBinaryCommand { + + public BinaryDeleteCommand(String key, byte[] keyBytes, long cas, CommandType cmdType, + CountDownLatch latch, boolean noreply) { + super(key, keyBytes, cmdType, latch, 0, cas, null, noreply, null); + this.opCode = noreply ? OpCode.DELETE_QUIETLY : OpCode.DELETE; + } + + /** + * optimistic,if no error,goto done + */ + @Override + protected void readHeader(ByteBuffer buffer) { + super.readHeader(buffer); + if (this.responseStatus == ResponseStatus.NO_ERROR) { + this.decodeStatus = BinaryDecodeStatus.DONE; + } + + } + + @Override + protected long getCasValue() { + if (this.cas > 0) { + return this.cas; + } else { + return super.getCasValue(); + } + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryFlushAllCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryFlushAllCommand.java new file mode 100644 index 0000000..29e0898 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryFlushAllCommand.java @@ -0,0 +1,75 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Flush command for binary protocol + * + * @author dennis + * + */ +public class BinaryFlushAllCommand extends BaseBinaryCommand { + + private int exptime; + + public BinaryFlushAllCommand(CountDownLatch latch, int exptime, boolean noreply) { + super("[flush_all]", null, CommandType.FLUSH_ALL, latch, 0, 0, null, noreply, null); + this.opCode = noreply ? OpCode.FLUSH_QUIETLY : OpCode.FLUSH; + this.expTime = exptime; + } + + @Override + protected void fillExtras(CachedData data) { + if (this.expTime > 0) { + this.ioBuffer.putInt(this.expTime); + } + } + + @Override + protected byte getExtrasLength() { + if (this.exptime > 0) { + return 4; + } else { + return 0; + } + } + + @Override + protected short getKeyLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + + @Override + protected void fillKey() { + // must not have key + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetAndTouchCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetAndTouchCommand.java new file mode 100644 index 0000000..878460e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetAndTouchCommand.java @@ -0,0 +1,41 @@ +package net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Binary GAT/GATQ command + * + * @author dennis + * + */ +public class BinaryGetAndTouchCommand extends BinaryGetCommand { + + public BinaryGetAndTouchCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, int exp, boolean noreply) { + super(key, keyBytes, cmdType, latch, null, noreply); + this.expTime = exp; + switch (cmdType) { + case GAT: + this.opCode = OpCode.GAT; + break; + case GATQ: + this.opCode = OpCode.GATQ; + break; + default: + throw new IllegalArgumentException("Invalid GAT command type:" + cmdType); + } + } + + @Override + protected void fillExtras(CachedData data) { + this.ioBuffer.putInt(this.expTime); + } + + @Override + protected byte getExtrasLength() { + return 4; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetCommand.java new file mode 100644 index 0000000..991a0c6 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetCommand.java @@ -0,0 +1,158 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.AssocCommandAware; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Implements get/getq,getk/getkq protocol + * + * @author dennis + * + */ +public class BinaryGetCommand extends BaseBinaryCommand implements AssocCommandAware { + private String responseKey; + private CachedData responseValue; + private List assocCommands; + + public final String getResponseKey() { + return this.responseKey; + } + + public final void setResponseKey(String responseKey) { + this.responseKey = responseKey; + } + + public BinaryGetCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + OpCode opCode, boolean noreply) { + super(key, keyBytes, cmdType, latch, 0, 0, null, noreply, null); + this.opCode = opCode; + this.responseValue = new CachedData(); + } + + public final List getAssocCommands() { + return this.assocCommands; + } + + public final void setAssocCommands(List assocCommands) { + this.assocCommands = assocCommands; + } + + /** + * Optimistic,if the value length is 0,then skip remaining buffer,set result as null + */ + protected void readHeader(ByteBuffer buffer) { + super.readHeader(buffer); + if (this.responseStatus != ResponseStatus.NO_ERROR) { + if (ByteUtils.stepBuffer(buffer, this.responseTotalBodyLength)) { + this.decodeStatus = BinaryDecodeStatus.DONE; + } + } + + } + + @Override + protected boolean finish() { + countDownLatch(); + return true; + } + + @Override + protected boolean readKey(ByteBuffer buffer, int keyLength) { + if (buffer.remaining() < keyLength) { + return false; + } + if (keyLength > 0) { + byte[] bytes = new byte[keyLength]; + buffer.get(bytes); + this.responseKey = ByteUtils.getString(bytes); + } + return true; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + if (this.responseStatus == ResponseStatus.NO_ERROR) { + int valueLength = bodyLength - keyLength - extrasLength; + if (valueLength >= 0 && this.responseValue.getCapacity() < 0) { + this.responseValue.setCapacity(valueLength); + this.responseValue.setData(new byte[valueLength]); + } + int remainingCapacity = this.responseValue.remainingCapacity(); + int remaining = buffer.remaining(); + if (remaining < remainingCapacity) { + int length = remaining > remainingCapacity ? remainingCapacity : remaining; + this.responseValue.fillData(buffer, length); + return false; + } else if (remainingCapacity > 0) { + this.responseValue.fillData(buffer, remainingCapacity); + } + setResult(this.responseValue); + return true; + } else { + return ByteUtils.stepBuffer(buffer, bodyLength - keyLength - extrasLength); + } + } + + @Override + protected boolean readExtras(ByteBuffer buffer, int extrasLength) { + if (buffer.remaining() < extrasLength) { + return false; + } + if (extrasLength > 0) { + // read flag + int flag = buffer.getInt(); + this.responseValue.setFlag(flag); + } + return true; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + + @Override + protected long readCAS(ByteBuffer buffer) { + long cas = buffer.getLong(); + this.responseValue.setCas(cas); + return cas; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetMultiCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetMultiCommand.java new file mode 100644 index 0000000..14b3c96 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryGetMultiCommand.java @@ -0,0 +1,193 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.MapReturnValueAware; +import net.rubyeye.xmemcached.command.MergeCommandsAware; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * A command for holding getkq commands + * + * @author dennis + * + */ +@SuppressWarnings("unchecked") +public class BinaryGetMultiCommand extends BaseBinaryCommand + implements MergeCommandsAware, MapReturnValueAware { + private boolean finished; + private String responseKey; + private long responseCAS; + private int responseFlag; + private Map mergeCommands; + + public BinaryGetMultiCommand(String key, CommandType cmdType, CountDownLatch latch) { + super(key, null, cmdType, latch, 0, 0, null, false, null); + this.result = new HashMap(); + } + + public Map getReturnValues() { + return (Map) this.result; + } + + @Override + protected boolean readOpCode(ByteBuffer buffer) { + byte opCode = buffer.get(); + // last response is GET_KEY,then finish decoding + if (opCode == OpCode.GET_KEY.fieldValue()) { + this.finished = true; + } + return true; + } + + /** + * optimistic,if response status is greater than zero,then skip buffer to next response,set result + * as null + */ + protected void readHeader(ByteBuffer buffer) { + super.readHeader(buffer); + if (this.responseStatus != ResponseStatus.NO_ERROR) { + if (ByteUtils.stepBuffer(buffer, this.responseTotalBodyLength)) { + this.decodeStatus = BinaryDecodeStatus.DONE; + } + } + } + + @Override + public void encode() { + // do nothing + } + + @Override + protected boolean finish() { + final CachedData cachedData = ((Map) this.result).get(this.responseKey); + Map mergetCommands = getMergeCommands(); + if (mergetCommands != null) { + final BinaryGetCommand command = (BinaryGetCommand) mergetCommands.remove(this.responseKey); + if (command != null) { + command.setResult(cachedData); + command.countDownLatch(); + this.mergeCount--; + if (command.getAssocCommands() != null) { + for (Command assocCommand : command.getAssocCommands()) { + assocCommand.setResult(cachedData); + assocCommand.countDownLatch(); + this.mergeCount--; + } + } + + } + } + if (this.finished) { + if (getMergeCommands() != null) { + Collection mergeCommands = getMergeCommands().values(); + getIoBuffer().free(); + for (Command nextCommand : mergeCommands) { + BinaryGetCommand command = (BinaryGetCommand) nextCommand; + command.countDownLatch(); + if (command.getAssocCommands() != null) { + for (Command assocCommand : command.getAssocCommands()) { + assocCommand.countDownLatch(); + } + } + } + } + countDownLatch(); + } else { + + this.responseKey = null; + } + return this.finished; + } + + @Override + protected boolean readKey(ByteBuffer buffer, int keyLength) { + if (buffer.remaining() < keyLength) { + return false; + } + if (keyLength > 0) { + byte[] bytes = new byte[keyLength]; + buffer.get(bytes); + this.responseKey = ByteUtils.getString(bytes); + CachedData value = new CachedData(); + value.setCas(this.responseCAS); + value.setFlag(this.responseFlag); + ((Map) this.result).put(this.responseKey, value); + } + return true; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + if (this.responseStatus == ResponseStatus.NO_ERROR) { + int valueLength = bodyLength - keyLength - extrasLength; + CachedData responseValue = ((Map) this.result).get(this.responseKey); + if (valueLength >= 0 && responseValue.getCapacity() < 0) { + responseValue.setCapacity(valueLength); + responseValue.setData(new byte[valueLength]); + } + int remainingCapacity = responseValue.remainingCapacity(); + int remaining = buffer.remaining(); + if (remaining < remainingCapacity) { + int length = remaining > remainingCapacity ? remainingCapacity : remaining; + responseValue.fillData(buffer, length); + return false; + } else if (remainingCapacity > 0) { + responseValue.fillData(buffer, remainingCapacity); + } + return true; + } else { + ((Map) this.result).remove(this.responseKey); + return true; + } + } + + @Override + protected boolean readExtras(ByteBuffer buffer, int extrasLength) { + if (buffer.remaining() < extrasLength) { + return false; + } + if (extrasLength == 4) { + // read flag + this.responseFlag = buffer.getInt(); + } + return true; + } + + @Override + protected long readCAS(ByteBuffer buffer) { + this.responseCAS = buffer.getLong(); + return this.responseCAS; + } + + public Map getMergeCommands() { + return this.mergeCommands; + } + + public void setMergeCommands(Map mergeCommands) { + this.mergeCommands = mergeCommands; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryIncrDecrCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryIncrDecrCommand.java new file mode 100644 index 0000000..5f9b224 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryIncrDecrCommand.java @@ -0,0 +1,101 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Binary incr/decr command + * + * @author dennis + * + */ +public class BinaryIncrDecrCommand extends BaseBinaryCommand { + + private long amount, initial; + + public final long getAmount() { + return this.amount; + } + + public final void setAmount(long amount) { + this.amount = amount; + } + + public final long getInitial() { + return this.initial; + } + + public final void setInitial(long initial) { + this.initial = initial; + } + + public BinaryIncrDecrCommand(String key, byte[] keyBytes, long amount, long initial, int expTime, + CommandType cmdType, boolean noreply) { + super(key, keyBytes, cmdType, new CountDownLatch(1), 0, 0, null, noreply, null); + this.amount = amount; + this.initial = initial; + this.expTime = expTime; + switch (cmdType) { + case INCR: + this.opCode = noreply ? OpCode.INCREMENT_QUIETLY : OpCode.INCREMENT; + break; + case DECR: + this.opCode = noreply ? OpCode.DECREMENT_QUIETLY : OpCode.DECREMENT; + break; + default: + throw new IllegalArgumentException("Unknow cmd type for incr/decr:" + cmdType); + } + + } + + @Override + protected void fillExtras(CachedData data) { + this.ioBuffer.putLong(this.amount); + this.ioBuffer.putLong(this.initial); + this.ioBuffer.putInt(this.expTime); + } + + @Override + protected byte getExtrasLength() { + return 20; + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + if (buffer.remaining() < 8) { + return false; + } + long returnValue = buffer.getLong(); + setResult(returnValue); + return true; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryNoopCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryNoopCommand.java new file mode 100644 index 0000000..41a3fb5 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryNoopCommand.java @@ -0,0 +1,35 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; + +/** + * Implement noop protocol + * + * @author dennis + * + */ +public class BinaryNoopCommand extends BaseBinaryCommand { + + public BinaryNoopCommand(CountDownLatch latch) { + super(null, null, CommandType.NOOP, latch, 0, 0, null, false, null); + this.opCode = OpCode.NOOP; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryQuitCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryQuitCommand.java new file mode 100644 index 0000000..9dac47b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryQuitCommand.java @@ -0,0 +1,74 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Quit command for binary protocol + * + * @author boyan + * + */ +public class BinaryQuitCommand extends BaseBinaryCommand { + + public BinaryQuitCommand() { + super("version", null, CommandType.VERSION, null, 0, 0, null, false, null); + commandType = CommandType.QUIT; + opCode = OpCode.QUITQ; + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + // ignore + return true; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected void fillKey() { + // must not have key + } + + @Override + protected short getKeyLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinarySetMultiCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinarySetMultiCommand.java new file mode 100644 index 0000000..6405f11 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinarySetMultiCommand.java @@ -0,0 +1,134 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.MapReturnValueAware; +import net.rubyeye.xmemcached.command.MergeCommandsAware; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * A command for holding getkq commands + * + * @author dennis + * + */ +@SuppressWarnings("unchecked") +public class BinarySetMultiCommand extends BaseBinaryCommand implements MergeCommandsAware { + private boolean finished; + private Integer responseOpaque; + private Map mergeCommands; + + public BinarySetMultiCommand(String key, CommandType cmdType, CountDownLatch latch) { + super(key, null, cmdType, latch, 0, 0, null, false, null); + this.result = new HashMap(); + } + + @Override + protected boolean readOpCode(ByteBuffer buffer) { + byte opCode = buffer.get(); + // last response is SET,then finish decoding + if (opCode == OpCode.SET.fieldValue()) { + this.finished = true; + } + return true; + } + + /** + * optimistic,if response status is greater than zero,then skip buffer to next response,set result + * to be false. + */ + protected void readHeader(ByteBuffer buffer) { + super.readHeader(buffer); + if (this.responseStatus != ResponseStatus.NO_ERROR) { + if (ByteUtils.stepBuffer(buffer, this.responseTotalBodyLength)) { + this.decodeStatus = BinaryDecodeStatus.DONE; + } + } + } + + public Map getMergeCommands() { + return mergeCommands; + } + + public void setMergeCommands(Map mergeCommands) { + this.mergeCommands = mergeCommands; + } + + @Override + public void encode() { + // do nothing + } + + @Override + protected boolean finish() { + final Boolean rt = ((Map) this.result).get(this.responseOpaque); + Map mergetCommands = getMergeCommands(); + if (mergetCommands != null) { + final BinaryStoreCommand command = + (BinaryStoreCommand) mergetCommands.remove(this.responseOpaque); + if (command != null) { + command.setResult(rt); + command.countDownLatch(); + this.mergeCount--; + } + } + if (this.finished) { + if (getMergeCommands() != null) { + Collection mergeCommands = getMergeCommands().values(); + getIoBuffer().free(); + for (Command nextCommand : mergeCommands) { + BinaryStoreCommand command = (BinaryStoreCommand) nextCommand; + // Default result is true,it's quiet. + command.setResult(Boolean.TRUE); + command.countDownLatch(); + this.mergeCount--; + } + } + assert (mergeCount == 0); + countDownLatch(); + } else { + this.responseOpaque = null; + } + return this.finished; + } + + protected boolean readOpaque(ByteBuffer buffer) { + responseOpaque = buffer.getInt(); + + Command cmd = this.getMergeCommands().get(responseOpaque); + if (cmd == null) { + // It's not this command's merged commands,we must ignore it. + return false; + } else { + if (this.responseStatus == ResponseStatus.NO_ERROR) { + ((Map) this.result).put(responseOpaque, Boolean.TRUE); + } else { + ((Map) this.result).put(responseOpaque, Boolean.FALSE); + } + return true; + } + + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryStatsCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryStatsCommand.java new file mode 100644 index 0000000..ab9f5cf --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryStatsCommand.java @@ -0,0 +1,152 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import net.rubyeye.xmemcached.exception.UnknownCommandException; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Stats command for binary protocol + * + * @author boyan + * + */ +public class BinaryStatsCommand extends BaseBinaryCommand implements ServerAddressAware { + + private InetSocketAddress server; + private String itemName; + private String currentResponseItem; + + public String getItemName() { + return this.itemName; + } + + public final InetSocketAddress getServer() { + return this.server; + } + + public final void setServer(InetSocketAddress server) { + this.server = server; + } + + public void setItemName(String item) { + this.itemName = item; + } + + public BinaryStatsCommand(InetSocketAddress server, CountDownLatch latch, String itemName) { + super(null, null, CommandType.STATS, latch, 0, 0, null, false, null); + this.server = server; + this.itemName = itemName; + this.opCode = OpCode.STAT; + this.result = new HashMap(); + } + + @Override + protected boolean finish() { + // last packet + if (this.currentResponseItem == null) { + return super.finish(); + } else { + // continue decode + this.currentResponseItem = null; + return false; + } + } + + @Override + protected void readStatus(ByteBuffer buffer) { + ResponseStatus responseStatus = ResponseStatus.parseShort(buffer.getShort()); + if (responseStatus == ResponseStatus.UNKNOWN_COMMAND) { + setException(new UnknownCommandException()); + } + } + + @Override + protected boolean readKey(ByteBuffer buffer, int keyLength) { + if (buffer.remaining() < keyLength) { + return false; + } + if (keyLength > 0) { + byte[] bytes = new byte[keyLength]; + buffer.get(bytes); + this.currentResponseItem = new String(bytes); + } + return true; + } + + @Override + @SuppressWarnings("unchecked") + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + int valueLength = bodyLength - keyLength - extrasLength; + if (buffer.remaining() < valueLength) { + return false; + } + if (valueLength > 0) { + byte[] bytes = new byte[valueLength]; + buffer.get(bytes); + String value = new String(bytes); + ((Map) this.result).put(this.currentResponseItem, value); + } + return true; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + + @Override + protected void fillKey() { + if (this.itemName != null) { + byte[] keyBytes = ByteUtils.getBytes(this.itemName); + this.ioBuffer.put(keyBytes); + } + } + + @Override + protected short getKeyLength() { + if (this.itemName != null) { + return (short) this.itemName.length(); + } else { + return 0; + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryStoreCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryStoreCommand.java new file mode 100644 index 0000000..0d95fa4 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryStoreCommand.java @@ -0,0 +1,67 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.AssocCommandAware; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.Transcoder; + +/** + * Base binary protocol implementation + * + * @author dennis + * + */ +public class BinaryStoreCommand extends BaseBinaryCommand { + + public BinaryStoreCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int exp, long cas, Object value, boolean noreply, Transcoder transcoder) { + super(key, keyBytes, cmdType, latch, exp, cas, value, noreply, transcoder); + switch (cmdType) { + case SET: + this.opCode = noreply ? OpCode.SET_QUIETLY : OpCode.SET; + break; + case REPLACE: + this.opCode = noreply ? OpCode.REPLACE_QUIETLY : OpCode.REPLACE; + break; + case ADD: + this.opCode = noreply ? OpCode.ADD_QUIETLY : OpCode.ADD; + break; + case SET_MANY: + // ignore + break; + default: + throw new IllegalArgumentException("Unknow cmd type for storage commands:" + cmdType); + + } + } + + /** + * optimistic,if no error,goto done + */ + protected void readHeader(ByteBuffer buffer) { + super.readHeader(buffer); + if (this.responseStatus == ResponseStatus.NO_ERROR) { + this.decodeStatus = BinaryDecodeStatus.DONE; + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryTouchCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryTouchCommand.java new file mode 100644 index 0000000..027cc5b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryTouchCommand.java @@ -0,0 +1,42 @@ +package net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Binary touch command + * + * @author dennis + * @since 1.3.3 + */ +public class BinaryTouchCommand extends BaseBinaryCommand { + + public BinaryTouchCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int exp, boolean noreply) { + super(key, keyBytes, cmdType, latch, exp, 0, null, noreply, null); + this.opCode = OpCode.TOUCH; + + } + + @Override + protected void fillExtras(CachedData data) { + this.ioBuffer.putInt(this.expTime); + } + + @Override + protected void fillValue(CachedData data) { + // Must not have value + } + + @Override + protected byte getExtrasLength() { + return 4; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryVerbosityCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryVerbosityCommand.java new file mode 100644 index 0000000..61120ba --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryVerbosityCommand.java @@ -0,0 +1,52 @@ +package net.rubyeye.xmemcached.command.binary; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Binary verbosity command + * + * @author dennis + * @since 1.3.3 + * + */ +public class BinaryVerbosityCommand extends BaseBinaryCommand { + + private int verbosity; + + public BinaryVerbosityCommand(CountDownLatch latch, int verbosity, boolean noreply) { + super(null, null, CommandType.VERBOSITY, latch, 0, 0, null, noreply, null); + this.opCode = OpCode.VERBOSITY; + } + + @Override + protected void fillExtras(CachedData data) { + this.ioBuffer.putInt(verbosity); + } + + protected void fillKey() { + // MUST NOT have key. + } + + @Override + protected byte getExtrasLength() { + // Total 4 bytes + return 4; + } + + @Override + protected short getKeyLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + + protected void fillValue(CachedData data) { + // MUST NOT have value. + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryVersionCommand.java b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryVersionCommand.java new file mode 100644 index 0000000..8ef2efa --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/BinaryVersionCommand.java @@ -0,0 +1,93 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Version command for binary protocol + * + * @author boyan + * + */ +public class BinaryVersionCommand extends BaseBinaryCommand implements ServerAddressAware { + public InetSocketAddress server; + + public final InetSocketAddress getServer() { + return this.server; + } + + public final void setServer(InetSocketAddress server) { + this.server = server; + } + + public BinaryVersionCommand(final CountDownLatch latch, InetSocketAddress server) { + super("[version]", null, CommandType.VERSION, latch, 0, 0, latch, false, null); + this.commandType = CommandType.VERSION; + this.server = server; + this.opCode = OpCode.VERSION; + } + + @Override + protected boolean readValue(ByteBuffer buffer, int bodyLength, int keyLength, int extrasLength) { + int valueLength = bodyLength - keyLength - extrasLength; + if (buffer.remaining() < valueLength) { + return false; + } + byte[] bytes = new byte[valueLength]; + buffer.get(bytes); + setResult(new String(bytes)); + countDownLatch(); + return true; + } + + @Override + protected void fillExtras(CachedData data) { + // must not have extras + } + + @Override + protected void fillValue(CachedData data) { + // must not have value + } + + @Override + protected byte getExtrasLength() { + return 0; + } + + @Override + protected void fillKey() { + // must not have key + } + + @Override + protected short getKeyLength() { + return 0; + } + + @Override + protected int getValueLength(CachedData data) { + return 0; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/OpCode.java b/src/main/java/net/rubyeye/xmemcached/command/binary/OpCode.java new file mode 100644 index 0000000..fd8e7cf --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/OpCode.java @@ -0,0 +1,285 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +/** + * Binary command Opcodes + * + * @author dennis + * + */ +public enum OpCode { + GET { + @Override + public byte fieldValue() { + return 0x00; + + } + }, + GET_QUIETLY { + @Override + public byte fieldValue() { + return 0x09; + + } + }, + GET_KEY { + @Override + public byte fieldValue() { + return 0x0C; + + } + }, + GET_KEY_QUIETLY { + @Override + public byte fieldValue() { + return 0x0D; + + } + }, + SET { + @Override + public byte fieldValue() { + return 0x01; + + } + }, + SET_QUIETLY { + @Override + public byte fieldValue() { + return 0x11; + + } + }, + REPLACE { + @Override + public byte fieldValue() { + return 0x03; + + } + }, + REPLACE_QUIETLY { + @Override + public byte fieldValue() { + return 0x13; + + } + }, + ADD { + @Override + public byte fieldValue() { + return 0x02; + + } + }, + ADD_QUIETLY { + @Override + public byte fieldValue() { + return 0x12; + + } + }, + APPEND { + @Override + public byte fieldValue() { + return 0x0E; + + } + }, + APPEND_QUIETLY { + @Override + public byte fieldValue() { + return 0x19; + + } + }, + PREPEND { + @Override + public byte fieldValue() { + return 0x0F; + + } + }, + PREPEND_QUIETLY { + @Override + public byte fieldValue() { + return 0x1A; + + } + }, + DELETE { + @Override + public byte fieldValue() { + return 0x04; + + } + }, + DELETE_QUIETLY { + @Override + public byte fieldValue() { + return 0x14; + + } + }, + VERSION { + @Override + public byte fieldValue() { + return 0x0b; + + } + }, + QUITQ { + @Override + public byte fieldValue() { + return 0x17; + + } + }, + STAT { + @Override + public byte fieldValue() { + return 0x10; + + } + }, + NOOP { + @Override + public byte fieldValue() { + return 0x0a; + + } + }, + INCREMENT { + @Override + public byte fieldValue() { + return 0x05; + + } + }, + INCREMENT_QUIETLY { + @Override + public byte fieldValue() { + return 0x15; + + } + }, + DECREMENT { + @Override + public byte fieldValue() { + return 0x06; + + } + }, + DECREMENT_QUIETLY { + @Override + public byte fieldValue() { + return 0x16; + + } + }, + FLUSH { + @Override + public byte fieldValue() { + return 0x08; + + } + }, + FLUSH_QUIETLY { + @Override + public byte fieldValue() { + return 0x18; + + } + }, + AUTH_LIST_MECHANISMS { + @Override + public byte fieldValue() { + return 0x20; + + } + }, + AUTH_START { + @Override + public byte fieldValue() { + return 0x21; + + } + }, + AUTH_STEP { + @Override + public byte fieldValue() { + return 0x22; + + } + }, + + VERBOSITY { + @Override + public byte fieldValue() { + return 0x1b; + + } + }, + + TOUCH { + @Override + public byte fieldValue() { + return 0x1c; + + } + }, + GAT { + @Override + public byte fieldValue() { + return 0x1d; + + } + }, + GATQ { + @Override + public byte fieldValue() { + return 0x1e; + + } + }, + // Auto Discovery config commands + // https://github.com/awslabs/aws-elasticache-cluster-client-memcached-for-java/commit/70bf7643963500db20749d97c071b64b954eabb3 + // https://cloud.google.com/memorystore/docs/memcached/use-auto-discovery + CONFIG_GET { + @Override + public byte fieldValue() { + return 0x60; + + } + }, + CONFIG_SET { + @Override + public byte fieldValue() { + return 0x64; + + } + }, + CONFIG_DEL { + @Override + public byte fieldValue() { + return 0x66; + + } + }; + + public abstract byte fieldValue(); +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/ResponseStatus.java b/src/main/java/net/rubyeye/xmemcached/command/binary/ResponseStatus.java new file mode 100644 index 0000000..5049039 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/ResponseStatus.java @@ -0,0 +1,284 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.binary; + +/** + * Binary protocol response status. + * + * @author dennis + * + */ +public enum ResponseStatus { + + NO_ERROR { + @Override + public short fieldValue() { + return 0x0000; + } + + @Override + public String errorMessage() { + return "No error"; + } + }, + KEY_NOT_FOUND { + @Override + public short fieldValue() { + return 0x0001; + } + + @Override + public String errorMessage() { + return "Key is not found."; + } + }, + KEY_EXISTS { + @Override + public short fieldValue() { + return 0x0002; + } + + @Override + public String errorMessage() { + return "Key is already existed."; + } + }, + VALUE_TOO_BIG { + @Override + public short fieldValue() { + return 0x0003; + } + + @Override + public String errorMessage() { + return "Value is too big."; + } + }, + INVALID_ARGUMENTS { + @Override + public short fieldValue() { + return 0x0004; + } + + @Override + public String errorMessage() { + return "Invalid arguments."; + } + }, + ITEM_NOT_STORED { + @Override + public short fieldValue() { + return 0x0005; + } + + @Override + public String errorMessage() { + return "Item is not stored."; + } + }, + INC_DEC_NON_NUM { + @Override + public short fieldValue() { + return 0x0006; + } + + @Override + public String errorMessage() { + return "Incr/Decr on non-numeric value."; + } + }, + BELONGS_TO_ANOTHER_SRV { + @Override + public short fieldValue() { + return 0x0007; + } + + @Override + public String errorMessage() { + return "The vbucket belongs to another server."; + } + }, + AUTH_ERROR { + @Override + public short fieldValue() { + return 0x0008; + } + + @Override + public String errorMessage() { + return "Authentication error ."; + } + }, + AUTH_CONTINUE { + @Override + public short fieldValue() { + return 0x0009; + } + + @Override + public String errorMessage() { + return "Authentication continue ."; + } + }, + UNKNOWN_COMMAND { + @Override + public short fieldValue() { + return 0x0081; + } + + @Override + public String errorMessage() { + return "Unknown command error."; + } + }, + OUT_OF_MEMORY { + @Override + public short fieldValue() { + return 0x0082; + } + + @Override + public String errorMessage() { + return "Out of memory ."; + } + }, + NOT_SUPPORTED { + @Override + public short fieldValue() { + return 0x0083; + } + + @Override + public String errorMessage() { + return "Not supported ."; + } + }, + INTERNAL_ERROR { + @Override + public short fieldValue() { + return 0x0084; + } + + @Override + public String errorMessage() { + return "Internal error ."; + } + }, + BUSY { + @Override + public short fieldValue() { + return 0x0085; + } + + @Override + public String errorMessage() { + return "Busy."; + } + }, + + TEMP_FAILURE { + @Override + public short fieldValue() { + return 0x0086; + } + + @Override + public String errorMessage() { + return "Temporary failure ."; + } + }, + + AUTH_REQUIRED { + @Override + public short fieldValue() { + return 0x20; + } + + @Override + public String errorMessage() { + return "Authentication required or not successful"; + } + }, + FUTHER_AUTH_REQUIRED { + @Override + public short fieldValue() { + return 0x21; + } + + @Override + public String errorMessage() { + return "Further authentication steps required. "; + } + }; + abstract short fieldValue(); + + /** + * Get status from short value + * + * @param value + * @return + */ + public static ResponseStatus parseShort(short value) { + switch (value) { + case 0x0000: + return NO_ERROR; + case 0x0001: + return KEY_NOT_FOUND; + case 0x0002: + return KEY_EXISTS; + case 0x0003: + return VALUE_TOO_BIG; + case 0x0004: + return INVALID_ARGUMENTS; + case 0x0005: + return ITEM_NOT_STORED; + case 0x0006: + return INC_DEC_NON_NUM; + case 0x0007: + return BELONGS_TO_ANOTHER_SRV; + case 0x0008: + return AUTH_ERROR; + case 0x0009: + return AUTH_CONTINUE; + case 0x0081: + return UNKNOWN_COMMAND; + case 0x0082: + return OUT_OF_MEMORY; + case 0x0083: + return NOT_SUPPORTED; + case 0x0084: + return INTERNAL_ERROR; + case 0x0085: + return BUSY; + case 0x0086: + return TEMP_FAILURE; + case 0x20: + return AUTH_REQUIRED; + case 0x21: + return FUTHER_AUTH_REQUIRED; + default: + throw new IllegalArgumentException("Unknow Response status:" + value); + } + } + + /** + * The status error message + * + * @return + */ + abstract String errorMessage(); +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/binary/package.html b/src/main/java/net/rubyeye/xmemcached/command/binary/package.html new file mode 100644 index 0000000..f399686 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/binary/package.html @@ -0,0 +1,10 @@ + + + + + Memcached text protocol implementations + + +

Memcached text protocol implementations

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelDeleteCommand.java b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelDeleteCommand.java new file mode 100644 index 0000000..4287f4a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelDeleteCommand.java @@ -0,0 +1,51 @@ +package net.rubyeye.xmemcached.command.kestrel; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.text.TextDeleteCommand; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.utils.ByteUtils; + +public class KestrelDeleteCommand extends TextDeleteCommand { + + public KestrelDeleteCommand(String key, byte[] keyBytes, int time, CountDownLatch latch, + boolean noreply) { + super(key, keyBytes, time, latch, noreply); + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + if (this.result == null) { + if (buffer.remaining() < 2) { + return false; + } + byte first = buffer.get(buffer.position()); + byte second = buffer.get(buffer.position() + 1); + if (first == 'E' && second == 'N') { + this.setResult(Boolean.TRUE); + this.countDownLatch(); + // END\r\n + return ByteUtils.stepBuffer(buffer, 5); + } else if (first == 'D' && second == 'E') { + this.setResult(Boolean.TRUE); + this.countDownLatch(); + // DELETED\r\n + return ByteUtils.stepBuffer(buffer, 9); + } else { + return this.decodeError(session, buffer); + } + } else { + Boolean result = (Boolean) this.result; + if (result) { + // END\r\n + return ByteUtils.stepBuffer(buffer, 5); + } else { + return this.decodeError(session, buffer); + } + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelFlushAllCommand.java b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelFlushAllCommand.java new file mode 100644 index 0000000..1688a6e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelFlushAllCommand.java @@ -0,0 +1,56 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.kestrel; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.text.TextFlushAllCommand; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Kestrel flush command + * + * @author dennis + * + */ +public class KestrelFlushAllCommand extends TextFlushAllCommand { + + public KestrelFlushAllCommand(CountDownLatch latch, int delay, boolean noreply) { + super(latch, delay, noreply); + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + String line = ByteUtils.nextLine(buffer); + if (line == null) { + return false; + } else { + if (line.startsWith("Flushed")) { + setResult(Boolean.TRUE); + countDownLatch(); + return true; + } else { + return decodeError(session, buffer); + } + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelGetCommand.java b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelGetCommand.java new file mode 100644 index 0000000..adfeb03 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelGetCommand.java @@ -0,0 +1,72 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.kestrel; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.text.TextGetCommand; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.transcoders.TranscoderUtils; + +/** + * Kestrel get command + * + * @author dennis + * + */ +public class KestrelGetCommand extends TextGetCommand { + + public KestrelGetCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + Transcoder transcoder) { + super(key, keyBytes, cmdType, latch); + this.transcoder = transcoder; + } + + public static final TranscoderUtils transcoderUtils = new TranscoderUtils(false); + + @Override + public void dispatch() { + if (this.returnValues.size() == 0) { + if (!this.wasFirst) { + decodeError(); + } else { + this.countDownLatch(); + } + } else { + CachedData value = this.returnValues.values().iterator().next(); + // If disable save primitive type as string,the response data have + // 4-bytes flag aheader. + if (!this.transcoder.isPrimitiveAsString()) { + byte[] data = value.getData(); + if (data.length >= 4) { + byte[] flagBytes = new byte[4]; + System.arraycopy(data, 0, flagBytes, 0, 4); + byte[] realData = new byte[data.length - 4]; + System.arraycopy(data, 4, realData, 0, data.length - 4); + int flag = transcoderUtils.decodeInt(flagBytes); + value.setFlag(flag); + value.setData(realData); + value.setCapacity(realData.length); + } + } + setResult(value); + this.countDownLatch(); + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelSetCommand.java b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelSetCommand.java new file mode 100644 index 0000000..e59bd06 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/kestrel/KestrelSetCommand.java @@ -0,0 +1,59 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.kestrel; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.text.TextStoreCommand; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.Transcoder; + +/** + * kestrel set command + * + * @author dennis + * + */ +public class KestrelSetCommand extends TextStoreCommand { + + @SuppressWarnings("unchecked") + public KestrelSetCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int exp, long cas, Object value, boolean noreply, Transcoder transcoder) { + super(key, keyBytes, cmdType, latch, exp, cas, value, noreply, transcoder); + } + + @Override + @SuppressWarnings("unchecked") + protected CachedData encodeValue() { + + final CachedData value = this.transcoder.encode(this.value); + // If disable save primitive type as string,prepend 4 bytes flag to + // value + if (!this.transcoder.isPrimitiveAsString()) { + int flags = value.getFlag(); + byte[] flagBytes = KestrelGetCommand.transcoderUtils.encodeInt(flags); + byte[] origData = value.getData(); + byte[] newData = new byte[origData.length + 4]; + System.arraycopy(flagBytes, 0, newData, 0, 4); + System.arraycopy(origData, 0, newData, 4, origData.length); + value.setCapacity(newData.length); + value.setData(newData); + } + return value; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/kestrel/package.html b/src/main/java/net/rubyeye/xmemcached/command/kestrel/package.html new file mode 100644 index 0000000..05eb23e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/kestrel/package.html @@ -0,0 +1,10 @@ + + + + + Kestrel protocol implementations + + +

Kestrel protocol implementations

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/command/package.html b/src/main/java/net/rubyeye/xmemcached/command/package.html new file mode 100644 index 0000000..07440b7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/package.html @@ -0,0 +1,13 @@ + + + + + Memcached protocol implementations/title> +</head> + + + + +

Memcached protocol implementations

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextAutoDiscoveryCacheConfigCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextAutoDiscoveryCacheConfigCommand.java new file mode 100644 index 0000000..aedc24a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextAutoDiscoveryCacheConfigCommand.java @@ -0,0 +1,71 @@ +package net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import com.google.code.yanf4j.buffer.IoBuffer; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * AWS ElasticCache and GCP Auto Discovery config command. + * + * @see Adding + * AWS Auto Discovery To Your Client Library + * @see Adding + * GCP Auto Discovery To Your Client Library + * Only supports Cache Engine version 1.4.14 or higher. + * + * @author dennis + * + */ +public class TextAutoDiscoveryCacheConfigCommand extends Command { + + private String key; + + private String subCommand; + + public TextAutoDiscoveryCacheConfigCommand(final CountDownLatch latch, String subCommand, + String key) { + super(subCommand + key, CommandType.AUTO_DISCOVERY_CONFIG, latch); + this.key = key; + this.subCommand = subCommand; + this.result = new StringBuilder(); + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + String line = null; + while ((line = ByteUtils.nextLine(buffer)) != null) { + if (line.equals("END")) { // at the end + return done(session); + } else if (line.startsWith("CONFIG")) { + // ignore + } else { + ((StringBuilder) this.getResult()).append(line); + } + } + return false; + } + + private final boolean done(MemcachedSession session) { + setResult(this.getResult().toString()); + countDownLatch(); + return true; + } + + @Override + public void encode() { + // config [sub-command] [key] + final byte[] subCmdBytes = ByteUtils.getBytes(this.subCommand); + final byte[] keyBytes = ByteUtils.getBytes(this.key); + this.ioBuffer = IoBuffer.allocate(6 + 1 + subCmdBytes.length + 1 + keyBytes.length + 2); + ByteUtils.setArguments(this.ioBuffer, "config", subCmdBytes, keyBytes); + this.ioBuffer.flip(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextCASCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextCASCommand.java new file mode 100644 index 0000000..97e13ec --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextCASCommand.java @@ -0,0 +1,91 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * CAS command for text protocol + * + * @author dennis + * + */ +public class TextCASCommand extends TextStoreCommand { + + @SuppressWarnings("unchecked") + public TextCASCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int exp, long cas, Object value, boolean noreply, Transcoder transcoder) { + super(key, keyBytes, cmdType, latch, exp, cas, value, noreply, transcoder); + } + + private FailStatus failStatus; + + static enum FailStatus { + NOT_FOUND, EXISTS + } + + @Override + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + + if (buffer == null || !buffer.hasRemaining()) + return false; + if (result == null) { + if (buffer.remaining() < 2) + return false; + byte first = buffer.get(buffer.position()); + byte second = buffer.get(buffer.position() + 1); + if (first == 'S' && second == 'T') { + setResult(Boolean.TRUE); + countDownLatch(); + // STORED\r\n + return ByteUtils.stepBuffer(buffer, 8); + } else if (first == 'N') { + setResult(Boolean.FALSE); + countDownLatch(); + failStatus = FailStatus.NOT_FOUND; + // NOT_FOUND\r\n + return ByteUtils.stepBuffer(buffer, 11); + } else if (first == 'E' && second == 'X') { + setResult(Boolean.FALSE); + countDownLatch(); + failStatus = FailStatus.EXISTS; + // EXISTS\r\n + return ByteUtils.stepBuffer(buffer, 8); + } else + return decodeError(session, buffer); + } else { + Boolean result = (Boolean) this.result; + if (result) { + return ByteUtils.stepBuffer(buffer, 8); + } else { + switch (this.failStatus) { + case NOT_FOUND: + return ByteUtils.stepBuffer(buffer, 11); + case EXISTS: + return ByteUtils.stepBuffer(buffer, 8); + default: + return decodeError(session, buffer); + } + } + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextCacheDumpCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextCacheDumpCommand.java new file mode 100644 index 0000000..7766fc7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextCacheDumpCommand.java @@ -0,0 +1,60 @@ +package net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +public class TextCacheDumpCommand extends Command { + public static final String CACHE_DUMP_COMMAND = "stats cachedump %d 0\r\n"; + private int itemNumber; + + public final int getItemNumber() { + return this.itemNumber; + } + + public final void setItemNumber(int itemNumber) { + this.itemNumber = itemNumber; + } + + public TextCacheDumpCommand(CountDownLatch latch, int itemNumber) { + super("stats", (byte[]) null, latch); + this.commandType = CommandType.STATS; + this.result = new LinkedList(); + this.itemNumber = itemNumber; + } + + @Override + @SuppressWarnings("unchecked") + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + String line = null; + while ((line = ByteUtils.nextLine(buffer)) != null) { + if (line.equals("END")) { // at the end + return done(session); + } else if (line.startsWith("ITEM")) { + String[] items = line.split(" "); + ((List) getResult()).add(items[1]); + } else { + return decodeError(line); + } + } + return false; + } + + private final boolean done(MemcachedSession session) { + countDownLatch(); + return true; + } + + @Override + public final void encode() { + String result = String.format(CACHE_DUMP_COMMAND, this.itemNumber); + this.ioBuffer = IoBuffer.wrap(ByteBuffer.wrap(result.getBytes())); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextDeleteCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextDeleteCommand.java new file mode 100644 index 0000000..d356eb1 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextDeleteCommand.java @@ -0,0 +1,99 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Delete command for text protocol + * + * @author dennis + * + */ +public class TextDeleteCommand extends Command { + + protected int time; + + public TextDeleteCommand(String key, byte[] keyBytes, int time, final CountDownLatch latch, + boolean noreply) { + super(key, keyBytes, latch); + this.commandType = CommandType.DELETE; + this.time = time; + this.noreply = noreply; + } + + public final int getTime() { + return this.time; + } + + public final void setTime(int time) { + this.time = time; + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + if (this.result == null) { + byte first = buffer.get(buffer.position()); + if (first == 'D') { + setResult(Boolean.TRUE); + countDownLatch(); + // DELETED\r\n + return ByteUtils.stepBuffer(buffer, 9); + } else if (first == 'N') { + setResult(Boolean.FALSE); + countDownLatch(); + // NOT_FOUND\r\n + return ByteUtils.stepBuffer(buffer, 11); + } else { + return decodeError(session, buffer); + } + } else { + Boolean result = (Boolean) this.result; + if (result) { + return ByteUtils.stepBuffer(buffer, 9); + } else { + return ByteUtils.stepBuffer(buffer, 11); + } + } + } + + @Override + public final void encode() { + int size = Constants.DELETE.length + 1 + this.keyBytes.length + Constants.CRLF.length; + if (isNoreply()) { + size += 8; + } + byte[] buf = new byte[size]; + if (isNoreply()) { + ByteUtils.setArguments(buf, 0, Constants.DELETE, this.keyBytes, Constants.NO_REPLY); + } else { + ByteUtils.setArguments(buf, 0, Constants.DELETE, this.keyBytes); + } + this.ioBuffer = IoBuffer.wrap(buf); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextFlushAllCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextFlushAllCommand.java new file mode 100644 index 0000000..1f8c684 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextFlushAllCommand.java @@ -0,0 +1,96 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * FlushAll command for text protocol + * + * @author dennis + * + */ +public class TextFlushAllCommand extends Command { + + public static final ByteBuffer FLUSH_ALL = ByteBuffer.wrap("flush_all\r\n".getBytes()); + + protected int exptime; + + public final int getExptime() { + return this.exptime; + } + + public TextFlushAllCommand(final CountDownLatch latch, int delay, boolean noreply) { + super("[flush_all]", (byte[]) null, latch); + this.commandType = CommandType.FLUSH_ALL; + this.exptime = delay; + this.noreply = noreply; + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + if (this.result == null) { + byte first = buffer.get(buffer.position()); + if (first == 'O') { + setResult(Boolean.TRUE); + countDownLatch(); + // OK\r\n + return ByteUtils.stepBuffer(buffer, 4); + } else { + return decodeError(session, buffer); + } + } else { + return ByteUtils.stepBuffer(buffer, 4); + } + } + + @Override + public final void encode() { + if (isNoreply()) { + if (this.exptime <= 0) { + this.ioBuffer = IoBuffer.allocate("flush_all".length() + 1 + Constants.NO_REPLY.length + 2); + ByteUtils.setArguments(this.ioBuffer, "flush_all", Constants.NO_REPLY); + } else { + byte[] delayBytes = ByteUtils.getBytes(String.valueOf(this.exptime)); + this.ioBuffer = IoBuffer + .allocate("flush_all".length() + 2 + delayBytes.length + Constants.NO_REPLY.length + 2); + ByteUtils.setArguments(this.ioBuffer, "flush_all", delayBytes, Constants.NO_REPLY); + } + this.ioBuffer.flip(); + } else { + if (this.exptime <= 0) { + this.ioBuffer = IoBuffer.wrap(FLUSH_ALL.slice()); + } else { + + byte[] delayBytes = ByteUtils.getBytes(String.valueOf(this.exptime)); + this.ioBuffer = IoBuffer.allocate("flush_all".length() + 1 + delayBytes.length + 2); + ByteUtils.setArguments(this.ioBuffer, "flush_all", delayBytes); + } + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextGetCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextGetCommand.java new file mode 100644 index 0000000..d7e76b0 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextGetCommand.java @@ -0,0 +1,309 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import com.google.code.yanf4j.buffer.IoBuffer; +import net.rubyeye.xmemcached.command.AssocCommandAware; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.MapReturnValueAware; +import net.rubyeye.xmemcached.command.MergeCommandsAware; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; + +/** + * Abstract get command for text protocol + * + * @author dennis + * + */ +public abstract class TextGetCommand extends Command + implements MergeCommandsAware, AssocCommandAware, MapReturnValueAware { + private static final char[] A_SPACE = new char[] {' '}; + private static final char[] EMPTY_CHARS = new char[] {}; + protected Map returnValues; + private String currentReturnKey; + private int offset; + /** + * When MemcachedClient merge get commands,those commans which have the same key will be merged + * into one get command.The result command's assocCommands contains all these commands with the + * same key. + */ + protected List assocCommands; + + protected Map mergeCommands; + + public final Map getMergeCommands() { + return this.mergeCommands; + } + + public final void setMergeCommands(final Map mergeCommands) { + this.mergeCommands = mergeCommands; + } + + public final List getAssocCommands() { + return this.assocCommands; + } + + public final void setAssocCommands(final List assocCommands) { + this.assocCommands = assocCommands; + } + + public TextGetCommand(final String key, final byte[] keyBytes, final CommandType cmdType, + final CountDownLatch latch) { + super(key, keyBytes, cmdType, latch); + this.returnValues = new HashMap(32); + } + + private ParseStatus parseStatus = ParseStatus.NULL; + + public static enum ParseStatus { + NULL, VALUE, KEY, FLAG, DATA_LEN, DATA_LEN_DONE, CAS, CAS_DONE, DATA + } + + protected boolean wasFirst = true; + + public ParseStatus getParseStatus() { + return this.parseStatus; + } + + public void setParseStatus(final ParseStatus parseStatus) { + this.parseStatus = parseStatus; + } + + @Override + public final boolean decode(final MemcachedTCPSession session, final ByteBuffer buffer) { + while (true) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + switch (this.parseStatus) { + case NULL: + if (buffer.remaining() < 2) { + return false; + } + int pos = buffer.position(); + byte first = buffer.get(pos); + byte second = buffer.get(pos + 1); + if (first == 'E' && second == 'N') { + // dispatch result + dispatch(); + this.currentReturnKey = null; + // END\r\n + return ByteUtils.stepBuffer(buffer, 5); + } else if (first == 'V') { + this.parseStatus = ParseStatus.VALUE; + this.wasFirst = false; + continue; + } else { + return decodeError(session, buffer); + } + case VALUE: + // VALUE[SPACE] + if (ByteUtils.stepBuffer(buffer, 6)) { + this.parseStatus = ParseStatus.KEY; + continue; + } else { + return false; + } + case KEY: + String item = getItem(buffer, ' ', EMPTY_CHARS); + if (item == null) { + return false; + } else { + this.currentReturnKey = item; + this.returnValues.put(this.currentReturnKey, new CachedData()); + this.parseStatus = ParseStatus.FLAG; + continue; + } + case FLAG: + item = getItem(buffer, ' ', EMPTY_CHARS); + if (item == null) { + return false; + } else { + final CachedData cachedData = this.returnValues.get(this.currentReturnKey); + cachedData.setFlag(Integer.parseInt(item)); + this.parseStatus = ParseStatus.DATA_LEN; + continue; + } + case DATA_LEN: + item = getItem(buffer, '\r', A_SPACE); + if (item == null) { + return false; + } else { + final CachedData cachedData = this.returnValues.get(this.currentReturnKey); + cachedData.setCapacity(Integer.parseInt(item)); + assert (cachedData.getCapacity() >= 0); + cachedData.setData(new byte[cachedData.getCapacity()]); + this.parseStatus = ParseStatus.DATA_LEN_DONE; + continue; + } + case DATA_LEN_DONE: + if (buffer.remaining() < 1) { + return false; + } else { + pos = buffer.position(); + first = buffer.get(pos); + // check if buffer has cas value + if (first == '\n') { + // skip '\n' + buffer.position(pos + 1); + this.parseStatus = ParseStatus.DATA; + continue; + } else { + this.parseStatus = ParseStatus.CAS; + continue; + } + } + case CAS: + // has cas value + item = getItem(buffer, '\r', EMPTY_CHARS); + if (item == null) { + return false; + } else { + final CachedData cachedData = this.returnValues.get(this.currentReturnKey); + cachedData.setCas(Long.parseLong(item)); + this.parseStatus = ParseStatus.CAS_DONE; + continue; + } + case CAS_DONE: + if (buffer.remaining() < 1) { + return false; + } else { + this.parseStatus = ParseStatus.DATA; + // skip '\n' + buffer.position(buffer.position() + 1); + continue; + } + case DATA: + final CachedData value = this.returnValues.get(this.currentReturnKey); + int remaining = buffer.remaining(); + int remainingCapacity = value.remainingCapacity(); + assert (remainingCapacity >= 0); + // Data is not enough,return false + if (remaining < remainingCapacity + 2) { + int length = remaining > remainingCapacity ? remainingCapacity : remaining; + value.fillData(buffer, length); + return false; + } else if (remainingCapacity > 0) { + value.fillData(buffer, remainingCapacity); + } + assert (value.remainingCapacity() == 0); + buffer.position(buffer.position() + ByteUtils.SPLIT.remaining()); + + Map mergetCommands = getMergeCommands(); + if (mergetCommands != null) { + final TextGetCommand command = + (TextGetCommand) mergetCommands.remove(this.currentReturnKey); + if (command != null) { + command.setResult(value); + command.countDownLatch(); + this.mergeCount--; + if (command.getAssocCommands() != null) { + for (Command assocCommand : command.getAssocCommands()) { + assocCommand.setResult(value); + assocCommand.countDownLatch(); + this.mergeCount--; + } + } + + } + } + this.currentReturnKey = null; + this.parseStatus = ParseStatus.NULL; + continue; + default: + return decodeError(session, buffer); + } + + } + } + + private String getItem(final ByteBuffer buffer, final char token, final char[] others) { + int pos = buffer.position() + this.offset; + final int limit = buffer.limit(); + for (; pos < limit; pos++) { + final byte b = buffer.get(pos); + if (b == token || isIn(b, others)) { + byte[] keyBytes = new byte[pos - buffer.position()]; + buffer.get(keyBytes); + this.offset = 0; + assert (pos == buffer.position()); + // skip token + buffer.position(pos + 1); + return getString(keyBytes); + } + } + this.offset = pos - buffer.position(); + return null; + } + + private boolean isIn(final byte b, final char[] others) { + if (others == EMPTY_CHARS) { + return false; + } + for (int i = 0; i < others.length; i++) { + if (b == others[i]) { + return true; + } + } + return false; + } + + private String getString(final byte[] keyBytes) { + try { + return new String(keyBytes, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /* + * (non-Javadoc) + * + * @see net.rubyeye.xmemcached.command.text.MapReturnValueAware#getReturnValues() + */ + public final Map getReturnValues() { + return this.returnValues; + } + + public final void setReturnValues(final Map returnValues) { + this.returnValues = returnValues; + } + + public abstract void dispatch(); + + @Override + public void encode() { + byte[] cmdBytes = + this.commandType == CommandType.GET_ONE || this.commandType == CommandType.GET_MANY + ? Constants.GET : Constants.GETS; + this.ioBuffer = + IoBuffer.allocate(cmdBytes.length + Constants.CRLF.length + 1 + this.keyBytes.length); + ByteUtils.setArguments(this.ioBuffer, cmdBytes, this.keyBytes); + this.ioBuffer.flip(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextGetMultiCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextGetMultiCommand.java new file mode 100644 index 0000000..d6c2bfe --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextGetMultiCommand.java @@ -0,0 +1,43 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.Transcoder; + +/** + * Bulk-get command for text protocol + * + * @author dennis + * + */ +public class TextGetMultiCommand extends TextGetCommand { + + @SuppressWarnings("unchecked") + public TextGetMultiCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + Transcoder transcoder) { + super(key, keyBytes, cmdType, latch); + this.transcoder = transcoder; + } + + @Override + public final void dispatch() { + setResult(this.returnValues); + countDownLatch(); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextGetOneCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextGetOneCommand.java new file mode 100644 index 0000000..f3d7be4 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextGetOneCommand.java @@ -0,0 +1,67 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; + +/** + * Get command for text protocol + * + * @author dennis + * + */ +public class TextGetOneCommand extends TextGetCommand { + + public TextGetOneCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch) { + super(key, keyBytes, cmdType, latch); + } + + @Override + public void dispatch() { + if (this.mergeCount < 0) { + // single get + if (this.returnValues.get(this.getKey()) == null) { + if (!this.wasFirst) { + decodeError(); + } else { + this.countDownLatch(); + } + } else { + CachedData data = this.returnValues.get(this.getKey()); + setResult(data); + this.countDownLatch(); + } + } else { + // merge get + // Collection mergeCommands = mergeCommands.values(); + getIoBuffer().free(); + for (Command nextCommand : mergeCommands.values()) { + TextGetCommand textGetCommand = (TextGetCommand) nextCommand; + textGetCommand.countDownLatch(); + if (textGetCommand.assocCommands != null) { + for (Command assocCommand : textGetCommand.assocCommands) { + assocCommand.countDownLatch(); + } + } + } + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextIncrDecrCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextIncrDecrCommand.java new file mode 100644 index 0000000..2562877 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextIncrDecrCommand.java @@ -0,0 +1,96 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Incr/Decr command for text protocol + * + * @author dennis + * + */ +public class TextIncrDecrCommand extends Command { + + private long delta; + private final long initial; + + public TextIncrDecrCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + long delta, long initial, boolean noreply) { + super(key, keyBytes, cmdType, latch); + this.delta = delta; + this.noreply = noreply; + this.initial = initial; + } + + public final long getDelta() { + return this.delta; + } + + public final void setDelta(int delta) { + this.delta = delta; + } + + @Override + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + String line = ByteUtils.nextLine(buffer); + if (line != null) { + if (line.equals("NOT_FOUND")) { + // setException(new MemcachedException( + // "The key's value is not found for increase or decrease")); + setResult("NOT_FOUND"); + countDownLatch(); + return true; + } else { + String rt = line.trim(); + if (Character.isDigit(rt.charAt(0))) { + setResult(Long.valueOf(rt)); + countDownLatch(); + return true; + } else { + return decodeError(line); + } + } + } + return false; + } + + @Override + public final void encode() { + byte[] cmdBytes = this.commandType == CommandType.INCR ? Constants.INCR : Constants.DECR; + int size = + 6 + this.keyBytes.length + ByteUtils.stringSize(this.getDelta()) + Constants.CRLF.length; + if (isNoreply()) { + size += 8; + } + byte[] buf = new byte[size]; + if (isNoreply()) { + ByteUtils.setArguments(buf, 0, cmdBytes, this.keyBytes, this.getDelta(), Constants.NO_REPLY); + } else { + ByteUtils.setArguments(buf, 0, cmdBytes, this.keyBytes, this.getDelta()); + } + this.ioBuffer = IoBuffer.wrap(buf); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextQuitCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextQuitCommand.java new file mode 100644 index 0000000..d2fb7e9 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextQuitCommand.java @@ -0,0 +1,50 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Quit command for text protocol + * + * @author dennis + * + */ +public class TextQuitCommand extends Command { + public TextQuitCommand() { + super("quit", (byte[]) null, null); + commandType = CommandType.QUIT; + } + + static final IoBuffer QUIT = IoBuffer.wrap("quit\r\n".getBytes()); + + @Override + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + // do nothing + return true; + } + + @Override + public final void encode() { + ioBuffer = QUIT.slice(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextStatsCachedumpCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextStatsCachedumpCommand.java new file mode 100644 index 0000000..4562f3b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextStatsCachedumpCommand.java @@ -0,0 +1,42 @@ +package net.rubyeye.xmemcached.command.text; + +import java.util.HashMap; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.utils.ByteUtils; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TextStatsCachedumpCommand extends TextStatsCommand { + + static Pattern itemPattern = + Pattern.compile("ITEM (?[^ ]+) \\[(?\\d+) b; (?\\d+) s\\]"); + + public TextStatsCachedumpCommand(InetSocketAddress server, CountDownLatch latch, int slabId, + int limit) { + super(server, latch, String.format("cachedump %s %s", slabId, limit)); + this.result = new HashMap(); + } + + @Override + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + String line = null; + while ((line = ByteUtils.nextLine(buffer)) != null) { + if (line.equals("END")) { // at the end + return super.done(session); + } else if (line.startsWith("ITEM")) { + Matcher m = itemPattern.matcher(line); + if (m.find()) { + ((Map) getResult()).put(m.group("key"), new Integer[] { + Integer.parseInt(m.group("size")), Integer.parseInt(m.group("expire"))}); + } + } else { + return decodeError(line); + } + } + return false; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextStatsCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextStatsCommand.java new file mode 100644 index 0000000..7dbd02e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextStatsCommand.java @@ -0,0 +1,102 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Stats command for text protocol + * + * @author dennis + * + */ +public class TextStatsCommand extends Command implements ServerAddressAware { + public static final ByteBuffer STATS = ByteBuffer.wrap("stats\r\n".getBytes()); + private InetSocketAddress server; + private String itemName; + + public String getItemName() { + return this.itemName; + } + + public final InetSocketAddress getServer() { + return this.server; + } + + public final void setServer(InetSocketAddress server) { + this.server = server; + } + + public void setItemName(String item) { + this.itemName = item; + } + + public TextStatsCommand(InetSocketAddress server, CountDownLatch latch, String itemName) { + super("stats", (byte[]) null, latch); + this.commandType = CommandType.STATS; + this.server = server; + this.itemName = itemName; + this.result = new HashMap(); + + } + + @Override + @SuppressWarnings("unchecked") + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + String line = null; + while ((line = ByteUtils.nextLine(buffer)) != null) { + if (line.equals("END")) { // at the end + return done(session); + } else if (line.startsWith("STAT") || line.startsWith("PREFIX") || line.startsWith("ITEM")) { + // Fixed issue 126 + String[] items = line.split(" "); + ((Map) getResult()).put(items[1], items[2]); + } else { + return decodeError(line); + } + } + return false; + } + + protected final boolean done(MemcachedSession session) { + countDownLatch(); + return true; + } + + @Override + public final void encode() { + if (this.itemName == null) { + this.ioBuffer = IoBuffer.wrap(STATS.slice()); + } else { + this.ioBuffer = IoBuffer.allocate(5 + this.itemName.length() + 3); + ByteUtils.setArguments(this.ioBuffer, "stats", this.itemName); + this.ioBuffer.flip(); + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextStoreCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextStoreCommand.java new file mode 100644 index 0000000..3f0a06a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextStoreCommand.java @@ -0,0 +1,189 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.StoreCommand; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Store command for text protocol + * + * @author dennis + * + */ +public class TextStoreCommand extends Command implements StoreCommand { + protected int expTime; + protected long cas; + protected Object value; + + @SuppressWarnings("unchecked") + public TextStoreCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int exp, long cas, Object value, boolean noreply, Transcoder transcoder) { + super(key, keyBytes, cmdType, latch); + this.expTime = exp; + this.cas = cas; + this.value = value; + this.noreply = noreply; + this.transcoder = transcoder; + } + + public final int getExpTime() { + return this.expTime; + } + + public final void setExpTime(int exp) { + this.expTime = exp; + } + + public final long getCas() { + return this.cas; + } + + public final void setCas(long cas) { + this.cas = cas; + } + + public final Object getValue() { + return this.value; + } + + public final void setValue(Object value) { + this.value = value; + } + + @Override + @SuppressWarnings("unchecked") + public final Transcoder getTranscoder() { + return this.transcoder; + } + + @Override + @SuppressWarnings("unchecked") + public final void setTranscoder(Transcoder transcoder) { + this.transcoder = transcoder; + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + if (this.result == null) { + if (buffer.remaining() < 2) + return false; + int pos = buffer.position(); + byte first = buffer.get(pos); + byte second = buffer.get(pos + 1); + if (first == 'S' && second == 'T') { + setResult(Boolean.TRUE); + countDownLatch(); + // STORED\r\n + return ByteUtils.stepBuffer(buffer, 8); + } else if (first == 'N') { + setResult(Boolean.FALSE); + countDownLatch(); + // NOT_STORED\r\n + return ByteUtils.stepBuffer(buffer, 12); + } else { + return decodeError(session, buffer); + } + } else { + Boolean result = (Boolean) this.result; + if (result) { + return ByteUtils.stepBuffer(buffer, 8); + } else { + return ByteUtils.stepBuffer(buffer, 12); + } + } + } + + private String getCommandName() { + switch (this.commandType) { + case ADD: + return "add"; + case SET: + return "set"; + case REPLACE: + return "replace"; + case APPEND: + return "append"; + case PREPEND: + return "prepend"; + case CAS: + return "cas"; + default: + throw new IllegalArgumentException(this.commandType.name() + " is not a store command"); + + } + } + + @Override + public final void encode() { + final CachedData data = encodeValue(); + String cmdStr = getCommandName(); + byte[] encodedData = data.getData(); + int flag = data.getFlag(); + int size = cmdStr.length() + this.keyBytes.length + ByteUtils.stringSize(flag) + + ByteUtils.stringSize(this.expTime) + encodedData.length + + ByteUtils.stringSize(encodedData.length) + 8; + if (this.commandType == CommandType.CAS) { + size += 1 + ByteUtils.stringSize(this.cas); + } + byte[] buf; + if (isNoreply()) { + buf = new byte[size + 8]; + } else { + buf = new byte[size]; + } + int offset = 0; + if (this.commandType == CommandType.CAS) { + if (isNoreply()) { + offset = ByteUtils.setArguments(buf, offset, cmdStr, this.keyBytes, flag, this.expTime, + encodedData.length, this.cas, Constants.NO_REPLY); + } else { + offset = ByteUtils.setArguments(buf, offset, cmdStr, this.keyBytes, flag, this.expTime, + encodedData.length, this.cas); + } + } else { + if (isNoreply()) { + offset = ByteUtils.setArguments(buf, offset, cmdStr, this.keyBytes, flag, this.expTime, + encodedData.length, Constants.NO_REPLY); + } else { + offset = ByteUtils.setArguments(buf, offset, cmdStr, this.keyBytes, flag, this.expTime, + encodedData.length); + } + } + ByteUtils.setArguments(buf, offset, encodedData); + this.ioBuffer = IoBuffer.wrap(buf); + } + + @SuppressWarnings("unchecked") + protected CachedData encodeValue() { + final CachedData data = this.transcoder.encode(this.value); + return data; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextTouchCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextTouchCommand.java new file mode 100644 index 0000000..8799db1 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextTouchCommand.java @@ -0,0 +1,105 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Touch command for touch protocol. + * + * @author dennis + * @since 1.3.8 + * + */ +public class TextTouchCommand extends Command { + + private static final String NOT_FOUND = "NOT_FOUND\r\n"; + private static final String TOUCHED = "TOUCHED\r\n"; + private int expTime; + + public TextTouchCommand(String key, byte[] keyBytes, CommandType cmdType, CountDownLatch latch, + int expTime, boolean noreply) { + super(key, keyBytes, cmdType, latch); + this.expTime = expTime; + this.noreply = noreply; + } + + public int getExpTime() { + return expTime; + } + + public void setExpTime(int expTime) { + this.expTime = expTime; + } + + @Override + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + if (this.result == null) { + if (buffer.remaining() < 1) + return false; + byte first = buffer.get(buffer.position()); + if (first == 'T') { + setResult(Boolean.TRUE); + countDownLatch(); + // TOUCHED\r\n + return ByteUtils.stepBuffer(buffer, TOUCHED.length()); + } else if (first == 'N') { + setResult(Boolean.FALSE); + countDownLatch(); + // NOT_FOUND\r\n + return ByteUtils.stepBuffer(buffer, NOT_FOUND.length()); + } else { + return decodeError(session, buffer); + } + } else { + Boolean result = (Boolean) this.result; + if (result) { + return ByteUtils.stepBuffer(buffer, TOUCHED.length()); + } else { + return ByteUtils.stepBuffer(buffer, NOT_FOUND.length()); + } + } + } + + @Override + public final void encode() { + byte[] cmdBytes = Constants.TOUCH; + int size = + 7 + this.keyBytes.length + ByteUtils.stringSize(this.expTime) + Constants.CRLF.length; + if (isNoreply()) { + size += 8; + } + byte[] buf = new byte[size]; + if (isNoreply()) { + ByteUtils.setArguments(buf, 0, cmdBytes, this.keyBytes, this.expTime, Constants.NO_REPLY); + } else { + ByteUtils.setArguments(buf, 0, cmdBytes, this.keyBytes, this.expTime); + } + this.ioBuffer = IoBuffer.wrap(buf); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextVerbosityCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextVerbosityCommand.java new file mode 100644 index 0000000..d49ad9c --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextVerbosityCommand.java @@ -0,0 +1,76 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.VerbosityCommand; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Verbosity command for text protocol + * + * @author dennis + * + */ +public class TextVerbosityCommand extends VerbosityCommand { + + public static final String VERBOSITY = "verbosity"; + + public TextVerbosityCommand(CountDownLatch latch, int level, boolean noreply) { + super(latch, level, noreply); + + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + if (buffer == null || !buffer.hasRemaining()) { + return false; + } + if (this.result == null) { + byte first = buffer.get(buffer.position()); + if (first == 'O') { + setResult(Boolean.TRUE); + countDownLatch(); + // OK\r\n + return ByteUtils.stepBuffer(buffer, 4); + } else { + return decodeError(session, buffer); + } + } else { + return ByteUtils.stepBuffer(buffer, 4); + } + } + + @Override + public void encode() { + final byte[] levelBytes = ByteUtils.getBytes(String.valueOf(this.level)); + if (isNoreply()) { + this.ioBuffer = + IoBuffer.allocate(4 + VERBOSITY.length() + levelBytes.length + Constants.NO_REPLY.length); + ByteUtils.setArguments(this.ioBuffer, VERBOSITY, levelBytes, Constants.NO_REPLY); + } else { + this.ioBuffer = IoBuffer.allocate(2 + 1 + VERBOSITY.length() + levelBytes.length); + ByteUtils.setArguments(this.ioBuffer, VERBOSITY, levelBytes); + + } + this.ioBuffer.flip(); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/TextVersionCommand.java b/src/main/java/net/rubyeye/xmemcached/command/text/TextVersionCommand.java new file mode 100644 index 0000000..6879b7f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/TextVersionCommand.java @@ -0,0 +1,73 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.command.text; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.utils.ByteUtils; +import com.google.code.yanf4j.buffer.IoBuffer; + +/** + * Version command for text protocol + * + * @author dennis + * + */ +public class TextVersionCommand extends Command implements ServerAddressAware { + public InetSocketAddress server; + + public final InetSocketAddress getServer() { + return this.server; + } + + public final void setServer(InetSocketAddress server) { + this.server = server; + } + + public TextVersionCommand(final CountDownLatch latch, InetSocketAddress server) { + super("[version]", (byte[]) null, latch); + this.commandType = CommandType.VERSION; + this.server = server; + } + + @Override + public final boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + String line = ByteUtils.nextLine(buffer); + if (line != null) { + if (line.startsWith("VERSION")) { + String[] items = line.split(" "); + setResult(items.length > 1 ? items[1] : "unknown version"); + countDownLatch(); + return true; + } else { + return decodeError(line); + } + } + return false; + } + + @Override + public final void encode() { + this.ioBuffer = IoBuffer.wrap(VERSION.slice()); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/command/text/package.html b/src/main/java/net/rubyeye/xmemcached/command/text/package.html new file mode 100644 index 0000000..f399686 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/command/text/package.html @@ -0,0 +1,10 @@ + + + + + Memcached text protocol implementations + + +

Memcached text protocol implementations

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/exception/MemcachedClientException.java b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedClientException.java new file mode 100644 index 0000000..a42d44d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedClientException.java @@ -0,0 +1,36 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.exception; + +/** + * Memcached Client Exception + * + * @author dennis + * + */ +public class MemcachedClientException extends MemcachedException { + + public MemcachedClientException() { + super(); + } + + public MemcachedClientException(String s) { + super(s); + } + + public MemcachedClientException(String message, Throwable cause) { + super(message, cause); + } + + public MemcachedClientException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -236562546568164115L; +} diff --git a/src/main/java/net/rubyeye/xmemcached/exception/MemcachedDecodeException.java b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedDecodeException.java new file mode 100644 index 0000000..caaecac --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedDecodeException.java @@ -0,0 +1,29 @@ +package net.rubyeye.xmemcached.exception; + +/** + * Memcached decode exception + * + * @author dennis + * + */ +public class MemcachedDecodeException extends RuntimeException { + + public MemcachedDecodeException() { + super(); + } + + public MemcachedDecodeException(String s) { + super(s); + } + + public MemcachedDecodeException(String message, Throwable cause) { + super(message, cause); + } + + public MemcachedDecodeException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = 939539859359568164L; + +} diff --git a/src/main/java/net/rubyeye/xmemcached/exception/MemcachedException.java b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedException.java new file mode 100644 index 0000000..20d91ad --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedException.java @@ -0,0 +1,36 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.exception; + +/** + * Base exception type for memcached client + * + * @author boyan + * + */ +public class MemcachedException extends Exception { + + public MemcachedException() { + super(); + } + + public MemcachedException(String s) { + super(s); + } + + public MemcachedException(String message, Throwable cause) { + super(message, cause); + } + + public MemcachedException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -136568012546568164L; +} diff --git a/src/main/java/net/rubyeye/xmemcached/exception/MemcachedServerException.java b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedServerException.java new file mode 100644 index 0000000..baabe12 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/MemcachedServerException.java @@ -0,0 +1,36 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.exception; + +/** + * Memcached server exception + * + * @author dennis + * + */ +public class MemcachedServerException extends MemcachedException { + + public MemcachedServerException() { + super(); + } + + public MemcachedServerException(String s) { + super(s); + } + + public MemcachedServerException(String message, Throwable cause) { + super(message, cause); + } + + public MemcachedServerException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -236562546568164115L; +} diff --git a/src/main/java/net/rubyeye/xmemcached/exception/NoValueException.java b/src/main/java/net/rubyeye/xmemcached/exception/NoValueException.java new file mode 100644 index 0000000..fb8a6cb --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/NoValueException.java @@ -0,0 +1,36 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.exception; + +/** + * Memcached Client Exception + * + * @author bmahe + * + */ +public class NoValueException extends MemcachedClientException { + + public NoValueException() { + super(); + } + + public NoValueException(String s) { + super(s); + } + + public NoValueException(String message, Throwable cause) { + super(message, cause); + } + + public NoValueException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = -8717259791309127913L; +} diff --git a/src/main/java/net/rubyeye/xmemcached/exception/UnknownCommandException.java b/src/main/java/net/rubyeye/xmemcached/exception/UnknownCommandException.java new file mode 100644 index 0000000..85ac2e2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/UnknownCommandException.java @@ -0,0 +1,36 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.exception; + +/** + * Unknown command exception + * + * @author boyan + * + */ +public class UnknownCommandException extends RuntimeException { + + public UnknownCommandException() { + super(); + } + + public UnknownCommandException(String s) { + super(s); + } + + public UnknownCommandException(String message, Throwable cause) { + super(message, cause); + } + + public UnknownCommandException(Throwable cause) { + super(cause); + } + + private static final long serialVersionUID = 8736625460917630395L; +} diff --git a/src/main/java/net/rubyeye/xmemcached/exception/package.html b/src/main/java/net/rubyeye/xmemcached/exception/package.html new file mode 100644 index 0000000..b891b37 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/exception/package.html @@ -0,0 +1,10 @@ + + + + + XMemcached Exceptions + + +

XMemcached Exceptions

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/impl/AbstractMemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/AbstractMemcachedSessionLocator.java new file mode 100644 index 0000000..5d601c9 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/AbstractMemcachedSessionLocator.java @@ -0,0 +1,20 @@ +package net.rubyeye.xmemcached.impl; + +import net.rubyeye.xmemcached.MemcachedSessionLocator; + +/** + * Abstract session locator + * + * @author dennis + * @date 2010-12-25 + */ +public abstract class AbstractMemcachedSessionLocator implements MemcachedSessionLocator { + + protected boolean failureMode; + + public void setFailureMode(boolean failureMode) { + this.failureMode = failureMode; + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/AddressMemcachedSessionComparator.java b/src/main/java/net/rubyeye/xmemcached/impl/AddressMemcachedSessionComparator.java new file mode 100644 index 0000000..486cd19 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/AddressMemcachedSessionComparator.java @@ -0,0 +1,31 @@ +package net.rubyeye.xmemcached.impl; + +import java.io.Serializable; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import com.google.code.yanf4j.core.Session; + +/** + * Connection comparator,compare with Address + * + * @author Jungsub Shin + * + */ +public class AddressMemcachedSessionComparator implements MemcachedSessionComparator, Serializable { + static final long serialVersionUID = -1L; + + public int compare(Session o1, Session o2) { + MemcachedSession session1 = (MemcachedSession) o1; + MemcachedSession session2 = (MemcachedSession) o2; + if (session1 == null || session1.getInetSocketAddressWrapper() == null + || session1.getInetSocketAddressWrapper().getInetSocketAddress() == null) { + return -1; + } + if (session2 == null || session2.getInetSocketAddressWrapper() == null + || session2.getInetSocketAddressWrapper().getInetSocketAddress() == null) { + return 1; + } + return session1.getInetSocketAddressWrapper().getInetSocketAddress().toString() + .compareTo(session2.getInetSocketAddressWrapper().getInetSocketAddress().toString()); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/ArrayMemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/ArrayMemcachedSessionLocator.java new file mode 100644 index 0000000..1cada1e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/ArrayMemcachedSessionLocator.java @@ -0,0 +1,138 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import com.google.code.yanf4j.core.Session; + +/** + * Session locator base on hash(key) mod sessions.size().Standard hash strategy + * + * @author dennis + * + */ +public class ArrayMemcachedSessionLocator extends AbstractMemcachedSessionLocator { + + private HashAlgorithm hashAlgorighm; + private transient volatile List> sessions; + + public ArrayMemcachedSessionLocator() { + this.hashAlgorighm = HashAlgorithm.NATIVE_HASH; + } + + public ArrayMemcachedSessionLocator(HashAlgorithm hashAlgorighm) { + this.hashAlgorighm = hashAlgorighm; + } + + public final void setHashAlgorighm(HashAlgorithm hashAlgorighm) { + this.hashAlgorighm = hashAlgorighm; + } + + public final long getHash(int size, String key) { + long hash = this.hashAlgorighm.hash(key); + return hash % size; + } + + final Random rand = new Random(); + + public final Session getSessionByKey(final String key) { + if (this.sessions == null || this.sessions.size() == 0) { + return null; + } + // Copy on read + List> sessionList = this.sessions; + int size = sessionList.size(); + if (size == 0) { + return null; + } + long start = this.getHash(size, key); + List sessions = sessionList.get((int) start); + Session session = getRandomSession(sessions); + + // If it is not failure mode,get next available session + if (!this.failureMode && (session == null || session.isClosed())) { + long next = this.getNext(size, start); + while ((session == null || session.isClosed()) && next != start) { + sessions = sessionList.get((int) next); + next = this.getNext(size, next); + session = getRandomSession(sessions); + } + } + return session; + } + + private Session getRandomSession(List sessions) { + if (sessions == null || sessions.isEmpty()) + return null; + return sessions.get(rand.nextInt(sessions.size())); + } + + public final long getNext(int size, long start) { + if (start == size - 1) { + return 0; + } else { + return start + 1; + } + } + + public final void updateSessions(final Collection list) { + if (list == null || list.isEmpty()) { + this.sessions = Collections.emptyList(); + return; + } + Collection copySessions = list; + List> tmpList = new ArrayList>(); + Session target = null; + List subList = null; + for (Session session : copySessions) { + if (target == null) { + target = session; + subList = new ArrayList(); + subList.add(target); + } else { + if (session.getRemoteSocketAddress().equals(target.getRemoteSocketAddress())) { + subList.add(session); + } else { + tmpList.add(subList); + target = session; + subList = new ArrayList(); + subList.add(target); + } + } + } + + // The last one + if (subList != null) { + tmpList.add(subList); + } + + List> newSessions = new ArrayList>(tmpList.size() * 2); + for (List sessions : tmpList) { + if (sessions != null && !sessions.isEmpty()) { + Session session = sessions.get(0); + if (session instanceof MemcachedTCPSession) { + int weight = ((MemcachedSession) session).getWeight(); + for (int i = 0; i < weight; i++) { + newSessions.add(sessions); + } + } else { + newSessions.add(sessions); + } + } + + } + this.sessions = newSessions; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/ClosedMemcachedTCPSession.java b/src/main/java/net/rubyeye/xmemcached/impl/ClosedMemcachedTCPSession.java new file mode 100644 index 0000000..c0c6ba7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/ClosedMemcachedTCPSession.java @@ -0,0 +1,223 @@ +package net.rubyeye.xmemcached.impl; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteOrder; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.networking.ClosedMemcachedSession; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.CodecFactory.Decoder; +import com.google.code.yanf4j.core.CodecFactory.Encoder; + +/** + * Closed session + * + * @author dennis + * + */ +public class ClosedMemcachedTCPSession implements ClosedMemcachedSession, MemcachedSession { + private InetSocketAddressWrapper inetSocketAddressWrapper; + private volatile boolean allowReconnect = true; + private volatile boolean authFailed = false; + + public ClosedMemcachedTCPSession(InetSocketAddressWrapper inetSocketAddressWrapper) { + super(); + this.inetSocketAddressWrapper = inetSocketAddressWrapper; + } + + public void setBufferAllocator(BufferAllocator allocator) { + + } + + public void destroy() { + + } + + public void quit() { + + } + + public boolean isAuthFailed() { + return authFailed; + } + + public void setAuthFailed(boolean authFailed) { + this.authFailed = authFailed; + + } + + public InetSocketAddressWrapper getInetSocketAddressWrapper() { + return this.inetSocketAddressWrapper; + } + + public int getOrder() { + return this.inetSocketAddressWrapper.getOrder(); + } + + public int getWeight() { + return this.inetSocketAddressWrapper.getWeight(); + } + + public boolean isAllowReconnect() { + return this.allowReconnect; + } + + public void setAllowReconnect(boolean allow) { + this.allowReconnect = allow; + } + + public void clearAttributes() { + + } + + public void close() { + + } + + public void flush() { + + } + + public Object getAttribute(String key) { + + return null; + } + + public Decoder getDecoder() { + + return null; + } + + public Encoder getEncoder() { + + return null; + } + + public Handler getHandler() { + + return null; + } + + public long getLastOperationTimeStamp() { + + return 0; + } + + public InetAddress getLocalAddress() { + + return null; + } + + public ByteOrder getReadBufferByteOrder() { + + return null; + } + + public InetSocketAddress getRemoteSocketAddress() { + return this.inetSocketAddressWrapper.getInetSocketAddress(); + } + + public long getScheduleWritenBytes() { + + return 0; + } + + public long getSessionIdleTimeout() { + + return 0; + } + + public long getSessionTimeout() { + + return 0; + } + + public boolean isClosed() { + return true; + } + + public boolean isExpired() { + + return false; + } + + public boolean isHandleReadWriteConcurrently() { + return true; + } + + public boolean isIdle() { + + return false; + } + + public boolean isLoopbackConnection() { + + return false; + } + + public boolean isUseBlockingRead() { + + return false; + } + + public boolean isUseBlockingWrite() { + + return false; + } + + public void removeAttribute(String key) { + + } + + public void setAttribute(String key, Object value) { + + } + + public Object setAttributeIfAbsent(String key, Object value) { + + return null; + } + + public void setDecoder(Decoder decoder) { + + } + + public void setEncoder(Encoder encoder) { + + } + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + + } + + public void setReadBufferByteOrder(ByteOrder readBufferByteOrder) { + + } + + public void setSessionIdleTimeout(long sessionIdleTimeout) { + + } + + public void setSessionTimeout(long sessionTimeout) { + + } + + public void setUseBlockingRead(boolean useBlockingRead) { + + } + + public void setUseBlockingWrite(boolean useBlockingWrite) { + + } + + public void start() { + + } + + public void write(Object packet) { + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/ConnectFuture.java b/src/main/java/net/rubyeye/xmemcached/impl/ConnectFuture.java new file mode 100644 index 0000000..eb80b28 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/ConnectFuture.java @@ -0,0 +1,41 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import com.google.code.yanf4j.core.impl.FutureImpl; + +/** + * Connect operation future + * + * @author boyan + * + */ +public class ConnectFuture extends FutureImpl { + + private final InetSocketAddressWrapper inetSocketAddressWrapper; + + public ConnectFuture(InetSocketAddressWrapper inetSocketAddressWrapper) { + super(); + this.inetSocketAddressWrapper = inetSocketAddressWrapper; + } + + public InetSocketAddressWrapper getInetSocketAddressWrapper() { + return this.inetSocketAddressWrapper; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/DefaultKeyProvider.java b/src/main/java/net/rubyeye/xmemcached/impl/DefaultKeyProvider.java new file mode 100644 index 0000000..94d3ec6 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/DefaultKeyProvider.java @@ -0,0 +1,19 @@ +package net.rubyeye.xmemcached.impl; + +import net.rubyeye.xmemcached.KeyProvider; + +/** + * Default key provider,returns the key itself. + * + * @author dennis + * @since 1.3.8 + */ +public final class DefaultKeyProvider implements KeyProvider { + + public static final KeyProvider INSTANCE = new DefaultKeyProvider(); + + public final String process(String key) { + return key; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/ElectionMemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/ElectionMemcachedSessionLocator.java new file mode 100644 index 0000000..70d32b7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/ElectionMemcachedSessionLocator.java @@ -0,0 +1,90 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import com.google.code.yanf4j.core.Session; + +/** + * Election hash strategy + * + * @author dennis + * + */ +public class ElectionMemcachedSessionLocator extends AbstractMemcachedSessionLocator { + + private transient volatile List sessions; + + private final HashAlgorithm hashAlgorithm; + + public ElectionMemcachedSessionLocator() { + this.hashAlgorithm = HashAlgorithm.ELECTION_HASH; + } + + public ElectionMemcachedSessionLocator(HashAlgorithm hashAlgorithm) { + super(); + this.hashAlgorithm = hashAlgorithm; + } + + public Session getSessionByKey(String key) { + // copy on write + List copySessionList = new ArrayList(this.sessions); + Session result = this.getSessionByElection(key, copySessionList); + while (!this.failureMode && (result == null || result.isClosed()) + && copySessionList.size() > 0) { + copySessionList.remove(result); + result = this.getSessionByElection(key, copySessionList); + } + return result; + } + + private Session getSessionByElection(String key, List copySessionList) { + Session result = null; + long highScore = 0; + for (Session session : copySessionList) { + long hash = 0; + if (session instanceof MemcachedTCPSession) { + MemcachedSession tcpSession = (MemcachedSession) session; + for (int i = 0; i < tcpSession.getWeight(); i++) { + hash = + this.hashAlgorithm.hash(session.getRemoteSocketAddress().toString() + "-" + i + key); + if (hash > highScore) { + highScore = hash; + result = session; + } + } + } else { + hash = this.hashAlgorithm.hash(session.getRemoteSocketAddress().toString() + key); + + } + if (hash > highScore) { + highScore = hash; + result = session; + } + } + return result; + } + + public void updateSessions(Collection list) { + this.sessions = new ArrayList(list); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/FlowControlLinkedTransferQueue.java b/src/main/java/net/rubyeye/xmemcached/impl/FlowControlLinkedTransferQueue.java new file mode 100644 index 0000000..9834343 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/FlowControlLinkedTransferQueue.java @@ -0,0 +1,156 @@ +package net.rubyeye.xmemcached.impl; + +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; +import net.rubyeye.xmemcached.FlowControl; +import net.rubyeye.xmemcached.command.Command; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.util.LinkedTransferQueue; + +public class FlowControlLinkedTransferQueue extends LinkedTransferQueue { + private FlowControl flowControl; + + public FlowControlLinkedTransferQueue(FlowControl flowControl) { + super(); + this.flowControl = flowControl; + } + + private void checkPermits(WriteMessage e) { + if (e.getMessage() instanceof Command) { + Command cmd = (Command) e.getMessage(); + if (cmd.isNoreply()) { + int i = 3; + boolean success = false; + while (i-- > 0) { + if (this.flowControl.aquire()) { + success = true; + break; + } else { + // reduce consuming cpu + Thread.yield(); + } + } + if (!success) + throw new IllegalStateException( + "No permit for noreply operation,max=" + flowControl.max()); + } + } + } + + @Override + public void put(WriteMessage e) throws InterruptedException { + checkPermits(e); + super.put(e); + } + + @Override + public boolean offer(WriteMessage e, long timeout, TimeUnit unit) throws InterruptedException { + checkPermits(e); + return super.offer(e, timeout, unit); + } + + @Override + public boolean offer(WriteMessage e) { + checkPermits(e); + return super.offer(e); + } + + @Override + public void transfer(WriteMessage e) throws InterruptedException { + checkPermits(e); + super.transfer(e); + } + + @Override + public boolean tryTransfer(WriteMessage e, long timeout, TimeUnit unit) + throws InterruptedException { + checkPermits(e); + return super.tryTransfer(e, timeout, unit); + } + + @Override + public boolean tryTransfer(WriteMessage e) { + checkPermits(e); + return super.tryTransfer(e); + } + + @Override + public WriteMessage take() throws InterruptedException { + WriteMessage rt = super.take(); + releasePermit(rt); + return rt; + } + + @Override + public WriteMessage poll(long timeout, TimeUnit unit) throws InterruptedException { + WriteMessage rt = super.poll(timeout, unit); + releasePermit(rt); + return rt; + } + + @Override + public WriteMessage poll() { + WriteMessage rt = super.poll(); + releasePermit(rt); + return rt; + } + + private void releasePermit(WriteMessage rt) { + if (rt != null) { + if (rt.getMessage() instanceof Command) { + Command cmd = (Command) rt.getMessage(); + if (cmd.isNoreply()) { + this.flowControl.release(); + } + } + + } + } + + @Override + public int drainTo(Collection c) { + return super.drainTo(c); + } + + @Override + public int drainTo(Collection c, int maxElements) { + return super.drainTo(c, maxElements); + } + + @Override + public Iterator iterator() { + return super.iterator(); + } + + @Override + public WriteMessage peek() { + return super.peek(); + } + + @Override + public boolean isEmpty() { + return super.isEmpty(); + } + + @Override + public boolean hasWaitingConsumer() { + return super.hasWaitingConsumer(); + } + + @Override + public int size() { + return super.size(); + } + + @Override + public int getWaitingConsumerCount() { + return super.getWaitingConsumerCount(); + } + + @Override + public int remainingCapacity() { + return super.remainingCapacity(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/IndexMemcachedSessionComparator.java b/src/main/java/net/rubyeye/xmemcached/impl/IndexMemcachedSessionComparator.java new file mode 100644 index 0000000..2a13062 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/IndexMemcachedSessionComparator.java @@ -0,0 +1,30 @@ +package net.rubyeye.xmemcached.impl; + +import java.io.Serializable; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import com.google.code.yanf4j.core.Session; + +/** + * Connection comparator,compare with index + * + * @author dennis + * + */ +public class IndexMemcachedSessionComparator implements MemcachedSessionComparator, Serializable { + static final long serialVersionUID = -1L; + + public int compare(Session o1, Session o2) { + MemcachedSession session1 = (MemcachedSession) o1; + MemcachedSession session2 = (MemcachedSession) o2; + if (session1 == null) { + return -1; + } + if (session2 == null) { + return 1; + } + return session1.getInetSocketAddressWrapper().getOrder() + - session2.getInetSocketAddressWrapper().getOrder(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/KetamaMemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/KetamaMemcachedSessionLocator.java new file mode 100644 index 0000000..c7c792e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/KetamaMemcachedSessionLocator.java @@ -0,0 +1,251 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; + +/** + * ConnectionFactory instance that sets up a ketama compatible connection. + * + *

+ * This implementation piggy-backs on the functionality of the DefaultConnectionFactory + * in terms of connections and queue handling. Where it differs is that it uses both the + * KetamaNodeLocator and the HashAlgorithm.KETAMA_HASH to provide consistent + * node hashing. + * + * @see http://www.last.fm/user/RJ/journal/2007/04/10/392555/ + * + *

+ */ +/** + * Consistent Hash Algorithm implementation,based on TreeMap.tailMap(hash) method. + * + * @author dennis + * + */ +public class KetamaMemcachedSessionLocator extends AbstractMemcachedSessionLocator { + + static final int NUM_REPS = 160; + private transient volatile TreeMap> ketamaSessions = + new TreeMap>(); + private final HashAlgorithm hashAlg; + private int maxTries; + + /** + * compatible with nginx-upstream-consistent,patched by wolfg1969 + */ + static final int DEFAULT_PORT = 11211; + private final boolean cwNginxUpstreamConsistent; + private final boolean gwhalinMemcachedJavaClientCompatibiltyConsistent; + + /** + * Create a KetamaMemcachedSessionLocator with default config. + */ + public KetamaMemcachedSessionLocator() { + this.hashAlg = HashAlgorithm.KETAMA_HASH; + this.cwNginxUpstreamConsistent = false; + this.gwhalinMemcachedJavaClientCompatibiltyConsistent = false; + } + + /** + * Create a KetamaMemcachedSessionLocator + * + * @param cwNginxUpstreamConsistent true if compatible with nginx up stream memcached consistent + * algorithm. + */ + public KetamaMemcachedSessionLocator(boolean cwNginxUpstreamConsistent) { + this.hashAlg = HashAlgorithm.KETAMA_HASH; + this.cwNginxUpstreamConsistent = cwNginxUpstreamConsistent; + this.gwhalinMemcachedJavaClientCompatibiltyConsistent = false; + } + + /** + * Create a KetamaMemcachedSessionLocator with a special hash algorithm. + * + * @param alg + */ + public KetamaMemcachedSessionLocator(HashAlgorithm alg) { + this.hashAlg = alg; + this.cwNginxUpstreamConsistent = false; + this.gwhalinMemcachedJavaClientCompatibiltyConsistent = false; + } + + public KetamaMemcachedSessionLocator(HashAlgorithm alg, boolean cwNginxUpstreamConsistent) { + this.hashAlg = alg; + this.cwNginxUpstreamConsistent = cwNginxUpstreamConsistent; + this.gwhalinMemcachedJavaClientCompatibiltyConsistent = false; + } + + public KetamaMemcachedSessionLocator(HashAlgorithm alg, boolean cwNginxUpstreamConsistent, + boolean gwhalinMemcachedJavaClientCompatibiltyConsistent) { + this.hashAlg = HashAlgorithm.KETAMA_HASH; + this.cwNginxUpstreamConsistent = cwNginxUpstreamConsistent; + this.gwhalinMemcachedJavaClientCompatibiltyConsistent = + gwhalinMemcachedJavaClientCompatibiltyConsistent; + } + + public KetamaMemcachedSessionLocator(List list, HashAlgorithm alg) { + super(); + this.hashAlg = alg; + this.cwNginxUpstreamConsistent = false; + this.gwhalinMemcachedJavaClientCompatibiltyConsistent = false; + this.buildMap(list, alg); + } + + private final void buildMap(Collection list, HashAlgorithm alg) { + TreeMap> sessionMap = new TreeMap>(); + + for (Session session : list) { + String sockStr = this.getSockStr(session); + /** + * Duplicate 160 X weight references + */ + int numReps = NUM_REPS; + if (session instanceof MemcachedSession) { + numReps *= ((MemcachedSession) session).getWeight(); + } + if (alg == HashAlgorithm.KETAMA_HASH) { + for (int i = 0; i < numReps / 4; i++) { + byte[] digest = HashAlgorithm.computeMd5(sockStr + "-" + i); + for (int h = 0; h < 4; h++) { + long k = + (long) (digest[3 + h * 4] & 0xFF) << 24 | (long) (digest[2 + h * 4] & 0xFF) << 16 + | (long) (digest[1 + h * 4] & 0xFF) << 8 | digest[h * 4] & 0xFF; + this.getSessionList(sessionMap, k).add(session); + } + + } + } else { + for (int i = 0; i < numReps; i++) { + long key = alg.hash(sockStr + "-" + i); + this.getSessionList(sessionMap, key).add(session); + } + } + } + // sort session list. + for (List sessions : sessionMap.values()) { + Collections.sort(sessions, new Comparator() { + + public int compare(Session o1, Session o2) { + String sockStr1 = KetamaMemcachedSessionLocator.this.getSockStr(o1); + String sockStr2 = KetamaMemcachedSessionLocator.this.getSockStr(o2); + return sockStr1.compareTo(sockStr2); + } + + }); + } + this.ketamaSessions = sessionMap; + this.maxTries = list.size(); + } + + private String getSockStr(Session session) { + String sockStr = null; + if (this.cwNginxUpstreamConsistent) { + InetSocketAddress serverAddress = session.getRemoteSocketAddress(); + sockStr = serverAddress.getAddress().getHostAddress(); + if (serverAddress.getPort() != DEFAULT_PORT) { + sockStr = sockStr + ":" + serverAddress.getPort(); + } + } else { + if (session instanceof MemcachedSession) { + MemcachedSession memcachedSession = (MemcachedSession) session; + InetSocketAddressWrapper inetSocketAddressWrapper = + memcachedSession.getInetSocketAddressWrapper(); + if (this.gwhalinMemcachedJavaClientCompatibiltyConsistent) { + sockStr = inetSocketAddressWrapper.getInetSocketAddress().getHostName() + ":" + + inetSocketAddressWrapper.getInetSocketAddress().getPort(); + + } else { + // Always use the first time resolved address. + sockStr = inetSocketAddressWrapper.getRemoteAddressStr(); + } + } + if (sockStr == null) { + sockStr = String.valueOf(session.getRemoteSocketAddress()); + } + } + return sockStr; + } + + private List getSessionList(TreeMap> sessionMap, long k) { + List sessionList = sessionMap.get(k); + if (sessionList == null) { + sessionList = new ArrayList(); + sessionMap.put(k, sessionList); + } + return sessionList; + } + + public final Session getSessionByKey(final String key) { + if (this.ketamaSessions == null || this.ketamaSessions.size() == 0) { + return null; + } + long hash = this.hashAlg.hash(key); + Session rv = this.getSessionByHash(hash); + int tries = 0; + while (!this.failureMode && (rv == null || rv.isClosed()) && tries++ < this.maxTries) { + hash = this.nextHash(hash, key, tries); + rv = this.getSessionByHash(hash); + } + return rv; + } + + public final Session getSessionByHash(final long hash) { + TreeMap> sessionMap = this.ketamaSessions; + if (sessionMap.size() == 0) { + return null; + } + Long resultHash = hash; + if (!sessionMap.containsKey(hash)) { + // Java 1.6 adds a ceilingKey method, but xmemcached is compatible + // with jdk5,So use tailMap method to do this. + SortedMap> tailMap = sessionMap.tailMap(hash); + if (tailMap.isEmpty()) { + resultHash = sessionMap.firstKey(); + } else { + resultHash = tailMap.firstKey(); + } + } + // + // if (!sessionMap.containsKey(resultHash)) { + // resultHash = sessionMap.ceilingKey(resultHash); + // if (resultHash == null && sessionMap.size() > 0) { + // resultHash = sessionMap.firstKey(); + // } + // } + List sessionList = sessionMap.get(resultHash); + if (sessionList == null || sessionList.size() == 0) { + return null; + } + int size = sessionList.size(); + return sessionList.get((int) (resultHash % size)); + } + + public final long nextHash(long hashVal, String key, int tries) { + long tmpKey = this.hashAlg.hash(tries + key); + hashVal += (int) (tmpKey ^ tmpKey >>> 32); + hashVal &= 0xffffffffL; /* truncate to 32-bits */ + return hashVal; + } + + public final void updateSessions(final Collection list) { + this.buildMap(list, this.hashAlg); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/KeyIteratorImpl.java b/src/main/java/net/rubyeye/xmemcached/impl/KeyIteratorImpl.java new file mode 100644 index 0000000..10dbfcd --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/KeyIteratorImpl.java @@ -0,0 +1,97 @@ +package net.rubyeye.xmemcached.impl; + +import java.net.InetSocketAddress; +import java.util.LinkedList; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.KeyIterator; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.command.text.TextCacheDumpCommand; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.exception.MemcachedServerException; +import net.rubyeye.xmemcached.utils.Protocol; +import com.google.code.yanf4j.core.Session; + +/** + * Default key iterator implementation + * + * @author dennis + * + */ +public final class KeyIteratorImpl implements KeyIterator { + private final LinkedList itemNumbersList; + private LinkedList currentKeyList; + private final MemcachedClient memcachedClient; + private final InetSocketAddress inetSocketAddress; + private long opTimeout = 1000; + + public KeyIteratorImpl(LinkedList itemNumbersList, MemcachedClient memcachedClient, + InetSocketAddress inetSocketAddress) { + super(); + this.itemNumbersList = itemNumbersList; + this.memcachedClient = memcachedClient; + this.inetSocketAddress = inetSocketAddress; + } + + public final InetSocketAddress getServerAddress() { + return this.inetSocketAddress; + } + + public final void setOpTimeout(long opTimeout) { + this.opTimeout = opTimeout; + } + + public void close() { + this.itemNumbersList.clear(); + this.currentKeyList.clear(); + this.currentKeyList = null; + } + + public boolean hasNext() { + return (this.itemNumbersList != null && !this.itemNumbersList.isEmpty()) + || (this.currentKeyList != null && !this.currentKeyList.isEmpty()); + } + + @SuppressWarnings("unchecked") + public String next() throws MemcachedException, TimeoutException, InterruptedException { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + if (this.currentKeyList != null && !this.currentKeyList.isEmpty()) { + return this.currentKeyList.remove(); + } + + int itemNumber = this.itemNumbersList.remove(); + Queue sessions = + this.memcachedClient.getConnector().getSessionByAddress(this.inetSocketAddress); + if (sessions == null || sessions.size() == 0) { + throw new MemcachedException( + "The memcached server is not connected,address=" + this.inetSocketAddress); + } + Session session = sessions.peek(); + CountDownLatch latch = new CountDownLatch(1); + if (this.memcachedClient.getProtocol() == Protocol.Text) { + TextCacheDumpCommand textCacheDumpCommand = new TextCacheDumpCommand(latch, itemNumber); + session.write(textCacheDumpCommand); + if (!latch.await(this.opTimeout, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("stats cachedump timeout"); + } + if (textCacheDumpCommand.getException() != null) { + if (textCacheDumpCommand.getException() instanceof MemcachedException) { + throw (MemcachedException) textCacheDumpCommand.getException(); + } else { + throw new MemcachedServerException(textCacheDumpCommand.getException()); + } + } + this.currentKeyList = (LinkedList) textCacheDumpCommand.getResult(); + } else { + throw new MemcachedException(this.memcachedClient.getProtocol().name() + + " protocol doesn't support iterating all keys in memcached"); + } + return next(); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/LibmemcachedMemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/LibmemcachedMemcachedSessionLocator.java new file mode 100644 index 0000000..208ae32 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/LibmemcachedMemcachedSessionLocator.java @@ -0,0 +1,129 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.SortedMap; +import java.util.TreeMap; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.impl.AbstractMemcachedSessionLocator; +import com.google.code.yanf4j.core.Session; + +/** + * Consistent Hash Algorithm implementation is compatible with libmemcached method. + * + * @author dennis + * + */ +public class LibmemcachedMemcachedSessionLocator extends AbstractMemcachedSessionLocator { + + static final int DEFAULT_NUM_REPS = 100; + private transient volatile TreeMap> ketamaSessions = + new TreeMap>(); + private int maxTries; + private int numReps = DEFAULT_NUM_REPS; + private final Random random = new Random(); + private HashAlgorithm hashAlgorithm = HashAlgorithm.ONE_AT_A_TIME; + + public LibmemcachedMemcachedSessionLocator() {} + + public LibmemcachedMemcachedSessionLocator(int numReps, HashAlgorithm hashAlgorithm) { + super(); + this.numReps = numReps; + this.hashAlgorithm = hashAlgorithm; + } + + private final void buildMap(Collection list, HashAlgorithm alg) { + TreeMap> sessionMap = new TreeMap>(); + + for (Session session : list) { + String sockStr = null; + if (session.getRemoteSocketAddress().getPort() != 11211) { + sockStr = session.getRemoteSocketAddress().getHostName() + ":" + + session.getRemoteSocketAddress().getPort(); + } else { + sockStr = session.getRemoteSocketAddress().getHostName(); + } + for (int i = 0; i < this.numReps; i++) { + long key = hashAlgorithm.hash(sockStr + "-" + i); + this.getSessionList(sessionMap, key).add(session); + } + } + this.ketamaSessions = sessionMap; + this.maxTries = list.size(); + } + + private List getSessionList(TreeMap> sessionMap, long k) { + List sessionList = sessionMap.get(k); + if (sessionList == null) { + sessionList = new ArrayList(); + sessionMap.put(k, sessionList); + } + return sessionList; + } + + public final Session getSessionByKey(final String key) { + if (this.ketamaSessions == null || this.ketamaSessions.size() == 0) { + return null; + } + long hash = hashAlgorithm.hash(key); + Session rv = this.getSessionByHash(hash); + int tries = 0; + while (!this.failureMode && (rv == null || rv.isClosed()) && tries++ < this.maxTries) { + hash = this.nextHash(hash, key, tries); + rv = this.getSessionByHash(hash); + } + return rv; + } + + public final Session getSessionByHash(final long hash) { + TreeMap> sessionMap = this.ketamaSessions; + if (sessionMap.size() == 0) { + return null; + } + Long resultHash = hash; + if (!sessionMap.containsKey(hash)) { + // Java 1.6 adds a ceilingKey method, but xmemcached is compatible + // with jdk5,So use tailMap method to do this. + SortedMap> tailMap = sessionMap.tailMap(hash); + if (tailMap.isEmpty()) { + resultHash = sessionMap.firstKey(); + } else { + resultHash = tailMap.firstKey(); + } + } + // + // if (!sessionMap.containsKey(resultHash)) { + // resultHash = sessionMap.ceilingKey(resultHash); + // if (resultHash == null && sessionMap.size() > 0) { + // resultHash = sessionMap.firstKey(); + // } + // } + List sessionList = sessionMap.get(resultHash); + if (sessionList == null || sessionList.size() == 0) { + return null; + } + int size = sessionList.size(); + return sessionList.get(this.random.nextInt(size)); + } + + public final long nextHash(long hashVal, String key, int tries) { + long tmpKey = hashAlgorithm.hash(tries + key); + hashVal += (int) (tmpKey ^ tmpKey >>> 32); + hashVal &= 0xffffffffL; /* truncate to 32-bits */ + return hashVal; + } + + public final void updateSessions(final Collection list) { + this.buildMap(list, null); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/MemcachedClientStateListenerAdapter.java b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedClientStateListenerAdapter.java new file mode 100644 index 0000000..2394ade --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedClientStateListenerAdapter.java @@ -0,0 +1,72 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientStateListener; +import com.google.code.yanf4j.core.Controller; +import com.google.code.yanf4j.core.ControllerStateListener; + +/** + * Adapte MemcachedClientStateListener to yanf4j's ControllStateListener + * + * @author dennis + * + */ +public class MemcachedClientStateListenerAdapter implements ControllerStateListener { + private final MemcachedClientStateListener memcachedClientStateListener; + private final MemcachedClient memcachedClient; + + public MemcachedClientStateListenerAdapter( + MemcachedClientStateListener memcachedClientStateListener, MemcachedClient memcachedClient) { + super(); + this.memcachedClientStateListener = memcachedClientStateListener; + this.memcachedClient = memcachedClient; + } + + public final MemcachedClientStateListener getMemcachedClientStateListener() { + return this.memcachedClientStateListener; + } + + public final MemcachedClient getMemcachedClient() { + return this.memcachedClient; + } + + public final void onAllSessionClosed(Controller acceptor) { + + } + + public final void onException(Controller acceptor, Throwable t) { + this.memcachedClientStateListener.onException(this.memcachedClient, t); + + } + + public final void onReady(Controller acceptor) { + + } + + public final void onStarted(Controller acceptor) { + this.memcachedClientStateListener.onStarted(this.memcachedClient); + + } + + public final void onStopped(Controller acceptor) { + this.memcachedClientStateListener.onShutDown(this.memcachedClient); + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/MemcachedConnector.java b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedConnector.java new file mode 100644 index 0000000..b5975cd --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedConnector.java @@ -0,0 +1,668 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.FlowControl; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedOptimizer; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.networking.Connector; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import net.rubyeye.xmemcached.utils.Protocol; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.Controller; +import com.google.code.yanf4j.core.ControllerStateListener; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.nio.NioSession; +import com.google.code.yanf4j.nio.NioSessionConfig; +import com.google.code.yanf4j.nio.impl.SocketChannelController; +import com.google.code.yanf4j.util.ConcurrentHashSet; +import com.google.code.yanf4j.util.SystemUtils; + +/** + * Connected session manager + * + * @author dennis + */ +public class MemcachedConnector extends SocketChannelController implements Connector { + private final DelayQueue waitingQueue = new DelayQueue(); + private BufferAllocator bufferAllocator; + + private final Set removedAddrSet = new ConcurrentHashSet(); + + private final MemcachedOptimizer optimiezer; + private long healSessionInterval = MemcachedClient.DEFAULT_HEAL_SESSION_INTERVAL; + private int connectionPoolSize; // session pool size + protected Protocol protocol; + private boolean enableHealSession = true; + private final CommandFactory commandFactory; + private boolean failureMode; + + private final ConcurrentHashMap/* + * standby + * sessions + */> standbySessionMap = + new ConcurrentHashMap>(); + + private final FlowControl flowControl; + + private volatile boolean shuttingDown = false; + + public void shuttingDown() { + this.shuttingDown = true; + } + + public void setSessionLocator(MemcachedSessionLocator sessionLocator) { + this.sessionLocator = sessionLocator; + } + + public void setSessionComparator(MemcachedSessionComparator sessionComparator) { + this.sessionComparator = sessionComparator; + } + + /** + * Session monitor for healing sessions. + * + * @author dennis + * + */ + class SessionMonitor extends Thread { + public SessionMonitor() { + this.setName("Heal-Session-Thread"); + } + + @Override + public void run() { + while (MemcachedConnector.this.isStarted() && MemcachedConnector.this.enableHealSession) { + ReconnectRequest request = null; + try { + request = MemcachedConnector.this.waitingQueue.take(); + + InetSocketAddress address = request.getInetSocketAddressWrapper().getInetSocketAddress(); + + if (!MemcachedConnector.this.removedAddrSet.contains(address)) { + boolean connected = false; + Future future = + MemcachedConnector.this.connect(request.getInetSocketAddressWrapper()); + request.setTries(request.getTries() + 1); + try { + log.info("Trying to connect to " + address.getAddress().getHostAddress() + ":" + + address.getPort() + " for " + request.getTries() + " times"); + if (!future.get(MemcachedClient.DEFAULT_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)) { + connected = false; + } else { + connected = true; + } + } catch (TimeoutException e) { + future.cancel(true); + } catch (ExecutionException e) { + future.cancel(true); + } finally { + if (!connected) { + this.rescheduleConnectRequest(request); + } else { + continue; + } + } + } else { + log.info("Remove invalid reconnect task for " + address); + // remove reconnect task + } + } catch (InterruptedException e) { + // ignore,check status + } catch (Exception e) { + log.error("SessionMonitor connect error", e); + this.rescheduleConnectRequest(request); + } + } + } + + private void rescheduleConnectRequest(ReconnectRequest request) { + if (request == null) { + return; + } + InetSocketAddress address = request.getInetSocketAddressWrapper().getInetSocketAddress(); + // update timestamp for next reconnecting + request.updateNextReconnectTimeStamp( + MemcachedConnector.this.healSessionInterval * request.getTries()); + log.error("Reconnected to " + address + " fail"); + // add to tail + MemcachedConnector.this.waitingQueue.offer(request); + } + } + + public void setEnableHealSession(boolean enableHealSession) { + this.enableHealSession = enableHealSession; + // wake up session monitor thread. + if (this.sessionMonitor != null && this.sessionMonitor.isAlive()) { + this.sessionMonitor.interrupt(); + } + } + + public Queue getReconnectRequestQueue() { + return this.waitingQueue; + } + + @Override + public Set getSessionSet() { + Collection> sessionQueues = this.sessionMap.values(); + Set result = new HashSet(); + for (Queue queue : sessionQueues) { + result.addAll(queue); + } + return result; + } + + public final void setHealSessionInterval(long healConnectionInterval) { + this.healSessionInterval = healConnectionInterval; + } + + public long getHealSessionInterval() { + return this.healSessionInterval; + } + + public void setOptimizeGet(boolean optimiezeGet) { + ((OptimizerMBean) this.optimiezer).setOptimizeGet(optimiezeGet); + } + + public void setOptimizeMergeBuffer(boolean optimizeMergeBuffer) { + ((OptimizerMBean) this.optimiezer).setOptimizeMergeBuffer(optimizeMergeBuffer); + } + + public Protocol getProtocol() { + return this.protocol; + } + + protected MemcachedSessionLocator sessionLocator; + protected MemcachedSessionComparator sessionComparator; + + protected final ConcurrentHashMap> sessionMap = + new ConcurrentHashMap>(); + + public synchronized void addSession(Session session) { + MemcachedSession tcpSession = (MemcachedSession) session; + + InetSocketAddressWrapper addrWrapper = tcpSession.getInetSocketAddressWrapper(); + // Remember the first time address resolved and use it in all + // application lifecycle. + if (addrWrapper.getRemoteAddressStr() == null) { + addrWrapper.setRemoteAddressStr(String.valueOf(session.getRemoteSocketAddress())); + } + + InetSocketAddress mainNodeAddress = addrWrapper.getMainNodeAddress(); + if (mainNodeAddress != null) { + // It is a standby session + this.addStandbySession(session, mainNodeAddress, + addrWrapper.getResolvedMainNodeSocketAddress(), addrWrapper); + } else { + // It is a main session + this.addMainSession(session, addrWrapper.getResolvedSocketAddress(), addrWrapper); + // Update main sessions + this.updateSessions(); + } + } + + private void addMainSession(Session session, InetSocketAddress lastReolvedAddr, + InetSocketAddressWrapper addrWrapper) { + InetSocketAddress remoteSocketAddress = session.getRemoteSocketAddress(); + log.info("Add a session: " + SystemUtils.getRawAddress(remoteSocketAddress) + ":" + + remoteSocketAddress.getPort()); + if (lastReolvedAddr != null && !lastReolvedAddr.equals(remoteSocketAddress)) { + log.warn("Memcached node {} is resolved into {}.", lastReolvedAddr, remoteSocketAddress); + // Remove and closed old resolved address. + Queue sessions = sessionMap.remove(lastReolvedAddr); + if (sessions != null) { + for (Session s : sessions) { + ((MemcachedSession) s).setAllowReconnect(false); + s.close(); + } + } + // updated resolve addr + addrWrapper.setResolvedSocketAddress(remoteSocketAddress); + } + Queue sessions = this.sessionMap.get(remoteSocketAddress); + if (sessions == null) { + sessions = new ConcurrentLinkedQueue(); + Queue oldSessions = this.sessionMap.putIfAbsent(remoteSocketAddress, sessions); + if (null != oldSessions) { + sessions = oldSessions; + } + } + // If it is in failure mode,remove closed session from list + if (this.failureMode) { + Iterator it = sessions.iterator(); + while (it.hasNext()) { + Session tmp = it.next(); + if (tmp.isClosed()) { + it.remove(); + break; + } + } + } + + sessions.offer(session); + // Remove old session and close it + while (sessions.size() > this.connectionPoolSize) { + Session oldSession = sessions.poll(); + ((MemcachedSession) oldSession).setAllowReconnect(false); + oldSession.close(); + } + } + + private void addStandbySession(Session session, InetSocketAddress mainNodeAddress, + InetSocketAddress lastResolvedMainAddr, InetSocketAddressWrapper addrWrapper) { + InetSocketAddress remoteSocketAddress = session.getRemoteSocketAddress(); + log.info("Add a standby session: " + SystemUtils.getRawAddress(remoteSocketAddress) + ":" + + remoteSocketAddress.getPort() + " for " + SystemUtils.getRawAddress(mainNodeAddress) + ":" + + mainNodeAddress.getPort()); + if (lastResolvedMainAddr != null && !lastResolvedMainAddr.equals(remoteSocketAddress)) { + log.warn("Memcached node {} is resolved into {}.", lastResolvedMainAddr, remoteSocketAddress); + // Remove and closed old resolved address. + List sessions = standbySessionMap.remove(lastResolvedMainAddr); + if (sessions != null) { + for (Session s : sessions) { + ((MemcachedSession) s).setAllowReconnect(false); + s.close(); + } + } + addrWrapper.setResolvedMainNodeSocketAddress(remoteSocketAddress); + } + List sessions = this.standbySessionMap.get(mainNodeAddress); + if (sessions == null) { + sessions = new CopyOnWriteArrayList(); + List oldSessions = this.standbySessionMap.putIfAbsent(mainNodeAddress, sessions); + if (null != oldSessions) { + sessions = oldSessions; + } + } + sessions.add(session); + } + + public List getSessionListBySocketAddress(InetSocketAddress inetSocketAddress) { + Queue queue = this.sessionMap.get(inetSocketAddress); + if (queue != null) { + return new ArrayList(queue); + } else { + return null; + } + } + + public void removeReconnectRequest(InetSocketAddress inetSocketAddress) { + this.removedAddrSet.add(inetSocketAddress); + Iterator it = this.waitingQueue.iterator(); + while (it.hasNext()) { + ReconnectRequest request = it.next(); + if (request.getInetSocketAddressWrapper().getInetSocketAddress().equals(inetSocketAddress)) { + it.remove(); + log.warn("Remove invalid reconnect task for " + + request.getInetSocketAddressWrapper().getInetSocketAddress()); + } + } + } + + public final void updateSessions() { + Collection> sessionCollection = this.sessionMap.values(); + List sessionList = new ArrayList(20); + for (Queue sessions : sessionCollection) { + sessionList.addAll(sessions); + } + // sort the sessions to keep order + Collections.sort(sessionList, sessionComparator); + this.sessionLocator.updateSessions(sessionList); + } + + public synchronized void removeSession(Session session) { + MemcachedTCPSession tcpSession = (MemcachedTCPSession) session; + InetSocketAddressWrapper addrWrapper = tcpSession.getInetSocketAddressWrapper(); + InetSocketAddress mainNodeAddr = addrWrapper.getMainNodeAddress(); + if (mainNodeAddr != null) { + this.removeStandbySession(session, mainNodeAddr); + } else { + this.removeMainSession(session); + } + } + + private void removeMainSession(Session session) { + InetSocketAddress remoteSocketAddress = session.getRemoteSocketAddress(); + // If it was in failure mode,we don't remove closed session from list. + if (this.failureMode && ((MemcachedSession) session).isAllowReconnect() && !this.shuttingDown + && this.isStarted()) { + log.warn("Client in failure mode,we don't remove session " + + SystemUtils.getRawAddress(remoteSocketAddress) + ":" + remoteSocketAddress.getPort()); + return; + } + log.info("Remove a session: " + SystemUtils.getRawAddress(remoteSocketAddress) + ":" + + remoteSocketAddress.getPort()); + Queue sessionQueue = this.sessionMap.get(session.getRemoteSocketAddress()); + if (null != sessionQueue) { + sessionQueue.remove(session); + if (sessionQueue.size() == 0) { + this.sessionMap.remove(session.getRemoteSocketAddress()); + } + this.updateSessions(); + } + } + + private void removeStandbySession(Session session, InetSocketAddress mainNodeAddr) { + List sessionList = this.standbySessionMap.get(mainNodeAddr); + if (null != sessionList) { + sessionList.remove(session); + if (sessionList.size() == 0) { + this.standbySessionMap.remove(mainNodeAddr); + } + } + } + + @Override + protected void doStart() throws IOException { + this.setLocalSocketAddress(new InetSocketAddress("localhost", 0)); + } + + @Override + public void onConnect(SelectionKey key) throws IOException { + key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT); + ConnectFuture future = (ConnectFuture) key.attachment(); + if (future == null || future.isCancelled()) { + this.cancelKey(key); + return; + } + try { + if (!((SocketChannel) key.channel()).finishConnect()) { + this.cancelKey(key); + future + .failure(new IOException("Connect to " + + SystemUtils.getRawAddress( + future.getInetSocketAddressWrapper().getInetSocketAddress()) + + ":" + future.getInetSocketAddressWrapper().getInetSocketAddress().getPort() + + " fail")); + } else { + key.attach(null); + this.addSession(this.createSession((SocketChannel) key.channel(), + future.getInetSocketAddressWrapper())); + future.setResult(Boolean.TRUE); + } + } catch (Exception e) { + future.failure(e); + this.cancelKey(key); + throw new IOException("Connect to " + + SystemUtils.getRawAddress(future.getInetSocketAddressWrapper().getInetSocketAddress()) + + ":" + future.getInetSocketAddressWrapper().getInetSocketAddress().getPort() + " fail," + + e.getMessage()); + } + } + + private void cancelKey(SelectionKey key) throws IOException { + try { + if (key.channel() != null) { + key.channel().close(); + } + } finally { + key.cancel(); + } + } + + protected MemcachedTCPSession createSession(SocketChannel socketChannel, + InetSocketAddressWrapper wrapper) { + MemcachedTCPSession session = (MemcachedTCPSession) this.buildSession(socketChannel); + session.setInetSocketAddressWrapper(wrapper); + this.selectorManager.registerSession(session, EventType.ENABLE_READ); + session.start(); + session.onEvent(EventType.CONNECTED, null); + return session; + } + + public void addToWatingQueue(ReconnectRequest request) { + this.waitingQueue.add(request); + } + + public Future connect(InetSocketAddressWrapper addressWrapper) { + if (addressWrapper == null) { + throw new NullPointerException("Null Address"); + } + // Remove addr from removed set + this.removedAddrSet.remove(addressWrapper.getInetSocketAddress()); + SocketChannel socketChannel = null; + ConnectFuture future = new ConnectFuture(addressWrapper); + try { + socketChannel = SocketChannel.open(); + this.configureSocketChannel(socketChannel); + if (!socketChannel.connect(addressWrapper.getInetSocketAddress())) { + this.selectorManager.registerChannel(socketChannel, SelectionKey.OP_CONNECT, future); + } else { + this.addSession(this.createSession(socketChannel, addressWrapper)); + future.setResult(true); + } + } catch (Exception e) { + if (socketChannel != null) { + try { + socketChannel.close(); + } catch (IOException e1) { + //propagate original exception + } + } + future.failure(e); + } + return future; + } + + public void closeChannel(Selector selector) throws IOException { + // do nothing + } + + private final Random random = new Random(); + + public Session send(final Command msg) throws MemcachedException { + MemcachedSession session = (MemcachedSession) this.findSessionByKey(msg.getKey()); + if (session == null) { + throw new MemcachedException("There is no available connection at this moment"); + } + // If session was closed,try to use standby memcached node + if (session.isClosed()) { + session = this.findStandbySession(session); + } + if (session.isClosed()) { + throw new MemcachedException( + "Session(" + SystemUtils.getRawAddress(session.getRemoteSocketAddress()) + ":" + + session.getRemoteSocketAddress().getPort() + ") has been closed"); + } + if (session.isAuthFailed()) { + throw new MemcachedException("Auth failed to connection " + session.getRemoteSocketAddress()); + } + session.write(msg); + return session; + } + + private MemcachedSession findStandbySession(MemcachedSession session) { + if (this.failureMode) { + List sessionList = + this.getStandbySessionListByMainNodeAddr(session.getRemoteSocketAddress()); + if (sessionList != null && !sessionList.isEmpty()) { + return (MemcachedTCPSession) sessionList.get(this.random.nextInt(sessionList.size())); + } + } + return session; + } + + /** + * Returns main node's standby session list. + * + * @param addr + * @return + */ + public List getStandbySessionListByMainNodeAddr(InetSocketAddress addr) { + return this.standbySessionMap.get(addr); + } + + private final SessionMonitor sessionMonitor = new SessionMonitor(); + + /** + * Inner state listenner,manage session monitor. + * + * @author boyan + * + */ + class InnerControllerStateListener implements ControllerStateListener { + + public void onAllSessionClosed(Controller controller) { + + } + + public void onException(Controller controller, Throwable t) { + log.error("Exception occured in controller", t); + } + + public void onReady(Controller controller) { + MemcachedConnector.this.sessionMonitor.setDaemon(true); + MemcachedConnector.this.sessionMonitor.start(); + } + + public void onStarted(Controller controller) { + + } + + public void onStopped(Controller controller) { + if (MemcachedConnector.this.sessionMonitor.isAlive()) { + MemcachedConnector.this.sessionMonitor.interrupt(); + } + } + + } + + public final Session findSessionByKey(String key) { + return this.sessionLocator.getSessionByKey(key); + } + + /** + * Get session by InetSocketAddress + * + * @param addr + * @return + */ + public final Queue getSessionByAddress(InetSocketAddress addr) { + return this.sessionMap.get(addr); + } + + public MemcachedConnector(Configuration configuration, MemcachedSessionLocator locator, + MemcachedSessionComparator comparator, BufferAllocator allocator, + CommandFactory commandFactory, int poolSize, int maxQueuedNoReplyOperations) { + super(configuration, null); + this.sessionLocator = locator; + this.sessionComparator = comparator; + this.protocol = commandFactory.getProtocol(); + this.addStateListener(new InnerControllerStateListener()); + this.updateSessions(); + this.bufferAllocator = allocator; + this.optimiezer = new Optimizer(this.protocol); + this.optimiezer.setBufferAllocator(this.bufferAllocator); + this.connectionPoolSize = poolSize; + this.soLingerOn = true; + this.commandFactory = commandFactory; + this.flowControl = new FlowControl(maxQueuedNoReplyOperations); + this.setSelectorPoolSize(configuration.getSelectorPoolSize()); + // setDispatchMessageThreadPoolSize(Runtime.getRuntime(). + // availableProcessors()); + } + + public final void setConnectionPoolSize(int poolSize) { + this.connectionPoolSize = poolSize; + } + + public void setMergeFactor(int mergeFactor) { + ((OptimizerMBean) this.optimiezer).setMergeFactor(mergeFactor); + } + + public FlowControl getNoReplyOpsFlowControl() { + return this.flowControl; + } + + @Override + protected NioSession buildSession(SocketChannel sc) { + Queue queue = this.buildQueue(); + final NioSessionConfig sessionCofig = this.buildSessionConfig(sc, queue); + MemcachedTCPSession session = + new MemcachedTCPSession(sessionCofig, this.configuration.getSessionReadBufferSize(), + this.optimiezer, this.getReadThreadCount(), this.commandFactory); + session.setBufferAllocator(this.bufferAllocator); + return session; + } + + /** + * Build write queue for session + * + * @return + */ + @Override + protected Queue buildQueue() { + return new FlowControlLinkedTransferQueue(this.flowControl); + } + + public BufferAllocator getBufferAllocator() { + return this.bufferAllocator; + } + + public synchronized void quitAllSessions() { + for (Session session : this.sessionSet) { + ((MemcachedSession) session).quit(); + } + int sleepCount = 0; + while (sleepCount++ < 5 && this.sessionSet.size() > 0) { + try { + this.wait(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + } + + public void setFailureMode(boolean failureMode) { + this.failureMode = failureMode; + } + + public void setBufferAllocator(BufferAllocator allocator) { + this.bufferAllocator = allocator; + for (Session session : this.getSessionSet()) { + ((MemcachedSession) session).setBufferAllocator(allocator); + } + } + + public Collection getServerAddresses() { + return Collections.unmodifiableCollection(this.sessionMap.keySet()); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/MemcachedHandler.java b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedHandler.java new file mode 100644 index 0000000..429ae5b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedHandler.java @@ -0,0 +1,293 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientStateListener; +import net.rubyeye.xmemcached.auth.AuthMemcachedConnectListener; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.MapReturnValueAware; +import net.rubyeye.xmemcached.command.OperationStatus; +import net.rubyeye.xmemcached.command.StoreCommand; +import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; +import net.rubyeye.xmemcached.command.binary.BinaryVersionCommand; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; +import net.rubyeye.xmemcached.command.text.TextVersionCommand; +import net.rubyeye.xmemcached.monitor.StatisticsHandler; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.networking.MemcachedSessionConnectListener; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import net.rubyeye.xmemcached.utils.Protocol; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.AbstractSession; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.util.SystemUtils; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * Memcached Session Handler,used for dispatching commands and session's lifecycle management + * + * @author dennis + * + */ +public class MemcachedHandler extends HandlerAdapter { + + private static final int MAX_HEARTBEAT_THREADS = + Integer.parseInt(System.getProperty("xmemcached.heartbeat.max_threads", + String.valueOf(SystemUtils.getSystemThreadCount()))); + + private final StatisticsHandler statisticsHandler; + + private ExecutorService heartBeatThreadPool; + + private final MemcachedSessionConnectListener listener; + + private final MemcachedClient client; + private static final Logger log = LoggerFactory.getLogger(MemcachedHandler.class); + + /** + * On receive message from memcached server + */ + @Override + public final void onMessageReceived(final Session session, final Object msg) { + Command command = (Command) msg; + if (this.statisticsHandler.isStatistics()) { + if (command.getCopiedMergeCount() > 0 && command instanceof MapReturnValueAware) { + Map returnValues = ((MapReturnValueAware) command).getReturnValues(); + int size = returnValues.size(); + this.statisticsHandler.statistics(CommandType.GET_HIT, size); + this.statisticsHandler.statistics(CommandType.GET_MISS, + command.getCopiedMergeCount() - size); + } else if (command instanceof TextGetOneCommand || command instanceof BinaryGetCommand) { + if (command.getResult() != null) { + this.statisticsHandler.statistics(CommandType.GET_HIT); + } else { + this.statisticsHandler.statistics(CommandType.GET_MISS); + } + } else { + if (command.getCopiedMergeCount() > 0) { + this.statisticsHandler.statistics(command.getCommandType(), + command.getCopiedMergeCount()); + } else + this.statisticsHandler.statistics(command.getCommandType()); + } + } + } + + private volatile boolean enableHeartBeat = true; + + public void setEnableHeartBeat(boolean enableHeartBeat) { + this.enableHeartBeat = enableHeartBeat; + } + + public static final IoBuffer EMPTY_BUF = IoBuffer.allocate(0); + + /** + * put command which have been sent to queue + */ + @Override + public final void onMessageSent(Session session, Object msg) { + Command command = (Command) msg; + command.setStatus(OperationStatus.SENT); + // After message sent,we can set the buffer to be null for gc friendly. + command.setIoBuffer(EMPTY_BUF); + switch (command.getCommandType()) { + case ADD: + case APPEND: + case SET: + case SET_MANY: + // After message sent,we can set the value to be null for gc + // friendly. + if (command instanceof StoreCommand) { + ((StoreCommand) command).setValue(null); + } + break; + } + } + + @Override + public void onExceptionCaught(Session session, Throwable throwable) { + log.error("XMemcached network layout exception", throwable); + } + + /** + * On session started + */ + @Override + public void onSessionStarted(Session session) { + session.setUseBlockingRead(true); + session.setAttribute(HEART_BEAT_FAIL_COUNT_ATTR, new AtomicInteger(0)); + for (MemcachedClientStateListener listener : this.client.getStateListeners()) { + listener.onConnected(this.client, session.getRemoteSocketAddress()); + } + this.listener.onConnect((MemcachedTCPSession) session, this.client); + } + + /** + * Check if have to reconnect on session closed + */ + @Override + public final void onSessionClosed(Session session) { + this.client.getConnector().removeSession(session); + // Clear write queue to release noreply operations. + ((AbstractSession) session).clearWriteQueue(); + MemcachedTCPSession memcachedSession = (MemcachedTCPSession) session; + // destroy memached session + memcachedSession.destroy(); + if (this.client.getConnector().isStarted() && memcachedSession.isAllowReconnect()) { + this.reconnect(memcachedSession); + } + for (MemcachedClientStateListener listener : this.client.getStateListeners()) { + listener.onDisconnected(this.client, session.getRemoteSocketAddress()); + } + + } + + /** + * Do a heartbeat action + */ + @Override + public void onSessionIdle(Session session) { + checkHeartBeat(session); + } + + private void checkHeartBeat(Session session) { + if (this.enableHeartBeat) { + log.debug("Check session ({}) is alive,send heartbeat", + session.getRemoteSocketAddress() == null ? "unknown" + : SystemUtils.getRawAddress(session.getRemoteSocketAddress()) + ":" + + session.getRemoteSocketAddress().getPort()); + Command versionCommand = null; + CountDownLatch latch = new CountDownLatch(1); + if (this.client.getProtocol() == Protocol.Binary) { + versionCommand = new BinaryVersionCommand(latch, session.getRemoteSocketAddress()); + + } else { + versionCommand = new TextVersionCommand(latch, session.getRemoteSocketAddress()); + } + session.write(versionCommand); + // Start a check thread,avoid blocking reactor thread + if (this.heartBeatThreadPool != null) { + this.heartBeatThreadPool.execute(new CheckHeartResultThread(versionCommand, session)); + } + } + } + + private static final String HEART_BEAT_FAIL_COUNT_ATTR = "heartBeatFailCount"; + private static final int MAX_HEART_BEAT_FAIL_COUNT = + Integer.parseInt(System.getProperty("xmemcached.heartbeat.max.fail.times", "3")); + + final static class CheckHeartResultThread implements Runnable { + + private final Command versionCommand; + private final Session session; + + public CheckHeartResultThread(Command versionCommand, Session session) { + super(); + this.versionCommand = versionCommand; + this.session = session; + } + + public void run() { + try { + AtomicInteger heartBeatFailCount = + (AtomicInteger) this.session.getAttribute(HEART_BEAT_FAIL_COUNT_ATTR); + if (heartBeatFailCount != null) { + if (!this.versionCommand.getLatch().await(2000, TimeUnit.MILLISECONDS)) { + heartBeatFailCount.incrementAndGet(); + } else { + if (this.versionCommand.getResult() == null) { + heartBeatFailCount.incrementAndGet(); + } else { + // reset the failure counter + heartBeatFailCount.set(0); + } + } + if (heartBeatFailCount.get() > MAX_HEART_BEAT_FAIL_COUNT) { + log.warn("Session(" + SystemUtils.getRawAddress(this.session.getRemoteSocketAddress()) + + ":" + this.session.getRemoteSocketAddress().getPort() + ") heartbeat fail " + + heartBeatFailCount.get() + " times,close session and try to heal it"); + this.session.close();// close session + heartBeatFailCount.set(0); + } + } + } catch (InterruptedException e) { + // ignore + } + } + } + + /** + * Auto reconect to memcached server + * + * @param session + */ + protected void reconnect(MemcachedTCPSession session) { + if (!this.client.isShutdown()) { + // Prevent reconnecting repeatedly + synchronized (session) { + if (!session.isAllowReconnect()) { + return; + } + session.setAllowReconnect(false); + } + MemcachedSession memcachedTCPSession = session; + InetSocketAddressWrapper inetSocketAddressWrapper = + memcachedTCPSession.getInetSocketAddressWrapper(); + this.client.getConnector().addToWatingQueue( + new ReconnectRequest(inetSocketAddressWrapper, 0, this.client.getHealSessionInterval())); + } + } + + public void stop() { + this.heartBeatThreadPool.shutdown(); + } + + final long HEARTBEAT_PERIOD = + Long.parseLong(System.getProperty("xmemcached.heartbeat.period", "5000")); + + public void start() { + final String name = "XMemcached-HeartBeatPool[" + client.getName() + "]"; + final AtomicInteger threadCounter = new AtomicInteger(); + + long keepAliveTime = client.getConnector().getSessionIdleTimeout() * 3 / 2; + + this.heartBeatThreadPool = new ThreadPoolExecutor(1, MAX_HEARTBEAT_THREADS, keepAliveTime, + TimeUnit.MILLISECONDS, new SynchronousQueue(), new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = new Thread(r, name + "-" + threadCounter.getAndIncrement()); + t.setDaemon(true); + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + }, new ThreadPoolExecutor.DiscardPolicy()); + } + + public MemcachedHandler(MemcachedClient client) { + super(); + this.client = client; + this.listener = new AuthMemcachedConnectListener(); + this.statisticsHandler = new StatisticsHandler(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/MemcachedTCPSession.java b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedTCPSession.java new file mode 100644 index 0000000..0f80aeb --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/MemcachedTCPSession.java @@ -0,0 +1,228 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.channels.SocketChannel; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.MemcachedOptimizer; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.OperationStatus; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import net.rubyeye.xmemcached.utils.Protocol; +import com.google.code.yanf4j.core.WriteMessage; +import com.google.code.yanf4j.core.impl.FutureImpl; +import com.google.code.yanf4j.nio.NioSessionConfig; +import com.google.code.yanf4j.nio.impl.NioTCPSession; +import com.google.code.yanf4j.util.LinkedTransferQueue; +import com.google.code.yanf4j.util.SystemUtils; + +/** + * Connected session for a memcached server + * + * @author dennis + */ +public class MemcachedTCPSession extends NioTCPSession implements MemcachedSession { + + /** + * Command which are already sent + */ + protected BlockingQueue commandAlreadySent; + + private final AtomicReference currentCommand = + new LinkedTransferQueue.PaddedAtomicReference(null); + + private SocketAddress remoteSocketAddress; // prevent channel is closed + private int sendBufferSize; + private final MemcachedOptimizer optimiezer; + private boolean allowReconnect; + + private volatile boolean authFailed; + + private final CommandFactory commandFactory; + + private InetSocketAddressWrapper inetSocketAddressWrapper; + + public MemcachedTCPSession(NioSessionConfig sessionConfig, int readRecvBufferSize, + MemcachedOptimizer optimiezer, int readThreadCount, CommandFactory commandFactory) { + super(sessionConfig, readRecvBufferSize); + this.optimiezer = optimiezer; + if (this.selectableChannel != null) { + this.remoteSocketAddress = + ((SocketChannel) this.selectableChannel).socket().getRemoteSocketAddress(); + this.allowReconnect = true; + try { + this.sendBufferSize = ((SocketChannel) this.selectableChannel).socket().getSendBufferSize(); + } catch (SocketException e) { + this.sendBufferSize = 8 * 1024; + } + } + this.commandAlreadySent = (BlockingQueue) SystemUtils.createTransferQueue(); + this.commandFactory = commandFactory; + } + + public InetSocketAddressWrapper getInetSocketAddressWrapper() { + return this.inetSocketAddressWrapper; + } + + public int getOrder() { + return this.getInetSocketAddressWrapper().getOrder(); + } + + public int getWeight() { + return this.getInetSocketAddressWrapper().getWeight(); + } + + public void setInetSocketAddressWrapper(InetSocketAddressWrapper inetSocketAddressWrapper) { + this.inetSocketAddressWrapper = inetSocketAddressWrapper; + } + + @Override + public String toString() { + return SystemUtils.getRawAddress(this.getRemoteSocketAddress()) + ":" + + this.getRemoteSocketAddress().getPort(); + } + + public void destroy() { + Command command = this.currentCommand.get(); + if (command != null) { + command.setException(new MemcachedException("Session has been closed")); + CountDownLatch latch = command.getLatch(); + if (latch != null) { + latch.countDown(); + } + } + while ((command = this.commandAlreadySent.poll()) != null) { + command.setException(new MemcachedException("Session has been closed")); + CountDownLatch latch = command.getLatch(); + if (latch != null) { + latch.countDown(); + } + } + + } + + @Override + public InetSocketAddress getRemoteSocketAddress() { + InetSocketAddress result = super.getRemoteSocketAddress(); + if (result == null && this.remoteSocketAddress != null) { + result = (InetSocketAddress) this.remoteSocketAddress; + } + return result; + } + + @Override + protected WriteMessage preprocessWriteMessage(WriteMessage writeMessage) { + Command currentCommand = (Command) writeMessage; + // Check if IoBuffer is null + if (currentCommand.getIoBuffer() == null) { + currentCommand.encode(); + } + if (currentCommand.getStatus() == OperationStatus.SENDING) { + /** + * optimize commands + */ + currentCommand = this.optimiezer.optimize(currentCommand, this.writeQueue, + this.commandAlreadySent, this.sendBufferSize); + } + + currentCommand.setStatus(OperationStatus.WRITING); + if (!currentCommand.isAdded() + && (!currentCommand.isNoreply() || this.commandFactory.getProtocol() == Protocol.Binary)) { + currentCommand.setAdded(true); + this.addCommand(currentCommand); + } + + return currentCommand; + } + + public boolean isAuthFailed() { + return this.authFailed; + } + + public void setAuthFailed(boolean authFailed) { + this.authFailed = authFailed; + } + + private BufferAllocator bufferAllocator; + + public final BufferAllocator getBufferAllocator() { + return this.bufferAllocator; + } + + public final void setBufferAllocator(BufferAllocator bufferAllocator) { + this.bufferAllocator = bufferAllocator; + } + + @Override + protected final WriteMessage wrapMessage(Object msg, Future writeFuture) { + ((Command) msg).encode(); + ((Command) msg).setWriteFuture((FutureImpl) writeFuture); + if (log.isDebugEnabled()) { + log.debug("After encoding" + ((Command) msg).toString()); + } + return (WriteMessage) msg; + } + + /** + * get current command from queue + * + * @return + */ + private Command takeExecutingCommand() { + try { + return this.commandAlreadySent.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return null; + } + + /** + * is allow auto recconect if closed? + * + * @return + */ + public boolean isAllowReconnect() { + return this.allowReconnect; + } + + public void setAllowReconnect(boolean reconnected) { + this.allowReconnect = reconnected; + } + + public void addCommand(Command command) { + this.commandAlreadySent.add(command); + } + + public void setCurrentCommand(Command cmd) { + this.currentCommand.set(cmd); + } + + public Command getCurrentCommand() { + return this.currentCommand.get(); + } + + public void takeCurrentCommand() { + this.setCurrentCommand(this.takeExecutingCommand()); + } + + public void quit() { + this.write(this.commandFactory.createQuitCommand()); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/Optimizer.java b/src/main/java/net/rubyeye/xmemcached/impl/Optimizer.java new file mode 100644 index 0000000..551544f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/Optimizer.java @@ -0,0 +1,606 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CountDownLatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.impl.FutureImpl; +import net.rubyeye.xmemcached.MemcachedOptimizer; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.AssocCommandAware; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.OperationStatus; +import net.rubyeye.xmemcached.command.binary.BaseBinaryCommand; +import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; +import net.rubyeye.xmemcached.command.binary.BinaryGetMultiCommand; +import net.rubyeye.xmemcached.command.binary.BinarySetMultiCommand; +import net.rubyeye.xmemcached.command.binary.BinaryStoreCommand; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.monitor.MemcachedClientNameHolder; +import net.rubyeye.xmemcached.monitor.XMemcachedMbeanServer; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.OpaqueGenerater; +import net.rubyeye.xmemcached.utils.Protocol; + +/** + * Memcached command optimizer,merge single-get comands to multi-get command, + * merge ByteBuffers to fit the socket's sendBufferSize etc. + * + * @author dennis + */ +public class Optimizer implements OptimizerMBean, MemcachedOptimizer { + + private static final int MARGIN = 24; + public static final int DEFAULT_MERGE_FACTOR = 32; + private int mergeFactor = DEFAULT_MERGE_FACTOR; // default merge factor; + private boolean optimiezeGet = true; + private final boolean optimiezeSet = true; + private boolean optimiezeMergeBuffer = true; + private static final Logger log = LoggerFactory.getLogger(Optimizer.class); + private Protocol protocol = Protocol.Binary; + + public Optimizer(final Protocol protocol) { + XMemcachedMbeanServer.getInstance().registMBean(this, this.getClass().getPackage().getName() + + ":type=" + this.getClass().getSimpleName() + "-" + MemcachedClientNameHolder.getName()); + this.protocol = protocol; + } + + public void setBufferAllocator(final BufferAllocator bufferAllocator) { + + } + + public int getMergeFactor() { + return this.mergeFactor; + } + + public void setMergeFactor(final int mergeFactor) { + if (this.mergeFactor != mergeFactor) { + log.warn("change mergeFactor from " + this.mergeFactor + " to " + mergeFactor); + } + this.mergeFactor = mergeFactor; + + } + + public boolean isOptimizeGet() { + return this.optimiezeGet; + } + + public void setOptimizeGet(final boolean optimiezeGet) { + log.warn(optimiezeGet ? "Enable merge get commands" : "Disable merge get commands"); + this.optimiezeGet = optimiezeGet; + } + + public boolean isOptimizeMergeBuffer() { + return this.optimiezeMergeBuffer; + } + + public void setOptimizeMergeBuffer(final boolean optimiezeMergeBuffer) { + log.warn(optimiezeMergeBuffer ? "Enable merge buffers" : "Disable merge buffers"); + this.optimiezeMergeBuffer = optimiezeMergeBuffer; + } + + @SuppressWarnings("unchecked") + public Command optimize(final Command currentCommand, final Queue writeQueue, + final Queue executingCmds, final int sendBufferSize) { + Command optimiezeCommand = currentCommand; + optimiezeCommand = optimiezeGet(writeQueue, executingCmds, optimiezeCommand); + optimiezeCommand = optimiezeSet(writeQueue, executingCmds, optimiezeCommand, sendBufferSize); + optimiezeCommand = + optimiezeMergeBuffer(optimiezeCommand, writeQueue, executingCmds, sendBufferSize); + return optimiezeCommand; + } + + /** + * merge buffers to fit socket's send buffer size + * + * @param currentCommand + * @return + * @throws InterruptedException + */ + @SuppressWarnings("unchecked") + public final Command optimiezeMergeBuffer(Command optimiezeCommand, final Queue writeQueue, + final Queue executingCmds, final int sendBufferSize) { + if (log.isDebugEnabled()) { + log.debug("Optimieze merge buffer:" + optimiezeCommand.toString()); + } + if (this.optimiezeMergeBuffer + && optimiezeCommand.getIoBuffer().remaining() < sendBufferSize - 24) { + optimiezeCommand = mergeBuffer(optimiezeCommand, writeQueue, executingCmds, sendBufferSize); + } + return optimiezeCommand; + } + + /** + * Merge get operation to multi-get operation + * + * @param currentCmd + * @param mergeCommands + * @return + * @throws InterruptedException + */ + @SuppressWarnings("unchecked") + public final Command optimiezeGet(final Queue writeQueue, final Queue executingCmds, + Command optimiezeCommand) { + if (optimiezeCommand.getCommandType() == CommandType.GET_ONE + || optimiezeCommand.getCommandType() == CommandType.GETS_ONE) { + if (this.optimiezeGet) { + optimiezeCommand = mergeGetCommands(optimiezeCommand, writeQueue, executingCmds, + optimiezeCommand.getCommandType()); + } + } + return optimiezeCommand; + } + + public final Command optimiezeSet(final Queue writeQueue, final Queue executingCmds, + Command optimiezeCommand, final int sendBufferSize) { + if (this.optimiezeSet && optimiezeCommand.getCommandType() == CommandType.SET + && !optimiezeCommand.isNoreply() && this.protocol == Protocol.Binary) { + optimiezeCommand = mergeSetCommands(optimiezeCommand, writeQueue, executingCmds, + optimiezeCommand.getCommandType(), sendBufferSize); + } + return optimiezeCommand; + } + + @SuppressWarnings("unchecked") + private final Command mergeBuffer(final Command firstCommand, final Queue writeQueue, + final Queue executingCmds, final int sendBufferSize) { + Command lastCommand = firstCommand; + Command nextCmd = (Command) writeQueue.peek(); + if (nextCmd == null) { + return lastCommand; + } + + final List commands = getLocalList(); + final ByteBuffer firstBuffer = firstCommand.getIoBuffer().buf(); + int totalBytes = firstBuffer.remaining(); + commands.add(firstCommand); + boolean wasFirst = true; + while (totalBytes + nextCmd.getIoBuffer().remaining() <= sendBufferSize + && (nextCmd = (Command) writeQueue.peek()) != null) { + if (nextCmd.getStatus() == OperationStatus.WRITING) { + break; + } + if (nextCmd.isCancel()) { + writeQueue.remove(); + continue; + } + nextCmd.setStatus(OperationStatus.WRITING); + + writeQueue.remove(); + + if (wasFirst) { + wasFirst = false; + } + // if it is get_one command,try to merge get commands + if ((nextCmd.getCommandType() == CommandType.GET_ONE + || nextCmd.getCommandType() == CommandType.GETS_ONE) && this.optimiezeGet) { + nextCmd = mergeGetCommands(nextCmd, writeQueue, executingCmds, nextCmd.getCommandType()); + } + + commands.add(nextCmd); + lastCommand = nextCmd; + totalBytes += nextCmd.getIoBuffer().remaining(); + if (totalBytes > sendBufferSize) { + break; + } + + } + if (commands.size() > 1) { + byte[] buf = new byte[totalBytes]; + int offset = 0; + for (Command command : commands) { + byte[] ba = command.getIoBuffer().array(); + System.arraycopy(ba, 0, buf, offset, ba.length); + offset += ba.length; + if (command != lastCommand + && (!command.isNoreply() || command instanceof BaseBinaryCommand)) { + executingCmds.add(command); + } + } + lastCommand.setIoBuffer(IoBuffer.wrap(buf)); + } + return lastCommand; + } + + private final ThreadLocal> threadLocal = new ThreadLocal>() { + + @Override + protected List initialValue() { + return new ArrayList(Optimizer.this.mergeFactor); + } + }; + + public final List getLocalList() { + List list = this.threadLocal.get(); + list.clear(); + return list; + } + + static interface CommandCollector { + public Object getResult(); + + public void visit(Command command); + + public void finish(); + + public CommandCollector reset(); + } + + static class KeyStringCollector implements CommandCollector { + char[] buf = new char[1024 * 2]; + int count = 0; + boolean wasFirst = true; + + public CommandCollector reset() { + this.count = 0; + this.wasFirst = true; + return this; + } + + public Object getResult() { + return new String(this.buf, 0, this.count); + } + + public void visit(final Command command) { + if (this.wasFirst) { + append(command.getKey()); + this.wasFirst = false; + } else { + append(" "); + append(command.getKey()); + } + } + + private void expandCapacity(final int minimumCapacity) { + int newCapacity = (this.buf.length + 1) * 2; + if (newCapacity < 0) { + newCapacity = Integer.MAX_VALUE; + } else if (minimumCapacity > newCapacity) { + newCapacity = minimumCapacity; + } + char[] copy = new char[newCapacity]; + System.arraycopy(this.buf, 0, copy, 0, Math.min(this.buf.length, newCapacity)); + this.buf = copy; + } + + private void append(final String str) { + int len = str.length(); + if (len == 0) { + return; + } + int newCount = this.count + len; + if (newCount > this.buf.length) { + expandCapacity(newCount); + } + str.getChars(0, len, this.buf, this.count); + this.count = newCount; + } + + public void finish() { + // do nothing + + } + + } + + private static class BinarySetQCollector implements CommandCollector { + ArrayList bufferList = new ArrayList(); + int totalBytes; + BinaryStoreCommand prevCommand; + Map mergeCommands; + + public CommandCollector reset() { + this.bufferList.clear(); + this.totalBytes = 0; + this.prevCommand = null; + this.mergeCommands = null; + return this; + } + + public Object getResult() { + byte[] buf = new byte[this.totalBytes]; + int offset = 0; + for (IoBuffer buffer : this.bufferList) { + byte[] ba = buffer.array(); + System.arraycopy(ba, 0, buf, offset, ba.length); + offset += ba.length; + } + BinarySetMultiCommand resultCommand = + new BinarySetMultiCommand(null, CommandType.SET_MANY, new CountDownLatch(1)); + resultCommand.setIoBuffer(IoBuffer.wrap(buf)); + resultCommand.setMergeCommands(this.mergeCommands); + resultCommand.setMergeCount(this.mergeCommands.size()); + return resultCommand; + } + + public void visit(final Command command) { + + // Encode prev command + if (this.prevCommand != null) { + // first n-1 send setq command + BinaryStoreCommand setqCmd = + new BinaryStoreCommand(this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), + CommandType.SET, null, this.prevCommand.getExpTime(), this.prevCommand.getCas(), + // set noreply to be true + this.prevCommand.getValue(), true, this.prevCommand.getTranscoder()); + // We must set the opaque to get error message. + int opaque = OpaqueGenerater.getInstance().getNextValue(); + setqCmd.setOpaque(opaque); + setqCmd.encode(); + this.totalBytes += setqCmd.getIoBuffer().remaining(); + + this.bufferList.add(setqCmd.getIoBuffer()); + // GC friendly + setqCmd.setIoBuffer(MemcachedHandler.EMPTY_BUF); + setqCmd.setValue(null); + this.prevCommand.setValue(null); + this.prevCommand.setIoBuffer(MemcachedHandler.EMPTY_BUF); + if (this.mergeCommands == null) { + this.mergeCommands = new HashMap(); + } + this.mergeCommands.put(opaque, this.prevCommand); + } + this.prevCommand = (BinaryStoreCommand) command; + } + + public void finish() { + if (this.mergeCommands == null) { + return; + } + // prevCommand is the last command,last command must be a SET + // command,ensure + // previous SETQ commands sending response back + BinaryStoreCommand setqCmd = + new BinaryStoreCommand(this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), + CommandType.SET, null, this.prevCommand.getExpTime(), this.prevCommand.getCas(), + // set noreply to be false. + this.prevCommand.getValue(), false, this.prevCommand.getTranscoder()); + // We must set the opaque to get error message. + int opaque = OpaqueGenerater.getInstance().getNextValue(); + setqCmd.setOpaque(opaque); + setqCmd.encode(); + this.bufferList.add(setqCmd.getIoBuffer()); + this.totalBytes += setqCmd.getIoBuffer().remaining(); + if (this.mergeCommands != null) { + this.mergeCommands.put(opaque, this.prevCommand); + } + } + + } + + private static class BinaryGetQCollector implements CommandCollector { + ArrayList bufferList = new ArrayList(50); + int totalBytes; + Command prevCommand; + + public CommandCollector reset() { + this.bufferList.clear(); + this.totalBytes = 0; + this.prevCommand = null; + return this; + } + + public Object getResult() { + byte[] buf = new byte[this.totalBytes]; + int offset = 0; + for (IoBuffer buffer : this.bufferList) { + byte[] ba = buffer.array(); + System.arraycopy(ba, 0, buf, offset, ba.length); + offset += ba.length; + } + BinaryGetMultiCommand resultCommand = + new BinaryGetMultiCommand(null, CommandType.GET_MANY, new CountDownLatch(1)); + resultCommand.setIoBuffer(IoBuffer.wrap(buf)); + return resultCommand; + } + + public void visit(final Command command) { + // Encode prev command + if (this.prevCommand != null) { + // first n-1 send getq command + Command getqCommand = new BinaryGetCommand(this.prevCommand.getKey(), + this.prevCommand.getKeyBytes(), null, null, OpCode.GET_KEY_QUIETLY, true); + getqCommand.encode(); + this.totalBytes += getqCommand.getIoBuffer().remaining(); + this.bufferList.add(getqCommand.getIoBuffer()); + } + this.prevCommand = command; + } + + public void finish() { + // prev command is the last command,last command must be getk,ensure + // getq commands sending response back + Command lastGetKCommand = + new BinaryGetCommand(this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), + CommandType.GET_ONE, new CountDownLatch(1), OpCode.GET_KEY, false); + lastGetKCommand.encode(); + this.bufferList.add(lastGetKCommand.getIoBuffer()); + this.totalBytes += lastGetKCommand.getIoBuffer().remaining(); + } + + } + + @SuppressWarnings("unchecked") + private final Command mergeGetCommands(final Command currentCmd, final Queue writeQueue, + final Queue executingCmds, final CommandType expectedCommandType) { + Map mergeCommands = null; + int mergeCount = 1; + final CommandCollector commandCollector = createGetCommandCollector(); + currentCmd.setStatus(OperationStatus.WRITING); + + commandCollector.visit(currentCmd); + while (mergeCount < this.mergeFactor) { + Command nextCmd = (Command) writeQueue.peek(); + if (nextCmd == null) { + break; + } + if (nextCmd.isCancel()) { + writeQueue.remove(); + continue; + } + if (nextCmd.getCommandType() == expectedCommandType) { + if (mergeCommands == null) { // lazy initialize + mergeCommands = new HashMap(this.mergeFactor / 2); + mergeCommands.put(currentCmd.getKey(), currentCmd); + } + if (log.isDebugEnabled()) { + log.debug("Merge get command:" + nextCmd.toString()); + } + nextCmd.setStatus(OperationStatus.WRITING); + Command removedCommand = (Command) writeQueue.remove(); + // If the key is exists,add the command to associated list. + if (mergeCommands.containsKey(removedCommand.getKey())) { + final AssocCommandAware mergedGetCommand = + (AssocCommandAware) mergeCommands.get(removedCommand.getKey()); + List assocCommands = mergedGetCommand.getAssocCommands(); + if (assocCommands == null) { + assocCommands = new ArrayList(5); + mergedGetCommand.setAssocCommands(assocCommands); + } + assocCommands.add(removedCommand); + } else { + commandCollector.visit(nextCmd); + mergeCommands.put(removedCommand.getKey(), removedCommand); + } + mergeCount++; + } else { + break; + } + } + if (mergeCount == 1) { + return currentCmd; + } else { + commandCollector.finish(); + if (log.isDebugEnabled()) { + log.debug("Merge optimieze:merge " + mergeCount + " get commands"); + } + return newMergedCommand(mergeCommands, mergeCount, commandCollector, expectedCommandType); + } + } + + private static final ThreadLocal BIN_SET_CMD_COLLECTOR_THREAD_LOCAL = + new ThreadLocal() { + + @Override + protected BinarySetQCollector initialValue() { + return new BinarySetQCollector(); + } + + }; + + private final Command mergeSetCommands(final Command currentCmd, final Queue writeQueue, + final Queue executingCmds, final CommandType expectedCommandType, + final int sendBufferSize) { + int mergeCount = 1; + final CommandCollector commandCollector = BIN_SET_CMD_COLLECTOR_THREAD_LOCAL.get().reset(); + currentCmd.setStatus(OperationStatus.WRITING); + int totalBytes = currentCmd.getIoBuffer().remaining(); + commandCollector.visit(currentCmd); + while (mergeCount < this.mergeFactor && totalBytes + MARGIN < sendBufferSize) { + Command nextCmd = (Command) writeQueue.peek(); + if (nextCmd == null) { + break; + } + if (nextCmd.isCancel()) { + writeQueue.remove(); + continue; + } + if (nextCmd.getCommandType() == expectedCommandType && !nextCmd.isNoreply()) { + if (log.isDebugEnabled()) { + log.debug("Merge set command:" + nextCmd.toString()); + } + nextCmd.setStatus(OperationStatus.WRITING); + writeQueue.remove(); + + commandCollector.visit(nextCmd); + + mergeCount++; + } else { + break; + } + totalBytes += nextCmd.getIoBuffer().remaining(); + } + if (mergeCount == 1) { + return currentCmd; + } else { + commandCollector.finish(); + if (log.isDebugEnabled()) { + log.debug("Merge optimieze:merge " + mergeCount + " get commands"); + } + return (Command) commandCollector.getResult(); + } + } + + private static ThreadLocal TEXT_GET_CMD_COLLECTOR_THREAD_LOCAL = + new ThreadLocal() { + @Override + public KeyStringCollector initialValue() { + return new KeyStringCollector(); + } + }; + + private static ThreadLocal BIN_GET_CMD_COLLECTOR_THREAD_LOCAL = + new ThreadLocal() { + @Override + public BinaryGetQCollector initialValue() { + return new BinaryGetQCollector(); + } + }; + + private CommandCollector createGetCommandCollector() { + switch (this.protocol) { + case Binary: + return BIN_GET_CMD_COLLECTOR_THREAD_LOCAL.get().reset(); + default: + return TEXT_GET_CMD_COLLECTOR_THREAD_LOCAL.get().reset(); + } + } + + private Command newMergedCommand(final Map mergeCommands, final int mergeCount, + final CommandCollector commandCollector, final CommandType commandType) { + if (this.protocol == Protocol.Text) { + String resultKey = (String) commandCollector.getResult(); + + byte[] keyBytes = ByteUtils.getBytes(resultKey); + byte[] cmdBytes = commandType == CommandType.GET_ONE ? Constants.GET : Constants.GETS; + final byte[] buf = new byte[cmdBytes.length + 3 + keyBytes.length]; + ByteUtils.setArguments(buf, 0, cmdBytes, keyBytes); + TextGetOneCommand cmd = new TextGetOneCommand(resultKey, keyBytes, commandType, null); + cmd.setMergeCommands(mergeCommands); + cmd.setWriteFuture(new FutureImpl()); + cmd.setMergeCount(mergeCount); + cmd.setIoBuffer(IoBuffer.wrap(buf)); + return cmd; + } else { + BinaryGetMultiCommand result = (BinaryGetMultiCommand) commandCollector.getResult(); + result.setMergeCount(mergeCount); + result.setMergeCommands(mergeCommands); + return result; + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/OptimizerMBean.java b/src/main/java/net/rubyeye/xmemcached/impl/OptimizerMBean.java new file mode 100644 index 0000000..0f168c1 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/OptimizerMBean.java @@ -0,0 +1,38 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +/** + * OptimizerMBean,used for changing the optimizer's factor + * + * @author dennis + * + */ +public interface OptimizerMBean { + public int getMergeFactor(); + + public boolean isOptimizeGet(); + + public boolean isOptimizeMergeBuffer(); + + public void setMergeFactor(int mergeFactor); + + public void setOptimizeGet(boolean optimiezeGet); + + public void setOptimizeMergeBuffer(boolean optimiezeMergeBuffer); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/PHPMemcacheSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/PHPMemcacheSessionLocator.java new file mode 100644 index 0000000..e666898 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/PHPMemcacheSessionLocator.java @@ -0,0 +1,94 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import com.google.code.yanf4j.core.Session; + +/** + * Session locator base on hash(key) mod sessions.size(). Uses the PHP memcached hash strategy so + * it's easier to share data with PHP based clients. + * + * @author aravind + * + */ +public class PHPMemcacheSessionLocator extends AbstractMemcachedSessionLocator { + + private HashAlgorithm hashAlgorithm; + private transient volatile List sessions; + + public PHPMemcacheSessionLocator() { + this.hashAlgorithm = HashAlgorithm.NATIVE_HASH; + } + + public PHPMemcacheSessionLocator(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + public final void setHashAlgorighm(HashAlgorithm hashAlgorithm) { + this.hashAlgorithm = hashAlgorithm; + } + + public final long getHash(int size, String key) { + long hash = this.hashAlgorithm.hash(key); + hash = (hash >> 16) & 0x7fff; + return hash % size; + } + + public final Session getSessionByKey(final String key) { + if (this.sessions == null || this.sessions.size() == 0) { + return null; + } + // Copy on read + List sessionList = this.sessions; + int size = sessionList.size(); + if (size == 0) { + return null; + } + long start = this.getHash(size, key); + Session session = sessionList.get((int) start); + // If it is not failure mode,get next available session + if (!this.failureMode && (session == null || session.isClosed())) { + long next = this.getNext(size, start); + while ((session == null || session.isClosed()) && next != start) { + session = sessionList.get((int) next); + next = this.getNext(size, next); + } + } + return session; + } + + public final long getNext(int size, long start) { + if (start == size - 1) { + return 0; + } else { + return start + 1; + } + } + + public final void updateSessions(final Collection list) { + Collection copySessions = list; + List newSessions = new ArrayList(copySessions.size() * 2); + for (Session session : copySessions) { + if (session instanceof MemcachedTCPSession) { + int weight = ((MemcachedSession) session).getWeight(); + for (int i = 0; i < weight; i++) { + newSessions.add(session); + } + } else { + newSessions.add(session); + } + } + this.sessions = newSessions; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/RandomMemcachedSessionLocaltor.java b/src/main/java/net/rubyeye/xmemcached/impl/RandomMemcachedSessionLocaltor.java new file mode 100644 index 0000000..bfe55ac --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/RandomMemcachedSessionLocaltor.java @@ -0,0 +1,37 @@ +package net.rubyeye.xmemcached.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.MemcachedSessionLocator; + +/** + * A random session locator,it can be used in kestrel. + * + * @author dennis + * + */ +public class RandomMemcachedSessionLocaltor implements MemcachedSessionLocator { + private transient volatile List sessions = Collections.emptyList(); + private final Random rand = new Random(); + + public Session getSessionByKey(String key) { + List copiedOnWrite = sessions; + if (copiedOnWrite == null || copiedOnWrite.isEmpty()) + return null; + return copiedOnWrite.get(rand.nextInt(copiedOnWrite.size())); + } + + public void updateSessions(Collection list) { + this.sessions = new ArrayList(list); + + } + + public void setFailureMode(boolean failureMode) { + // ignore + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/ReconnectRequest.java b/src/main/java/net/rubyeye/xmemcached/impl/ReconnectRequest.java new file mode 100644 index 0000000..adb806d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/ReconnectRequest.java @@ -0,0 +1,105 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.impl; + +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; + +/** + * A auto reconnect request,associating a socket address for reconnecting + * + * @author dennis + * + */ +public final class ReconnectRequest implements Delayed { + + private InetSocketAddressWrapper inetSocketAddressWrapper; + private int tries; + + private static final long MIN_RECONNECT_INTERVAL = 1000; + + private static final long MAX_RECONNECT_INTERVAL = 60 * 1000; + + private volatile long nextReconnectTimestamp; + + public ReconnectRequest(InetSocketAddressWrapper inetSocketAddressWrapper, int tries, + long reconnectInterval) { + super(); + this.setInetSocketAddressWrapper(inetSocketAddressWrapper); + this.setTries(tries); // record reconnect times + reconnectInterval = this.normalInterval(reconnectInterval); + this.nextReconnectTimestamp = System.currentTimeMillis() + reconnectInterval; + } + + private long normalInterval(long reconnectInterval) { + if (reconnectInterval < MIN_RECONNECT_INTERVAL) { + reconnectInterval = MIN_RECONNECT_INTERVAL; + } + if (reconnectInterval > MAX_RECONNECT_INTERVAL) { + reconnectInterval = MAX_RECONNECT_INTERVAL; + } + return reconnectInterval; + } + + public long getDelay(TimeUnit unit) { + return unit.convert(this.nextReconnectTimestamp - System.currentTimeMillis(), + TimeUnit.MILLISECONDS); + } + + public int compareTo(Delayed o) { + ReconnectRequest other = (ReconnectRequest) o; + if (this.nextReconnectTimestamp > other.nextReconnectTimestamp) { + return 1; + } else { + return -1; + } + } + + /** + * Returns a reconnect socket address wrapper + * + * @see InetSocketAddressWrapper + * @return + */ + public final InetSocketAddressWrapper getInetSocketAddressWrapper() { + return this.inetSocketAddressWrapper; + } + + public void updateNextReconnectTimeStamp(long interval) { + interval = this.normalInterval(interval); + this.nextReconnectTimestamp = System.currentTimeMillis() + interval; + } + + public final void setInetSocketAddressWrapper(InetSocketAddressWrapper inetSocketAddressWrapper) { + this.inetSocketAddressWrapper = inetSocketAddressWrapper; + } + + public final void setTries(int tries) { + this.tries = tries; + } + + /** + * Returns retry times + * + * @return retry times,it is zero if it does not retry to connect + */ + public final int getTries() { + return this.tries; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/RoundRobinMemcachedSessionLocator.java b/src/main/java/net/rubyeye/xmemcached/impl/RoundRobinMemcachedSessionLocator.java new file mode 100644 index 0000000..fce0fb3 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/RoundRobinMemcachedSessionLocator.java @@ -0,0 +1,51 @@ +package net.rubyeye.xmemcached.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.networking.MemcachedSession; + +/** + * A round-robin session locator for some special applications,memcacheq or kestrel etc.They doesn't + * need the same key must always to be stored in same memcached but want to make a cluster. + * + * @author apple + * + */ +public class RoundRobinMemcachedSessionLocator implements MemcachedSessionLocator { + private transient volatile List sessions; + private AtomicInteger sets = new AtomicInteger(0); + + public Session getSessionByKey(String key) { + List copyList = this.sessions; + if (copyList == null || copyList.isEmpty()) + return null; + int size = copyList.size(); + return copyList.get(Math.abs(sets.getAndIncrement()) % size); + } + + public final void updateSessions(final Collection list) { + Collection copySessions = list; + List newSessions = new ArrayList(copySessions.size() * 2); + for (Session session : copySessions) { + if (session instanceof MemcachedTCPSession) { + int weight = ((MemcachedSession) session).getWeight(); + for (int i = 0; i < weight; i++) { + newSessions.add(session); + } + } else { + newSessions.add(session); + } + } + this.sessions = newSessions; + } + + public void setFailureMode(boolean failureMode) { + // ignore + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/impl/package.html b/src/main/java/net/rubyeye/xmemcached/impl/package.html new file mode 100644 index 0000000..a0ca93d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/impl/package.html @@ -0,0 +1,10 @@ + + + + + Memcached Implementation + + +

Manage tcp connection,memcached protocol optimized,and some MBeans for monitor.

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/monitor/Constants.java b/src/main/java/net/rubyeye/xmemcached/monitor/Constants.java new file mode 100644 index 0000000..a617c4a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/monitor/Constants.java @@ -0,0 +1,64 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.monitor; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Constants + * + * @author dennis + * + */ +public class Constants { + /** + * Whether to enable client statisitics + */ + public static final String XMEMCACHED_STATISTICS_ENABLE = "xmemcached.statistics.enable"; + /** + * JMX RMI service name + * + */ + public static final String XMEMCACHED_RMI_NAME = "xmemcached.rmi.name"; + /** + * JMX RMI port + * + * + */ + public static final String XMEMCACHED_RMI_PORT = "xmemcached.rmi.port"; + /** + * Whether to enable jmx supports + */ + public static final String XMEMCACHED_JMX_ENABLE = "xmemcached.jmx.enable"; + public static final byte[] CRLF = {'\r', '\n'}; + public static final byte[] GET = {'g', 'e', 't'}; + public static final byte[] GETS = {'g', 'e', 't', 's'}; + public static final byte SPACE = ' '; + public static final byte[] INCR = {'i', 'n', 'c', 'r'}; + public static final byte[] DECR = {'d', 'e', 'c', 'r'}; + public static final byte[] DELETE = {'d', 'e', 'l', 'e', 't', 'e'}; + public static final byte[] TOUCH = {'t', 'o', 'u', 'c', 'h'}; + /** + * Max session read buffer size,758k + */ + public static final int MAX_SESSION_READ_BUFFER_SIZE = 768 * 1024; + public static final byte[] NO_REPLY = {'n', 'o', 'r', 'e', 'p', 'l', 'y'}; + /** + * Client instance counter + */ + public static final AtomicInteger MEMCACHED_CLIENT_COUNTER = new AtomicInteger(0); +} diff --git a/src/main/java/net/rubyeye/xmemcached/monitor/MemcachedClientNameHolder.java b/src/main/java/net/rubyeye/xmemcached/monitor/MemcachedClientNameHolder.java new file mode 100644 index 0000000..3cd005b --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/monitor/MemcachedClientNameHolder.java @@ -0,0 +1,24 @@ +package net.rubyeye.xmemcached.monitor; + +/** + * MemcachedClient insntance name holder + * + * @author dennis + * + */ +public class MemcachedClientNameHolder { + private static ThreadLocal cacheName = new ThreadLocal(); + + public static void setName(String name) { + cacheName.set(name); + } + + public static String getName() { + return cacheName.get(); + } + + public static void clear() { + cacheName.remove(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/monitor/StatisticsHandler.java b/src/main/java/net/rubyeye/xmemcached/monitor/StatisticsHandler.java new file mode 100644 index 0000000..c47a75f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/monitor/StatisticsHandler.java @@ -0,0 +1,144 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.monitor; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import net.rubyeye.xmemcached.command.CommandType; + +/** + * Statistics helper + * + * @author dennis + * + */ +public class StatisticsHandler implements StatisticsHandlerMBean { + private Map counterMap = new HashMap(); + + public StatisticsHandler() { + buildCounterMap(); + XMemcachedMbeanServer.getInstance().registMBean(this, this.getClass().getPackage().getName() + + ":type=" + this.getClass().getSimpleName() + "-" + MemcachedClientNameHolder.getName()); + } + + private boolean statistics = + Boolean.valueOf(System.getProperty(Constants.XMEMCACHED_STATISTICS_ENABLE, "false")); + + private void buildCounterMap() { + if (this.statistics) { + Map map = new HashMap(); + map.put(CommandType.APPEND, new AtomicLong()); + map.put(CommandType.SET, new AtomicLong()); + map.put(CommandType.SET_MANY, new AtomicLong()); + map.put(CommandType.PREPEND, new AtomicLong()); + map.put(CommandType.CAS, new AtomicLong()); + map.put(CommandType.ADD, new AtomicLong()); + map.put(CommandType.REPLACE, new AtomicLong()); + map.put(CommandType.DELETE, new AtomicLong()); + map.put(CommandType.INCR, new AtomicLong()); + map.put(CommandType.DECR, new AtomicLong()); + map.put(CommandType.GET_HIT, new AtomicLong()); + map.put(CommandType.GET_MISS, new AtomicLong()); + map.put(CommandType.GET_MANY, new AtomicLong()); + map.put(CommandType.GETS_MANY, new AtomicLong()); + this.counterMap = map; + } + } + + public final boolean isStatistics() { + return this.statistics; + } + + public final void statistics(CommandType cmdType) { + if (this.statistics && this.counterMap.get(cmdType) != null) { + this.counterMap.get(cmdType).incrementAndGet(); + } + } + + public final void statistics(CommandType cmdType, int count) { + if (this.statistics && this.counterMap.get(cmdType) != null) { + this.counterMap.get(cmdType).addAndGet(count); + } + } + + public final void setStatistics(boolean statistics) { + this.statistics = statistics; + buildCounterMap(); + + } + + public void resetStats() { + if (this.statistics) { + buildCounterMap(); + } + } + + public long getAppendCount() { + return this.counterMap.get(CommandType.APPEND).get(); + } + + public long getCASCount() { + return this.counterMap.get(CommandType.CAS).get(); + } + + public long getDecrCount() { + return this.counterMap.get(CommandType.DECR).get(); + } + + public long getDeleteCount() { + return this.counterMap.get(CommandType.DELETE).get(); + } + + public long getGetHitCount() { + return this.counterMap.get(CommandType.GET_HIT).get(); + } + + public long getGetMissCount() { + return this.counterMap.get(CommandType.GET_MISS).get(); + } + + public long getIncrCount() { + return this.counterMap.get(CommandType.INCR).get(); + } + + public long getMultiGetCount() { + return this.counterMap.get(CommandType.GET_MANY).get(); + } + + public long getMultiGetsCount() { + return this.counterMap.get(CommandType.GETS_MANY).get(); + } + + public long getPrependCount() { + return this.counterMap.get(CommandType.PREPEND).get(); + } + + public long getSetCount() { + return this.counterMap.get(CommandType.SET).get() + + this.counterMap.get(CommandType.SET_MANY).get(); + } + + public long getAddCount() { + return this.counterMap.get(CommandType.ADD).get(); + } + + public long getReplaceCount() { + return this.counterMap.get(CommandType.REPLACE).get(); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/monitor/StatisticsHandlerMBean.java b/src/main/java/net/rubyeye/xmemcached/monitor/StatisticsHandlerMBean.java new file mode 100644 index 0000000..f77ac45 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/monitor/StatisticsHandlerMBean.java @@ -0,0 +1,63 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.monitor; + +/** + * Statistics MBean for jmx + * + * @author dennis + * + */ +public interface StatisticsHandlerMBean { + public long getGetHitCount(); + + public long getGetMissCount(); + + public long getSetCount(); + + public long getAppendCount(); + + public long getPrependCount(); + + public long getCASCount(); + + public long getDeleteCount(); + + public long getIncrCount(); + + public long getDecrCount(); + + public long getMultiGetCount(); + + public long getMultiGetsCount(); + + public long getAddCount(); + + public long getReplaceCount(); + + public boolean isStatistics(); + + public void setStatistics(boolean statistics); + + /** + * Reset the statistics + * + * @since 1.3.9 + */ + public void resetStats(); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/monitor/XMemcachedMbeanServer.java b/src/main/java/net/rubyeye/xmemcached/monitor/XMemcachedMbeanServer.java new file mode 100644 index 0000000..a958a61 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/monitor/XMemcachedMbeanServer.java @@ -0,0 +1,178 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.monitor; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXConnectorServerFactory; +import javax.management.remote.JMXServiceURL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Enable JMX supports,default is false:
+ * + *   java -Dxmemcached.jmx.enable=true -Dxmemcached.rmi.port=7077 + * -Dxmemcached.rmi.name=xmemcachedServer
+ * + * Access MBean through:
+ * + *   service:jmx:rmi:///jndi/rmi://[host]:7077/xmemcachedServer
+ * + * You can add or remove memcached server dynamically and monitor XmemcachedClient?'s behavior + * through MBeans.Other options:
+ *
    + *
  • -Dxmemcached.rmi.port
  • + *
  • -Dxmemcached.rmi.name
  • + *
+ * + * @author dennis + * + */ +public final class XMemcachedMbeanServer { + private static final Logger log = LoggerFactory.getLogger(XMemcachedMbeanServer.class); + + private MBeanServer mbserver = null; + + private static XMemcachedMbeanServer instance = new XMemcachedMbeanServer(); + private JMXConnectorServer connectorServer; + + private Thread shutdownHookThread; + private volatile boolean isHutdownHookCalled = false; + + private XMemcachedMbeanServer() { + initialize(); + } + + private void initialize() { + if (mbserver != null && connectorServer != null && connectorServer.isActive()) { + return; + } + try { + boolean enableJMX = + Boolean.parseBoolean(System.getProperty(Constants.XMEMCACHED_JMX_ENABLE, "false")); + if (enableJMX) { + // 鍒涘缓MBServer + String hostName = null; + try { + InetAddress addr = InetAddress.getLocalHost(); + + hostName = addr.getHostName(); + } catch (IOException e) { + log.error("Get HostName Error", e); + hostName = "localhost"; + } + String host = System.getProperty("hostName", hostName); + mbserver = ManagementFactory.getPlatformMBeanServer(); + int port = Integer.parseInt(System.getProperty(Constants.XMEMCACHED_RMI_PORT, "7077")); + String rmiName = System.getProperty(Constants.XMEMCACHED_RMI_NAME, "xmemcachedServer"); + Registry registry = null; + try { + registry = LocateRegistry.getRegistry(port); + registry.list(); + } catch (Exception e) { + registry = null; + } + if (null == registry) { + registry = LocateRegistry.createRegistry(port); + } + registry.list(); + String serverURL = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/" + rmiName; + JMXServiceURL url = new JMXServiceURL(serverURL); + connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mbserver); + connectorServer.start(); + shutdownHookThread = new Thread() { + @Override + public void run() { + try { + isHutdownHookCalled = true; + if (connectorServer.isActive()) { + connectorServer.stop(); + log.warn("JMXConnector stop"); + } + } catch (IOException e) { + log.error("Shutdown Xmemcached MBean server error", e); + } + } + }; + + Runtime.getRuntime().addShutdownHook(shutdownHookThread); + log.warn("jmx url: " + serverURL); + } + } catch (Exception e) { + log.error("create MBServer error", e); + } + } + + public static XMemcachedMbeanServer getInstance() { + return instance; + } + + public final void shutdown() { + try { + if (connectorServer != null && connectorServer.isActive()) { + connectorServer.stop(); + log.warn("JMXConnector stop"); + if (!isHutdownHookCalled) { + Runtime.getRuntime().removeShutdownHook(shutdownHookThread); + } + } + } catch (IOException e) { + log.error("Shutdown Xmemcached MBean server error", e); + } + } + + public boolean isRegistered(String name) { + try { + return mbserver != null && mbserver.isRegistered(new ObjectName(name)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean isActive() { + return mbserver != null && connectorServer != null && connectorServer.isActive(); + } + + public int getMBeanCount() { + if (mbserver != null) { + return mbserver.getMBeanCount(); + } else { + return 0; + } + } + + public void registMBean(Object o, String name) { + if (isRegistered(name)) { + return; + } + // 娉ㄥ唽MBean + if (mbserver != null) { + try { + mbserver.registerMBean(o, new ObjectName(name)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/monitor/package.html b/src/main/java/net/rubyeye/xmemcached/monitor/package.html new file mode 100644 index 0000000..465c562 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/monitor/package.html @@ -0,0 +1,10 @@ + + + + + XMemcached MBeans + + +

JMX MBean Server and MBeans for monitor and statistics

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/networking/ClosedMemcachedSession.java b/src/main/java/net/rubyeye/xmemcached/networking/ClosedMemcachedSession.java new file mode 100644 index 0000000..5fe76e2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/networking/ClosedMemcachedSession.java @@ -0,0 +1,20 @@ +package net.rubyeye.xmemcached.networking; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; + +public interface ClosedMemcachedSession extends Session { + + public void setAllowReconnect(boolean allow); + + public boolean isAllowReconnect(); + + public InetSocketAddressWrapper getInetSocketAddressWrapper(); + + @Deprecated + public int getWeight(); + + @Deprecated + public int getOrder(); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/networking/Connector.java b/src/main/java/net/rubyeye/xmemcached/networking/Connector.java new file mode 100644 index 0000000..aea468a --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/networking/Connector.java @@ -0,0 +1,101 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.networking; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Future; +import net.rubyeye.xmemcached.FlowControl; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.impl.ReconnectRequest; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import com.google.code.yanf4j.core.Controller; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.SocketOption; + +/** + * Connector which is used to connect to memcached server. + * + * @author dennis + * + */ +public interface Connector extends Controller { + public void setOptimizeMergeBuffer(boolean optimiezeMergeBuffer); + + public void setMergeFactor(int factor); + + public void setOptimizeGet(boolean optimizeGet); + + public void removeSession(Session session); + + public Queue getSessionByAddress(InetSocketAddress address); + + public List getStandbySessionListByMainNodeAddr(InetSocketAddress address); + + public Set getSessionSet(); + + public void setHealSessionInterval(long interval); + + public long getHealSessionInterval(); + + public Session send(Command packet) throws MemcachedException; + + public void setConnectionPoolSize(int connectionPoolSize); + + public void setBufferAllocator(BufferAllocator bufferAllocator); + + public void removeReconnectRequest(InetSocketAddress address); + + public void setEnableHealSession(boolean enableHealSession); + + public void addToWatingQueue(ReconnectRequest request); + + @SuppressWarnings("unchecked") + public void setSocketOptions(Map options); + + public Future connect(InetSocketAddressWrapper addressWrapper) throws IOException; + + public void updateSessions(); + + public void setSessionLocator(MemcachedSessionLocator sessionLocator); + + public void setSessionComparator(MemcachedSessionComparator sessionComparator); + + /** + * Make all connection sending a quit command to memcached + */ + public void quitAllSessions(); + + public Queue getReconnectRequestQueue(); + + public void setFailureMode(boolean failureMode); + + /** + * Returns the noreply operations flow control manager. + * + * @return + */ + public FlowControl getNoReplyOpsFlowControl(); +} diff --git a/src/main/java/net/rubyeye/xmemcached/networking/MemcachedSession.java b/src/main/java/net/rubyeye/xmemcached/networking/MemcachedSession.java new file mode 100644 index 0000000..d71b4ea --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/networking/MemcachedSession.java @@ -0,0 +1,38 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.networking; + +import net.rubyeye.xmemcached.buffer.BufferAllocator; + +/** + * Abstract interface for memcached connection. + * + * @author dennis + * + */ +public interface MemcachedSession extends ClosedMemcachedSession { + + public void setBufferAllocator(BufferAllocator allocator); + + public void destroy(); + + public void quit(); + + public boolean isAuthFailed(); + + public void setAuthFailed(boolean authFailed); +} diff --git a/src/main/java/net/rubyeye/xmemcached/networking/MemcachedSessionConnectListener.java b/src/main/java/net/rubyeye/xmemcached/networking/MemcachedSessionConnectListener.java new file mode 100644 index 0000000..daeba08 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/networking/MemcachedSessionConnectListener.java @@ -0,0 +1,14 @@ +package net.rubyeye.xmemcached.networking; + +import net.rubyeye.xmemcached.MemcachedClient; + +/** + * + * @author dennis + * + */ +public interface MemcachedSessionConnectListener { + + public void onConnect(MemcachedSession session, MemcachedClient client); + +} diff --git a/src/main/java/net/rubyeye/xmemcached/networking/package.html b/src/main/java/net/rubyeye/xmemcached/networking/package.html new file mode 100644 index 0000000..8d4a69f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/networking/package.html @@ -0,0 +1,10 @@ + + + + + XMemcached Networking layout + + +

Networking layout to talk with memcached.

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/package.html b/src/main/java/net/rubyeye/xmemcached/package.html new file mode 100644 index 0000000..97732c2 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/package.html @@ -0,0 +1,10 @@ + + + + + XMemcached Main Class and Interface + + +

XMemcached's main classes and interfaces,use these classes/interfaces to interact with memcached servers.

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/BaseSerializingTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/BaseSerializingTranscoder.java new file mode 100644 index 0000000..f76ca28 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/BaseSerializingTranscoder.java @@ -0,0 +1,328 @@ +package net.rubyeye.xmemcached.transcoders; + +import java.io.*; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.InflaterInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base class for any transcoders that may want to work with serialized or compressed data. + */ +public abstract class BaseSerializingTranscoder { + + private static final class XmcObjectInputStream extends ObjectInputStream { + private XmcObjectInputStream(InputStream in) throws IOException { + super(in); + } + + @Override + protected Class resolveClass(ObjectStreamClass desc) + throws IOException, ClassNotFoundException { + try { + // When class is not found,try to load it from context class + // loader. + return super.resolveClass(desc); + } catch (ClassNotFoundException e) { + return Thread.currentThread().getContextClassLoader().loadClass(desc.getName()); + } + } + } + + /** + * Default compression threshold value. + */ + public static final int DEFAULT_COMPRESSION_THRESHOLD = 16384; + + public static final String DEFAULT_CHARSET = "UTF-8"; + + protected int compressionThreshold = DEFAULT_COMPRESSION_THRESHOLD; + protected String charset = DEFAULT_CHARSET; + protected CompressionMode compressMode = CompressionMode.GZIP; + protected static final Logger log = LoggerFactory.getLogger(BaseSerializingTranscoder.class); + + /** + * Set the compression threshold to the given number of bytes. This transcoder will attempt to + * compress any data being stored that's larger than this. + * + * @param to the number of bytes + */ + public void setCompressionThreshold(int to) { + this.compressionThreshold = to; + } + + public CompressionMode getCompressMode() { + return compressMode; + } + + public void setCompressionMode(CompressionMode compressMode) { + this.compressMode = compressMode; + } + + /** + * Set the character set for string value transcoding (defaults to UTF-8). + */ + public void setCharset(String to) { + // Validate the character set. + try { + new String(new byte[97], to); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + this.charset = to; + } + + /** + * Get the bytes representing the given serialized object. + */ + protected byte[] serialize(Object o) { + if (o == null) { + throw new NullPointerException("Can't serialize null"); + } + byte[] rv = null; + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream os = new ObjectOutputStream(bos); + os.writeObject(o); + os.close(); + bos.close(); + rv = bos.toByteArray(); + } catch (IOException e) { + throw new IllegalArgumentException("Non-serializable object", e); + } + return rv; + } + + /** + * Get the object represented by the given serialized bytes. + */ + protected Object deserialize(byte[] in) { + Object rv = null; + ByteArrayInputStream bis = null; + ObjectInputStream is = null; + try { + if (in != null) { + bis = new ByteArrayInputStream(in); + is = new XmcObjectInputStream(bis); + rv = is.readObject(); + + } + } catch (IOException e) { + log.error("Caught IOException decoding " + in.length + " bytes of data", e); + } catch (ClassNotFoundException e) { + log.error("Caught CNFE decoding " + in.length + " bytes of data", e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // ignore + } + } + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + // ignore + } + } + } + return rv; + } + + /** + * Compress the given array of bytes. + */ + public final byte[] compress(byte[] in) { + switch (this.compressMode) { + case GZIP: + return gzipCompress(in); + case ZIP: + return zipCompress(in); + default: + return gzipCompress(in); + } + + } + + private byte[] zipCompress(byte[] in) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(in.length); + DeflaterOutputStream os = new DeflaterOutputStream(baos); + try { + os.write(in); + os.finish(); + try { + os.close(); + } catch (IOException e) { + log.error("Close DeflaterOutputStream error", e); + } + } catch (IOException e) { + throw new RuntimeException("IO exception compressing data", e); + } finally { + try { + baos.close(); + } catch (IOException e) { + log.error("Close ByteArrayOutputStream error", e); + } + } + return baos.toByteArray(); + } + + private static byte[] gzipCompress(byte[] in) { + if (in == null) { + throw new NullPointerException("Can't compress null"); + } + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + GZIPOutputStream gz = null; + try { + gz = new GZIPOutputStream(bos); + gz.write(in); + } catch (IOException e) { + throw new RuntimeException("IO exception compressing data", e); + } finally { + if (gz != null) { + try { + gz.close(); + } catch (IOException e) { + log.error("Close GZIPOutputStream error", e); + } + } + if (bos != null) { + try { + bos.close(); + } catch (IOException e) { + log.error("Close ByteArrayOutputStream error", e); + } + } + } + byte[] rv = bos.toByteArray(); + // log.debug("Compressed %d bytes to %d", in.length, rv.length); + return rv; + } + + static int COMPRESS_RATIO = 8; + + /** + * Decompress the given array of bytes. + * + * @return null if the bytes cannot be decompressed + */ + protected byte[] decompress(byte[] in) { + switch (this.compressMode) { + case GZIP: + return gzipDecompress(in); + case ZIP: + return zipDecompress(in); + default: + return gzipDecompress(in); + } + } + + private byte[] zipDecompress(byte[] in) { + int size = in.length * COMPRESS_RATIO; + ByteArrayInputStream bais = new ByteArrayInputStream(in); + InflaterInputStream is = new InflaterInputStream(bais); + ByteArrayOutputStream baos = new ByteArrayOutputStream(size); + try { + byte[] uncompressMessage = new byte[size]; + while (true) { + int len = is.read(uncompressMessage); + if (len <= 0) { + break; + } + baos.write(uncompressMessage, 0, len); + } + baos.flush(); + return baos.toByteArray(); + + } catch (IOException e) { + log.error("Failed to decompress data", e); + // baos = null; + } finally { + try { + is.close(); + } catch (IOException e) { + log.error("failed to close InflaterInputStream"); + } + try { + bais.close(); + } catch (IOException e) { + log.error("failed to close ByteArrayInputStream"); + } + try { + baos.close(); + } catch (IOException e) { + log.error("failed to close ByteArrayOutputStream"); + } + } + return baos == null ? null : baos.toByteArray(); + } + + private byte[] gzipDecompress(byte[] in) { + ByteArrayOutputStream bos = null; + if (in != null) { + ByteArrayInputStream bis = new ByteArrayInputStream(in); + bos = new ByteArrayOutputStream(); + GZIPInputStream gis = null; + try { + gis = new GZIPInputStream(bis); + + byte[] buf = new byte[64 * 1024]; + int r = -1; + while ((r = gis.read(buf)) > 0) { + bos.write(buf, 0, r); + } + } catch (IOException e) { + log.error("Failed to decompress data", e); + bos = null; + } finally { + if (gis != null) { + try { + gis.close(); + } catch (IOException e) { + log.error("Close GZIPInputStream error", e); + } + } + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + log.error("Close ByteArrayInputStream error", e); + } + } + } + } + return bos == null ? null : bos.toByteArray(); + } + + /** + * Decode the string with the current character set. + */ + protected String decodeString(byte[] data) { + String rv = null; + try { + if (data != null) { + rv = new String(data, this.charset); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return rv; + } + + /** + * Encode a string into the current character set. + */ + protected byte[] encodeString(String in) { + byte[] rv = null; + try { + rv = in.getBytes(this.charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return rv; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/CachedData.java b/src/main/java/net/rubyeye/xmemcached/transcoders/CachedData.java new file mode 100644 index 0000000..a95d567 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/CachedData.java @@ -0,0 +1,144 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.transcoders; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Cached data with its attributes. + */ +public final class CachedData { + + /** + * Maximum data size allowed by memcached. + */ + public final static int MAX_SIZE = + Integer.valueOf(System.getProperty("xmemcached.cached.data.size.max", "1048576")); + + protected int flag; + protected long cas; + private int capacity = -1; + + protected int size = 0; + + // cache decoded object. + public volatile Object decodedObject; + + // padding fields + public long p1, p2, p3, p4; + public int p5; + + protected byte[] data; + + public final int getSize() { + return this.size; + } + + public final void fillData(final ByteBuffer buffer, final int offset, final int length) { + buffer.get(this.data, offset, length); + this.size += length; + } + + public final void fillData(final ByteBuffer buffer, final int length) { + buffer.get(this.data, this.size, length); + this.size += length; + } + + public final int getCapacity() { + return this.capacity; + } + + public final void setSize(final int size) { + this.size = size; + } + + public final void setCapacity(final int dataLen) { + this.capacity = dataLen; + } + + public static final int getMAX_SIZE() { + return MAX_SIZE; + } + + public final void setFlag(final int flags) { + this.flag = flags; + } + + public final void setData(final byte[] data) { + if (data.length > this.capacity) { + throw new IllegalArgumentException("Cannot cache data larger than 1MB (you tried to cache a " + + data.length + " byte object)"); + } + this.data = data; + } + + public final void setCas(final long cas) { + this.cas = cas; + } + + public long getCas() { + return this.cas; + } + + public CachedData() { + super(); + } + + /** + * Get a CachedData instance for the given flags and byte array. + * + * @param f the flags + * @param d the data + * @param capacity the maximum allowable size. + */ + public CachedData(final int f, final byte[] d, final int capacity, final long casId) { + super(); + this.capacity = capacity; + this.size = d != null ? d.length : 0; + if (d != null && d.length > capacity) { + throw new IllegalArgumentException( + "Cannot cache data larger than 1MB (you tried to cache a " + d.length + " byte object)"); + } + this.flag = f; + this.data = d; + this.cas = casId; + } + + /** + * Get a CachedData instance for the given flags and byte array. + * + * @param f the flags + * @param d the data + */ + public CachedData(final int f, final byte[] d) { + this(f, d, MAX_SIZE, -1); + } + + /** + * Get the stored data. + */ + public final byte[] getData() { + return this.data; + } + + /** + * Get the flags stored along with this value. + */ + public final int getFlag() { + return this.flag; + } + + @Override + public String toString() { + return "{CachedData flags=" + this.flag + " data=" + Arrays.toString(this.data) + "}"; + } + + public int remainingCapacity() { + if (getCapacity() < 0) { + return -1; + } + int remainingCapacity = getCapacity() - getSize(); + return remainingCapacity; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/CompressionMode.java b/src/main/java/net/rubyeye/xmemcached/transcoders/CompressionMode.java new file mode 100644 index 0000000..4937f9e --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/CompressionMode.java @@ -0,0 +1,18 @@ +package net.rubyeye.xmemcached.transcoders; + +/** + * Compress mode for compressing data + * + * @author apple + * + */ +public enum CompressionMode { + /** + * Gzip mode + */ + GZIP, + /** + * Zip mode + */ + ZIP +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/IntegerTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/IntegerTranscoder.java new file mode 100644 index 0000000..ac1af61 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/IntegerTranscoder.java @@ -0,0 +1,69 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.transcoders; + +/** + * Transcoder that serializes and unserializes longs. + */ +public final class IntegerTranscoder extends PrimitiveTypeTranscoder { + + public CachedData encode(java.lang.Integer l) { + /** + * store integer as string + */ + if (this.primitiveAsString) { + byte[] b = encodeString(l.toString()); + int flags = 0; + if (b.length > this.compressionThreshold) { + byte[] compressed = compress(b); + if (compressed.length < b.length) { + if (log.isDebugEnabled()) { + log.debug("Compressed " + l.getClass().getName() + " from " + b.length + " to " + + compressed.length); + } + b = compressed; + flags |= SerializingTranscoder.COMPRESSED; + } else { + if (log.isDebugEnabled()) { + log.debug("Compression increased the size of " + l.getClass().getName() + " from " + + b.length + " to " + compressed.length); + } + } + } + return new CachedData(flags, b, b.length, -1); + } + return new CachedData(SerializingTranscoder.SPECIAL_INT, this.tu.encodeInt(l)); + } + + public Integer decode(CachedData d) { + if (this.primitiveAsString) { + byte[] data = d.getData(); + if ((d.getFlag() & SerializingTranscoder.COMPRESSED) != 0) { + data = decompress(d.getData()); + } + int flag = d.getFlag(); + if (flag == 0) { + return Integer.valueOf(decodeString(data)); + } else { + return null; + } + } else { + if (SerializingTranscoder.SPECIAL_INT == d.getFlag()) { + return this.tu.decodeInt(d.getData()); + } else { + return null; + } + } + } + + @Override + public void setPrimitiveAsString(boolean primitiveAsString) { + this.primitiveAsString = primitiveAsString; + } + + @Override + public void setPackZeros(boolean packZeros) { + this.tu.setPackZeros(packZeros); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/LongTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/LongTranscoder.java new file mode 100644 index 0000000..b8e1459 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/LongTranscoder.java @@ -0,0 +1,65 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.transcoders; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Transcoder that serializes and unserializes longs. + */ +public final class LongTranscoder extends PrimitiveTypeTranscoder { + private static final Logger log = LoggerFactory.getLogger(LongTranscoder.class); + + public CachedData encode(java.lang.Long l) { + /** + * store Long as string + */ + if (this.primitiveAsString) { + byte[] b = encodeString(l.toString()); + int flags = 0; + if (b.length > this.compressionThreshold) { + byte[] compressed = compress(b); + if (compressed.length < b.length) { + if (log.isDebugEnabled()) { + log.debug("Compressed " + l.getClass().getName() + " from " + b.length + " to " + + compressed.length); + } + b = compressed; + flags |= SerializingTranscoder.COMPRESSED; + } else { + if (log.isDebugEnabled()) { + log.debug("Compression increased the size of " + l.getClass().getName() + " from " + + b.length + " to " + compressed.length); + } + } + } + return new CachedData(flags, b, b.length, -1); + } + return new CachedData(SerializingTranscoder.SPECIAL_LONG, this.tu.encodeLong(l)); + } + + public Long decode(CachedData d) { + if (this.primitiveAsString) { + byte[] data = d.getData(); + if ((d.getFlag() & SerializingTranscoder.COMPRESSED) != 0) { + data = decompress(d.getData()); + } + int flag = d.getFlag(); + if (flag == 0) { + return Long.valueOf(decodeString(data)); + } else { + return null; + } + } else { + if (SerializingTranscoder.SPECIAL_LONG == d.getFlag()) { + return this.tu.decodeLong(d.getData()); + } else { + log.error("Unexpected flags for long: " + d.getFlag() + " wanted " + + SerializingTranscoder.SPECIAL_LONG); + return null; + } + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/PrimitiveTypeTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/PrimitiveTypeTranscoder.java new file mode 100644 index 0000000..91bad46 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/PrimitiveTypeTranscoder.java @@ -0,0 +1,27 @@ +package net.rubyeye.xmemcached.transcoders; + +public abstract class PrimitiveTypeTranscoder extends BaseSerializingTranscoder + implements Transcoder { + protected final TranscoderUtils tu = new TranscoderUtils(true); + + protected boolean primitiveAsString; + + public void setPackZeros(boolean packZeros) { + this.tu.setPackZeros(packZeros); + + } + + public boolean isPackZeros() { + return this.tu.isPackZeros(); + } + + public boolean isPrimitiveAsString() { + return this.primitiveAsString; + } + + public void setPrimitiveAsString(boolean primitiveAsString) { + this.primitiveAsString = primitiveAsString; + + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/SerializingTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/SerializingTranscoder.java new file mode 100644 index 0000000..d9a91d5 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/SerializingTranscoder.java @@ -0,0 +1,227 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.transcoders; + +import java.util.Date; + +/** + * Transcoder that serializes and compresses objects. + */ +public class SerializingTranscoder extends BaseSerializingTranscoder implements Transcoder { + + public void setPackZeros(boolean packZeros) { + this.transcoderUtils.setPackZeros(packZeros); + + } + + public void setPrimitiveAsString(boolean primitiveAsString) { + this.primitiveAsString = primitiveAsString; + } + + private final int maxSize; + + private boolean primitiveAsString; + + public final int getMaxSize() { + return this.maxSize; + } + + // General flags + public static final int SERIALIZED = 1; + public static final int COMPRESSED = 2; + + // Special flags for specially handled types. + public static final int SPECIAL_MASK = 0xff00; + public static final int SPECIAL_BOOLEAN = (1 << 8); + public static final int SPECIAL_INT = (2 << 8); + public static final int SPECIAL_LONG = (3 << 8); + public static final int SPECIAL_DATE = (4 << 8); + public static final int SPECIAL_BYTE = (5 << 8); + public static final int SPECIAL_FLOAT = (6 << 8); + public static final int SPECIAL_DOUBLE = (7 << 8); + public static final int SPECIAL_BYTEARRAY = (8 << 8); + + private final TranscoderUtils transcoderUtils = new TranscoderUtils(true); + + public TranscoderUtils getTranscoderUtils() { + return transcoderUtils; + } + + /** + * Get a serializing transcoder with the default max data size. + */ + public SerializingTranscoder() { + this(CachedData.MAX_SIZE); + } + + /** + * Get a serializing transcoder that specifies the max data size. + */ + public SerializingTranscoder(int max) { + this.maxSize = max; + } + + public boolean isPackZeros() { + return this.transcoderUtils.isPackZeros(); + } + + public boolean isPrimitiveAsString() { + return this.primitiveAsString; + } + + /* + * (non-Javadoc) + * + * @see net.spy.memcached.Transcoder#decode(net.spy.memcached.CachedData) + */ + public final Object decode(CachedData d) { + Object obj = d.decodedObject; + if (obj != null) { + return obj; + } + byte[] data = d.data; + + int flags = d.flag; + if ((flags & COMPRESSED) != 0) { + data = decompress(data); + } + flags = flags & SPECIAL_MASK; + obj = decode0(d, data, flags); + d.decodedObject = obj; + return obj; + } + + protected final Object decode0(CachedData cachedData, byte[] data, int flags) { + Object rv = null; + if ((cachedData.flag & SERIALIZED) != 0 && data != null) { + rv = deserialize(data); + } else { + if (this.primitiveAsString) { + if (flags == 0) { + return decodeString(data); + } + } + if (flags != 0 && data != null) { + switch (flags) { + case SPECIAL_BOOLEAN: + rv = Boolean.valueOf(this.transcoderUtils.decodeBoolean(data)); + break; + case SPECIAL_INT: + rv = Integer.valueOf(this.transcoderUtils.decodeInt(data)); + break; + case SPECIAL_LONG: + rv = Long.valueOf(this.transcoderUtils.decodeLong(data)); + break; + case SPECIAL_BYTE: + rv = Byte.valueOf(this.transcoderUtils.decodeByte(data)); + break; + case SPECIAL_FLOAT: + rv = new Float(Float.intBitsToFloat(this.transcoderUtils.decodeInt(data))); + break; + case SPECIAL_DOUBLE: + rv = new Double(Double.longBitsToDouble(this.transcoderUtils.decodeLong(data))); + break; + case SPECIAL_DATE: + rv = new Date(this.transcoderUtils.decodeLong(data)); + break; + case SPECIAL_BYTEARRAY: + rv = data; + break; + default: + log.warn(String.format("Undecodeable with flags %x", flags)); + } + } else { + rv = decodeString(data); + } + } + return rv; + } + + /* + * (non-Javadoc) + * + * @see net.spy.memcached.Transcoder#encode(java.lang.Object) + */ + public final CachedData encode(Object o) { + byte[] b = null; + int flags = 0; + if (o instanceof String) { + b = encodeString((String) o); + } else if (o instanceof Long) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.transcoderUtils.encodeLong((Long) o); + } + flags |= SPECIAL_LONG; + } else if (o instanceof Integer) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.transcoderUtils.encodeInt((Integer) o); + } + flags |= SPECIAL_INT; + } else if (o instanceof Boolean) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.transcoderUtils.encodeBoolean((Boolean) o); + } + flags |= SPECIAL_BOOLEAN; + } else if (o instanceof Date) { + b = this.transcoderUtils.encodeLong(((Date) o).getTime()); + flags |= SPECIAL_DATE; + } else if (o instanceof Byte) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.transcoderUtils.encodeByte((Byte) o); + } + flags |= SPECIAL_BYTE; + } else if (o instanceof Float) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.transcoderUtils.encodeInt(Float.floatToRawIntBits((Float) o)); + } + flags |= SPECIAL_FLOAT; + } else if (o instanceof Double) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.transcoderUtils.encodeLong(Double.doubleToRawLongBits((Double) o)); + } + flags |= SPECIAL_DOUBLE; + } else if (o instanceof byte[]) { + b = (byte[]) o; + flags |= SPECIAL_BYTEARRAY; + } else { + b = serialize(o); + flags |= SERIALIZED; + } + assert b != null; + if (this.primitiveAsString) { + // It is not be SERIALIZED,so change it to string type + if ((flags & SERIALIZED) == 0) { + flags = 0; + } + } + if (b.length > this.compressionThreshold) { + byte[] compressed = compress(b); + if (compressed.length < b.length) { + if (log.isDebugEnabled()) { + log.debug("Compressed " + o.getClass().getName() + " from " + b.length + " to " + + compressed.length); + } + b = compressed; + flags |= COMPRESSED; + } else { + if (log.isDebugEnabled()) { + log.debug("Compression increased the size of " + o.getClass().getName() + " from " + + b.length + " to " + compressed.length); + } + } + } + return new CachedData(flags, b, this.maxSize, -1); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/StringTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/StringTranscoder.java new file mode 100644 index 0000000..851cae7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/StringTranscoder.java @@ -0,0 +1,52 @@ +package net.rubyeye.xmemcached.transcoders; + +import java.io.UnsupportedEncodingException; + +/** + * String Transcoder + * + * @author dennis + * + */ +public class StringTranscoder extends PrimitiveTypeTranscoder { + + private String charset = BaseSerializingTranscoder.DEFAULT_CHARSET; + + public StringTranscoder(String charset) { + this.charset = charset; + } + + public StringTranscoder() { + this(BaseSerializingTranscoder.DEFAULT_CHARSET); + } + + public String decode(CachedData d) { + if (d.getFlag() == 0) { + String rv = null; + try { + if (d.getData() != null) { + rv = new String(d.getData(), this.charset); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return rv; + } else { + throw new RuntimeException("Decode String error"); + } + } + + public static final int STRING_FLAG = 0; + + public CachedData encode(String o) { + byte[] b = null; + + try { + b = o.getBytes(this.charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + return new CachedData(STRING_FLAG, b); + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/TokyoTyrantTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/TokyoTyrantTranscoder.java new file mode 100644 index 0000000..94a85b7 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/TokyoTyrantTranscoder.java @@ -0,0 +1,104 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.transcoders; + +import net.rubyeye.xmemcached.exception.MemcachedDecodeException; + +/** + * Transcoder for TokyoTyrant.Add 4-bytes flag before value. + * + * @author boyan + * + */ +public class TokyoTyrantTranscoder implements Transcoder { + private final SerializingTranscoder serializingTranscoder; + + public TokyoTyrantTranscoder(int maxSize) { + serializingTranscoder = new SerializingTranscoder(maxSize); + serializingTranscoder.setPackZeros(false); + } + + public TokyoTyrantTranscoder() { + serializingTranscoder = new SerializingTranscoder(); + serializingTranscoder.setPackZeros(false); + } + + public final Object decode(CachedData d) { + byte[] compositeData = d.getData(); + if (compositeData.length <= 4) + throw new MemcachedDecodeException( + "There are no four bytes before value for TokyoTyrantTranscoder"); + byte[] flagBytes = new byte[4]; + byte[] realData = new byte[compositeData.length - 4]; + System.arraycopy(compositeData, 0, flagBytes, 0, 4); + System.arraycopy(compositeData, 4, realData, 0, compositeData.length - 4); + int flag = serializingTranscoder.getTranscoderUtils().decodeInt(flagBytes); + d.setFlag(flag); + if ((flag & SerializingTranscoder.COMPRESSED) != 0) { + realData = serializingTranscoder.decompress(realData); + } + flag = flag & SerializingTranscoder.SPECIAL_MASK; + return serializingTranscoder.decode0(d, realData, flag); + } + + public void setCompressionMode(CompressionMode compressMode) { + this.serializingTranscoder.setCompressionMode(compressMode); + } + + public final CachedData encode(Object o) { + CachedData result = serializingTranscoder.encode(o); + byte[] realData = result.getData(); + int flag = result.getFlag(); + byte[] flagBytes = serializingTranscoder.getTranscoderUtils().encodeInt(flag); + + byte[] compisiteData = new byte[4 + realData.length]; + System.arraycopy(flagBytes, 0, compisiteData, 0, 4); + System.arraycopy(realData, 0, compisiteData, 4, realData.length); + + result.setData(compisiteData); + return result; + } + + public final int getMaxSize() { + return serializingTranscoder.getMaxSize(); + } + + public boolean isPackZeros() { + return serializingTranscoder.isPackZeros(); + } + + public boolean isPrimitiveAsString() { + return serializingTranscoder.isPrimitiveAsString(); + } + + public void setCharset(String to) { + serializingTranscoder.setCharset(to); + } + + public void setCompressionThreshold(int to) { + serializingTranscoder.setCompressionThreshold(to); + } + + public void setPackZeros(boolean packZeros) { + throw new UnsupportedOperationException("TokyoTyrantTranscoder doesn't support pack zeros"); + } + + public void setPrimitiveAsString(boolean primitiveAsString) { + throw new UnsupportedOperationException( + "TokyoTyrantTranscoder doesn't support save primitive type as String"); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/Transcoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/Transcoder.java new file mode 100644 index 0000000..2a92914 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/Transcoder.java @@ -0,0 +1,69 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.transcoders; + +/** + * Transcoder is an interface for classes that convert between byte arrays and objects for storage + * in the cache. + */ +public interface Transcoder { + + /** + * Encode the given object for storage. + * + * @param o the object + * @return the CachedData representing what should be sent + */ + CachedData encode(T o); + + /** + * Decode the cached object into the object it represents. + * + * @param d the data + * @return the return value + */ + T decode(CachedData d); + + /** + * Set whether store primitive type as string. + * + * @param primitiveAsString + */ + public void setPrimitiveAsString(boolean primitiveAsString); + + /** + * Set whether pack zeros + * + * @param primitiveAsString + */ + public void setPackZeros(boolean packZeros); + + /** + * Set compression threshold in bytes + * + * @param to + */ + public void setCompressionThreshold(int to); + + /** + * Returns if client stores primitive type as string. + * + * @return + */ + public boolean isPrimitiveAsString(); + + /** + * Returns if transcoder packs zero. + * + * @return + */ + public boolean isPackZeros(); + + /** + * Set compress mode,default is ZIP + * + * @see CompressionMode + * @param compressMode + */ + public void setCompressionMode(CompressionMode compressMode); +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/TranscoderUtils.java b/src/main/java/net/rubyeye/xmemcached/transcoders/TranscoderUtils.java new file mode 100644 index 0000000..7c16adb --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/TranscoderUtils.java @@ -0,0 +1,96 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.transcoders; + +/** + * Utility class for transcoding Java types. + */ +public final class TranscoderUtils { + + private boolean packZeros; + + /** + * Get an instance of TranscoderUtils. + * + * @param pack if true, remove all zero bytes from the MSB of the packed num + */ + public TranscoderUtils(boolean pack) { + super(); + this.packZeros = pack; + } + + public final boolean isPackZeros() { + return this.packZeros; + } + + public final void setPackZeros(boolean packZeros) { + this.packZeros = packZeros; + } + + public final byte[] encodeNum(long l, int maxBytes) { + byte[] rv = new byte[maxBytes]; + for (int i = 0; i < rv.length; i++) { + int pos = rv.length - i - 1; + rv[pos] = (byte) ((l >> (8 * i)) & 0xff); + } + if (this.packZeros) { + int firstNon0 = 0; + for (; firstNon0 < rv.length && rv[firstNon0] == 0; firstNon0++) { + // Just looking for what we can reduce + } + if (firstNon0 > 0) { + byte[] tmp = new byte[rv.length - firstNon0]; + System.arraycopy(rv, firstNon0, tmp, 0, rv.length - firstNon0); + rv = tmp; + } + } + return rv; + } + + public final byte[] encodeLong(long l) { + return encodeNum(l, 8); + } + + public final long decodeLong(byte[] b) { + assert b.length <= 8 : "Too long to be an long (" + b.length + ") bytes"; + long rv = 0; + for (byte i : b) { + rv = (rv << 8) | (i < 0 ? 256 + i : i); + } + return rv; + } + + public final byte[] encodeInt(int in) { + return encodeNum(in, 4); + } + + public final int decodeInt(byte[] in) { + assert in.length <= 4 : "Too long to be an int (" + in.length + ") bytes"; + return (int) decodeLong(in); + } + + public final byte[] encodeByte(byte in) { + return new byte[] {in}; + } + + public final byte decodeByte(byte[] in) { + assert in.length <= 1 : "Too long for a byte"; + byte rv = 0; + if (in.length == 1) { + rv = in[0]; + } + return rv; + } + + public final byte[] encodeBoolean(boolean b) { + byte[] rv = new byte[1]; + rv[0] = (byte) (b ? '1' : '0'); + return rv; + } + + public final boolean decodeBoolean(byte[] in) { + assert in.length == 1 : "Wrong length for a boolean"; + return in[0] == '1'; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/WhalinTranscoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/WhalinTranscoder.java new file mode 100644 index 0000000..a1cc960 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/WhalinTranscoder.java @@ -0,0 +1,247 @@ +package net.rubyeye.xmemcached.transcoders; + +import java.util.Date; + +/** + * Transcoder that provides compatibility with Greg Whalin's memcached client. + */ +public class WhalinTranscoder extends BaseSerializingTranscoder implements Transcoder { + + public static final int SPECIAL_BYTE = 1; + public static final int SPECIAL_BOOLEAN = 8192; + public static final int SPECIAL_INT = 4; + public static final int SPECIAL_LONG = 16384; + public static final int SPECIAL_CHARACTER = 16; + public static final int SPECIAL_STRING = 32; + public static final int SPECIAL_STRINGBUFFER = 64; + public static final int SPECIAL_FLOAT = 128; + public static final int SPECIAL_SHORT = 256; + public static final int SPECIAL_DOUBLE = 512; + public static final int SPECIAL_DATE = 1024; + public static final int SPECIAL_STRINGBUILDER = 2048; + public static final int SPECIAL_BYTEARRAY = 4096; + public static final int COMPRESSED = 2; + public static final int SERIALIZED = 8; + + private int maxSize; + + private boolean primitiveAsString; + + public void setPackZeros(boolean packZeros) { + this.tu.setPackZeros(packZeros); + } + + public void setPrimitiveAsString(boolean primitiveAsString) { + this.primitiveAsString = primitiveAsString; + } + + public WhalinTranscoder() { + this(CachedData.MAX_SIZE); + } + + public WhalinTranscoder(int maxSize) { + super(); + this.maxSize = maxSize; + } + + public final int getMaxSize() { + return this.maxSize; + } + + public final void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + public boolean isPackZeros() { + return this.tu.isPackZeros(); + } + + public boolean isPrimitiveAsString() { + return this.primitiveAsString; + } + + private final TranscoderUtils tu = new TranscoderUtils(false); + + /* + * (non-Javadoc) + * + * @see net.spy.memcached.Transcoder#decode(net.spy.memcached.CachedData) + */ + public Object decode(CachedData d) { + byte[] data = d.getData(); + Object rv = null; + if ((d.getFlag() & COMPRESSED) != 0) { + data = decompress(d.getData()); + } + if ((d.getFlag() & SERIALIZED) != 0) { + rv = deserialize(data); + } else { + int f = d.getFlag() & ~COMPRESSED; + if (this.primitiveAsString) { + if (f == SPECIAL_STRING) { + return decodeString(d.getData()); + } + } + switch (f) { + case SPECIAL_BOOLEAN: + rv = Boolean.valueOf(this.decodeBoolean(data)); + break; + case SPECIAL_INT: + rv = Integer.valueOf(this.tu.decodeInt(data)); + break; + case SPECIAL_SHORT: + rv = Short.valueOf((short) this.tu.decodeInt(data)); + break; + case SPECIAL_LONG: + rv = Long.valueOf(this.tu.decodeLong(data)); + break; + case SPECIAL_DATE: + rv = new Date(this.tu.decodeLong(data)); + break; + case SPECIAL_BYTE: + rv = Byte.valueOf(this.tu.decodeByte(data)); + break; + case SPECIAL_FLOAT: + rv = new Float(Float.intBitsToFloat(this.tu.decodeInt(data))); + break; + case SPECIAL_DOUBLE: + rv = new Double(Double.longBitsToDouble(this.tu.decodeLong(data))); + break; + case SPECIAL_BYTEARRAY: + rv = data; + break; + case SPECIAL_STRING: + rv = decodeString(data); + break; + case SPECIAL_STRINGBUFFER: + rv = new StringBuffer(decodeString(data)); + break; + case SPECIAL_STRINGBUILDER: + rv = new StringBuilder(decodeString(data)); + break; + case SPECIAL_CHARACTER: + rv = decodeCharacter(data); + break; + default: + log.warn(String.format("Cannot handle data with flags %x", f)); + } + } + return rv; + } + + public CachedData encode(Object o) { + byte[] b = null; + int flags = 0; + if (o instanceof String) { + b = encodeString((String) o); + flags |= SPECIAL_STRING; + } else if (o instanceof StringBuffer) { + flags |= SPECIAL_STRINGBUFFER; + b = encodeString(String.valueOf(o)); + } else if (o instanceof StringBuilder) { + flags |= SPECIAL_STRINGBUILDER; + b = encodeString(String.valueOf(o)); + } else if (o instanceof Long) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.tu.encodeLong((Long) o); + } + flags |= SPECIAL_LONG; + } else if (o instanceof Integer) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.tu.encodeInt((Integer) o); + } + flags |= SPECIAL_INT; + } else if (o instanceof Short) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.tu.encodeInt((Short) o); + } + flags |= SPECIAL_SHORT; + } else if (o instanceof Boolean) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.encodeBoolean((Boolean) o); + } + flags |= SPECIAL_BOOLEAN; + } else if (o instanceof Date) { + b = this.tu.encodeLong(((Date) o).getTime()); + flags |= SPECIAL_DATE; + } else if (o instanceof Byte) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.tu.encodeByte((Byte) o); + } + flags |= SPECIAL_BYTE; + } else if (o instanceof Float) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.tu.encodeInt(Float.floatToIntBits((Float) o)); + } + flags |= SPECIAL_FLOAT; + } else if (o instanceof Double) { + if (this.primitiveAsString) { + b = encodeString(o.toString()); + } else { + b = this.tu.encodeLong(Double.doubleToLongBits((Double) o)); + } + flags |= SPECIAL_DOUBLE; + } else if (o instanceof byte[]) { + b = (byte[]) o; + flags |= SPECIAL_BYTEARRAY; + } else if (o instanceof Character) { + b = this.tu.encodeInt((Character) o); + flags |= SPECIAL_CHARACTER; + } else { + b = serialize(o); + flags |= SERIALIZED; + } + assert b != null; + if (this.primitiveAsString) { + if ((flags & SERIALIZED) == 0) { + flags = 0; + flags |= SPECIAL_STRING; + } + } + if (b.length > this.compressionThreshold) { + byte[] compressed = compress(b); + if (compressed.length < b.length) { + if (log.isDebugEnabled()) { + log.debug(String.format("Compressed %s from %d to %d", o.getClass().getName(), b.length, + compressed.length)); + } + b = compressed; + flags |= COMPRESSED; + } else { + if (log.isDebugEnabled()) { + log.debug(String.format("Compression increased the size of %s from %d to %d", + o.getClass().getName(), b.length, compressed.length)); + } + } + } + return new CachedData(flags, b); + } + + protected Character decodeCharacter(byte[] b) { + return Character.valueOf((char) this.tu.decodeInt(b)); + } + + public byte[] encodeBoolean(boolean b) { + byte[] rv = new byte[1]; + rv[0] = (byte) (b ? 1 : 0); + return rv; + } + + public boolean decodeBoolean(byte[] in) { + assert in.length == 1 : "Wrong length for a boolean"; + return in[0] == 1; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/WhalinV1Transcoder.java b/src/main/java/net/rubyeye/xmemcached/transcoders/WhalinV1Transcoder.java new file mode 100644 index 0000000..89793e6 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/WhalinV1Transcoder.java @@ -0,0 +1,294 @@ +package net.rubyeye.xmemcached.transcoders; + +import java.io.UnsupportedEncodingException; +import java.util.Date; + +/** + * Handles old whalin (tested with v1.6) encoding: data type is in the first byte of the value. + * + * @author bpartensky + * @since Oct 16, 2008 + */ +public class WhalinV1Transcoder extends BaseSerializingTranscoder implements Transcoder { + + public static final int SPECIAL_BYTE = 1; + public static final int SPECIAL_BOOLEAN = 2; + public static final int SPECIAL_INTEGER = 3; + public static final int SPECIAL_LONG = 4; + public static final int SPECIAL_CHARACTER = 5; + public static final int SPECIAL_STRING = 6; + public static final int SPECIAL_STRINGBUFFER = 7; + public static final int SPECIAL_FLOAT = 8; + public static final int SPECIAL_SHORT = 9; + public static final int SPECIAL_DOUBLE = 10; + public static final int SPECIAL_DATE = 11; + public static final int SPECIAL_STRINGBUILDER = 12; + public static final int COMPRESSED = 2; + public static final int SERIALIZED = 8; + + public void setPackZeros(boolean packZeros) { + throw new UnsupportedOperationException(); + + } + + public void setPrimitiveAsString(boolean primitiveAsString) { + throw new UnsupportedOperationException(); + } + + public boolean isPackZeros() { + return false; + } + + public boolean isPrimitiveAsString() { + return false; + } + + public CachedData encode(Object o) { + byte[] b = null; + int flags = 0; + if (o instanceof String) { + b = encodeW1String((String) o); + } else if (o instanceof StringBuffer) { + b = encodeStringBuffer((StringBuffer) o); + } else if (o instanceof StringBuilder) { + b = encodeStringbuilder((StringBuilder) o); + } else if (o instanceof Long) { + b = encodeLong((Long) o); + } else if (o instanceof Integer) { + b = encodeInteger((Integer) o); + } else if (o instanceof Short) { + b = encodeShort((Short) o); + } else if (o instanceof Boolean) { + b = encodeBoolean((Boolean) o); + } else if (o instanceof Date) { + b = encodeLong(((Date) o).getTime(), SPECIAL_DATE); + } else if (o instanceof Byte) { + b = encodeByte((Byte) o); + } else if (o instanceof Float) { + b = encodeFloat((Float) o); + } else if (o instanceof Double) { + b = encodeDouble((Double) o); + } else if (o instanceof byte[]) { + throw new IllegalArgumentException("Cannot handle byte arrays."); + } else if (o instanceof Character) { + b = encodeCharacter((Character) o); + } else { + b = serialize(o); + flags |= SERIALIZED; + } + assert b != null; + if (b.length > this.compressionThreshold) { + byte[] compressed = compress(b); + if (compressed.length < b.length) { + log.debug(String.format("Compressed %s from %d to %d", o.getClass().getName(), b.length, + compressed.length)); + b = compressed; + flags |= COMPRESSED; + } else { + log.debug(String.format("Compression increased the size of %s from %d to %d", + o.getClass().getName(), b.length, compressed.length)); + } + } + return new CachedData(flags, b); + } + + public Object decode(CachedData d) { + byte[] data = d.getData(); + Object rv = null; + if ((d.getFlag() & COMPRESSED) != 0) { + data = decompress(d.getData()); + } + if ((d.getFlag() & SERIALIZED) != 0) { + rv = deserialize(data); + } else { + int f = data[0]; + switch (f) { + case SPECIAL_BOOLEAN: + rv = decodeBoolean(data); + break; + case SPECIAL_INTEGER: + rv = decodeInteger(data); + break; + case SPECIAL_SHORT: + rv = decodeShort(data); + break; + case SPECIAL_LONG: + rv = decodeLong(data); + break; + case SPECIAL_DATE: + rv = new Date(decodeLong(data)); + break; + case SPECIAL_BYTE: + rv = decodeByte(data); + break; + case SPECIAL_FLOAT: + rv = decodeFloat(data); + break; + case SPECIAL_DOUBLE: + rv = decodeDouble(data); + break; + case SPECIAL_STRING: + rv = decodeW1String(data); + break; + case SPECIAL_STRINGBUFFER: + rv = new StringBuffer(decodeW1String(data)); + break; + case SPECIAL_STRINGBUILDER: + rv = new StringBuilder(decodeW1String(data)); + break; + case SPECIAL_CHARACTER: + rv = decodeCharacter(data); + break; + default: + log.warn(String.format("Cannot handle data with flags %x", f)); + } + } + return rv; + } + + private Short decodeShort(byte[] data) { + return Short.valueOf((short) decodeInteger(data).intValue()); + } + + private Byte decodeByte(byte[] in) { + assert in.length == 2 : "Wrong length for a byte"; + byte value = in[1]; + return Byte.valueOf(value); + + } + + private Integer decodeInteger(byte[] in) { + assert in.length == 5 : "Wrong length for an int"; + return Integer.valueOf((int) decodeLong(in).longValue()); + + } + + private Float decodeFloat(byte[] in) { + assert in.length == 5 : "Wrong length for a float"; + Integer l = decodeInteger(in); + return Float.valueOf(Float.intBitsToFloat(l.intValue())); + } + + private Double decodeDouble(byte[] in) { + assert in.length == 9 : "Wrong length for a double"; + Long l = decodeLong(in); + return Double.valueOf(Double.longBitsToDouble(l.longValue())); + } + + private Boolean decodeBoolean(byte[] in) { + assert in.length == 2 : "Wrong length for a boolean"; + return Boolean.valueOf(in[1] == 1); + } + + private Long decodeLong(byte[] in) { + long rv = 0L; + for (int idx = 1; idx < in.length; idx++) { + byte i = in[idx]; + rv = (rv << 8) | (i < 0 ? 256 + i : i); + } + return Long.valueOf(rv); + } + + private Character decodeCharacter(byte[] b) { + return Character.valueOf((char) decodeInteger(b).intValue()); + } + + private String decodeW1String(byte[] b) { + try { + return new String(b, 1, b.length - 1, this.charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private byte[] encodeByte(Byte value) { + byte[] b = new byte[2]; + b[0] = SPECIAL_BYTE; + b[1] = value.byteValue(); + return b; + } + + private byte[] encodeBoolean(Boolean value) { + byte[] b = new byte[2]; + b[0] = SPECIAL_BOOLEAN; + b[1] = (byte) (value.booleanValue() ? 1 : 0); + return b; + } + + private byte[] encodeInteger(Integer value) { + byte[] b = encodeNum(value, 4); + b[0] = SPECIAL_INTEGER; + return b; + } + + private byte[] encodeLong(Long value, int type) { + byte[] b = encodeNum(value, 8); + b[0] = (byte) type; + return b; + } + + private byte[] encodeLong(Long value) { + return encodeLong(value, SPECIAL_LONG); + } + + private byte[] encodeShort(Short value) { + byte[] b = encodeInteger((int) value.shortValue()); + b[0] = SPECIAL_SHORT; + return b; + } + + private byte[] encodeFloat(Float value) { + byte[] b = encodeInteger(Float.floatToIntBits(value)); + b[0] = SPECIAL_FLOAT; + return b; + } + + private byte[] encodeDouble(Double value) { + byte[] b = encodeLong(Double.doubleToLongBits(value)); + b[0] = SPECIAL_DOUBLE; + return b; + } + + private byte[] encodeCharacter(Character value) { + byte[] result = encodeInteger((int) value.charValue()); + result[0] = SPECIAL_CHARACTER; + return result; + } + + private byte[] encodeStringBuffer(StringBuffer value) { + byte[] b = encodeW1String(value.toString()); + b[0] = SPECIAL_STRINGBUFFER; + return b; + } + + private byte[] encodeStringbuilder(StringBuilder value) { + byte[] b = encodeW1String(value.toString()); + b[0] = SPECIAL_STRINGBUILDER; + return b; + } + + private byte[] encodeW1String(String value) { + byte[] svalue = null; + try { + svalue = value.getBytes(this.charset); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + byte[] result = new byte[svalue.length + 1]; + System.arraycopy(svalue, 0, result, 1, svalue.length); + result[0] = SPECIAL_STRING; + return result; + } + + private byte[] encodeNum(long l, int maxBytes) { + byte[] rv = new byte[maxBytes + 1]; + + for (int i = 0; i < rv.length - 1; i++) { + int pos = rv.length - i - 1; + rv[pos] = (byte) ((l >> (8 * i)) & 0xff); + } + + return rv; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/transcoders/package.html b/src/main/java/net/rubyeye/xmemcached/transcoders/package.html new file mode 100644 index 0000000..ca56fc6 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/transcoders/package.html @@ -0,0 +1,10 @@ + + + + + Transcoders convert data to and from data and flags + + +

Transcoders convert data to and from data and flags,from spymemcached

+ + \ No newline at end of file diff --git a/src/main/java/net/rubyeye/xmemcached/utils/AddrUtil.java b/src/main/java/net/rubyeye/xmemcached/utils/AddrUtil.java new file mode 100644 index 0000000..25e5c88 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/AddrUtil.java @@ -0,0 +1,147 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.utils; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Convenience utilities for simplifying common address parsing. + */ +public class AddrUtil { + + /** + * Split a string in the form of "host1:port1,host2:port2 host3:port3,host4:port4" into a Map of + * InetSocketAddress instances suitable for instantiating a MemcachedClient,map's key is the main + * memcached node,and value is the standby node for main node. Note that colon-delimited IPv6 is + * also supported. For example: ::1:11211 + * + * @param s + * @return + */ + public static Map getAddressMap(String s) { + if (s == null) { + throw new NullPointerException("Null host list"); + } + if (s.trim().equals("")) { + throw new IllegalArgumentException("No hosts in list: ``" + s + "''"); + } + s = s.trim(); + Map result = + new LinkedHashMap(); + for (String hosts : s.split(" ")) { + String[] nodes = hosts.split(","); + + if (nodes.length < 1) { + throw new IllegalArgumentException("Invalid server ``" + hosts + "'' in list: " + s); + } + String mainHost = nodes[0].trim(); + InetSocketAddress mainAddress = getInetSocketAddress(s, mainHost); + if (nodes.length >= 2) { + InetSocketAddress standByAddress = getInetSocketAddress(s, nodes[1].trim()); + result.put(mainAddress, standByAddress); + } else { + result.put(mainAddress, null); + } + + } + assert !result.isEmpty() : "No addrs found"; + return result; + } + + private static InetSocketAddress getInetSocketAddress(String s, String mainHost) { + int finalColon = mainHost.lastIndexOf(':'); + if (finalColon < 1) { + throw new IllegalArgumentException("Invalid server ``" + mainHost + "'' in list: " + s); + + } + String hostPart = mainHost.substring(0, finalColon).trim(); + String portNum = mainHost.substring(finalColon + 1).trim(); + + InetSocketAddress mainAddress = new InetSocketAddress(hostPart, Integer.parseInt(portNum)); + return mainAddress; + } + + /** + * Split a string in the form of "host:port host2:port" into a List of InetSocketAddress instances + * suitable for instantiating a MemcachedClient. + * + * Note that colon-delimited IPv6 is also supported. For example: ::1:11211 + */ + public static List getAddresses(String s) { + if (s == null) { + throw new NullPointerException("Null host list"); + } + if (s.trim().equals("")) { + throw new IllegalArgumentException("No hosts in list: ``" + s + "''"); + } + s = s.trim(); + ArrayList addrs = new ArrayList(); + + for (String hoststuff : s.split(" ")) { + int finalColon = hoststuff.lastIndexOf(':'); + if (finalColon < 1) { + throw new IllegalArgumentException("Invalid server ``" + hoststuff + "'' in list: " + s); + + } + String hostPart = hoststuff.substring(0, finalColon).trim(); + String portNum = hoststuff.substring(finalColon + 1).trim(); + + addrs.add(new InetSocketAddress(hostPart, Integer.parseInt(portNum))); + } + assert !addrs.isEmpty() : "No addrs found"; + return addrs; + } + + public static InetSocketAddress getOneAddress(String server) { + if (server == null) { + throw new NullPointerException("Null host"); + } + if (server.trim().equals("")) { + throw new IllegalArgumentException("No hosts in: ``" + server + "''"); + } + server = server.trim(); + int finalColon = server.lastIndexOf(':'); + if (finalColon < 1) { + throw new IllegalArgumentException("Invalid server ``" + server + "''"); + + } + String hostPart = server.substring(0, finalColon).trim(); + String portNum = server.substring(finalColon + 1).trim(); + return new InetSocketAddress(hostPart, Integer.parseInt(portNum)); + } + + /** + * System property to control shutdown hook, issue #44 + * + * @since 2.0.1 + */ + public static boolean isEnableShutDownHook() { + return Boolean.valueOf(System.getProperty("xmemcached.shutdown.hook.enable", "false")); + } + + /** + * Create an unresolved server string (hostname:port) from an InetSocketAddress. + */ + public static final String getServerString(InetSocketAddress addr) { + return addr.getHostString() + ":" + Integer.toString(addr.getPort()); + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/ByteUtils.java b/src/main/java/net/rubyeye/xmemcached/utils/ByteUtils.java new file mode 100644 index 0000000..1c80318 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/ByteUtils.java @@ -0,0 +1,502 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.utils; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.google.code.yanf4j.buffer.IoBuffer; +import net.rubyeye.xmemcached.codec.MemcachedDecoder; +import net.rubyeye.xmemcached.monitor.Constants; + +/** + * Utilities for byte process + * + * @author dennis + * + */ +public final class ByteUtils { + public static final Logger log = LoggerFactory.getLogger(ByteUtils.class); + public static final String DEFAULT_CHARSET_NAME = "utf-8"; + public static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_CHARSET_NAME); + public static final ByteBuffer SPLIT = ByteBuffer.wrap(Constants.CRLF); + + public static final boolean ENABLE_FAST_STRING_ENCODER = + Boolean.valueOf(System.getProperty("xmemcached.string.fast.encoder", "false")); + /** + * if it is testing,check key argument even if use binary protocol. The user must never change + * this value at all. + */ + public static boolean testing; + + private ByteUtils() {} + + public static boolean isValidString(String s) { + return s != null && s.trim().length() > 0; + } + + public static boolean isNumber(String string) { + if (string == null || string.isEmpty()) { + return false; + } + int i = 0; + if (string.charAt(0) == '-') { + if (string.length() > 1) { + i++; + } else { + return false; + } + } + for (; i < string.length(); i++) { + if (!Character.isDigit(string.charAt(i))) { + return false; + } + } + return true; + } + + public static final byte[] getBytes(String k) { + if (k == null || k.length() == 0) { + throw new IllegalArgumentException("Key must not be blank"); + } + if (!ENABLE_FAST_STRING_ENCODER) { + try { + return k.getBytes(DEFAULT_CHARSET_NAME); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } + } else { + return FastStringEncoder.encodeUTF8(k); + } + } + + public static final void setArguments(IoBuffer bb, Object... args) { + boolean wasFirst = true; + for (Object o : args) { + if (wasFirst) { + wasFirst = false; + } else { + bb.put(Constants.SPACE); + } + if (o instanceof byte[]) { + bb.put((byte[]) o); + } else { + bb.put(getBytes(String.valueOf(o))); + } + } + bb.put(Constants.CRLF); + } + + public static final int setArguments(byte[] bb, int index, Object... args) { + boolean wasFirst = true; + int s = index; + for (Object o : args) { + if (wasFirst) { + wasFirst = false; + } else { + bb[s++] = Constants.SPACE; + } + if (o instanceof byte[]) { + byte[] tmp = (byte[]) o; + System.arraycopy(tmp, 0, bb, s, tmp.length); + s += tmp.length; + } else if (o instanceof Integer) { + int v = ((Integer) o).intValue(); + s += stringSize(v); + getBytes(v, s, bb); + } else if (o instanceof String) { + byte[] tmp = getBytes((String) o); + System.arraycopy(tmp, 0, bb, s, tmp.length); + s += tmp.length; + } else if (o instanceof Long) { + long v = ((Long) o).longValue(); + s += stringSize(v); + getBytes(v, s, bb); + } + + } + System.arraycopy(Constants.CRLF, 0, bb, s, 2); + s += 2; + return s; + } + + public static final void checkKey(final byte[] keyBytes) { + + if (keyBytes.length > ByteUtils.maxKeyLength) { + throw new IllegalArgumentException( + "Key is too long (maxlen = " + ByteUtils.maxKeyLength + ")"); + } + // Validate the key + if (memcachedProtocol == Protocol.Text || testing) { + for (byte b : keyBytes) { + if (b == ' ' || b == '\n' || b == '\r' || b == 0) { + try { + throw new IllegalArgumentException( + "Key contains invalid characters: " + new String(keyBytes, "utf-8")); + + } catch (UnsupportedEncodingException e) { + } + } + + } + } + } + + public static final void checkKey(final String key) { + if (key == null || key.length() == 0) { + throw new IllegalArgumentException("Key must not be blank"); + } + byte[] keyBytes = getBytes(key); + if (keyBytes.length > ByteUtils.maxKeyLength) { + throw new IllegalArgumentException( + "Key is too long (maxlen = " + ByteUtils.maxKeyLength + ")"); + } + if (memcachedProtocol == Protocol.Text || testing) { + // Validate the key + for (byte b : keyBytes) { + if (b == ' ' || b == '\n' || b == '\r' || b == 0) { + try { + throw new IllegalArgumentException( + "Key contains invalid characters:" + new String(keyBytes, "utf-8")); + + } catch (UnsupportedEncodingException e) { + } + } + + } + } + } + + private static Protocol memcachedProtocol = Protocol.Text; + + private static int maxKeyLength = 250; + + public static void setProtocol(Protocol protocol) { + if (protocol == null) { + throw new NullPointerException("Null Protocol"); + } + memcachedProtocol = protocol; + // if (protocol == Protocol.Text) { + // maxKeyLength = 250; + // } + // else { + // maxKeyLength = 65535; + // } + } + + public static final int normalizeCapacity(int requestedCapacity) { + switch (requestedCapacity) { + case 0: + case 1 << 0: + case 1 << 1: + case 1 << 2: + case 1 << 3: + case 1 << 4: + case 1 << 5: + case 1 << 6: + case 1 << 7: + case 1 << 8: + case 1 << 9: + case 1 << 10: + case 1 << 11: + case 1 << 12: + case 1 << 13: + case 1 << 14: + case 1 << 15: + case 1 << 16: + case 1 << 17: + case 1 << 18: + case 1 << 19: + case 1 << 21: + case 1 << 22: + case 1 << 23: + case 1 << 24: + case 1 << 25: + case 1 << 26: + case 1 << 27: + case 1 << 28: + case 1 << 29: + case 1 << 30: + case Integer.MAX_VALUE: + return requestedCapacity; + } + + int newCapacity = 1; + while (newCapacity < requestedCapacity) { + newCapacity <<= 1; + if (newCapacity < 0) { + return Integer.MAX_VALUE; + } + } + return newCapacity; + } + + public static final boolean stepBuffer(ByteBuffer buffer, int remaining) { + if (buffer.remaining() >= remaining) { + buffer.position(buffer.position() + remaining); + return true; + } else { + return false; + } + } + + /** + * Read next line from ByteBuffer + * + * @param buffer + * @return + */ + public static final String nextLine(ByteBuffer buffer) { + if (buffer == null) { + return null; + } + + int index = MemcachedDecoder.SPLIT_MATCHER + .matchFirst(com.google.code.yanf4j.buffer.IoBuffer.wrap(buffer)); + if (index >= 0) { + int limit = buffer.limit(); + buffer.limit(index); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + buffer.limit(limit); + buffer.position(index + ByteUtils.SPLIT.remaining()); + return getString(bytes); + + } + return null; + } + + public static String getString(byte[] bytes) { + try { + return new String(bytes, DEFAULT_CHARSET_NAME); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public static void byte2hex(byte b, StringBuffer buf) { + char[] hexChars = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + int high = (b & 0xf0) >> 4; + int low = b & 0x0f; + buf.append(hexChars[high]); + buf.append(hexChars[low]); + } + + public static void int2hex(int a, StringBuffer str) { + str.append(Integer.toHexString(a)); + } + + public static void short2hex(int a, StringBuffer str) { + str.append(Integer.toHexString(a)); + } + + public static void getBytes(long i, int index, byte[] buf) { + long q; + int r; + int pos = index; + byte sign = 0; + + if (i < 0) { + sign = '-'; + i = -i; + } + + // Get 2 digits/iteration using longs until quotient fits into an int + while (i > Integer.MAX_VALUE) { + q = i / 100; + // really: r = i - (q * 100); + r = (int) (i - ((q << 6) + (q << 5) + (q << 2))); + i = q; + buf[--pos] = DigitOnes[r]; + buf[--pos] = DigitTens[r]; + } + + // Get 2 digits/iteration using ints + int q2; + int i2 = (int) i; + while (i2 >= 65536) { + q2 = i2 / 100; + // really: r = i2 - (q * 100); + r = i2 - ((q2 << 6) + (q2 << 5) + (q2 << 2)); + i2 = q2; + buf[--pos] = DigitOnes[r]; + buf[--pos] = DigitTens[r]; + } + + // Fall thru to fast mode for smaller numbers + // assert(i2 <= 65536, i2); + for (;;) { + q2 = i2 * 52429 >>> 16 + 3; + r = i2 - ((q2 << 3) + (q2 << 1)); // r = i2-(q2*10) ... + buf[--pos] = digits[r]; + i2 = q2; + if (i2 == 0) { + break; + } + } + if (sign != 0) { + buf[--pos] = sign; + } + } + + /** + * Places characters representing the integer i into the character array buf. The characters are + * placed into the buffer backwards starting with the least significant digit at the specified + * index (exclusive), and working backwards from there. + * + * Will fail if i == Integer.MIN_VALUE + */ + static void getBytes(int i, int index, byte[] buf) { + int q, r; + int pos = index; + byte sign = 0; + + if (i < 0) { + sign = '-'; + i = -i; + } + + // Generate two digits per iteration + while (i >= 65536) { + q = i / 100; + // really: r = i - (q * 100); + r = i - ((q << 6) + (q << 5) + (q << 2)); + i = q; + buf[--pos] = DigitOnes[r]; + buf[--pos] = DigitTens[r]; + } + + // Fall thru to fast mode for smaller numbers + // assert(i <= 65536, i); + for (;;) { + q = i * 52429 >>> 16 + 3; + r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ... + buf[--pos] = digits[r]; + i = q; + if (i == 0) { + break; + } + } + if (sign != 0) { + buf[--pos] = sign; + } + } + + /** + * All possible chars for representing a number as a String + */ + final static byte[] digits = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + + final static byte[] DigitTens = {'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', + '1', '1', '1', '1', '1', '1', '1', '2', '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', + '3', '3', '3', '3', '3', '3', '3', '3', '4', '4', '4', '4', '4', '4', '4', '4', '4', '4', '5', + '5', '5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6', '6', '6', '6', '6', '6', '6', '6', + '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8', '8', '8', '8', '8', '8', + '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9',}; + + final static byte[] DigitOnes = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', + '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', + '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', + '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',}; + + final static int[] sizeTable = + {9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE}; + + // Requires positive x + public static final int stringSize(int x) { + for (int i = 0;; i++) { + if (x <= sizeTable[i]) { + return i + 1; + } + } + } + + // Requires positive x + public static final int stringSize(long x) { + long p = 10; + for (int i = 1; i < 19; i++) { + if (x < p) { + return i; + } + p = 10 * p; + } + return 19; + } + + final static int[] byte_len_array = new int[256]; + static { + for (int i = Byte.MIN_VALUE; i <= Byte.MAX_VALUE; ++i) { + int size = i < 0 ? stringSize(-i) + 1 : stringSize(i); + byte_len_array[i & 0xFF] = size; + } + } + + public static byte int3(int x) { + return (byte) (x >> 24); + } + + public static byte int2(int x) { + return (byte) (x >> 16); + } + + public static byte int1(int x) { + return (byte) (x >> 8); + } + + public static byte int0(int x) { + return (byte) x; + } + + public static byte short1(short x) { + return (byte) (x >> 8); + } + + public static byte short0(short x) { + return (byte) x; + } + + public static byte long7(long x) { + return (byte) (x >> 56); + } + + public static byte long6(long x) { + return (byte) (x >> 48); + } + + public static byte long5(long x) { + return (byte) (x >> 40); + } + + public static byte long4(long x) { + return (byte) (x >> 32); + } + + public static byte long3(long x) { + return (byte) (x >> 24); + } + + public static byte long2(long x) { + return (byte) (x >> 16); + } + + public static byte long1(long x) { + return (byte) (x >> 8); + } + + public static byte long0(long x) { + return (byte) x; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/FastStringEncoder.java b/src/main/java/net/rubyeye/xmemcached/utils/FastStringEncoder.java new file mode 100644 index 0000000..7f40406 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/FastStringEncoder.java @@ -0,0 +1,110 @@ +package net.rubyeye.xmemcached.utils; + +import java.util.Arrays; + +/** + * Fast string utf encoder. + * + * @author dennis + * + */ +public class FastStringEncoder { + + private static final int STEP = 128; + private static ThreadLocal bufLocal = new ThreadLocal(); + + private static byte[] getBuf(int length) { + byte[] buf = bufLocal.get(); + if (buf != null) { + bufLocal.set(null); + } else { + buf = new byte[length < STEP ? STEP : length]; + } + return buf; + } + + private static void close(byte[] buf) { + if (buf.length <= 1024 * 64) { + bufLocal.set(buf); + } + } + + private static byte[] expandCapacity(byte[] buf, int minNewCapacity) { + int newCapacity = buf.length + (buf.length >> 1) + 1; + if (newCapacity < minNewCapacity) { + newCapacity = minNewCapacity; + } + return Arrays.copyOf(buf, newCapacity); + } + + public static byte[] encodeUTF8(String s) { + int len = s.length(); + byte[] bytes = getBuf(len); + + int offset = 0; + int sl = offset + len; + int dp = 0; + int dlASCII = dp + Math.min(len, bytes.length); + + // ASCII only optimized loop + while (dp < dlASCII && s.charAt(offset) < '\u0080') { + bytes[dp++] = (byte) s.charAt(offset++); + } + + while (offset < sl) { + if (dp >= bytes.length - 4) { + bytes = expandCapacity(bytes, bytes.length + STEP * 2); + } + char c = s.charAt(offset++); + if (c < 0x80) { + // Have at most seven bits + bytes[dp++] = (byte) c; + } else if (c < 0x800) { + // 2 bytes, 11 bits + bytes[dp++] = (byte) (0xc0 | c >> 6); + bytes[dp++] = (byte) (0x80 | c & 0x3f); + } else if (c >= '\uD800' && c < '\uDFFF' + 1) { // Character.isSurrogate(c) + // but 1.7 + final int uc; + int ip = offset - 1; + if (Character.isHighSurrogate(c)) { + if (sl - ip < 2) { + uc = -1; + } else { + char d = s.charAt(ip + 1); + if (Character.isLowSurrogate(d)) { + uc = Character.toCodePoint(c, d); + } else { + throw new IllegalStateException("encodeUTF8 error"); + } + } + } else { + if (Character.isLowSurrogate(c)) { + throw new IllegalStateException("encodeUTF8 error"); + } else { + uc = c; + } + } + + if (uc < 0) { + bytes[dp++] = (byte) '?'; + } else { + bytes[dp++] = (byte) (0xf0 | uc >> 18); + bytes[dp++] = (byte) (0x80 | uc >> 12 & 0x3f); + bytes[dp++] = (byte) (0x80 | uc >> 6 & 0x3f); + bytes[dp++] = (byte) (0x80 | uc & 0x3f); + offset++; // 2 chars + } + } else { + // 3 bytes, 16 bits + bytes[dp++] = (byte) (0xe0 | c >> 12); + bytes[dp++] = (byte) (0x80 | c >> 6 & 0x3f); + bytes[dp++] = (byte) (0x80 | c & 0x3f); + } + } + byte[] resultBytes = new byte[dp]; + System.arraycopy(bytes, 0, resultBytes, 0, dp); + close(bytes); + return resultBytes; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/InetSocketAddressWrapper.java b/src/main/java/net/rubyeye/xmemcached/utils/InetSocketAddressWrapper.java new file mode 100644 index 0000000..e580cce --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/InetSocketAddressWrapper.java @@ -0,0 +1,110 @@ +package net.rubyeye.xmemcached.utils; + +import java.net.InetSocketAddress; + +/** + * InetSocketAddress wrapper,encapsulate an order number. + * + * @author dennis + * + */ +public class InetSocketAddressWrapper { + private volatile InetSocketAddress inetSocketAddress; + private int order; // The address order in list + private int weight; // The weight of this address + private volatile String remoteAddressStr; + private volatile String hostName; + private volatile String mainNodeHostName; + private boolean resolve; + /** + * Main memcached node address,if this is a main node,then this value is null. + */ + private volatile InetSocketAddress mainNodeAddress; + + public InetSocketAddressWrapper(InetSocketAddress inetSocketAddress, int order, int weight, + InetSocketAddress mainNodeAddress) { + this(inetSocketAddress, order, weight, mainNodeAddress, true); + } + + public InetSocketAddressWrapper(InetSocketAddress inetSocketAddress, int order, int weight, + InetSocketAddress mainNodeAddress, boolean resolve) { + super(); + this.resolve = resolve; + setInetSocketAddress(inetSocketAddress); + setMainNodeAddress(mainNodeAddress); + this.order = order; + this.weight = weight; + } + + public String getRemoteAddressStr() { + return this.remoteAddressStr; + } + + public void setRemoteAddressStr(String remoteAddressStr) { + this.remoteAddressStr = remoteAddressStr; + } + + public final InetSocketAddress getInetSocketAddress() { + if (resolve && ByteUtils.isValidString(this.hostName)) { + // If it has a hostName, we try to resolve it again. + return new InetSocketAddress(this.hostName, this.inetSocketAddress.getPort()); + } else { + return this.inetSocketAddress; + } + } + + public final InetSocketAddress getResolvedSocketAddress() { + return this.inetSocketAddress; + } + + public final void setResolvedSocketAddress(InetSocketAddress addr) { + this.inetSocketAddress = addr; + } + + public final InetSocketAddress getResolvedMainNodeSocketAddress() { + return this.mainNodeAddress; + } + + public final void setResolvedMainNodeSocketAddress(InetSocketAddress addr) { + this.mainNodeAddress = addr; + } + + private final void setInetSocketAddress(InetSocketAddress inetSocketAddress) { + this.inetSocketAddress = inetSocketAddress; + if (resolve && inetSocketAddress != null) { + this.hostName = inetSocketAddress.getHostName(); + } + } + + public final int getOrder() { + return this.order; + } + + public int getWeight() { + return this.weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public InetSocketAddress getMainNodeAddress() { + if (resolve && ByteUtils.isValidString(this.mainNodeHostName)) { + return new InetSocketAddress(this.mainNodeHostName, this.mainNodeAddress.getPort()); + } else { + return this.mainNodeAddress; + } + } + + private void setMainNodeAddress(InetSocketAddress mainNodeAddress) { + this.mainNodeAddress = mainNodeAddress; + if (resolve && mainNodeAddress != null) { + this.mainNodeHostName = mainNodeAddress.getHostName(); + } + } + + public final void setOrder(int order) { + this.order = order; + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/OpaqueGenerater.java b/src/main/java/net/rubyeye/xmemcached/utils/OpaqueGenerater.java new file mode 100644 index 0000000..14fa3f0 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/OpaqueGenerater.java @@ -0,0 +1,59 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.utils; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Opaque generator for memcached binary xxxq(getq,addq etc.) commands + * + * @author dennis + * + */ +public final class OpaqueGenerater { + private OpaqueGenerater() { + + } + + private AtomicInteger counter = new AtomicInteger(0); + + static final class SingletonHolder { + static final OpaqueGenerater opaqueGenerater = new OpaqueGenerater(); + } + + public static OpaqueGenerater getInstance() { + return SingletonHolder.opaqueGenerater; + } + + // just for test. + public void setValue(int v) { + this.counter.set(v); + } + + public int getNextValue() { + int val = counter.incrementAndGet(); + if (val < 0) { + while (val < 0 && !counter.compareAndSet(val, 0)) { + val = counter.get(); + } + return counter.incrementAndGet(); + } else { + return val; + } + } + +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/Protocol.java b/src/main/java/net/rubyeye/xmemcached/utils/Protocol.java new file mode 100644 index 0000000..9bc435d --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/Protocol.java @@ -0,0 +1,27 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.utils; + +/** + * Memcached protocol enum + * + * @author dennis + * + */ +public enum Protocol { + Text, Binary, Kestrel +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/XMemcachedClientFactoryBean.java b/src/main/java/net/rubyeye/xmemcached/utils/XMemcachedClientFactoryBean.java new file mode 100644 index 0000000..a11d81f --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/XMemcachedClientFactoryBean.java @@ -0,0 +1,377 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 + */ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.utils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.KeyProvider; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.MemcachedSessionComparator; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.autodiscovery.AutoDiscoveryCacheClient; +import net.rubyeye.xmemcached.autodiscovery.AutoDiscoveryCacheClientBuilder; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.DefaultKeyProvider; +import net.rubyeye.xmemcached.impl.IndexMemcachedSessionComparator; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import org.springframework.beans.factory.FactoryBean; +import com.google.code.yanf4j.config.Configuration; + +/** + * Implement spring's factory bean,for integrating to spring framework. + * + * @author dennis + * + */ +public class XMemcachedClientFactoryBean implements FactoryBean { + + private MemcachedSessionLocator sessionLocator = new ArrayMemcachedSessionLocator(); + private MemcachedSessionComparator sessionComparator = new IndexMemcachedSessionComparator(); + private BufferAllocator bufferAllocator = new SimpleBufferAllocator(); + private String servers; + private String autoDiscoveryServers; + private List weights; + @SuppressWarnings("unchecked") + private Transcoder transcoder = new SerializingTranscoder(); + private Configuration configuration = XMemcachedClientBuilder.getDefaultConfiguration(); + private CommandFactory commandFactory = new TextCommandFactory(); + + private Map authInfoMap = new HashMap(); + + private String name; + + private int connectionPoolSize = MemcachedClient.DEFAULT_CONNECTION_POOL_SIZE; + + private MemcachedClient memcachedClient; + + private boolean failureMode; + + private long opTimeout = MemcachedClient.DEFAULT_OP_TIMEOUT; + + private long connectTimeout = MemcachedClient.DEFAULT_CONNECT_TIMEOUT; + + private KeyProvider keyProvider = DefaultKeyProvider.INSTANCE; + + private int maxQueuedNoReplyOperations = MemcachedClient.DEFAULT_MAX_QUEUED_NOPS; + + private long healSessionInterval = MemcachedClient.DEFAULT_HEAL_SESSION_INTERVAL; + + private boolean enableHealSession = true; + + private int timeoutExceptionThreshold = MemcachedClient.DEFAULT_MAX_TIMEOUTEXCEPTION_THRESHOLD; + + private long pollConfigIntervalMs = AutoDiscoveryCacheClient.DEFAULT_POLL_CONFIG_INTERVAL_MS; + + public int getTimeoutExceptionThreshold() { + return timeoutExceptionThreshold; + } + + public void setTimeoutExceptionThreshold(int timeoutExceptionThreshold) { + this.timeoutExceptionThreshold = timeoutExceptionThreshold; + } + + public long getHealSessionInterval() { + return healSessionInterval; + } + + public void setHealSessionInterval(long healSessionInterval) { + this.healSessionInterval = healSessionInterval; + } + + public boolean isEnableHealSession() { + return enableHealSession; + } + + public void setEnableHealSession(boolean enableHealSession) { + this.enableHealSession = enableHealSession; + } + + public long getOpTimeout() { + return opTimeout; + } + + public KeyProvider getKeyProvider() { + return keyProvider; + } + + public void setKeyProvider(KeyProvider keyProvider) { + this.keyProvider = keyProvider; + } + + public void setOpTimeout(long opTimeout) { + this.opTimeout = opTimeout; + } + + public final CommandFactory getCommandFactory() { + return this.commandFactory; + } + + public final void setCommandFactory(CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + public XMemcachedClientFactoryBean() { + + } + + public Map getAuthInfoMap() { + return this.authInfoMap; + } + + public void setAuthInfoMap(Map authInfoMap) { + this.authInfoMap = authInfoMap; + } + + public boolean isFailureMode() { + return this.failureMode; + } + + public void setFailureMode(boolean failureMode) { + this.failureMode = failureMode; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public int getConnectionPoolSize() { + return this.connectionPoolSize; + } + + public final void setConnectionPoolSize(int poolSize) { + this.connectionPoolSize = poolSize; + } + + public void setSessionLocator(MemcachedSessionLocator sessionLocator) { + this.sessionLocator = sessionLocator; + } + + public void setSessionComparator(MemcachedSessionComparator sessionComparator) { + this.sessionComparator = sessionComparator; + } + + public void setBufferAllocator(BufferAllocator bufferAllocator) { + this.bufferAllocator = bufferAllocator; + } + + @SuppressWarnings("unchecked") + public void setTranscoder(Transcoder transcoder) { + this.transcoder = transcoder; + } + + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + } + + public String getServers() { + return this.servers; + } + + public void setServers(String servers) { + this.servers = servers; + } + + public MemcachedSessionLocator getSessionLocator() { + return this.sessionLocator; + } + + public MemcachedSessionComparator getSessionComparator() { + return this.sessionComparator; + } + + public BufferAllocator getBufferAllocator() { + return this.bufferAllocator; + } + + @SuppressWarnings("unchecked") + public Transcoder getTranscoder() { + return this.transcoder; + } + + public List getWeights() { + return this.weights; + } + + /** + * Set max queued noreply operations number + * + * @see MemcachedClient#DEFAULT_MAX_QUEUED_NOPS + * @param maxQueuedNoReplyOperations + * @since 1.3.8 + */ + public void setMaxQueuedNoReplyOperations(int maxQueuedNoReplyOperations) { + if (maxQueuedNoReplyOperations <= 1) + throw new IllegalArgumentException("maxQueuedNoReplyOperations<=1"); + this.maxQueuedNoReplyOperations = maxQueuedNoReplyOperations; + } + + public void setWeights(List weights) { + this.weights = weights; + } + + public Configuration getConfiguration() { + return this.configuration; + } + + public Object getObject() throws Exception { + this.checkAttribute(); + MemcachedClientBuilder builder = this.newBuilder(); + this.configBuilder(builder); + this.memcachedClient = builder.build(); + this.memcachedClient.setOpTimeout(opTimeout); + this.memcachedClient.setTimeoutExceptionThreshold(timeoutExceptionThreshold); + return this.memcachedClient; + } + + private MemcachedClientBuilder newBuilder() { + if (this.autoDiscoveryServers == null) { + Map serverMap = this.getServerMap(); + int[] weightsArray = this.getWeightsArray(serverMap); + return this.newBuilder(serverMap, weightsArray); + } else { + return this.newBuilder(this.autoDiscoveryServers); + } + } + + private AutoDiscoveryCacheClientBuilder newBuilder(String autoDiscoveryServers) { + AutoDiscoveryCacheClientBuilder builder = + new AutoDiscoveryCacheClientBuilder(autoDiscoveryServers); + builder.setPollConfigIntervalMs(this.pollConfigIntervalMs); + return builder; + } + + private MemcachedClientBuilder newBuilder(Map serverMap, + int[] weightsArray) { + MemcachedClientBuilder builder; + if (weightsArray == null) { + builder = new XMemcachedClientBuilder(serverMap); + } else { + builder = new XMemcachedClientBuilder(serverMap, weightsArray); + } + return builder; + } + + private void configBuilder(MemcachedClientBuilder builder) { + builder.setConfiguration(this.configuration); + builder.setBufferAllocator(this.bufferAllocator); + builder.setSessionLocator(this.sessionLocator); + builder.setSessionComparator(this.sessionComparator); + builder.setTranscoder(this.transcoder); + builder.setCommandFactory(this.commandFactory); + builder.setConnectionPoolSize(this.connectionPoolSize); + builder.setAuthInfoMap(this.authInfoMap); + builder.setFailureMode(this.failureMode); + builder.setKeyProvider(keyProvider); + builder.setMaxQueuedNoReplyOperations(this.maxQueuedNoReplyOperations); + builder.setName(this.name); + builder.setEnableHealSession(this.enableHealSession); + builder.setHealSessionInterval(this.healSessionInterval); + builder.setConnectTimeout(connectTimeout); + builder.setOpTimeout(opTimeout); + } + + private int[] getWeightsArray(Map serverMap) { + int[] weightsArray = null; + if (serverMap != null && serverMap.size() > 0 && this.weights != null) { + if (this.weights.size() < serverMap.size()) { + throw new IllegalArgumentException("Weight list's size is less than server list's size"); + } + weightsArray = new int[this.weights.size()]; + for (int i = 0; i < weightsArray.length; i++) { + weightsArray[i] = this.weights.get(i); + } + } + return weightsArray; + } + + private Map getServerMap() { + Map serverMap = null; + + if (this.servers != null && this.servers.length() > 0) { + serverMap = AddrUtil.getAddressMap(this.servers); + + } + return serverMap; + } + + private void checkAttribute() { + if (this.bufferAllocator == null) { + throw new NullPointerException("Null BufferAllocator"); + } + if (this.sessionLocator == null) { + throw new NullPointerException("Null MemcachedSessionLocator"); + } + if (this.sessionComparator == null) { + throw new NullPointerException("Null MemcachedSessionComparator"); + } + if (this.configuration == null) { + throw new NullPointerException("Null networking configuration"); + } + if (this.commandFactory == null) { + throw new NullPointerException("Null command factory"); + } + if (this.weights != null && this.servers == null) { + throw new NullPointerException("Empty server list"); + } + } + + public void shutdown() throws IOException { + if (this.memcachedClient != null) { + this.memcachedClient.shutdown(); + } + } + + @SuppressWarnings("rawtypes") + public Class getObjectType() { + return MemcachedClient.class; + } + + public boolean isSingleton() { + return true; + } + + public long getConnectTimeout() { + return connectTimeout; + } + + public void setConnectTimeout(long connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public long getPollConfigIntervalMs() { + return pollConfigIntervalMs; + } + + public void setPollConfigIntervalMs(long pollConfigIntervalMs) { + this.pollConfigIntervalMs = pollConfigIntervalMs; + } +} diff --git a/src/main/java/net/rubyeye/xmemcached/utils/package.html b/src/main/java/net/rubyeye/xmemcached/utils/package.html new file mode 100644 index 0000000..dd65d29 --- /dev/null +++ b/src/main/java/net/rubyeye/xmemcached/utils/package.html @@ -0,0 +1,10 @@ + + + + + XMemcached Utilities + + +

XMemcached Utilities

+ + \ No newline at end of file diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/Main.java b/src/test/java/com/google/code/yanf4j/test/unittest/Main.java new file mode 100644 index 0000000..6aaebde --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/Main.java @@ -0,0 +1,18 @@ +package com.google.code.yanf4j.test.unittest; + +public class Main { + public static void main(String[] args) { + long start = System.nanoTime(); + int threadNum = 10000; + int sum = 0; + for (int i = 0; i < threadNum; i++) + sum += testLock(i, i * 2); + System.out.println((System.nanoTime() - start) / threadNum); + System.out.println(sum); + } + + public final static synchronized int testLock(int a, int b) { + return a + b; + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/buffer/Bar.java b/src/test/java/com/google/code/yanf4j/test/unittest/buffer/Bar.java new file mode 100644 index 0000000..f1fd403 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/buffer/Bar.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.test.unittest.buffer; + +/** + * The subtype of {@link Foo}. It is used to test the serialization of inherited object in + * {@link IoBufferTest}. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + */ +public class Bar extends Foo { + + private static final long serialVersionUID = -7360624845308368521L; + + private int barValue; + + public int getBarValue() { + return barValue; + } + + public void setBarValue(int barValue) { + this.barValue = barValue; + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/buffer/Foo.java b/src/test/java/com/google/code/yanf4j/test/unittest/buffer/Foo.java new file mode 100644 index 0000000..bc03bba --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/buffer/Foo.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.test.unittest.buffer; + +import java.io.Serializable; + +/** + * The parent class of {@link Bar}. It is used to test the serialization of inherited object in + * {@link IoBufferTest}. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 671827 $, $Date: 2008-06-26 10:49:48 +0200 (Thu, 26 Jun 2008) $ + */ +public class Foo implements Serializable { + + private static final long serialVersionUID = 6467037996528575216L; + + private int fooValue; + + public int getFooValue() { + return fooValue; + } + + public void setFooValue(int fooValue) { + this.fooValue = fooValue; + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/buffer/IoBufferTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/buffer/IoBufferTest.java new file mode 100644 index 0000000..56130e0 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/buffer/IoBufferTest.java @@ -0,0 +1,1121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to you 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.google.code.yanf4j.test.unittest.buffer; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ReadOnlyBufferException; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import com.google.code.yanf4j.buffer.IoBuffer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + + +/** + * Tests {@link IoBuffer}. + * + * @author The Apache MINA Project (dev@mina.apache.org) + * @version $Rev: 754874 $, $Date: 2009-03-16 12:04:01 +0100 (Mon, 16 Mar 2009) $ + */ +public class IoBufferTest { + + /* + * public static void main(String[] args) { junit.textui.TestRunner.run(IoBufferTest.class); } + */ + + @BeforeEach + public void setUp() throws Exception {} + + @AfterEach + public void tearDown() throws Exception {} + + @Test + public void testAllocate() throws Exception { + for (int i = 10; i < 1048576 * 2; i = i * 11 / 10) // increase by 10% + { + IoBuffer buf = IoBuffer.allocate(i); + assertEquals(0, buf.position()); + assertEquals(buf.capacity(), buf.remaining()); + assertTrue(buf.capacity() >= i); + assertTrue(buf.capacity() < i * 2); + } + } + + @Test + public void testAutoExpand() throws Exception { + IoBuffer buf = IoBuffer.allocate(1); + + buf.put((byte) 0); + try { + buf.put((byte) 0); + fail("Buffer can't auto expand, with autoExpand property set at false"); + } catch (BufferOverflowException e) { + // Expected Exception as auto expand property is false + assertTrue(true); + } + + buf.setAutoExpand(true); + buf.put((byte) 0); + assertEquals(2, buf.position()); + assertEquals(2, buf.limit()); + assertEquals(2, buf.capacity()); + + buf.setAutoExpand(false); + try { + buf.put(3, (byte) 0); + fail("Buffer can't auto expand, with autoExpand property set at false"); + } catch (IndexOutOfBoundsException e) { + // Expected Exception as auto expand property is false + assertTrue(true); + } + + buf.setAutoExpand(true); + buf.put(3, (byte) 0); + assertEquals(2, buf.position()); + assertEquals(4, buf.limit()); + assertEquals(4, buf.capacity()); + + // Make sure the buffer is doubled up. + buf = IoBuffer.allocate(1).setAutoExpand(true); + int lastCapacity = buf.capacity(); + for (int i = 0; i < 1048576; i++) { + buf.put((byte) 0); + if (lastCapacity != buf.capacity()) { + assertEquals(lastCapacity * 2, buf.capacity()); + lastCapacity = buf.capacity(); + } + } + } + + @Test + public void testAutoExpandMark() throws Exception { + IoBuffer buf = IoBuffer.allocate(4).setAutoExpand(true); + + buf.put((byte) 0); + buf.put((byte) 0); + buf.put((byte) 0); + + // Position should be 3 when we reset this buffer. + buf.mark(); + + // Overflow it + buf.put((byte) 0); + buf.put((byte) 0); + + assertEquals(5, buf.position()); + buf.reset(); + assertEquals(3, buf.position()); + } + + @Test + public void testAutoShrink() throws Exception { + IoBuffer buf = IoBuffer.allocate(8).setAutoShrink(true); + + // Make sure the buffer doesn't shrink too much (less than the initial + // capacity.) + buf.sweep((byte) 1); + buf.fill(7); + buf.compact(); + assertEquals(8, buf.capacity()); + assertEquals(1, buf.position()); + assertEquals(8, buf.limit()); + buf.clear(); + assertEquals(1, buf.get()); + + // Expand the buffer. + buf.capacity(32).clear(); + assertEquals(32, buf.capacity()); + + // Make sure the buffer shrinks when only 1/4 is being used. + buf.sweep((byte) 1); + buf.fill(24); + buf.compact(); + assertEquals(16, buf.capacity()); + assertEquals(8, buf.position()); + assertEquals(16, buf.limit()); + buf.clear(); + for (int i = 0; i < 8; i++) { + assertEquals(1, buf.get()); + } + + // Expand the buffer. + buf.capacity(32).clear(); + assertEquals(32, buf.capacity()); + + // Make sure the buffer shrinks when only 1/8 is being used. + buf.sweep((byte) 1); + buf.fill(28); + buf.compact(); + assertEquals(8, buf.capacity()); + assertEquals(4, buf.position()); + assertEquals(8, buf.limit()); + buf.clear(); + for (int i = 0; i < 4; i++) { + assertEquals(1, buf.get()); + } + + // Expand the buffer. + buf.capacity(32).clear(); + assertEquals(32, buf.capacity()); + + // Make sure the buffer shrinks when 0 byte is being used. + buf.fill(32); + buf.compact(); + assertEquals(8, buf.capacity()); + assertEquals(0, buf.position()); + assertEquals(8, buf.limit()); + + // Expand the buffer. + buf.capacity(32).clear(); + assertEquals(32, buf.capacity()); + + // Make sure the buffer doesn't shrink when more than 1/4 is being used. + buf.sweep((byte) 1); + buf.fill(23); + buf.compact(); + assertEquals(32, buf.capacity()); + assertEquals(9, buf.position()); + assertEquals(32, buf.limit()); + buf.clear(); + for (int i = 0; i < 9; i++) { + assertEquals(1, buf.get()); + } + } + + @Test + public void testGetString() throws Exception { + IoBuffer buf = IoBuffer.allocate(16); + CharsetDecoder decoder; + + Charset charset = Charset.forName("UTF-8"); + buf.clear(); + buf.putString("hello", charset.newEncoder()); + buf.put((byte) 0); + buf.flip(); + assertEquals("hello", buf.getString(charset.newDecoder())); + + buf.clear(); + buf.putString("hello", charset.newEncoder()); + buf.flip(); + assertEquals("hello", buf.getString(charset.newDecoder())); + + decoder = Charset.forName("ISO-8859-1").newDecoder(); + buf.clear(); + buf.put((byte) 'A'); + buf.put((byte) 'B'); + buf.put((byte) 'C'); + buf.put((byte) 0); + + buf.position(0); + assertEquals("ABC", buf.getString(decoder)); + assertEquals(4, buf.position()); + + buf.position(0); + buf.limit(1); + assertEquals("A", buf.getString(decoder)); + assertEquals(1, buf.position()); + + buf.clear(); + assertEquals("ABC", buf.getString(10, decoder)); + assertEquals(10, buf.position()); + + buf.clear(); + assertEquals("A", buf.getString(1, decoder)); + assertEquals(1, buf.position()); + + // Test a trailing garbage + buf.clear(); + buf.put((byte) 'A'); + buf.put((byte) 'B'); + buf.put((byte) 0); + buf.put((byte) 'C'); + buf.position(0); + assertEquals("AB", buf.getString(4, decoder)); + assertEquals(4, buf.position()); + + buf.clear(); + buf.fillAndReset(buf.limit()); + decoder = Charset.forName("UTF-16").newDecoder(); + buf.put((byte) 0); + buf.put((byte) 'A'); + buf.put((byte) 0); + buf.put((byte) 'B'); + buf.put((byte) 0); + buf.put((byte) 'C'); + buf.put((byte) 0); + buf.put((byte) 0); + + buf.position(0); + assertEquals("ABC", buf.getString(decoder)); + assertEquals(8, buf.position()); + + buf.position(0); + buf.limit(2); + assertEquals("A", buf.getString(decoder)); + assertEquals(2, buf.position()); + + buf.position(0); + buf.limit(3); + assertEquals("A", buf.getString(decoder)); + assertEquals(2, buf.position()); + + buf.clear(); + assertEquals("ABC", buf.getString(10, decoder)); + assertEquals(10, buf.position()); + + buf.clear(); + assertEquals("A", buf.getString(2, decoder)); + assertEquals(2, buf.position()); + + buf.clear(); + try { + buf.getString(1, decoder); + fail(); + } catch (IllegalArgumentException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + + // Test getting strings from an empty buffer. + buf.clear(); + buf.limit(0); + assertEquals("", buf.getString(decoder)); + assertEquals("", buf.getString(2, decoder)); + + // Test getting strings from non-empty buffer which is filled with 0x00 + buf.clear(); + buf.putInt(0); + buf.clear(); + buf.limit(4); + assertEquals("", buf.getString(decoder)); + assertEquals(2, buf.position()); + assertEquals(4, buf.limit()); + + buf.position(0); + assertEquals("", buf.getString(2, decoder)); + assertEquals(2, buf.position()); + assertEquals(4, buf.limit()); + } + + @Test + public void testGetStringWithFailure() throws Exception { + String test = "\u30b3\u30e1\u30f3\u30c8\u7de8\u96c6"; + IoBuffer buffer = IoBuffer.wrap(test.getBytes("Shift_JIS")); + + // Make sure the limit doesn't change when an exception arose. + int oldLimit = buffer.limit(); + int oldPos = buffer.position(); + try { + buffer.getString(3, Charset.forName("ASCII").newDecoder()); + fail(); + } catch (Exception e) { + assertEquals(oldLimit, buffer.limit()); + assertEquals(oldPos, buffer.position()); + } + + try { + buffer.getString(Charset.forName("ASCII").newDecoder()); + fail(); + } catch (Exception e) { + assertEquals(oldLimit, buffer.limit()); + assertEquals(oldPos, buffer.position()); + } + } + + @Test + public void testPutString() throws Exception { + CharsetEncoder encoder; + IoBuffer buf = IoBuffer.allocate(16); + encoder = Charset.forName("ISO-8859-1").newEncoder(); + + buf.putString("ABC", encoder); + assertEquals(3, buf.position()); + buf.clear(); + assertEquals('A', buf.get(0)); + assertEquals('B', buf.get(1)); + assertEquals('C', buf.get(2)); + + buf.putString("D", 5, encoder); + assertEquals(5, buf.position()); + buf.clear(); + assertEquals('D', buf.get(0)); + assertEquals(0, buf.get(1)); + + buf.putString("EFG", 2, encoder); + assertEquals(2, buf.position()); + buf.clear(); + assertEquals('E', buf.get(0)); + assertEquals('F', buf.get(1)); + assertEquals('C', buf.get(2)); // C may not be overwritten + + // UTF-16: We specify byte order to omit BOM. + encoder = Charset.forName("UTF-16BE").newEncoder(); + buf.clear(); + + buf.putString("ABC", encoder); + assertEquals(6, buf.position()); + buf.clear(); + + assertEquals(0, buf.get(0)); + assertEquals('A', buf.get(1)); + assertEquals(0, buf.get(2)); + assertEquals('B', buf.get(3)); + assertEquals(0, buf.get(4)); + assertEquals('C', buf.get(5)); + + buf.putString("D", 10, encoder); + assertEquals(10, buf.position()); + buf.clear(); + assertEquals(0, buf.get(0)); + assertEquals('D', buf.get(1)); + assertEquals(0, buf.get(2)); + assertEquals(0, buf.get(3)); + + buf.putString("EFG", 4, encoder); + assertEquals(4, buf.position()); + buf.clear(); + assertEquals(0, buf.get(0)); + assertEquals('E', buf.get(1)); + assertEquals(0, buf.get(2)); + assertEquals('F', buf.get(3)); + assertEquals(0, buf.get(4)); // C may not be overwritten + assertEquals('C', buf.get(5)); // C may not be overwritten + + // Test putting an emptry string + buf.putString("", encoder); + assertEquals(0, buf.position()); + buf.putString("", 4, encoder); + assertEquals(4, buf.position()); + assertEquals(0, buf.get(0)); + assertEquals(0, buf.get(1)); + } + + @Test + public void testGetPrefixedString() throws Exception { + IoBuffer buf = IoBuffer.allocate(16); + CharsetEncoder encoder; + CharsetDecoder decoder; + encoder = Charset.forName("ISO-8859-1").newEncoder(); + decoder = Charset.forName("ISO-8859-1").newDecoder(); + + buf.putShort((short) 3); + buf.putString("ABCD", encoder); + buf.clear(); + assertEquals("ABC", buf.getPrefixedString(decoder)); + } + + @Test + public void testPutPrefixedString() throws Exception { + CharsetEncoder encoder; + IoBuffer buf = IoBuffer.allocate(16); + buf.fillAndReset(buf.remaining()); + encoder = Charset.forName("ISO-8859-1").newEncoder(); + + // Without autoExpand + buf.putPrefixedString("ABC", encoder); + assertEquals(5, buf.position()); + assertEquals(0, buf.get(0)); + assertEquals(3, buf.get(1)); + assertEquals('A', buf.get(2)); + assertEquals('B', buf.get(3)); + assertEquals('C', buf.get(4)); + + buf.clear(); + try { + buf.putPrefixedString("123456789012345", encoder); + fail(); + } catch (BufferOverflowException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + + // With autoExpand + buf.clear(); + buf.setAutoExpand(true); + buf.putPrefixedString("123456789012345", encoder); + assertEquals(17, buf.position()); + assertEquals(0, buf.get(0)); + assertEquals(15, buf.get(1)); + assertEquals('1', buf.get(2)); + assertEquals('2', buf.get(3)); + assertEquals('3', buf.get(4)); + assertEquals('4', buf.get(5)); + assertEquals('5', buf.get(6)); + assertEquals('6', buf.get(7)); + assertEquals('7', buf.get(8)); + assertEquals('8', buf.get(9)); + assertEquals('9', buf.get(10)); + assertEquals('0', buf.get(11)); + assertEquals('1', buf.get(12)); + assertEquals('2', buf.get(13)); + assertEquals('3', buf.get(14)); + assertEquals('4', buf.get(15)); + assertEquals('5', buf.get(16)); + } + + @Test + public void testPutPrefixedStringWithPrefixLength() throws Exception { + CharsetEncoder encoder = Charset.forName("ISO-8859-1").newEncoder(); + IoBuffer buf = IoBuffer.allocate(16).sweep().setAutoExpand(true); + + buf.putPrefixedString("A", 1, encoder); + assertEquals(2, buf.position()); + assertEquals(1, buf.get(0)); + assertEquals('A', buf.get(1)); + + buf.sweep(); + buf.putPrefixedString("A", 2, encoder); + assertEquals(3, buf.position()); + assertEquals(0, buf.get(0)); + assertEquals(1, buf.get(1)); + assertEquals('A', buf.get(2)); + + buf.sweep(); + buf.putPrefixedString("A", 4, encoder); + assertEquals(5, buf.position()); + assertEquals(0, buf.get(0)); + assertEquals(0, buf.get(1)); + assertEquals(0, buf.get(2)); + assertEquals(1, buf.get(3)); + assertEquals('A', buf.get(4)); + } + + @Test + public void testPutPrefixedStringWithPadding() throws Exception { + CharsetEncoder encoder = Charset.forName("ISO-8859-1").newEncoder(); + IoBuffer buf = IoBuffer.allocate(16).sweep().setAutoExpand(true); + + buf.putPrefixedString("A", 1, 2, (byte) 32, encoder); + assertEquals(3, buf.position()); + assertEquals(2, buf.get(0)); + assertEquals('A', buf.get(1)); + assertEquals(' ', buf.get(2)); + + buf.sweep(); + buf.putPrefixedString("A", 1, 4, (byte) 32, encoder); + assertEquals(5, buf.position()); + assertEquals(4, buf.get(0)); + assertEquals('A', buf.get(1)); + assertEquals(' ', buf.get(2)); + assertEquals(' ', buf.get(3)); + assertEquals(' ', buf.get(4)); + } + + @Test + public void testWideUtf8Characters() throws Exception { + Runnable r = new Runnable() { + public void run() { + IoBuffer buffer = IoBuffer.allocate(1); + buffer.setAutoExpand(true); + + Charset charset = Charset.forName("UTF-8"); + + CharsetEncoder encoder = charset.newEncoder(); + + for (int i = 0; i < 5; i++) { + try { + buffer.putString("\u89d2", encoder); + buffer.putPrefixedString("\u89d2", encoder); + } catch (CharacterCodingException e) { + fail(e.getMessage()); + } + } + } + }; + + Thread t = new Thread(r); + t.setDaemon(true); + t.start(); + + for (int i = 0; i < 50; i++) { + Thread.sleep(100); + if (!t.isAlive()) { + break; + } + } + + if (t.isAlive()) { + t.interrupt(); + + fail("Went into endless loop trying to encode character"); + } + } + + @Test + public void testObjectSerialization() throws Exception { + IoBuffer buf = IoBuffer.allocate(16); + buf.setAutoExpand(true); + List o = new ArrayList(); + o.add(new Date()); + o.add(long.class); + + // Test writing an object. + buf.putObject(o); + + // Test reading an object. + buf.clear(); + Object o2 = buf.getObject(); + assertEquals(o, o2); + + // This assertion is just to make sure that deserialization occurred. + assertNotSame(o, o2); + } + + @Test + public void testInheritedObjectSerialization() throws Exception { + IoBuffer buf = IoBuffer.allocate(16); + buf.setAutoExpand(true); + + Bar expected = new Bar(); + expected.setFooValue(0x12345678); + expected.setBarValue(0x90ABCDEF); + + // Test writing an object. + buf.putObject(expected); + + // Test reading an object. + buf.clear(); + Bar actual = (Bar) buf.getObject(); + assertSame(Bar.class, actual.getClass()); + assertEquals(expected.getFooValue(), actual.getFooValue()); + assertEquals(expected.getBarValue(), actual.getBarValue()); + + // This assertion is just to make sure that deserialization occurred. + assertNotSame(expected, actual); + } + + @Test + public void testSweepWithZeros() throws Exception { + IoBuffer buf = IoBuffer.allocate(4); + buf.putInt(0xdeadbeef); + buf.clear(); + assertEquals(0xdeadbeef, buf.getInt()); + assertEquals(4, buf.position()); + assertEquals(4, buf.limit()); + + buf.sweep(); + assertEquals(0, buf.position()); + assertEquals(4, buf.limit()); + assertEquals(0x0, buf.getInt()); + } + + @Test + public void testSweepNonZeros() throws Exception { + IoBuffer buf = IoBuffer.allocate(4); + buf.putInt(0xdeadbeef); + buf.clear(); + assertEquals(0xdeadbeef, buf.getInt()); + assertEquals(4, buf.position()); + assertEquals(4, buf.limit()); + + buf.sweep((byte) 0x45); + assertEquals(0, buf.position()); + assertEquals(4, buf.limit()); + assertEquals(0x45454545, buf.getInt()); + } + + @Test + public void testWrapNioBuffer() throws Exception { + ByteBuffer nioBuf = ByteBuffer.allocate(10); + nioBuf.position(3); + nioBuf.limit(7); + + IoBuffer buf = IoBuffer.wrap(nioBuf); + assertEquals(3, buf.position()); + assertEquals(7, buf.limit()); + assertEquals(10, buf.capacity()); + } + + @Test + public void testWrapSubArray() throws Exception { + byte[] array = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + IoBuffer buf = IoBuffer.wrap(array, 3, 4); + assertEquals(3, buf.position()); + assertEquals(7, buf.limit()); + assertEquals(10, buf.capacity()); + + buf.clear(); + assertEquals(0, buf.position()); + assertEquals(10, buf.limit()); + assertEquals(10, buf.capacity()); + } + + @Test + public void testDuplicate() throws Exception { + IoBuffer original; + IoBuffer duplicate; + + // Test if the buffer is duplicated correctly. + original = IoBuffer.allocate(16).sweep(); + original.position(4); + original.limit(10); + duplicate = original.duplicate(); + original.put(4, (byte) 127); + assertEquals(4, duplicate.position()); + assertEquals(10, duplicate.limit()); + assertEquals(16, duplicate.capacity()); + assertNotSame(original.buf(), duplicate.buf()); + assertSame(original.buf().array(), duplicate.buf().array()); + assertEquals(127, duplicate.get(4)); + + // Test a duplicate of a duplicate. + original = IoBuffer.allocate(16); + duplicate = original.duplicate().duplicate(); + assertNotSame(original.buf(), duplicate.buf()); + assertSame(original.buf().array(), duplicate.buf().array()); + + // Try to expand. + original = IoBuffer.allocate(16); + original.setAutoExpand(true); + duplicate = original.duplicate(); + assertFalse(original.isAutoExpand()); + + try { + original.setAutoExpand(true); + fail("Derived buffers and their parent can't be expanded"); + } catch (IllegalStateException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + + try { + duplicate.setAutoExpand(true); + fail("Derived buffers and their parent can't be expanded"); + } catch (IllegalStateException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + } + + @Test + public void testSlice() throws Exception { + IoBuffer original; + IoBuffer slice; + + // Test if the buffer is sliced correctly. + original = IoBuffer.allocate(16).sweep(); + original.position(4); + original.limit(10); + slice = original.slice(); + original.put(4, (byte) 127); + assertEquals(0, slice.position()); + assertEquals(6, slice.limit()); + assertEquals(6, slice.capacity()); + assertNotSame(original.buf(), slice.buf()); + assertEquals(127, slice.get(0)); + } + + @Test + @Disabled + public void testReadOnlyBuffer() throws Exception { + IoBuffer original; + IoBuffer duplicate; + + // Test if the buffer is duplicated correctly. + original = IoBuffer.allocate(16).sweep(); + original.position(4); + original.limit(10); + duplicate = original.asReadOnlyBuffer(); + original.put(4, (byte) 127); + assertEquals(4, duplicate.position()); + assertEquals(10, duplicate.limit()); + assertEquals(16, duplicate.capacity()); + assertNotSame(original.buf(), duplicate.buf()); + assertEquals(127, duplicate.get(4)); + + // Try to expand. + try { + original = IoBuffer.allocate(16); + duplicate = original.asReadOnlyBuffer(); + duplicate.putString("A very very very very looooooong string", + Charset.forName("ISO-8859-1").newEncoder()); + fail("ReadOnly buffer's can't be expanded"); + } catch (ReadOnlyBufferException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + } + + @Test + public void testGetUnsigned() throws Exception { + IoBuffer buf = IoBuffer.allocate(16); + buf.put((byte) 0xA4); + buf.put((byte) 0xD0); + buf.put((byte) 0xB3); + buf.put((byte) 0xCD); + buf.flip(); + + buf.order(ByteOrder.LITTLE_ENDIAN); + + buf.mark(); + assertEquals(0xA4, buf.getUnsigned()); + buf.reset(); + assertEquals(0xD0A4, buf.getUnsignedShort()); + buf.reset(); + assertEquals(0xCDB3D0A4L, buf.getUnsignedInt()); + } + + @Test + public void testIndexOf() throws Exception { + boolean direct = false; + for (int i = 0; i < 2; i++, direct = !direct) { + IoBuffer buf = IoBuffer.allocate(16, direct); + buf.put((byte) 0x1); + buf.put((byte) 0x2); + buf.put((byte) 0x3); + buf.put((byte) 0x4); + buf.put((byte) 0x1); + buf.put((byte) 0x2); + buf.put((byte) 0x3); + buf.put((byte) 0x4); + buf.position(2); + buf.limit(5); + + assertEquals(4, buf.indexOf((byte) 0x1)); + assertEquals(-1, buf.indexOf((byte) 0x2)); + assertEquals(2, buf.indexOf((byte) 0x3)); + assertEquals(3, buf.indexOf((byte) 0x4)); + } + } + + // We need an enum with 64 values + private static enum TestEnum { + E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15, E16, E17, E18, E19, E20, E21, E22, E23, E24, E25, E26, E27, E28, E29, E30, E31, E32, E33, E34, E35, E36, E37, E38, E39, E40, E41, E42, E43, E44, E45, E46, E77, E48, E49, E50, E51, E52, E53, E54, E55, E56, E57, E58, E59, E60, E61, E62, E63, E64 + } + + private static enum TooBigEnum { + E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15, E16, E17, E18, E19, E20, E21, E22, E23, E24, E25, E26, E27, E28, E29, E30, E31, E32, E33, E34, E35, E36, E37, E38, E39, E40, E41, E42, E43, E44, E45, E46, E77, E48, E49, E50, E51, E52, E53, E54, E55, E56, E57, E58, E59, E60, E61, E62, E63, E64, E65 + } + + @Test + public void testPutEnumSet() { + IoBuffer buf = IoBuffer.allocate(8); + + // Test empty set + buf.putEnumSet(EnumSet.noneOf(TestEnum.class)); + buf.flip(); + assertEquals(0, buf.get()); + + buf.clear(); + buf.putEnumSetShort(EnumSet.noneOf(TestEnum.class)); + buf.flip(); + assertEquals(0, buf.getShort()); + + buf.clear(); + buf.putEnumSetInt(EnumSet.noneOf(TestEnum.class)); + buf.flip(); + assertEquals(0, buf.getInt()); + + buf.clear(); + buf.putEnumSetLong(EnumSet.noneOf(TestEnum.class)); + buf.flip(); + assertEquals(0, buf.getLong()); + + // Test complete set + buf.clear(); + buf.putEnumSet(EnumSet.range(TestEnum.E1, TestEnum.E8)); + buf.flip(); + assertEquals((byte) -1, buf.get()); + + buf.clear(); + buf.putEnumSetShort(EnumSet.range(TestEnum.E1, TestEnum.E16)); + buf.flip(); + assertEquals((short) -1, buf.getShort()); + + buf.clear(); + buf.putEnumSetInt(EnumSet.range(TestEnum.E1, TestEnum.E32)); + buf.flip(); + assertEquals(-1, buf.getInt()); + + buf.clear(); + buf.putEnumSetLong(EnumSet.allOf(TestEnum.class)); + buf.flip(); + assertEquals(-1L, buf.getLong()); + + // Test high bit set + buf.clear(); + buf.putEnumSet(EnumSet.of(TestEnum.E8)); + buf.flip(); + assertEquals(Byte.MIN_VALUE, buf.get()); + + buf.clear(); + buf.putEnumSetShort(EnumSet.of(TestEnum.E16)); + buf.flip(); + assertEquals(Short.MIN_VALUE, buf.getShort()); + + buf.clear(); + buf.putEnumSetInt(EnumSet.of(TestEnum.E32)); + buf.flip(); + assertEquals(Integer.MIN_VALUE, buf.getInt()); + + buf.clear(); + buf.putEnumSetLong(EnumSet.of(TestEnum.E64)); + buf.flip(); + assertEquals(Long.MIN_VALUE, buf.getLong()); + + // Test high low bits set + buf.clear(); + buf.putEnumSet(EnumSet.of(TestEnum.E1, TestEnum.E8)); + buf.flip(); + assertEquals(Byte.MIN_VALUE + 1, buf.get()); + + buf.clear(); + buf.putEnumSetShort(EnumSet.of(TestEnum.E1, TestEnum.E16)); + buf.flip(); + assertEquals(Short.MIN_VALUE + 1, buf.getShort()); + + buf.clear(); + buf.putEnumSetInt(EnumSet.of(TestEnum.E1, TestEnum.E32)); + buf.flip(); + assertEquals(Integer.MIN_VALUE + 1, buf.getInt()); + + buf.clear(); + buf.putEnumSetLong(EnumSet.of(TestEnum.E1, TestEnum.E64)); + buf.flip(); + assertEquals(Long.MIN_VALUE + 1, buf.getLong()); + } + + @Test + public void testGetEnumSet() { + IoBuffer buf = IoBuffer.allocate(8); + + // Test empty set + buf.put((byte) 0); + buf.flip(); + assertEquals(EnumSet.noneOf(TestEnum.class), buf.getEnumSet(TestEnum.class)); + + buf.clear(); + buf.putShort((short) 0); + buf.flip(); + assertEquals(EnumSet.noneOf(TestEnum.class), buf.getEnumSet(TestEnum.class)); + + buf.clear(); + buf.putInt(0); + buf.flip(); + assertEquals(EnumSet.noneOf(TestEnum.class), buf.getEnumSet(TestEnum.class)); + + buf.clear(); + buf.putLong(0L); + buf.flip(); + assertEquals(EnumSet.noneOf(TestEnum.class), buf.getEnumSet(TestEnum.class)); + + // Test complete set + buf.clear(); + buf.put((byte) -1); + buf.flip(); + assertEquals(EnumSet.range(TestEnum.E1, TestEnum.E8), buf.getEnumSet(TestEnum.class)); + + buf.clear(); + buf.putShort((short) -1); + buf.flip(); + assertEquals(EnumSet.range(TestEnum.E1, TestEnum.E16), buf.getEnumSetShort(TestEnum.class)); + + buf.clear(); + buf.putInt(-1); + buf.flip(); + assertEquals(EnumSet.range(TestEnum.E1, TestEnum.E32), buf.getEnumSetInt(TestEnum.class)); + + buf.clear(); + buf.putLong(-1L); + buf.flip(); + assertEquals(EnumSet.allOf(TestEnum.class), buf.getEnumSetLong(TestEnum.class)); + + // Test high bit set + buf.clear(); + buf.put(Byte.MIN_VALUE); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E8), buf.getEnumSet(TestEnum.class)); + + buf.clear(); + buf.putShort(Short.MIN_VALUE); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E16), buf.getEnumSetShort(TestEnum.class)); + + buf.clear(); + buf.putInt(Integer.MIN_VALUE); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E32), buf.getEnumSetInt(TestEnum.class)); + + buf.clear(); + buf.putLong(Long.MIN_VALUE); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E64), buf.getEnumSetLong(TestEnum.class)); + + // Test high low bits set + buf.clear(); + byte b = Byte.MIN_VALUE + 1; + buf.put(b); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E1, TestEnum.E8), buf.getEnumSet(TestEnum.class)); + + buf.clear(); + short s = Short.MIN_VALUE + 1; + buf.putShort(s); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E1, TestEnum.E16), buf.getEnumSetShort(TestEnum.class)); + + buf.clear(); + buf.putInt(Integer.MIN_VALUE + 1); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E1, TestEnum.E32), buf.getEnumSetInt(TestEnum.class)); + + buf.clear(); + buf.putLong(Long.MIN_VALUE + 1); + buf.flip(); + assertEquals(EnumSet.of(TestEnum.E1, TestEnum.E64), buf.getEnumSetLong(TestEnum.class)); + } + + @Test + public void testBitVectorOverFlow() { + IoBuffer buf = IoBuffer.allocate(8); + try { + buf.putEnumSet(EnumSet.of(TestEnum.E9)); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + + try { + buf.putEnumSetShort(EnumSet.of(TestEnum.E17)); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + + try { + buf.putEnumSetInt(EnumSet.of(TestEnum.E33)); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + + try { + buf.putEnumSetLong(EnumSet.of(TooBigEnum.E65)); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected an Exception, signifies test success + assertTrue(true); + } + } + + @Test + public void testGetPutEnum() { + IoBuffer buf = IoBuffer.allocate(4); + + buf.putEnum(TestEnum.E64); + buf.flip(); + assertEquals(TestEnum.E64, buf.getEnum(TestEnum.class)); + + buf.clear(); + buf.putEnumShort(TestEnum.E64); + buf.flip(); + assertEquals(TestEnum.E64, buf.getEnumShort(TestEnum.class)); + + buf.clear(); + buf.putEnumInt(TestEnum.E64); + buf.flip(); + assertEquals(TestEnum.E64, buf.getEnumInt(TestEnum.class)); + } + + @Test + public void testGetMediumInt() { + IoBuffer buf = IoBuffer.allocate(3); + + buf.put((byte) 0x01); + buf.put((byte) 0x02); + buf.put((byte) 0x03); + assertEquals(3, buf.position()); + + buf.flip(); + assertEquals(0x010203, buf.getMediumInt()); + assertEquals(0x010203, buf.getMediumInt(0)); + buf.flip(); + assertEquals(0x010203, buf.getUnsignedMediumInt()); + assertEquals(0x010203, buf.getUnsignedMediumInt(0)); + buf.flip(); + assertEquals(0x010203, buf.getUnsignedMediumInt()); + buf.flip().order(ByteOrder.LITTLE_ENDIAN); + assertEquals(0x030201, buf.getMediumInt()); + assertEquals(0x030201, buf.getMediumInt(0)); + + // Test max medium int + buf.flip().order(ByteOrder.BIG_ENDIAN); + buf.put((byte) 0x7f); + buf.put((byte) 0xff); + buf.put((byte) 0xff); + buf.flip(); + assertEquals(0x7fffff, buf.getMediumInt()); + assertEquals(0x7fffff, buf.getMediumInt(0)); + + // Test negative number + buf.flip().order(ByteOrder.BIG_ENDIAN); + buf.put((byte) 0xff); + buf.put((byte) 0x02); + buf.put((byte) 0x03); + buf.flip(); + + assertEquals(0xffff0203, buf.getMediumInt()); + assertEquals(0xffff0203, buf.getMediumInt(0)); + buf.flip(); + + assertEquals(0x00ff0203, buf.getUnsignedMediumInt()); + assertEquals(0x00ff0203, buf.getUnsignedMediumInt(0)); + } + + @Test + public void testPutMediumInt() { + IoBuffer buf = IoBuffer.allocate(3); + + checkMediumInt(buf, 0); + checkMediumInt(buf, 1); + checkMediumInt(buf, -1); + checkMediumInt(buf, 0x7fffff); + } + + private void checkMediumInt(IoBuffer buf, int x) { + buf.putMediumInt(x); + assertEquals(3, buf.position()); + buf.flip(); + assertEquals(x, buf.getMediumInt()); + assertEquals(3, buf.position()); + + buf.putMediumInt(0, x); + assertEquals(3, buf.position()); + assertEquals(x, buf.getMediumInt(0)); + + buf.flip(); + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/config/ConfigurationTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/config/ConfigurationTest.java new file mode 100644 index 0000000..d03ee7a --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/config/ConfigurationTest.java @@ -0,0 +1,64 @@ +package com.google.code.yanf4j.test.unittest.config; + +import com.google.code.yanf4j.config.Configuration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class ConfigurationTest { + Configuration configuration; + + @BeforeEach + protected void setUp() throws Exception { + this.configuration = new Configuration(); + } + + @Test + public void testDefaultConfig() { + assertTrue(this.configuration.isHandleReadWriteConcurrently()); + + assertEquals(32 * 1024, this.configuration.getSessionReadBufferSize()); + assertEquals(0, this.configuration.getSoTimeout()); + + assertEquals(1, this.configuration.getReadThreadCount()); + assertEquals(1000, this.configuration.getCheckSessionTimeoutInterval()); + assertEquals(5 * 60 * 1000, this.configuration.getStatisticsInterval()); + assertFalse(this.configuration.isStatisticsServer()); + assertEquals(5000L, this.configuration.getSessionIdleTimeout()); + + this.configuration.setSessionReadBufferSize(8 * 1024); + assertEquals(8 * 1024, this.configuration.getSessionReadBufferSize()); + try { + this.configuration.setSessionReadBufferSize(0); + fail(); + } catch (IllegalArgumentException e) { + + } + this.configuration.setReadThreadCount(11); + assertEquals(11, this.configuration.getReadThreadCount()); + try { + this.configuration.setReadThreadCount(-10); + fail(); + } catch (IllegalArgumentException e) { + + } + + this.configuration.setSoTimeout(1000); + assertEquals(1000, this.configuration.getSoTimeout()); + this.configuration.setSoTimeout(0); + assertEquals(0, this.configuration.getSoTimeout()); + try { + this.configuration.setSoTimeout(-1000); + fail(); + } catch (IllegalArgumentException e) { + + } + + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/SocketOptionUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/SocketOptionUnitTest.java new file mode 100644 index 0000000..bec2a69 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/SocketOptionUnitTest.java @@ -0,0 +1,36 @@ +package com.google.code.yanf4j.test.unittest.core; + +import com.google.code.yanf4j.core.SocketOption; +import com.google.code.yanf4j.core.impl.StandardSocketOption; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SocketOptionUnitTest { + @Test + public void testType() { + assertEquals(Integer.class, StandardSocketOption.SO_LINGER.type()); + assertEquals(Boolean.class, StandardSocketOption.SO_KEEPALIVE.type()); + assertEquals(Integer.class, StandardSocketOption.SO_RCVBUF.type()); + assertEquals(Integer.class, StandardSocketOption.SO_SNDBUF.type()); + assertEquals(Boolean.class, StandardSocketOption.SO_REUSEADDR.type()); + assertEquals(Boolean.class, StandardSocketOption.TCP_NODELAY.type()); + } + + @Test + public void testPutInMap() { + Map map = new HashMap(); + map.put(StandardSocketOption.SO_KEEPALIVE, true); + map.put(StandardSocketOption.SO_RCVBUF, 4096); + map.put(StandardSocketOption.SO_SNDBUF, 4096); + map.put(StandardSocketOption.TCP_NODELAY, false); + + assertEquals(4096, map.get(StandardSocketOption.SO_RCVBUF)); + assertEquals(4096, map.get(StandardSocketOption.SO_SNDBUF)); + assertEquals(false, map.get(StandardSocketOption.TCP_NODELAY)); + assertEquals(true, map.get(StandardSocketOption.SO_KEEPALIVE)); + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/AbstractControllerUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/AbstractControllerUnitTest.java new file mode 100644 index 0000000..dd1aa51 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/AbstractControllerUnitTest.java @@ -0,0 +1,165 @@ +package com.google.code.yanf4j.test.unittest.core.impl; + +import java.io.IOException; +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.impl.AbstractController; +import com.google.code.yanf4j.core.impl.ByteBufferCodecFactory; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.core.impl.StandardSocketOption; +import com.google.code.yanf4j.nio.TCPController; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-25 ����11:04:49 + */ + +public abstract class AbstractControllerUnitTest { + protected AbstractController controller; + + @Test + public void testConfigThreadCount() throws Exception { + Configuration configuration = new Configuration(); + configuration.setReadThreadCount(10); + configuration.setWriteThreadCount(1); + configuration.setDispatchMessageThreadCount(11); + this.controller = new TCPController(configuration); + this.controller.setHandler(new HandlerAdapter()); + assertEquals(10, this.controller.getReadThreadCount()); + assertEquals(1, this.controller.getWriteThreadCount()); + assertEquals(11, this.controller.getDispatchMessageThreadCount()); + + this.controller.setReadThreadCount(0); + this.controller.setWriteThreadCount(0); + this.controller.setDispatchMessageThreadCount(0); + + assertEquals(0, this.controller.getReadThreadCount()); + assertEquals(0, this.controller.getWriteThreadCount()); + assertEquals(0, this.controller.getDispatchMessageThreadCount()); + try { + this.controller.setReadThreadCount(-1); + fail(); + } catch (IllegalArgumentException e) { + + } + try { + this.controller.setWriteThreadCount(-1); + fail(); + } catch (IllegalArgumentException e) { + + } + try { + this.controller.setDispatchMessageThreadCount(-1); + fail(); + } catch (IllegalArgumentException e) { + + } + this.controller.start(); + try { + this.controller.setReadThreadCount(1); + fail(); + } catch (IllegalStateException e) { + + } + + try { + this.controller.setWriteThreadCount(1); + fail(); + } catch (IllegalStateException e) { + + } + + try { + this.controller.setDispatchMessageThreadCount(1); + fail(); + } catch (IllegalStateException e) { + + } + + } + + @Test + public void testSetSocketOption() throws Exception { + this.controller = new TCPController(new Configuration()); + this.controller.setSocketOption(StandardSocketOption.SO_KEEPALIVE, true); + assertEquals(true, this.controller.getSocketOption(StandardSocketOption.SO_KEEPALIVE)); + + this.controller.setSocketOption(StandardSocketOption.SO_RCVBUF, 4096); + assertEquals((Integer) 4096, + this.controller.getSocketOption(StandardSocketOption.SO_RCVBUF)); + + try { + this.controller.setSocketOption(null, 3); + fail(); + } catch (NullPointerException e) { + assertEquals("Null socketOption", e.getMessage()); + } + try { + this.controller.setSocketOption(StandardSocketOption.SO_RCVBUF, null); + fail(); + } catch (NullPointerException e) { + assertEquals("Null value", e.getMessage()); + } + } + + @Test + public void testNoHandler() throws Exception { + this.controller = new TCPController(new Configuration()); + assertNull(this.controller.getHandler()); + try { + this.controller.start(); + fail(); + } catch (IOException e) { + assertEquals("The handler is null", e.getMessage()); + } + } + + @Test + public void testNoCodecFactory() throws Exception { + this.controller = new TCPController(new Configuration()); + this.controller.setHandler(new HandlerAdapter()); + assertNull(this.controller.getCodecFactory()); + this.controller.start(); + assertTrue(this.controller.getCodecFactory() instanceof ByteBufferCodecFactory); + } + + @Test + public void testConfig() { + Configuration configuration = new Configuration(); + this.controller = new TCPController(configuration); + assertEquals(configuration.isHandleReadWriteConcurrently(), + this.controller.isHandleReadWriteConcurrently()); + this.controller.setHandleReadWriteConcurrently(false); + assertFalse(this.controller.isHandleReadWriteConcurrently()); + + this.controller.setSessionIdleTimeout(100000); + assertEquals(100000, this.controller.getSessionIdleTimeout()); + this.controller.setSessionTimeout(5000); + assertEquals(5000, this.controller.getSessionTimeout()); + + this.controller.setSoTimeout(9000); + assertEquals(9000, this.controller.getSoTimeout()); + + } + + @AfterEach + public void tearDown() throws Exception { + if (this.controller != null) { + this.controller.stop(); + } + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/ByteBufferCodecFactoryUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/ByteBufferCodecFactoryUnitTest.java new file mode 100644 index 0000000..16d6684 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/ByteBufferCodecFactoryUnitTest.java @@ -0,0 +1,75 @@ +package com.google.code.yanf4j.test.unittest.core.impl; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory.Encoder; +import com.google.code.yanf4j.core.impl.ByteBufferCodecFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 10:49:54 + */ + +public class ByteBufferCodecFactoryUnitTest { + ByteBufferCodecFactory codecFactory; + + @BeforeEach + public void setUp() { + this.codecFactory = new ByteBufferCodecFactory(); + } + + @Test + public void testEncodeNormal() throws Exception { + Encoder encoder = this.codecFactory.getEncoder(); + assertNotNull(encoder); + IoBuffer buffer = encoder.encode(IoBuffer.wrap("hello".getBytes("utf-8")), null); + assertNotNull(buffer); + assertTrue(buffer.hasRemaining()); + assertArrayEquals("hello".getBytes("utf-8"), buffer.array()); + + } + + @Test + public void testEncodeEmpty() throws Exception { + Encoder encoder = this.codecFactory.getEncoder(); + assertNull(encoder.encode(null, null)); + assertEquals(IoBuffer.allocate(0), encoder.encode(IoBuffer.allocate(0), null)); + } + + @Test + public void decodeNormal() throws Exception { + Encoder encoder = this.codecFactory.getEncoder(); + assertNotNull(encoder); + IoBuffer buffer = encoder.encode(IoBuffer.wrap("hello".getBytes("utf-8")), null); + + IoBuffer decodeBuffer = (IoBuffer) this.codecFactory.getDecoder().decode(buffer, null); + assertEquals(IoBuffer.wrap("hello".getBytes("utf-8")), decodeBuffer); + } + + @Test + public void decodeEmpty() throws Exception { + assertNull(this.codecFactory.getDecoder().decode(null, null)); + assertEquals(IoBuffer.allocate(0), + this.codecFactory.getDecoder().decode(IoBuffer.allocate(0), null)); + } + + @Test + public void testDirectEncoder() throws Exception { + this.codecFactory = new ByteBufferCodecFactory(true); + IoBuffer msg = IoBuffer.allocate(100); + IoBuffer buffer = this.codecFactory.getEncoder().encode(msg, null); + assertTrue(buffer.isDirect()); + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/FutureImplUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/FutureImplUnitTest.java new file mode 100644 index 0000000..44179eb --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/FutureImplUnitTest.java @@ -0,0 +1,128 @@ +package com.google.code.yanf4j.test.unittest.core.impl; + +import com.google.code.yanf4j.core.impl.FutureImpl; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 11:04:22 + */ + +public class FutureImplUnitTest { + + private static final Logger log = LoggerFactory.getLogger(FutureImplUnitTest.class); + + private static final class NotifyFutureRunner implements Runnable { + FutureImpl future; + long sleepTime; + Throwable throwable; + + public NotifyFutureRunner(FutureImpl future, long sleepTime, Throwable throwable) { + super(); + this.future = future; + this.sleepTime = sleepTime; + this.throwable = throwable; + } + + public void run() { + try { + Thread.sleep(this.sleepTime); + if (this.throwable != null) { + this.future.failure(this.throwable); + } else { + this.future.setResult(true); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testGet() throws Exception { + FutureImpl future = new FutureImpl(); + new Thread(new NotifyFutureRunner(future, 2000, null)).start(); + boolean result = future.get(); + assertTrue(result); + assertTrue(future.isDone()); + assertFalse(future.isCancelled()); + } + + @Test + public void testGetImmediately() throws Exception { + FutureImpl future = new FutureImpl(); + future.setResult(true); + boolean result = future.get(); + assertTrue(result); + assertTrue(future.isDone()); + assertFalse(future.isCancelled()); + } + + @Test + public void testGetException() throws Exception { + FutureImpl future = new FutureImpl(); + new Thread(new NotifyFutureRunner(future, 2000, new IOException("hello"))).start(); + try { + future.get(); + fail(); + } catch (ExecutionException e) { + assertEquals("hello", e.getCause().getMessage()); + + } + assertTrue(future.isDone()); + assertFalse(future.isCancelled()); + + } + + @Test + public void testCancel() throws Exception { + final FutureImpl future = new FutureImpl(); + new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(3000); + future.cancel(true); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + }).start(); + try { + future.get(); + fail(); + } catch (CancellationException e) { + assertTrue(true); + + } + assertTrue(future.isDone()); + assertTrue(future.isCancelled()); + } + + @Test + public void testGetTimeout() throws Exception { + FutureImpl future = new FutureImpl(); + try { + future.get(1000, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + assertTrue(true); + } + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/FutureLockImplUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/FutureLockImplUnitTest.java new file mode 100644 index 0000000..38afafb --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/FutureLockImplUnitTest.java @@ -0,0 +1,124 @@ +package com.google.code.yanf4j.test.unittest.core.impl; + +import com.google.code.yanf4j.core.impl.FutureLockImpl; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 11:04:22 + */ + +public class FutureLockImplUnitTest { + + private static final class NotifyFutureRunner implements Runnable { + FutureLockImpl future; + long sleepTime; + Throwable throwable; + + public NotifyFutureRunner(FutureLockImpl future, long sleepTime, Throwable throwable) { + super(); + this.future = future; + this.sleepTime = sleepTime; + this.throwable = throwable; + } + + public void run() { + try { + Thread.sleep(this.sleepTime); + if (this.throwable != null) { + this.future.failure(this.throwable); + } else { + this.future.setResult(true); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void testGet() throws Exception { + FutureLockImpl future = new FutureLockImpl(); + new Thread(new NotifyFutureRunner(future, 2000, null)).start(); + boolean result = future.get(); + assertTrue(result); + assertTrue(future.isDone()); + assertFalse(future.isCancelled()); + } + + @Test + public void testGetImmediately() throws Exception { + FutureLockImpl future = new FutureLockImpl(); + future.setResult(true); + boolean result = future.get(); + assertTrue(result); + assertTrue(future.isDone()); + assertFalse(future.isCancelled()); + } + + @Test + public void testGetException() throws Exception { + FutureLockImpl future = new FutureLockImpl(); + new Thread(new NotifyFutureRunner(future, 2000, new IOException("hello"))).start(); + try { + future.get(); + fail(); + } catch (ExecutionException e) { + assertEquals("hello", e.getCause().getMessage()); + + } + assertTrue(future.isDone()); + assertFalse(future.isCancelled()); + + } + + @Test + public void testCancel() throws Exception { + final FutureLockImpl future = new FutureLockImpl(); + new Thread(new Runnable() { + public void run() { + try { + Thread.sleep(3000); + future.cancel(true); + } catch (Exception e) { + e.printStackTrace(); + } + } + }).start(); + try { + future.get(); + fail(); + } catch (CancellationException e) { + assertTrue(true); + + } + assertTrue(future.isDone()); + assertTrue(future.isCancelled()); + } + + @Test + public void testGetTimeout() throws Exception { + FutureLockImpl future = new FutureLockImpl(); + try { + future.get(1000, TimeUnit.MILLISECONDS); + fail(); + } catch (TimeoutException e) { + assertTrue(true); + } + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/PoolDispatcherUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/PoolDispatcherUnitTest.java new file mode 100644 index 0000000..017f14a --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/PoolDispatcherUnitTest.java @@ -0,0 +1,96 @@ +package com.google.code.yanf4j.test.unittest.core.impl; + +import com.google.code.yanf4j.core.impl.PoolDispatcher; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 ����11:34:43 + */ + +public class PoolDispatcherUnitTest { + PoolDispatcher dispatcher; + + @BeforeEach + public void setUp() { + this.dispatcher = + new PoolDispatcher(10, 60, TimeUnit.SECONDS, new ThreadPoolExecutor.AbortPolicy(), "test"); + } + + @AfterEach + public void tearDown() { + this.dispatcher.stop(); + } + + private static final class TestRunner implements Runnable { + boolean ran; + + public void run() { + this.ran = true; + + } + } + + @Test + public void testDispatch() throws Exception { + TestRunner runner = new TestRunner(); + this.dispatcher.dispatch(runner); + Thread.sleep(1000); + assertTrue(runner.ran); + + } + + @Test + public void testDispatchNull() throws Exception { + assertThrows(NullPointerException.class, ()-> { + this.dispatcher.dispatch(null); + }); + } + + @Test + public void testDispatcherStop() throws Exception { + this.dispatcher.stop(); + TestRunner runner = new TestRunner(); + this.dispatcher.dispatch(runner); + Thread.sleep(1000); + assertFalse(runner.ran); + } + + @Test + public void testDispatchReject() { + this.dispatcher = new PoolDispatcher(1, 1, 1, 60, TimeUnit.SECONDS, + new ThreadPoolExecutor.AbortPolicy(), "test"); + this.dispatcher.dispatch(new Runnable() { + public void run() { + while (!Thread.currentThread().isInterrupted()) { + + } + } + }); + this.dispatcher.dispatch(new Runnable() { + public void run() { + while (!Thread.currentThread().isInterrupted()) { + + } + } + }); + assertThrows(RejectedExecutionException.class, () -> { + // Should throw a RejectedExecutionException + this.dispatcher.dispatch(new TestRunner()); + }); + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/TextLineCodecFactoryUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/TextLineCodecFactoryUnitTest.java new file mode 100644 index 0000000..2ec5d9e --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/core/impl/TextLineCodecFactoryUnitTest.java @@ -0,0 +1,68 @@ +package com.google.code.yanf4j.test.unittest.core.impl; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory.Encoder; +import com.google.code.yanf4j.core.impl.TextLineCodecFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 10:33:59 + */ + +public class TextLineCodecFactoryUnitTest { + TextLineCodecFactory textLineCodecFactory; + + @BeforeEach + public void setUp() { + this.textLineCodecFactory = new TextLineCodecFactory(); + TextLineCodecFactory.SPLIT.clear(); + } + + @Test + public void testEncodeNormal() throws Exception { + Encoder encoder = this.textLineCodecFactory.getEncoder(); + assertNotNull(encoder); + IoBuffer buffer = encoder.encode("hello", null); + assertNotNull(buffer); + assertTrue(buffer.hasRemaining()); + assertArrayEquals("hello\r\n".getBytes("utf-8"), buffer.array()); + + } + + @Test + public void testEncodeEmpty() throws Exception { + Encoder encoder = this.textLineCodecFactory.getEncoder(); + assertNull(encoder.encode(null, null)); + assertEquals(TextLineCodecFactory.SPLIT, encoder.encode("", null)); + } + + @Test + public void decodeNormal() throws Exception { + Encoder encoder = this.textLineCodecFactory.getEncoder(); + assertNotNull(encoder); + IoBuffer buffer = encoder.encode("hello", null); + + String str = (String) this.textLineCodecFactory.getDecoder().decode(buffer, null); + assertEquals("hello", str); + } + + @Test + public void decodeEmpty() throws Exception { + assertNull(this.textLineCodecFactory.getDecoder().decode(null, null)); + assertEquals("", + this.textLineCodecFactory.getDecoder().decode(TextLineCodecFactory.SPLIT, null)); + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/MockSelectableChannel.java b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/MockSelectableChannel.java new file mode 100644 index 0000000..f5f891e --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/MockSelectableChannel.java @@ -0,0 +1,83 @@ +package com.google.code.yanf4j.test.unittest.nio.impl; + +import java.io.IOException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.spi.SelectorProvider; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 01:40:15 + */ + +public class MockSelectableChannel extends SelectableChannel { + Selector selector; + int ops; + Object attch; + MockSelectionKey selectionKey = new MockSelectionKey(); + + @Override + public Object blockingLock() { + // TODO Auto-generated method stub + return null; + } + + @Override + public SelectableChannel configureBlocking(boolean block) throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isBlocking() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isRegistered() { + // TODO Auto-generated method stub + return false; + } + + @Override + public SelectionKey keyFor(Selector sel) { + // TODO Auto-generated method stub + return null; + } + + @Override + public SelectorProvider provider() { + // TODO Auto-generated method stub + return null; + } + + @Override + public SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException { + this.selector = sel; + this.ops = ops; + this.attch = att; + this.selectionKey.channel = this; + this.selectionKey.selector = sel; + return this.selectionKey; + } + + @Override + public int validOps() { + // TODO Auto-generated method stub + return 0; + } + + @Override + protected void implCloseChannel() throws IOException { + // TODO Auto-generated method stub + + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/MockSelectionKey.java b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/MockSelectionKey.java new file mode 100644 index 0000000..3baea50 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/MockSelectionKey.java @@ -0,0 +1,59 @@ +package com.google.code.yanf4j.test.unittest.nio.impl; + +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 02:01:20 + */ + +public class MockSelectionKey extends SelectionKey { + MockSelectableChannel channel; + int interestOps; + boolean valid = true; + Selector selector; + + @Override + public void cancel() { + this.valid = false; + + } + + @Override + public SelectableChannel channel() { + return this.channel; + } + + @Override + public int interestOps() { + return this.interestOps; + } + + @Override + public SelectionKey interestOps(int ops) { + this.interestOps = ops; + return this; + } + + @Override + public boolean isValid() { + return this.valid; + } + + @Override + public int readyOps() { + return this.interestOps; + } + + @Override + public Selector selector() { + return this.selector; + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/ReactorUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/ReactorUnitTest.java new file mode 100644 index 0000000..20e4288 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/ReactorUnitTest.java @@ -0,0 +1,230 @@ +package com.google.code.yanf4j.test.unittest.nio.impl; + +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.nio.NioSession; +import com.google.code.yanf4j.nio.TCPController; +import com.google.code.yanf4j.nio.impl.Reactor; +import com.google.code.yanf4j.nio.impl.SelectorManager; +import org.easymock.EasyMock; +import org.easymock.IMocksControl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 01:18:18 + */ + +public class ReactorUnitTest { + private Reactor reactor; + private SelectorManager selectorManager; + + @BeforeEach + public void setUp() throws Exception { + Configuration configuration = new Configuration(); + TCPController controller = new TCPController(configuration); + this.selectorManager = new SelectorManager(1, controller, configuration); + this.selectorManager.start(); + this.reactor = this.selectorManager.getReactorByIndex(0); + controller.setSessionTimeout(1000); + controller.getConfiguration().setSessionIdleTimeout(1000); + } + + @Test + public void testRegisterOpenChannel() throws Exception { + MockSelectableChannel channel = new MockSelectableChannel(); + channel.selectionKey = new MockSelectionKey(); + this.reactor.registerChannel(channel, 1, "hello"); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + assertSame(this.reactor.getSelector(), channel.selector); + assertSame(this.reactor.getSelector(), channel.selectionKey.selector); + assertEquals(1, channel.ops); + assertEquals("hello", channel.attch); + } + + @Test + public void testRegisterCloseChannel() throws Exception { + MockSelectableChannel channel = new MockSelectableChannel(); + channel.close(); + this.reactor.registerChannel(channel, 1, "hello"); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + assertNull(channel.selector); + assertEquals(0, channel.ops); + assertNull(channel.attch); + } + + @Test + public void testRegisterOpenSession() throws Exception { + IMocksControl control = EasyMock.createControl(); + NioSession session = control.createMock(NioSession.class); + session.onEvent(EventType.ENABLE_READ, this.reactor.getSelector()); + EasyMock.expectLastCall(); + EasyMock.expect(session.isClosed()).andReturn(false); + + control.replay(); + this.reactor.registerSession(session, EventType.ENABLE_READ); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + control.verify(); + } + + @Test + public void testRegisterCloseSession() throws Exception { + IMocksControl control = EasyMock.createControl(); + NioSession session = control.createMock(NioSession.class); + EasyMock.expect(session.isClosed()).andReturn(true); + control.replay(); + this.reactor.registerSession(session, EventType.ENABLE_READ); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + control.verify(); + } + + @Test + public void testDispatchEventAllValid() throws Exception { + IMocksControl control = EasyMock.createControl(); + Set keySet = new HashSet(); + addReadableKey(keySet, control, this.reactor.getSelector()); + addWritableKey(keySet, control, this.reactor.getSelector()); + control.replay(); + this.reactor.dispatchEvent(keySet); + assertEquals(0, keySet.size()); + control.verify(); + } + + @Test + public void testDispatchEventSomeInValid() throws Exception { + IMocksControl control = EasyMock.createControl(); + Set keySet = new HashSet(); + addInvalidKey(keySet); + addWritableKey(keySet, control, this.reactor.getSelector()); + control.replay(); + this.reactor.dispatchEvent(keySet); + assertEquals(0, keySet.size()); + control.verify(); + } + + @Test + public void testPostSelectOneTimeout() { + IMocksControl mocksControl = EasyMock.createControl(); + Set selectedKeys = new HashSet(); + Set allKeys = new HashSet(); + addTimeoutKey(mocksControl, allKeys); + allKeys.addAll(selectedKeys); + + mocksControl.replay(); + this.reactor.postSelect(selectedKeys, allKeys); + mocksControl.verify(); + + } + + private void addTimeoutKey(IMocksControl mocksControl, Set allKeys) { + MockSelectionKey key = new MockSelectionKey(); + NioSession session = mocksControl.createMock(NioSession.class); + key.attach(session); + // �ж�session�Ƿ���ڣ�����Ϊ���� + EasyMock.expect(session.isExpired()).andReturn(true); + // ���ھͻ����onSessionExpired�����ر����� + // ͬʱ��expired��session�����ж�idle + session.onEvent(EventType.EXPIRED, this.reactor.getSelector()); + EasyMock.expectLastCall(); + session.close(); + EasyMock.expectLastCall(); + + allKeys.add(key); + } + + @Test + public void testPostSelectOneIdle() { + IMocksControl mocksControl = EasyMock.createControl(); + Set selectedKeys = new HashSet(); + Set allKeys = new HashSet(); + addIdleKey(mocksControl, allKeys); + allKeys.addAll(selectedKeys); + + mocksControl.replay(); + this.reactor.postSelect(selectedKeys, allKeys); + mocksControl.verify(); + + } + + @Test + public void testPostSelectMoreKeys() { + IMocksControl mocksControl = EasyMock.createControl(); + Set selectedKeys = new HashSet(); + Set allKeys = new HashSet(); + addIdleKey(mocksControl, allKeys); + addTimeoutKey(mocksControl, allKeys); + selectedKeys.add(new MockSelectionKey()); + selectedKeys.add(new MockSelectionKey()); + allKeys.addAll(selectedKeys); + + mocksControl.replay(); + this.reactor.postSelect(selectedKeys, allKeys); + mocksControl.verify(); + } + + private void addIdleKey(IMocksControl mocksControl, Set allKeys) { + MockSelectionKey key = new MockSelectionKey(); + NioSession session = mocksControl.createMock(NioSession.class); + key.attach(session); + // session + EasyMock.expect(session.isExpired()).andReturn(false); + // session idle + EasyMock.expect(session.isIdle()).andReturn(true); + // idle onSessionIdle + session.onEvent(EventType.IDLE, this.reactor.getSelector()); + EasyMock.expectLastCall(); + + allKeys.add(key); + } + + private void addInvalidKey(Set keySet) { + MockSelectionKey key = new MockSelectionKey(); + key.valid = false; + keySet.add(key); + } + + private void addReadableKey(Set keySet, IMocksControl control, Selector selector) { + MockSelectionKey key = new MockSelectionKey(); + key.interestOps = SelectionKey.OP_READ; + NioSession session = control.createMock(NioSession.class); + session.onEvent(EventType.READABLE, selector); + EasyMock.expectLastCall(); + key.attach(session); + key.selector = selector; + keySet.add(key); + } + + private void addWritableKey(Set keySet, IMocksControl control, Selector selector) { + MockSelectionKey key = new MockSelectionKey(); + key.interestOps = SelectionKey.OP_WRITE; + NioSession session = control.createMock(NioSession.class); + session.onEvent(EventType.WRITEABLE, selector); + EasyMock.expectLastCall(); + key.attach(session); + key.selector = selector; + keySet.add(key); + } + + @AfterEach + public void tearDown() throws Exception { + if (this.selectorManager != null) { + this.selectorManager.stop(); + } + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/SelectorManagerUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/SelectorManagerUnitTest.java new file mode 100644 index 0000000..6136b56 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/nio/impl/SelectorManagerUnitTest.java @@ -0,0 +1,117 @@ +package com.google.code.yanf4j.test.unittest.nio.impl; + +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.core.EventType; +import com.google.code.yanf4j.nio.NioSession; +import com.google.code.yanf4j.nio.TCPController; +import com.google.code.yanf4j.nio.impl.Reactor; +import com.google.code.yanf4j.nio.impl.SelectorManager; +import org.easymock.EasyMock; +import org.easymock.IMocksControl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * + * + * + * @author boyan + * + * @since 1.0, 2009-12-24 03:09:42 + */ + +public class SelectorManagerUnitTest { + private SelectorManager selectorManager; + int selectorPoolSize = 3; + + @BeforeEach + public void setUp() throws Exception { + Configuration configuration = new Configuration(); + TCPController controller = new TCPController(configuration); + this.selectorManager = new SelectorManager(this.selectorPoolSize, controller, configuration); + this.selectorManager.start(); + controller.setSessionTimeout(1000); + controller.getConfiguration().setSessionIdleTimeout(1000); + } + + @Test + public void testNextReactor() { + long start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + Reactor reactor = this.selectorManager.nextReactor(); + assertNotNull(reactor); + assertTrue(reactor.getReactorIndex() > 0); + } + System.out.println(System.currentTimeMillis() - start); + } + + @Test + public void testRegisterOpenChannel() throws Exception { + MockSelectableChannel channel = new MockSelectableChannel(); + channel.selectionKey = new MockSelectionKey(); + Reactor reactor = this.selectorManager.registerChannel(channel, 1, "hello"); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + assertSame(reactor.getSelector(), channel.selector); + assertSame(reactor.getSelector(), channel.selectionKey.selector); + assertEquals(1, channel.ops); + assertEquals("hello", channel.attch); + } + + @Test + public void testRegisterCloseChannel() throws Exception { + MockSelectableChannel channel = new MockSelectableChannel(); + channel.close(); + this.selectorManager.registerChannel(channel, 1, "hello"); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + assertNull(channel.selector); + assertEquals(0, channel.ops); + assertNull(channel.attch); + } + + @Test + public void testRegisterOpenSession() throws Exception { + IMocksControl control = EasyMock.createControl(); + NioSession session = control.createMock(NioSession.class); + EasyMock.makeThreadSafe(session, true); + // next reactor index=2 + Reactor nextReactor = this.selectorManager.getReactorByIndex(2); + session.onEvent(EventType.ENABLE_READ, nextReactor.getSelector()); + EasyMock.expectLastCall(); + EasyMock.expect(session.isClosed()).andReturn(false).times(2); + EasyMock.expect(session.getAttribute(SelectorManager.REACTOR_ATTRIBUTE)).andReturn(null); + EasyMock.expect(session.setAttributeIfAbsent(SelectorManager.REACTOR_ATTRIBUTE, nextReactor)) + .andReturn(null); + + control.replay(); + this.selectorManager.registerSession(session, EventType.ENABLE_READ); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + control.verify(); + } + + @Test + public void testRegisterCloseSession() throws Exception { + IMocksControl control = EasyMock.createControl(); + NioSession session = control.createMock(NioSession.class); + EasyMock.expect(session.isClosed()).andReturn(true); + control.replay(); + this.selectorManager.registerSession(session, EventType.ENABLE_READ); + Thread.sleep(Reactor.DEFAULT_WAIT * 3); + control.verify(); + } + + @AfterEach + public void tearDown() throws Exception { + if (this.selectorManager != null) { + this.selectorManager.getController().stop(); + this.selectorManager.stop(); + } + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/statistics/SimpleStatisticsTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/statistics/SimpleStatisticsTest.java new file mode 100644 index 0000000..79f165b --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/statistics/SimpleStatisticsTest.java @@ -0,0 +1,129 @@ +package com.google.code.yanf4j.test.unittest.statistics; + +import com.google.code.yanf4j.statistics.Statistics; +import com.google.code.yanf4j.statistics.impl.SimpleStatistics; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SimpleStatisticsTest { + + Statistics statistics; + + @BeforeEach + protected void setUp() throws Exception { + statistics = new SimpleStatistics(); + statistics.start(); + } + + @AfterEach + protected void tearDown() throws Exception { + statistics.stop(); + assertFalse(statistics.isStatistics()); + } + + @Test + public void testRead() throws Exception { + assertTrue(statistics.isStatistics()); + assertEquals(0, statistics.getRecvMessageCount()); + assertEquals(0.0, statistics.getRecvMessageCountPerSecond()); + assertEquals(0, statistics.getRecvMessageTotalSize()); + assertEquals(0, statistics.getRecvMessageAverageSize()); + statistics.statisticsRead(4096); + assertEquals(1, statistics.getRecvMessageCount()); + assertEquals(4096, statistics.getRecvMessageTotalSize()); + assertEquals(4096, statistics.getRecvMessageAverageSize()); + + statistics.statisticsRead(1024); + assertEquals(2, statistics.getRecvMessageCount()); + assertEquals(5120, statistics.getRecvMessageTotalSize()); + assertEquals(2560, statistics.getRecvMessageAverageSize()); + Thread.sleep(1000); + assertEquals(2.0, statistics.getRecvMessageCountPerSecond(), 0.5); + + statistics.statisticsRead(512); + assertEquals(3, statistics.getRecvMessageCount()); + assertEquals(5632, statistics.getRecvMessageTotalSize()); + assertEquals(1877, statistics.getRecvMessageAverageSize()); + assertEquals(3.0, statistics.getRecvMessageCountPerSecond(), 0.5); + // 忽略0和负数 + statistics.statisticsRead(0); + assertEquals(3, statistics.getRecvMessageCount()); + assertEquals(5632, statistics.getRecvMessageTotalSize()); + assertEquals(1877, statistics.getRecvMessageAverageSize()); + assertEquals(3.0, statistics.getRecvMessageCountPerSecond(), 0.5); + + statistics.statisticsRead(-100); + assertEquals(3, statistics.getRecvMessageCount()); + assertEquals(5632, statistics.getRecvMessageTotalSize()); + assertEquals(1877, statistics.getRecvMessageAverageSize()); + assertEquals(3.0, statistics.getRecvMessageCountPerSecond(), 0.5); + + statistics.restart(); + assertEquals(0, statistics.getRecvMessageCount()); + assertEquals(0.0, statistics.getRecvMessageCountPerSecond()); + assertEquals(0, statistics.getRecvMessageTotalSize()); + assertEquals(0, statistics.getRecvMessageAverageSize()); + } + + public void testWrite() throws Exception { + assertEquals(0, statistics.getWriteMessageCount()); + assertEquals(0.0, statistics.getWriteMessageCountPerSecond()); + assertEquals(0, statistics.getWriteMessageTotalSize()); + assertEquals(0, statistics.getWriteMessageAverageSize()); + statistics.statisticsWrite(4096); + assertEquals(1, statistics.getWriteMessageCount()); + assertEquals(4096, statistics.getWriteMessageTotalSize()); + assertEquals(4096, statistics.getWriteMessageAverageSize()); + + statistics.statisticsWrite(1024); + assertEquals(2, statistics.getWriteMessageCount()); + assertEquals(5120, statistics.getWriteMessageTotalSize()); + assertEquals(2560, statistics.getWriteMessageAverageSize()); + Thread.sleep(1000); + assertEquals(2.0, statistics.getWriteMessageCountPerSecond(), 0.5); + + statistics.statisticsWrite(512); + assertEquals(3, statistics.getWriteMessageCount()); + assertEquals(5632, statistics.getWriteMessageTotalSize()); + assertEquals(1877, statistics.getWriteMessageAverageSize()); + assertEquals(3.0, statistics.getWriteMessageCountPerSecond(), 0.5); + // 忽略负数和0 + statistics.statisticsWrite(0); + assertEquals(3, statistics.getWriteMessageCount()); + assertEquals(5632, statistics.getWriteMessageTotalSize()); + assertEquals(1877, statistics.getWriteMessageAverageSize()); + assertEquals(3.0, statistics.getWriteMessageCountPerSecond(), 0.5); + statistics.statisticsWrite(-1); + assertEquals(3, statistics.getWriteMessageCount()); + assertEquals(5632, statistics.getWriteMessageTotalSize()); + assertEquals(1877, statistics.getWriteMessageAverageSize()); + assertEquals(3.0, statistics.getWriteMessageCountPerSecond(), 0.5); + } + + public void testProcess() { + // 初始状态 + assertEquals(0.0, statistics.getProcessedMessageAverageTime()); + assertEquals(0, statistics.getProcessedMessageCount()); + statistics.statisticsProcess(1500); + assertEquals(1500.0, statistics.getProcessedMessageAverageTime(), 0.5); + assertEquals(1, statistics.getProcessedMessageCount()); + // 允许0 + statistics.statisticsProcess(0); + assertEquals(750.0, statistics.getProcessedMessageAverageTime(), 0.5); + assertEquals(2, statistics.getProcessedMessageCount()); + + statistics.statisticsProcess(987); + assertEquals(829.0, statistics.getProcessedMessageAverageTime(), 0.5); + assertEquals(3, statistics.getProcessedMessageCount()); + // 测试负数 + statistics.statisticsProcess(-100); + assertEquals(829.0, statistics.getProcessedMessageAverageTime(), 0.5); + assertEquals(3, statistics.getProcessedMessageCount()); + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/ByteBufferMatcherTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ByteBufferMatcherTest.java new file mode 100644 index 0000000..91f9234 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ByteBufferMatcherTest.java @@ -0,0 +1,54 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.util.ByteBufferMatcher; +import com.google.code.yanf4j.util.ShiftAndByteBufferMatcher; +import com.google.code.yanf4j.util.ShiftOrByteBufferMatcher; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public abstract class ByteBufferMatcherTest { + + @Test + public void testMatchFirst() { + String hello = "hel;lo"; + ByteBufferMatcher m = createByteBufferMatcher(hello); + assertEquals(0, m.matchFirst(IoBuffer.wrap("hel;lo".getBytes()))); + assertEquals(-1, m.matchFirst(IoBuffer.wrap("hel;l0".getBytes()))); + assertEquals(6, m.matchFirst(IoBuffer.wrap("hello hel;lo".getBytes()))); + assertEquals(0, (m.matchFirst(IoBuffer.wrap("hel;lo good ".getBytes())))); + assertEquals(7, m.matchFirst(IoBuffer.wrap("abcdefghel;lo good ".getBytes()))); + assertEquals(-1, m.matchFirst(IoBuffer.wrap("".getBytes()))); + + assertEquals(6, m.matchFirst(IoBuffer.wrap("hello hel;lo".getBytes()).position(4))); + assertEquals(6, m.matchFirst(IoBuffer.wrap("hello hel;lo".getBytes()).position(6))); + assertEquals(-1, m.matchFirst(IoBuffer.wrap("hello hel;lo".getBytes()).limit(6))); + + assertEquals(-1, m.matchFirst(null)); + assertEquals(-1, m.matchFirst(IoBuffer.allocate(0))); + + ByteBufferMatcher newline = new ShiftAndByteBufferMatcher(IoBuffer.wrap("\r\n".getBytes())); + + String memcachedGet = "VALUE test 0 0 100\r\nhello\r\n"; + assertEquals(memcachedGet.indexOf("\r\n"), + newline.matchFirst(IoBuffer.wrap(memcachedGet.getBytes()))); + assertEquals(25, newline.matchFirst(IoBuffer.wrap(memcachedGet.getBytes()).position(20))); + } + + public abstract ByteBufferMatcher createByteBufferMatcher(String hello); + + @Test + public void testMatchAll() { + String memcachedGet = + "VALUE test 0 0 100\r\nhello\r\n\rtestgood\r\nh\rfasdfasd\n\rdfasdfad\r\n\r\n"; + ByteBufferMatcher newline = new ShiftOrByteBufferMatcher(IoBuffer.wrap("\r\n".getBytes())); + List list = newline.matchAll(IoBuffer.wrap(memcachedGet.getBytes())); + for (int i : list) { + System.out.println(i); + } + + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/ByteBufferUtilsTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ByteBufferUtilsTest.java new file mode 100644 index 0000000..df992ed --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ByteBufferUtilsTest.java @@ -0,0 +1,152 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import com.google.code.yanf4j.config.Configuration; +import com.google.code.yanf4j.util.ByteBufferUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ByteBufferUtilsTest { + + @Test + public void testIncreaseBlankBufferCapatity() { + ByteBuffer buffer = ByteBuffer.allocate(1024); + buffer = ByteBufferUtils.increaseBufferCapatity(buffer); + + assertEquals(1024 + Configuration.DEFAULT_INCREASE_BUFF_SIZE, buffer.capacity()); + buffer = ByteBufferUtils.increaseBufferCapatity(buffer); + assertEquals(1024 + 2 * Configuration.DEFAULT_INCREASE_BUFF_SIZE, buffer.capacity()); + + } + + @Test + public void testIncreaseNotBlankBufferCapatity() { + ByteBuffer buffer = ByteBuffer.allocate(1024); + buffer.putInt(100); + buffer = ByteBufferUtils.increaseBufferCapatity(buffer); + assertEquals(1024 + Configuration.DEFAULT_INCREASE_BUFF_SIZE, buffer.capacity()); + assertEquals(4, buffer.position()); + assertEquals(1024 + Configuration.DEFAULT_INCREASE_BUFF_SIZE - 4, buffer.remaining()); + buffer.putLong(100l); + assertEquals(12, buffer.position()); + buffer = ByteBufferUtils.increaseBufferCapatity(buffer); + assertEquals(12, buffer.position()); + assertEquals(1024 + 2 * Configuration.DEFAULT_INCREASE_BUFF_SIZE - 4 - 8, buffer.remaining()); + + } + + @Test + public void testIncreaseNullBufferCapacity() { + try { + assertNull(ByteBufferUtils.increaseBufferCapatity(null)); + } catch (IllegalArgumentException e) { + assertEquals("buffer is null", e.getMessage()); + } + } + + @Test + public void testFlip() { + ByteBuffer[] buffers = new ByteBuffer[2]; + ByteBufferUtils.flip(buffers); + buffers[0] = ByteBuffer.allocate(4).putInt(4); + buffers[1] = ByteBuffer.allocate(10).put("hello".getBytes()); + + assertEquals(4, buffers[0].position()); + assertEquals(5, buffers[1].position()); + assertEquals(4, buffers[0].limit()); + assertEquals(10, buffers[1].limit()); + ByteBufferUtils.flip(buffers); + assertEquals(0, buffers[0].position()); + assertEquals(0, buffers[1].position()); + assertEquals(4, buffers[0].limit()); + assertEquals(5, buffers[1].limit()); + + ByteBufferUtils.flip(null); + } + + @Test + public void testClear() { + ByteBuffer[] buffers = new ByteBuffer[2]; + ByteBufferUtils.clear(buffers); + buffers[0] = ByteBuffer.allocate(4).putInt(4); + buffers[1] = ByteBuffer.allocate(10).put("hello".getBytes()); + + assertEquals(4, buffers[0].position()); + assertEquals(5, buffers[1].position()); + assertEquals(4, buffers[0].limit()); + assertEquals(10, buffers[1].limit()); + assertEquals(0, buffers[0].remaining()); + assertEquals(5, buffers[1].remaining()); + ByteBufferUtils.clear(buffers); + assertEquals(0, buffers[0].position()); + assertEquals(0, buffers[1].position()); + assertEquals(4, buffers[0].limit()); + assertEquals(10, buffers[1].limit()); + assertEquals(4, buffers[0].remaining()); + assertEquals(10, buffers[1].remaining()); + ByteBufferUtils.clear(null); + } + + @Test + public void testHasRemaining() { + ByteBuffer[] buffers = new ByteBuffer[2]; + assertFalse(ByteBufferUtils.hasRemaining(buffers)); + buffers[0] = ByteBuffer.allocate(4).putInt(4); + buffers[1] = ByteBuffer.allocate(10).put("hello".getBytes()); + assertTrue(ByteBufferUtils.hasRemaining(buffers)); + + buffers[1].put("yanfj".getBytes()); + assertFalse(ByteBufferUtils.hasRemaining(buffers)); + + ByteBufferUtils.clear(buffers); + assertTrue(ByteBufferUtils.hasRemaining(buffers)); + ByteBuffer[] moreBuffers = new ByteBuffer[3]; + moreBuffers[0] = ByteBuffer.allocate(4).putInt(4); + moreBuffers[1] = ByteBuffer.allocate(10).put("hello".getBytes()); + moreBuffers[2] = ByteBuffer.allocate(12).putLong(9999); + assertTrue(ByteBufferUtils.hasRemaining(moreBuffers)); + moreBuffers[2].putInt(4); + assertTrue(ByteBufferUtils.hasRemaining(moreBuffers)); + moreBuffers[1].put("yanfj".getBytes()); + assertFalse(ByteBufferUtils.hasRemaining(moreBuffers)); + + assertFalse(ByteBufferUtils.hasRemaining(null)); + } + + @Test + public void testIndexOf() { + String words = "hello world good hello"; + ByteBuffer buffer = ByteBuffer.wrap(words.getBytes()); + + String world = "world"; + assertEquals(6, ByteBufferUtils.indexOf(buffer, ByteBuffer.wrap(world.getBytes()))); + assertEquals(0, ByteBufferUtils.indexOf(buffer, ByteBuffer.wrap("hello".getBytes()))); + long start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + assertEquals(17, ByteBufferUtils.indexOf(buffer, ByteBuffer.wrap("hello".getBytes()), 6)); + } + System.out.println(System.currentTimeMillis() - start); + assertEquals(-1, ByteBufferUtils.indexOf(buffer, ByteBuffer.wrap("test".getBytes()))); + assertEquals(-1, ByteBufferUtils.indexOf(buffer, (ByteBuffer) null)); + assertEquals(-1, ByteBufferUtils.indexOf(null, buffer)); + } + + @Test + public void testGather() { + ByteBuffer buffer1 = ByteBuffer.wrap("hello".getBytes()); + ByteBuffer buffer2 = ByteBuffer.wrap(" dennis".getBytes()); + + ByteBuffer gather = ByteBufferUtils.gather(new ByteBuffer[]{buffer1, buffer2}); + + assertEquals("hello dennis", new String(gather.array())); + + assertNull(ByteBufferUtils.gather(null)); + assertNull(ByteBufferUtils.gather(new ByteBuffer[]{})); + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/DispatcherFactoryUnitTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/DispatcherFactoryUnitTest.java new file mode 100644 index 0000000..353c45d --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/DispatcherFactoryUnitTest.java @@ -0,0 +1,52 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; +import com.google.code.yanf4j.core.Dispatcher; +import com.google.code.yanf4j.util.DispatcherFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class DispatcherFactoryUnitTest { + @Test + public void testNewDispatcher() throws Exception { + Dispatcher dispatcher = + DispatcherFactory.newDispatcher(1, new ThreadPoolExecutor.AbortPolicy(), "test"); + assertNotNull(dispatcher); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicInteger count = new AtomicInteger(); + dispatcher.dispatch(new Runnable() { + public void run() { + count.incrementAndGet(); + latch.countDown(); + } + }); + latch.await(); + assertEquals(1, count.get()); + + assertNull( + DispatcherFactory.newDispatcher(0, new ThreadPoolExecutor.AbortPolicy(), "test")); + assertNull( + DispatcherFactory.newDispatcher(-1, new ThreadPoolExecutor.AbortPolicy(), "test")); + dispatcher.stop(); + try { + dispatcher.dispatch(new Runnable() { + public void run() { + fail(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/FastStringEncoderTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/FastStringEncoderTest.java new file mode 100644 index 0000000..de3e5e3 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/FastStringEncoderTest.java @@ -0,0 +1,50 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.FastStringEncoder; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FastStringEncoderTest { + + @Test + public void testASCII() { + this.assertEncodeEquals("hello world!@#$%^&*()_+|"); + for (int i = 0; i < 10000; i++) { + String uuid = UUID.randomUUID().toString(); + this.assertEncodeEquals(uuid); + } + } + + private void assertEncodeEquals(String s) { + assertEquals(s, ByteUtils.getString(FastStringEncoder.encodeUTF8(s))); + } + + @Test + public void testCJK() { + this.assertEncodeEquals("中华人民共和国"); + this.assertEncodeEquals("我能吞下玻璃而不傷身體"); + this.assertEncodeEquals("驚いた彼は道を走っていった。"); + this.assertEncodeEquals(" 나는 유리를 먹을 수 있어요. 그래도 아프지 않아요"); + this.assertEncodeEquals("私はガラスを食べられます。それは私を傷つけません"); + this.assertEncodeEquals("ฉันกินกระจกได้ แต่มันไม่ทำให้ฉันเจ็บ"); + } + + @Test + public void testBigString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 65535; i++) { + sb.append("a"); + } + this.assertEncodeEquals(sb.toString()); + } + + @Test + public void testEmoj() { + this.assertEncodeEquals( + "😀 😃 😄 😁 😆 😅 😂 🤣 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 😘 😗 😙 😚 😋 😜 😝 😛 🤑 🤗 🤓 😎 🤡 🤠 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 😤 😠 😡 😶 😐 😑 😯 😦 😧 😮 😲 😵 😳 😱 😨 😰 😢 😥 🤤 😭 😓 😪 😴 🙄 🤔 🤥 😬 🤐 🤢 🤧 😷 🤒 🤕 😈 👿 👹 👺 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾 "); + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/QueueTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/QueueTest.java new file mode 100644 index 0000000..7721f81 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/QueueTest.java @@ -0,0 +1,124 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import com.google.code.yanf4j.util.SimpleQueue; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.NoSuchElementException; +import java.util.Queue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class QueueTest { + private Queue queue; + + @BeforeEach + protected void setUp() throws Exception { + queue = new SimpleQueue(); + } + + @Test + public void testADD() { + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + queue.add("a"); + assertEquals(1, queue.size()); + assertFalse(queue.isEmpty()); + + queue.add("a"); + assertEquals(2, queue.size()); + assertFalse(queue.isEmpty()); + + queue.add("b"); + assertEquals(3, queue.size()); + assertFalse(queue.isEmpty()); + } + + @Test + public void testOffer() { + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + queue.offer("a"); + assertEquals(1, queue.size()); + assertFalse(queue.isEmpty()); + + queue.offer("a"); + assertEquals(2, queue.size()); + assertFalse(queue.isEmpty()); + + queue.offer("b"); + assertEquals(3, queue.size()); + assertFalse(queue.isEmpty()); + } + + @Test + public void testPoll() { + assertNull(queue.poll()); + queue.add("a"); + assertEquals("a", queue.poll()); + assertNull(queue.poll()); + queue.add("a"); + queue.add("b"); + assertEquals("a", queue.poll()); + assertEquals("b", queue.poll()); + assertNull(queue.poll()); + } + + @Test + public void testPeek() { + assertNull(queue.peek()); + queue.add("a"); + assertEquals("a", queue.peek()); + queue.add("b"); + assertEquals("a", queue.peek()); + queue.add("c"); + assertEquals("a", queue.peek()); + queue.poll(); + assertEquals("b", queue.peek()); + queue.poll(); + assertEquals("c", queue.peek()); + queue.poll(); + assertNull(queue.peek()); + } + + @Test + public void testRemove() { + try { + this.queue.remove(); + fail(); + } catch (NoSuchElementException e) { + + } + queue.add("a"); + assertEquals("a", queue.remove()); + try { + this.queue.remove(); + fail(); + } catch (NoSuchElementException e) { + + } + queue.add("b"); + queue.add("c"); + assertEquals("b", queue.remove()); + assertEquals("c", queue.remove()); + try { + this.queue.remove(); + fail(); + } catch (NoSuchElementException e) { + + } + } + + @AfterEach + protected void tearDown() throws Exception { + queue.clear(); + assertEquals(0, queue.size()); + assertTrue(queue.isEmpty()); + } + +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/ShiftAndByteBufferMatcherTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ShiftAndByteBufferMatcherTest.java new file mode 100644 index 0000000..769a6dd --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ShiftAndByteBufferMatcherTest.java @@ -0,0 +1,13 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.util.ByteBufferMatcher; +import com.google.code.yanf4j.util.ShiftAndByteBufferMatcher; + +public class ShiftAndByteBufferMatcherTest extends ByteBufferMatcherTest { + @Override + public ByteBufferMatcher createByteBufferMatcher(String hello) { + ByteBufferMatcher m = new ShiftAndByteBufferMatcher(IoBuffer.wrap(hello.getBytes())); + return m; + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/ShiftOrByteBufferMatcherTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ShiftOrByteBufferMatcherTest.java new file mode 100644 index 0000000..31b2307 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/ShiftOrByteBufferMatcherTest.java @@ -0,0 +1,13 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.util.ByteBufferMatcher; +import com.google.code.yanf4j.util.ShiftOrByteBufferMatcher; + +public class ShiftOrByteBufferMatcherTest extends ByteBufferMatcherTest { + @Override + public ByteBufferMatcher createByteBufferMatcher(String hello) { + ByteBufferMatcher m = new ShiftOrByteBufferMatcher(IoBuffer.wrap(hello.getBytes())); + return m; + } +} diff --git a/src/test/java/com/google/code/yanf4j/test/unittest/utils/SystemUtilsUniTest.java b/src/test/java/com/google/code/yanf4j/test/unittest/utils/SystemUtilsUniTest.java new file mode 100644 index 0000000..2d52ff3 --- /dev/null +++ b/src/test/java/com/google/code/yanf4j/test/unittest/utils/SystemUtilsUniTest.java @@ -0,0 +1,31 @@ +package com.google.code.yanf4j.test.unittest.utils; + +import com.google.code.yanf4j.util.SystemUtils; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.channels.Selector; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SystemUtilsUniTest { + + @Test + public void testOpenSelector() throws IOException { + Selector selector = SystemUtils.openSelector(); + assertNotNull(selector); + assertTrue(selector.isOpen()); + if (SystemUtils.isLinuxPlatform()) { + final String pollClassName = selector.provider().getClass().getCanonicalName(); + assertTrue(pollClassName.equals("sun.nio.ch.EPollSelectorProvider") + || pollClassName.equals("sun.nio.ch.PollSelectorProvider")); + } + Selector selector2 = SystemUtils.openSelector();; + assertNotSame(selector, selector2); + selector.close(); + selector2.close(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/example/BinaryProtocolExample.java b/src/test/java/net/rubyeye/xmemcached/example/BinaryProtocolExample.java new file mode 100644 index 0000000..4a27ecd --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/example/BinaryProtocolExample.java @@ -0,0 +1,73 @@ +package net.rubyeye.xmemcached.example; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple example for xmemcached,use binary protocol + * + * @author boyan + * + */ +public class BinaryProtocolExample { + + private static final Logger log = LoggerFactory.getLogger(BinaryProtocolExample.class); + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Useage:java BinaryProtocolExample [servers]"); + System.exit(1); + } + MemcachedClient memcachedClient = getMemcachedClient(args[0]); + if (memcachedClient == null) { + throw new NullPointerException( + "Null MemcachedClient,please check memcached has been started"); + } + try { + // add a + System.out.println("Add a,b,c"); + memcachedClient.set("a", 0, "Hello,xmemcached"); + // get a + String value = memcachedClient.get("a"); + System.out.println("get a=" + value); + System.out.println("delete a"); + // delete a + memcachedClient.delete("a"); + value = memcachedClient.get("a"); + System.out.println("after delete,a=" + value); + } catch (MemcachedException e) { + log.error("MemcachedClient operation fail", e); + } catch (TimeoutException e) { + log.error("MemcachedClient operation timeout", e); + } catch (InterruptedException e) { + // ignore + log.info(e.getMessage()); + } + try { + memcachedClient.shutdown(); + } catch (Exception e) { + log.error("Shutdown MemcachedClient fail", e); + } + } + + public static MemcachedClient getMemcachedClient(String servers) { + try { + MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); + // use binary protocol + builder.setCommandFactory(new BinaryCommandFactory()); + return builder.build(); + } catch (IOException e) { + System.err.println("Create MemcachedClient fail"); + e.printStackTrace(); + } + return null; + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/example/CASExample.java b/src/test/java/net/rubyeye/xmemcached/example/CASExample.java new file mode 100644 index 0000000..afafdc9 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/example/CASExample.java @@ -0,0 +1,99 @@ +/** + * Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] 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 net.rubyeye.xmemcached.example; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.CASOperation; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * CASOperation example + * + * @author dennis + */ +class CASThread extends Thread { + + private static final Logger log = LoggerFactory.getLogger(CASThread.class); + + /** + * Increase Operation + * + * @author dennis + * + */ + static final class IncrmentOperation implements CASOperation { + + public int getMaxTries() { + return Integer.MAX_VALUE; // Max repeat times + } + + public Integer getNewValue(long currentCAS, Integer currentValue) { + return currentValue + 1; + } + } + + private MemcachedClient mc; + private CountDownLatch cd; + + public CASThread(MemcachedClient mc, CountDownLatch cdl) { + super(); + this.mc = mc; + this.cd = cdl; + + } + + @Override + public void run() { + try { + for (int i = 0; i < 100; i++) + if (this.mc.cas("a", 0, new IncrmentOperation())) { + this.cd.countDown(); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } +} + + +public class CASExample { + + public static void main(String[] args) throws Exception { + // 125489 + if (args.length < 2) { + System.err.println("Usage:java CASTest [threadNum] [server]"); + System.exit(1); + } + int NUM = Integer.parseInt(args[0]); + MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(args[1])); + // use binary protocol + builder.setCommandFactory(new BinaryCommandFactory()); + MemcachedClient mc = builder.build(); + // initial value is 0 + mc.set("a", 0, 0); + CountDownLatch cdl = new CountDownLatch(NUM * 100); + long start = System.currentTimeMillis(); + // start Num threads to increase 'a' + for (int i = 0; i < NUM; i++) { + new CASThread(mc, cdl).start(); + } + + cdl.await(); + System.out.println("test cas,timed:" + (System.currentTimeMillis() - start)); + // print result,must equals to NUM + System.out.println("result=" + mc.get("a")); + mc.shutdown(); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/example/MemcachedStateListenerExample.java b/src/test/java/net/rubyeye/xmemcached/example/MemcachedStateListenerExample.java new file mode 100644 index 0000000..1bb08dc --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/example/MemcachedStateListenerExample.java @@ -0,0 +1,75 @@ +package net.rubyeye.xmemcached.example; + +import java.io.IOException; +import java.net.InetSocketAddress; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.MemcachedClientStateListener; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class MyListener implements MemcachedClientStateListener { + + private static final Logger log = LoggerFactory.getLogger(MyListener.class); + + public void onConnected(MemcachedClient memcachedClient, InetSocketAddress inetSocketAddress) { + System.out.println("Connect to " + inetSocketAddress); + } + + public void onDisconnected(MemcachedClient memcachedClient, InetSocketAddress inetSocketAddress) { + System.out.println("Disconnect from " + inetSocketAddress); + + } + + public void onException(MemcachedClient memcachedClient, Throwable throwable) { + log.error(throwable.getMessage(), throwable); + + } + + public void onShutDown(MemcachedClient memcachedClient) { + System.out.println("MemcachedClient has been shutdown"); + + } + + public void onStarted(MemcachedClient memcachedClient) { + System.out.println("MemcachedClient has been started"); + + } + +} + + +public class MemcachedStateListenerExample { + + private static final Logger log = LoggerFactory.getLogger(MemcachedStateListenerExample.class); + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Useage:java MemcachedStateListenerExample [servers]"); + System.exit(1); + } + MemcachedClient memcachedClient = getMemcachedClient(args[0]); + try { + memcachedClient.shutdown(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + public static MemcachedClient getMemcachedClient(String servers) { + try { + // use text protocol by default + MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); + // Add my state listener + builder.addStateListener(new MyListener()); + return builder.build(); + } catch (IOException e) { + System.err.println("Create MemcachedClient fail"); + log.error(e.getMessage(), e); + } + return null; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/example/SASLExample.java b/src/test/java/net/rubyeye/xmemcached/example/SASLExample.java new file mode 100644 index 0000000..d40ab63 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/example/SASLExample.java @@ -0,0 +1,80 @@ +package net.rubyeye.xmemcached.example; + +/** + * Memcached using sasl for authentication + * + * @author dennis + * + */ +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.auth.AuthInfo; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple example for xmemcached,use binary protocol + * + * @author boyan + * + */ +public class SASLExample { + + private static final Logger log = LoggerFactory.getLogger(SASLExample.class); + + public static void main(String[] args) { + if (args.length < 3) { + System.err.println("Useage:java SASLExample servers username password"); + System.exit(1); + } + MemcachedClient memcachedClient = getMemcachedClient(args[0], args[1], args[2]); + if (memcachedClient == null) { + throw new NullPointerException( + "Null MemcachedClient,please check memcached has been started"); + } + try { + // add a + System.out.println("Add a,b,c"); + memcachedClient.set("a", 0, "Hello,xmemcached"); + // get a + String value = memcachedClient.get("a"); + System.out.println("get a=" + value); + System.out.println("delete a"); + // delete a + memcachedClient.delete("a"); + value = memcachedClient.get("a"); + System.out.println("after delete,a=" + value); + } catch (MemcachedException e) { + log.error("MemcachedClient operation fail", e); + } catch (TimeoutException e) { + log.error("MemcachedClient operation timeout", e); + } catch (InterruptedException e) { + // ignore + } + try { + memcachedClient.shutdown(); + } catch (Exception e) { + log.error("Shutdown MemcachedClient fail", e); + } + } + + public static MemcachedClient getMemcachedClient(String servers, String username, + String password) { + try { + MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); + builder.addAuthInfo(AddrUtil.getOneAddress(servers), AuthInfo.typical(username, password)); + // Must use binary protocol + builder.setCommandFactory(new BinaryCommandFactory()); + return builder.build(); + } catch (IOException e) { + log.error("Create MemcachedClient fail", e); + } + return null; + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/example/SimpleExample.java b/src/test/java/net/rubyeye/xmemcached/example/SimpleExample.java new file mode 100644 index 0000000..47a9a2e --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/example/SimpleExample.java @@ -0,0 +1,83 @@ +package net.rubyeye.xmemcached.example; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import net.rubyeye.xmemcached.KeyIterator; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simple example for xmemcached + * + * @author boyan + * + */ +public class SimpleExample { + + private static final Logger log = LoggerFactory.getLogger(SimpleExample.class); + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Useage:java SimpleExample [servers]"); + System.exit(1); + } + MemcachedClient memcachedClient = getMemcachedClient(args[0]); + if (memcachedClient == null) { + throw new NullPointerException( + "Null MemcachedClient,please check memcached has been started"); + } + try { + // add a,b,c + System.out.println("Add a,b,c"); + memcachedClient.set("a", 0, "Hello,xmemcached"); + memcachedClient.set("b", 0, "Hello,xmemcached"); + memcachedClient.set("c", 0, "Hello,xmemcached"); + // get a + String value = memcachedClient.get("a"); + System.out.println("get a=" + value); + System.out.println("delete a"); + // delete a + memcachedClient.delete("a"); + // reget a + value = memcachedClient.get("a"); + System.out.println("after delete,a=" + value); + + System.out.println("Iterate all keys..."); + // iterate all keys + KeyIterator it = memcachedClient.getKeyIterator(AddrUtil.getOneAddress(args[0])); + while (it.hasNext()) { + System.out.println(it.next()); + } + System.out.println(memcachedClient.touch("b", 1000)); + + } catch (MemcachedException e) { + log.error("MemcachedClient operation fail", e); + } catch (TimeoutException e) { + log.error("MemcachedClient operation timeout", e); + } catch (InterruptedException e) { + // ignore + log.info(e.getMessage()); + } + try { + memcachedClient.shutdown(); + } catch (Exception e) { + log.error("Shutdown MemcachedClient fail", e); + } + } + + public static MemcachedClient getMemcachedClient(String servers) { + try { + // use text protocol by default + MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); + return builder.build(); + } catch (IOException e) { + log.error("Create MemcachedClient fail", e); + } + return null; + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/example/SpringExample.java b/src/test/java/net/rubyeye/xmemcached/example/SpringExample.java new file mode 100644 index 0000000..f7b5bb6 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/example/SpringExample.java @@ -0,0 +1,26 @@ +package net.rubyeye.xmemcached.example; + +import org.springframework.context.ApplicationContext; +import net.rubyeye.xmemcached.*; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class SpringExample { + public static void main(String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("sampleApplicationContext.xml"); + + MemcachedClient client1 = (MemcachedClient) ctx.getBean("memcachedClient1"); + MemcachedClient client2 = (MemcachedClient) ctx.getBean("memcachedClient2"); + test(client1); + test(client2); + client1.shutdown(); + client2.shutdown(); + + } + + public static void test(MemcachedClient client) throws Exception { + client.set("a", 0, 1); + if ((Integer) client.get("a") != 1) + System.err.println("get error"); + client.delete("a"); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/AbstractChecker.java b/src/test/java/net/rubyeye/xmemcached/helper/AbstractChecker.java new file mode 100644 index 0000000..8e31453 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/AbstractChecker.java @@ -0,0 +1,334 @@ +package net.rubyeye.xmemcached.helper; + + +import org.opentest4j.AssertionFailedError; + +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; + +public abstract class AbstractChecker implements ExceptionChecker { + + private static final double EPSILON = 0.00000001; + + public void call() throws Exception { + // TODO Auto-generated method stub + + } + + /** + * Asserts that a condition is true. If it isn't it throws an AssertionFailedError with the given + * message. + */ + static public void assertTrue(String message, boolean condition) { + if (!condition) + fail(message); + } + + /** + * Asserts that a condition is true. If it isn't it throws an AssertionFailedError. + */ + static public void assertTrue(boolean condition) { + assertTrue(null, condition); + } + + /** + * Asserts that a condition is false. If it isn't it throws an AssertionFailedError with the given + * message. + */ + static public void assertFalse(String message, boolean condition) { + assertTrue(message, !condition); + } + + /** + * Asserts that a condition is false. If it isn't it throws an AssertionFailedError. + */ + static public void assertFalse(boolean condition) { + assertFalse(null, condition); + } + + /** + * Fails a test with the given message. + */ + static public void fail(String message) { + throw new AssertionFailedError(message); + } + + /** + * Fails a test with no message. + */ + static public void fail() { + fail(null); + } + + /** + * Asserts that two objects are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, Object expected, Object actual) { + if (expected == null && actual == null) + return; + if (expected != null && expected.equals(actual)) + return; + failNotEquals(message, expected, actual); + } + + /** + * Asserts that two objects are equal. If they are not an AssertionFailedError is thrown. + */ + static public void assertEquals(Object expected, Object actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two Strings are equal. + */ + static public void assertEquals(String message, String expected, String actual) { + if (expected == null && actual == null) + return; + if (expected != null && expected.equals(actual)) + return; + assertionFailure() + .message(message) + .expected(expected) + .actual(actual) + .buildAndThrow(); + } + + /** + * Asserts that two Strings are equal. + */ + static public void assertEquals(String expected, String actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two doubles are equal concerning a delta. If they are not an AssertionFailedError + * is thrown with the given message. If the expected value is infinity then the delta value is + * ignored. + */ + static public void assertEquals(String message, double expected, double actual, double delta) { + // handle infinity specially since subtracting to infinite values gives + // NaN and the + // the following test fails + if (Double.isInfinite(expected)) { + if (!(Math.abs(expected - actual) <= EPSILON)) + failNotEquals(message, (Double) expected, (Double) actual); + } else if (!(Math.abs(expected - actual) <= delta)) // Because + // comparison with + // NaN always + // returns false + failNotEquals(message, (Double) expected, (Double) actual); + } + + /** + * Asserts that two doubles are equal concerning a delta. If the expected value is infinity then + * the delta value is ignored. + */ + static public void assertEquals(double expected, double actual, double delta) { + assertEquals(null, expected, actual, delta); + } + + /** + * Asserts that two floats are equal concerning a delta. If they are not an AssertionFailedError + * is thrown with the given message. If the expected value is infinity then the delta value is + * ignored. + */ + static public void assertEquals(String message, float expected, float actual, float delta) { + // handle infinity specially since subtracting to infinite values gives + // NaN and the + // the following test fails + if (Float.isInfinite(expected)) { + if (!(Math.abs(expected - actual) <= EPSILON)) + failNotEquals(message, (Float) expected, (Float) actual); + } else if (!(Math.abs(expected - actual) <= delta)) + failNotEquals(message, (Float) expected, (Float) actual); + } + + /** + * Asserts that two floats are equal concerning a delta. If the expected value is infinity then + * the delta value is ignored. + */ + static public void assertEquals(float expected, float actual, float delta) { + assertEquals(null, expected, actual, delta); + } + + /** + * Asserts that two longs are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, long expected, long actual) { + assertEquals(message, (Long) expected, (Long) actual); + } + + /** + * Asserts that two longs are equal. + */ + static public void assertEquals(long expected, long actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two booleans are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, boolean expected, boolean actual) { + assertEquals(message, (Boolean) expected, (Boolean) actual); + } + + /** + * Asserts that two booleans are equal. + */ + static public void assertEquals(boolean expected, boolean actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two bytes are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, byte expected, byte actual) { + assertEquals(message, (Byte) expected, (Byte) actual); + } + + /** + * Asserts that two bytes are equal. + */ + static public void assertEquals(byte expected, byte actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two chars are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, char expected, char actual) { + assertEquals(message, (Character) expected, (Character) actual); + } + + /** + * Asserts that two chars are equal. + */ + static public void assertEquals(char expected, char actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two shorts are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, short expected, short actual) { + assertEquals(message, (Short) expected, (Short) actual); + } + + /** + * Asserts that two shorts are equal. + */ + static public void assertEquals(short expected, short actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that two ints are equal. If they are not an AssertionFailedError is thrown with the + * given message. + */ + static public void assertEquals(String message, int expected, int actual) { + assertEquals(message, (Integer) expected, (Integer) actual); + } + + /** + * Asserts that two ints are equal. + */ + static public void assertEquals(int expected, int actual) { + assertEquals(null, expected, actual); + } + + /** + * Asserts that an object isn't null. + */ + static public void assertNotNull(Object object) { + assertNotNull(null, object); + } + + /** + * Asserts that an object isn't null. If it is an AssertionFailedError is thrown with the given + * message. + */ + static public void assertNotNull(String message, Object object) { + assertTrue(message, object != null); + } + + /** + * Asserts that an object is null. + */ + static public void assertNull(Object object) { + assertNull(null, object); + } + + /** + * Asserts that an object is null. If it is not an AssertionFailedError is thrown with the given + * message. + */ + static public void assertNull(String message, Object object) { + assertTrue(message, object == null); + } + + /** + * Asserts that two objects refer to the same object. If they are not an AssertionFailedError is + * thrown with the given message. + */ + static public void assertSame(String message, Object expected, Object actual) { + if (expected == actual) + return; + failNotSame(message, expected, actual); + } + + /** + * Asserts that two objects refer to the same object. If they are not the same an + * AssertionFailedError is thrown. + */ + static public void assertSame(Object expected, Object actual) { + assertSame(null, expected, actual); + } + + /** + * Asserts that two objects refer to the same object. If they are not an AssertionFailedError is + * thrown with the given message. + */ + static public void assertNotSame(String message, Object expected, Object actual) { + if (expected == actual) + failSame(message); + } + + /** + * Asserts that two objects refer to the same object. If they are not the same an + * AssertionFailedError is thrown. + */ + static public void assertNotSame(Object expected, Object actual) { + assertNotSame(null, expected, actual); + } + + static private void failSame(String message) { + String formatted = ""; + if (message != null) + formatted = message + " "; + fail(formatted + "expected not same"); + } + + static private void failNotSame(String message, Object expected, Object actual) { + String formatted = ""; + if (message != null) + formatted = message + " "; + fail(formatted + "expected same:<" + expected + "> was not:<" + actual + ">"); + } + + static private void failNotEquals(String message, Object expected, Object actual) { + fail(format(message, expected, actual)); + } + + static String format(String message, Object expected, Object actual) { + String formatted = ""; + if (message != null) + formatted = message + " "; + return formatted + "expected:<" + expected + "> but was:<" + actual + ">"; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/BlankKeyChecker.java b/src/test/java/net/rubyeye/xmemcached/helper/BlankKeyChecker.java new file mode 100644 index 0000000..d4a2c6a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/BlankKeyChecker.java @@ -0,0 +1,14 @@ +package net.rubyeye.xmemcached.helper; + +public class BlankKeyChecker extends AbstractChecker { + + public void check() throws Exception { + try { + call(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Key must not be blank", e.getMessage()); + } + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/ExceptionChecker.java b/src/test/java/net/rubyeye/xmemcached/helper/ExceptionChecker.java new file mode 100644 index 0000000..d1adb58 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/ExceptionChecker.java @@ -0,0 +1,13 @@ +package net.rubyeye.xmemcached.helper; + +/** + * 单元测试包装接口,用于检测异常是否正确 + * + * @author boyan + * + */ +public interface ExceptionChecker { + public void call() throws Exception; + + public void check() throws Exception; +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/InValidKeyChecker.java b/src/test/java/net/rubyeye/xmemcached/helper/InValidKeyChecker.java new file mode 100644 index 0000000..e89dbab --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/InValidKeyChecker.java @@ -0,0 +1,15 @@ +package net.rubyeye.xmemcached.helper; + +public class InValidKeyChecker extends AbstractChecker { + + public void check() throws Exception { + try { + call(); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().startsWith("Key contains invalid characters")); + } + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/MockTranscoder.java b/src/test/java/net/rubyeye/xmemcached/helper/MockTranscoder.java new file mode 100644 index 0000000..8a30f2d --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/MockTranscoder.java @@ -0,0 +1,54 @@ +package net.rubyeye.xmemcached.helper; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.CompressionMode; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; + +public class MockTranscoder implements Transcoder { + private volatile int count; + private SerializingTranscoder serializingTranscoder = new SerializingTranscoder(); + + public void setCompressionMode(CompressionMode compressMode) { + this.serializingTranscoder.setCompressionMode(compressMode); + + } + + public int getCount() { + return count; + } + + public T decode(CachedData d) { + count++; + return (T) serializingTranscoder.decode(d); + } + + public CachedData encode(T o) { + count++; + return serializingTranscoder.encode(o); + } + + public boolean isPackZeros() { + return serializingTranscoder.isPackZeros(); + } + + public boolean isPrimitiveAsString() { + return serializingTranscoder.isPrimitiveAsString(); + } + + public void setCompressionThreshold(int to) { + serializingTranscoder.setCompressionThreshold(to); + + } + + public void setPackZeros(boolean packZeros) { + serializingTranscoder.setPackZeros(packZeros); + + } + + public void setPrimitiveAsString(boolean primitiveAsString) { + serializingTranscoder.setPrimitiveAsString(primitiveAsString); + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/TooLongKeyChecker.java b/src/test/java/net/rubyeye/xmemcached/helper/TooLongKeyChecker.java new file mode 100644 index 0000000..1777097 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/TooLongKeyChecker.java @@ -0,0 +1,25 @@ +package net.rubyeye.xmemcached.helper; + +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.utils.Protocol; + +public class TooLongKeyChecker extends AbstractChecker { + private MemcachedClient client; + + public TooLongKeyChecker(MemcachedClient client) { + super(); + this.client = client; + } + + public void check() throws Exception { + try { + call(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Key is too long (maxlen = " + + (this.client.getProtocol() == Protocol.Text ? 250 : 250) + ")", e.getMessage()); + } + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/helper/TranscoderChecker.java b/src/test/java/net/rubyeye/xmemcached/helper/TranscoderChecker.java new file mode 100644 index 0000000..ebe5c86 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/helper/TranscoderChecker.java @@ -0,0 +1,18 @@ +package net.rubyeye.xmemcached.helper; + +public class TranscoderChecker extends AbstractChecker { + private MockTranscoder mockTranscoder; + private int expectCount; + + public TranscoderChecker(MockTranscoder mockTranscoder, int expectedCount) { + super(); + this.mockTranscoder = mockTranscoder; + this.expectCount = expectedCount; + } + + public void check() throws Exception { + call(); + assertEquals(expectCount, mockTranscoder.getCount()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/pressure/MemcachedClientPressureTest.java b/src/test/java/net/rubyeye/xmemcached/pressure/MemcachedClientPressureTest.java new file mode 100644 index 0000000..1dd49da --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/pressure/MemcachedClientPressureTest.java @@ -0,0 +1,131 @@ +package net.rubyeye.xmemcached.pressure; + +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicInteger; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.utils.AddrUtil; + +public class MemcachedClientPressureTest { + private static final class ClockWatch implements Runnable { + private long startTime; + private long stopTime; + + public synchronized void run() { + if (this.startTime == -1) { + this.startTime = System.nanoTime(); + } else { + this.stopTime = System.nanoTime(); + } + + } + + public synchronized void start() { + this.startTime = -1; + } + + public synchronized long getDurationInNano() { + return this.stopTime - this.startTime; + } + + public synchronized long getDurationInMillis() { + return (this.stopTime - this.startTime) / 1000000; + } + } + + static class TestThread extends Thread { + int repeat; + int start; + int size; + MemcachedClient client; + AtomicInteger failure; + AtomicInteger success; + CyclicBarrier barrier; + + public TestThread(int repeat, int start, int size, MemcachedClient client, + AtomicInteger failure, AtomicInteger success, CyclicBarrier barrier) { + super(); + this.repeat = repeat; + this.start = start; + this.size = size; + this.client = client; + this.failure = failure; + this.success = success; + this.barrier = barrier; + } + + public void run() { + byte[] value = new byte[size]; + try { + barrier.await(); + for (int i = 0; i < repeat; i++) { + String key = String.valueOf(start + i); + try { + if (!client.set(key, 10, value)) + throw new RuntimeException("set failed"); + byte[] v = client.get(key); + if (v == null || v.length != size) + throw new RuntimeException("get failed"); + if (!client.touch(key, 10)) + throw new RuntimeException("touch failed"); + if (!client.delete(key, 10)) + throw new RuntimeException("delete failed"); + success.incrementAndGet(); + } catch (Exception e) { + e.printStackTrace(); + failure.incrementAndGet(); + } + } + barrier.await(); + } catch (Exception e) { + // ignore; + } + } + + } + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + throw new RuntimeException("Please provide memcached servers."); + } + final int threads = 100; + final int repeat = 50000; + int size = 1024; + + String servers = args[0]; + final AtomicInteger failure = new AtomicInteger(); + final AtomicInteger success = new AtomicInteger(); + MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses(servers)); + // builder.setCommandFactory(new BinaryCommandFactory()); + MemcachedClient client = builder.build(); + ClockWatch watch = new ClockWatch(); + CyclicBarrier barrier = new CyclicBarrier(threads + 1, watch); + for (int i = 0; i < threads; i++) { + new TestThread(repeat, i * repeat * 2, size, client, failure, success, barrier).start(); + } + new Thread() { + public void run() { + try { + while (success.get() + failure.get() < repeat * threads) { + Thread.sleep(1000); + System.out.println("success:" + success.get() + ",failure:" + failure.get()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }.start(); + watch.start(); + barrier.await(); + barrier.await(); + + long secs = watch.getDurationInMillis() / 1000; + int total = 4 * repeat * threads; + long tps = total / secs; + client.shutdown(); + System.out.println( + "duration:" + secs + " seconds,tps:" + tps + " op/seconds,total:" + total + " ops"); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/AWSElasticCacheClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/AWSElasticCacheClientIT.java new file mode 100644 index 0000000..f2433ec --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/AWSElasticCacheClientIT.java @@ -0,0 +1,174 @@ +package net.rubyeye.xmemcached.test.unittest; + +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.core.impl.TextLineCodecFactory; +import com.google.code.yanf4j.nio.TCPController; +import com.google.code.yanf4j.util.ResourcesUtils; +import net.rubyeye.xmemcached.autodiscovery.ClusterConfiguration; +import net.rubyeye.xmemcached.aws.AWSElasticCacheClient; +import net.rubyeye.xmemcached.aws.AWSElasticCacheClientBuilder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class AWSElasticCacheClientIT { + + private String serverList; + private List addresses; + + /** + * elasticcache config node mock handler + * + * @author dennis + * + */ + private static final class MockHandler extends HandlerAdapter { + private final String response; + private int version; + + public MockHandler(int version, String response) { + super(); + this.response = response; + this.version = version; + } + + @Override + public void onMessageReceived(Session session, Object message) { + if (message.equals("quit")) { + session.close(); + return; + } + session.write("CONFIG cluster 0 " + this.response.length()); + session.write(String.valueOf(version) + "\n" + this.response); + session.write("END"); + this.version++; + } + + } + + @BeforeEach + public void setUp() throws Exception { + Properties properties = ResourcesUtils.getResourceAsProperties("test.properties"); + List addresses = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + StringBuffer sb = new StringBuffer(); + boolean wasFirst = true; + for (InetSocketAddress addr : addresses) { + if (wasFirst) { + wasFirst = false; + } else { + sb.append(" "); + } + sb.append(addr.getHostName() + "|" + addr.getHostName() + "|" + addr.getPort()); + + } + + this.addresses = addresses; + serverList = sb.toString(); + } + + @Test + public void testInvalidConfig() throws Exception { + TCPController configServer = new TCPController(); + int version = 10; + configServer.setHandler(new MockHandler(version, "invalid")); + configServer.setCodecFactory(new TextLineCodecFactory()); + configServer.bind(new InetSocketAddress(2271)); + + try { + AWSElasticCacheClient client = new AWSElasticCacheClient(new InetSocketAddress(2271)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("failed")); + } finally { + configServer.stop(); + } + } + + @Test + public void testPollConfigAndUsage() throws Exception { + TCPController configServer = new TCPController(); + int version = 10; + configServer.setHandler(new MockHandler(version, serverList)); + configServer.setCodecFactory(new TextLineCodecFactory()); + configServer.bind(new InetSocketAddress(2271)); + + try { + AWSElasticCacheClient client = new AWSElasticCacheClient(new InetSocketAddress(2271)); + ClusterConfiguration config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version); + assertEquals(addresses.size(), config.getNodeList().size()); + + client.set("aws-cache", 0, "foobar"); + assertEquals("foobar", client.get("aws-cache")); + } finally { + configServer.stop(); + } + } + + @Test + public void testPollConfigAndUsageWithBuilder() throws Exception { + TCPController configServer = new TCPController(); + int version = 10; + configServer.setHandler(new MockHandler(version, serverList)); + configServer.setCodecFactory(new TextLineCodecFactory()); + configServer.bind(new InetSocketAddress(2279)); + + try { + AWSElasticCacheClientBuilder builder = + new AWSElasticCacheClientBuilder(new InetSocketAddress(2279)); + builder.setConnectionPoolSize(2); + builder.setEnableHealSession(false); + AWSElasticCacheClient client = builder.build(); + ClusterConfiguration config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version); + assertEquals(addresses.size(), config.getNodeList().size()); + + client.set("aws-cache", 0, "foobar"); + assertEquals("foobar", client.get("aws-cache")); + } finally { + configServer.stop(); + } + } + + @Test + public void testPollConfigInterval() throws Exception { + TCPController cs1 = new TCPController(); + int version = 10; + cs1.setHandler(new MockHandler(version, "localhost|localhost|2272")); + cs1.setCodecFactory(new TextLineCodecFactory()); + cs1.bind(new InetSocketAddress(2271)); + TCPController cs2 = new TCPController(); + cs2.setHandler( + new MockHandler(version + 1, "localhost|localhost|2271 localhost|localhost|2272")); + cs2.setCodecFactory(new TextLineCodecFactory()); + cs2.bind(new InetSocketAddress(2272)); + + try { + AWSElasticCacheClient client = new AWSElasticCacheClient(new InetSocketAddress(2271), 3000); + ClusterConfiguration config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version); + assertEquals(1, config.getNodeList().size()); + assertEquals(2272, config.getNodeList().get(0).getPort()); + Thread.sleep(3500); + config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version + 1); + assertEquals(2, config.getNodeList().size()); + assertEquals(2271, config.getNodeList().get(0).getPort()); + assertEquals(2272, config.getNodeList().get(1).getPort()); + } finally { + cs1.stop(); + cs2.stop(); + } + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/AutoDiscoveryCacheClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/AutoDiscoveryCacheClientIT.java new file mode 100644 index 0000000..8df7d23 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/AutoDiscoveryCacheClientIT.java @@ -0,0 +1,174 @@ +package net.rubyeye.xmemcached.test.unittest; + +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.core.impl.TextLineCodecFactory; +import com.google.code.yanf4j.nio.TCPController; +import com.google.code.yanf4j.util.ResourcesUtils; +import net.rubyeye.xmemcached.autodiscovery.AutoDiscoveryCacheClient; +import net.rubyeye.xmemcached.autodiscovery.AutoDiscoveryCacheClientBuilder; +import net.rubyeye.xmemcached.autodiscovery.ClusterConfiguration; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class AutoDiscoveryCacheClientIT { + + private String serverList; + private List addresses; + + /** + * elasticcache config node mock handler + * + * @author dennis + * + */ + private static final class MockHandler extends HandlerAdapter { + private final String response; + private int version; + + public MockHandler(int version, String response) { + super(); + this.response = response; + this.version = version; + } + + @Override + public void onMessageReceived(Session session, Object message) { + if (message.equals("quit")) { + session.close(); + return; + } + session.write("CONFIG cluster 0 " + this.response.length()); + session.write(String.valueOf(version) + "\n" + this.response); + session.write("END"); + this.version++; + } + + } + + @BeforeEach + public void setUp() throws Exception { + Properties properties = ResourcesUtils.getResourceAsProperties("test.properties"); + List addresses = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + StringBuffer sb = new StringBuffer(); + boolean wasFirst = true; + for (InetSocketAddress addr : addresses) { + if (wasFirst) { + wasFirst = false; + } else { + sb.append(" "); + } + sb.append(addr.getHostName() + "|" + addr.getHostName() + "|" + addr.getPort()); + + } + + this.addresses = addresses; + serverList = sb.toString(); + } + + @Test + public void testInvalidConfig() throws Exception { + TCPController configServer = new TCPController(); + int version = 10; + configServer.setHandler(new MockHandler(version, "invalid")); + configServer.setCodecFactory(new TextLineCodecFactory()); + configServer.bind(new InetSocketAddress(2271)); + + try { + AutoDiscoveryCacheClient client = new AutoDiscoveryCacheClient(new InetSocketAddress(2271)); + fail(); + } catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("failed")); + } finally { + configServer.stop(); + } + } + + @Test + public void testPollConfigAndUsage() throws Exception { + TCPController configServer = new TCPController(); + int version = 10; + configServer.setHandler(new MockHandler(version, serverList)); + configServer.setCodecFactory(new TextLineCodecFactory()); + configServer.bind(new InetSocketAddress(2271)); + + try { + AutoDiscoveryCacheClient client = new AutoDiscoveryCacheClient(new InetSocketAddress(2271)); + ClusterConfiguration config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version); + assertEquals(addresses.size(), config.getNodeList().size()); + + client.set("auto-discovery-cache", 0, "foobar"); + assertEquals("foobar", client.get("auto-discovery-cache")); + } finally { + configServer.stop(); + } + } + + @Test + public void testPollConfigAndUsageWithBuilder() throws Exception { + TCPController configServer = new TCPController(); + int version = 10; + configServer.setHandler(new MockHandler(version, serverList)); + configServer.setCodecFactory(new TextLineCodecFactory()); + configServer.bind(new InetSocketAddress(2279)); + + try { + AutoDiscoveryCacheClientBuilder builder = + new AutoDiscoveryCacheClientBuilder(new InetSocketAddress(2279)); + builder.setConnectionPoolSize(2); + builder.setEnableHealSession(false); + AutoDiscoveryCacheClient client = builder.build(); + ClusterConfiguration config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version); + assertEquals(addresses.size(), config.getNodeList().size()); + + client.set("auto-discovery-cache", 0, "foobar"); + assertEquals("foobar", client.get("auto-discovery-cache")); + } finally { + configServer.stop(); + } + } + + @Test + public void testPollConfigInterval() throws Exception { + TCPController cs1 = new TCPController(); + int version = 10; + cs1.setHandler(new MockHandler(version, "localhost|localhost|2272")); + cs1.setCodecFactory(new TextLineCodecFactory()); + cs1.bind(new InetSocketAddress(2271)); + TCPController cs2 = new TCPController(); + cs2.setHandler( + new MockHandler(version + 1, "localhost|localhost|2271 localhost|localhost|2272")); + cs2.setCodecFactory(new TextLineCodecFactory()); + cs2.bind(new InetSocketAddress(2272)); + + try { + AutoDiscoveryCacheClient client = + new AutoDiscoveryCacheClient(new InetSocketAddress(2271), 3000); + ClusterConfiguration config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version); + assertEquals(1, config.getNodeList().size()); + assertEquals(2272, config.getNodeList().get(0).getPort()); + Thread.sleep(3500); + config = client.getCurrentConfig(); + assertEquals(config.getVersion(), version + 1); + assertEquals(2, config.getNodeList().size()); + assertEquals(2271, config.getNodeList().get(0).getPort()); + assertEquals(2272, config.getNodeList().get(1).getPort()); + } finally { + cs1.stop(); + cs2.stop(); + } + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/BinaryMemcachedClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/BinaryMemcachedClientIT.java new file mode 100644 index 0000000..da21d38 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/BinaryMemcachedClientIT.java @@ -0,0 +1,155 @@ +package net.rubyeye.xmemcached.test.unittest; + +import net.rubyeye.xmemcached.GetsResponse; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.transcoders.StringTranscoder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 为了使Binary协议的测试通过,将检测key是否有非法字符,事实上binary协议无需检测的。 + * + * @author boyan + * + */ +public class BinaryMemcachedClientIT extends XMemcachedClientIT { + @Override + public MemcachedClientBuilder createBuilder() throws Exception { + + MemcachedClientBuilder builder = new XMemcachedClientBuilder( + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers"))); + builder.setCommandFactory(new BinaryCommandFactory()); + ByteUtils.testing = true; + return builder; + } + + @Override + public MemcachedClientBuilder createWeightedBuilder() throws Exception { + List addressList = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + int[] weights = new int[addressList.size()]; + for (int i = 0; i < weights.length; i++) { + weights[i] = i + 1; + } + + MemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights); + builder.setCommandFactory(new BinaryCommandFactory()); + builder.setSessionLocator(new KetamaMemcachedSessionLocator()); + ByteUtils.testing = true; + return builder; + } + + @Override + @Test + public void testTouch() throws Exception { + if (this.isMemcached1_6()) { + assertNull(this.memcachedClient.get("name")); + this.memcachedClient.set("name", 1, "dennis", new StringTranscoder(), 1000); + assertEquals("dennis", this.memcachedClient.get("name", new StringTranscoder())); + + // touch expiration to three seconds + System.out.println(this.memcachedClient.touch("name", 3)); + Thread.sleep(2000); + assertEquals("dennis", this.memcachedClient.get("name", new StringTranscoder())); + Thread.sleep(1500); + assertNull(this.memcachedClient.get("name")); + } + } + + @Test + public void testTouchNotExists() throws Exception { + if (this.isMemcached1_6()) { + assertNull(this.memcachedClient.get("name")); + assertFalse(this.memcachedClient.touch("name", 3)); + } + } + + @Test + public void testGetAndTouch_OneKey() throws Exception { + if (this.isMemcached1_6()) { + assertNull(this.memcachedClient.get("name")); + this.memcachedClient.set("name", 1, "dennis", new StringTranscoder(), 1000); + assertEquals("dennis", this.memcachedClient.getAndTouch("name", 3)); + + Thread.sleep(2000); + assertEquals("dennis", this.memcachedClient.get("name", new StringTranscoder())); + Thread.sleep(1500); + assertNull(this.memcachedClient.get("name")); + } + } + + private boolean isMemcached1_6() throws Exception { + Map versions = this.memcachedClient.getVersions(); + for (String v : versions.values()) { + if (!v.startsWith("1.6")) { + return false; + } + } + return true; + } + + @Test + public void testGetAndTouch_NotExistsKey() throws Exception { + if (this.isMemcached1_6()) { + assertNull(this.memcachedClient.get("name")); + assertNull(this.memcachedClient.getAndTouch("name", 3)); + assertNull(this.memcachedClient.getAndTouch("name", 3)); + assertNull(this.memcachedClient.get("name")); + } + } + + @Test + public void testDeleteWithCAS() throws Exception { + this.memcachedClient.set("a", 0, 1); + GetsResponse gets = this.memcachedClient.gets("a"); + this.memcachedClient.set("a", 0, 2); + assertFalse(this.memcachedClient.delete("a", gets.getCas(), 1000)); + assertEquals(2, (int) this.memcachedClient.get("a")); + gets = this.memcachedClient.gets("a"); + assertTrue(this.memcachedClient.delete("a", gets.getCas(), 1000)); + assertNull(this.memcachedClient.get("a")); + } + + @Test + @Disabled + public void testBulkGetAndTouch() throws Exception { + int count = 100; + + Map keys = new HashMap(); + int exp = 3; + for (int i = 0; i < count; i++) { + String key = String.valueOf(i); + keys.put(key, exp); + assertNull(this.memcachedClient.get(key)); + } + for (int i = 0; i < count; i++) { + this.memcachedClient.set(String.valueOf(i), i, i); + } + + // Map result = this.memcachedClient.getAndTouch(keys, + // 5000L); + // for (Map.Entry entry : result.entrySet()) { + // assertEquals(entry.getKey(), String.valueOf(entry.getValue())); + // } + + Thread.sleep(3500); + // assertTrue(this.memcachedClient.getAndTouch(keys, 5000L).isEmpty()); + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/ConnectionPoolMemcachedClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/ConnectionPoolMemcachedClientIT.java new file mode 100644 index 0000000..1ba1ddd --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/ConnectionPoolMemcachedClientIT.java @@ -0,0 +1,169 @@ +package net.rubyeye.xmemcached.test.unittest; + +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.nio.TCPController; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClient; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ConnectionPoolMemcachedClientIT extends XMemcachedClientIT { + private static final int CONNECTION_POOL_SIZE = 3; + + @Override + public MemcachedClientBuilder createBuilder() throws Exception { + MemcachedClientBuilder builder = new XMemcachedClientBuilder( + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers"))); + builder.setConnectionPoolSize(CONNECTION_POOL_SIZE); + return builder; + } + + /** + * Mock memcached server + * + * @author boyan + * + */ + static class MockServer { + private static final int PORT = 9999; + TCPController controller; + AtomicInteger sessionCounter = new AtomicInteger(0); + + public void start() { + controller = new TCPController(); + controller.setHandler(new HandlerAdapter() { + + @Override + public void onSessionClosed(Session session) { + sessionCounter.decrementAndGet(); + } + + @Override + public void onSessionCreated(Session session) { + System.out.println("connection created," + sessionCounter.incrementAndGet()); + } + + }); + try { + controller.bind(PORT); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public InetSocketAddress getServerAddress() { + return new InetSocketAddress("localhost", PORT); + } + + public void stop() { + try { + controller.stop(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Test + public void testHealSession() throws Exception { + MockServer server = new MockServer(); + server.start(); + InetSocketAddress serverAddress = server.getServerAddress(); + XMemcachedClient client = new XMemcachedClient(); + client.setConnectionPoolSize(5); + client.setEnableHeartBeat(false); + client.addServer(serverAddress); + synchronized (this) { + while (server.sessionCounter.get() < 5) { + this.wait(1000); + } + } + assertEquals(1, client.getAvaliableServers().size()); + assertEquals(5, client.getConnectionSizeBySocketAddress(serverAddress)); + assertEquals(5, server.sessionCounter.get()); + + // stop mock server,try to heal sessions + server.stop(); + + Thread.sleep(10000); + assertEquals(0, client.getAvaliableServers().size()); + assertEquals(0, client.getConnectionSizeBySocketAddress(serverAddress)); + // new server start + server = new MockServer(); + server.start(); + Thread.sleep(30000); + assertEquals(1, client.getAvaliableServers().size()); + assertEquals(5, client.getConnectionSizeBySocketAddress(serverAddress)); + assertEquals(5, server.sessionCounter.get()); + + server.stop(); + client.shutdown(); + + } + + @Test + public void testDisableHealSession() throws Exception { + MockServer server = new MockServer(); + server.start(); + InetSocketAddress serverAddress = server.getServerAddress(); + XMemcachedClient client = new XMemcachedClient(); + client.setConnectionPoolSize(5); + client.setEnableHeartBeat(false); + // disable heal session. + client.setEnableHealSession(false); + client.addServer(serverAddress); + synchronized (this) { + while (server.sessionCounter.get() < 5) { + this.wait(1000); + } + } + assertEquals(1, client.getAvaliableServers().size()); + assertEquals(5, client.getConnectionSizeBySocketAddress(serverAddress)); + assertEquals(5, server.sessionCounter.get()); + + // stop mock server,try to heal sessions + server.stop(); + + Thread.sleep(10000); + assertEquals(0, client.getAvaliableServers().size()); + assertEquals(0, client.getConnectionSizeBySocketAddress(serverAddress)); + // new server start + server = new MockServer(); + server.start(); + Thread.sleep(30000); + // Still empty. + assertEquals(0, client.getAvaliableServers().size()); + assertEquals(0, client.getConnectionSizeBySocketAddress(serverAddress)); + + server.stop(); + client.shutdown(); + + } + + @Override + public MemcachedClientBuilder createWeightedBuilder() throws Exception { + List addressList = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + int[] weights = new int[addressList.size()]; + for (int i = 0; i < weights.length; i++) { + weights[i] = i + 1; + } + + MemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights); + builder.setConnectionPoolSize(CONNECTION_POOL_SIZE); + builder.setCommandFactory(new BinaryCommandFactory()); + builder.setSessionLocator(new KetamaMemcachedSessionLocator()); + return builder; + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/ConsistentHashMemcachedClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/ConsistentHashMemcachedClientIT.java new file mode 100644 index 0000000..b071a48 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/ConsistentHashMemcachedClientIT.java @@ -0,0 +1,33 @@ +package net.rubyeye.xmemcached.test.unittest; + +import java.net.InetSocketAddress; +import java.util.List; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.utils.AddrUtil; + +public class ConsistentHashMemcachedClientIT extends StandardHashMemcachedClientIT { + @Override + public MemcachedClientBuilder createBuilder() throws Exception { + MemcachedClientBuilder builder = new XMemcachedClientBuilder( + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers"))); + builder.setSessionLocator(new KetamaMemcachedSessionLocator()); + + return builder; + } + + @Override + public MemcachedClientBuilder createWeightedBuilder() throws Exception { + List addressList = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + int[] weights = new int[addressList.size()]; + for (int i = 0; i < weights.length; i++) { + weights[i] = i + 1; + } + MemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights); + builder.setSessionLocator(new KetamaMemcachedSessionLocator()); + return builder; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/FailureModeUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/FailureModeUnitTest.java new file mode 100644 index 0000000..2cdd478 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/FailureModeUnitTest.java @@ -0,0 +1,338 @@ +package net.rubyeye.xmemcached.test.unittest; + +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.core.impl.TextLineCodecFactory; +import com.google.code.yanf4j.nio.TCPController; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * Unit test for failure mode + * + * @author dennis + * @date 2010-12-28 + */ +@Disabled +public class FailureModeUnitTest { + /** + * Mock handler for memcached server + * + * @author dennis + * @date 2010-12-28 + */ + private static final class MockHandler extends HandlerAdapter { + private final String response; + + public MockHandler(String response) { + super(); + this.response = response; + } + + @Override + public void onMessageReceived(Session session, Object message) { + String line = (String) message; + String key = line.split(" ")[1]; + session.write("VALUE " + key + " 0 " + this.response.length()); + session.write(this.response); + session.write("END"); + } + + } + + @Test + public void testFailureMode_HasStandbyNode() throws Exception { + TCPController memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + + TCPController memServer2 = new TCPController(); + memServer2.setHandler(new MockHandler("response from server2")); + memServer2.setCodecFactory(new TextLineCodecFactory()); + memServer2.bind(new InetSocketAddress(4798)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799,localhost:4798")); + // It must be in failure mode + builder.setFailureMode(true); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + + try { + + assertEquals("response from server1", client.get("a")); + assertEquals("response from server1", client.get("a")); + memServer1.stop(); + Thread.sleep(1000); + assertEquals("response from server2", client.get("a")); + // restart server1 + memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + Thread.sleep(5000); + assertEquals("response from server1", client.get("a")); + } finally { + memServer1.stop(); + memServer2.stop(); + client.shutdown(); + } + + } + + @Test + public void testFailureMode_OneServerDownOnStartup() throws Exception { + + TCPController memServer2 = new TCPController(); + memServer2.setHandler(new MockHandler("response from server2")); + memServer2.setCodecFactory(new TextLineCodecFactory()); + memServer2.bind(new InetSocketAddress(4798)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799 localhost:4798")); + // It must be in failure mode + builder.setFailureMode(true); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + TCPController memServer1 = null; + try { + + assertEquals("response from server2", client.get("a")); + try { + assertEquals("response from server1", client.get("b")); + fail(); + } catch (MemcachedException e) { + assertEquals("Session(127.0.0.1:4799) has been closed", e.getMessage()); + } + assertEquals(1, client.getConnector() + .getSessionByAddress(AddrUtil.getOneAddress("localhost:4799")).size()); + memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + Thread.sleep(5000); + assertEquals(1, client.getConnector() + .getSessionByAddress(AddrUtil.getOneAddress("localhost:4799")).size()); + assertEquals("response from server2", client.get("a")); + assertEquals("response from server1", client.get("b")); + } finally { + if (memServer1 != null) + memServer1.stop(); + memServer2.stop(); + client.shutdown(); + } + + } + + @Test + public void testFailureMode_StandbyNodeDown_Recover() throws Exception { + TCPController memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + + TCPController memServer2 = new TCPController(); + memServer2.setHandler(new MockHandler("response from server2")); + memServer2.setCodecFactory(new TextLineCodecFactory()); + memServer2.bind(new InetSocketAddress(4798)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799,localhost:4798")); + // It must be in failure mode + builder.setFailureMode(true); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + + try { + assertEquals("response from server1", client.get("a")); + assertEquals("response from server1", client.get("a")); + memServer1.stop(); + Thread.sleep(1000); + assertEquals("response from server2", client.get("a")); + memServer2.stop(); + Thread.sleep(1000); + try { + client.get("a"); + fail(); + } catch (MemcachedException e) { + assertEquals("Session(127.0.0.1:4799) has been closed", e.getMessage()); + // e.printStackTrace(); + } + // restart server2 + memServer2 = new TCPController(); + memServer2.setHandler(new MockHandler("response from server2")); + memServer2.setCodecFactory(new TextLineCodecFactory()); + memServer2.bind(new InetSocketAddress(4798)); + Thread.sleep(5000); + assertEquals("response from server2", client.get("a")); + + // restart server1 + memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + Thread.sleep(10000); + assertEquals("response from server1", client.get("a")); + + } finally { + memServer1.stop(); + memServer2.stop(); + client.shutdown(); + } + + } + + @Test + public void testFailureMode_NoStandbyNode() throws Exception { + TCPController memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799")); + builder.setFailureMode(true); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + + try { + + assertEquals("response from server1", client.get("a")); + assertEquals("response from server1", client.get("a")); + memServer1.stop(); + Thread.sleep(1000); + try { + client.get("a"); + fail(); + } catch (MemcachedException e) { + assertEquals("Session(127.0.0.1:4799) has been closed", e.getMessage()); + assertTrue(true); + } + } finally { + memServer1.stop(); + client.shutdown(); + } + + } + + @Test + public void testNotFailureMode_HasStandbyNode() throws Exception { + TCPController memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + + TCPController memServer2 = new TCPController(); + memServer2.setHandler(new MockHandler("response from server2")); + memServer2.setCodecFactory(new TextLineCodecFactory()); + memServer2.bind(new InetSocketAddress(4798)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799,localhost:4798")); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + try { + + assertEquals("response from server1", client.get("a")); + assertEquals("response from server1", client.get("a")); + memServer1.stop(); + Thread.sleep(1000); + try { + client.get("a"); + fail(); + } catch (MemcachedException e) { + assertEquals("There is no available connection at this moment", e.getMessage()); + assertTrue(true); + } + } finally { + memServer1.stop(); + memServer2.stop(); + client.shutdown(); + } + + } + + @Test + public void testNotFailureMode_NoStandbyNode() throws Exception { + TCPController memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799")); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + try { + + assertEquals("response from server1", client.get("a")); + assertEquals("response from server1", client.get("a")); + memServer1.stop(); + Thread.sleep(1000); + try { + client.get("a"); + fail(); + } catch (MemcachedException e) { + assertEquals("There is no available connection at this moment", e.getMessage()); + assertTrue(true); + } + } finally { + memServer1.stop(); + client.shutdown(); + } + + } + + @Test + public void testNotFailureMode_NoStandbyNode_TwoServers() throws Exception { + TCPController memServer1 = new TCPController(); + memServer1.setHandler(new MockHandler("response from server1")); + memServer1.setCodecFactory(new TextLineCodecFactory()); + memServer1.bind(new InetSocketAddress(4799)); + + TCPController memServer2 = new TCPController(); + memServer2.setHandler(new MockHandler("response from server2")); + memServer2.setCodecFactory(new TextLineCodecFactory()); + memServer2.bind(new InetSocketAddress(4798)); + + MemcachedClientBuilder builder = + new XMemcachedClientBuilder(AddrUtil.getAddressMap("localhost:4799 localhost:4798")); + MemcachedClient client = builder.build(); + + client.setEnableHeartBeat(false); + + try { + + assertEquals("response from server2", client.get("a")); + assertEquals("response from server2", client.get("a")); + memServer2.stop(); + Thread.sleep(1000); + assertEquals("response from server1", client.get("a")); + } finally { + memServer1.stop(); + memServer2.stop(); + client.shutdown(); + } + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/KestrelClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/KestrelClientIT.java new file mode 100644 index 0000000..bd73502 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/KestrelClientIT.java @@ -0,0 +1,262 @@ +package net.rubyeye.xmemcached.test.unittest; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CyclicBarrier; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.command.KestrelCommandFactory; +import net.rubyeye.xmemcached.utils.AddrUtil; +import com.google.code.yanf4j.util.ResourcesUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class KestrelClientIT { + static class UserDefinedClass implements Serializable { + private String name; + + public UserDefinedClass(String name) { + super(); + this.name = name; + } + + } + + Properties properties; + private MemcachedClient memcachedClient; + + @BeforeEach + public void setUp() throws Exception { + this.properties = ResourcesUtils.getResourceAsProperties("test.properties"); + MemcachedClientBuilder builder = newBuilder(); + builder.setConnectionPoolSize(5); + // builder.getConfiguration().setSessionIdleTimeout(5); + this.memcachedClient = builder.build(); + this.memcachedClient.flushAll(); + } + + private MemcachedClientBuilder newBuilder() { + MemcachedClientBuilder builder = new XMemcachedClientBuilder( + AddrUtil.getAddresses(this.properties.getProperty("test.kestrel.servers"))); + // Use kestrel command factory + builder.setCommandFactory(new KestrelCommandFactory()); + return builder; + } + + public void testPrimitiveAsString() throws Exception { + this.memcachedClient.setPrimitiveAsString(true); + // store integer + for (int i = 0; i < 1000; i++) { + this.memcachedClient.set("queue1", 0, i); + } + // but get string + for (int i = 0; i < 1000; i++) { + assertEquals(String.valueOf(i), this.memcachedClient.get("queue1")); + } + + this.memcachedClient.setPrimitiveAsString(false); + // store integer + for (int i = 0; i < 1000; i++) { + this.memcachedClient.set("queue1", 0, i); + } + // still get integer + for (int i = 0; i < 1000; i++) { + assertEquals(i, (int) this.memcachedClient.get("queue1")); + } + } + + @AfterEach + public void tearDown() throws IOException { + this.memcachedClient.shutdown(); + } + + public void testNormalSetAndGet() throws Exception { + assertNull(this.memcachedClient.get("queue1")); + + assertTrue(this.memcachedClient.set("queue1", 0, "hello world")); + assertEquals("hello world", this.memcachedClient.get("queue1")); + + assertNull(this.memcachedClient.get("queue1")); + } + + public void testNormalSetAndGetMore() throws Exception { + assertNull(this.memcachedClient.get("queue1")); + for (int i = 0; i < 10; i++) { + assertTrue(this.memcachedClient.set("queue1", 0, i)); + } + for (int i = 0; i < 10; i++) { + assertEquals(i, (int) this.memcachedClient.get("queue1")); + } + assertNull(this.memcachedClient.get("queue1")); + } + + public void testSetAndGetObject() throws Exception { + Map map = new HashMap(); + map.put("a", "a"); + map.put("b", "b"); + map.put("c", "c"); + assertTrue(this.memcachedClient.set("queue1", 0, map)); + + Map mapFromMQ = (Map) this.memcachedClient.get("queue1"); + + assertEquals(3, mapFromMQ.size()); + assertEquals("a", mapFromMQ.get("a")); + assertEquals("b", mapFromMQ.get("b")); + assertEquals("c", mapFromMQ.get("c")); + assertNull(this.memcachedClient.get("queue1")); + + List userDefinedClassList = new ArrayList(); + userDefinedClassList.add(new UserDefinedClass("a")); + userDefinedClassList.add(new UserDefinedClass("b")); + userDefinedClassList.add(new UserDefinedClass("c")); + + assertTrue(this.memcachedClient.set("queue1", 0, userDefinedClassList)); + List userDefinedClassListFromMQ = + (List) this.memcachedClient.get("queue1"); + + assertEquals(3, userDefinedClassListFromMQ.size()); + + } + + public void testBlockingFetch() throws Exception { + this.memcachedClient.setOpTimeout(60000); + long start = System.currentTimeMillis(); + // blocking read 1 second + assertNull(this.memcachedClient.get("queue1/t=1000")); + assertEquals(1000, System.currentTimeMillis() - start, 100); + + assertTrue(this.memcachedClient.set("queue1", 0, "hello world")); + assertEquals("hello world", this.memcachedClient.get("queue1/t=1000")); + + } + + public void testPeek() throws Exception { + assertNull(this.memcachedClient.get("queue1/peek")); + this.memcachedClient.set("queue1", 0, 1); + assertEquals(1, (int) this.memcachedClient.get("queue1/peek")); + this.memcachedClient.set("queue1", 0, 10); + assertEquals(1, (int) this.memcachedClient.get("queue1/peek")); + this.memcachedClient.set("queue1", 0, 11); + assertEquals(1, (int) this.memcachedClient.get("queue1/peek")); + + assertEquals(1, (int) this.memcachedClient.get("queue1")); + assertEquals(10, (int) this.memcachedClient.get("queue1/peek")); + assertEquals(10, (int) this.memcachedClient.get("queue1")); + assertEquals(11, (int) this.memcachedClient.get("queue1/peek")); + assertEquals(11, (int) this.memcachedClient.get("queue1")); + assertNull(this.memcachedClient.get("queue1/peek")); + } + + public void testDelete() throws Exception { + assertNull(this.memcachedClient.get("queue1")); + for (int i = 0; i < 10; i++) { + assertTrue(this.memcachedClient.set("queue1", 0, i)); + } + this.memcachedClient.delete("queue1"); + for (int i = 0; i < 10; i++) { + assertNull(this.memcachedClient.get("queue1")); + } + } + + public void testReliableFetch() throws Exception { + assertTrue(this.memcachedClient.set("queue1", 0, "hello world")); + assertEquals("hello world", this.memcachedClient.get("queue1/open")); + // close connection + this.memcachedClient.shutdown(); + // still can fetch it + MemcachedClient newClient = newBuilder().build(); + newClient.setOptimizeGet(false); + // begin transaction + assertEquals("hello world", newClient.get("queue1/open")); + // confirm + assertNull(newClient.get("queue1/close")); + assertNull(newClient.get("queue1")); + + // test abort,for kestrel 1.2 + assertTrue(newClient.set("queue1", 0, "hello world")); + assertEquals("hello world", newClient.get("queue1/open")); + // abort + assertNull(newClient.get("queue1/abort")); + // still alive + assertEquals("hello world", newClient.get("queue1/open")); + // confirm + assertNull(newClient.get("queue1/close")); + // null + assertNull(newClient.get("queue1")); + + newClient.shutdown(); + } + + public void testPerformance() throws Exception { + long start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + this.memcachedClient.set("queue1", 0, "hello"); + } + System.out.println("push 10000 message:" + (System.currentTimeMillis() - start) + "ms"); + + start = System.currentTimeMillis(); + for (int i = 0; i < 10000; i++) { + this.memcachedClient.get("queue1"); + } + System.out.println("fetch 10000 message:" + (System.currentTimeMillis() - start) + "ms"); + } + + class AccessThread extends Thread { + private CyclicBarrier cyclicBarrier; + + public AccessThread(CyclicBarrier cyclicBarrier) { + super(); + this.cyclicBarrier = cyclicBarrier; + } + + @Override + public void run() { + try { + this.cyclicBarrier.await(); + for (int i = 0; i < 10000; i++) { + KestrelClientIT.this.memcachedClient.set("queue1", 0, "hello"); + } + for (int i = 0; i < 10000; i++) { + KestrelClientIT.this.memcachedClient.get("queue1"); + } + this.cyclicBarrier.await(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public void ignoreTestConcurrentAccess() throws Exception { + int threadCount = 100; + CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount + 1); + for (int i = 0; i < threadCount; i++) { + new AccessThread(cyclicBarrier).start(); + } + cyclicBarrier.await(); + cyclicBarrier.await(); + + } + + public void testHearBeat() throws Exception { + Thread.sleep(30 * 1000); + this.memcachedClient.set("queue1", 0, "hello"); + assertEquals("hello", this.memcachedClient.get("queue1")); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/MockMemcachedSession.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/MockMemcachedSession.java new file mode 100644 index 0000000..f87d45e --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/MockMemcachedSession.java @@ -0,0 +1,65 @@ +package net.rubyeye.xmemcached.test.unittest; + +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; + +public class MockMemcachedSession extends MockSession implements MemcachedSession { + + public MockMemcachedSession(int port) { + super(port); + } + + public void setAllowReconnect(boolean allow) { + // TODO Auto-generated method stub + + } + + public boolean isAllowReconnect() { + // TODO Auto-generated method stub + return false; + } + + public void setBufferAllocator(BufferAllocator allocator) { + // TODO Auto-generated method stub + + } + + public InetSocketAddressWrapper getInetSocketAddressWrapper() { + InetSocketAddressWrapper inetSocketAddressWrapper = + new InetSocketAddressWrapper(getRemoteSocketAddress(), 1, 1, null); + inetSocketAddressWrapper.setRemoteAddressStr("localhost/127.0.0.1:" + this.port); + return inetSocketAddressWrapper; + } + + public void destroy() { + // TODO Auto-generated method stub + + } + + public int getWeight() { + // TODO Auto-generated method stub + return 1; + } + + public int getOrder() { + // TODO Auto-generated method stub + return 0; + } + + public void quit() { + // TODO Auto-generated method stub + + } + + public boolean isAuthFailed() { + // TODO Auto-generated method stub + return false; + } + + public void setAuthFailed(boolean authFailed) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/MockSession.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/MockSession.java new file mode 100644 index 0000000..9b7613a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/MockSession.java @@ -0,0 +1,182 @@ +package net.rubyeye.xmemcached.test.unittest; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteOrder; +import java.util.concurrent.Future; +import com.google.code.yanf4j.core.CodecFactory.Decoder; +import com.google.code.yanf4j.core.CodecFactory.Encoder; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import com.google.code.yanf4j.core.impl.FutureImpl; + +public class MockSession implements Session { + private boolean closed = false; + protected final int port; + + public MockSession(int port) { + this.port = port; + } + + public void write(Object packet) { + // TODO Auto-generated method stub + + } + + public Handler getHandler() { + // TODO Auto-generated method stub + return null; + } + + public InetAddress getLocalAddress() { + // TODO Auto-generated method stub + return null; + } + + public boolean isIdle() { + // TODO Auto-generated method stub + return false; + } + + public void clearAttributes() { + // TODO Auto-generated method stub + + } + + public Object getAttribute(String key) { + // TODO Auto-generated method stub + return null; + } + + public void removeAttribute(String key) { + // TODO Auto-generated method stub + + } + + public void setAttribute(String key, Object value) { + // TODO Auto-generated method stub + + } + + public void close() { + this.closed = true; + + } + + public void flush() { + + } + + public Decoder getDecoder() { + + return null; + } + + public Encoder getEncoder() { + + return null; + } + + public ByteOrder getReadBufferByteOrder() { + + return null; + } + + public InetSocketAddress getRemoteSocketAddress() { + return new InetSocketAddress("localhost", this.port); + } + + public boolean isClosed() { + + return this.closed; + } + + public boolean isExpired() { + + return false; + } + + public boolean isHandleReadWriteConcurrently() { + + return false; + } + + public boolean isUseBlockingRead() { + + return false; + } + + public boolean isUseBlockingWrite() { + + return false; + } + + public Future asyncWrite(Object packet) { + + return new FutureImpl(); + } + + public void setDecoder(Decoder decoder) { + + } + + public void setEncoder(Encoder encoder) { + + } + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + + } + + public void setReadBufferByteOrder(ByteOrder readBufferByteOrder) { + + } + + public void setUseBlockingRead(boolean useBlockingRead) { + + } + + public void setUseBlockingWrite(boolean useBlockingWrite) { + + } + + public void start() { + + } + + public long getScheduleWritenBytes() { + return 0; + } + + public void updateTimeStamp() {} + + public long getLastOperationTimeStamp() { + return 0; + } + + public final boolean isLoopbackConnection() { + return false; + } + + public long getSessionIdleTimeout() { + return 0; + } + + public void setSessionIdleTimeout(long sessionIdelTimeout) {} + + public long getSessionTimeout() { + return 0; + } + + public void setSessionTimeout(long sessionTimeout) {} + + public Object setAttributeIfAbsent(String key, Object value) { + return null; + } + + @Override + public String toString() { + return "localhost:" + this.port; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/StandardHashMemcachedClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/StandardHashMemcachedClientIT.java new file mode 100644 index 0000000..f915f1e --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/StandardHashMemcachedClientIT.java @@ -0,0 +1,206 @@ +package net.rubyeye.xmemcached.test.unittest; + +import net.rubyeye.xmemcached.CASOperation; +import net.rubyeye.xmemcached.GetsResponse; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.exception.MemcachedServerException; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.StringTranscoder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class StandardHashMemcachedClientIT extends XMemcachedClientIT { + + @Override + public MemcachedClientBuilder createBuilder() throws Exception { + MemcachedClientBuilder builder = new XMemcachedClientBuilder( + AddrUtil.getAddresses(this.properties.getProperty("test.memcached.servers"))); + // builder.setConnectionPoolSize(Runtime.getRuntime().availableProcessors()); + return builder; + } + + private static final String KEY_LARGE_OBJECT = "largeObject"; + + @Test + public void testLargeObject() throws Exception { + int megabyte_plus1 = 1048577; // 1024 * 1024 + 1 + SerializingTranscoder transcoder = new SerializingTranscoder(megabyte_plus1 * 2); // something + // bigger than + // memcached + // daemon + // max value size. + transcoder.setCompressionThreshold(transcoder.getMaxSize()); // bumping + // up + // compression + // threshold + // so + // that + // xmemcached + // client + // does + // not + // compress. + + for (int i = 0; i < 5; i++) { + try { + String largeObject = createString(megabyte_plus1); + + this.memcachedClient.set(KEY_LARGE_OBJECT, 60, largeObject, transcoder); + fail(); + + } catch (MemcachedServerException exception) { + assertTrue(exception.getMessage().contains("object too large for cache")); + } + } + String readLargeObject = memcachedClient.get(KEY_LARGE_OBJECT); + assertNull(readLargeObject); + } + + private static String createString(int size) { + char[] chars = new char[size]; + Arrays.fill(chars, 'f'); + return new String(chars); + } + + public void testStoreNoReply() throws Exception { + memcachedClient.replaceWithNoReply("name", 0, 1); + assertNull(memcachedClient.get("name")); + + memcachedClient.setWithNoReply("name", 1, "dennis", new StringTranscoder()); + assertEquals("dennis", memcachedClient.get("name")); + Thread.sleep(2000); + assertNull(memcachedClient.get("name")); + + memcachedClient.setWithNoReply("name", 0, "dennis", new StringTranscoder()); + memcachedClient.appendWithNoReply("name", " zhuang"); + memcachedClient.prependWithNoReply("name", "hello "); + assertEquals("hello dennis zhuang", memcachedClient.get("name")); + + memcachedClient.addWithNoReply("name", 0, "test", new StringTranscoder()); + assertEquals("hello dennis zhuang", memcachedClient.get("name")); + memcachedClient.replaceWithNoReply("name", 0, "test", new StringTranscoder()); + assertEquals("test", memcachedClient.get("name")); + + memcachedClient.setWithNoReply("a", 0, 1); + GetsResponse getsResponse = memcachedClient.gets("a"); + memcachedClient.casWithNoReply("a", 0, getsResponse, new CASOperation() { + + public int getMaxTries() { + return 100; + } + + public Integer getNewValue(long currentCAS, Integer currentValue) { + return currentValue + 1; + } + + }); + assertEquals(2, (int) memcachedClient.get("a")); + // repeat onece,it is not effected,because cas value is changed + memcachedClient.casWithNoReply("a", getsResponse, new CASOperation() { + + public int getMaxTries() { + return 1; + } + + public Integer getNewValue(long currentCAS, Integer currentValue) { + return currentValue + 1; + } + + }); + assertEquals(2, (int) memcachedClient.get("a")); + + memcachedClient.casWithNoReply("a", new CASOperation() { + + public int getMaxTries() { + return 1; + } + + public Integer getNewValue(long currentCAS, Integer currentValue) { + return currentValue + 1; + } + + }); + assertEquals(3, (int) memcachedClient.get("a")); + } + + public void testDeleteWithNoReply() throws Exception { + assertTrue(memcachedClient.set("name", 0, "dennis")); + assertEquals("dennis", memcachedClient.get("name")); + memcachedClient.deleteWithNoReply("name"); + assertNull(memcachedClient.get("name")); + memcachedClient.deleteWithNoReply("not_exists"); + + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + memcachedClient.deleteWithNoReply("name"); + assertNull(memcachedClient.get("name")); + // add,replace success + assertTrue(memcachedClient.add("name", 0, "zhuang")); + assertTrue(memcachedClient.replace("name", 0, "zhuang")); + } + + public void testFlushAllWithNoReply() throws Exception { + for (int i = 0; i < 10; i++) { + assertTrue(memcachedClient.add(String.valueOf(i), 0, i)); + } + List keys = new ArrayList(); + for (int i = 0; i < 20; i++) { + keys.add(String.valueOf(i)); + } + Map result = memcachedClient.get(keys); + assertEquals(10, result.size()); + for (int i = 0; i < 10; i++) { + assertEquals((Integer) i, result.get(String.valueOf(i))); + } + memcachedClient.flushAllWithNoReply(); + result = memcachedClient.get(keys, 5000); + assertTrue(result.isEmpty()); + } + + public void testIncrWithNoReply() throws Exception { + memcachedClient.incrWithNoReply("a", 5); + assertTrue(memcachedClient.set("a", 0, "1")); + memcachedClient.incrWithNoReply("a", 5); + assertEquals("6", memcachedClient.get("a")); + memcachedClient.incrWithNoReply("a", 4); + assertEquals("10", memcachedClient.get("a")); + } + + public void testDecrWithNoReply() throws Exception { + memcachedClient.decrWithNoReply("a", 5); + + assertTrue(memcachedClient.set("a", 0, "100")); + memcachedClient.decrWithNoReply("a", 50); + assertEquals("50", ((String) memcachedClient.get("a")).trim()); + memcachedClient.decrWithNoReply("a", 4); + assertEquals("46", ((String) memcachedClient.get("a")).trim()); + } + + @Override + public MemcachedClientBuilder createWeightedBuilder() throws Exception { + List addressList = + AddrUtil.getAddresses(this.properties.getProperty("test.memcached.servers")); + int[] weights = new int[addressList.size()]; + for (int i = 0; i < weights.length; i++) { + weights[i] = i + 1; + } + + MemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights); + builder.setSessionLocator(new KetamaMemcachedSessionLocator()); + return builder; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/XMemcachedClientIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/XMemcachedClientIT.java new file mode 100644 index 0000000..fb00457 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/XMemcachedClientIT.java @@ -0,0 +1,1321 @@ +package net.rubyeye.xmemcached.test.unittest; + +import com.google.code.yanf4j.buffer.IoBuffer; +import net.rubyeye.xmemcached.CASOperation; +import net.rubyeye.xmemcached.Counter; +import net.rubyeye.xmemcached.GetsResponse; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.MemcachedClientCallable; +import net.rubyeye.xmemcached.XMemcachedClient; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.exception.UnknownCommandException; +import net.rubyeye.xmemcached.helper.BlankKeyChecker; +import net.rubyeye.xmemcached.helper.InValidKeyChecker; +import net.rubyeye.xmemcached.helper.MockTranscoder; +import net.rubyeye.xmemcached.helper.TooLongKeyChecker; +import net.rubyeye.xmemcached.helper.TranscoderChecker; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import net.rubyeye.xmemcached.test.unittest.mock.MockDecodeTimeoutBinaryGetOneCommand; +import net.rubyeye.xmemcached.test.unittest.mock.MockDecodeTimeoutTextGetOneCommand; +import net.rubyeye.xmemcached.test.unittest.mock.MockEncodeTimeoutBinaryGetCommand; +import net.rubyeye.xmemcached.test.unittest.mock.MockEncodeTimeoutTextGetOneCommand; +import net.rubyeye.xmemcached.test.unittest.mock.MockErrorBinaryGetOneCommand; +import net.rubyeye.xmemcached.test.unittest.mock.MockErrorCommand; +import net.rubyeye.xmemcached.test.unittest.mock.MockErrorTextGetOneCommand; +import net.rubyeye.xmemcached.transcoders.IntegerTranscoder; +import net.rubyeye.xmemcached.transcoders.StringTranscoder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.utils.ByteUtils; +import net.rubyeye.xmemcached.utils.Protocol; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public abstract class XMemcachedClientIT { + protected MemcachedClient memcachedClient; + Properties properties; + private MockTranscoder mockTranscoder; + + + @BeforeEach + public void setUp() throws Exception { + createClients(); + mockTranscoder = new MockTranscoder(); + } + + @Test + public void testCreateClientWithEmptyServers() throws Exception { + MemcachedClient client = new XMemcachedClient(); + assertFalse(client.isShutdown()); + client.shutdown(); + assertTrue(client.isShutdown()); + + MemcachedClientBuilder builder = new XMemcachedClientBuilder(); + client = builder.build(); + assertFalse(client.isShutdown()); + client.shutdown(); + assertTrue(client.isShutdown()); + } + + protected void createClients() + throws IOException, Exception, TimeoutException, InterruptedException, MemcachedException { + properties = System.getProperties(); //ResourcesUtils.getResourceAsProperties("test.properties"); + + MemcachedClientBuilder builder = createBuilder(); + builder.getConfiguration().setStatisticsServer(true); + memcachedClient = builder.build(); + memcachedClient.flushAll(); + } + + public MemcachedClientBuilder createBuilder() throws Exception { + return null; + } + + public MemcachedClientBuilder createWeightedBuilder() throws Exception { + return null; + } + + @Test + public void testGet() throws Exception { + assertNull(memcachedClient.get("name")); + + memcachedClient.set("name", 1, "dennis", new StringTranscoder(), 1000); + + assertEquals("dennis", memcachedClient.get("name", new StringTranscoder())); + new TranscoderChecker(mockTranscoder, 1) { + @Override + public void call() throws Exception { + assertEquals("dennis", memcachedClient.get("name", mockTranscoder)); + } + + }.check(); + Thread.sleep(2000); + // expire + assertNull(memcachedClient.get("name")); + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.get(""); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.get((String) null); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.get("test\r\n"); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.get("test test2"); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.get(sb.toString()); + } + }.check(); + + // client is shutdown + try { + memcachedClient.shutdown(); + memcachedClient.get("name"); + fail(); + } catch (MemcachedException e) { + assertEquals("Xmemcached is stopped", e.getMessage()); + } + } + + @Test + public void testAppendPrepend() throws Exception { + // append,prepend + assertTrue(memcachedClient.set("name", 0, "dennis", new StringTranscoder(), 1000)); + assertTrue(memcachedClient.prepend("name", "hello ")); + assertEquals("hello dennis", memcachedClient.get("name")); + assertTrue(memcachedClient.append("name", " zhuang")); + assertEquals("hello dennis zhuang", memcachedClient.get("name")); + memcachedClient.delete("name"); + assertFalse(memcachedClient.prepend("name", "hello ", 2000)); + assertFalse(memcachedClient.append("name", " zhuang", 2000)); + // append test + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.append("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.append((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.append("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.append("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.append(sb.toString(), 0, 1); + } + }.check(); + + // prepend test + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.prepend("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.prepend((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.prepend("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.prepend("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.prepend(sb.toString(), 0, 1); + } + }.check(); + + } + + @Test + public void testStoreCollection() + throws TimeoutException, InterruptedException, MemcachedException { + // store list + List list = new ArrayList(); + for (int i = 0; i < 100; i++) { + list.add(String.valueOf(i)); + } + assertTrue(memcachedClient.add("list", 0, list)); + List listFromCache = memcachedClient.get("list"); + assertEquals(100, listFromCache.size()); + + for (int i = 0; i < listFromCache.size(); i++) { + assertEquals(list.get(i), listFromCache.get(i)); + } + // store map + Map map = new HashMap(); + + for (int i = 0; i < 100; i++) { + map.put(String.valueOf(i), i); + } + assertTrue(memcachedClient.add("map", 0, map)); + Map mapFromCache = memcachedClient.get("map"); + assertEquals(100, listFromCache.size()); + + for (int i = 0; i < listFromCache.size(); i++) { + assertEquals(mapFromCache.get(i), map.get(i)); + } + + } + + @Test + public void testSet() throws Exception { + assertTrue(memcachedClient.set("name", 0, "dennis")); + assertEquals("dennis", memcachedClient.get("name", 2000)); + + assertTrue(memcachedClient.set("name", 1, "zhuang", new StringTranscoder())); + assertEquals("zhuang", memcachedClient.get("name", 2000)); + Thread.sleep(2000); + // expired + assertNull(memcachedClient.get("zhuang")); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.set("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.set((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.set("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.set("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.set(sb.toString(), 0, 1); + } + }.check(); + + // Transcoder + new TranscoderChecker(mockTranscoder, 2) { + @Override + public void call() throws Exception { + memcachedClient.set("name", 0, "xmemcached", mockTranscoder); + assertEquals("xmemcached", memcachedClient.get("name", mockTranscoder)); + + } + }.check(); + } + + @Test + public void testReplace() throws Exception { + assertTrue(memcachedClient.add("name", 0, "dennis")); + assertFalse(memcachedClient.replace("unknownKey", 0, "test")); + assertTrue(memcachedClient.replace("name", 1, "zhuang")); + assertEquals("zhuang", memcachedClient.get("name", 2000)); + Thread.sleep(2000); + // expire + assertNull(memcachedClient.get("name")); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.replace("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.replace((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.replace("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.replace("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.replace(sb.toString(), 0, 1); + } + }.check(); + + // timeout + // new TimeoutChecker(0) { + // @Override + // public void call() throws Exception { + // XMemcachedClientTest.this.memcachedClient.replace("0", 0, 1, 0); + // } + // }.check(); + + // Transcoder + new TranscoderChecker(mockTranscoder, 2) { + @Override + public void call() throws Exception { + memcachedClient.set("name", 0, 1); + memcachedClient.replace("name", 0, "xmemcached", mockTranscoder); + assertEquals("xmemcached", memcachedClient.get("name", mockTranscoder)); + + } + }.check(); + } + + @Test + public void testAdd() throws Exception { + assertTrue(memcachedClient.add("name", 0, "dennis")); + assertFalse(memcachedClient.add("name", 0, "dennis")); + assertEquals("dennis", memcachedClient.get("name", 2000)); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.add("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.add((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.add("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.add("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.add(sb.toString(), 0, 1); + } + }.check(); + + // Transcoder + new TranscoderChecker(mockTranscoder, 2) { + @Override + public void call() throws Exception { + memcachedClient.add("a", 0, 100, mockTranscoder); + assertEquals(100, memcachedClient.get("a", mockTranscoder)); + + } + }.check(); + } + + @Test + public void testDelete() throws Exception { + assertTrue(memcachedClient.set("name", 0, "dennis")); + assertEquals("dennis", memcachedClient.get("name")); + assertTrue(memcachedClient.delete("name")); + assertNull(memcachedClient.get("name")); + assertFalse(memcachedClient.delete("not_exists")); + + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + assertTrue(memcachedClient.delete("name")); + assertNull(memcachedClient.get("name")); + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + assertTrue(memcachedClient.delete("name", 2000L)); + assertNull(memcachedClient.get("name")); + + // add,replace success + assertTrue(memcachedClient.add("name", 0, "zhuang")); + assertTrue(memcachedClient.replace("name", 0, "zhuang")); + } + + @Test + public void testMultiGet() throws Exception { + for (int i = 0; i < 50; i++) { + assertTrue(memcachedClient.add(String.valueOf(i), 0, i)); + } + + List keys = new ArrayList(); + for (int i = 0; i < 100; i++) { + keys.add(String.valueOf(i)); + } + + Map result = memcachedClient.get(keys, 10000); + assertEquals(50, result.size()); + + for (int i = 0; i < 50; i++) { + assertEquals((Integer) i, result.get(String.valueOf(i))); + } + + // blank collection + assertNull(memcachedClient.get((Collection) null)); + assertNull(memcachedClient.get(new HashSet())); + + } + + @Test + public void testGets() throws Exception { + memcachedClient.add("name", 0, "dennis"); + GetsResponse getsResponse = memcachedClient.gets("name"); + GetsResponse oldGetsResponse = getsResponse; + assertEquals("dennis", getsResponse.getValue()); + long oldCas = getsResponse.getCas(); + getsResponse = memcachedClient.gets("name", 2000, new StringTranscoder()); + assertEquals("dennis", getsResponse.getValue()); + // check the same + assertEquals(oldCas, getsResponse.getCas()); + assertEquals(oldGetsResponse, getsResponse); + + memcachedClient.set("name", 0, "zhuang"); + getsResponse = memcachedClient.gets("name", 2000); + assertEquals("zhuang", getsResponse.getValue()); + assertFalse(oldCas == getsResponse.getCas()); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.gets(""); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.gets((String) null); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.gets("test\r\n"); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.gets("test test2"); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.gets(sb.toString()); + } + }.check(); + + // client is shutdown + try { + memcachedClient.shutdown(); + memcachedClient.gets("name"); + fail(); + } catch (MemcachedException e) { + assertEquals("Xmemcached is stopped", e.getMessage()); + } + + } + + @Test + public void testVersion() throws Exception { + assertTrue(memcachedClient.getVersions(5000).size() > 0); + System.out.println(memcachedClient.getVersions()); + } + + @Test + public void testStats() throws Exception { + assertTrue(memcachedClient.getStats().size() > 0); + System.out.println(memcachedClient.getStats()); + memcachedClient.set("a", 0, 1); + assertTrue(memcachedClient.getStatsByItem("items").size() > 0); + System.out.println(memcachedClient.getStatsByItem("items")); + } + + @Test + public void testIssue126() throws Exception { + Map> result = + this.memcachedClient.getStatsByItem("detail dump"); + assertNotNull(result); + } + + @Test + public void testFlushAll() throws Exception { + for (int i = 0; i < 50; i++) { + assertTrue(memcachedClient.add(String.valueOf(i), 0, i)); + } + List keys = new ArrayList(); + for (int i = 0; i < 100; i++) { + keys.add(String.valueOf(i)); + } + Map result = memcachedClient.get(keys); + assertEquals(50, result.size()); + for (int i = 0; i < 50; i++) { + assertEquals((Integer) i, result.get(String.valueOf(i))); + } + memcachedClient.flushAll(); + result = memcachedClient.get(keys); + assertTrue(result.isEmpty()); + } + + @Test + public void testSetLoggingLevelVerbosity() throws Exception { + if (memcachedClient.getProtocol() == Protocol.Text + || memcachedClient.getProtocol() == Protocol.Binary) { + memcachedClient.setLoggingLevelVerbosity( + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")).get(0), 2); + memcachedClient.setLoggingLevelVerbosityWithNoReply( + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")).get(0), 3); + memcachedClient.setLoggingLevelVerbosityWithNoReply( + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")).get(0), 0); + } else { + // do nothing,binary protocol doesn't have verbosity protocol. + } + } + + @Test + public void testIssue150() throws Exception { + memcachedClient.set("a", 0, 1); + try { + memcachedClient.incr("a", 1); + fail(); + } catch (MemcachedException e) { + // assertEquals("cannot increment or decrement non-numeric + // value",e.getMessage()); + } + memcachedClient.set("a", 0, "1"); + assertEquals(3, memcachedClient.incr("a", 2)); + } + + @Test + public void testIncr() throws Exception { + assertEquals(0, memcachedClient.incr("a", 5)); + assertTrue(memcachedClient.set("a", 0, "1")); + assertEquals(6, memcachedClient.incr("a", 5)); + assertEquals(10, memcachedClient.incr("a", 4)); + + // test incr with initValue + memcachedClient.delete("a"); + assertEquals(1, memcachedClient.incr("a", 5, 1)); + assertEquals(6, memcachedClient.incr("a", 5)); + assertEquals(10, memcachedClient.incr("a", 4)); + + // test incr with initValue and expire time + memcachedClient.delete("a"); + assertEquals(1, memcachedClient.incr("a", 5, 1, 1000, 1)); + Thread.sleep(2000); + assertNull(memcachedClient.get("a")); + + // key is chinese + assertEquals(1, memcachedClient.incr("娴��", 5, 1, 1000, 0)); + assertEquals(6, memcachedClient.incr("娴��", 5)); + assertEquals(10, memcachedClient.incr("娴��", 4)); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.incr("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.incr((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.incr("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.incr("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.incr(sb.toString(), 0, 1); + } + }.check(); + + } + + @Test + public void testDecr() throws Exception { + assertEquals(0, memcachedClient.decr("a", 5)); + + assertTrue(memcachedClient.set("a", 0, "100")); + assertEquals(50, memcachedClient.decr("a", 50)); + assertEquals(46, memcachedClient.decr("a", 4)); + + // test decr with initValue + memcachedClient.delete("a"); + assertEquals(100, memcachedClient.decr("a", 5, 100)); + assertEquals(50, memcachedClient.decr("a", 50)); + assertEquals(46, memcachedClient.decr("a", 4)); + + // test decr with initValue and expire time + memcachedClient.delete("a"); + assertEquals(1, memcachedClient.decr("a", 5, 1, 1000, 1)); + Thread.sleep(2000); + assertNull(memcachedClient.get("a")); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.decr("", 0, 1); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.decr((String) null, 0, 1); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.decr("test\r\n", 0, 1); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.decr("test test2", 0, 1); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.decr(sb.toString(), 0, 1); + } + }.check(); + + } + + @Test + public void testCAS() throws Exception { + memcachedClient.add("name", 0, "dennis"); + GetsResponse getsResponse = memcachedClient.gets("name"); + assertEquals("dennis", getsResponse.getValue()); + final CASOperation operation = new CASOperation() { + + public int getMaxTries() { + return 1; + } + + public String getNewValue(long currentCAS, String currentValue) { + return "zhuang"; + } + + }; + assertTrue(memcachedClient.cas("name", getsResponse, operation)); + assertEquals("zhuang", memcachedClient.get("name")); + getsResponse = memcachedClient.gets("name"); + memcachedClient.set("name", 0, "dennis"); + // cas fail + assertFalse(memcachedClient.cas("name", 0, "zhuang", getsResponse.getCas())); + assertEquals("dennis", memcachedClient.get("name")); + + // blank key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.cas("", operation); + } + }.check(); + // null key + new BlankKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.cas((String) null, operation); + } + }.check(); + + // invalid key + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.cas("test\r\n", operation); + } + }.check(); + new InValidKeyChecker() { + @Override + public void call() throws Exception { + memcachedClient.cas("test test2", operation); + } + }.check(); + + // key is too long + new TooLongKeyChecker(memcachedClient) { + @Override + public void call() throws Exception { + int keyLength = memcachedClient.getProtocol() == Protocol.Text ? 256 : 65536; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < keyLength; i++) { + sb.append(i); + } + memcachedClient.cas(sb.toString(), operation); + } + }.check(); + + } + + @Test + public void testAutoReconnect() throws Exception { + final String key = "name"; + memcachedClient.set(key, 0, "dennis"); + assertEquals("dennis", memcachedClient.get(key)); + CountDownLatch latch = new CountDownLatch(1); + int currentServerCount = memcachedClient.getAvaliableServers().size(); + MockErrorCommand errorCommand = null; + if (memcachedClient.getProtocol() == Protocol.Text) { + errorCommand = + new MockErrorTextGetOneCommand(key, key.getBytes(), CommandType.GET_ONE, latch); + } else { + errorCommand = new MockErrorBinaryGetOneCommand(key, key.getBytes(), CommandType.GET_ONE, + latch, OpCode.GET, false); + } + memcachedClient.getConnector().send((Command) errorCommand); + latch.await(MemcachedClient.DEFAULT_OP_TIMEOUT, TimeUnit.MILLISECONDS); + assertTrue(errorCommand.isDecoded()); + // wait for reconnecting + Thread.sleep(2000 * 3); + assertEquals(currentServerCount, memcachedClient.getAvaliableServers().size()); + // It works + assertEquals("dennis", memcachedClient.get(key)); + } + + @Test + public void testOperationDecodeTimeOut() throws Exception { + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + CountDownLatch latch = new CountDownLatch(1); + Command errorCommand = null; + if (memcachedClient.getProtocol() == Protocol.Text) { + errorCommand = new MockDecodeTimeoutTextGetOneCommand("name", "name".getBytes(), + CommandType.GET_ONE, latch, 1000); + } else { + errorCommand = new MockDecodeTimeoutBinaryGetOneCommand("name", "name".getBytes(), + CommandType.GET_ONE, latch, OpCode.GET, false, 1000); + } + memcachedClient.getConnector().send(errorCommand); + // wait 100 milliseconds,the operation will be timeout + latch.await(100, TimeUnit.MILLISECONDS); + assertNull(errorCommand.getResult()); + Thread.sleep(1000); + // It works. + assertNotNull(errorCommand.getResult()); + assertEquals("dennis", memcachedClient.get("name")); + } + + @Test + public void _testOperationEncodeTimeout() throws Exception { + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + long writeMessageCount = memcachedClient.getConnector().getStatistics().getWriteMessageCount(); + CountDownLatch latch = new CountDownLatch(1); + Command errorCommand = null; + if (memcachedClient.getProtocol() == Protocol.Text) { + errorCommand = new MockEncodeTimeoutTextGetOneCommand("name", "name".getBytes(), + CommandType.GET_ONE, latch, 1000); + } else { + errorCommand = new MockEncodeTimeoutBinaryGetCommand("name", "name".getBytes(), + CommandType.GET_ONE, latch, OpCode.GET, false, 1000); + } + + memcachedClient.getConnector().send(errorCommand); + // Force write thread to encode command + errorCommand.setIoBuffer(null); + // wait 100 milliseconds,the operation will be timeout + if (!latch.await(100, TimeUnit.MILLISECONDS)) { + errorCommand.cancel(); + } + Thread.sleep(1000); + // It is not written to channel,because it is canceled. + assertEquals(writeMessageCount, + memcachedClient.getConnector().getStatistics().getWriteMessageCount()); + // It works + assertEquals("dennis", memcachedClient.get("name")); + } + + @Test + public void testRemoveAndAddServer() throws Exception { + String servers = properties.getProperty("test.memcached.servers"); + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + memcachedClient.removeServer(servers); + + synchronized (this) { + while (memcachedClient.getAvaliableServers().size() > 0) { + wait(1000); + } + } + assertEquals(0, memcachedClient.getAvaliableServers().size()); + try { + memcachedClient.get("name"); + fail(); + } catch (MemcachedException e) { + assertEquals("There is no available connection at this moment", e.getMessage()); + } + + memcachedClient.addServer(properties.getProperty("test.memcached.servers")); + synchronized (this) { + while (memcachedClient.getAvaliableServers().size() < AddrUtil.getAddresses(servers).size()) { + wait(1000); + } + } + Thread.sleep(5000); + assertEquals("dennis", memcachedClient.get("name")); + } + + @Test + public void testWeightedServers() throws Exception { + // shutdown current client + memcachedClient.shutdown(); + + MemcachedClientBuilder builder = createWeightedBuilder(); + builder.getConfiguration().setStatisticsServer(true); + memcachedClient = builder.build(); + memcachedClient.flushAll(5000); + + Map> oldStats = memcachedClient.getStats(); + + for (int i = 0; i < 100; i++) { + assertTrue(memcachedClient.set(String.valueOf(i), 0, i)); + } + for (int i = 0; i < 100; i++) { + assertEquals(i, (int) memcachedClient.get(String.valueOf(i))); + } + + List addressList = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + Map> newStats = memcachedClient.getStats(); + for (InetSocketAddress address : addressList) { + int oldSets = Integer.parseInt(oldStats.get(address).get("cmd_set")); + int newSets = Integer.parseInt(newStats.get(address).get("cmd_set")); + System.out.println("sets:" + (newSets - oldSets)); + int oldGets = Integer.parseInt(oldStats.get(address).get("cmd_get")); + int newGets = Integer.parseInt(newStats.get(address).get("cmd_get")); + System.out.println("gets:" + (newGets - oldGets)); + } + } + + @Test + public void _testErrorCommand() throws Exception { + Command nonexisCmd = new Command() { + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + return decodeError(ByteUtils.nextLine(buffer)); + } + + @Override + public void encode() { + ioBuffer = IoBuffer.wrap(ByteBuffer.wrap("test\r\n".getBytes())); + } + + }; + nonexisCmd.setKey("test"); + nonexisCmd.setLatch(new CountDownLatch(1)); + memcachedClient.getConnector().send(nonexisCmd); + // this.memcachedClient.flushAll(); + nonexisCmd.getLatch().await(); + + assertNotNull(nonexisCmd.getException()); + assertEquals("Nonexist command,check your memcached version please.", + nonexisCmd.getException().getMessage()); + assertTrue(nonexisCmd.getException() instanceof UnknownCommandException); + + memcachedClient.set("name", 0, "dennis"); + assertEquals("dennis", memcachedClient.get("name")); + } + + @Test + public void testGetAvaliableServers() { + Collection servers = memcachedClient.getAvaliableServers(); + + List serverList = + AddrUtil.getAddresses(properties.getProperty("test.memcached.servers")); + assertEquals(servers.size(), serverList.size()); + for (InetSocketAddress address : servers) { + assertTrue(serverList.contains(address)); + } + } + + @Test + public void testSanitizeKey() throws Exception { + memcachedClient.setSanitizeKeys(true); + + String key = "The string 眉@foo-bar"; + assertTrue(memcachedClient.add(key, 0, 0)); + assertEquals(0, (int) memcachedClient.get(key)); + + assertTrue(memcachedClient.replace(key, 0, 1)); + assertEquals(1, (int) memcachedClient.get(key, 2000)); + + assertTrue(memcachedClient.set(key, 0, 2)); + assertEquals((Integer) 2, memcachedClient.get(key, 2000, new IntegerTranscoder())); + + assertTrue(memcachedClient.set(key, 0, "xmemcached", new StringTranscoder())); + assertTrue(memcachedClient.append(key, " great")); + assertTrue(memcachedClient.prepend(key, "hello ")); + + assertEquals("hello xmemcached great", memcachedClient.get(key)); + + // test bulk get + List keys = new ArrayList(); + for (int i = 0; i < 100; i++) { + memcachedClient.add(key + i, 0, i); + keys.add(key + i); + } + + Map result = memcachedClient.get(keys, 5000); + for (int i = 0; i < 100; i++) { + assertEquals((Integer) i, result.get(key + i)); + } + + for (int i = 0; i < 100; i++) { + assertTrue(memcachedClient.delete(key + i)); + assertNull(memcachedClient.get(key + i)); + } + + // test cas + memcachedClient.set(key, 0, 1); + memcachedClient.cas(key, new CASOperation() { + + public int getMaxTries() { + return 1; + } + + public Integer getNewValue(long currentCAS, Integer currentValue) { + return currentValue + 1; + } + + }); + assertEquals((Integer) 2, memcachedClient.get(key, 2000, new IntegerTranscoder())); + + } + + @AfterEach + public void tearDown() throws Exception { + memcachedClient.shutdown(); + } + + @Test + public void testCounter() throws Exception { + Counter counter = memcachedClient.getCounter("a"); + assertEquals(0, counter.get()); + assertEquals(0, counter.get()); + + assertEquals(1, counter.incrementAndGet()); + assertEquals(2, counter.incrementAndGet()); + assertEquals(3, counter.incrementAndGet()); + + assertEquals(2, counter.decrementAndGet()); + assertEquals(1, counter.decrementAndGet()); + assertEquals(0, counter.decrementAndGet()); + assertEquals(0, counter.decrementAndGet()); + + assertEquals(4, counter.addAndGet(4)); + assertEquals(7, counter.addAndGet(3)); + assertEquals(0, counter.addAndGet(-7)); + + counter.set(1000); + assertEquals(1000, counter.get()); + assertEquals(1001, counter.incrementAndGet()); + + counter = memcachedClient.getCounter("b", 100); + assertEquals(101, counter.incrementAndGet()); + assertEquals(102, counter.incrementAndGet()); + assertEquals(101, counter.decrementAndGet()); + + // test issue 74 + counter = memcachedClient.getCounter("issue74", 0); + for (int i = 0; i < 100; i++) { + assertEquals(i + 1, counter.incrementAndGet()); + } + for (int i = 0; i < 100; i++) { + counter.decrementAndGet(); + } + assertEquals(0, counter.get()); + } + + @Test + public void testIssue142() throws Exception { + Counter counter = this.memcachedClient.getCounter("counter", 6); + counter.get(); // + counter.incrementAndGet(); // counter=7 + counter.decrementAndGet(); // counter=6 + counter.addAndGet(2);// counter=8 + assertEquals(8, counter.get()); // counter=8 + assertTrue(this.memcachedClient.delete("counter")); + } + + // public void testKeyIterator() throws Exception { + // if (memcachedClient.getProtocol() == Protocol.Text) { + // Collection avaliableServers = memcachedClient + // .getAvaliableServers(); + // InetSocketAddress address = avaliableServers.iterator().next(); + // KeyIterator it = memcachedClient.getKeyIterator(address); + // while (it.hasNext()) { + // memcachedClient.delete(it.next()); + // } + // it = memcachedClient.getKeyIterator(address); + // assertFalse(it.hasNext()); + // try { + // it.next(); + // fail(); + // } catch (NoSuchElementException e) { + // assertTrue(true); + // } + // for (int i = 0; i < 10; i++) { + // memcachedClient.set(String.valueOf(i), 0, i); + // } + // it = memcachedClient.getKeyIterator(address); + // assertTrue(it.hasNext()); + // assertEquals(address, it.getServerAddress()); + // while (it.hasNext()) { + // String key = it.next(); + // assertEquals(Integer.parseInt(key), + // memcachedClient.get(key)); + // } + // assertFalse(it.hasNext()); + // } else { + // // ignore + // } + // + // } + + @Test + public void testNamespace() throws Exception { + String ns = "user-id"; + this.memcachedClient.withNamespace(ns, new MemcachedClientCallable() { + + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + assertNull(client.get("a")); + assertNull(client.get("b")); + assertNull(client.get("c")); + return null; + } + }); + + this.memcachedClient.withNamespace(ns, new MemcachedClientCallable() { + + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + assertTrue(client.set("a", 0, 1)); + assertTrue(client.set("b", 0, 2)); + assertTrue(client.set("c", 0, 3)); + return null; + } + }); + + this.memcachedClient.withNamespace(ns, new MemcachedClientCallable() { + + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + assertEquals(1, (int) client.get("a")); + assertEquals(2, (int) client.get("b")); + assertEquals(3, (int) client.get("c")); + return null; + } + }); + + this.memcachedClient.invalidateNamespace(ns); + this.memcachedClient.withNamespace(ns, new MemcachedClientCallable() { + + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + assertNull(client.get("a")); + assertNull(client.get("b")); + assertNull(client.get("c")); + return null; + } + }); + + } + + @Test + public void testNameSpaceWithSantiKeys() throws Exception { + this.memcachedClient.setSanitizeKeys(true); + this.memcachedClient.withNamespace("hello", new MemcachedClientCallable() { + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + client.set("a", 0, 1); + assertEquals(1, (int) client.get("a")); + return null; + } + }); + this.memcachedClient.invalidateNamespace("hello"); + this.memcachedClient.withNamespace("hello", new MemcachedClientCallable() { + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + assertNull(client.get("a")); + return null; + } + }); + } + + @Test + public void testNamespaceWithGetMulti() throws Exception { + String ns = "user"; + this.memcachedClient.withNamespace(ns, new MemcachedClientCallable() { + + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + client.set("a", 0, 1); + client.set("b", 0, 2); + client.set("c", 0, 3); + Map values = client.get(Arrays.asList("a", "b", "c")); + assertEquals(3, values.size()); + assertEquals(1, values.get("a")); + assertEquals(2, values.get("b")); + assertEquals(3, values.get("c")); + return null; + } + }); + } + + @Test + public void testTouch() throws Exception { + this.memcachedClient.set("x", 1, 0); + assertEquals(0, (int) this.memcachedClient.get("x")); + assertTrue(this.memcachedClient.touch("x", 1)); + assertEquals(0, (int) this.memcachedClient.get("x")); + assertTrue(this.memcachedClient.touch("x", 1)); + Thread.sleep(1100); + assertNull(this.memcachedClient.get("x")); + if (memcachedClient.getProtocol() == Protocol.Binary) { + this.memcachedClient.set("x", 1, 0); + assertEquals(0, (int) this.memcachedClient.getAndTouch("x", 1)); + } + + // touch not exists + assertFalse(memcachedClient.touch("not_exists", 0)); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/XMemcachedClientWithKeyProviderIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/XMemcachedClientWithKeyProviderIT.java new file mode 100644 index 0000000..52986ce --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/XMemcachedClientWithKeyProviderIT.java @@ -0,0 +1,88 @@ +package net.rubyeye.xmemcached.test.unittest; + +import net.rubyeye.xmemcached.KeyProvider; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.MemcachedClientCallable; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.exception.MemcachedException; +import net.rubyeye.xmemcached.utils.AddrUtil; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class XMemcachedClientWithKeyProviderIT extends XMemcachedClientIT { + + private KeyProvider keyProvider; + + @Override + public void setUp() throws Exception { + super.setUp(); + keyProvider = new KeyProvider() { + + public String process(String key) { + return "prefix-" + key; + } + }; + } + + @Override + public MemcachedClientBuilder createBuilder() throws Exception { + + MemcachedClientBuilder builder = new XMemcachedClientBuilder( + AddrUtil.getAddresses(this.properties.getProperty("test.memcached.servers"))); + ByteUtils.testing = true; + return builder; + } + + @Override + public MemcachedClientBuilder createWeightedBuilder() throws Exception { + List addressList = + AddrUtil.getAddresses(this.properties.getProperty("test.memcached.servers")); + int[] weights = new int[addressList.size()]; + for (int i = 0; i < weights.length; i++) { + weights[i] = i + 1; + } + + MemcachedClientBuilder builder = new XMemcachedClientBuilder(addressList, weights); + ByteUtils.testing = true; + return builder; + } + + @Test + public void testKeyProvider() { + String process = keyProvider.process("namespace:a"); + assertEquals("prefix-namespace:a", process); + } + + @Test + public void testWithNamespaceAndKeyProvider() throws Exception { + memcachedClient.setKeyProvider(keyProvider); + memcachedClient.withNamespace("a", new MemcachedClientCallable() { + + public Void call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + client.set("name", 0, "Mike Liu"); + return null; + } + }); + + memcachedClient.invalidateNamespace("a"); + + Object result = memcachedClient.withNamespace("a", new MemcachedClientCallable() { + public Object call(MemcachedClient client) + throws MemcachedException, InterruptedException, TimeoutException { + return memcachedClient.get("name"); + } + }); + + assertNull(result); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/AbstractBufferAllocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/AbstractBufferAllocatorUnitTest.java new file mode 100644 index 0000000..e886868 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/AbstractBufferAllocatorUnitTest.java @@ -0,0 +1,102 @@ +package net.rubyeye.xmemcached.test.unittest.buffer; + +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.IoBuffer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.BufferOverflowException; +import java.nio.ByteOrder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public abstract class AbstractBufferAllocatorUnitTest { + + protected BufferAllocator allocator; + + @BeforeEach + protected void setUp() throws Exception { + createBufferAllocator(); + } + + public abstract void createBufferAllocator(); + + @Test + public void testEmptyBuffer() { + IoBuffer emptyBuffer = this.allocator.allocate(0); + + assertNotNull(emptyBuffer); + assertEquals(0, emptyBuffer.capacity()); + assertEquals(0, emptyBuffer.position()); + assertEquals(0, emptyBuffer.limit()); + + try { + emptyBuffer.put((byte) 0); + fail(); + } catch (BufferOverflowException e) { + assertTrue(true); + } + + assertSame(emptyBuffer, this.allocator.allocate(0)); + } + + @Test + public void testAllocate() { + IoBuffer buffer = this.allocator.allocate(64); + assertNotNull(buffer); + assertEquals(ByteOrder.BIG_ENDIAN, buffer.order()); + assertNotNull(buffer.getByteBuffer()); + + assertEquals(64, buffer.capacity()); + assertEquals(0, buffer.position()); + assertEquals(64, buffer.limit()); + + buffer.put("test".getBytes()); + assertEquals(64, buffer.capacity()); + assertEquals(4, buffer.position()); + assertEquals(64, buffer.limit()); + assertEquals(60, buffer.remaining()); + buffer.mark(); + + buffer.position(32); + assertEquals(32, buffer.position()); + buffer.reset(); + assertEquals(4, buffer.position()); + + assertTrue(buffer.hasRemaining()); + buffer.position(64); + assertFalse(buffer.hasRemaining()); + + buffer.order(ByteOrder.LITTLE_ENDIAN); + assertEquals(ByteOrder.LITTLE_ENDIAN, buffer.order()); + buffer.order(ByteOrder.BIG_ENDIAN); + + buffer.position(4); + buffer.flip(); + assertEquals(0, buffer.position()); + assertEquals(4, buffer.limit()); + assertEquals(4, buffer.remaining()); + + buffer.clear(); + assertEquals(64, buffer.capacity()); + assertEquals(0, buffer.position()); + assertEquals(64, buffer.limit()); + + buffer.free(); + assertNull(buffer.getByteBuffer()); + } + + @AfterEach + protected void tearDown() throws Exception { + this.allocator.dispose(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/BufferAllocatorTestSuite.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/BufferAllocatorTestSuite.java new file mode 100644 index 0000000..00ab033 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/BufferAllocatorTestSuite.java @@ -0,0 +1,15 @@ +package net.rubyeye.xmemcached.test.unittest.buffer; + + +public class BufferAllocatorTestSuite { + +// public static Test suite() { +// TestSuite suite = new TestSuite("Test for net.rubyeye.xmemcached.test.unittest.buffer"); +// // $JUnit-BEGIN$ +// suite.addTestSuite(SimpleBufferAllocatorUnitTest.class); +// suite.addTestSuite(CachedBufferAllocatorUnitTest.class); +// // $JUnit-END$ +// return suite; +// } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/CachedBufferAllocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/CachedBufferAllocatorUnitTest.java new file mode 100644 index 0000000..0b99c7a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/CachedBufferAllocatorUnitTest.java @@ -0,0 +1,10 @@ +package net.rubyeye.xmemcached.test.unittest.buffer; + +import net.rubyeye.xmemcached.buffer.CachedBufferAllocator; + +public class CachedBufferAllocatorUnitTest extends AbstractBufferAllocatorUnitTest { + public void createBufferAllocator() { + this.allocator = CachedBufferAllocator.newInstance(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/SimpleBufferAllocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/SimpleBufferAllocatorUnitTest.java new file mode 100644 index 0000000..5a17544 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/buffer/SimpleBufferAllocatorUnitTest.java @@ -0,0 +1,13 @@ +package net.rubyeye.xmemcached.test.unittest.buffer; + +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; + +public class SimpleBufferAllocatorUnitTest extends AbstractBufferAllocatorUnitTest { + + @Override + public void createBufferAllocator() { + this.allocator = SimpleBufferAllocator.newInstance(); + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/codec/MemcachedDecoderUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/codec/MemcachedDecoderUnitTest.java new file mode 100644 index 0000000..d56cebe --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/codec/MemcachedDecoderUnitTest.java @@ -0,0 +1,42 @@ +package net.rubyeye.xmemcached.test.unittest.codec; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory.Decoder; +import com.google.code.yanf4j.core.impl.ByteBufferCodecFactory; +import com.google.code.yanf4j.core.impl.HandlerAdapter; +import com.google.code.yanf4j.nio.NioSessionConfig; +import net.rubyeye.xmemcached.codec.MemcachedCodecFactory; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + + +public class MemcachedDecoderUnitTest { + private Decoder decoder; + + @Test + public void testDecode() { + decoder = new MemcachedCodecFactory().getDecoder(); + MemcachedTCPSession session = buildSession(); + Command versionCommand = + new TextCommandFactory().createVersionCommand(new CountDownLatch(1), null); + session.addCommand(versionCommand); + Command decodedCommand = (Command) decoder + .decode(IoBuffer.wrap(ByteBuffer.wrap("VERSION 1.28\r\n".getBytes())), session); + assertSame(decodedCommand, versionCommand); + assertEquals("1.28", decodedCommand.getResult()); + } + + public MemcachedTCPSession buildSession() { + NioSessionConfig sessionConfig = new NioSessionConfig(null, new HandlerAdapter(), null, + new ByteBufferCodecFactory(), null, null, null, true, 0, 0); + return new MemcachedTCPSession(sessionConfig, 16 * 1024, null, 0, new TextCommandFactory()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/codec/MemcachedEncoderUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/codec/MemcachedEncoderUnitTest.java new file mode 100644 index 0000000..4628386 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/codec/MemcachedEncoderUnitTest.java @@ -0,0 +1,27 @@ +package net.rubyeye.xmemcached.test.unittest.codec; + +import com.google.code.yanf4j.buffer.IoBuffer; +import com.google.code.yanf4j.core.CodecFactory.Encoder; +import net.rubyeye.xmemcached.codec.MemcachedCodecFactory; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MemcachedEncoderUnitTest { + private Encoder encoder; + + @Test + public void testEncode() { + this.encoder = new MemcachedCodecFactory().getEncoder(); + Command command = new TextCommandFactory().createVersionCommand(new CountDownLatch(1), null); + command.encode(); + IoBuffer buffer = this.encoder.encode(command, null); + assertEquals(buffer.buf(), ServerAddressAware.VERSION); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BaseBinaryCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BaseBinaryCommandUnitTest.java new file mode 100644 index 0000000..38013fd --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BaseBinaryCommandUnitTest.java @@ -0,0 +1,52 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import java.nio.ByteBuffer; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.transcoders.TranscoderUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; + +@Disabled +public class BaseBinaryCommandUnitTest { + protected CommandFactory commandFactory; + protected Transcoder transcoder; + TranscoderUtils transcoderUtils = new TranscoderUtils(false); + protected BufferAllocator bufferAllocator = new SimpleBufferAllocator(); + + @BeforeEach + protected void setUp() throws Exception { + commandFactory = new BinaryCommandFactory(); + this.transcoder = new SerializingTranscoder(); + } + + public ByteBuffer constructResponse(byte opCode, short keyLength, byte extraLength, byte dataType, + short status, int totalBodyLength, int opaque, long cas, byte[] extras, byte[] keyBytes, + byte[] valueBytes) { + + ByteBuffer result = ByteBuffer.allocate(24 + totalBodyLength); + result.put((byte) 0x81); + result.put(opCode); + result.putShort(keyLength); + result.put(extraLength); + result.put(dataType); + result.putShort(status); + result.putInt(totalBodyLength); + result.putInt(opaque); + result.putLong(cas); + if (extras != null) + result.put(extras); + if (keyBytes != null) + result.put(keyBytes); + if (valueBytes != null) + result.put(valueBytes); + result.flip(); + return result; + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryAppendPrependCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryAppendPrependCommandUnitTest.java new file mode 100644 index 0000000..e14706d --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryAppendPrependCommandUnitTest.java @@ -0,0 +1,78 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryAppendPrependCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + String value = "!"; + boolean noreply = false; + + @Test + public void testAppendEncodeAndDecode() { + + Command command = this.commandFactory.createAppendCommand(this.key, this.keyBytes, this.value, + this.noreply, this.transcoder); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(30, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.APPEND.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.APPEND.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.APPEND.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = this.commandFactory.createAppendCommand(this.key, this.keyBytes, this.value, + this.noreply, this.transcoder); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } + + @Test + public void testPrependEncodeAndDecode() { + + Command command = this.commandFactory.createPrependCommand(this.key, this.keyBytes, this.value, + this.noreply, this.transcoder); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(30, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.PREPEND.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.PREPEND.fieldValue(), (short) 0, (byte) 0, + (byte) 0, (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.PREPEND.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = this.commandFactory.createPrependCommand(this.key, this.keyBytes, this.value, + this.noreply, this.transcoder); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryCASCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryCASCommandUnitTest.java new file mode 100644 index 0000000..9debd30 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryCASCommandUnitTest.java @@ -0,0 +1,51 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryCASCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + String value = "world"; + boolean noreply = false; + + @Test + public void testAddEncodeAndDecode() { + + Command command = this.commandFactory.createCASCommand(this.key, this.keyBytes, 0, this.value, + 9L, this.noreply, this.transcoder); + + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(42, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + // cas use set command + assertEquals(OpCode.SET.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.SET.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0, 0, 10L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.SET.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 0L, null, null, null); + command = this.commandFactory.createCASCommand(this.key, this.keyBytes, 0, this.value, 9L, + this.noreply, this.transcoder); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryCommandAllTests.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryCommandAllTests.java new file mode 100644 index 0000000..bf4a5b6 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryCommandAllTests.java @@ -0,0 +1,24 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +//import junit.framework.Test; +//import junit.framework.TestSuite; + +public class BinaryCommandAllTests { + +// public static Test suite() { +// TestSuite suite = +// new TestSuite("Test for net.rubyeye.xmemcached.test.unittest.commands.binary"); +// // $JUnit-BEGIN$ +// suite.addTestSuite(BinaryGetCommandUnitTest.class); +// suite.addTestSuite(BinaryDeleteCommandUnitTest.class); +// suite.addTestSuite(BinaryStoreCommandUnitTest.class); +// suite.addTestSuite(BinaryIncrDecrUnitTest.class); +// suite.addTestSuite(BinaryAppendPrependCommandUnitTest.class); +// suite.addTestSuite(BinaryStatsCommandUnitTest.class); +// suite.addTestSuite(BinaryGetMultiCommandUnitTest.class); +// suite.addTestSuite(BinaryCASCommandUnitTest.class); +// // $JUnit-END$ +// return suite; +// } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryDeleteCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryDeleteCommandUnitTest.java new file mode 100644 index 0000000..0127852 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryDeleteCommandUnitTest.java @@ -0,0 +1,49 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryDeleteCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + boolean noreply = false; + + @Test + public void testDeleteEncodeAndDecode() { + + Command command = + this.commandFactory.createDeleteCommand(this.key, this.keyBytes, 0, 999L, this.noreply); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(29, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.DELETE.fieldValue(), opCode); + + ByteBuffer buffer = this.constructResponse(OpCode.DELETE.fieldValue(), (short) 0, (byte) 0, + (byte) 0, (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = this.constructResponse(OpCode.DELETE.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = + this.commandFactory.createDeleteCommand(this.key, this.keyBytes, 0, 999L, this.noreply); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetAndTouchCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetAndTouchCommandUnitTest.java new file mode 100644 index 0000000..3671cc0 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetAndTouchCommandUnitTest.java @@ -0,0 +1,51 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryGetAndTouchCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + boolean noreply = false; + + @Test + public void testGetEncodeAndDecode() { + + Command command = + this.commandFactory.createGetAndTouchCommand(this.key, this.keyBytes, null, 10, false); + + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(24 + 4 + 5, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.GAT.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.GAT.fieldValue(), (short) 0, (byte) 0x04, (byte) 0, + (short) 0, 0x00000009, 0, 1L, this.transcoderUtils.encodeInt(0), null, "world".getBytes()); + + assertEquals(33, buffer.capacity()); + assertTrue(command.decode(null, buffer)); + assertEquals("world", this.transcoder.decode((CachedData) command.getResult())); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.GAT.fieldValue(), (short) 0, (byte) 0x04, (byte) 0, (short) 0, + 0x00000004, 0, 1L, this.transcoderUtils.encodeInt(0), null, null); + assertEquals(28, buffer.capacity()); + command = + this.commandFactory.createGetAndTouchCommand(this.key, this.keyBytes, null, 10, false); + assertTrue(command.decode(null, buffer)); + assertEquals(0, ((CachedData) command.getResult()).getData().length); + assertEquals(0, buffer.remaining()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetCommandUnitTest.java new file mode 100644 index 0000000..c29c540 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetCommandUnitTest.java @@ -0,0 +1,52 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryGetCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + boolean noreply = false; + + @Test + public void testGetEncodeAndDecode() { + + Command command = this.commandFactory.createGetCommand(this.key, this.keyBytes, + CommandType.GET_ONE, this.transcoder); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(29, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.GET.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.GET.fieldValue(), (short) 0, (byte) 0x04, (byte) 0, + (short) 0, 0x00000009, 0, 1L, this.transcoderUtils.encodeInt(0), null, "world".getBytes()); + + assertEquals(33, buffer.capacity()); + assertTrue(command.decode(null, buffer)); + assertEquals("world", this.transcoder.decode((CachedData) command.getResult())); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.GET.fieldValue(), (short) 0, (byte) 0x04, (byte) 0, (short) 0, + 0x00000004, 0, 1L, this.transcoderUtils.encodeInt(0), null, null); + assertEquals(28, buffer.capacity()); + command = this.commandFactory.createGetCommand(this.key, this.keyBytes, CommandType.GET_ONE, + this.transcoder); + assertTrue(command.decode(null, buffer)); + assertEquals(0, ((CachedData) command.getResult()).getData().length); + assertEquals(0, buffer.remaining()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetMultiCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetMultiCommandUnitTest.java new file mode 100644 index 0000000..b02c11d --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryGetMultiCommandUnitTest.java @@ -0,0 +1,98 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryGetMultiCommandUnitTest extends BaseBinaryCommandUnitTest { + + @Test + public void testEncodeDecode() { + List keys = new ArrayList(); + for (int i = 0; i < 10; i++) { + keys.add(String.valueOf(i)); + } + + Command command = this.commandFactory.createGetMultiCommand(keys, new CountDownLatch(1), + CommandType.GET_MANY, this.transcoder); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + // (header length + key length) x Count + assertEquals((24 + 1) * keys.size(), encodeBuffer.capacity()); + + byte firstOpCode = encodeBuffer.get(1); + assertEquals(OpCode.GET_KEY_QUIETLY.fieldValue(), firstOpCode); + byte lastOpCode = encodeBuffer.get(25 * (keys.size() - 1) + 1); + assertEquals(OpCode.GET_KEY.fieldValue(), lastOpCode); + + // decode + ByteBuffer[] buffers = new ByteBuffer[10]; + int capacity = 0; + // First 9 buffer is getkq + for (int i = 0; i < 9; i++) { + int flag = 0; + byte[] flagBytes = this.transcoderUtils.encodeInt(flag); + String key = String.valueOf(i); + byte[] keyBytes = ByteUtils.getBytes(key); + if (i % 2 == 0) { + // value==key + buffers[i] = constructResponse(OpCode.GET_KEY_QUIETLY.fieldValue(), (short) 1, (byte) 0x04, + (byte) 0, (short) 0, 6, 0, 1L, flagBytes, keyBytes, keyBytes); + } else { + // key not found + buffers[i] = constructResponse(OpCode.GET_KEY_QUIETLY.fieldValue(), (short) 1, (byte) 0x04, + (byte) 0, (short) 0x0001, 5, 0, 1L, flagBytes, keyBytes, null); + } + capacity += buffers[i].capacity(); + } + // last buffer is getk + int flag = 0; + byte[] flagBytes = this.transcoderUtils.encodeInt(flag); + String key = String.valueOf(9); + byte[] keyBytes = ByteUtils.getBytes(key); + buffers[9] = constructResponse(OpCode.GET_KEY.fieldValue(), (short) 1, (byte) 0x04, (byte) 0, + (short) 0x0001, 5, 0, 1L, flagBytes, keyBytes, null); + + capacity += buffers[9].capacity(); + + ByteBuffer totalBuffer = ByteBuffer.allocate(capacity); + for (ByteBuffer buffer : buffers) { + totalBuffer.put(buffer); + } + totalBuffer.flip(); + + assertTrue(command.decode(null, totalBuffer)); + Map result = (Map) command.getResult(); + + assertNotNull(result); + assertEquals(5, result.size()); + + for (int i = 0; i < 10; i++) { + if (i % 2 == 0) { + assertNotNull(result.get(String.valueOf(i))); + assertEquals(String.valueOf(i), this.transcoder.decode(result.get(String.valueOf(i)))); + } else { + assertNull(result.get(String.valueOf(i))); + } + } + + assertEquals(0, totalBuffer.remaining()); + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryIncrDecrUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryIncrDecrUnitTest.java new file mode 100644 index 0000000..b7c593b --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryIncrDecrUnitTest.java @@ -0,0 +1,81 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryIncrDecrUnitTest extends BaseBinaryCommandUnitTest { + + String key = "counter"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + long delta = 0x01; + long initial = 0x00; + int exp = 2 * 3600; + + @Test + public void testIncrementEncodeDecode() { + Command command = this.commandFactory.createIncrDecrCommand(this.key, this.keyBytes, this.delta, + this.initial, this.exp, CommandType.INCR, false); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(51, encodeBuffer.capacity()); + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.INCREMENT.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.INCREMENT.fieldValue(), (short) 0, (byte) 0, + (byte) 0, (short) 0, 0x00000008, 0, 5L, null, null, this.transcoderUtils.encodeLong(0L)); + assertEquals(32, buffer.capacity()); + assertTrue(command.decode(null, buffer)); + assertEquals(0L, command.getResult()); + assertEquals(0, buffer.remaining()); + + command = this.commandFactory.createIncrDecrCommand(this.key, this.keyBytes, this.delta, + this.initial, this.exp, CommandType.INCR, false); + buffer = constructResponse(OpCode.INCREMENT.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0x00000008, 0, 5L, null, null, this.transcoderUtils.encodeLong(9999L)); + assertEquals(32, buffer.capacity()); + assertTrue(command.decode(null, buffer)); + assertEquals(9999L, command.getResult()); + assertEquals(0, buffer.remaining()); + + } + + @Test + public void testDecrementEncodeDecode() { + Command command = this.commandFactory.createIncrDecrCommand(this.key, this.keyBytes, this.delta, + this.initial, this.exp, CommandType.DECR, false); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(51, encodeBuffer.capacity()); + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.DECREMENT.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.DECREMENT.fieldValue(), (short) 0, (byte) 0, + (byte) 0, (short) 0, 0x00000008, 0, 5L, null, null, this.transcoderUtils.encodeLong(0L)); + assertEquals(32, buffer.capacity()); + assertTrue(command.decode(null, buffer)); + assertEquals(0L, command.getResult()); + assertEquals(0, buffer.remaining()); + + command = this.commandFactory.createIncrDecrCommand(this.key, this.keyBytes, this.delta, + this.initial, this.exp, CommandType.DECR, false); + buffer = constructResponse(OpCode.DECREMENT.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0x00000008, 0, 5L, null, null, this.transcoderUtils.encodeLong(9999L)); + assertEquals(32, buffer.capacity()); + assertTrue(command.decode(null, buffer)); + assertEquals(9999L, command.getResult()); + assertEquals(0, buffer.remaining()); + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryStatsCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryStatsCommandUnitTest.java new file mode 100644 index 0000000..00c4973 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryStatsCommandUnitTest.java @@ -0,0 +1,56 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryStatsCommandUnitTest extends BaseBinaryCommandUnitTest { + @Test + public void testEncodeDecode() { + Command command = this.commandFactory.createStatsCommand(new InetSocketAddress("localhost", 80), + new CountDownLatch(1), null); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertEquals(24, encodeBuffer.capacity()); + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.STAT.fieldValue(), opCode); + + ByteBuffer[] buffers = new ByteBuffer[5]; + int capacity = 0; + for (int i = 0; i < buffers.length; i++) { + String key = String.valueOf(i); + buffers[i] = constructResponse(OpCode.STAT.fieldValue(), (short) key.length(), (byte) 0, + (byte) 0, (short) 0, 2 * key.length(), 0, 0L, null, key.getBytes(), key.getBytes()); + capacity += buffers[i].capacity(); + + } + ByteBuffer totalBuffer = ByteBuffer.allocate(capacity + 24); + for (ByteBuffer buffer : buffers) { + totalBuffer.put(buffer); + } + totalBuffer.put(constructResponse(OpCode.STAT.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0, 0, 0L, null, null, null)); + totalBuffer.flip(); + + assertTrue(command.decode(null, totalBuffer)); + Map result = (Map) command.getResult(); + + assertNotNull(result); + assertTrue(result.size() > 0); + assertEquals(5, result.size()); + + for (Map.Entry entry : result.entrySet()) { + assertEquals(entry.getKey(), entry.getValue()); + } + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryStoreCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryStoreCommandUnitTest.java new file mode 100644 index 0000000..47577fd --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryStoreCommandUnitTest.java @@ -0,0 +1,111 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryStoreCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + String value = "world"; + boolean noreply = false; + + @Test + public void testAddEncodeAndDecode() { + + Command command = this.commandFactory.createAddCommand(this.key, this.keyBytes, 0, this.value, + this.noreply, this.transcoder); + + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(42, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.ADD.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.ADD.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.ADD.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = this.commandFactory.createAddCommand(this.key, this.keyBytes, 0, this.value, + this.noreply, this.transcoder); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } + + @Test + public void testReplaceEncodeAndDecode() { + + Command command = this.commandFactory.createReplaceCommand(this.key, this.keyBytes, 0, + this.value, this.noreply, this.transcoder); + + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(42, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.REPLACE.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.REPLACE.fieldValue(), (short) 0, (byte) 0, + (byte) 0, (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.REPLACE.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = this.commandFactory.createReplaceCommand(this.key, this.keyBytes, 0, this.value, + this.noreply, this.transcoder); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } + + @Test + public void testSetEncodeAndDecode() { + + Command command = this.commandFactory.createSetCommand(this.key, this.keyBytes, 0, this.value, + this.noreply, this.transcoder); + + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(42, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.SET.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.SET.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.SET.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = this.commandFactory.createSetCommand(this.key, this.keyBytes, 0, this.value, + this.noreply, this.transcoder); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryTouchCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryTouchCommandUnitTest.java new file mode 100644 index 0000000..0f00750 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/binary/BinaryTouchCommandUnitTest.java @@ -0,0 +1,45 @@ +package net.rubyeye.xmemcached.test.unittest.commands.binary; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BinaryTouchCommandUnitTest extends BaseBinaryCommandUnitTest { + String key = "hello"; + byte[] keyBytes = ByteUtils.getBytes(this.key); + + @Test + public void testEncodeDecode() { + Command command = this.commandFactory.createTouchCommand(key, keyBytes, null, 10, false); + command.encode(); + ByteBuffer encodeBuffer = command.getIoBuffer().buf(); + assertNotNull(encodeBuffer); + assertEquals(24 + 4 + 5, encodeBuffer.capacity()); + + byte opCode = encodeBuffer.get(1); + assertEquals(OpCode.TOUCH.fieldValue(), opCode); + + ByteBuffer buffer = constructResponse(OpCode.TOUCH.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0, 0, 0, 1L, null, null, null); + + assertTrue(command.decode(null, buffer)); + assertTrue((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + + buffer = constructResponse(OpCode.TOUCH.fieldValue(), (short) 0, (byte) 0, (byte) 0, + (short) 0x0005, 0, 0, 1L, null, null, null); + command = this.commandFactory.createTouchCommand(key, keyBytes, null, 10, false); + assertTrue(command.decode(null, buffer)); + assertFalse((Boolean) command.getResult()); + assertEquals(0, buffer.remaining()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/factory/TextCommandFactoryTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/factory/TextCommandFactoryTest.java new file mode 100644 index 0000000..bbe4255 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/factory/TextCommandFactoryTest.java @@ -0,0 +1,121 @@ +package net.rubyeye.xmemcached.test.unittest.commands.factory; + +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.transcoders.StringTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.ByteUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + + +public class TextCommandFactoryTest { + static final BufferAllocator bufferAllocator = new SimpleBufferAllocator(); + + private CommandFactory commandFactory; + + @BeforeEach + protected void setUp() throws Exception { + this.commandFactory = new TextCommandFactory(); + } + + @Test + public void testCreateDeleteCommand() { + String key = "test"; + byte[] keyBytes = ByteUtils.getBytes(key); + int time = 10; + Command deleteCmd = this.commandFactory.createDeleteCommand("test", keyBytes, time, 0, false); + deleteCmd.encode(); + assertEquals(CommandType.DELETE, deleteCmd.getCommandType()); + String commandStr = new String(deleteCmd.getIoBuffer().buf().array()); + + String expectedStr = "delete test\r\n"; + + assertEquals(expectedStr, commandStr); + } + + @Test + public void testCreateVersionCommand() { + Command versionCmd = this.commandFactory.createVersionCommand(new CountDownLatch(1), null); + versionCmd.encode(); + String commandStr = new String(versionCmd.getIoBuffer().buf().array()); + assertEquals("version\r\n", commandStr); + assertEquals(CommandType.VERSION, versionCmd.getCommandType()); + } + + @Test + public void testCreateStoreCommand() { + String key = "test"; + String value = "test"; + byte[] keyBytes = ByteUtils.getBytes(key); + int exp = 0; + Transcoder transcoder = new StringTranscoder(); + Command storeCmd = + this.commandFactory.createSetCommand(key, keyBytes, exp, value, false, transcoder); + storeCmd.encode(); + assertFalse(storeCmd.isNoreply()); + assertEquals(CommandType.SET, storeCmd.getCommandType()); + String commandStr = new String(storeCmd.getIoBuffer().buf().array()); + + String expectedStr = "set test " + StringTranscoder.STRING_FLAG + " 0 4\r\ntest\r\n"; + assertEquals(expectedStr, commandStr); + } + + @Test + public void testCreateGetCommand() { + String key = "test"; + byte[] keyBytes = ByteUtils.getBytes(key); + Command getCmd = this.commandFactory.createGetCommand(key, keyBytes, CommandType.GET_ONE, null); + getCmd.encode(); + assertEquals(CommandType.GET_ONE, getCmd.getCommandType()); + String commandStr = new String(getCmd.getIoBuffer().buf().array()); + + String expectedStr = "get test\r\n"; + assertEquals(expectedStr, commandStr); + } + + @Test + public void testCreateIncrDecrCommand() { + String key = "test"; + byte[] keyBytes = ByteUtils.getBytes(key); + int num = 10; + Command inCr = this.commandFactory.createIncrDecrCommand(key, keyBytes, num, 0, 0, + CommandType.INCR, false); + inCr.encode(); + assertEquals(CommandType.INCR, inCr.getCommandType()); + String commandStr = new String(inCr.getIoBuffer().buf().array()); + + String expectedStr = "incr test 10\r\n"; + assertEquals(expectedStr, commandStr); + + } + + @Test + public void testCreateGetMultiCommand() { + List keys = new ArrayList(); + keys.add("a"); + keys.add("b"); + keys.add("c"); + keys.add("a"); + + Command cmd = this.commandFactory.createGetMultiCommand(keys, null, CommandType.GET_MANY, null); + cmd.encode(); + assertEquals(CommandType.GET_MANY, cmd.getCommandType()); + String commandStr = new String(cmd.getIoBuffer().buf().array()); + + String expectedStr = "get a b c a\r\n"; + assertEquals(expectedStr, commandStr); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/BaseTextCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/BaseTextCommandUnitTest.java new file mode 100644 index 0000000..38aedbd --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/BaseTextCommandUnitTest.java @@ -0,0 +1,61 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.exception.MemcachedDecodeException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +public class BaseTextCommandUnitTest { + static final String DECODE_ERROR_SESSION_WILL_BE_CLOSED = "Decode error,session will be closed"; + protected CommandFactory commandFactory; + protected BufferAllocator bufferAllocator; + + @BeforeEach + public void setUp() { + this.bufferAllocator = new SimpleBufferAllocator(); + this.commandFactory = new TextCommandFactory(); + } + + protected void checkDecodeNullAndNotLineByteBuffer(Command command) { + assertFalse(command.decode(null, null)); + assertFalse(command.decode(null, ByteBuffer.allocate(0))); + assertFalse(command.decode(null, ByteBuffer.wrap("test".getBytes()))); + } + + protected void checkDecodeInvalidLine(Command command, String key, String invalidLine) { + try { + + command.decode(null, ByteBuffer.wrap(invalidLine.getBytes())); + fail(); + } catch (MemcachedDecodeException e) { + assertTrue(true); + assertEquals(DECODE_ERROR_SESSION_WILL_BE_CLOSED + ",key=" + key + ",server returns=" + + invalidLine.replace("\r\n", ""), e.getMessage()); + } + } + + @Test + public void testDecodeError() { + + } + + protected void checkDecodeValidLine(Command command, String validLine) { + assertTrue(command.decode(null, ByteBuffer.wrap(validLine.getBytes()))); + } + + protected void checkByteBufferEquals(Command command, String line) { + assertEquals(ByteBuffer.wrap(line.getBytes()), command.getIoBuffer().buf()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextCommandsAllTests.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextCommandsAllTests.java new file mode 100644 index 0000000..9e81ec0 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextCommandsAllTests.java @@ -0,0 +1,23 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +//import junit.framework.Test; +//import junit.framework.TestSuite; + +public class TextCommandsAllTests { + +// public static Test suite() { +// TestSuite suite = new TestSuite("Test for net.rubyeye.xmemcached.test.unittest.commands.text"); +// // $JUnit-BEGIN$ +// suite.addTestSuite(TextStatsCommandUnitTest.class); +// suite.addTestSuite(TextIncrDecrCommandUnitTest.class); +// suite.addTestSuite(TextVersionCommandUnitTest.class); +// suite.addTestSuite(TextStoreCommandUnitTest.class); +// suite.addTestSuite(TextFlushAllCommandUnitTest.class); +// suite.addTestSuite(TextDeleteCommandUnitTest.class); +// suite.addTestSuite(TextGetOneCommandUnitTest.class); +// suite.addTestSuite(TextGetMultiCommandUnitTest.class); +// // $JUnit-END$ +// return suite; +// } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextDeleteCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextDeleteCommandUnitTest.java new file mode 100644 index 0000000..1d11e22 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextDeleteCommandUnitTest.java @@ -0,0 +1,39 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TextDeleteCommandUnitTest extends BaseTextCommandUnitTest { + @Test + public void testEncode() { + Command command = + this.commandFactory.createDeleteCommand("test", "test".getBytes(), 10, 0, false); + assertNull(command.getIoBuffer()); + command.encode(); + this.checkByteBufferEquals(command, "delete test\r\n"); + + command = this.commandFactory.createDeleteCommand("test", "test".getBytes(), 10, 0, true); + assertNull(command.getIoBuffer()); + command.encode(); + this.checkByteBufferEquals(command, "delete test noreply\r\n"); + } + + @Test + public void testDecode() { + Command command = + this.commandFactory.createDeleteCommand("test", "test".getBytes(), 10, 0, false); + this.checkDecodeNullAndNotLineByteBuffer(command); + this.checkDecodeInvalidLine(command, "test", "STORED\r\n"); + this.checkDecodeInvalidLine(command, "test", "VALUE test 4 5 1\r\n"); + this.checkDecodeInvalidLine(command, "test", "END\r\n"); + this.checkDecodeValidLine(command, "NOT_FOUND\r\n"); + assertFalse((Boolean) command.getResult()); + command.setResult(null); + this.checkDecodeValidLine(command, "DELETED\r\n"); + assertTrue((Boolean) command.getResult()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextFlushAllCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextFlushAllCommandUnitTest.java new file mode 100644 index 0000000..9148b4f --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextFlushAllCommandUnitTest.java @@ -0,0 +1,49 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.text.TextFlushAllCommand; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TextFlushAllCommandUnitTest extends BaseTextCommandUnitTest { + + @Test + public void testEncode() { + Command command = this.commandFactory.createFlushAllCommand(new CountDownLatch(1), 0, false); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals(TextFlushAllCommand.FLUSH_ALL, command.getIoBuffer().buf()); + + command = this.commandFactory.createFlushAllCommand(new CountDownLatch(1), 0, true); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals("flush_all noreply\r\n", new String(command.getIoBuffer().buf().array())); + + } + + @Test + public void testEncodeWithDelay() { + Command command = this.commandFactory.createFlushAllCommand(new CountDownLatch(1), 10, false); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals("flush_all 10\r\n", new String(command.getIoBuffer().buf().array())); + + command = this.commandFactory.createFlushAllCommand(new CountDownLatch(1), 10, true); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals("flush_all 10 noreply\r\n", new String(command.getIoBuffer().buf().array())); + } + + @Test + public void testDecode() { + Command command = this.commandFactory.createFlushAllCommand(new CountDownLatch(1), 0, false); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "[flush_all]", "END\r\n"); + checkDecodeInvalidLine(command, "[flush_all]", "STORED\r\n"); + checkDecodeValidLine(command, "OK\r\n"); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextGetMultiCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextGetMultiCommandUnitTest.java new file mode 100644 index 0000000..c18fe5a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextGetMultiCommandUnitTest.java @@ -0,0 +1,110 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.text.TextGetCommand; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + + +@SuppressWarnings("unchecked") +public class TextGetMultiCommandUnitTest extends BaseTextCommandUnitTest { + static final Transcoder transcoder = new SerializingTranscoder(); + static List keys = new ArrayList(); + + static { + keys.add("test1"); + keys.add("test2"); + keys.add("test3"); + keys.add("test4"); + } + + @Test + public void testGetManyEncode() { + Command command = this.commandFactory.createGetMultiCommand(keys, new CountDownLatch(1), + CommandType.GET_MANY, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + + checkByteBufferEquals(command, "get test1 test2 test3 test4\r\n"); + + } + + @Test + public void testGetsManyEncode() { + Command command = this.commandFactory.createGetMultiCommand(keys, new CountDownLatch(1), + CommandType.GETS_MANY, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + + checkByteBufferEquals(command, "gets test1 test2 test3 test4\r\n"); + } + + @Test + public void testGetManyDecode() { + TextGetCommand command = (TextGetCommand) this.commandFactory.createGetMultiCommand(keys, + new CountDownLatch(1), CommandType.GET_MANY, transcoder); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "test1", "STORED\r\n"); + checkDecodeInvalidLine(command, "test1", "NOT_FOUND\r\n"); + checkDecodeInvalidLine(command, "test1", "NOT_STORED\r\n"); + checkDecodeInvalidLine(command, "test1", "DELETED\r\n"); + + checkDecodeValidLine(command, "END\r\n"); + assertEquals(0, ((Map) command.getResult()).size()); + // data not complelte + command.setParseStatus(net.rubyeye.xmemcached.command.text.TextGetCommand.ParseStatus.NULL); + assertFalse(command.decode(null, + ByteBuffer.wrap("VALUE test1 0 2\r\n10\r\nVALUE test2 0 4\r\n10".getBytes()))); + // data coming,but not with END + assertFalse(command.decode(null, ByteBuffer.wrap("00\r\n".getBytes()))); + checkDecodeValidLine(command, "END\r\n"); + + assertEquals(2, ((Map) command.getResult()).size()); + assertEquals("10", + transcoder.decode(((Map) command.getResult()).get("test1"))); + assertEquals("1000", + transcoder.decode(((Map) command.getResult()).get("test2"))); + } + + @Test + public void testGetsManyDecode() { + TextGetCommand command = (TextGetCommand) this.commandFactory.createGetMultiCommand(keys, + new CountDownLatch(1), CommandType.GETS_MANY, transcoder); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "test1", "STORED\r\n"); + checkDecodeInvalidLine(command, "test1", "NOT_FOUND\r\n"); + checkDecodeInvalidLine(command, "test1", "NOT_STORED\r\n"); + checkDecodeInvalidLine(command, "test1", "DELETED\r\n"); + + checkDecodeValidLine(command, "END\r\n"); + assertEquals(0, ((Map) command.getResult()).size()); + command.setParseStatus(net.rubyeye.xmemcached.command.text.TextGetCommand.ParseStatus.NULL); + // data not complelte + assertFalse(command.decode(null, + ByteBuffer.wrap("VALUE test1 0 2 999\r\n10\r\nVALUE test2 0 4 1000\r\n10".getBytes()))); + // data coming,but not with END + assertFalse(command.decode(null, ByteBuffer.wrap("00\r\n".getBytes()))); + checkDecodeValidLine(command, "END\r\n"); + + assertEquals(2, ((Map) command.getResult()).size()); + assertEquals(999, ((Map) command.getResult()).get("test1").getCas()); + assertEquals(1000, ((Map) command.getResult()).get("test2").getCas()); + assertEquals("10", + transcoder.decode(((Map) command.getResult()).get("test1"))); + assertEquals("1000", + transcoder.decode(((Map) command.getResult()).get("test2"))); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextGetOneCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextGetOneCommandUnitTest.java new file mode 100644 index 0000000..e250915 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextGetOneCommandUnitTest.java @@ -0,0 +1,84 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.transcoders.CachedData; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TextGetOneCommandUnitTest extends BaseTextCommandUnitTest { + @Test + public void testGetOneEncode() { + Command command = + this.commandFactory.createGetCommand("test", "test".getBytes(), CommandType.GET_ONE, null); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "get test\r\n"); + } + + @Test + public void testGetsOneEncode() { + Command command = + this.commandFactory.createGetCommand("test", "test".getBytes(), CommandType.GETS_ONE, null); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "gets test\r\n"); + } + + @Test + public void testGetOneDecode() { + Command command = + this.commandFactory.createGetCommand("test", "test".getBytes(), CommandType.GET_ONE, null); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "test", "STORED\r\n"); + checkDecodeInvalidLine(command, "test", "NOT_FOUND\r\n"); + checkDecodeInvalidLine(command, "test", "NOT_STORED\r\n"); + checkDecodeInvalidLine(command, "test", "DELETED\r\n"); + + checkDecodeValidLine(command, "END\r\n"); + assertNull(command.getResult()); + assertEquals(0, command.getLatch().getCount()); + + command = + this.commandFactory.createGetCommand("test", "test".getBytes(), CommandType.GET_ONE, null); + assertFalse(command.decode(null, ByteBuffer.wrap("VALUE test 0 2\r\n10\r\n".getBytes()))); + assertNull(command.getResult()); + + assertFalse(command.decode(null, ByteBuffer.wrap("VALUE test 0 4\r\n1".getBytes()))); + assertFalse(command.decode(null, ByteBuffer.wrap("0".getBytes()))); + assertFalse(command.decode(null, ByteBuffer.wrap("0".getBytes()))); + assertFalse(command.decode(null, ByteBuffer.wrap("0".getBytes()))); + checkDecodeValidLine(command, "\r\nEND\r\n"); + + assertEquals("1000", new String(((CachedData) command.getResult()).getData())); + } + + @Test + public void testGetsOneDecode() { + Command command = + this.commandFactory.createGetCommand("test", "test".getBytes(), CommandType.GETS_ONE, null); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "test", "STORED\r\n"); + checkDecodeInvalidLine(command, "test", "NOT_FOUND\r\n"); + checkDecodeInvalidLine(command, "test", "NOT_STORED\r\n"); + checkDecodeInvalidLine(command, "test", "DELETED\r\n"); + + checkDecodeValidLine(command, "END\r\n"); + assertNull(command.getResult()); + assertEquals(0, command.getLatch().getCount()); + + command = + this.commandFactory.createGetCommand("test", "test".getBytes(), CommandType.GET_ONE, null); + assertFalse(command.decode(null, ByteBuffer.wrap("VALUE test 0 2 999\r\n10\r\n".getBytes()))); + assertNull(command.getResult()); + checkDecodeValidLine(command, "VALUE test 0 2 999\r\n10\r\nEND\r\n"); + + assertEquals("10", new String(((CachedData) command.getResult()).getData())); + assertEquals(999, ((CachedData) command.getResult()).getCas()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextIncrDecrCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextIncrDecrCommandUnitTest.java new file mode 100644 index 0000000..bd5180f --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextIncrDecrCommandUnitTest.java @@ -0,0 +1,80 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TextIncrDecrCommandUnitTest extends BaseTextCommandUnitTest { + @Test + public void testEncodeIncr() { + Command command = this.commandFactory.createIncrDecrCommand("test", "test".getBytes(), 10, 0, 0, + CommandType.INCR, false); + assertNull(command.getIoBuffer()); + command.encode(); + + checkByteBufferEquals(command, "incr test 10\r\n"); + + command = this.commandFactory.createIncrDecrCommand("test", "test".getBytes(), 10, 0, 0, + CommandType.INCR, true); + assertNull(command.getIoBuffer()); + command.encode(); + + checkByteBufferEquals(command, "incr test 10 noreply\r\n"); + } + + @Test + public void testEncodeDecr() { + Command command = this.commandFactory.createIncrDecrCommand("test", "test".getBytes(), 10, 0, 0, + CommandType.DECR, false); + assertNull(command.getIoBuffer()); + command.encode(); + + checkByteBufferEquals(command, "decr test 10\r\n"); + + command = this.commandFactory.createIncrDecrCommand("test", "test".getBytes(), 10, 0, 0, + CommandType.DECR, true); + assertNull(command.getIoBuffer()); + command.encode(); + + checkByteBufferEquals(command, "decr test 10 noreply\r\n"); + } + + @Test + public void testDecode() { + Command command = this.commandFactory.createIncrDecrCommand("test", "test".getBytes(), 10, 0, 0, + CommandType.DECR, false); + checkDecodeNullAndNotLineByteBuffer(command); + // try { + // command.decode(null, ByteBuffer.wrap("NOT_STORED\r\n".getBytes())); + // fail(); + // } catch (NumberFormatException e) { + // assertEquals("For input string: \"NOT_STORED\"", e.getMessage()); + // } + // try { + // command.decode(null, ByteBuffer.wrap("test\r\n".getBytes())); + // fail(); + // } catch (NumberFormatException e) { + // assertEquals("For input string: \"test\"", e.getMessage()); + // } + // try { + // command.decode(null, ByteBuffer.wrap("STORED\r\n".getBytes())); + // fail(); + // } catch (NumberFormatException e) { + // assertEquals("For input string: \"STORED\"", e.getMessage()); + // } + // try { + // command.decode(null, ByteBuffer.wrap("VALUE test\r\n".getBytes())); + // fail(); + // } catch (NumberFormatException e) { + // assertEquals("For input string: \"VALUE test\"", e.getMessage()); + // } + checkDecodeValidLine(command, "NOT_FOUND\r\n"); + assertEquals("NOT_FOUND", command.getResult()); + checkDecodeValidLine(command, "3\r\n"); + assertEquals(3L, command.getResult()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextStatsCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextStatsCommandUnitTest.java new file mode 100644 index 0000000..b0718ee --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextStatsCommandUnitTest.java @@ -0,0 +1,68 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.command.text.TextStatsCommand; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author dennis + */ +public class TextStatsCommandUnitTest extends BaseTextCommandUnitTest { + + @Test + public void testEncode() { + Command command = this.commandFactory.createStatsCommand(null, new CountDownLatch(1), null); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals(TextStatsCommand.STATS, command.getIoBuffer().buf()); + } + + @Test + public void testItemEncode() { + Command command = this.commandFactory.createStatsCommand(null, new CountDownLatch(1), "items"); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals(ByteBuffer.wrap("stats items\r\n".getBytes()), command.getIoBuffer().buf()); + } + + @Test + public void testDecode() { + Command command = this.commandFactory.createStatsCommand(null, new CountDownLatch(1), null); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "stats", "OK\r\n"); + checkDecodeValidLine(command, "END\r\n"); + assertFalse( + command.decode(null, ByteBuffer.wrap("STAT bytes 100\r\nSTAT threads 1\r\n".getBytes()))); + Map result = (Map) command.getResult(); + assertEquals("100", result.get("bytes")); + assertEquals("1", result.get("threads")); + checkDecodeValidLine(command, "STAT connections 5\r\nEND\r\n"); + assertEquals(3, result.size()); + assertEquals("5", result.get("connections")); + + } + + @Test + public void testCachedumpDecode() { + Command command = ((TextCommandFactory) this.commandFactory).createStatsCachedumpCommand(null, + new CountDownLatch(1), 1, 10); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "stats", "OK\r\n"); + assertFalse(command.decode(null, ByteBuffer.wrap("ITEM some-key [6 b; 0 s]\r\n".getBytes()))); + checkDecodeValidLine(command, "END\r\n"); + Map result = (Map) command.getResult(); + Integer[] item = result.get("some-key"); + assertEquals(Integer.valueOf(6), item[0]); + assertEquals(Integer.valueOf(0), item[1]); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextStoreCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextStoreCommandUnitTest.java new file mode 100644 index 0000000..c40a239 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextStoreCommandUnitTest.java @@ -0,0 +1,164 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.transcoders.StringTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings("unchecked") +public class TextStoreCommandUnitTest extends BaseTextCommandUnitTest { + static final String key = "test"; + static final String value = "10"; + static final int exp = 0; + static final long cas = 999; + static final Transcoder transcoder = new StringTranscoder(); + + @Test + public void testCASEncode() { + Command command = this.commandFactory.createCASCommand(key, key.getBytes(), exp, value, cas, + false, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "cas test 0 0 2 999\r\n10\r\n"); + command = this.commandFactory.createCASCommand(key, key.getBytes(), exp, value, cas, true, + transcoder); + command.encode(); + checkByteBufferEquals(command, "cas test 0 0 2 999 noreply\r\n10\r\n"); + } + + @Test + public void testSetEncode() { + Command command = + this.commandFactory.createSetCommand(key, key.getBytes(), exp, value, false, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "set test 0 0 2\r\n10\r\n"); + command = + this.commandFactory.createSetCommand(key, key.getBytes(), exp, value, true, transcoder); + command.encode(); + checkByteBufferEquals(command, "set test 0 0 2 noreply\r\n10\r\n"); + } + + @Test + public void testAddEncode() { + Command command = + this.commandFactory.createAddCommand(key, key.getBytes(), exp, value, false, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "add test 0 0 2\r\n10\r\n"); + + command = + this.commandFactory.createAddCommand(key, key.getBytes(), exp, value, true, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "add test 0 0 2 noreply\r\n10\r\n"); + } + + @Test + public void testReplaceEncode() { + Command command = this.commandFactory.createReplaceCommand(key, key.getBytes(), exp, value, + false, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "replace test 0 0 2\r\n10\r\n"); + + command = + this.commandFactory.createReplaceCommand(key, key.getBytes(), exp, value, true, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "replace test 0 0 2 noreply\r\n10\r\n"); + } + + @Test + public void testAppendEncode() { + Command command = + this.commandFactory.createAppendCommand(key, key.getBytes(), value, false, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "append test 0 0 2\r\n10\r\n"); + + command = this.commandFactory.createAppendCommand(key, key.getBytes(), value, true, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "append test 0 0 2 noreply\r\n10\r\n"); + } + + @Test + public void testPrependEncode() { + Command command = + this.commandFactory.createPrependCommand(key, key.getBytes(), value, false, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "prepend test 0 0 2\r\n10\r\n"); + + command = + this.commandFactory.createPrependCommand(key, key.getBytes(), value, true, transcoder); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "prepend test 0 0 2 noreply\r\n10\r\n"); + } + + @Test + public void testCASDecode() { + Command command = this.commandFactory.createCASCommand(key, key.getBytes(), exp, value, cas, + false, transcoder); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, key, "VALUE test 4 0 5\r\n"); + checkDecodeInvalidLine(command, key, "DELETED\r\n"); + checkDecodeValidLine(command, "STORED\r\n"); + assertTrue((Boolean) command.getResult()); + command.setResult(null); + checkDecodeValidLine(command, "EXISTS\r\n"); + assertFalse((Boolean) command.getResult()); + command.setResult(null); + checkDecodeValidLine(command, "STORED\r\n"); + assertTrue((Boolean) command.getResult()); + command.setResult(null); + checkDecodeValidLine(command, "NOT_FOUND\r\n"); + assertFalse((Boolean) command.getResult()); + + } + + @Test + public void testIssue128() { + // store command + Command command = + this.commandFactory.createSetCommand(key, key.getBytes(), exp, value, false, transcoder); + command.decode(null, + ByteBuffer.wrap("SERVER_ERROR out of memory storing object\r\n".getBytes())); + Exception e = command.getException(); + assertNotNull(e); + assertEquals("out of memory storing object,key=test", e.getMessage()); + + // cas command + command = this.commandFactory.createCASCommand(key, key.getBytes(), exp, value, cas, false, + transcoder); + command.decode(null, + ByteBuffer.wrap("SERVER_ERROR out of memory storing object\r\n".getBytes())); + e = command.getException(); + assertNotNull(e); + assertEquals("out of memory storing object,key=test", e.getMessage()); + } + + @Test + public void testStoreDecode() { + Command command = + this.commandFactory.createSetCommand(key, key.getBytes(), exp, value, false, transcoder); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, key, "EXISTS\r\n"); + checkDecodeInvalidLine(command, key, "END\r\n"); + checkDecodeValidLine(command, "STORED\r\n"); + assertTrue((Boolean) command.getResult()); + command.setResult(null); + checkDecodeValidLine(command, "NOT_STORED\r\n"); + assertFalse((Boolean) command.getResult()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextTouchCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextTouchCommandUnitTest.java new file mode 100644 index 0000000..715b71a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextTouchCommandUnitTest.java @@ -0,0 +1,37 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TextTouchCommandUnitTest extends BaseTextCommandUnitTest { + @Test + public void testEncode() { + Command command = + this.commandFactory.createTouchCommand("test", "test".getBytes(), null, 10, false); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "touch test 10\r\n"); + + command = this.commandFactory.createTouchCommand("test", "test".getBytes(), null, 10, true); + assertNull(command.getIoBuffer()); + command.encode(); + checkByteBufferEquals(command, "touch test 10 noreply\r\n"); + } + + @Test + public void testDecode() { + Command command = + this.commandFactory.createTouchCommand("test", "test".getBytes(), null, 10, false); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "test", "STORED\r\n"); + checkDecodeValidLine(command, "NOT_FOUND\r\n"); + assertFalse((Boolean) command.getResult()); + command.setResult(null); + checkDecodeValidLine(command, "TOUCHED\r\n"); + assertTrue((Boolean) command.getResult()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextVerbositylCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextVerbositylCommandUnitTest.java new file mode 100644 index 0000000..26f457a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextVerbositylCommandUnitTest.java @@ -0,0 +1,34 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TextVerbositylCommandUnitTest extends BaseTextCommandUnitTest { + + @Test + public void testEncode() { + Command command = this.commandFactory.createVerbosityCommand(new CountDownLatch(1), 1, false); + assertNull(command.getIoBuffer()); + command.encode(); + assertEquals("verbosity 1\r\n", new String(command.getIoBuffer().buf().array())); + + command = this.commandFactory.createVerbosityCommand(new CountDownLatch(1), 1, true); + command.encode(); + assertEquals("verbosity 1 noreply\r\n", new String(command.getIoBuffer().buf().array())); + + } + + @Test + public void testDecode() { + Command command = this.commandFactory.createVerbosityCommand(new CountDownLatch(1), 0, false); + checkDecodeNullAndNotLineByteBuffer(command); + checkDecodeInvalidLine(command, "[verbosity]", "END\r\n"); + checkDecodeInvalidLine(command, "[verbosity]", "STORED\r\n"); + checkDecodeValidLine(command, "OK\r\n"); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextVersionCommandUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextVersionCommandUnitTest.java new file mode 100644 index 0000000..91298cc --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/commands/text/TextVersionCommandUnitTest.java @@ -0,0 +1,33 @@ +package net.rubyeye.xmemcached.test.unittest.commands.text; + +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.ServerAddressAware; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class TextVersionCommandUnitTest extends BaseTextCommandUnitTest { + @Test + public void testEncode() { + Command versionCommand = this.commandFactory.createVersionCommand(new CountDownLatch(1), null); + assertNull(versionCommand.getIoBuffer()); + versionCommand.encode(); + assertEquals(ServerAddressAware.VERSION, versionCommand.getIoBuffer().buf()); + } + + @Test + public void testDecode() { + Command versionCommand = this.commandFactory.createVersionCommand(new CountDownLatch(1), null); + checkDecodeNullAndNotLineByteBuffer(versionCommand); + checkDecodeInvalidLine(versionCommand, "[version]", "test\r\n"); + + checkDecodeValidLine(versionCommand, "VERSION\r\n"); + assertEquals("unknown version", versionCommand.getResult()); + + checkDecodeValidLine(versionCommand, "VERSION 1.2.5\r\n"); + assertEquals("1.2.5", versionCommand.getResult()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/AbstractMemcachedSessionLocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/AbstractMemcachedSessionLocatorUnitTest.java new file mode 100644 index 0000000..c990f4e --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/AbstractMemcachedSessionLocatorUnitTest.java @@ -0,0 +1,35 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.test.unittest.MockSession; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + + +public abstract class AbstractMemcachedSessionLocatorUnitTest { + protected MemcachedSessionLocator locator; + + @Test + public void testGetSessionByKey_EmptyList() { + assertNull(this.locator.getSessionByKey("test")); + } + + @Test + public void testGetSessionByKey_OneSession() { + MockSession session = new MockSession(8080); + List list = new ArrayList(); + list.add(session); + this.locator.updateSessions(list); + + assertSame(session, this.locator.getSessionByKey("a")); + assertSame(session, this.locator.getSessionByKey("b")); + assertSame(session, this.locator.getSessionByKey("c")); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/AddressMemcachedSessionComparatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/AddressMemcachedSessionComparatorUnitTest.java new file mode 100644 index 0000000..9e050ea --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/AddressMemcachedSessionComparatorUnitTest.java @@ -0,0 +1,284 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.CodecFactory.Decoder; +import com.google.code.yanf4j.core.CodecFactory.Encoder; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.impl.AddressMemcachedSessionComparator; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AddressMemcachedSessionComparatorUnitTest { + static private class MockSession implements MemcachedSession, Session { + private final InetSocketAddress address; + + public InetSocketAddressWrapper getInetSocketAddressWrapper() { + return new InetSocketAddressWrapper(this.address, 0, 0, null); + } + + public MockSession(InetSocketAddress address) { + super(); + this.address = address; + } + + public void quit() { + // TODO Auto-generated method stub + + } + + public boolean isAuthFailed() { + // TODO Auto-generated method stub + return false; + } + + public void setAuthFailed(boolean authFailed) { + // TODO Auto-generated method stub + + } + + public Future asyncWrite(Object packet) { + // TODO Auto-generated method stub + return null; + } + + public void clearAttributes() { + // TODO Auto-generated method stub + + } + + public void destroy() { + // TODO Auto-generated method stub + + } + + public void close() { + // TODO Auto-generated method stub + + } + + public void flush() { + // TODO Auto-generated method stub + + } + + public Object getAttribute(String key) { + // TODO Auto-generated method stub + return null; + } + + public Decoder getDecoder() { + // TODO Auto-generated method stub + return null; + } + + public Encoder getEncoder() { + // TODO Auto-generated method stub + return null; + } + + public Handler getHandler() { + // TODO Auto-generated method stub + return null; + } + + public long getLastOperationTimeStamp() { + // TODO Auto-generated method stub + return 0; + } + + public InetAddress getLocalAddress() { + // TODO Auto-generated method stub + return null; + } + + public ByteOrder getReadBufferByteOrder() { + // TODO Auto-generated method stub + return null; + } + + public InetSocketAddress getRemoteSocketAddress() { + // TODO Auto-generated method stub + return null; + } + + public long getScheduleWritenBytes() { + // TODO Auto-generated method stub + return 0; + } + + public long getSessionIdleTimeout() { + // TODO Auto-generated method stub + return 0; + } + + public long getSessionTimeout() { + // TODO Auto-generated method stub + return 0; + } + + public boolean isClosed() { + // TODO Auto-generated method stub + return false; + } + + public boolean isExpired() { + // TODO Auto-generated method stub + return false; + } + + public boolean isHandleReadWriteConcurrently() { + // TODO Auto-generated method stub + return false; + } + + public boolean isIdle() { + // TODO Auto-generated method stub + return false; + } + + public boolean isLoopbackConnection() { + // TODO Auto-generated method stub + return false; + } + + public boolean isUseBlockingRead() { + // TODO Auto-generated method stub + return false; + } + + public boolean isUseBlockingWrite() { + // TODO Auto-generated method stub + return false; + } + + public void removeAttribute(String key) { + // TODO Auto-generated method stub + + } + + public void setAttribute(String key, Object value) { + // TODO Auto-generated method stub + + } + + public Object setAttributeIfAbsent(String key, Object value) { + // TODO Auto-generated method stub + return null; + } + + public void setDecoder(Decoder decoder) { + // TODO Auto-generated method stub + + } + + public void setEncoder(Encoder encoder) { + // TODO Auto-generated method stub + + } + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + // TODO Auto-generated method stub + + } + + public void setReadBufferByteOrder(ByteOrder readBufferByteOrder) { + // TODO Auto-generated method stub + + } + + public void setSessionIdleTimeout(long sessionIdleTimeout) { + // TODO Auto-generated method stub + + } + + public void setSessionTimeout(long sessionTimeout) { + // TODO Auto-generated method stub + + } + + public void setUseBlockingRead(boolean useBlockingRead) { + // TODO Auto-generated method stub + + } + + public void setUseBlockingWrite(boolean useBlockingWrite) { + // TODO Auto-generated method stub + + } + + public void start() { + // TODO Auto-generated method stub + + } + + public void write(Object packet) { + // TODO Auto-generated method stub + + } + + public int getOrder() { + // TODO Auto-generated method stub + return 0; + } + + public int getWeight() { + // TODO Auto-generated method stub + return 0; + } + + public boolean isAllowReconnect() { + // TODO Auto-generated method stub + return false; + } + + public void setAllowReconnect(boolean allow) { + // TODO Auto-generated method stub + + } + + public void setBufferAllocator(BufferAllocator allocator) { + // TODO Auto-generated method stub + + } + + } + + @Test + public void testCompare() throws UnknownHostException { + + List sessionList = new ArrayList(); + for (int i = 0; i < 100; i++) { + sessionList.add(new MockSession(new InetSocketAddress("hostA", i))); + sessionList.add(new MockSession(new InetSocketAddress("hostB", i))); + + byte[] ipAddr = new byte[]{127, 0, 0, 1}; + sessionList.add(new MockSession(new InetSocketAddress(InetAddress.getByAddress(ipAddr), i))); + } + Collections.sort(sessionList, new AddressMemcachedSessionComparator()); + + for (int i = 0; i < sessionList.size(); i++) { + if (i < sessionList.size() - 1) { + int next = i + 1; + MockSession nextSession = (MockSession) sessionList.get(next); + MockSession currentSession = (MockSession) sessionList.get(i); + assertTrue(currentSession.getInetSocketAddressWrapper().getInetSocketAddress().toString() + .compareTo( + nextSession.getInetSocketAddressWrapper().getInetSocketAddress().toString()) < 0); + } + } + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/ArrayMemcachedSessionLocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/ArrayMemcachedSessionLocatorUnitTest.java new file mode 100644 index 0000000..7cc810f --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/ArrayMemcachedSessionLocatorUnitTest.java @@ -0,0 +1,127 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.test.unittest.MockSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertSame; + +public class ArrayMemcachedSessionLocatorUnitTest extends AbstractMemcachedSessionLocatorUnitTest { + + @BeforeEach + public void setUp() { + this.locator = new ArrayMemcachedSessionLocator(); + } + + @Test + public void testGetSessionByKey_SessionPool() { + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session1); + list.add(session1); + list.add(session2); + list.add(session2); + list.add(session3); + + this.locator.updateSessions(list); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + } + + @Test + public void testGetSessionByKey_MoreSessions() { + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + } + + @Test + public void testGetSessionByKey_MoreSessions_OneClosed() { + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + session2.close(); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session3, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session3, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session3, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + } + + @Test + public void testGetSessionByKey_MoreSessions_OneClosed_FailureMode() { + this.locator.setFailureMode(true); + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + session2.close(); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + assertSame(session2, this.locator.getSessionByKey("a")); + assertSame(session3, this.locator.getSessionByKey("b")); + assertSame(session1, this.locator.getSessionByKey("c")); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/IndexMemcachedSessionComparatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/IndexMemcachedSessionComparatorUnitTest.java new file mode 100644 index 0000000..54bd97a --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/IndexMemcachedSessionComparatorUnitTest.java @@ -0,0 +1,277 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.CodecFactory.Decoder; +import com.google.code.yanf4j.core.CodecFactory.Encoder; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.buffer.BufferAllocator; +import net.rubyeye.xmemcached.impl.IndexMemcachedSessionComparator; +import net.rubyeye.xmemcached.networking.MemcachedSession; +import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; + +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class IndexMemcachedSessionComparatorUnitTest { + static private class MockSession implements MemcachedSession, Session { + private final int order; + + public InetSocketAddressWrapper getInetSocketAddressWrapper() { + return new InetSocketAddressWrapper(null, this.order, 0, null); + } + + public MockSession(int order) { + super(); + this.order = order; + } + + public void quit() { + // TODO Auto-generated method stub + + } + + public boolean isAuthFailed() { + // TODO Auto-generated method stub + return false; + } + + public void setAuthFailed(boolean authFailed) { + // TODO Auto-generated method stub + + } + + public Future asyncWrite(Object packet) { + // TODO Auto-generated method stub + return null; + } + + public void clearAttributes() { + // TODO Auto-generated method stub + + } + + public void destroy() { + // TODO Auto-generated method stub + + } + + public void close() { + // TODO Auto-generated method stub + + } + + public void flush() { + // TODO Auto-generated method stub + + } + + public Object getAttribute(String key) { + // TODO Auto-generated method stub + return null; + } + + public Decoder getDecoder() { + // TODO Auto-generated method stub + return null; + } + + public Encoder getEncoder() { + // TODO Auto-generated method stub + return null; + } + + public Handler getHandler() { + // TODO Auto-generated method stub + return null; + } + + public long getLastOperationTimeStamp() { + // TODO Auto-generated method stub + return 0; + } + + public InetAddress getLocalAddress() { + // TODO Auto-generated method stub + return null; + } + + public ByteOrder getReadBufferByteOrder() { + // TODO Auto-generated method stub + return null; + } + + public InetSocketAddress getRemoteSocketAddress() { + // TODO Auto-generated method stub + return null; + } + + public long getScheduleWritenBytes() { + // TODO Auto-generated method stub + return 0; + } + + public long getSessionIdleTimeout() { + // TODO Auto-generated method stub + return 0; + } + + public long getSessionTimeout() { + // TODO Auto-generated method stub + return 0; + } + + public boolean isClosed() { + // TODO Auto-generated method stub + return false; + } + + public boolean isExpired() { + // TODO Auto-generated method stub + return false; + } + + public boolean isHandleReadWriteConcurrently() { + // TODO Auto-generated method stub + return false; + } + + public boolean isIdle() { + // TODO Auto-generated method stub + return false; + } + + public boolean isLoopbackConnection() { + // TODO Auto-generated method stub + return false; + } + + public boolean isUseBlockingRead() { + // TODO Auto-generated method stub + return false; + } + + public boolean isUseBlockingWrite() { + // TODO Auto-generated method stub + return false; + } + + public void removeAttribute(String key) { + // TODO Auto-generated method stub + + } + + public void setAttribute(String key, Object value) { + // TODO Auto-generated method stub + + } + + public Object setAttributeIfAbsent(String key, Object value) { + // TODO Auto-generated method stub + return null; + } + + public void setDecoder(Decoder decoder) { + // TODO Auto-generated method stub + + } + + public void setEncoder(Encoder encoder) { + // TODO Auto-generated method stub + + } + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + // TODO Auto-generated method stub + + } + + public void setReadBufferByteOrder(ByteOrder readBufferByteOrder) { + // TODO Auto-generated method stub + + } + + public void setSessionIdleTimeout(long sessionIdleTimeout) { + // TODO Auto-generated method stub + + } + + public void setSessionTimeout(long sessionTimeout) { + // TODO Auto-generated method stub + + } + + public void setUseBlockingRead(boolean useBlockingRead) { + // TODO Auto-generated method stub + + } + + public void setUseBlockingWrite(boolean useBlockingWrite) { + // TODO Auto-generated method stub + + } + + public void start() { + // TODO Auto-generated method stub + + } + + public void write(Object packet) { + // TODO Auto-generated method stub + + } + + public int getOrder() { + return this.order; + } + + public int getWeight() { + // TODO Auto-generated method stub + return 0; + } + + public boolean isAllowReconnect() { + // TODO Auto-generated method stub + return false; + } + + public void setAllowReconnect(boolean allow) { + // TODO Auto-generated method stub + + } + + public void setBufferAllocator(BufferAllocator allocator) { + // TODO Auto-generated method stub + + } + + } + + @Test + public void testCompare() { + + List sessionList = new ArrayList(); + for (int i = 0; i < 100; i++) { + sessionList.add(new MockSession(i)); + } + Collections.sort(sessionList, new IndexMemcachedSessionComparator()); + + for (int i = 0; i < sessionList.size(); i++) { + if (i < sessionList.size() - 1) { + int next = i + 1; + MockSession nextSession = (MockSession) sessionList.get(next); + MockSession currentSession = (MockSession) sessionList.get(i); + assertTrue(currentSession.getOrder() < nextSession.getOrder()); + } + } + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorGwhalinMemcachedJavaClientUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorGwhalinMemcachedJavaClientUnitTest.java new file mode 100644 index 0000000..a6e4792 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorGwhalinMemcachedJavaClientUnitTest.java @@ -0,0 +1,101 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.test.unittest.MockMemcachedSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertSame; + +public class KetamaMemcachedSessionLocatorGwhalinMemcachedJavaClientUnitTest + extends AbstractMemcachedSessionLocatorUnitTest { + + @BeforeEach + public void setUp() { + this.locator = new KetamaMemcachedSessionLocator(HashAlgorithm.KETAMA_HASH, false, true); + } + + @Test + public void testGetSessionByKey_MoreSessions() { + MockMemcachedSession session1 = new MockMemcachedSession(8080); + MockMemcachedSession session2 = new MockMemcachedSession(8081); + MockMemcachedSession session3 = new MockMemcachedSession(8082); + System.err.print(session1.getInetSocketAddressWrapper().getRemoteAddressStr()); + + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session1, this.locator.getSessionByKey("a3")); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session1, this.locator.getSessionByKey("a3")); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session1, this.locator.getSessionByKey("a3")); + + } + + @Test + public void testGetSessionByKey_MoreSessions_OneClosed() { + MockMemcachedSession session1 = new MockMemcachedSession(8080); + MockMemcachedSession session2 = new MockMemcachedSession(8081); + session1.close(); + MockMemcachedSession session3 = new MockMemcachedSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + } + + @Test + public void testGetSessionByKey_MoreSessions_OneClosed_FailureMode() { + this.locator.setFailureMode(true); + MockMemcachedSession session1 = new MockMemcachedSession(8080); + MockMemcachedSession session2 = new MockMemcachedSession(8081); + session1.close(); + MockMemcachedSession session3 = new MockMemcachedSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session1, this.locator.getSessionByKey("a3")); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session1, this.locator.getSessionByKey("a3")); + + assertSame(session2, this.locator.getSessionByKey("a1")); + assertSame(session3, this.locator.getSessionByKey("a2")); + assertSame(session1, this.locator.getSessionByKey("a3")); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorNginxUpstreamConsistentUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorNginxUpstreamConsistentUnitTest.java new file mode 100644 index 0000000..f1b5de8 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorNginxUpstreamConsistentUnitTest.java @@ -0,0 +1,234 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.CodecFactory; +import com.google.code.yanf4j.core.Handler; +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +public class KetamaMemcachedSessionLocatorNginxUpstreamConsistentUnitTest + extends AbstractMemcachedSessionLocatorUnitTest { + + static private class MockSession implements Session { + + private boolean closed = false; + private final String host; + private final int port; + + public MockSession(String host, int port) { + this.host = host; + this.port = port; + } + + public void start() { + } + + public void write(Object packet) { + } + + public boolean isClosed() { + return this.closed; + } + + public void close() { + this.closed = true; + } + + public InetSocketAddress getRemoteSocketAddress() { + return new InetSocketAddress(this.host, this.port); + } + + public InetAddress getLocalAddress() { + return null; + } + + public boolean isUseBlockingWrite() { + return false; + } + + public void setUseBlockingWrite(boolean useBlockingWrite) { + } + + public boolean isUseBlockingRead() { + return false; + } + + public void setUseBlockingRead(boolean useBlockingRead) { + } + + public void flush() { + } + + public boolean isExpired() { + return false; + } + + public boolean isIdle() { + return false; + } + + public CodecFactory.Encoder getEncoder() { + return null; + } + + public void setEncoder(CodecFactory.Encoder encoder) { + } + + public CodecFactory.Decoder getDecoder() { + return null; + } + + public void setDecoder(CodecFactory.Decoder decoder) { + } + + public boolean isHandleReadWriteConcurrently() { + return false; + } + + public void setHandleReadWriteConcurrently(boolean handleReadWriteConcurrently) { + } + + public ByteOrder getReadBufferByteOrder() { + return null; + } + + public void setReadBufferByteOrder(ByteOrder readBufferByteOrder) { + } + + public void setAttribute(String key, Object value) { + } + + public void removeAttribute(String key) { + } + + public Object getAttribute(String key) { + return null; + } + + public void clearAttributes() { + } + + public long getScheduleWritenBytes() { + return 0; + } + + public long getLastOperationTimeStamp() { + return 0; + } + + public boolean isLoopbackConnection() { + return false; + } + + public long getSessionIdleTimeout() { + return 0; + } + + public void setSessionIdleTimeout(long sessionIdleTimeout) { + } + + public long getSessionTimeout() { + return 0; + } + + public void setSessionTimeout(long sessionTimeout) { + } + + public Object setAttributeIfAbsent(String key, Object value) { + return null; + } + + public Handler getHandler() { + return null; + } + } + + @BeforeEach + public void setUp() { + this.locator = new KetamaMemcachedSessionLocator(); + } + + @Test + public void testSessionKey_CompatibleWithNginxUpstreamConsistent() { + + this.locator = new KetamaMemcachedSessionLocator(true); + + MockSession session1 = new MockSession("127.0.0.1", 11211); + MockSession session2 = new MockSession("127.0.0.1", 11212); + MockSession session3 = new MockSession("127.0.0.1", 11213); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertEquals(session1.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1-0").getRemoteSocketAddress()); + assertEquals(session1.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1-1").getRemoteSocketAddress()); + assertEquals(session1.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1-39").getRemoteSocketAddress()); + + assertEquals(session2.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1:11212-0").getRemoteSocketAddress()); + assertEquals(session2.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1:11212-1").getRemoteSocketAddress()); + assertEquals(session2.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1:11212-39").getRemoteSocketAddress()); + + assertEquals(session3.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1:11213-0").getRemoteSocketAddress()); + assertEquals(session3.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1:11213-1").getRemoteSocketAddress()); + assertEquals(session3.getRemoteSocketAddress(), + this.locator.getSessionByKey("127.0.0.1:11213-39").getRemoteSocketAddress()); + + } + + @Test + public void testSessionKey_CompatibleWithNginxUpstreamConsistent_DefaultPort() { + + this.locator = new KetamaMemcachedSessionLocator(true); + + MockSession session1 = new MockSession("192.168.1.1", 11211); + MockSession session2 = new MockSession("192.168.1.2", 11211); + MockSession session3 = new MockSession("192.168.1.3", 11211); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertEquals(session1.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.1-0").getRemoteSocketAddress()); + assertEquals(session1.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.1-1").getRemoteSocketAddress()); + assertEquals(session1.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.1-39").getRemoteSocketAddress()); + + assertEquals(session2.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.2-0").getRemoteSocketAddress()); + assertEquals(session2.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.2-1").getRemoteSocketAddress()); + assertEquals(session2.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.2-39").getRemoteSocketAddress()); + + assertEquals(session3.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.3-0").getRemoteSocketAddress()); + assertEquals(session3.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.3-1").getRemoteSocketAddress()); + assertEquals(session3.getRemoteSocketAddress(), + this.locator.getSessionByKey("192.168.1.3-39").getRemoteSocketAddress()); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorUnitTest.java new file mode 100644 index 0000000..140cfa8 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/KetamaMemcachedSessionLocatorUnitTest.java @@ -0,0 +1,98 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.test.unittest.MockSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertSame; + + +public class KetamaMemcachedSessionLocatorUnitTest extends AbstractMemcachedSessionLocatorUnitTest { + + @BeforeEach + public void setUp() { + this.locator = new KetamaMemcachedSessionLocator(); + } + + @Test + public void testGetSessionByKey_MoreSessions() { + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session1, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session1, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session1, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + } + + @Test + public void testGetSessionByKey_MoreSessions_OneClosed() { + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + session1.close(); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session2, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session2, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session2, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + } + + @Test + public void testGetSessionByKey_MoreSessions_OneClosed_FailureMode() { + this.locator.setFailureMode(true); + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8081); + session1.close(); + MockSession session3 = new MockSession(8082); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session1, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session1, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + + assertSame(session3, this.locator.getSessionByKey("a1")); + assertSame(session1, this.locator.getSessionByKey("a2")); + assertSame(session2, this.locator.getSessionByKey("a3")); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/MemcachedClientStateListenerIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/MemcachedClientStateListenerIT.java new file mode 100644 index 0000000..203c9ec --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/MemcachedClientStateListenerIT.java @@ -0,0 +1,104 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.util.ResourcesUtils; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientBuilder; +import net.rubyeye.xmemcached.XMemcachedClientBuilder; +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MemcachedClientStateListenerIT { + + MemcachedClient memcachedClient; + MockMemcachedClientStateListener listener; + + @BeforeEach + protected void setUp() throws Exception { + MemcachedClientBuilder builder = new XMemcachedClientBuilder(); + listener = new MockMemcachedClientStateListener(); + builder.addStateListener(listener); + memcachedClient = builder.build(); + } + + @Test + public void testStarted() { + assertEquals(1, listener.getNum()); + } + + @Test + public void testShutDown() throws Exception { + memcachedClient.shutdown(); + assertEquals(2, listener.getNum()); + } + + @Test + public void testRemoveListener() throws Exception { + assertEquals(1, listener.getNum()); + memcachedClient.removeStateListener(this.listener); + assertEquals(0, memcachedClient.getStateListeners().size()); + memcachedClient.shutdown(); + assertEquals(1, listener.getNum()); + } + + @Test + public void testAddListener() { + assertEquals(1, memcachedClient.getStateListeners().size()); + memcachedClient.addStateListener(listener); + assertEquals(2, memcachedClient.getStateListeners().size()); + memcachedClient.addStateListener(listener); + assertEquals(3, memcachedClient.getStateListeners().size()); + + memcachedClient.removeStateListener(this.listener); + assertEquals(0, memcachedClient.getStateListeners().size()); + } + + @Test + public void testConnected() throws Exception { + Properties properties = ResourcesUtils.getResourceAsProperties("test.properties"); + String serversString = properties.getProperty("test.memcached.servers"); + List list = AddrUtil.getAddresses(serversString); + memcachedClient.addServer(serversString); + synchronized (this) { + while (memcachedClient.getAvaliableServers().size() < list.size()) + wait(1000); + } + assertEquals(1 + memcachedClient.getAvaliableServers().size(), listener.getNum()); + } + + @Test + public void testDisconnected() throws Exception { + Properties properties = ResourcesUtils.getResourceAsProperties("test.properties"); + String serversString = properties.getProperty("test.memcached.servers"); + List list = AddrUtil.getAddresses(serversString); + memcachedClient.addServer(serversString); + synchronized (this) { + while (memcachedClient.getAvaliableServers().size() < list.size()) + wait(1000); + } + int serverCount = memcachedClient.getAvaliableServers().size(); + Thread.sleep(2000); + memcachedClient.shutdown(); + synchronized (this) { + + while (memcachedClient.getAvaliableServers().size() > 0) { + // System.out.println(memcachedClient.getAvaliableServers().size()); + wait(1000); + } + } + assertEquals(2 + 2 * serverCount, listener.getNum()); + } + + @AfterEach + protected void tearDown() throws Exception { + memcachedClient.shutdown(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/MockMemcachedClientStateListener.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/MockMemcachedClientStateListener.java new file mode 100644 index 0000000..8f0ce40 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/MockMemcachedClientStateListener.java @@ -0,0 +1,48 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicInteger; +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.MemcachedClientStateListener; + +public class MockMemcachedClientStateListener implements MemcachedClientStateListener { + AtomicInteger num; + + public MockMemcachedClientStateListener() { + this.num = new AtomicInteger(0); + } + + public void onConnected(MemcachedClient memcachedClient, InetSocketAddress inetSocketAddress) { + this.num.incrementAndGet(); + System.out.println( + "Connected to " + inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + } + + public void onDisconnected(MemcachedClient memcachedClient, InetSocketAddress inetSocketAddress) { + this.num.incrementAndGet(); + System.out.println( + "Disconnected to " + inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + } + + public void onException(MemcachedClient memcachedClient, Throwable throwable) { + this.num.incrementAndGet(); + System.out.println("Client onException"); + + } + + public int getNum() { + return this.num.get(); + } + + public void onShutDown(MemcachedClient memcachedClient) { + this.num.incrementAndGet(); + System.out.println("Client shutdown"); + + } + + public void onStarted(MemcachedClient memcachedClient) { + this.num.incrementAndGet(); + System.out.println("Client started"); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/OptimizerTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/OptimizerTest.java new file mode 100644 index 0000000..976fc7b --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/OptimizerTest.java @@ -0,0 +1,320 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.impl.FutureImpl; +import com.google.code.yanf4j.util.LinkedTransferQueue; +import com.google.code.yanf4j.util.SimpleQueue; +import net.rubyeye.xmemcached.CommandFactory; +import net.rubyeye.xmemcached.buffer.SimpleBufferAllocator; +import net.rubyeye.xmemcached.command.BinaryCommandFactory; +import net.rubyeye.xmemcached.command.Command; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.TextCommandFactory; +import net.rubyeye.xmemcached.command.binary.BinarySetMultiCommand; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; +import net.rubyeye.xmemcached.impl.Optimizer; +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.utils.Protocol; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +@SuppressWarnings("unchecked") +public class OptimizerTest { + Optimizer optimiezer; + + Queue writeQueue; + BlockingQueue executingCmds; + + Command currentCmd; + + private CommandFactory commandFactory; + + @BeforeEach + protected void setUp() throws Exception { + this.optimiezer = new Optimizer(Protocol.Text); + this.commandFactory = new TextCommandFactory(); + this.optimiezer.setBufferAllocator(new SimpleBufferAllocator()); + this.writeQueue = new LinkedTransferQueue(); + this.executingCmds = new LinkedTransferQueue(); + for (int i = 0; i < 10; i++) { + Command cmd = this.commandFactory.createGetCommand(String.valueOf(i), + String.valueOf(i).getBytes(), CommandType.GET_ONE, null); + cmd.encode(); + this.writeQueue.add(cmd); + cmd.setWriteFuture(new FutureImpl()); + } + this.currentCmd = (Command) this.writeQueue.poll(); + this.currentCmd.encode(); + } + + @Test + public void testOptimiezeSetLimitBuffer() { + this.optimiezer = new Optimizer(Protocol.Binary); + this.commandFactory = new BinaryCommandFactory(); + this.writeQueue = new LinkedTransferQueue(); + this.executingCmds = new LinkedTransferQueue(); + SerializingTranscoder transcoder = new SerializingTranscoder(); + int oneBufferSize = 0; + for (int i = 0; i < 10; i++) { + Command cmd = this.commandFactory.createSetCommand(String.valueOf(i), + String.valueOf(i).getBytes(), 0, i, false, transcoder); + cmd.encode(); + this.writeQueue.add(cmd); + oneBufferSize = cmd.getIoBuffer().remaining(); + cmd.setWriteFuture(new FutureImpl()); + } + this.currentCmd = (Command) this.writeQueue.poll(); + this.currentCmd.encode(); + + int limit = 100; + BinarySetMultiCommand optimiezedCommand = (BinarySetMultiCommand) this.optimiezer + .optimiezeSet(this.writeQueue, this.executingCmds, this.currentCmd, limit); + assertEquals(optimiezedCommand.getMergeCount(), Math.round((double) limit / oneBufferSize)); + } + + @Test + public void testOptimiezeSetAllBuffers() { + this.optimiezer = new Optimizer(Protocol.Binary); + this.commandFactory = new BinaryCommandFactory(); + this.writeQueue = new LinkedTransferQueue(); + this.executingCmds = new LinkedTransferQueue(); + SerializingTranscoder transcoder = new SerializingTranscoder(); + for (int i = 0; i < 10; i++) { + Command cmd = this.commandFactory.createSetCommand(String.valueOf(i), + String.valueOf(i).getBytes(), 0, i, false, transcoder); + cmd.encode(); + this.writeQueue.add(cmd); + cmd.setWriteFuture(new FutureImpl()); + } + this.currentCmd = (Command) this.writeQueue.poll(); + this.currentCmd.encode(); + + BinarySetMultiCommand optimiezedCommand = (BinarySetMultiCommand) this.optimiezer + .optimiezeSet(this.writeQueue, this.executingCmds, this.currentCmd, Integer.MAX_VALUE); + assertEquals(optimiezedCommand.getMergeCount(), 10); + } + + @Test + public void testOptimiezeGet() { + + TextGetOneCommand optimiezeCommand = (TextGetOneCommand) this.optimiezer + .optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + + assertEquals(10, optimiezeCommand.getMergeCommands().size()); + assertEquals(10, optimiezeCommand.getMergeCount()); + assertEquals(0, this.writeQueue.size()); + assertNull(this.writeQueue.peek()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertEquals(10, optimiezeCommand.getMergeCount()); + assertEquals("get 0 1 2 3 4 5 6 7 8 9\r\n", + new String(optimiezeCommand.getIoBuffer().buf().array())); + } + + @Test + public void testOptimiezeGetWithSameKey() { + this.writeQueue.clear(); + Queue localQueue = new SimpleQueue(); + for (int i = 0; i < 10; i++) { + Command cmd = this.commandFactory.createGetCommand(String.valueOf(0), + String.valueOf(0).getBytes(), CommandType.GET_ONE, null); + cmd.encode(); + this.writeQueue.add(cmd); + cmd.setWriteFuture(new FutureImpl()); + localQueue.add(cmd); + } + TextGetOneCommand optimiezeCommand = (TextGetOneCommand) this.optimiezer + .optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + + assertEquals(1, optimiezeCommand.getMergeCommands().size()); + assertEquals(11, optimiezeCommand.getMergeCount()); + assertEquals(0, this.writeQueue.size()); + assertNull(this.writeQueue.peek()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertEquals(11, optimiezeCommand.getMergeCount()); + assertEquals("get 0\r\n", new String(optimiezeCommand.getIoBuffer().buf().array())); + optimiezeCommand.decode(null, ByteBuffer.wrap("VALUE 0 0 2\r\n10\r\n".getBytes())); + + assertEquals(0, this.currentCmd.getLatch().getCount()); + Transcoder transcoder = new SerializingTranscoder(); + assertEquals("10", transcoder.decode((CachedData) this.currentCmd.getResult())); + for (Command cmd : localQueue) { + assertEquals(0, cmd.getLatch().getCount()); + assertEquals("10", transcoder.decode((CachedData) this.currentCmd.getResult())); + } + assertEquals(0, optimiezeCommand.getMergeCount()); + } + + @Test + public void testMergeFactorDecrease() { + this.optimiezer.setMergeFactor(5); + TextGetOneCommand optimiezeCommand = (TextGetOneCommand) this.optimiezer + .optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + + assertEquals(5, optimiezeCommand.getMergeCommands().size()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertEquals(5, optimiezeCommand.getMergeCount()); + assertEquals("get 0 1 2 3 4\r\n", new String(optimiezeCommand.getIoBuffer().buf().array())); + assertEquals(5, this.writeQueue.size()); // remain five commands + } + + @Test + public void testMergeFactorEqualsZero() { + this.optimiezer.setMergeFactor(0); + TextGetOneCommand optimiezeCommand = (TextGetOneCommand) this.optimiezer + .optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + optimiezeCommand.encode(); + + assertNull(optimiezeCommand.getMergeCommands()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertNull(optimiezeCommand.getMergeCommands()); + assertEquals(-1, optimiezeCommand.getMergeCount()); + assertEquals("get 0\r\n", new String(optimiezeCommand.getIoBuffer().buf().array())); + assertEquals(9, this.writeQueue.size()); + assertSame(this.currentCmd, optimiezeCommand); + } + + @Test + public void testDisableMergeGet() { + this.optimiezer.setOptimizeGet(false); // disable merge get + TextGetOneCommand optimiezeCommand = (TextGetOneCommand) this.optimiezer + .optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + optimiezeCommand.encode(); + assertNull(optimiezeCommand.getMergeCommands()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertNull(optimiezeCommand.getMergeCommands()); + assertEquals(-1, optimiezeCommand.getMergeCount()); + assertEquals("get 0\r\n", new String(optimiezeCommand.getIoBuffer().buf().array())); + assertEquals(9, this.writeQueue.size()); + assertSame(this.currentCmd, optimiezeCommand); + } + + @Test + public void testMergeDifferenceCommands() { + this.writeQueue.clear(); + // send five get operation,include current command + for (int i = 0; i < 5; i++) { + Command cmd = this.commandFactory.createGetCommand(String.valueOf(i), + String.valueOf(i).getBytes(), CommandType.GET_ONE, null); + this.writeQueue.add(cmd); + cmd.setWriteFuture(new FutureImpl()); + } + // send five delete operation + for (int i = 5; i < 10; i++) { + Command cmd = this.commandFactory.createDeleteCommand(String.valueOf(i), + String.valueOf(i).getBytes(), 0, 0, false); + this.writeQueue.add(cmd); + cmd.setWriteFuture(new FutureImpl()); + } + // merge five get commands at most + TextGetOneCommand optimiezeCommand = (TextGetOneCommand) this.optimiezer + .optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + + assertEquals(5, optimiezeCommand.getMergeCommands().size()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertEquals(6, optimiezeCommand.getMergeCount()); + assertEquals("get 0 1 2 3 4\r\n", new String(optimiezeCommand.getIoBuffer().buf().array())); + assertEquals(5, this.writeQueue.size()); // remain five commands + + } + + @Test + public void testMergeGetCommandsWithEmptyWriteQueue() { + this.writeQueue.clear(); + Command optimiezeCommand = + this.optimiezer.optimiezeGet(this.writeQueue, this.executingCmds, this.currentCmd); + optimiezeCommand.encode(); + ByteBuffer mergeBuffer = optimiezeCommand.getIoBuffer().buf(); + assertSame(this.currentCmd, optimiezeCommand); + assertTrue(mergeBuffer.remaining() < 100); + assertSame(mergeBuffer, this.currentCmd.getIoBuffer().buf()); + assertEquals(0, this.writeQueue.size()); + assertEquals(-1, optimiezeCommand.getMergeCount()); + assertEquals("get 0\r\n", new String(mergeBuffer.array())); + } + + @Test + public void testMergeLimitBuffer() { + // set send buffer size to 30,merge four commands at most + this.optimiezer.setOptimizeMergeBuffer(true); + Command optimiezeCommand = this.optimiezer.optimiezeMergeBuffer(this.currentCmd, + this.writeQueue, this.executingCmds, 54); + assertNotSame(this.currentCmd, optimiezeCommand); + ByteBuffer mergeBuffer = optimiezeCommand.getIoBuffer().buf(); + assertEquals(0, this.writeQueue.size()); + assertSame(CommandType.GET_ONE, optimiezeCommand.getCommandType()); + assertEquals("get 0\r\nget 1 2 3 4 5 6 7 8 9\r\n", new String(mergeBuffer.array())); // current + // command + // at last + } + + @Test + public void testMergeAllBuffer() { + // merge 10 buffers + this.optimiezer.setOptimizeMergeBuffer(true); + Command optimiezeCommand = this.optimiezer.optimiezeMergeBuffer(this.currentCmd, + this.writeQueue, this.executingCmds, 100); + ByteBuffer mergeBuffer = optimiezeCommand.getIoBuffer().buf(); + assertNotSame(this.currentCmd, optimiezeCommand); + assertTrue(mergeBuffer.remaining() < 100); + assertEquals(0, this.writeQueue.size()); + assertEquals("get 0\r\nget 1 2 3 4 5 6 7 8 9\r\n", new String(mergeBuffer.array())); // current + // command + // at last + } + + @Test + public void testMergeBufferWithEmptyWriteQueue() { + this.writeQueue.clear(); + Command optimiezeCommand = this.optimiezer.optimiezeMergeBuffer(this.currentCmd, + this.writeQueue, this.executingCmds, 100); + ByteBuffer mergeBuffer = optimiezeCommand.getIoBuffer().buf(); + assertSame(this.currentCmd, optimiezeCommand); + assertTrue(mergeBuffer.remaining() < 100); + assertSame(mergeBuffer, this.currentCmd.getIoBuffer().buf()); + assertEquals(0, this.writeQueue.size()); + assertEquals("get 0\r\n", new String(mergeBuffer.array())); + + } + + @Test + public void testOptimieze() { + this.optimiezer.setOptimizeMergeBuffer(true); + for (int i = 0; i < 10; i++) { + Command deleteCommand = this.commandFactory.createDeleteCommand(String.valueOf(i), + String.valueOf(i).getBytes(), 0, 0, false); + deleteCommand.encode(); + this.writeQueue.add(deleteCommand); + deleteCommand.setWriteFuture(new FutureImpl()); + } + Command optimiezeCommand = + this.optimiezer.optimize(this.currentCmd, this.writeQueue, this.executingCmds, 16 * 1024); + ByteBuffer mergeBuffer = optimiezeCommand.getIoBuffer().buf(); + StringBuilder sb = new StringBuilder("get "); + for (int i = 0; i < 10; i++) { + if (i != 9) { + sb.append(String.valueOf(i) + " "); + } else { + sb.append(String.valueOf(i)); + } + } + sb.append("\r\n"); + for (int i = 0; i < 10; i++) { + sb.append("delete " + String.valueOf(i) + "\r\n"); + } + assertEquals(sb.toString(), new String(mergeBuffer.array())); + assertEquals(0, this.writeQueue.size()); + assertNull(this.writeQueue.peek()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/RoundRobinMemcachedSessionLocatorUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/RoundRobinMemcachedSessionLocatorUnitTest.java new file mode 100644 index 0000000..fd8b16b --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/RoundRobinMemcachedSessionLocatorUnitTest.java @@ -0,0 +1,42 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.impl.RoundRobinMemcachedSessionLocator; +import net.rubyeye.xmemcached.test.unittest.MockSession; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertSame; + + +public class RoundRobinMemcachedSessionLocatorUnitTest + extends AbstractMemcachedSessionLocatorUnitTest { + @BeforeEach + public void setUp() { + this.locator = new RoundRobinMemcachedSessionLocator(); + } + + @Test + public void testGetSessionByKey() { + MockSession session1 = new MockSession(8080); + MockSession session2 = new MockSession(8080); + MockSession session3 = new MockSession(8080); + List list = new ArrayList(); + list.add(session1); + list.add(session2); + list.add(session3); + this.locator.updateSessions(list); + + assertSame(session1, this.locator.getSessionByKey("a")); + assertSame(session2, this.locator.getSessionByKey("b")); + assertSame(session3, this.locator.getSessionByKey("c")); + + assertSame(session1, this.locator.getSessionByKey("a")); + assertSame(session2, this.locator.getSessionByKey("b")); + assertSame(session3, this.locator.getSessionByKey("c")); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/SessionLocatorTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/SessionLocatorTest.java new file mode 100644 index 0000000..bf9f376 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/impl/SessionLocatorTest.java @@ -0,0 +1,71 @@ +package net.rubyeye.xmemcached.test.unittest.impl; + +import com.google.code.yanf4j.core.Session; +import net.rubyeye.xmemcached.HashAlgorithm; +import net.rubyeye.xmemcached.MemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.ArrayMemcachedSessionLocator; +import net.rubyeye.xmemcached.impl.KetamaMemcachedSessionLocator; +import net.rubyeye.xmemcached.test.unittest.MockSession; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class SessionLocatorTest { + MemcachedSessionLocator sessionLocator; + + @Test + public void testArraySessionLocator() { + sessionLocator = new ArrayMemcachedSessionLocator(); + + List sessions = new ArrayList(); + for (int i = 8080; i < 8100; i++) { + sessions.add(new MockSession(i)); + } + sessionLocator.updateSessions(sessions); + for (int i = 1; i <= 10; i++) { + String key = String.valueOf(i); + int mod = key.hashCode() % sessions.size(); + assertSame(sessions.get(mod), sessionLocator.getSessionByKey(key)); + } + + String key = "test"; + int oldIndex = key.hashCode() % sessions.size(); + Session oldSession = sessions.get(oldIndex); + + assertSame(oldSession, sessionLocator.getSessionByKey(key)); + // close old session + oldSession.close(); + assertNotSame(oldSession, sessionLocator.getSessionByKey(key)); + // use next + assertSame(sessions.get(oldIndex + 1), sessionLocator.getSessionByKey(key)); + sessions = new ArrayList(); + sessionLocator.updateSessions(sessions); + assertNull(sessionLocator.getSessionByKey(key)); + } + + @Test + public void testKetamaMemcachedSessionLocator() { + sessionLocator = new KetamaMemcachedSessionLocator(HashAlgorithm.NATIVE_HASH); + List sessions = new ArrayList(); + for (int i = 8080; i < 8100; i++) { + sessions.add(new MockSession(i)); + } + sessionLocator.updateSessions(sessions); + for (int i = 1; i <= 10; i++) { + String key = String.valueOf(i); + + Session session = sessionLocator.getSessionByKey(key); + assertSame(((KetamaMemcachedSessionLocator) sessionLocator).getSessionByHash(key.hashCode()), + session); + assertSame(sessionLocator.getSessionByKey(key), session); + assertSame(sessionLocator.getSessionByKey(key), session); + } + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockDecodeTimeoutBinaryGetOneCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockDecodeTimeoutBinaryGetOneCommand.java new file mode 100644 index 0000000..5361b74 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockDecodeTimeoutBinaryGetOneCommand.java @@ -0,0 +1,32 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; +import net.rubyeye.xmemcached.command.binary.OpCode; +import net.rubyeye.xmemcached.impl.MemcachedTCPSession; + +public class MockDecodeTimeoutBinaryGetOneCommand extends BinaryGetCommand { + private long sleepTime; + + public MockDecodeTimeoutBinaryGetOneCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, OpCode opCode, boolean noreply, long sleepTime) { + super(key, keyBytes, cmdType, latch, opCode, noreply); + this.sleepTime = sleepTime; + // TODO Auto-generated constructor stub + } + + @Override + public boolean decode(MemcachedTCPSession session, ByteBuffer buffer) { + // TODO Auto-generated method stub + try { + Thread.sleep(this.sleepTime); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return super.decode(session, buffer); + + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockDecodeTimeoutTextGetOneCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockDecodeTimeoutTextGetOneCommand.java new file mode 100644 index 0000000..15cddf2 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockDecodeTimeoutTextGetOneCommand.java @@ -0,0 +1,28 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; + +public class MockDecodeTimeoutTextGetOneCommand extends TextGetOneCommand { + + private long sleepTime; + + public MockDecodeTimeoutTextGetOneCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, long sleepTime) { + super(key, keyBytes, cmdType, latch); + this.sleepTime = sleepTime; + } + + @Override + public void dispatch() { + // Sleep,then operation is timeout + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + super.dispatch(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockEncodeTimeoutBinaryGetCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockEncodeTimeoutBinaryGetCommand.java new file mode 100644 index 0000000..e3733f6 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockEncodeTimeoutBinaryGetCommand.java @@ -0,0 +1,27 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; +import net.rubyeye.xmemcached.command.binary.OpCode; + +public class MockEncodeTimeoutBinaryGetCommand extends BinaryGetCommand { + private long sleepTime; + + public MockEncodeTimeoutBinaryGetCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, OpCode opCode, boolean noreply, long sleepTime) { + super(key, keyBytes, cmdType, latch, opCode, noreply); + this.sleepTime = sleepTime; + } + + @Override + public void encode() { + try { + Thread.sleep(this.sleepTime); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + super.encode(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockEncodeTimeoutTextGetOneCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockEncodeTimeoutTextGetOneCommand.java new file mode 100644 index 0000000..4c2e5af --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockEncodeTimeoutTextGetOneCommand.java @@ -0,0 +1,28 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +import java.nio.ByteBuffer; +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; + +public class MockEncodeTimeoutTextGetOneCommand extends TextGetOneCommand { + + private long sleepTime; + + public MockEncodeTimeoutTextGetOneCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, long sleepTime) { + super(key, keyBytes, cmdType, latch); + this.sleepTime = sleepTime; + } + + public void encode() { + // Sleep,then encode timeout + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + super.encode(); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorBinaryGetOneCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorBinaryGetOneCommand.java new file mode 100644 index 0000000..a9f31ce --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorBinaryGetOneCommand.java @@ -0,0 +1,30 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; +import net.rubyeye.xmemcached.command.binary.OpCode; + +public class MockErrorBinaryGetOneCommand extends BinaryGetCommand implements MockErrorCommand { + + private boolean decode; + + public MockErrorBinaryGetOneCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch, OpCode opCode, boolean noreply) { + super(key, keyBytes, cmdType, latch, opCode, noreply); + // TODO Auto-generated constructor stub + } + + @Override + protected boolean finish() { + this.decode = true; + super.finish(); + decodeError(); + return true; + } + + public boolean isDecoded() { + return this.decode; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorCommand.java new file mode 100644 index 0000000..231db72 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorCommand.java @@ -0,0 +1,6 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +public interface MockErrorCommand { + public boolean isDecoded(); + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorTextGetOneCommand.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorTextGetOneCommand.java new file mode 100644 index 0000000..6fcabe1 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/mock/MockErrorTextGetOneCommand.java @@ -0,0 +1,32 @@ +package net.rubyeye.xmemcached.test.unittest.mock; + +import java.util.concurrent.CountDownLatch; +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.command.text.TextGetOneCommand; + +public class MockErrorTextGetOneCommand extends TextGetOneCommand implements MockErrorCommand { + + private volatile boolean decoded; + + public MockErrorTextGetOneCommand(String key, byte[] keyBytes, CommandType cmdType, + CountDownLatch latch) { + super(key, keyBytes, cmdType, latch); + + } + + @Override + public void dispatch() { + this.decoded = true; + countDownLatch(); + decodeError(); + } + + public boolean isDecoded() { + return this.decoded; + } + + public void setDecoded(boolean decoded) { + this.decoded = decoded; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/MemcachedClientHolderUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/MemcachedClientHolderUnitTest.java new file mode 100644 index 0000000..d538973 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/MemcachedClientHolderUnitTest.java @@ -0,0 +1,46 @@ +package net.rubyeye.xmemcached.test.unittest.monitor; + +import net.rubyeye.xmemcached.monitor.MemcachedClientNameHolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class MemcachedClientHolderUnitTest { + + @BeforeEach + @AfterEach + public void setUpTearDown() { + MemcachedClientNameHolder.clear(); + + } + + @Test + public void setGetClearName() { + assertNull(MemcachedClientNameHolder.getName()); + + MemcachedClientNameHolder.setName("MemcachedClient-1"); + + assertEquals("MemcachedClient-1", MemcachedClientNameHolder.getName()); + + MemcachedClientNameHolder.clear(); + assertNull(MemcachedClientNameHolder.getName()); + + } + + @Test + public void setTwice() { + assertNull(MemcachedClientNameHolder.getName()); + + MemcachedClientNameHolder.setName("MemcachedClient-1"); + + assertEquals("MemcachedClient-1", MemcachedClientNameHolder.getName()); + MemcachedClientNameHolder.setName("MemcachedClient-2"); + assertEquals("MemcachedClient-2", MemcachedClientNameHolder.getName()); + MemcachedClientNameHolder.clear(); + assertNull(MemcachedClientNameHolder.getName()); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/Mock.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/Mock.java new file mode 100644 index 0000000..2fc7bee --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/Mock.java @@ -0,0 +1,9 @@ +package net.rubyeye.xmemcached.test.unittest.monitor; + +public class Mock implements MockMBean { + + public String say(String name) { + return "hello," + name; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/MockMBean.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/MockMBean.java new file mode 100644 index 0000000..e88ed5c --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/MockMBean.java @@ -0,0 +1,5 @@ +package net.rubyeye.xmemcached.test.unittest.monitor; + +public interface MockMBean { + public String say(String name); +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/StatisticsHandlerUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/StatisticsHandlerUnitTest.java new file mode 100644 index 0000000..b242911 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/StatisticsHandlerUnitTest.java @@ -0,0 +1,47 @@ +package net.rubyeye.xmemcached.test.unittest.monitor; + +import net.rubyeye.xmemcached.command.CommandType; +import net.rubyeye.xmemcached.monitor.StatisticsHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StatisticsHandlerUnitTest { + StatisticsHandler handler; + + @BeforeEach + public void setUp() { + handler = new StatisticsHandler(); + handler.setStatistics(true); + + } + + @Test + public void testStatistics() { + Map map = new HashMap(); + long i = 0; + for (CommandType cmdType : CommandType.values()) { + map.put(cmdType, i++); + } + for (CommandType cmdType : CommandType.values()) { + for (int j = 0; j < map.get(cmdType); j++) { + handler.statistics(cmdType); + } + } + + assertEquals((long) map.get(CommandType.GET_MANY), (long) handler.getMultiGetCount()); + assertEquals((long) map.get(CommandType.GETS_MANY), (long) handler.getMultiGetsCount()); + assertEquals((long) map.get(CommandType.SET) + (long) map.get(CommandType.SET_MANY), + (long) handler.getSetCount()); + assertEquals((long) map.get(CommandType.ADD), (long) handler.getAddCount()); + assertEquals((long) map.get(CommandType.CAS), (long) handler.getCASCount()); + assertEquals((long) map.get(CommandType.REPLACE), (long) handler.getReplaceCount()); + assertEquals((long) map.get(CommandType.APPEND), (long) handler.getAppendCount()); + assertEquals((long) map.get(CommandType.PREPEND), (long) handler.getPrependCount()); + assertEquals((long) map.get(CommandType.DELETE), (long) handler.getDeleteCount()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/XMemcachedMBeanServerUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/XMemcachedMBeanServerUnitTest.java new file mode 100644 index 0000000..16628a2 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/monitor/XMemcachedMBeanServerUnitTest.java @@ -0,0 +1,40 @@ +package net.rubyeye.xmemcached.test.unittest.monitor; + +import net.rubyeye.xmemcached.monitor.Constants; +import net.rubyeye.xmemcached.monitor.XMemcachedMbeanServer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class XMemcachedMBeanServerUnitTest { + Mock mock; + + @BeforeEach + public void setUp() { + System.setProperty(Constants.XMEMCACHED_JMX_ENABLE, "true"); + this.mock = new Mock(); + } + + @Test + public void testMBeanServer() throws Exception { + Method method = XMemcachedMbeanServer.getInstance().getClass().getDeclaredMethod("initialize", + new Class[]{}); + method.setAccessible(true); + method.invoke(XMemcachedMbeanServer.getInstance()); + + assertTrue(XMemcachedMbeanServer.getInstance().isActive()); + int oldCount = XMemcachedMbeanServer.getInstance().getMBeanCount(); + String name = + mock.getClass().getPackage().getName() + ":type=" + mock.getClass().getSimpleName(); + XMemcachedMbeanServer.getInstance().registMBean(mock, name); + assertEquals(oldCount + 1, XMemcachedMbeanServer.getInstance().getMBeanCount()); + assertTrue(XMemcachedMbeanServer.getInstance().isRegistered(name)); + XMemcachedMbeanServer.getInstance().shutdown(); + assertFalse(XMemcachedMbeanServer.getInstance().isActive()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/BaseSerializingTranscoderTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/BaseSerializingTranscoderTest.java new file mode 100644 index 0000000..3d1b72b --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/BaseSerializingTranscoderTest.java @@ -0,0 +1,154 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.BaseSerializingTranscoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.UnsupportedEncodingException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Base tests of the base serializing transcoder stuff. + */ +public class BaseSerializingTranscoderTest { + + private Exposer ex; + + @BeforeEach + protected void setUp() throws Exception { + ex = new Exposer(); + } + + @Test + public void testValidCharacterSet() { + ex.setCharset("KOI8"); + } + + @Test + public void testInvalidCharacterSet() { + try { + ex.setCharset("Dustin's Kick Ass Character Set"); + } catch (RuntimeException e) { + assertTrue(e.getCause() instanceof UnsupportedEncodingException); + } + } + + @Test + public void testCompressNull() { + try { + ex.compress(null); + fail("Expected an assertion error"); + } catch (NullPointerException e) { + // pass + } + } + + @Test + public void testDecodeStringNull() { + assertNull(ex.decodeString(null)); + } + + @Test + public void testDeserializeNull() { + assertNull(ex.deserialize(null)); + } + + @Test + public void testEncodeStringNull() { + try { + ex.encodeString(null); + fail("Expected an assertion error"); + } catch (NullPointerException e) { + // pass + } + } + + @Test + public void testSerializeNull() { + try { + ex.serialize(null); + fail("Expected an assertion error"); + } catch (NullPointerException e) { + // pass + } + } + + @Test + public void testDecompressNull() { + assertNull(ex.decompress(null)); + } + + @Test + public void testUndeserializable() throws Exception { + byte[] data = {-84, -19, 0, 5, 115, 114, 0, 4, 84, 101, 115, 116, 2, 61, 102, -87, -28, 17, 52, + 30, 2, 0, 1, 73, 0, 9, 115, 111, 109, 101, 116, 104, 105, 110, 103, 120, 112, 0, 0, 0, 5}; + assertNull(ex.deserialize(data)); + } + + @Test + public void testDeserializable() throws Exception { + byte[] data = {-84, -19, 0, 5, 116, 0, 5, 104, 101, 108, 108, 111}; + assertEquals("hello", ex.deserialize(data)); + } + + @Test + public void testBadCharsetDecode() { + ex.overrideCharsetSet("Some Crap"); + try { + ex.encodeString("Woo!"); + fail("Expected runtime exception"); + } catch (RuntimeException e) { + assertSame(UnsupportedEncodingException.class, e.getCause().getClass()); + } + } + + @Test + public void testBadCharsetEncode() { + ex.overrideCharsetSet("Some Crap"); + try { + ex.decodeString("Woo!".getBytes()); + fail("Expected runtime exception"); + } catch (RuntimeException e) { + assertSame(UnsupportedEncodingException.class, e.getCause().getClass()); + } + } + + // Expose the protected methods so I can test them. + static class Exposer extends BaseSerializingTranscoder { + + public void overrideCharsetSet(String to) { + charset = to; + } + + @Override + public String decodeString(byte[] data) { + return super.decodeString(data); + } + + @Override + public byte[] decompress(byte[] in) { + return super.decompress(in); + } + + @Override + public Object deserialize(byte[] in) { + return super.deserialize(in); + } + + @Override + public byte[] encodeString(String in) { + return super.encodeString(in); + } + + @Override + public byte[] serialize(Object o) { + return super.serialize(o); + } + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/BaseTranscoderCase.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/BaseTranscoderCase.java new file mode 100644 index 0000000..c66b0d4 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/BaseTranscoderCase.java @@ -0,0 +1,205 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Basic behavior validation for all transcoders that work with objects. + */ +public abstract class BaseTranscoderCase { + + private Transcoder tc; + + protected void setTranscoder(Transcoder t) { + assert t != null; + this.tc = t; + } + + protected Transcoder getTranscoder() { + return this.tc; + } + + @Test + public void testSomethingBigger() throws Exception { + Collection dates = new ArrayList(); + for (int i = 0; i < 1024; i++) { + dates.add(new Date()); + } + CachedData d = this.tc.encode(dates); + assertEquals(dates, this.tc.decode(d)); + } + + @Test + public void testDate() throws Exception { + Date d = new Date(); + CachedData cd = this.tc.encode(d); + assertEquals(d, this.tc.decode(cd)); + } + + @Test + public void testLong() throws Exception { + assertEquals(923L, this.tc.decode(this.tc.encode(923L))); + } + + @Test + public void testInt() throws Exception { + assertEquals(923, this.tc.decode(this.tc.encode(923))); + } + + @Test + public void testShort() throws Exception { + assertEquals((short) 923, this.tc.decode(this.tc.encode((short) 923))); + } + + @Test + public void testChar() throws Exception { + assertEquals('c', this.tc.decode(this.tc.encode('c'))); + } + + @Test + public void testBoolean() throws Exception { + assertSame(Boolean.TRUE, this.tc.decode(this.tc.encode(true))); + assertSame(Boolean.FALSE, this.tc.decode(this.tc.encode(false))); + } + + @Test + public void testByte() throws Exception { + assertEquals((byte) -127, this.tc.decode(this.tc.encode((byte) -127))); + } + + @Test + public void testCharacter() throws Exception { + assertEquals('c', this.tc.decode(this.tc.encode('c'))); + } + + @Test + public void testStringBuilder() throws Exception { + StringBuilder sb = new StringBuilder("test"); + StringBuilder sb2 = (StringBuilder) this.tc.decode(this.tc.encode(sb)); + assertEquals(sb.toString(), sb2.toString()); + } + + @Test + public void testStringBuffer() throws Exception { + StringBuffer sb = new StringBuffer("test"); + StringBuffer sb2 = (StringBuffer) this.tc.decode(this.tc.encode(sb)); + assertEquals(sb.toString(), sb2.toString()); + } + + private void assertFloat(float f) { + assertEquals(f, this.tc.decode(this.tc.encode(f))); + } + + @Test + public void testFloat() throws Exception { + assertFloat(0f); + assertFloat(Float.MIN_VALUE); + assertFloat(Float.MAX_VALUE); + assertFloat(3.14f); + assertFloat(-3.14f); + assertFloat(Float.NaN); + assertFloat(Float.POSITIVE_INFINITY); + assertFloat(Float.NEGATIVE_INFINITY); + } + + private void assertDouble(double d) { + assertEquals(d, this.tc.decode(this.tc.encode(d))); + } + + @Test + public void testDouble() throws Exception { + assertDouble(0d); + assertDouble(Double.MIN_VALUE); + assertDouble(Double.MAX_VALUE); + assertDouble(3.14d); + assertDouble(-3.14d); + assertDouble(Double.NaN); + assertDouble(Double.POSITIVE_INFINITY); + assertDouble(Double.NEGATIVE_INFINITY); + } + + private void assertLong(long l) { + CachedData encoded = this.tc.encode(l); + long decoded = (Long) this.tc.decode(encoded); + assertEquals(l, decoded); + } + + /* + * private void displayBytes(long l, byte[] encoded) { System.out.print(l + " ["); for(byte b : + * encoded) { System.out.print((b<0?256+b:b) + " "); } System.out.println("]"); } + */ + + @Test + public void testLongEncoding() throws Exception { + assertLong(Long.MIN_VALUE); + assertLong(1); + assertLong(23852); + assertLong(0L); + assertLong(-1); + assertLong(-23835); + assertLong(Long.MAX_VALUE); + } + + private void assertInt(int i) { + CachedData encoded = this.tc.encode(i); + int decoded = (Integer) this.tc.decode(encoded); + assertEquals(i, decoded); + } + + @Test + public void testIntEncoding() throws Exception { + assertInt(Integer.MIN_VALUE); + assertInt(83526); + assertInt(1); + assertInt(0); + assertInt(-1); + assertInt(-238526); + assertInt(Integer.MAX_VALUE); + } + + @Test + public void testBooleanEncoding() throws Exception { + assertTrue((Boolean) this.tc.decode(this.tc.encode(true))); + assertFalse((Boolean) this.tc.decode(this.tc.encode(false))); + } + + @Test + public void testByteArray() throws Exception { + byte[] a = {'a', 'b', 'c'}; + CachedData cd = this.tc.encode(a); + assertTrue(Arrays.equals(a, cd.getData())); + assertTrue(Arrays.equals(a, (byte[]) this.tc.decode(cd))); + } + + @Test + public void testStrings() throws Exception { + String s1 = "This is a simple test string."; + CachedData cd = this.tc.encode(s1); + assertEquals(getStringFlags(), cd.getFlag()); + assertEquals(s1, this.tc.decode(cd)); + } + + @Test + public void testUTF8String() throws Exception { + String s1 = "\u2013\u00f3\u2013\u00a5\u2014\u00c4\u2013\u221e\u2013" + + "\u2264\u2014\u00c5\u2014\u00c7\u2013\u2264\u2014\u00c9\u2013" + + "\u03c0, \u2013\u00ba\u2013\u220f\u2014\u00c4."; + CachedData cd = this.tc.encode(s1); + assertEquals(getStringFlags(), cd.getFlag()); + assertEquals(s1, this.tc.decode(cd)); + } + + protected abstract int getStringFlags(); +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/CachedDataTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/CachedDataTest.java new file mode 100644 index 0000000..ed6afe3 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/CachedDataTest.java @@ -0,0 +1,22 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test a couple aspects of CachedData. + */ +public class CachedDataTest { + @Test + public void testToString() throws Exception { + String exp = "{CachedData flags=13 data=[84, 104, 105, 115, 32, 105, " + + "115, 32, 97, 32, 115, 105, 109, 112, 108, 101, 32, 116, 101, " + + "115, 116, 32, 115, 116, 114, 105, 110, 103, 46]}"; + CachedData cd = new CachedData(13, "This is a simple test string.".getBytes("UTF-8"), + CachedData.MAX_SIZE, -1); + assertEquals(exp, String.valueOf(cd)); + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/IntegerTranscoderTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/IntegerTranscoderTest.java new file mode 100644 index 0000000..271acdc --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/IntegerTranscoderTest.java @@ -0,0 +1,33 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.IntegerTranscoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Test the integer transcoder. + */ +public class IntegerTranscoderTest { + + private IntegerTranscoder tc = null; + + @BeforeEach + protected void setUp() throws Exception { + tc = new IntegerTranscoder(); + } + + @Test + public void testInt() throws Exception { + assertEquals(923, tc.decode(tc.encode(923)).intValue()); + } + + @Test + public void testBadFlags() throws Exception { + CachedData cd = tc.encode(9284); + assertNull(tc.decode(new CachedData(cd.getFlag() + 1, cd.getData(), CachedData.MAX_SIZE, -1))); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/LongTranscoderTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/LongTranscoderTest.java new file mode 100644 index 0000000..1f063b2 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/LongTranscoderTest.java @@ -0,0 +1,34 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.LongTranscoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + + +/** + * Test the long transcoder. + */ +public class LongTranscoderTest { + + private LongTranscoder tc = null; + + @BeforeEach + protected void setUp() throws Exception { + tc = new LongTranscoder(); + } + + @Test + public void testLong() throws Exception { + assertEquals(923, tc.decode(tc.encode(923L)).longValue()); + } + + @Test + public void testBadFlags() throws Exception { + CachedData cd = tc.encode(9284L); + assertNull(tc.decode(new CachedData(cd.getFlag() + 1, cd.getData(), CachedData.MAX_SIZE, -1))); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/PrimitiveAsStringUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/PrimitiveAsStringUnitTest.java new file mode 100644 index 0000000..2e4e1b4 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/PrimitiveAsStringUnitTest.java @@ -0,0 +1,99 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.Transcoder; +import net.rubyeye.xmemcached.transcoders.WhalinTranscoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PrimitiveAsStringUnitTest { + private SerializingTranscoder serializingTranscoder = new SerializingTranscoder(); + private WhalinTranscoder whalinTranscoder = new WhalinTranscoder(); + String str = "hello"; + int i = 1; + byte b = (byte) 2; + long l = 34930403040L; + float f = 1.02f; + double d = 3.14d; + short s = 1024; + Map map = new HashMap(); + + @BeforeEach + public void setUp() { + this.serializingTranscoder.setPrimitiveAsString(true); + this.whalinTranscoder.setPrimitiveAsString(true); + } + + @Test + public void testXmemcachedTranscoderEncode() { + + assertEquals(this.str, encodeAndGet(this.str, this.serializingTranscoder)); + assertEquals("1", encodeAndGet(this.i, this.serializingTranscoder)); + assertEquals("2", encodeAndGet(this.b, this.serializingTranscoder)); + assertEquals("34930403040", encodeAndGet(this.l, this.serializingTranscoder)); + assertEquals("1.02", encodeAndGet(this.f, this.serializingTranscoder)); + assertEquals("3.14", encodeAndGet(this.d, this.serializingTranscoder)); + // assertEquals("1024", encodeAndGet(this.s, + // this.serializingTranscoder)); + + } + + @Test + public void testXmemcachedDecode() { + assertEquals(this.str, decodeAndGet(this.str, this.serializingTranscoder)); + assertEquals("1", decodeAndGet(this.i, this.serializingTranscoder)); + assertEquals("2", decodeAndGet(this.b, this.serializingTranscoder)); + assertEquals("34930403040", decodeAndGet(this.l, this.serializingTranscoder)); + assertEquals("1.02", decodeAndGet(this.f, this.serializingTranscoder)); + assertEquals("3.14", decodeAndGet(this.d, this.serializingTranscoder)); + assertEquals(this.s, decodeAndGet(this.s, this.serializingTranscoder)); + assertEquals(this.map, decodeAndGet(this.map, this.serializingTranscoder)); + } + + @Test + public void testWhalinTranscoderTranscoderEncode() { + + assertEquals(this.str, encodeAndGet(this.str, this.whalinTranscoder)); + assertEquals("1", encodeAndGet(this.i, this.whalinTranscoder)); + assertEquals("2", encodeAndGet(this.b, this.whalinTranscoder)); + assertEquals("34930403040", encodeAndGet(this.l, this.whalinTranscoder)); + assertEquals("1.02", encodeAndGet(this.f, this.whalinTranscoder)); + assertEquals("3.14", encodeAndGet(this.d, this.whalinTranscoder)); + assertEquals("1024", encodeAndGet(this.s, this.whalinTranscoder)); + } + + @Test + public void testWhalinTranscoderDecode() { + assertEquals(this.str, decodeAndGet(this.str, this.whalinTranscoder)); + assertEquals("1", decodeAndGet(this.i, this.whalinTranscoder)); + assertEquals("2", decodeAndGet(this.b, this.whalinTranscoder)); + assertEquals("34930403040", decodeAndGet(this.l, this.whalinTranscoder)); + assertEquals("1.02", decodeAndGet(this.f, this.whalinTranscoder)); + assertEquals("3.14", decodeAndGet(this.d, this.whalinTranscoder)); + assertEquals("1024", decodeAndGet(this.s, this.whalinTranscoder)); + assertEquals(this.map, decodeAndGet(this.map, this.whalinTranscoder)); + } + + private Object decodeAndGet(Object obj, Transcoder transcoder) { + CachedData data = transcoder.encode(obj); + Object decodeString = transcoder.decode(data); + return decodeString; + } + + private String encodeAndGet(Object obj, Transcoder transcoder) { + CachedData data = encode(obj, transcoder); + String encodeString = new String(data.getData()); + return encodeString; + } + + private CachedData encode(Object str, Transcoder transcoder) { + CachedData data = transcoder.encode(str); + return data; + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/SerializingTranscoderTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/SerializingTranscoderTest.java new file mode 100644 index 0000000..384e248 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/SerializingTranscoderTest.java @@ -0,0 +1,135 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.CompressionMode; +import net.rubyeye.xmemcached.transcoders.SerializingTranscoder; +import net.rubyeye.xmemcached.transcoders.TranscoderUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Calendar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test the serializing transcoder. + */ +public class SerializingTranscoderTest extends BaseTranscoderCase { + + private SerializingTranscoder tc; + private TranscoderUtils tu; + + @BeforeEach + protected void setUp() throws Exception { + tc = new SerializingTranscoder(); + setTranscoder(tc); + tu = new TranscoderUtils(true); + } + + @Test + public void testNonserializable() throws Exception { + try { + tc.encode(new Object()); + fail("Processed a non-serializable object."); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Test + public void testCompressedStringNotSmaller() throws Exception { + String s1 = "This is a test simple string that will not be compressed."; + // Reduce the compression threshold so it'll attempt to compress it. + tc.setCompressionThreshold(8); + CachedData cd = tc.encode(s1); + // This should *not* be compressed because it is too small + assertEquals(0, cd.getFlag()); + assertTrue(Arrays.equals(s1.getBytes(), cd.getData())); + assertEquals(s1, tc.decode(cd)); + } + + @Test + public void testCompressedString() throws Exception { + // This one will actually compress + String s1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + tc.setCompressionThreshold(8); + CachedData cd = tc.encode(s1); + assertEquals(SerializingTranscoder.COMPRESSED, cd.getFlag()); + assertFalse(Arrays.equals(s1.getBytes(), cd.getData())); + assertEquals(s1, tc.decode(cd)); + } + + @Test + public void testObject() throws Exception { + Calendar c = Calendar.getInstance(); + CachedData cd = tc.encode(c); + assertEquals(SerializingTranscoder.SERIALIZED, cd.getFlag()); + assertEquals(c, tc.decode(cd)); + } + + @Test + public void testCompressedObject() throws Exception { + tc.setCompressionThreshold(8); + Calendar c = Calendar.getInstance(); + CachedData cd = tc.encode(c); + assertEquals(SerializingTranscoder.SERIALIZED | SerializingTranscoder.COMPRESSED, cd.getFlag()); + assertEquals(c, tc.decode(cd)); + } + + @Test + public void testCompressedObjectDecompressZipMode() throws Exception { + tc.setCompressionThreshold(8); + tc.setCompressionMode(CompressionMode.ZIP); + Calendar c = Calendar.getInstance(); + CachedData cd = tc.encode(c); + assertEquals(SerializingTranscoder.SERIALIZED | SerializingTranscoder.COMPRESSED, cd.getFlag()); + assertEquals(c, tc.decode(cd)); + tc.setCompressionMode(CompressionMode.GZIP); + } + + @Test + public void testUnencodeable() throws Exception { + try { + CachedData cd = tc.encode(new Object()); + fail("Should fail to serialize, got" + cd); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Test + public void testUndecodeable() throws Exception { + CachedData cd = new CachedData( + Integer.MAX_VALUE & ~(SerializingTranscoder.COMPRESSED | SerializingTranscoder.SERIALIZED), + tu.encodeInt(Integer.MAX_VALUE), tc.getMaxSize(), -1); + assertNull(tc.decode(cd)); + } + + @Test + public void testUndecodeableSerialized() throws Exception { + CachedData cd = new CachedData(SerializingTranscoder.SERIALIZED, + tu.encodeInt(Integer.MAX_VALUE), tc.getMaxSize(), -1); + assertNull(tc.decode(cd)); + } + + @Test + public void testUndecodeableCompressed() throws Exception { + CachedData cd = new CachedData(SerializingTranscoder.COMPRESSED, + tu.encodeInt(Integer.MAX_VALUE), tc.getMaxSize(), -1); + System.out.println("got " + tc.decode(cd)); + assertNull(tc.decode(cd)); + } + + @Override + protected int getStringFlags() { + return 0; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TokyoTyrantTranscoderUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TokyoTyrantTranscoderUnitTest.java new file mode 100644 index 0000000..6364f73 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TokyoTyrantTranscoderUnitTest.java @@ -0,0 +1,145 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.TokyoTyrantTranscoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class TokyoTyrantTranscoderUnitTest { + TokyoTyrantTranscoder tokyoTyrantTranscoder; + + @BeforeEach + public void setUp() { + tokyoTyrantTranscoder = new TokyoTyrantTranscoder(5 * 1024 * 1024); + } + + static class Person implements Serializable { + private String name; + private int age; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + age; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Person other = (Person) obj; + if (age != other.age) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + } + + @Test + public void testEncodeDecodeLargeValue() { + List list = new ArrayList(1000000); + for (long i = 0; i < 1000000; i++) { + list.add(i); + } + CachedData cachedData = tokyoTyrantTranscoder.encode(list); + List decodeList = (List) tokyoTyrantTranscoder.decode(cachedData); + assertNotSame(list, decodeList); + assertEquals(list, decodeList); + + List decodeList2 = (List) tokyoTyrantTranscoder.decode(cachedData); + assertNotSame(list, decodeList2); + assertNotSame(decodeList2, decodeList); + assertEquals(list, decodeList2); + } + + @Test + public void testDecodeManayTimes() { + Person p = new Person(); + p.name = "xmc"; + p.age = 1; + CachedData cachedData = tokyoTyrantTranscoder.encode(p); + Person decodePerson = (Person) tokyoTyrantTranscoder.decode(cachedData); + assertNotSame(p, decodePerson); + assertEquals(p, decodePerson); + + Person decodePerson2 = (Person) tokyoTyrantTranscoder.decode(cachedData); + assertNotSame(p, decodePerson2); + assertNotSame(decodePerson, decodePerson2); + assertEquals(p, decodePerson2); + + } + + @Test + public void testEncodeDecode() { + // simple type + CachedData cachedData = tokyoTyrantTranscoder.encode(1); + assertEquals(8, cachedData.getData().length); + assertEquals(1, tokyoTyrantTranscoder.decode(cachedData)); + + long currentTimeMillis = System.currentTimeMillis(); + cachedData = tokyoTyrantTranscoder.encode(currentTimeMillis); + assertEquals(12, cachedData.getData().length); + assertEquals(currentTimeMillis, tokyoTyrantTranscoder.decode(cachedData)); + + cachedData = tokyoTyrantTranscoder.encode("hello"); + assertEquals(9, cachedData.getData().length); + assertEquals("hello", tokyoTyrantTranscoder.decode(cachedData)); + + cachedData = tokyoTyrantTranscoder.encode(2.3d); + assertEquals(12, cachedData.getData().length); + assertEquals(2.3d, tokyoTyrantTranscoder.decode(cachedData)); + + // collection + List list = new ArrayList(); + list.add("1"); + cachedData = tokyoTyrantTranscoder.encode(list); + int oldLength = cachedData.getData().length; + List decodedList = (List) tokyoTyrantTranscoder.decode(cachedData); + assertEquals(1, decodedList.size()); + assertTrue(decodedList.contains("1")); + + // compress + tokyoTyrantTranscoder.setCompressionThreshold(1); + cachedData = tokyoTyrantTranscoder.encode(list); + decodedList = (List) tokyoTyrantTranscoder.decode(cachedData); + assertEquals(1, decodedList.size()); + assertNotSame(decodedList, list); + assertTrue(decodedList.contains("1")); + + tokyoTyrantTranscoder.setCompressionThreshold(16 * 1024); + // serialize type + + Person p = new Person(); + p.name = "xmc"; + p.age = 1; + cachedData = tokyoTyrantTranscoder.encode(p); + + Person decodePerson = (Person) tokyoTyrantTranscoder.decode(cachedData); + + assertNotSame(p, decodePerson); + + assertEquals(p, decodePerson); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TranscoderAllTests.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TranscoderAllTests.java new file mode 100644 index 0000000..ab5f568 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TranscoderAllTests.java @@ -0,0 +1,24 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +//import junit.framework.Test; +//import junit.framework.TestSuite; + +public class TranscoderAllTests { + +// public static Test suite() { +// TestSuite suite = new TestSuite("Test for net.rubyeye.xmemcached.test.unittest.transcoder"); +// // $JUnit-BEGIN$ +// suite.addTestSuite(WhalinV1TranscoderTest.class); +// suite.addTestSuite(WhalinTranscoderTest.class); +// suite.addTestSuite(LongTranscoderTest.class); +// suite.addTestSuite(BaseSerializingTranscoderTest.class); +// suite.addTestSuite(SerializingTranscoderTest.class); +// suite.addTestSuite(TranscoderUtilsTest.class); +// suite.addTestSuite(IntegerTranscoderTest.class); +// suite.addTestSuite(CachedDataTest.class); +// suite.addTestSuite(PrimitiveAsStringUnitTest.class); +// // $JUnit-END$ +// return suite; +// } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TranscoderUtilsTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TranscoderUtilsTest.java new file mode 100644 index 0000000..5e44ce5 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/TranscoderUtilsTest.java @@ -0,0 +1,75 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.TranscoderUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Some test coverage for transcoder utils. + */ +public class TranscoderUtilsTest { + + private TranscoderUtils tu; + byte[] oversizeBytes = new byte[16]; + + @BeforeEach + protected void setUp() throws Exception { + tu = new TranscoderUtils(true); + } + + @Test + public void testBooleanOverflow() { + try { + boolean b = tu.decodeBoolean(oversizeBytes); + fail("Got " + b + " expected assertion."); + } catch (AssertionError e) { + // pass + } + } + + @Test + public void testByteOverflow() { + try { + byte b = tu.decodeByte(oversizeBytes); + fail("Got " + b + " expected assertion."); + } catch (AssertionError e) { + // pass + } + } + + @Test + public void testIntOverflow() { + try { + int b = tu.decodeInt(oversizeBytes); + fail("Got " + b + " expected assertion."); + } catch (AssertionError e) { + // pass + } + } + + @Test + public void testLongOverflow() { + try { + long b = tu.decodeLong(oversizeBytes); + fail("Got " + b + " expected assertion."); + } catch (AssertionError e) { + // pass + } + } + + @Test + public void testPackedLong() { + assertEquals("[1]", Arrays.toString(tu.encodeLong(1))); + } + + @Test + public void testUnpackedLong() { + assertEquals("[0, 0, 0, 0, 0, 0, 0, 1]", + Arrays.toString(new TranscoderUtils(false).encodeLong(1))); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/WhalinTranscoderTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/WhalinTranscoderTest.java new file mode 100644 index 0000000..eca7731 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/WhalinTranscoderTest.java @@ -0,0 +1,122 @@ +// Copyright (c) 2006 Dustin Sallings + +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.TranscoderUtils; +import net.rubyeye.xmemcached.transcoders.WhalinTranscoder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Calendar; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test the serializing transcoder. + */ +public class WhalinTranscoderTest extends BaseTranscoderCase { + + private WhalinTranscoder tc; + private TranscoderUtils tu; + + @BeforeEach + protected void setUp() throws Exception { + tc = new WhalinTranscoder(); + setTranscoder(tc); + tu = new TranscoderUtils(false); + } + + @Test + public void testNonserializable() throws Exception { + try { + tc.encode(new Object()); + fail("Processed a non-serializable object."); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Test + public void testCompressedStringNotSmaller() throws Exception { + String s1 = "This is a test simple string that will not be compressed."; + // Reduce the compression threshold so it'll attempt to compress it. + tc.setCompressionThreshold(8); + CachedData cd = tc.encode(s1); + // This should *not* be compressed because it is too small + assertEquals(WhalinTranscoder.SPECIAL_STRING, cd.getFlag()); + assertTrue(Arrays.equals(s1.getBytes(), cd.getData())); + assertEquals(s1, tc.decode(cd)); + } + + @Test + public void testCompressedString() throws Exception { + // This one will actually compress + String s1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + tc.setCompressionThreshold(8); + CachedData cd = tc.encode(s1); + assertEquals(WhalinTranscoder.COMPRESSED | WhalinTranscoder.SPECIAL_STRING, cd.getFlag()); + assertFalse(Arrays.equals(s1.getBytes(), cd.getData())); + assertEquals(s1, tc.decode(cd)); + } + + @Test + public void testObject() throws Exception { + Calendar c = Calendar.getInstance(); + CachedData cd = tc.encode(c); + assertEquals(WhalinTranscoder.SERIALIZED, cd.getFlag()); + assertEquals(c, tc.decode(cd)); + } + + @Test + public void testCompressedObject() throws Exception { + tc.setCompressionThreshold(8); + Calendar c = Calendar.getInstance(); + CachedData cd = tc.encode(c); + assertEquals(WhalinTranscoder.SERIALIZED | WhalinTranscoder.COMPRESSED, cd.getFlag()); + assertEquals(c, tc.decode(cd)); + } + + @Test + public void testUnencodeable() throws Exception { + try { + CachedData cd = tc.encode(new Object()); + fail("Should fail to serialize, got" + cd); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Test + public void testUndecodeable() throws Exception { + CachedData cd = new CachedData( + Integer.MAX_VALUE & ~(WhalinTranscoder.COMPRESSED | WhalinTranscoder.SERIALIZED), + tu.encodeInt(Integer.MAX_VALUE), tc.getMaxSize(), -1); + assertNull(tc.decode(cd)); + } + + @Test + public void testUndecodeableSerialized() throws Exception { + CachedData cd = new CachedData(WhalinTranscoder.SERIALIZED, tu.encodeInt(Integer.MAX_VALUE), + tc.getMaxSize(), -1); + assertNull(tc.decode(cd)); + } + + @Test + public void testUndecodeableCompressed() throws Exception { + CachedData cd = new CachedData(WhalinTranscoder.COMPRESSED, tu.encodeInt(Integer.MAX_VALUE), + tc.getMaxSize(), -1); + assertNull(tc.decode(cd)); + } + + @Override + protected int getStringFlags() { + return WhalinTranscoder.SPECIAL_STRING; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/WhalinV1TranscoderTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/WhalinV1TranscoderTest.java new file mode 100644 index 0000000..73b28d6 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/transcoder/WhalinV1TranscoderTest.java @@ -0,0 +1,33 @@ +package net.rubyeye.xmemcached.test.unittest.transcoder; + +import net.rubyeye.xmemcached.transcoders.CachedData; +import net.rubyeye.xmemcached.transcoders.WhalinV1Transcoder; +import org.junit.jupiter.api.BeforeEach; + +import static org.junit.jupiter.api.Assertions.fail; + +public class WhalinV1TranscoderTest extends BaseTranscoderCase { + + @BeforeEach + protected void setUp() throws Exception { + setTranscoder(new WhalinV1Transcoder()); + } + + @Override + public void testByteArray() throws Exception { + byte[] a = {'a', 'b', 'c'}; + try { + CachedData cd = getTranscoder().encode(a); + fail("Expected IllegalArgumentException, got " + cd); + } catch (IllegalArgumentException e) { + // pass + } + } + + @Override + protected int getStringFlags() { + // Flags are not used by this transcoder. + return 0; + } + +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/AddrUtilTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/AddrUtilTest.java new file mode 100644 index 0000000..f85b3f1 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/AddrUtilTest.java @@ -0,0 +1,138 @@ +package net.rubyeye.xmemcached.test.unittest.utils; + +import net.rubyeye.xmemcached.utils.AddrUtil; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class AddrUtilTest { + @Test + public void testGetAddresses() { + try { + AddrUtil.getAddresses(null); + fail(); + } catch (NullPointerException e) { + assertEquals("Null host list", e.getMessage()); + } + + try { + AddrUtil.getAddresses(" "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("No hosts in list: ``" + " " + "''", e.getMessage()); + } + + List addresses = AddrUtil.getAddresses("localhost:12000 192.168.0.98:12000"); + + assertEquals(2, addresses.size()); + assertEquals("localhost", addresses.get(0).getHostName()); + // assertEquals("192.168.0.98",addresses.get(1).getHostName()); + assertEquals(12000, addresses.get(0).getPort()); + assertEquals(12000, addresses.get(1).getPort()); + } + + @Test + public void testOneAddress() { + try { + AddrUtil.getOneAddress(null); + fail(); + } catch (NullPointerException e) { + assertEquals("Null host", e.getMessage()); + } + + try { + AddrUtil.getOneAddress(" "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("No hosts in: ``" + " " + "''", e.getMessage()); + } + + try { + AddrUtil.getOneAddress("localhost"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Invalid server ``localhost''", e.getMessage()); + } + + InetSocketAddress addresses = AddrUtil.getOneAddress("localhost:12000"); + + assertEquals("localhost", addresses.getHostName()); + // assertEquals("192.168.0.98",addresses.get(1).getHostName()); + assertEquals(12000, addresses.getPort()); + } + + @Test + public void testGetAddressMap_IllegalArgument() { + try { + AddrUtil.getAddressMap(", localhost:12000,localhost:12001"); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(true); + } + try { + AddrUtil.getAddressMap(" "); + fail(); + } catch (IllegalArgumentException e) { + assertTrue(true); + } + } + + @Test + public void testGetAddressMap_OnlyMainAddr() { + Map addressMap = + AddrUtil.getAddressMap("localhost:12000 localhost:12001 localhost:12002 "); + assertEquals(3, addressMap.size()); + assertNull(addressMap.get(new InetSocketAddress("localhost", 12002))); + assertNull(addressMap.get(new InetSocketAddress("localhost", 12000))); + assertNull(addressMap.get(new InetSocketAddress("localhost", 12001))); + } + + @Test + public void testGetAddressMap() { + Map addressMap = AddrUtil.getAddressMap( + "localhost:12000,localhost:12001 localhost:12002 localhost:12001,localhost:12003"); + assertEquals(3, addressMap.size()); + assertEquals(new InetSocketAddress("localhost", 12001), + addressMap.get(new InetSocketAddress("localhost", 12000))); + assertNull(addressMap.get(new InetSocketAddress("localhost", 12002))); + assertEquals(addressMap.get(new InetSocketAddress("localhost", 12001)), + new InetSocketAddress("localhost", 12003)); + } + + @Test + public void testGetAddress() { + try { + AddrUtil.getOneAddress(null); + fail(); + } catch (NullPointerException e) { + assertEquals("Null host", e.getMessage()); + } + + try { + AddrUtil.getOneAddress(" "); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("No hosts in: ``" + " " + "''", e.getMessage()); + } + + try { + AddrUtil.getOneAddress("localhost"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Invalid server ``localhost''", e.getMessage()); + } + + InetSocketAddress addresses = AddrUtil.getOneAddress("localhost:12000"); + + assertEquals("localhost", addresses.getHostName()); + // assertEquals("192.168.0.98",addresses.get(1).getHostName()); + assertEquals(12000, addresses.getPort()); + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/OpaqueGeneraterUnitTest.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/OpaqueGeneraterUnitTest.java new file mode 100644 index 0000000..5bcae69 --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/OpaqueGeneraterUnitTest.java @@ -0,0 +1,42 @@ +package net.rubyeye.xmemcached.test.unittest.utils; + +import net.rubyeye.xmemcached.utils.OpaqueGenerater; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertNull; + + +public class OpaqueGeneraterUnitTest { + + @Test + public void testGetNextValue() throws Exception { + OpaqueGenerater.getInstance().setValue(Integer.MAX_VALUE - 10000); + final CyclicBarrier barrier = new CyclicBarrier(200 + 1); + final AtomicReference exceptionRef = new AtomicReference(); + for (int i = 0; i < 200; i++) { + new Thread() { + public void run() { + try { + barrier.await(); + for (int i = 0; i < 10000; i++) { + if (OpaqueGenerater.getInstance().getNextValue() < 0) + throw new RuntimeException("Test failed."); + } + barrier.await(); + } catch (Exception e) { + exceptionRef.set(e); + e.printStackTrace(); + } + } + }.start(); + } + barrier.await(); + barrier.await(); + assertNull(exceptionRef.get()); + System.out.println(OpaqueGenerater.getInstance().getNextValue()); + + } +} diff --git a/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/XMemcachedClientFactoryBeanIT.java b/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/XMemcachedClientFactoryBeanIT.java new file mode 100644 index 0000000..fe303ea --- /dev/null +++ b/src/test/java/net/rubyeye/xmemcached/test/unittest/utils/XMemcachedClientFactoryBeanIT.java @@ -0,0 +1,59 @@ +package net.rubyeye.xmemcached.test.unittest.utils; + +import net.rubyeye.xmemcached.MemcachedClient; +import net.rubyeye.xmemcached.exception.MemcachedException; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +public class XMemcachedClientFactoryBeanIT { + + ApplicationContext ctx; + + @BeforeEach + public void setUp() throws Exception { + this.ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); + } + + public void testSimpleConfig() throws Exception { + MemcachedClient memcachedClient = (MemcachedClient) this.ctx.getBean("memcachedClient1"); + + validateClient(memcachedClient); + } + + public void testAllConfig() throws Exception { + MemcachedClient memcachedClient = (MemcachedClient) this.ctx.getBean("memcachedClient2"); + validateClient(memcachedClient); + assertEquals(memcachedClient.getTimeoutExceptionThreshold(), 999); + } + + public void testComposite() throws Exception { + MemcachedClient memcachedClient1 = (MemcachedClient) this.ctx.getBean("memcachedClient1"); + MemcachedClient memcachedClient2 = (MemcachedClient) this.ctx.getBean("memcachedClient2"); + validateClient(memcachedClient1); + memcachedClient1.flushAll(); + validateClient(memcachedClient2); + } + + private void validateClient(MemcachedClient memcachedClient) + throws TimeoutException, InterruptedException, MemcachedException, IOException { + // memcachedClient.setLoggingLevelVerbosity(new InetSocketAddress( + // "localhost", 12000), 3); + assertNotNull(memcachedClient); + assertTrue(memcachedClient.getConnector().isStarted()); + assertFalse(memcachedClient.isShutdown()); + memcachedClient.set("test", 0, 1, 1000000); + assertEquals(1, (int) memcachedClient.get("test")); + memcachedClient.shutdown(); + } + +} diff --git a/src/test/resources/applicationContext.xml b/src/test/resources/applicationContext.xml new file mode 100644 index 0000000..3ec3167 --- /dev/null +++ b/src/test/resources/applicationContext.xml @@ -0,0 +1,50 @@ + + + + + classpath:test.properties + + + + + ${test.memcached.servers} + + + + + + ${test.memcached.servers} + + + + 1 + 2 + 3 + 4 + 5 + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/golden_compass.txt b/src/test/resources/golden_compass.txt new file mode 100644 index 0000000..906ec97 --- /dev/null +++ b/src/test/resources/golden_compass.txt @@ -0,0 +1,1251 @@ +THE DECANTER OF TOKAY + +Lyra and her daemon moved through the darkening hall, taking care to keep to one side, out of sight of the kitchen. The three great tables that ran the length of the hall were laid already, the silver and the glass catching what little light there was, and the long benches were pulled out ready for the guests. Portraits of former Masters hung high up in the gloom along the walls. Lyra reached the dais and looked back at the open kitchen door, and, seeing no one, stepped up beside the high table. The places here were laid with gold, not silver, and the fourteen seats were not oak benches but mahogany chairs with velvet cushions. + +Lyra stopped beside the Master's chair and flicked the biggest glass gently with a fingernail. The sound rang clearly through the hall. + +"You're not taking this seriously," whispered her daemon. "Behave yourself." + +Her daemon's name was Pantalaimon, and he was currently in the form of a moth, a dark brown one so as not to show up in the darkness of the hall. + +"They're making too much noise to hear from the kitchen," Lyra whispered back. "And the Steward doesn't come in till the first bell. Stop fussing." + +But she put her palm over the ringing crystal anyway, and Pantalaimon fluttered ahead and through the slightly open door of the Retiring Room at the other end of the dais. After a moment he appeared again. + +"There's no one there," he whispered. "But we must be quick." + +Crouching behind the high table, Lyra darted along and through the door into the Retiring Room, where she stood up and looked around. The only light in here came from the fireplace, where a bright blaze of logs settled slightly as she looked, sending a fountain of sparks up into the chimney. She had lived most of her life in the college, but had never seen the Retiring Room before: only Scholars and their guests were allowed in here, and never females. Even the maidservants didn't clean in here. That was the Butler's job alone. + +Pantalaimon settled on her shoulder. + +"Happy now? Can we go?" he whispered. + +"Don't be silly! I want to look around!" + +It was a large room, with an oval table of polished rosewood on which stood various decanters and glasses, and a silver smoking stand with a rack of pipes. On a sideboard nearby there was a little chafing dish and a basket of poppy heads. + +"They do themselves well, don't they, Pan?" she said under her breath. + +She sat in one of the green leather armchairs. It was so deep she found herself nearly lying down, but she sat up again and tucked her legs under her to look at the portraits on the walls. More old Scholars, probably; robed, bearded, and gloomy, they stared out of their frames in solemn disapproval. + +"What d'you think they talk about?" Lyra said, or began to say, because before she'd finished the question she heard voices outside the door. + +"Behind the chair-quick!" whispered Pantalaimon, and in a flash Lyra was out of the armchair and crouching behind it. It wasn't the best one for hiding behind: she'd chosen one in the very center of the room, and unless she kept very quiet... + +The door opened, and the light changed in the room; one of the incomers was carrying a lamp, which he put down on the sideboard. Lyra could see his legs, in their dark green trousers and shiny black shoes. It was a servant. + +Then a deep voice said, "Has Lord Asriel arrived yet?" + +It was the Master. As Lyra held her breath, she saw the servant's daemon (a dog, like all servants' daemons) trot in and sit quietly at his feet, and then the Master's feet became visible too, in the shabby black shoes he always wore. + +"No, Master," said the Butler. "No word from the aerodock, either." + +"I expect he'll be hungry when he arrives. Show him straight into Hall, will you?" + +"Very good, Master." + +"And you've decanted some of the special Tokay for him?" + +"Yes, Master. The 1898, as you ordered. His Lordship is very partial to that, I remember." + +"Good. Now leave me, please." + +"Do you need the lamp, Master?" + +"Yes, leave that too. Look in during dinner to trim it, will you?" + +The Butler bowed slightly and turned to leave, his daemon trotting obediently after him. From her not-much-of-a-hiding place Lyra watched as the Master went to a large oak wardrobe in the corner of the room, took his gown from a hanger, and pulled it laboriously on. The Master had been a powerful man, but he was well over seventy now, and his movements were stiff and slow. The Master's daemon had the form of a raven, and as soon as his robe was on, she jumped down from the wardrobe and settled in her accustomed place on his right shoulder. + +Lyra could feel Pantalaimon bristling with anxiety, though he made no sound. For herself, she was pleasantly excited. + +The visitor mentioned by the Master, Lord Asriel, was her uncle, a man whom she admired and feared greatly. He was said to be involved in high politics, in secret exploration, in distant warfare, and she never knew when he was going to appear. He was fierce: if he caught her in here she'd be severely punished, but she could put up with that. + +What she saw next, however, changed things completely. + +The Master took from his pocket a folded paper and laid it on the table beside the wine. He took the stopper out of the mouth of a decanter containing a rich golden wine, unfolded the paper, and poured a thin stream of white powder into the decanter before crumpling the paper and throwing it into the fire. Then he took a pencil from his pocket, stirred the wine until the powder had dissolved, and replaced the stopper. + +His daemon gave a soft brief squawk. The Master replied in an undertone, and looked around with his hooded, clouded eyes before leaving through the door he'd come in by. + +Lyra whispered, "Did you see that, Pan?" + +"Of course I did! Now hurry out, before the Steward comes!" + +But as he spoke, there came the sound of a bell ringing once from the far end of the hall. + +"That's the Steward's bell!" said Lyra. "I thought we had more time than that." + +Pantalaimon fluttered swiftly to the hall door, and swiftly back. + +"The Steward's there already," he said. "And you can't get out of the other door..." + +The other door, the one the Master had entered and left by, opened onto the busy corridor between the library and the Scholars' common room. At this time of day it was thronged with men pulling on their gowns for dinner, or hurrying to leave papers or briefcases in the common room before moving nto the hall. Lyra had planned to leave the way she'd come, banking on another few minutes before the Steward's bell rang. + +And if she hadn't seen the Master tipping that powder into the wine, she might have risked the Steward's anger, or hoped to avoid being noticed in the busy corridor. But she was confused, and that made her hesitate. + +Then she heard heavy footsteps on the dais. The Steward was coming to make sure the Retiring Room was ready for the Scholars' poppy and wine after dinner. Lyra darted to the oak wardrobe, opened it, and hid inside, pulling the door shut just as the Steward entered. She had no fear for Pantalaimon: the room was somber colored, and he could always creep under a chair. + +She heard the Steward's heavy wheezing, and through the crack where the door hadn't quite shut she saw him adjust the pipes in the rack by the smoking stand and cast a glance over the decanters and glasses. Then he smoothed the hair over his ears with both palms and said something to his daemon. He was a servant, so she was a dog; but a superior servant, so a superior dog. In fact, she had the form of a red setter. The daemon seemed suspicious, and cast around as if she'd sensed an intruder, but didn't make for the wardrobe, to Lyra's intense relief. Lyra was afraid of the Steward, who had twice beaten her. + +Lyra heard a tiny whisper; obviously Pantalaimon had squeezed in beside her. + +"We're going to have to stay here now. Why don't you listen to me?" + +She didn't reply until the Steward had left. It was his job to supervise the waiting at the high table; she could hear the Scholars coming into the hall, the murmur of voices, the shuffle of feet. + +"It's a good thing I didn't," she whispered back. "We wouldn't have seen the Master put poison in the wine otherwise. Pan, that was the Tokay he asked the Butler about! They're going to kill Lord Asriel!" + +"You don't know it's poison." + +"Oh, of course it is. Don't you remember, he made the Butler leave the room before he did it? If it was innocent, it wouldn't have mattered the Butler seeing. And I know there's something going on-something political. The servants have been talking about it for days. Pan, we could prevent a murder!" + +"I've never heard such nonsense," he said shortly. "How do you think you're going to keep still for four hours in this poky wardrobe? Let me go and look in the corridor. I'll tell you when it's clear." + +He fluttered from her shoulder, and she saw his little shadow appear in the crack of light. + +"It's no good, Pan, I'm staying," she said. "There's another robe or something here. I'll put that on the floor and make myself comfortable. I've just got to see what they do." + +She had been crouching. She carefully stood up, feeling around for the clothes hangers in order not to make a noise, and found that the wardrobe was bigger than she'd thought. There were several academic robes and hoods, some with fur around them, most faced with silk. + +"I wonder if these are all the Master's?" she whispered. "When he gets honorary degrees from other places, perhaps they give him fancy robes and he keeps them here for dressing-up....Pan, do you really think it's not poison in that wine?" + +"No," he said. "I think it is, like you do. And I think it's none of our Business. And I think it would be the silliest thing you've ever done in a lifetime of silly things to interfere. It's nothing to do with us." + +"Don't be stupid," Lyra said. "I can't sit in here and watch them give him poison!" + +"Come somewhere else, then." + +"You're a coward, Pan." + +"Certainly I am. May I ask what you intend to do? Are you going to leap out and snatch the glass from his trembling fingers? What did you have in mind?" + +"I didn't have anything in mind, and well you know it," she snapped quietly. "But now I've seen what the Master did, I haven't got any choice. You're supposed to know about conscience, aren't you? How can I just go and sit in the library or somewhere and twiddle my thumbs, knowing what's going to happen? I don't intend to do that, I promise you." + +"This is what you wanted all the time," he said after a moment. "You wanted to hide in here and watch. Why didn't I realize that before?" + +"All right, I do," she said. "Everyone knows they get up to something secret. They have a ritual or something. And I just wanted to know what it was." + +"It's none of your Business! If they want to enjoy their little secrets you should just feel superior and let them get on with it. Hiding and spying is for silly children." + +"Exactly what I knew you'd say. Now stop nagging." + +The two of them sat in silence for a while, Lyra uncomfortable on the hard floor of the wardrobe and Pantalaimon self-righteously twitching his temporary antennae on one of the robes. Lyra felt a mixture of thoughts contending in her head, and she would have liked nothing better than to share them with her daemon, but she was proud too. Perhaps she should try to clear them up without his help. + +Her main thought was anxiety, and it wasn't for herself. She'd been in trouble often enough to be used to it. This time she was anxious about Lord Asriel, and about what this all meant. It wasn't often that he visited the college, and the fact that this was a time of high political tension meant that he hadn't come simply to eat and drink and smoke with a few old friends. She knew that both Lord Asriel and the Master were members of the Cabinet Council, the Prime Minister's special advisory body, so it might have been something to do with that; but meetings of the Cabinet Council were held in the palace, not in the Retiring Room of Jordan College. Then there was the rumor that had been keeping the college servants whispering for days. It was said that the Tartars had invaded Muscovy, and were surging north to St. Petersburg, from where they would be able to dominate the Baltic Sea and eventually overcome the entire west of Europe. And Lord Asriel had been in the far North: when she'd seen him last, he was preparing an expedition to Lapland... + +"Pan," she whispered. + +"Yes?" + +"Do you think there'll be a war?" + +"Not yet. Lord Asriel wouldn't be dining here if it was going to break out in the next week or so." "That's what I thought. But later?" "Shh! Someone's coming." + +She sat up and put her eye to the crack of the door. It was the Butler, coming to trim the lamp as the Master had ordered him to. The common room and the library were lit by anbar-ic power, but the Scholars preferred the older, softer naphtha lamps in the Retiring Room. They wouldn't change that in the Master's lifetime. + +The Butler trimmed the wick, and put another log on the fire as well, and then listened carefully at the hall door before helping himself to a handful of leaf from the smoking stand. He had hardly replaced the lid when the handle of the other door turned, making him jump nervously. Lyra tried not to laugh. The Butler hastily stuffed the leaf into his pocket and turned to face the incomer. + +"Lord Asriel!" he said, and a shiver of cold surprise ran down Lyra's back. She couldn't see him from where she was, and she tried to smother the urge to move and look. + +"Good evening, Wren," said Lord Asriel. Lyra always heard that harsh voice with a mixture of pleasure and apprehension. "I arrived too late to dine. I'll wait in here." + +The Butler looked uncomfortable. Guests entered the Retiring Room at the Master's invitation only, and Lord Asriel knew that; but the Butler also saw Lord Asriel looking pointedly at the bulge in his pocket, and decided not to protest. + +"Shall I let the Master know you've arrived, my lord?" + +"No harm in that. You might bring me some Coffee." + +"Very good, my lord." + +The Butler bowed and hastened out, his daemon trotting submissively at his heels. Lyra's uncle moved across to the fire and stretched his arms high above his head, yawning like a lion. He was wearing traveling clothes. Lyra was reminded, as she always was when she saw him again, of how much he frightened her. There was no question now of creeping out unnoticed: she'd have to sit tight and hope. + +Lord Asriel's daemon, a snow leopard, stood behind him. + +"Are you going to show the projections in here?" she said quietly. + +"Yes. It'll create less fuss than moving to the lecture theater. They'll want to see the specimens too; I'll send for the Porter in a minute. This is a bad time, Stelmaria." + +"You should rest." + +He stretched out in one of the armchairs, so that Lyra could no longer see his face. + +"Yes, yes. I should also change my clothes. There's probably some ancient etiquette that allows them to fine me a dozen bottles for coming in here dressed improperly. I should sleep for three days. The fact remains that-" + +There was a knock, and the Butler came in with a silver tray bearing a Coffeepot and a cup. + +"Thank you, Wren," said Lord Asriel. "Is that the Tokay I can see on the table?" + +"The Master ordered it decanted especially for you, my I lord," said the Butler. "There are only three dozen bottles left I of the'98." + +"All good things pass away. Leave the tray here beside me. Oh, ask the Porter to send up the two cases I left in the Lodge, would you?" + +"Here, my lord?" + +"Yes, here, man. And I shall need a screen and a projecting lantern, also here, also now." + +The Butler could hardly prevent himself from opening his mouth in surprise, but managed to suppress the question, or the protest. + +"Wren, you're forgetting your place," said Lord Asriel. "Don't question me; just do as I tell you." + +"Very good, my lord," said the Butler. "If I may suggest it, I should perhaps let Mr. Cawson know what you're planning, my lord, or else he'll be somewhat taken aback, if you see what I mean." + +"Yes. Tell him, then." + +Mr. Cawson was the Steward. There was an old and well-established rivalry between him and the Butler. The Steward was the superior, but the Butler had more opportunities to ingratiate himself with the Scholars, and made full use of them. He would be delighted to have this chance of showing the Steward that he knew more about what was going on in the Retiring Room. + +He bowed and left. Lyra watched as her uncle poured a cup of Coffee, drained it at once, and poured another before sipping more slowly. She was agog: cases of specimens? A projecting lantern? What did he have to show the Scholars that was so urgent and important? + +Then Lord Asriel stood up and turned away from the fire. She saw him fully, and marveled at the contrast he made with the plump Butler, the stooped and languid Scholars. Lord Asriel was a tall man with powerful shoulders, a fierce dark face, and eyes that seemed to flash and glitter with savage laughter. It was a face to be dominated by, or to fight: never a face to patronize or pity. All his movements were large and perfectly balanced, like those of a wild animal, and when he appeared in a room like this, he seemed a wild animal held in a cage too small for it. + +At the moment his expression was distant and preoccupied. His daemon came close and leaned her head on his waist, and he looked down at her unfathomably before turning away and walking to the table. Lyra suddenly felt her stomach lurch, for Lord Asriel had taken the stopper from the decanter of Tokay, and was pouring a glass. + +"No!" + +The quiet cry came before she could hold it back. Lord Asriel heard and turned at once. + +"Who's there?" + +She couldn't help herself. She tumbled out of the wardrobe and scrambled up to snatch the glass from his hand. The wine flew out, splashing on the edge of the table and the carpet, and then the glass fell and smashed. He seized her wrist and twisted hard. + +"Lyra! What the hell are you doing?" + +"Let go of me and I'll tell you!" + +"I'll break your arm first. How dare you come in here?" + +"I've just saved your life!" + +They were still for a moment, the girl twisted in pain but grimacing to prevent herself from crying out louder, the man bent over her frowning like thunder. + +"What did you say?" he said more quietly. + +"That wine is poisoned," she muttered between clenched teeth. "I saw the Master put some powder in it." + +He let go. She sank to the floor, and Pantalaimon fluttered anxiously to her shoulder. Her uncle looked down with a restrained fury, and she didn't dare meet his eyes. + +"I came in just to see what the room was like," she said. "I know I shouldn't have. But I was going to go out before anyone came in, except that I heard the Master coming and got ^ trapped. The wardrobe was the only place to hide. And I saw him put the powder in the wine. If I hadn't..." + +There was a knock on the door. + +"That'll be the Porter," said Lord Asriel. "Back in the wardrobe. If I hear the slightest noise, I'll make you wish you were dead." + +She darted back there at once, and no sooner had she pulled the door shut than Lord Asriel called, "Come in." + +As he'd said, it was the Porter. + +"In here, my lord?" + +Lyra saw the old man standing doubtfully in the doorway, and behind him, the corner of a large wooden box. + +"That's right, Shuter," said Lord Asriel. "Bring them both in and put them down by the table." + +Lyra relaxed a little, and allowed herself to feel the pain in her shoulder and wrist. It might have been enough to make her cry, if she was the sort of girl who cried. Instead she gritted her teeth and moved the arm gently until it felt looser. + +Then came a crash of glass and the glug of spilled liquid. + +"Damn you, Shuter, you careless old fool! Look what you've done!" + +Lyra could see, just. Her uncle had managed to knock the + +decanter of Tokay off the table, and made it look as if the Porter had done it. The old man put the box down carefully and began to apologize. + +"I'm truly sorry, my lord-I must have been closer than I thought-" + +"Get something to clear this mess up. Go on, before it soaks into the carpet!" + +The Porter hurried out. Lord Asriel moved closer to the wardrobe and spoke in an undertone. + +"Since you're in there, you can make yourself useful. Watch the Master closely when he comes in. If you tell me something interesting about him, I'll keep you from getting further into the trouble you're already in. Understand?" + +"Yes, Uncle." + +"Make a noise in there and I won't help you. You're on your own." + +He moved away and stood with his back to the fire again as the Porter came back with a brush and dustpan for the glass and a bowl and cloth. + +"I can only say once again, my lord, I do most earnestly beg your pardon; I don't know what-" + +"Just clear up the mess." + +As the Porter began to mop the wine from the carpet, the Butler knocked and came in with Lord Asriel's manservant, a man called Thorold. They were carrying between them a heavy case of polished wood with brass handles. They saw what the Porter was doing and stopped dead. + +"Yes, it was the Tokay," said Lord Asriel. "Too bad. Is that the lantern? Set it up by the wardrobe, Thorold, if you would. I'll have the screen up at the other end." + +Lyra realized that she would be able to see the screen and whatever was on it through the crack in the door, and wondered whether her uncle had arranged it like that for the purpose. Under the noise the manservant made unrolling the stiff linen and setting it up on its frame, she whispered: + +"See? It was worth coming, wasn't it?" + +"It might be," Pantalaimon said austerely, in his tiny moth voice. "And it might not." + +Lord Asriel stood by the fire sipping the last of the Coffee and watching darkly as Thorold opened the case of the projecting lantern and uncapped the lens before checking the oil tank. + +"There's plenty of oil, my lord," he said. "Shall I send for a technician to operate it?" + +"No. I'll do it myself. Thank you, Thorold. Have they finished dinner yet, Wren?" + +"Very nearly, I think, my lord," replied the Butler. "If I understand Mr. Cawson aright, the Master and his guests won't be disposed to linger once they know you're here. Shall I take the Coffee tray?" + +"Take it and go." + +"Very good, my lord." + +With a slight bow, the Butler took the tray and left, and Thorold went with him. As soon as the door closed, Lord Asriel looked across the room directly at the wardrobe, and Lyra felt the force of his glance almost as if it had physical form, as if it were an arrow or a spear. Then he looked away and spoke softly to his dasmon. + +She came to sit calmly at his side, alert and elegant and dangerous, her tawny eyes surveying the room before turning, like his black ones, to the door from the hall as the handle turned. Lyra couldn't see the door, but she heard an intake of breath as the first man came in. + +TWO + +THE IDEA OF NORTH + +"Master," said Lord Asriel. "Yes, I'm back. Do bring in your guests; I've got something very interesting to show you." + +"Lord Asriel," said the Master heavily, and came forward to shake his hand. From her hiding place Lyra watched the Master's eyes, and indeed, they flicked toward the table for a second, where the Tokay had been. + +"Master," said Lord Asriel. "I came too late to disturb your dinner, so I made myself at Home in here. Hello, Sub-Rector. Glad to see you looking so well. Excuse my rough appearance; I've only just landed. Yes, Master, the Tokay's gone. I think you're standing in it. The Porter knocked it off the table, but it was my fault. Hello, Chaplain. I read your latest paper with great interest." + +He moved away with the Chaplain, leaving Lyra with a clear view of the Master's face. It was impassive, but the daemon on his shoulder was shuffling her feathers and moving restlessly from foot to foot. Lord Asriel was already dominating the room, and although he was careful to be courteous to the Master in the Master's own territory, it was clear where the power lay. + +The Scholars greeted the visitor and moved into the room, some sitting around the table, some in the armchairs, and soon a buzz of conversation filled the air. Lyra could see that they were powerfully intrigued by the wooden case, the screen, and the lantern. She knew the Scholars well: the Librarian, the Sub-Rector, the Enquirer, and the rest; they were men who had been around her all her life, taught her, chastised her, consoled her, given her little presents, chased her away from the fruit trees in the garden; they were all she had for a family. They might even have felt like a family if she knew what a family was, though if she did, she'd have been more likely to feel that about the college servants. The Scholars had more important things to do than attend to the affections of a half-wild, half-civilized girl, left among them by chance. + +The Master lit the spirit lamp under the little silver chafing dish and heated some butter before cutting half a dozen poppy heads open and tossing them in. Poppy was always served after a feast: it clarified the mind and stimulated the tongue, and made for rich conversation. It was traditional for the Master to cook it himself. + +Under the sizzle of the frying butter and the hum of talk, Lyra shifted around to find a more comfortable position for herself. With enormous care she took one of the robes-a full-length fur-off its hanger and laid it on the floor of the wardrobe. + +"You should have used a scratchy old one," whispered Pantalaimon. "If you get too comfortable, you'll go to sleep." + +"If I do, it's your job to wake me up," she replied. + +She sat and listened to the talk. Mighty dull talk it was, too; almost all of it politics, and London politics at that, nothing exciting about Tartars. The smells of frying poppy and smoke-leaf drifted pleasantly in through the wardrobe door, and more than once Lyra found herself nodding. But finally she heard someone rap on the table. The voices fell silent, and then the Master spoke. + +"Gentlemen," he said. "I feel sure I speak for all of us when I bid Lord Asriel welcome. His visits are rare but always immensely valuable, and I understand he has something of particular interest to show us tonight. This is a time of high political tension, as we are all aware; Lord Asriel's presence is required early tomorrow morning in White Hall, and a train is waiting with steam up ready to carry him to London as soon as we have finished our conversation here; so we must use our time wisely. When he has finished speaking to us, I imagine there will be some questions. Please keep them brief and to the point. Lord Asriel, would you like to begin?" + +"Thank you, Master," said Lord Asriel. "To start with, I have a few slides to show you. Sub-Rector, you can see best from here, I think. Perhaps the Master would like to take the chair near the wardrobe?" + +Lyra marveled at her uncle's skill. The old Sub-Rector was nearly blind, so it was courteous to make room for him nearer the screen, and his moving forward meant that the Master would be sitting next to the Librarian, only a matter of a yard or so from where Lyra was crouched in the wardrobe. As the Master settled in the armchair, Lyra heard him murmur: + +"The devil! He knew about the wine, I'm sure of it." + +The Librarian murmured back, "He's going to ask for funds. If he forces a vote-" + +"If he does that, we must just argue against, with all the eloquence we have." + +The lantern began to hiss as Lord Asriel pumped it hard. Lyra moved slightly so that she could see the screen, where a brilliant white circle had begun to glow. Lord Asriel called, "Could someone turn the lamp down?" + +One of the Scholars got up to do that, and the room darkened. + +Lord Asriel began: + +"As some of you know, I set out for the North twelve months ago on a diplomatic mission to the King of Lapland. At least, that's what I pretended to be doing. In fact, my real aim was to go further north still, right on to the ice, in fact, to try and discover what had happened to the Grumman expedition. One of Grumman's last messages to the academy in Berlin spoke of a certain natural phenomenon only seen in the lands of the North. I was determined to investigate that as well as find out what I could about Grumman. But the first picture I'm going to show you isn't directly about either of those things." + +And he put the first slide into the frame and slid it behind the lens. A circular photogram in sharp black and white appeared on the screen. It had been taken at night under a full moon, and it showed a wooden hut in the middle distance, its walls dark against the snow that surrounded it and lay thickly on the roof. Beside the hut stood an array of philosophical instruments, which looked to Lyra's eye like something from the Anbaric Park on the road to Yarnton: aerials, wires, porcelain insulators, all glittering in the moonlight and thickly covered in frost. A man in furs, his face hardly visible in the deep hood of his garment, stood in the foreground, with his hand raised as if in greeting. To one side of him stood a smaller figure. The moonlight bathed everything in the same pallid gleam. + +"That photogram was taken with a standard silver nitrate emulsion," Lord Asriel said. "I'd like you to look at another one, taken from the same spot only a minute later, with a new specially prepared emulsion." + +He lifted out the first slide and dropped another into the frame. This was much darker; it was as if the moonlight had been filtered out. The horizon was still visible, with the dark shape of the hut and its light snow-covered roof standing out, but the complexity of the instruments was hidden in darkness. But the man had altogether changed: he was bathed in light, and a fountain of glowing particles seemed to be streaming from his upraised hand. + +"That light," said the Chaplain, "is it going up or coming down?" + +"It's coming down," said Lord Asriel, "but it isn't light. It's Dust." + +Something in the way he said it made Lyra imagine dust with a capital letter, as if this wasn't ordinary dust. The reaction of the Scholars confirmed her feeling, because Lord Asriel's words caused a sudden collective silence, followed by gasps of incredulity. + +"But how-" + +"Surely-" + +"It can't-" + +"Gentlemen!" came the voice of the Chaplain. "Let Lord Asriel explain." + +"It's Dust," Lord Asriel repeated. "It registered as light on the plate because particles of Dust affect this emulsion as photons affect silver nitrate emulsion. It was partly to test it that my expedition went north in the first place. As you see, the figure of the man is perfectly visible. Now I'd like you to look at the shape to his left." + +He indicated the blurred shape of the smaller figure. + +"I thought that was the man's daemon," said the Enquirer. + +"No. His daemon was at the time coiled around his neck in the form of a snake. That shape you can dimly see is a child." + +"A severed child-?" said someone, and the way he stopped showed that he knew this was something that shouldn't have been voiced. + +There was an intense silence. + +Then Lord Asriel said calmly, "An entire child. Which, given the nature of Dust, is precisely the point, is it not?" + +No one spoke for several seconds. Then came the voice of the Chaplain. + +"Ah," he said, like a thirsty man who, having just drunk deeply, puts down the glass to let out the breath he has held while drinking. "And the streams of Dust..." + +"-Come from the sky, and bathe him in what looks like light. You may examine this picture as closely as you wish: I'll leave it behind when I go. I'm showing it to you now to demonstrate the effect of this new emulsion. Now I'd like to show you another picture." + +He changed the slide. The next picture was also taken at night, but this time without moonlight. It showed a small group of tents in the foreground, dimly outlined against the low horizon, and beside them an untidy heap of wooden boxes and a sledge. But the main interest of the picture lay in the sky. Streams and veils of light hung like curtains, looped and festooned on invisible hooks hundreds of miles high or blowing out sideways in the stream of some unimaginable wind. + +"What is that?" said the voice of the Sub-Rector. + +"It's a picture of the Aurora." + +"It's a very fine photogram," said the Palmerian Professor. "One of the best I've seen." + +"Forgive my ignorance," said the shaky voice of the old Precentor, "but if I ever knew what the Aurora was, I have forgotten. Is it what they call the Northern Lights?" + +"Yes. It has many names. It's composed of storms of charged particles and solar rays of intense and extraordinary strength-invisible in themselves, but causing this luminous radiation when they interact with the atmosphere. If there'd been time, I would have had this slide tinted to show you the colors; pale green and rose, for the most part, with a tinge of crimson along the lower edge of that curtain-like formation. This is taken with ordinary emulsion. Now I'd like you to look at a picture taken with the special emulsion." + +He took out the slide. Lyra heard the Master say quietly, "If he forces a vote, we could try to invoke the residence clause. He hasn't been resident in the college for thirty weeks out of the last fifty-two." + +"He's already got the Chaplain on his side..." the Librarian murmured in reply. + +Lord Asriel put a new slide in the lantern frame. It showed the same scene. As with the previous pair of pictures, many of the features visible by ordinary light were much dimmer in this one, and so were the curtains of radiance in the sky. + +But in the middle of the Aurora, high above the bleak landscape, Lyra could see something solid. She pressed her face to the crack to see more clearly, and she could see the Scholars near the screen leaning forward too. As she gazed, her wonder grew, because there in the sky was the unmistakable outline of a city: towers, domes, walls...Buildings and streets, suspended in the air! She nearly gasped with wonder. The Cassington Scholar said, "That looks like...a city." "Exactly so," said Lord Asriel. + +"A city in another world, no doubt?" said the Dean, with contempt in his voice. + +Lord Asriel ignored him. There was a stir of excitement among some of the Scholars, as if, having written treatises on the existence of the unicorn without ever having seen one, they'd been presented with a living example newly captured. "Is this the Barnard-Stokes Business?" said the Palmerian Professor. "It is, isn't it?" + +"That's what I want to find out," said Lord Asriel. He stood to one side of the illuminated screen. Lyra could see his dark eyes searching among the Scholars as they peered up at the slide of the Aurora, and the green glow of his demon's eyes beside him. All the venerable heads were craning forward, their spectacles glinting; only the Master and the Librarian leaned back in their chairs, with their heads close together. + +The Chaplain was saying, "You said you were searching for news of the Grumman expedition, Lord Asriel. + +Was Dr. Grumman investigating this phenomenon too?" + +"I believe he was, and I believe he had a good deal of information about it. But he won't be able to tell us what it was, because he's dead." + +"No!" said the Chaplain. + +"I'm afraid so, and I have the proof here." + +A ripple of excited apprehension ran round the Retiring Room as, under Lord Asriel's direction, two or three of the younger Scholars carried the wooden box to the front of the room. Lord Asriel took out the last slide but left the lantern on, and in the dramatic glare of the circle of light he bent to lever open the box. Lyra heard the screech of nails coming out of damp wood. The Master stood up to look, blocking Lyra's view. Her uncle spoke again: + +"If you remember, Grumman's expedition vanished eighteen months ago. The German Academy sent him up there to go as far north as the magnetic pole and make various celestial observations. It was in the course of that journey that he observed the curious phenomenon we've already seen. Shortly after that, he vanished. It's been assumed that he had an accident and that his body's been lying in a crevasse all this time. In fact, there was no accident." + +"What have you got there?" said the Dean. "Is that a vacuum container?" + +Lord Asriel didn't answer at first. Lyra heard the snap of metal clips and a hiss as air rushed into a vessel, and then there was a silence. But the silence didn't last long. After a moment or two Lyra heard a confused babble break out: cries of horror, loud protests, voices raised in anger and fear. + +"But what-" + +"-hardly human-" + +"-it's been-" + +"-what's happened to it?" + +The Master's voice cut through them all. + +"Lord Asriel, what in God's name have you got there?" + +"This is the head of Stanislaus Grumman," said Lord Asriel's voice. + +Over the jumble of voices Lyra heard someone stumble to the door and out, making incoherent sounds of distress. She wished she could see what they were seeing. + +Lord Asriel said, "I found his body preserved in the ice off Svalbard. The head was treated in this way by his killers. You'll notice the characteristic scalping pattern. I think you might be familiar with it, Sub-Rector." + +The old man's voice was steady as he said, "I have seen the Tartars do this. It's a technique you find among the aboriginals of Siberia and the Tungusk. From there, of course, it spread into the lands of the Skraelings, though I understand that it is now banned in New Denmark. May I examine it more closely, Lord Asriel?" + +After a short silence he spoke again. + +"My eyes are not very clear, and the ice is dirty, but it seems to me that there is a hole in the top of the skull. Am I right?" + +"You are." + +"Trepanning?" + +"Exactly." + +That caused a murmur of excitement. The Master moved out of the way and Lyra could see again. The old Sub-Rector, in the circle of light thrown by the lantern, was holding a heavy block of ice up close to his eyes, and Lyra could see the object inside it: a bloody lump barely recognizable as a human head. Pantalaimon fluttered around Lyra, his distress affecting her. + +"Hush," she whispered. "Listen." + +"Dr. Grumman was once a Scholar of this college," said the Dean hotly. + +"To fall into the hands of the Tartars-" "But that far north?" + +"They must have penetrated further than anyone imagined!" + +"Did I hear you say you found it near Svalbard?" said the Dean. + +"That's right." + +"Are we to understand that the panserbj0rne had anything to do with this?" + +Lyra didn't recognize that word, but clearly the Scholars did. + +"Impossible," said the Cassington Scholar firmly. "They'd never behave in that manner." + +"Then you don't know lofur Raknison," said the Palmerian Professor, who had made several expeditions himself to the arctic regions. "It wouldn't surprise me at all to learn that he had taken to scalping people in the Tartar fashion." + +Lyra looked again at her uncle, who was watching the Scholars with a glitter of sardonic amusement, and saying nothing. + +"Who is lofur Raknison?" said someone. "The king of Svalbard," said the Palmerian Professor. "Yes, that's right, one of the panserb)0me. He's a usurper, of sorts; tricked his way onto the throne, or so I understand; but a powerful figure, by no means a fool, in spite of his ludicrous affectations-having a palace built of imported marble-setting up what he calls a university-" + +"For whom? For the bears?" said someone else, and every-one laughed. + +But the Palmerian Professor went on: "For all that, I tell you that lofur Raknison would be capable of doing this to Grumman. At the same time, he could be flattered into behaving quite differently, if the need arose." + +"And you know how, do you, Trelawney?" said the Dean sneeringly. + +"Indeed I do. Do you know what he wants above all else? Even more than an honorary degree? He wants a daemon! Find a way to give him a daemon, and he'd do anything for you." + +The Scholars laughed heartily. + +Lyra was following this with puzzlement; what the Palmerian Professor said made no sense at all. Besides, she was impatient to hear more about scalping and the Northern Lights and that mysterious Dust. But she was disappointed, for Lord Asriel had finished showing his relics and pictures, and the talk soon turned into a college wrangle about whether or not they should give him some money to fit out another expedition. Back and forth the arguments ranged, and Lyra felt her eyes closing. Soon she was fast asleep, with Pantalaimon curled around her neck in his favorite sleeping form as an ermine. + + + +She woke up with a start when someone shook her shoulder. + +"Quiet," said her uncle. The wardrobe door was open, and he was crouched there against the light. "They've all gone, but there are still some servants around. Go to your bedroom now, and take care that you say nothing about this." + +"Did they vote to give you the money?" she said sleepily. + +"Yes." + +"What's Dust?" she said, struggling to stand up after having been cramped for so long. + +"Nothing to do with you." + +"It is to do with me," she said. "If you wanted me to be a spy in the wardrobe, you ought to tell me what I'm spying about. Can I see the man's head?" + +Pantalaimon's white ermine fur bristled: she felt it tickling her neck. Lord Asriel laughed shortly. + +"Don't be disgusting," he said, and began to pack his slides and specimen box. "Did you watch the Master?" + +"Yes, and he looked for the wine before he did anything else." + +"Good. But I've scotched him for now. Do as you're told and go to bed." + +"But where are you going?" + +"Back to the North. I'm leaving in ten minutes." + +"Can I come?" + +He stopped what he was doing, and looked at her as if for the first time. His daemon turned her great tawny leopard eyes on her too, and under the concentrated gaze of both of them, Lyra blushed. But she gazed back fiercely. + +"Your place is here," said her uncle finally. + +"But why? Why is my place here? Why can't I come to the North with you? I want to see the Northern Lights and bears and icebergs and everything. I want to know about Dust. And that city in the air. Is it another world?" + +"You're not coming, child. Put it out of your head; the times are too dangerous. Do as you're told and go to bed, and if you're a good girl, I'll bring you back a walrus tusk with some Eskimo carving on it. Don't argue anymore or I shall be angry." + +And his daemon growled with a deep savage rumble that made Lyra suddenly aware of what it would be like to have teeth meeting in her throat. + +She compressed her lips and frowned hard at her uncle. He was pumping the air from the vacuum flask, and took no notice; it was as if he'd already forgotten her. Without a word, but with lips tight and eyes narrowed, the girl and her daemon left and went to bed. + + + +* * * + + + +The Master and the Librarian were old friends and allies, and it was their habit, after a difficult episode, to take a glass of brantwijn and console each other. So after they'd seen Lord Asriel away, they strolled to the Master's lodging and settled in his study with the curtains drawn and the fire refreshed, their daemons in their familiar places on knee or shoulder, and prepared to think through what had just happened. + +"Do you really believe he knew about the wine?" said the Librarian. + +"Of course he did. I have no idea how, but he knew, and he spilled the decanter himself. Of course he did." + +"Forgive me, Master, but I can't help being relieved. I was never happy about the idea of..." + +"Of poisoning him?" + +"Yes. Of murder." + +"Hardly anyone would be happy at that idea, Charles. The question was whether doing that would be worse than the consequences of not doing it. Well, some providence has intervened, and it hasn't happened. I'm only sorry I burdened you with the knowledge of it." + +"No, no," protested the Librarian. "But I wish you had told me more. + +The Master was silent for a while before saying, "Yes, perhaps I should have done. The alethiometer warns of appalling consequences if Lord Asriel pursues this research. Apart from anything else, the child will be drawn in, and I want to keep her safe as long as possible." + +"Is Lord Asriel's Business anything to do with this new initiative of the Consistorial Court of Discipline? The what-do-they-call-it: the Oblation Board?" + +"Lord Asriel-no, no. Quite the reverse. The Oblation Board isn't entirely answerable to the Consistorial Court, either. It's a semiprivate initiative; it's being run by someone + +who has no love of Lord Asriel. Between them both, Charles, I tremble." + +The Librarian was silent in his turn. Ever since Pope John Calvin had moved the seat of the Papacy to Geneva and set up the Consistorial Court of Discipline, the Church's power over every aspect of life had been absolute. The Papacy itself had been abolished after Calvin's death, and a tangle of courts, colleges, and councils, collectively known as the Magisterium, had grown up in its place. These agencies were not always united; sometimes a bitter rivalry grew up between them. For a large part of the previous century, the most powerful had been the college of Bishops, but in recent years the Consistorial Court of Discipline had taken its place as the most active and the most feared of all the Church's bodies. + +But it was always possible for independent agencies to grow up under the protection of another part of the Magisterium, and the Oblation Board, which the Librarian had referred to, was one of these. The Librarian didn't know much about it, but he disliked and feared what he'd heard, and he completely understood the Master's anxiety. + +"The Palmerian Professor mentioned a name," he said after a minute or so. "Barnard-Stokes? What is the Barnard-Stokes Business?" + +"Ah, it's not our field, Charles. As I understand it, the Holy Church teaches that there are two worlds: the world of everything we can see and hear and touch, and another world, the spiritual world of heaven and hell. Barnard and Stokes were two-how shall I put it-renegade theologians who postulated the existence of numerous other worlds like this one, neither heaven nor hell, but material and sinful. They are there, close by, but invisible and unreachable. The Holy Church naturally disapproved of this abominable heresy, and Barnard and Stokes were silenced. + +"But unfortunately for the Magisterium there seem to be sound mathematical arguments for this other-world theory. I have never followed them myself, but the Cassington Scholar tells me that they are sound." + +"And now Lord Asriel has taken a picture of one of these other worlds," the Librarian said. "And we have funded him to go and look for it. I see." + +"Quite. It'll seem to the Oblation Board, and to its powerful protectors, that Jordan college is a hotbed of support for heresy. And between the Consistorial Court and the Oblation Board, Charles, I have to keep a balance; and meanwhile the child is growing. They won't have forgotten her. Sooner or later she would have become involved, but she'll be drawn in now whether I want to protect her or not." + +"But how do you know that, for God's sake? The alethiometer again?" + +"Yes. Lyra has a part to play in all this, and a major one. The irony is that she must do it all without realizing what she's doing. She can be helped, though, and if my plan with the Tokay had succeeded, she would have been safe for a little longer. I would have liked to spare her a journey to the North. I wish above all things that I were able to explain it to her..." + +"She wouldn't listen," the Librarian said. "I know her ways only too well. Try to tell her anything serious and she'll half-listen for five minutes and then start fidgeting. Quiz her about it next time and she'll have completely forgotten." + +"If I talked to her about Dust? You don't think she'd listen to that?" + +The Librarian made a noise to indicate how unlikely he thought that was. + +"Why on earth should she?" he said. "Why should a distant theological riddle interest a healthy, thoughtless child?" + +"Because of what she must experience. Part of that includes a great betrayal...." + +"Who's going to betray her?" + +"No, no, that's the saddest thing: she will be the betrayer, and the experience will be terrible. She mustn't know that, of course, but there's no reason for her not to know about the problem of Dust. And you might be wrong, Charles; she might well take an interest in it, if it were explained in a simple way. And it might help her later on. It would certainly help me to be less anxious about her." + +"That's the duty of the old," said the Librarian, "to be anxious on behalf of the young. And the duty of the young is to scorn the anxiety of the old." + +They sat for a while longer, and then parted, for it was late, and they were old and anxious. + +THREE + +LYRA'S JORDAN + +Jordan College was the grandest and richest of all the colleges in Oxford. It was probably the largest, too, though no one knew for certain. The buildings, which were grouped around three irregular quadrangles, dated from every period from the early Middle Ages to the mid-eighteenth century. It had never been planned; it had grown piecemeal, with past and present overlapping at every spot, and the final effect was one of jumbled and squalid grandeur. Some part was always about to fall down, and for five generations the same family, the Parslows, had been employed full time by the college as masons and scaffolders. The present Mr. Parslow was teaching his son the craft; the two of them and their three workmen would scramble like industrious termites over the scaffolding they'd erected at the corner of the library, or over the roof of the chapel, and haul up bright new blocks of stone or rolls of shiny lead or balks of timber. + +The college owned farms and estates all over England. It was said that you could walk from Oxford to Bristol in one direction and London in the other, and never leave Jordan land. In every part of the kingdom there were dye works and brick kilns, forests and atomcraft works that paid rent to Jordan, and every quarter-day the bursar and his clerks would tot it all up, announce the total to Concilium, and order a pair of swans for the feast. Some of the money was put by for reinvestment-Concilium had just approved the purchase of an office block in Manchester-and the rest was used to pay the Scholars' modest stipends and the wages of the servants (and the Parslows, and the other dozen or so families of craftsmen and traders who served the college), to keep the wine cellar richly filled, to buy books and anbarographs for the immense library that filled one side of the Melrose Quadrangle and extended, burrow-like, for several floors beneath the ground, and, not least, to buy the latest philosophical apparatus to equip the chapel. + +It was important to keep the chapel up to date, because Jordan College had no rival, either in Europe or in New France, as a center of experimental theology. Lyra knew that much, at least. She was proud of her college's eminence, and liked to boast of it to the various urchins and ragamuffins she played with by the canal or the claybeds; and she regarded visiting Scholars and eminent professors from elsewhere with pitying scorn, because they didn't belong to Jordan and so must know less, poor things, than the humblest of Jordan's under-Scholars. + +As for what experimental theology was, Lyra had no more idea than the urchins. She had formed the notion that it was concerned with magic, with the movements of the stars and planets, with tiny particles of matter, but that was guesswork, really. Probably the stars had daemons just as humans did, and experimental theology involved talking to them. Lyra imagined the Chaplain speaking loftily, listening to the star daemons' remarks, and then nodding judiciously or shaking his head in regret. But what might be passing between them, she couldn't conceive. + +Nor was she particularly interested. In many ways Lyra was a barbarian. What she liked best was clambering over the college roofs with Roger, the kitchen boy who was her particular friend, to spit plum stones on the heads of passing Scholars or to hoot like owls outside a window where a tutorial was going on, or racing through the narrow streets, or stealing apples from the market, or waging war. Just as she was unaware of the hidden currents of politics running below the surface of college affairs, so the Scholars, for their part, would have been unable to see the rich seething stew of alliances and enmities and feuds and treaties which was a child's life in Oxford. Children playing together: how pleasant to see! What could be more innocent and charming? + +In fact, of course, Lyra and her peers were engaged in deadly warfare. There were several wars running at once. The children (young servants, and the children of servants, and Lyra) of one college waged war on those of another. Lyra had once been captured by the children of Gabriel college, and Roger and their friends Hugh Lovat and Simon Parslow had raided the place to rescue her, creeping through the Precentor's garden and gathering armfuls of small stone-hard plums to throw at the kidnappers. There were twenty-four colleges, which allowed for endless permutations of alliance and betrayal. But the enmity between the colleges was forgotten in a moment when the town children attacked a colleger: then all the collegers banded together and went into battle against the town-ies.This rivalry was hundreds of years old, and very deep and satisfying. + +But even this was forgotten when the other enemies threatened. One enemy was perennial: the brickburners' children, who lived by the claybeds and were despised by collegers and townies alike. Last year Lyra and some townies had made a temporary truce and raided the claybeds, pelting the brick-burners' children with lumps of heavy clay and tipping over the soggy castle they'd built, before rolling them over and over in the clinging substance they lived by until victors and vanquished alike resembled a flock of shrieking golems. + +The other regular enemy was seasonal. The gyptian families, who lived in canal boats, came and went with the spring and autumn fairs, and were always good for a fight. There was one family of gyptians in particular, who regularly returned to their mooring in that part of the city known as Jericho, with whom Lyra'd been feuding ever since she could first throw a stone. When they were last in Oxford, she and Roger and some of the other kitchen boys from Jordan and St. Michael's college had laid an ambush for them, throwing mud at their brightly painted narrowboat until the whole family came out to chase them away-at which point the reserve squad under Lyra raided the boat and cast it off from the bank, to float down the canal, getting in the way of all the other water traffic while Lyra's raiders searched the boat from end to end, looking for the bung. Lyra firmly believed in this bung. If they pulled it out, she assured her troop, the boat would sink at once; but they didn't find it, and had to abandon ship when the gyptians caught them up, to flee dripping and crowing with triumph through the narrow lanes of Jericho. + +That was Lyra's world and her delight. She was a coarse and greedy little savage, for the most part. But she always had a dim sense that it wasn't her whole world; that part of her also belonged in the grandeur and ritual of Jordan college; and that somewhere in her life there was a connection with the high world of politics represented by Lord Asriel. All she did with that knowledge was to give herself airs and lord it over the other urchins. It had never occurred to her to find out more. + +So she had passed her childhood, like a half-wild cat. The only variation in her days came on those irregular occasions when Lord Asriel visited the college. A rich and powerful uncle was all very well to boast about, but the price of boasting was having to be caught by the most agile Scholar and brought to the Housekeeper to be washed and dressed in a clean frock, following which she was escorted (with many threats) to the Senior Common Room to have tea with Lord Asriel and an invited group of senior Scholars. She dreaded being seen by Roger. He'd caught sight of her on one of these occasions and hooted with laughter at this beribboned and pink-frilled vision. She had responded with a volley of shrieking curses that shocked the poor Scholar who was escorting her, and in the Senior Common Room she'd slumped mutinously in an armchair until the Master told her sharply to sit up, and then she'd glowered at them all till even the Chaplain had to laugh. + +What happened on those awkward, formal visits never varied. After the tea, the Master and the other few Scholars who'd been invited left Lyra and her uncle together, and he called her to stand in front of him and tell him what she'd learned since his last visit. And she would mutter whatever she could dredge up about geometry or Arabic or history or anbarology, and he would sit back with one ankle resting on the other knee and watch her inscrutably until her words failed. + +Last year, before his expedition to the North, he'd gone on to say, "And how do you spend your time when you're not diligently studying?" + +And she mumbled, "I just play. Sort of around the college. Just...play, really." + +And he said, "Let me see your hands, child." + +She held out her hands for inspection, and he took them and turned them over to look at her fingernails. Beside him, his daemon lay sphinxlike on the carpet, swishing her tail occasionally and gazing unblinkingly at Lyra. + +"Dirty," said Lord Asriel, pushing her hands away. "Don't they make you wash in this place?" + +"Yes," she said. "But the Chaplain's fingernails are always dirty. They're even dirtier than mine." + +"He's a learned man. What's your excuse?" + +"I must've got them dirty after I washed." + +"Where do you play to get so dirty?" + +She looked at him suspiciously. She had the feeling that being on the roof was forbidden, though no one had actually said so. "In some of the old rooms," she said finally. + +"And where else?" + +"In the claybeds, sometimes." + +"And?" + +"Jericho and Port Meadow." + +"Nowhere else?" + +"No." + +"You're a liar. I saw you on the roof only yesterday." + +She bit her lip and said nothing. He was watching her sardonically. + +"So, you play on the roof as well," he went on. "Do you ever go into the library?" + +"No. I found a rook on the library roof, though," she went on. + +"Did you? Did you catch it?" + +"It had a hurt foot. I was going to kill it and roast it but Roger said we should help it get better. So we gave it scraps of food and some wine and then it got better and flew away." + +"Who's Roger?" + +"My friend. The kitchen boy." + +"I see. So you've been all over the roof-" + +"Not all over. You can't get onto the Sheldon Building because you have to jump up from Pilgrim's Tower across a gap. There's a skylight that opens onto it, but I'm not tall enough to reach it." + +"You've been all over the roof except the Sheldon Building. What about underground?" + +"Underground?" + +"There's as much college below ground as there is above it. I'm surprised you haven't found that out. Well, I'm going in a minute. You look healthy enough. Here." + +He fished in his pocket and drew out a handful of coins, from which he gave her five gold dollars. + +"Haven't they taught you to say thank you?" he said. + +"Thank you," she mumbled. + +"Do you obey the Master?" + +"Oh, yes." + +"And respect the Scholars?" + +"Yes." + +Lord Asriel's daemon laughed softly. It was the first sound she'd made, and Lyra blushed. + +"Go and play, then," said Lord Asriel. + +Lyra turned and darted to the door with relief, remembering to turn and blurt out a "Goodbye." + + + +So Lyra's life had been, before the day when she decided to hide in the Retiring Room, and first heard about Dust. + +And of course the Librarian was wrong in saying to the Master that she wouldn't have been interested. She would have listened eagerly now to anyone who could tell her about Dust. She was to hear a great deal more about it in the months to come, and eventually she would know more about Dust than anyone in the world; but in the meantime, there was all the rich life of Jordan still being lived around her. + +And in any case there was something else to think about. A rumor had been filtering through the streets for some weeks: a rumor that made some people laugh and others grow silent, as some people scoff at ghosts and others fear them. For no reason that anyone could imagine, children were beginning to disappear. + + + +* * * + + + +It would happen like this. + +East along the great highway of the River Isis, thronged with slow-moving brick barges and asphalt boats and corn tankers, way down past Henley and Maidenhead to Teddington, where the tide from the German Ocean reaches, and further down still: to Mortlake, past the house of the great magician Dr. Dee; past Falkeshall, where the pleasure gardens spread out bright with fountains and banners by day, with tree lamps and fireworks by night; past White Hall Palace, where the king holds his weekly council of state; past the Shot Tower, dropping its endless drizzle of molten lead into vats of murky water; further down still, to where the river, wide and filthy now, swings in a great curve to the south. + +This is Limehouse, and here is the child who is going to disappear. + +He is called Tony Makarios. His mother thinks he's nine years old, but she has a poor memory that the drink has rotted; he might be eight, or ten. His surname is Greek, but like his age, that is a guess on his mother's part, because he looks more Chinese than Greek, and there's Irish and Skraeling and Lascar in him from his mother's side too. Tony's not very bright, but he has a sort of clumsy tenderness that sometimes prompts him to give his mother a rough hug and plant a sticky kiss on her cheeks. The poor woman is usually too fuddled to start such a procedure herself; but she responds warmly enough, once she realizes what's happening. + +At the moment Tony is hanging about the market in Pie Street. He's hungry. It's early evening, and he won't get fed at Home. He's got a shilling in his pocket that a soldier gave him for taking a message to his best girl, but Tony's not going to waste that on food, when you can pick up so much for nothing. + +So he wanders through the market, between the old-clothes stalls and the fortune-paper stalls, the fruitmongers and the fried-fish seller, with his little daemon on his shoulder, a sparrow, watching this way and that; and when a stall holder and her daemon are both looking elsewhere, a brisk chirp sounds, and Tony's hand shoots out and returns to his loose shirt with an apple or a couple of nuts, and finally with a hot pie. + +The stall holder sees that, and shouts, and her cat daemon leaps, but Tony's sparrow is aloft and Tony himself halfway down the street already. Curses and abuse go with him, but not far. He stops running at the steps of St. Catherine's Oratory, where he sits down and takes out his steaming, battered prize, leaving a trail of gravy on his shirt. + +And he's being watched. A lady in a long yellow-red fox-fur coat, a beautiful young lady whose dark hair falls, shining delicately, under the shadow of her fur-lined hood, is standing in the doorway of the oratory, half a dozen steps above him. It might be that a service is finishing, for light comes from the doorway behind her, an organ is playing inside, and the lady is holding a jeweled breviary. + +Tony knows nothing of this. His face contentedly deep in the pie, his toes curled inward and his bare soles together, he sits and chews and swallows while his daemon becomes a mouse and grooms her whiskers. + +The young lady's daemon is moving out from beside the fox-fur coat. He is in the form of a monkey, but no ordinary monkey: his fur is long and silky and of the most deep and lustrous gold. With sinuous movements he inches down the steps toward the boy, and sits a step above him. + +Then the mouse senses something, and becomes a sparrow again, cocking her head a fraction sideways, and hops along the stone a step or two. + +The monkey watches the sparrow; the sparrow watches the monkey. + +The monkey reaches out slowly. His little hand is black, his nails perfect horny claws, his movements gentle and inviting. The sparrow can't resist. She hops further, and further, and then, with a little flutter, up on to the monkey's hand. + +The monkey lifts her up, and gazes closely at her before standing and swinging back to his human, taking the sparrow daemon with him. The lady bends her scented head to whisper. + +And then Tony turns. He can't help it. + +"Ratter!" he says, half in alarm, his mouth full. + +The sparrow chirps. It must be safe. Tony swallows his mouthful and stares. + +"Hello," says the beautiful lady. "What's your name?" + +"Tony." + +"Where do you live, Tony?" + +"Clarice Walk." + +"What's in that pie?" + +"Beefsteak." + +"Do you like chocolatl?" + +"Yeah!" + +"As it happens, I've got more chocolatl than I can drink myself. Will you come and help me drink it?" + +He's lost already. He was lost the moment his slow-witted daemon hopped onto the monkey's hand. He follows the beautiful young lady and the golden monkey down Denmark Street and along to Hangman's Wharf, and down King George's Steps to a little green door in the side of a tall warehouse. She knocks, the door is opened, they go in, the door is closed. Tony will never come out-at least, by that entrance; and he'll never see his mother again. She, poor drunken thing, will think he's run away, and when she remembers him, she'll think it was her fault, and sob her sorry heart out. + + + +* * * + + + +Little Tony Makarios wasn't the only child to be caught by the lady with the golden monkey. He found a dozen others in the cellar of the warehouse, boys and girls, none older than twelve or so; though since all of them had histories like his, none could be sure of their age. What Tony didn't notice, of course, was the factor that they all had in common. None of the children in that warm and steamy cellar had reached the age of puberty. + +The kind lady saw him settled on a bench against the wall, and provided by a silent serving woman with a mug of chocolatl from the saucepan on the iron stove. Tony ate the rest of his pie and drank the sweet hot liquor without taking much notice of his surroundings, and the surroundings took little notice of him: he was too small to be a threat, and too stolid to promise much satisfaction as a victim. + +It was another boy who asked the obvious question. + +"Hey, lady! What you got us all here for?" + +He was a tough-looking wretch with dark chocolatl on his top lip and a gaunt black rat for a daemon. The lady was standing near the door, talking to a stout man with the air of a sea captain, and as she turned to answer, she looked so angelic in the hissing naphtha light that all the children fell silent. + +"We want your help," she said. "You don't mind helping us, do you?" + +No one could say a word. They all gazed, suddenly shy. They had never seen a lady like this; she was so gracious and sweet and kind that they felt they hardly deserved their good luck, and whatever she asked, they'd give it gladly so as to stay in her presence a little longer. + +She told them that they were going on a voyage. They would be well fed and warmly clothed, and those who wanted to could send messages back to their families to let them know they were safe. Captain Magnusson would take them on board his ship very soon, and then when the tide was right, they'd sail out to sea and set a course for the North. + +Soon those few who did want to send a message to what-ever Home they had were sitting around the beautiful lady as she wrote a few lines at their dictation and, having let them scratch a clumsy X at the foot of the page, folded it into a scented envelope and wrote the address they told her. Tony would have liked to send something to his mother, but he had a realistic idea of her ability to read it. He plucked at the lady's fox-fur sleeve and whispered that he'd like her to tell his mum where he was going, and all, and she bent her gracious head close enough to his malodorous little body to hear, and stroked his head and promised to pass the message on. + +Then the children clustered around to say goodbye. The golden monkey stroked all their daemons, and they all touched the fox fur for luck, or as if they were drawing some strength or hope or goodness out of the lady, and she bade them all farewell and saw them in the care of the bold captain on board a steam launch at the jetty. The sky was dark now, the river a mass of bobbing lights. The lady stood on the jetty and waved till she could see their faces no more. + +Then she turned back inside, with the golden monkey nestled in her breast, and threw the little bundle of letters into the furnace before leaving the way she had come. + + + +Children from the slums were easy enough to entice away, but eventually people noticed, and the police were stirred into reluctant action. For a while there were no more bewitchings. But a rumor had been born, and little by little it changed and grew and spread, and when after a while a few children disappeared in Norwich, and then Sheffield, and then Manchester, the people in those places who'd heard of the disappearances elsewhere added the new vanishings to the story and gave it new strength. + +And so the legend grew of a mysterious group of enchanters who spirited children away. Some said their leader was a beautiful lady, others said a tall man with red eyes, while a third story told of a youth who laughed and sang to his victims so that they followed him like sheep. + +As for where they took these lost children, no two stories agreed. Some said it was to Hell, under the ground, to Fairyland. Others said to a farm where the children were kept and fattened for the table. Others said that the children were kept and sold as slaves to rich Tartars....And so on. + +But one thing on which everyone agreed was the name of these invisible kidnappers. They had to have a name, or not be referred to at all, and talking about them-especially if you were safe and snug at Home, or in Jordan college-was delicious. And the name that seemed to settle on them, without anyone's knowing why, was the Gobblers. + +"Don't stay out late, or the Gobblers'11 get you!" + +"My cousin in Northampton, she knows a woman whose little boy was took by the Gobblers...." + +"The Gobblers've been in Stratford. They say they're coming south!" + + + +And, inevitably: + +"Let's play kids and Gobblers!" + +So said Lyra to Roger, one rainy afternoon when they were alone in the dusty attics. He was her devoted slave by this time; he would have followed her to the ends of the earth. + +"How d'you play that?" + +"You hide and I find you and slice you open, right, like the Gobblers do." + +"You don't know what they do. They might not do that at all." + +"You're afraid of 'em," she said. "I can tell." + +"I en't. I don't believe in 'em anyway." + +"I do," she said decisively. "But I en't afraid either. I'd just do what my uncle done last time he came to Jordan. I seen him. He was in the Retiring Room and there was this guest who weren't polite, and my uncle just give him a hard look and the man fell dead on the spot, with all foam and froth round his mouth." + +"He never," said Roger doubtfully. "They never said anything about that in the kitchen. Anyway, you en't allowed in the Retiring Room." + +'"Course not. They wouldn't tell servants about a thing like that. And I have been in the Retiring Room, so there. Anyway, my uncle's always doing that. He done it to some Tartars when they caught him once. They tied him up and they was going to cut his guts out, but when the first man come up with the knife, my uncle just looked at him, and he fell dead, so another one come up and he done the same to him, and finally there was only one left. My uncle said he'd leave him alive if he untied him, so he did, and then my uncle killed him anyway just to teach him a lesson." + +Roger was less sure about that than about Gobblers, but the story was too good to waste, so they took it in turns to be Lord Asriel and the expiring Tartars, using sherbet dip for the foam. + +However, that was a distraction; Lyra was still intent on playing Gobblers, and she inveigled Roger down into the wine cellars, which they entered by means of the Butler's spare set of keys. Together they crept through the great vaults where the college's Tokay and Canary, its Burgundy, its brantwijn were lying under the cobwebs of ages. Ancient stone arches rose above them supported by pillars as thick as ten trees, irregular flagstones lay underfoot, and on all sides were ranged rack upon rack, tier upon tier, of bottles and barrels. It was fascinating. With Gobblers forgotten again, the two children tiptoed from end to end holding a candle in trembling fingers, peering into every dark corner, with a single question growing more urgent in Lyra's mind every moment: what did the wine taste like? + +There was an easy way of answering that. Lyra-over Roger's fervent protests-picked out the oldest, twistiest, greenest bottle she could find, and, not having anything to extract the cork with, broke it off at the neck. Huddled in the furthest corner, they sipped at the heady crimson liquor, wondering when they'd become drunk, and how they'd tell when they were. Lyra didn't like the taste much, but she had to admit how grand and complicated it was. The funniest thing was watching their two daemons, who seemed to be getting more and more muddled: falling over, giggling senselessly, and changing shape to look like gargoyles, each trying to be uglier than the other. + +Finally, and almost simultaneously, the children discovered what it was like to be drunk. + +"Do they like doing this?" gasped Roger, after vomiting copiously. + +"Yes," said Lyra, in the same condition. "And so do I," she added stubbornly. + + + +Lyra learned nothing from that episode except that playing Gobblers led to interesting places. She remembered her uncle's words in their last interview, and began to explore underground, for what was above ground was only a small fraction of the whole. Like some enormous fungus whose root system extended over acres, Jordan (finding itself jostling for space above ground with St. Michael's College on one side, Gabriel College on the other, and the University Library behind) had begun, sometime in the Middle Ages, to spread below the surface. Tunnels, shafts, vaults, cellars, staircases had so hollowed out the earth below Jordan and for several hundred yards around it that there was almost as much air below ground as above; Jordan college stood on a sort of froth of stone. + +And now that Lyra had the taste for exploring it, she abandoned her usual haunt, the irregular alps of the college roofs, and plunged with Roger into this netherworld. From playing at Gobblers she had turned to hunting them, for what could be more likely than that they were lurking out of sight below the ground? + +So one day she and Roger made their way into the crypt below the oratory. This was where generations of Masters had been buried, each in his lead-lined oak coffin in niches along the stone walls. A stone tablet below each space gave their names: + + + +SIMON LE CLERC, MASTER 1765-1789 CEREBATON + +REQUIESCANT IN PACE + + + + + +"What's that mean?" said Roger. + +"The first part's his name, and the last bit's Roman. And there's the dates in the middle when he was Master. And the other name must be his daemon." + +They moved along the silent vault, tracing the letters of more inscriptions: + + + +FRANCIS LYALL, MASTER 1748-1765 ZOHARIEL + +REQUIESCANT IN PACE + + + +ICNATIUS COLE, MASTER 1745-1748 MUSCA + +REQUIESCANT IN PACE + + + +On each coffin, Lyra was interested to see, a brass plaque bore a picture of a different being: this one a basilisk, this a serpent, this a monkey. She realized that they were images of the dead men's daemons. As people became adult, their daemons lost the power to change and assumed one shape, keeping it permanently. + +"These coffins' ve got skeletons in "em!" whispered Roger. + +"Moldering flesh," whispered Lyra. "And worms and maggots all twisting about in their eye sockets." + +"Must be ghosts down here," said Roger, shivering pleasantly. + +Beyond the first crypt they found a passage lined with stone shelves. Each shelf was partitioned off into square sections, and in each section rested a skull. + +Roger's daemon, tail tucked firmly between her legs, shivered against him and gave a little quiet howl. + +"Hush," he said. + +Lyra couldn't see Pantalaimon, but she knew his moth form was resting on her shoulder and probably shivering too. + +She reached up and lifted the nearest skull gently out of its resting place. + +"What you doing?" said Roger. "You en't supposed to touch em." + +She turned it over and over, taking no notice. Something suddenly fell out of the hole at the base of the skull - fell through her fingers and rang as it hit the floor, and she nearly dropped the skull in alarm. + +"It's a coin!" said Roger, feeling for it. "Might be treasure!" + +He held it up to the candle and they both gazed wide-eyed. It was not a coin, but a little disc of bronze with a crudely engraved inscription showing a cat. + +"It's like the ones on the coffins," said Lyra. "It's his daemon. Must be." + +"Better put it back," said Roger uneasily, and Lyra upturned the skull and dropped the disk back into its immemorial resting place before returning the skull to the shelf. Each of the other skulls, they found, had its own daemon-coin, showing its owner's lifetime companion still close to him in death. + +"Who d'you think these were when they were alive?" said Lyra. "Probably Scholars, I reckon. Only the Masters get coffins. There's probably been so many Scholars all down the centuries that there wouldn't be room to bury the whole of 'em, so they just cut their heads off and keep them. That's the most important part of 'em anyway." + +They found no Gobblers, but the catacombs under the oratory kept Lyra and Roger busy for days. Once she tried to play a trick on some of the dead Scholars, by switching around the coins in their skulls so they were with the wrong daemons. Pantalaimon became so agitated at this that he changed into a bat and flew up and down uttering shrill cries and flapping his wings in her face, but she took no notice: it was too good a joke to waste. She paid for it later, though. In bed in her narrow room at the top of Staircase Twelve she was visited by a night-ghast, and woke up screaming at the three robed figures who stood at the bedside pointing their bony fingers before throwing back their cowls to show bleeding stumps where their heads should have been. Only when Pantalaimon became a lion and roared at them did they retreat, backing away into the substance of the wall until all that was visible was their arms, then their horny yellow-gray hands, then their twitching fingers, then nothing. First thing in the morning she hastened down to the catacombs and restored the daemon-coins to their rightful places, and whispered "Sorry! Sorry!" to the skulls. + +The catacombs were much larger than the wine cellars, but they too had a limit. When Lyra and Roger had explored every corner of them and were sure there were no Gobblers to be found there, they turned their attention elsewhere-but not before they were spotted leaving the crypt by the Intercessor, who called them back into the oratory. + +The Intercessor was a plump, elderly man known as Father Heyst. It was his job to lead all the college services, to preach and pray and hear confessions. When Lyra was younger, he had taken an interest in her spiritual welfare, only to be confounded by her sly indifference and insincere repentances. She was not spiritually promising, he had decided. + +When they heard him call, Lyra and Roger turned reluctantly and walked, dragging their feet, into the great musty-smelling dimness of the oratory. Candles flickered here and there in front of images of the saints; a faint and distant clatter came from the organ loft, where some repairs were going on; a servant was polishing the brass lectern. Father Heyst beckoned from the vestry door. + +"Where have you been?" he said to them. "I've seen you come in here two or three times now. What are you up to?" + +His tone was not accusatory. He sounded as if he were genuinely interested. His daemon flicked a lizard tongue at them from her perch on his shoulder. + +Lyra said, "We wanted to look down in the crypt." + +"Whatever for?" + +"The...the coffins. We wanted to see all the coffins," she said. + +"But why?" + +She shrugged. It was her constant response when she was pressed. + +"And you," he went on, turning to Roger. Roger's daemon anxiously wagged her terrier tail to propitiate him. "What's your name?" + +"Roger, Father." + +"If you're a servant, where do you work?" "In the kitchen, Father." "Should you be there now?" "Yes, Father." "Then be off with you." + +Roger turned and ran. Lyra dragged her foot from side to side on the floor. + +"As for you, Lyra," said Father Heyst, "I'm pleased to see you taking an interest in what lies in the oratory. You are a lucky child, to have all this history around you." "Mm," said Lyra. + +"But I wonder about your choice of companions. Are you a lonely child?" "No," she said. + +"Do you...do you miss the society of other children?" "No." + +"I don't mean Roger the kitchen boy. I mean children such as yourself. Nobly born children. Would you like to have some companions of that sort?" "No." + +"But other girls, perhaps..." "No." + +"You see, none of us would want you to miss all the usual childhood pleasures and pastimes. I sometimes think it must be a lonely life for you here among a company of elderly Scholars, Lyra. Do you feel that?" "No." + +He tapped his thumbs together over his interlaced fingers, unable to think of anything else to ask this stubborn child. + +"If there is anything troubling you," he said finally, "you know you can come and tell me about it. I hope you feel you can always do that." "Yes," she said. + +"Do you say your prayers?" + +"Yes." + +"Good girl. Well, run along." + +With a barely concealed sigh of relief, she turned and left. Having failed to find Gobblers below ground, Lyra took to the streets again. She was at Home there. + +Then, almost when she'd lost interest in them, the Gobblers appeared in Oxford. + + + +The first Lyra heard of it was when a young boy went missing from a gyptian family she knew. + +It was about the time of the horse fair, and the canal basin was crowded with narrowboats and butty boats, with traders and travelers, and the wharves along the waterfront in Jericho were bright with gleaming harness and loud with the clop of hooves and the clamor of bargaining. Lyra always enjoyed the horse fair; as well as the chance of stealing a ride on a less-than-well-attended horse, there were endless opportunities for provoking warfare. + +And this year she had a grand plan. Inspired by the capture of the narrowboat the year before, she intended this time to make a proper voyage before being turned out. If she and her cronies from the college kitchens could get as far as Abingdon, they could play havoc with the weir.... + +But this year there was to be no war. Something else happened. Lyra was sauntering along the edge of the Port Meadow boatyard in the morning sun, without Roger for once (he had been detailed to wash the buttery floor) but with Hugh Lovat and Simon Parslow, passing a stolen cigarette from one to another and blowing out the smoke ostentatiously, when she heard a cry in a voice she recognized. + +"Well, what have you done with him, you half-arsed pillock?" + +It was a mighty voice, a woman's voice, but a woman with lungs of brass and leather. Lyra looked around for her at once, because this was Ma Costa, who had clouted Lyra dizzy on two occasions but given her hot gingerbread on three, and whose family was noted for the grandeur and sumptu-ousness of their boat. They were princes among gyptians, and Lyra admired Ma Costa greatly, but she intended to be wary of her for some time yet, for theirs was the boat she had hijacked. + +One of Lyra's brat companions picked up a stone automatically when he heard the commotion, but Lyra said, "Put it down. She's in a temper. She could snap your backbone like a twig." + +In fact, Ma Costa looked more anxious than angry. The man she was addressing, a horse trader, was shrugging and spreading his hands. + +"Well, I dunno," he was saying. "He was here one minute and gone the next. I never saw where he went...." + +"He was helping you! He was holding your bloody horses for you!" + +"Well, he should've stayed there, shouldn't he? Runs off in the middle of a job-" + +He got no further, because Ma Costa suddenly dealt him a mighty blow on the side of the head, and followed it up with such a volley of curses and slaps that he yelled and turned to flee. The other horse traders nearby jeered, and a flighty colt reared up in alarm. + +"What's going on?" said Lyra to a gyptian child who'd been watching open-mouthed. "What's she angry about?" + +"It's her kid," said the child. "It's Billy. She probly reckons the Gobblers got him. They might've done, too. I ain't seen him meself since-" + +"The Gobblers? Has they come to Oxford, then?" + +The gyptian boy turned away to call to his friends, who were all watching Ma Costa. + +"She don't know what's going on! She don't know the Gobblers is here!" + +Half a dozen brats turned with expressions of derision, and Lyra threw her cigarette down, recognizing the cue for a fight. Everyone's daemon instantly became warlike: each child was accompanied by fangs, or claws, or bristling fur, and Pantalaimon, contemptuous of the limited imaginations of these gyptian daemons, became a dragon the size of a deer hound. + +But before they could all join battle, Ma Costa herself waded in, smacking two of the gyptians aside and confronting Lyra like a prizefighter. + +"You seen him?" she demanded of Lyra. "You seen Billy?" + +"No," Lyra said. "We just got here. I en't seen Billy for months." + +Ma Costa's daemon was wheeling in the bright air above her head, a hawk, fierce yellow eyes snapping this way and that, unblinking. Lyra was frightened. No one worried about a child gone missing for a few hours, certainly not a gyptian: in the tight-knit gyptian boat world, all children were precious and extravagantly loved, and a mother knew that if a child was out of sight, it wouldn't be far from someone else's who would protect it instinctively. + +But here was Ma Costa, a queen among the gyptians, in a terror for a missing child. What was going on? + +Ma Costa looked half-blindly over the little group of children and turned away to stumble through the crowd on the wharf, bellowing for her child. At once the children turned back to one another, their feud abandoned in the face of her grief. + +"What is them Gobblers?" said Simon Parslow, one of Lyra's companions. + +The first gyptian boy said, "You know. They been stealing kids all over the country. They're pirates-" + +"They en't pirates," corrected another gyptian. "They're cannaboles. That's why they call 'em Gobblers." + +"They eat kids?" said Lyra's other crony, Hugh Lovat, a kitchen boy from St. Michael's. + +"No one knows," said the first gyptian. "They take 'em away and they en't never seen again." + +"We all know that," said Lyra. "We been playing kids and Gobblers for months, before you were, I bet. But 1 bet no one's seen 'em." + +"They have," said one boy. + +"Who, then?" persisted Lyra. "Have you seen 'em? How d'you know it en't just one person?" + +"Charlie seen 'em in Banbury," said a gyptian girl. "They come and talked to this lady while another man took her little boy out the garden." + +"Yeah," piped up Charlie, a gyptian boy. "I seen 'em do it!" + +"What did they look like?" said Lyra. + +"Well...l never properly saw 'em," Charlie said. "I saw their truck, though," he added. "They come in a white truck. They put the little boy in the truck and drove off quick." + +"But why do they call 'em Gobblers?" Lyra asked. + +'"Cause they eat 'em," said the first gyptian boy. "Someone told us in Northampton. They been up there and all. This girl in Northampton, her brother was took, and she said the men as took him told her they was going to eat him. Everyone knows that. They gobble 'em up." + +A gyptian girl standing nearby began to cry loudly. + +"That's Billy's cousin," said Charlie. + +Lyra said, "Who saw Billy last?" + +"Me," said half a dozen voices. "I seen him holding Johnny + +Fiorelli's old horse-I seen him by the toffee-apple seller-I seen him swinging on the crane-" + +When Lyra had sorted it out, she gathered that Billy had been seen for certain not less than two hours previously. + +"So," she said, "sometime in the last two hours there must've been Gobblers here...." + +They all looked around, shivering in spite of the warm sun, the crowded wharf, the familiar smells of tar and horses and smokeleaf. The trouble was that because no one knew what these Gobblers looked like, anyone might be a Gobbler, as Lyra pointed out to the appalled gang, who were now all under her sway, collegers and gyptians alike. + +"They're bound to look like ordinary people, else they'd be seen at once," she explained. "If they only came at night, they could look like anything. But if they come in the daylight, they got to look ordinary. So any of these people might be Gobblers...." + +"They en't," said a gyptian uncertainly. "I know 'em all." + +"All right, not these, but anyone else," said Lyra. "Let's go and look for 'em! And their white truck!" + +And that precipitated a swarm. Other searchers soon joined the first ones, and before long, thirty or more gyptian children were racing from end to end of the wharves, running in and out of stables, scrambling over the cranes and derricks in the boatyard, leaping over the fence into the wide meadow, swinging fifteen at a time on the old swing bridge over the green water, and running full pelt through the narrow streets of Jericho, between the little brick terraced houses and into the great square-towered oratory of St. Barnabas the Chymist. Half of them didn't know what they were looking for, and thought it was just a lark, but those closest to Lyra felt a real fear and apprehension every time they glimpsed a solitary figure down an alley or in the dimness of the oratory: was it a Gobbler? + +But of course it wasn't. Eventually, with no success, and with the shadow of Billy's real disappearance hanging over them all, the fun faded away. As Lyra and the two college boys left Jericho when suppertime neared, they saw the gyptians gathering on the wharf next to where the Costas' boat was moored. Some of the women were crying loudly, and the men were standing in angry groups, with all their daemons agitated and rising in nervous flight or snarling at shadows. + +"I bet them Gobblers wouldn't dare come in here," said Lyra to Simon Parslow, as the two of them stepped over the threshold into the great lodge of Jordan. + +"No," he said uncertainly. "But I know there's a kid missing from the market." + +"Who?" Lyra said. She knew most of the market children, but she hadn't heard of this. + +"Jessie Reynolds, out the saddler's. She weren't there at shutting-up time yesterday, and she'd only gone for a bit of fish for her dad's tea. She never come back and no one'd seen her. They searched all through the market and everywhere." + +"I never heard about that!" said Lyra, indignant. She considered it a deplorable lapse on the part of her subjects not to tell her everything and at once. + +"Well, it was only yesterday. She might've turned up now." + +"I'm going to ask," said Lyra, and turned to leave the lodge. + +But she hadn't got out of the gate before the Porter called her. + +"Here, Lyra! You're not to go out again this evening. Master's orders." + +"Why not?" + +"I told you, Master's orders. He says if you come in, you stay in." + +"You catch me," she said, and darted out before the old man could leave his doorway. + +She ran across the narrow street and down into the alley where the vans unloaded goods for the covered market. This being shutting-up time, there were few vans there now, but a knot of youths stood smoking and talking by the central gate opposite the high stone wall of St. Michael's college. Lyra knew one of them, a sixteen-year-old she admired because he could spit further than anyone else she'd ever heard of, and she went and waited humbly for him to notice her. + +"Yeah? What do you want?" he said finally. + +"Is Jessie Reynolds disappeared?" + +"Yeah. Why?" + +'"Cause a gyptian kid disappeared today and all." + +"They're always disappearing, gyptians. After every horse fair they disappear." + +"So do horses," said one of his friends. + +"This is different," said Lyra. "This is a kid. We was looking for him all afternoon and the other kids said the Gobblers got him." + +"The what?" + +"The Gobblers," she said. "En't you heard of the Gobblers?" + +It was news to the other boys as well, and apart from a few coarse comments they listened closely to what she told them. + +"Gobblers," said Lyra's acquaintance, whose name was Dick. "It's stupid. These gyptians, they pick up all kinds of stupid ideas." + +"They said there was Gobblers in Banbury a couple of weeks ago," Lyra insisted, "and there was five kids taken. They probably come to Oxford now to get kids from us. It must've been them what got Jessie." + +"There was a kid lost over Cowley way," said one of the other boys. "I remember now. My auntie, she was there yesterday, 'cause she sells fish and chips out a van, and she heard about it....Some little boy, that's it...I dunno about the Gobblers, though. They en't real, Gobblers. Just a story." + +"They are!" Lyra said. "The gyptians seen 'em. They reckon they eat the kids they catch, and..." + +She stopped in midsentence, because something had suddenly come into her mind. During that strange evening she'd spent hidden in the Retiring Room, Lord Asriel had shown a lantern slide of a man with streams of light pouring from his hand; and there'd been a small figure beside him, with less light around it; and he'd said it was a child; and someone had asked if it was a severed child, and her uncle had said no, that was the point. Lyra remembered that severed meant "cut." + +And then something else hit her heart: where was Roger? + +She hadn't seen him since the morning.... + +Suddenly she felt afraid. Pantalaimon, as a miniature lion, sprang into her arms and growled. She said goodbye to the youths by the gate and walked quietly back into Turl Street, and then ran full pelt for Jordan lodge, tumbling in through the door a second before the now cheetah-shaped daemon. + +The Porter was sanctimonious. + +"I had to ring the Master and tell him," he said. "He en't pleased at all. I wouldn't be in your shoes, not for money I wouldn't." + +"Where's Roger?" she demanded. + +"I en't seen him. He'll be for it, too. Ooh, when Mr. Cawson catches him-" + +Lyra ran to the kitchen and thrust her way into the hot, clangorous, steaming bustle. + +"Where's Roger?" she shouted. + +"Clear off, Lyra! We're busy here!" + +"But where is he? Has he turned up or not?" + +No one seemed interested. + +"But where is he? You must've heard!" Lyra shouted at the chef, who boxed her ears and sent her storming away. + +Bernie the pastry cook tried to calm her down, but she wouldn't be consoled. + +"They got him! Them bloody Gobblers, they oughter catch 'em and bloody kill 'em! I hate 'em! You don't care about Roger-" + +"Lyra, we all care about Roger-" + +"You don't, else you'd all stop work and go and look for him right now! I hate you!" + +"There could be a dozen reasons why Roger en't turned up. Listen to sense. We got dinner to prepare and serve in less than an hour; the Master's got guests in the lodging, and he'll be eating over there, and that means Chef'11 have to attend to getting the food there quick so it don't go cold; and what with one thing and another, Lyra, life's got to go on. I'm sure Roger'11 turn up...." + +Lyra turned and ran out of the kitchen, knocking over a stack of silver dish covers and ignoring the roar of anger that arose. She sped down the steps and across the quadrangle, between the chapel and Palmer's Tower and into the Yaxley Quad, where the oldest buildings of the college stood. + +Pantalaimon scampered before her, flowing up the stairs to the very top, where Lyra's bedroom was. Lyra barged open the door, dragged her rickety chair to the window, flung wide the casement, and scrambled out. There was a lead-lined stone gutter a foot wide just below the window, and once she was standing in that, she turned and clambered up over the rough tiles until she stood on the topmost ridge of the roof. There she opened her mouth and screamed. Pantalaimon, who always became a bird once on the roof, flew round and round shrieking rook shrieks with her. + +The evening sky was awash with peach, apricot, cream: tender little ice-cream clouds in a wide orange sky. The spires and towers of Oxford stood around them, level but no higher; the green woods of Chateau-Vert and White Ham rose on either side to the east and the west. Rooks were cawing somewhere, and bells were ringing, and from the oxpens the steady beat of a gas engine announced the ascent of the evening Royal Mail zeppelin for London. Lyra watched it climb away beyond the spire of St. Michael's Chapel, as big at first as the tip of her little finger when she held it at arm's length, and then steadily smaller until it was a dot in the pearly sky. + +She turned and looked down into the shadowed quadrangle, where the black-gowned figures of the Scholars were already beginning to drift in ones and twos toward the buttery, their daemons strutting or fluttering alongside or perching calmly on their shoulders. The lights were going on in the Hall; she could see the stained-glass windows gradually beginning to glow as a servant moved up the tables lighting the naphtha lamps. The Steward's bell began to toll, announcing half an hour before dinner. + +This was her world. She wanted it to stay the same forever and ever, but it was changing around her, for someone out there was stealing children. She sat on the roof ridge, chin in hands. + +"We better rescue him, Pantalaimon," she said. He answered in his rook voice from the chimney. "It'll be dangerous," he said. '"Course! I know that." + +"Remember what they said in the Retiring Room." "What?" + +"Something about a child up in the Arctic. The one that wasn't attracting the Dust." + +"They said it was an entire child....What about it?" + +"That might be what they're going to do to Roger and the gyptians and the other kids." + +"What?" + +"Well, what does entire mean?" + +"Dunno. They cut 'em in half, probably. I reckon they make slaves out of 'em. That'd be more use. They probably got mines up there. Uranium mines for atomcraft. I bet that's what it is. And if they sent grownups down the mine, they'd be dead, so they use kids instead because they cost less. That's what they've done with him." + +"I think-" + +But what Pantalaimon thought had to wait, because someone began to shout from below. + +"Lyra! Lyra! You come in this instant!" + +There was a banging on the window frame. Lyra knew the voice and the impatience: it was Mrs. Lonsdale, the Housekeeper. There was no hiding from her. + +Tight-faced, Lyra slid down the roof and into the gutter, and then climbed in through the window again. Mrs. Lonsdale was running some water into the little chipped basin, to the accompaniment of a great groaning and hammering from the pipes. + +"The number of times you been told about going out there-Look at you! Just look at your skirt-it's filthy! Take it off at once and wash yourself while I look for something decent that en't torn. Why you can't keep yourself clean and tidy..." + +Lyra was too sulky even to ask why she was having to wash and dress, and no grownup ever gave reasons of their own accord. She dragged the dress over her head and dropped it on the narrow bed, and began to wash desultorily while Pantalaimon, a canary now, hopped closer and closer to Mrs. Lonsdale's daemon, a stolid retriever, trying in vain to annoy him. + +"Look at the state of this wardrobe! You en't hung nothing up for weeks! Look at the creases in this-" + +Look at this, look at that...Lyra didn't want to look. She shut her eyes as she rubbed at her face with the thin towel. + +"You'll just have to wear it as it is. There en't time to take an iron to it. God bless me, girl, your knees-look at the state of them...." + +"Don't want to look at nothing," Lyra muttered. + +Mrs. Lonsdale smacked her leg. "Wash," she said ferociously. "You get all that dirt off." + +"Why?" Lyra said at last. "I never wash my knees usually. No one's going to look at my knees. What've I got to do all this for? You don't care about Roger neither, any more than Chef does. I'm the only one that-" Another smack, on the other leg. + +"None of that nonsense. I'm a Parslow, same as Roger's father. He's my second cousin. I bet you didn't know that, 'cause I bet you never asked, Miss Lyra. I bet it never occurred to you. Don't you chide me with not caring about the boy. God knows, I even care about you, and you give me little enough reason and no thanks." + +She seized the flannel and rubbed Lyra's knees so hard she left the skin bright pink and sore, but clean. + +"The reason for this is you're going to have dinner with the Master and his guests. I hope to God you behave. Speak when you're spoken to, be quiet and polite, smile nicely and don't you ever say Dunno when someone asks you a question." + +She dragged the best dress onto Lyra's skinny frame, tugged it straight, fished a bit of red ribbon out of the tangle in a drawer, and brushed Lyra's hair with a coarse brush. + +"If they'd let me know earlier, I could've given your hair a proper wash. Well, that's too bad. As long as they don't look too close...There. Now stand up straight. Where's those best patent-leather shoes?" + +Five minutes later Lyra was knocking on the door of the Master's lodging, the grand and slightly gloomy house that opened into the Yaxley Quadrangle and backed onto the Library garden. Pantalaimon, an ermine now for politeness, rubbed himself against her leg. The door was opened by the Master's manservant Cousins, an old enemy of Lyra's; but both knew that this was a state of truce. + +"Mrs. Lonsdale said I was to come," said Lyra. + +"Yes," said Cousins, stepping aside. "The Master's in the drawing room." + +He showed her into the large room that overlooked the Library garden. The last of the sun shone into it, through the gap between the library and Palmer's Tower, and lit up the heavy pictures and the glum silver the Master collected. It also lit up the guests, and Lyra realized why they weren't going to dine in Hall: three of the guests were women. + +"Ah, Lyra," said the Master. "I'm so glad you could come. Cousins, could you find some sort of soft drink? Dame Hannah, I don't think you've met Lyra...Lord Asriel's niece, you know." + +Dame Hannah Relf was the head of one of the women's colleges, an elderly gray-haired lady whose daemon was a marmoset. Lyra shook hands as politely as she could, and was then introduced to the other guests, who were, like Dame Hannah, Scholars from other colleges and quite uninteresting. Then the Master came to the final guest. + +"Mrs. Coulter," he said, "this is our Lyra. Lyra, come and say hello to Mrs. Coulter." + +"Hello, Lyra," said Mrs. Coulter. + +She was beautiful and young. Her sleek black hair framed her cheeks, and her daemon was a golden monkey. \ No newline at end of file diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..f90d3b0 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=INFO,CONSOLE +log4j.appender.Threshold=INFO +log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender +log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout +log4j.appender.CONSOLE.layout.ConversionPattern=[%-5p]%d{yyyy-MM-dd HH:mm:ssS} %c %m%n diff --git a/src/test/resources/sampleApplicationContext.xml b/src/test/resources/sampleApplicationContext.xml new file mode 100644 index 0000000..26ff1d7 --- /dev/null +++ b/src/test/resources/sampleApplicationContext.xml @@ -0,0 +1,112 @@ + + + + + + + + ${test.memcached.host} + + + ${test.memcached.port} + + + + + + + + ${test.memcached.servers} + + + + 1 + 2 + + + + + + + + + cacheuser + + + 123456 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 2 + + + + + + + + cacheuser + + + 123456 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties new file mode 100644 index 0000000..fa3a252 --- /dev/null +++ b/src/test/resources/test.properties @@ -0,0 +1,4 @@ +test.memcached.servers=${test.memcached.servers} +test.memcached.host=${test.memcached.host} +test.memcached.port=${test.memcached.port} +test.kestrel.servers=localhost\:22133