├── website ├── static │ ├── .nojekyll │ └── img │ │ ├── hero.jpg │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── hero-bg-right.webp │ │ └── skyhook-hero.webp ├── versions.json ├── babel.config.js ├── docs │ ├── intro.mdx │ ├── images │ │ └── scaling-out-diagram.png │ ├── usage.md │ └── docker.md ├── versioned_docs │ ├── version-0.10.0 │ │ ├── intro.mdx │ │ ├── images │ │ │ └── scaling-out-diagram.png │ │ ├── usage.md │ │ └── docker.md │ └── version-0.9.0 │ │ ├── intro.mdx │ │ ├── images │ │ └── scaling-out-diagram.png │ │ ├── usage.md │ │ ├── docker.md │ │ └── _intro.md ├── .vscode │ └── settings.json ├── src │ ├── pages │ │ ├── _README.md │ │ ├── SITE_index.tsx │ │ └── index.module.css │ └── css │ │ └── custom.css ├── .prettierrc ├── tsconfig.json ├── .gitignore ├── versioned_sidebars │ ├── version-0.9.0-sidebars.json │ └── version-0.10.0-sidebars.json ├── README.md ├── sidebars.js ├── package.json └── docusaurus.config.js ├── README.md ├── config └── skyhook.yml ├── settings.gradle.kts ├── gradle.properties ├── .dockerignore ├── docs ├── images │ └── scaling-out-diagram.png └── scaling-out.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── pkg ├── deb │ ├── preUninstall.sh │ ├── postUninstall.sh │ └── postInstall.sh └── install │ └── etc │ ├── skyhook │ └── skyhook.yml │ └── systemd │ └── system │ └── skyhook.service ├── src ├── test │ └── kotlin │ │ └── com │ │ └── aerospike │ │ └── skyhook │ │ ├── util │ │ └── ScanResponse.kt │ │ ├── SystemCommandsTest.kt │ │ ├── TransactionTest.kt │ │ ├── ListCommandsTest.kt │ │ ├── ScanCommandsTest.kt │ │ ├── HyperLogCommandsTest.kt │ │ ├── SetCommandsTest.kt │ │ └── TypeCommandTest.kt └── main │ ├── kotlin │ └── com │ │ └── aerospike │ │ └── skyhook │ │ ├── util │ │ ├── RegexUtils.kt │ │ ├── client │ │ │ ├── AerospikeClientPool.kt │ │ │ ├── AuthDetails.kt │ │ │ └── AerospikeClientPoolImpl.kt │ │ ├── Merge.kt │ │ ├── Extensions.kt │ │ ├── SystemUtils.kt │ │ ├── TransactionState.kt │ │ ├── InfoUtils.kt │ │ ├── Typed.kt │ │ └── Intervals.kt │ │ ├── Server.kt │ │ ├── handler │ │ ├── CommandHandler.kt │ │ ├── aerospike │ │ │ ├── FlushCommandHandler.kt │ │ │ ├── DbsizeCommandHandler.kt │ │ │ └── TransactionCommandsHandler.kt │ │ ├── redis │ │ │ ├── EchoCommandHandler.kt │ │ │ ├── LolwutCommandHandler.kt │ │ │ ├── MockCommandHandler.kt │ │ │ ├── PingCommandHandler.kt │ │ │ ├── TimeCommandHandler.kt │ │ │ ├── AuthCommandHandler.kt │ │ │ └── CommandCommandHandler.kt │ │ └── AerospikeChannelHandler.kt │ │ ├── listener │ │ ├── ValueType.kt │ │ ├── scan │ │ │ ├── RecordSet.kt │ │ │ ├── ZscanCommandListener.kt │ │ │ ├── SscanCommandListener.kt │ │ │ ├── ScanCommand.kt │ │ │ ├── KeysCommandListener.kt │ │ │ ├── HscanCommandListener.kt │ │ │ └── ScanCommandListener.kt │ │ ├── key │ │ │ ├── DelCommandListener.kt │ │ │ ├── TypeCommandListener.kt │ │ │ ├── ExistsCommandListener.kt │ │ │ ├── RandomkeyCommandListener.kt │ │ │ ├── TtlCommandListener.kt │ │ │ ├── MgetCommandListener.kt │ │ │ ├── AppendCommandListener.kt │ │ │ ├── StrlenCommandListener.kt │ │ │ ├── GetsetCommandListener.kt │ │ │ ├── MsetCommandListener.kt │ │ │ ├── UnaryCommandListener.kt │ │ │ ├── ExpireCommandListener.kt │ │ │ └── GetCommandListener.kt │ │ ├── list │ │ │ ├── LlenCommandListener.kt │ │ │ ├── LindexCommandListener.kt │ │ │ ├── LrangeCommandListener.kt │ │ │ ├── ListPopCommandListener.kt │ │ │ └── ListPushCommandListener.kt │ │ ├── map │ │ │ ├── MapSizeCommandListener.kt │ │ │ ├── HstrlenCommandListener.kt │ │ │ ├── MapDelCommandListener.kt │ │ │ ├── SmergeCommandListener.kt │ │ │ ├── ZrangestoreCommandListener.kt │ │ │ ├── HexistsCommandListener.kt │ │ │ ├── HincrbyCommandListener.kt │ │ │ ├── SstoreCommandListener.kt │ │ │ ├── SaddCommandListener.kt │ │ │ ├── HsetCommandListener.kt │ │ │ ├── ZpopCommandListener.kt │ │ │ ├── ZcountCommandListener.kt │ │ │ ├── MapGetCommandListener.kt │ │ │ └── ZaddCommandListener.kt │ │ └── hyperlog │ │ │ ├── PfmergeCommandListener.kt │ │ │ ├── PfaddCommandListener.kt │ │ │ └── PfcountCommandListener.kt │ │ ├── command │ │ ├── RequestCommand.kt │ │ └── RedisCommandDetails.kt │ │ ├── config │ │ ├── AerospikeContext.kt │ │ ├── ServerConfiguration.kt │ │ └── ClientPolicyConfig.kt │ │ ├── pipeline │ │ └── AerospikeChannelInitializer.kt │ │ ├── Main.kt │ │ └── SkyhookServer.kt │ └── resources │ └── logback.xml ├── .gitignore ├── .github ├── workflows │ ├── gradle-wrapper-validation.yml │ ├── docker.yml │ ├── build.yml │ └── documentation.yml └── dependabot.yml ├── Dockerfile └── gradlew.bat /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | website/docs/_intro.md -------------------------------------------------------------------------------- /config/skyhook.yml: -------------------------------------------------------------------------------- 1 | ../pkg/install/etc/skyhook/skyhook.yml -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = "skyhook" 3 | 4 | -------------------------------------------------------------------------------- /website/versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | "0.10.0", 3 | "0.9.0" 4 | ] 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | version=0.11.0 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .gradle 4 | .idea 5 | build/ 6 | website/ 7 | -------------------------------------------------------------------------------- /website/static/img/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/static/img/hero.jpg -------------------------------------------------------------------------------- /website/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/static/img/logo.png -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: '' 3 | slug: / 4 | --- 5 | 6 | import Intro from './_intro.md'; 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/images/scaling-out-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/docs/images/scaling-out-diagram.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /website/static/img/hero-bg-right.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/static/img/hero-bg-right.webp -------------------------------------------------------------------------------- /website/static/img/skyhook-hero.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/static/img/skyhook-hero.webp -------------------------------------------------------------------------------- /website/docs/images/scaling-out-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/docs/images/scaling-out-diagram.png -------------------------------------------------------------------------------- /website/versioned_docs/version-0.10.0/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: '' 3 | slug: / 4 | --- 5 | 6 | import Intro from './_intro.md'; 7 | 8 | 9 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.9.0/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: '' 3 | slug: / 4 | --- 5 | 6 | import Intro from './_intro.md'; 7 | 8 | 9 | -------------------------------------------------------------------------------- /pkg/deb/preUninstall.sh: -------------------------------------------------------------------------------- 1 | # Stop the connector service. 2 | if [ -d /run/systemd/system ]; then 3 | systemctl stop skyhook >/dev/null 2>&1 || true 4 | fi -------------------------------------------------------------------------------- /pkg/deb/postUninstall.sh: -------------------------------------------------------------------------------- 1 | # Reload systemd daemon. 2 | if [ -d /run/systemd/system ]; then 3 | systemctl --system daemon-reload >/dev/null 2>&1 || true 4 | fi -------------------------------------------------------------------------------- /website/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.codeActionsOnSave": { "source.fixAll": true }, 4 | "editor.formatOnSave": true 5 | } 6 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.9.0/images/scaling-out-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/versioned_docs/version-0.9.0/images/scaling-out-diagram.png -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/util/ScanResponse.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | data class ScanResponse( 4 | val cursor: String, 5 | val elements: List 6 | ) 7 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.10.0/images/scaling-out-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerospike-community/skyhook/HEAD/website/versioned_docs/version-0.10.0/images/scaling-out-diagram.png -------------------------------------------------------------------------------- /website/src/pages/_README.md: -------------------------------------------------------------------------------- 1 | # Why no pages 2 | 3 | Until we have a marketing front page to use we will keep this docs only. If we would like to go back to a graphical first page, SITE_index.tsx should be renamed to index.tsx. 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/RegexUtils.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | object RegexUtils { 4 | 5 | fun format(regex: String): String { 6 | return regex.replace("*", ".*") 7 | .replace("?", ".") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /website/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "overrides": [ 6 | { 7 | "files": ["{src,docs}/**/*.{js,tsx,mdx,ts,md}"], 8 | "options": { 9 | "printWidth": 85 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "paths": { 6 | "@site/*": ["./*"] 7 | } 8 | }, 9 | "include": ["src/"] 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/Server.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | /** 4 | * The Server interface. 5 | */ 6 | interface Server { 7 | /** 8 | * Start the server. 9 | */ 10 | fun start() 11 | 12 | /** 13 | * Stop the server. 14 | */ 15 | fun stop() 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/CommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | 5 | interface CommandHandler { 6 | 7 | /** 8 | * Handles the incoming request command. 9 | */ 10 | fun handle(cmd: RequestCommand) 11 | } 12 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/client/AerospikeClientPool.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util.client 2 | 3 | import com.aerospike.client.IAerospikeClient 4 | 5 | interface AerospikeClientPool { 6 | 7 | fun getClient(authDetails: AuthDetails): IAerospikeClient? 8 | 9 | fun getClient(authDetailsHash: String?): IAerospikeClient 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.nar 11 | *.ear 12 | *.zip 13 | *.tar.gz 14 | *.rar 15 | 16 | .project 17 | .settings 18 | .classpath 19 | .checkstyle 20 | **/target 21 | features.conf 22 | release.properties 23 | !gradle-wrapper.jar 24 | 25 | .gradle/ 26 | .idea/ 27 | build/ 28 | out/ 29 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/client/AuthDetails.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util.client 2 | 3 | import com.google.common.hash.Hashing 4 | 5 | data class AuthDetails( 6 | val user: String, 7 | val password: String 8 | ) { 9 | val hashString: String by lazy { 10 | Hashing.sha256() 11 | .hashBytes(toString().encodeToByteArray()) 12 | .toString() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/gradle-wrapper-validation.yml: -------------------------------------------------------------------------------- 1 | name: "Validate Gradle Wrapper" 2 | on: 3 | pull_request: 4 | paths: 5 | - '**gradle-wrapper.jar' 6 | push: 7 | paths: 8 | - '**gradle-wrapper.jar' 9 | 10 | jobs: 11 | validation: 12 | name: "Gradle wrapper validation" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: gradle/wrapper-validation-action@v1 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/ValueType.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Redis supported value types. 7 | */ 8 | enum class ValueType(val str: String) { 9 | STRING("string"), 10 | LIST("list"), 11 | SET("set"), 12 | ZSET("zset"), 13 | HASH("hash"), 14 | STREAM("stream"); 15 | 16 | companion object { 17 | fun valueOf(ba: ByteArray) = valueOf(String(ba).uppercase(Locale.ENGLISH)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:7.6.1-jdk8 AS build 2 | 3 | COPY --chown=gradle:gradle . /home/gradle/src 4 | WORKDIR /home/gradle/src 5 | RUN gradle build -x test 6 | 7 | FROM eclipse-temurin:8-jre 8 | 9 | EXPOSE 6379 10 | 11 | RUN mkdir /app 12 | 13 | COPY --from=build /home/gradle/src/build/libs/*all.jar /app/skyhook.jar 14 | COPY config/skyhook.yml /app/skyhook.yml 15 | 16 | ENTRYPOINT ["java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-jar", "/app/skyhook.jar"] 17 | CMD ["-f", "/app/skyhook.yml"] 18 | -------------------------------------------------------------------------------- /website/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage With Redis client 2 | 3 | ## Connectivity 4 | 5 | Any Redis client can connect to Skyhook as if it were a regular Redis server. 6 | 7 | For tests purposes use [redis-cli](https://redis.io/topics/rediscli) or the [nc](https://www.commandlinux.com/man-page/man1/nc.1.html) (or netcat) utility: 8 | 9 | ```sh 10 | echo "GET key1\r\n" | nc localhost 6379 11 | ``` 12 | 13 | ## Supported Commands 14 | 15 | The list of Redis commands supported by Skyhook is maintained [here](supported-redis-api). 16 | 17 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.10.0/usage.md: -------------------------------------------------------------------------------- 1 | # Usage With Redis client 2 | 3 | ## Connectivity 4 | 5 | Any Redis client can connect to Skyhook as if it were a regular Redis server. 6 | 7 | For tests purposes use [redis-cli](https://redis.io/topics/rediscli) or the [nc](https://www.commandlinux.com/man-page/man1/nc.1.html) (or netcat) utility: 8 | 9 | ```sh 10 | echo "GET key1\r\n" | nc localhost 6379 11 | ``` 12 | 13 | ## Supported Commands 14 | 15 | The list of Redis commands supported by Skyhook is maintained [here](supported-redis-api). 16 | 17 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.9.0/usage.md: -------------------------------------------------------------------------------- 1 | # Usage With Redis client 2 | 3 | ## Connectivity 4 | 5 | Any Redis client can connect to Skyhook as if it were a regular Redis server. 6 | 7 | For tests purposes use [redis-cli](https://redis.io/topics/rediscli) or the [nc](https://www.commandlinux.com/man-page/man1/nc.1.html) (or netcat) utility: 8 | 9 | ```sh 10 | echo "GET key1\r\n" | nc localhost 6379 11 | ``` 12 | 13 | ## Supported Commands 14 | 15 | The list of Redis commands supported by Skyhook is maintained [here](supported-redis-api). 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/website" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/RecordSet.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.client.query.KeyRecord 4 | import java.util.* 5 | 6 | class RecordSet() : LinkedList() { 7 | 8 | private var lastRecord: KeyRecord? = null 9 | 10 | fun nextCursor(): String? { 11 | return lastRecord?.key?.userKey?.`object`?.let { it as String } 12 | } 13 | 14 | override fun add(element: KeyRecord): Boolean { 15 | this.lastRecord = element 16 | return super.add(element) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /website/versioned_sidebars/version-0.9.0-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-0.9.0/docsSidebar": [ 3 | { 4 | "type": "doc", 5 | "label": "Skyhook", 6 | 7 | "id": "version-0.9.0/intro" 8 | }, 9 | { 10 | "type": "doc", 11 | "id": "version-0.9.0/supported-redis-api" 12 | }, 13 | { 14 | "type": "doc", 15 | "id": "version-0.9.0/scaling-out" 16 | }, 17 | { 18 | "type": "doc", 19 | "id": "version-0.9.0/usage" 20 | }, 21 | { 22 | "type": "doc", 23 | "id": "version-0.9.0/docker" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /website/versioned_sidebars/version-0.10.0-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-0.10.0/docsSidebar": [ 3 | { 4 | "type": "doc", 5 | "label": "Skyhook", 6 | "id": "version-0.10.0/intro" 7 | }, 8 | { 9 | "type": "doc", 10 | "id": "version-0.10.0/supported-redis-api" 11 | }, 12 | { 13 | "type": "doc", 14 | "id": "version-0.10.0/scaling-out" 15 | }, 16 | { 17 | "type": "doc", 18 | "id": "version-0.10.0/usage" 19 | }, 20 | { 21 | "type": "doc", 22 | "id": "version-0.10.0/docker" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/command/RequestCommand.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.command 2 | 3 | import com.aerospike.client.Value 4 | 5 | data class RequestCommand( 6 | val args: List 7 | ) { 8 | 9 | val argCount: Int = args.size 10 | 11 | val key: Value by lazy { 12 | Value.get(String(args[1])) 13 | } 14 | 15 | val command: RedisCommand = 16 | RedisCommand.getValue(String(args[0])) 17 | 18 | val transactional: Boolean by lazy { 19 | command == RedisCommand.MULTI || 20 | command == RedisCommand.EXEC || 21 | command == RedisCommand.DISCARD 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/Merge.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | interface Merge { 4 | fun merge(data: List>): Set 5 | } 6 | 7 | interface UnionMerge : Merge { 8 | override fun merge(data: List>): Set { 9 | return data.flatten().filterNotNull().toSet() 10 | } 11 | } 12 | 13 | interface IntersectMerge : Merge { 14 | override fun merge(data: List>): Set { 15 | if (data.isEmpty()) { 16 | return setOf() 17 | } 18 | return data.reduce { acc, l -> acc.intersect(l.filterNotNull().toSet()) } 19 | .filterNotNull().toSet() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | import io.netty.channel.ChannelHandlerContext 4 | 5 | fun List.toPair(): Pair { 6 | require(this.size == 2) { "List is not of length 2" } 7 | val (a, b) = this 8 | return Pair(a, b) 9 | } 10 | 11 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 12 | fun ChannelHandlerContext.wait() = (this as Object).wait() 13 | 14 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 15 | fun ChannelHandlerContext.wait(timeout: Long) = (this as Object).wait(timeout) 16 | 17 | @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") 18 | fun ChannelHandlerContext.notify() = (this as Object).notify() 19 | -------------------------------------------------------------------------------- /pkg/install/etc/skyhook/skyhook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Skyhook server configuration 3 | 4 | hostList: localhost:3000 5 | namespace: test 6 | set: redis 7 | bin: b 8 | redisPort: 6379 9 | 10 | # Aerospike Java Client [com.aerospike.client.policy.ClientPolicy] configuration. 11 | #clientPolicy: 12 | # user: admin 13 | # password: pwd@1234 14 | # clusterName: cluster1 15 | # authMode: EXTERNAL_INSECURE 16 | # timeout: 1500 17 | # loginTimeout: 3000 18 | # asyncMinConnsPerNode: 50 19 | # asyncMaxConnsPerNode: 200 20 | # failIfNotConnected: true 21 | # useServicesAlternate: true 22 | 23 | # Bind on unix socket. 24 | #unixSocket: "/tmp/skyhook.sock" 25 | 26 | workerThreads: 2 27 | bossThreads: 1 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/config/AerospikeContext.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.config 2 | 3 | data class AerospikeContext( 4 | 5 | /** 6 | * The Aerospike namespace to map to. 7 | */ 8 | val namespace: String, 9 | 10 | /** 11 | * The Aerospike set name to map to. 12 | */ 13 | val set: String?, 14 | 15 | /** 16 | * The Aerospike bin name to set values. 17 | */ 18 | val bin: String, 19 | 20 | /** 21 | * The Aerospike bin name to set value type. 22 | */ 23 | val typeBin: String, 24 | 25 | /** 26 | * The Aerospike transaction id bin name. 27 | */ 28 | val transactionIdBin: String, 29 | ) 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/ZscanCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.client.Operation 4 | import com.aerospike.client.cdt.MapOperation 5 | import com.aerospike.client.cdt.MapReturnType 6 | import io.netty.channel.ChannelHandlerContext 7 | 8 | open class ZscanCommandListener( 9 | ctx: ChannelHandlerContext 10 | ) : SscanCommandListener(ctx) { 11 | 12 | override fun getOperation(): Operation { 13 | return MapOperation.getByRankRange( 14 | aeroCtx.bin, 15 | scanCommand.cursor.toInt(), 16 | scanCommand.COUNT.toInt(), 17 | MapReturnType.KEY 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/deb/postInstall.sh: -------------------------------------------------------------------------------- 1 | # create aerospike group if it isn't already there 2 | if ! getent group aerospike >/dev/null; then 3 | groupadd -r aerospike 4 | fi 5 | 6 | # create aerospike user if it isn't already there 7 | if ! getent passwd aerospike >/dev/null; then 8 | useradd -r -d /opt/aerospike -c 'Aerospike services' -g aerospike -s /sbin/nologin aerospike 9 | fi 10 | 11 | mkdir -p /var/log/skyhook 12 | mkdir -p /etc/skyhook 13 | mkdir -p /opt/skyhook/usr-lib 14 | 15 | for dir in /opt/skyhook /var/log/skyhook ; do 16 | if [ -d $dir ]; then 17 | chown -R aerospike:aerospike $dir 18 | fi 19 | done 20 | 21 | if [ -d /run/systemd/system ]; then 22 | systemctl --system daemon-reload >/dev/null 2>&1 || true 23 | fi -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.9.0/docker.md: -------------------------------------------------------------------------------- 1 | # Running In Docker 2 | 3 | ## Running on Docker 4 | 5 | :::warning 6 | 7 | This section requires installing docker directly from source. Future updates will include a docker image. 8 | 9 | ::: 10 | 11 | Build an image: 12 | 13 | ```sh 14 | docker build -t skyhook . 15 | ``` 16 | 17 | Run as a Docker container: 18 | 19 | ```sh 20 | docker run -d --name=skyhook -p 6379:6379 skyhook 21 | ``` 22 | 23 | The image uses the repository configuration file by default. 24 | [Bind mount](https://docs.docker.com/storage/bind-mounts/) a custom file to configure the server: 25 | 26 | ```sh 27 | docker run -d --name=skyhook -v "$(pwd)"/config/server.yml:/app/server.yml -p 6379:6379 skyhook 28 | ``` 29 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/aerospike/FlushCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.aerospike 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.listener.BaseListener 6 | import io.netty.channel.ChannelHandlerContext 7 | 8 | class FlushCommandHandler( 9 | ctx: ChannelHandlerContext 10 | ) : BaseListener(ctx), CommandHandler { 11 | 12 | override fun handle(cmd: RequestCommand) { 13 | require(cmd.argCount <= 2) { argValidationErrorMsg(cmd) } 14 | 15 | client.truncate(null, aeroCtx.namespace, aeroCtx.set, null) 16 | writeOK() 17 | flushCtxTransactionAware() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/EchoCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.handler.NettyResponseWriter 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import io.netty.channel.ChannelHandlerContext 8 | 9 | class EchoCommandHandler( 10 | ctx: ChannelHandlerContext 11 | ) : NettyResponseWriter(ctx), CommandHandler { 12 | 13 | override fun handle(cmd: RequestCommand) { 14 | require(cmd.argCount == 2) { BaseListener.argValidationErrorMsg(cmd) } 15 | 16 | writeSimpleString(String(cmd.args[1])) 17 | flushCtxTransactionAware() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/SscanCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.client.Operation 4 | import com.aerospike.client.cdt.MapOperation 5 | import com.aerospike.client.cdt.MapReturnType 6 | import io.netty.channel.ChannelHandlerContext 7 | 8 | open class SscanCommandListener( 9 | ctx: ChannelHandlerContext 10 | ) : HscanCommandListener(ctx) { 11 | 12 | override fun getOperation(): Operation { 13 | return MapOperation.getByIndexRange( 14 | aeroCtx.bin, 15 | scanCommand.cursor.toInt(), 16 | scanCommand.COUNT.toInt(), 17 | MapReturnType.KEY 18 | ) 19 | } 20 | 21 | override fun writeElementsArray(list: List<*>) { 22 | writeObject(list) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/SystemUtils.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | import java.util.* 4 | 5 | object SystemUtils { 6 | 7 | enum class OS { 8 | LINUX, MAC, WINDOWS, OTHER 9 | } 10 | 11 | val os: OS by lazy { 12 | val os = System.getProperty("os.name").lowercase(Locale.ENGLISH) 13 | when { 14 | os.contains("nux") -> { 15 | OS.LINUX 16 | } 17 | os.contains("mac") -> { 18 | OS.MAC 19 | } 20 | os.contains("win") -> { 21 | OS.WINDOWS 22 | } 23 | else -> OS.OTHER 24 | } 25 | } 26 | 27 | val version: String by lazy { 28 | SystemUtils.javaClass.getPackage().implementationVersion?.trim() ?: "NA" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/LolwutCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.handler.NettyResponseWriter 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import com.aerospike.skyhook.util.SystemUtils 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | class LolwutCommandHandler( 11 | ctx: ChannelHandlerContext 12 | ) : NettyResponseWriter(ctx), CommandHandler { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount <= 3) { BaseListener.argValidationErrorMsg(cmd) } 16 | 17 | writeSimpleString("Skyhook ver. ${SystemUtils.version}\n") 18 | flushCtxTransactionAware() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/install/etc/systemd/system/skyhook.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Redis API compatible gateway to Aerospike Database. 3 | Documentation=https://aerospike.github.io/skyhook/ 4 | After=network-online.target firewalld.service 5 | Wants=network-online.target 6 | 7 | [Service] 8 | User=aerospike 9 | Group=aerospike 10 | Type=simple 11 | 12 | ExecStart=/opt/skyhook/bin/skyhook -f /etc/skyhook/skyhook.yml 13 | ExecReload=/bin/kill -s HUP $MAINPID 14 | 15 | TimeoutSec=0 16 | RestartSec=2 17 | Restart=always 18 | StartLimitBurst=3 19 | StartLimitInterval=60s 20 | 21 | LimitNOFILE=infinity 22 | LimitNPROC=infinity 23 | LimitCORE=infinity 24 | 25 | TasksMax=infinity 26 | 27 | # Kill only the Skyhook process. 28 | KillMode=process 29 | 30 | # JVM exits with this status for SIGTERM. 31 | SuccessExitStatus=143 32 | 33 | [Install] 34 | WantedBy=multi-user.target 35 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | module.exports = { 13 | // By default, Docusaurus generates a sidebar from the docs folder structure 14 | docsSidebar: [ 15 | { 16 | type: 'doc', 17 | id: 'intro', 18 | label: 'Skyhook' 19 | }, 20 | { 21 | type: 'doc', 22 | id: 'supported-redis-api', 23 | }, 24 | { 25 | type:'doc', 26 | id: 'scaling-out' 27 | }, 28 | { 29 | type:'doc', 30 | id: 'usage' 31 | }, 32 | { 33 | type:'doc', 34 | id: 'docker' 35 | }, 36 | ] 37 | 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/command/RedisCommandDetails.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.command 2 | 3 | import com.aerospike.skyhook.handler.NettyResponseWriter 4 | import io.netty.channel.ChannelHandlerContext 5 | 6 | data class RedisCommandDetails( 7 | val commandName: String, 8 | val commandArity: Int, 9 | val commandFlags: List, 10 | val firstKeyPosition: Int, 11 | val lastKeyPosition: Int, 12 | val stepCount: Int 13 | ) { 14 | 15 | fun write(ctx: ChannelHandlerContext) { 16 | val nrw = NettyResponseWriter(ctx) 17 | nrw.writeArrayHeader(6) 18 | nrw.writeSimpleString(commandName) 19 | nrw.writeLong(commandArity) 20 | nrw.writeObjectListStr(commandFlags) 21 | nrw.writeLong(firstKeyPosition) 22 | nrw.writeLong(lastKeyPosition) 23 | nrw.writeLong(stepCount) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/TransactionState.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.skyhook.command.RequestCommand 5 | import java.util.* 6 | import java.util.concurrent.ExecutorService 7 | 8 | class TransactionState(val pool: ExecutorService) { 9 | var inTransaction: Boolean = false 10 | private set 11 | 12 | var transactionId: String? = null 13 | private set 14 | 15 | val commands: LinkedList = LinkedList() 16 | val keys: LinkedHashSet = LinkedHashSet() 17 | 18 | fun startTransaction() { 19 | inTransaction = true 20 | transactionId = UUID.randomUUID().toString() 21 | } 22 | 23 | fun clear() { 24 | inTransaction = false 25 | transactionId = null 26 | commands.clear() 27 | keys.clear() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/MockCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import com.aerospike.skyhook.command.RequestCommand 5 | import com.aerospike.skyhook.handler.CommandHandler 6 | import com.aerospike.skyhook.handler.NettyResponseWriter 7 | import com.aerospike.skyhook.listener.BaseListener 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | class MockCommandHandler( 11 | ctx: ChannelHandlerContext 12 | ) : NettyResponseWriter(ctx), CommandHandler { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount >= 1) { BaseListener.argValidationErrorMsg(cmd) } 16 | 17 | when (cmd.command) { 18 | RedisCommand.RESET -> writeSimpleString("RESET") 19 | else -> writeOK() 20 | } 21 | flushCtxTransactionAware() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/PingCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.handler.NettyResponseWriter 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import io.netty.channel.ChannelHandlerContext 8 | 9 | class PingCommandHandler( 10 | ctx: ChannelHandlerContext 11 | ) : NettyResponseWriter(ctx), CommandHandler { 12 | 13 | override fun handle(cmd: RequestCommand) { 14 | require(cmd.argCount < 3) { BaseListener.argValidationErrorMsg(cmd) } 15 | 16 | val responseString = if (cmd.argCount == 2) { 17 | String(cmd.args[1]) 18 | } else { 19 | "PONG" 20 | } 21 | 22 | writeSimpleString(responseString) 23 | flushCtxTransactionAware() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/InfoUtils.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | import com.aerospike.client.Info 4 | import com.aerospike.client.cluster.Node 5 | import java.util.regex.Pattern 6 | 7 | object InfoUtils { 8 | 9 | fun getNamespaceInfo(ns: String, node: Node): Map { 10 | val schemaInfo = Info.request(null, node, "namespace/$ns") 11 | return schemaInfo.split(";") 12 | .associate { it.split("=".toRegex(), 2).toPair() } 13 | } 14 | 15 | fun getSetInfo(ns: String, set: String?, node: Node): Map { 16 | val sets = Info.request(null, node, "sets") 17 | val tableInfo = sets.split(";") 18 | .filter { it.startsWith("ns=$ns:set=$set") }[0] 19 | return Pattern.compile("\\s*:\\s*").split(tableInfo).toList() 20 | .filterNotNull().associate { it.split("=".toRegex(), 2).toPair() } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/docs/docker.md: -------------------------------------------------------------------------------- 1 | # Running In Docker 2 | 3 | ## Running on Docker 4 | 5 | :::warning 6 | 7 | This section requires installing docker directly from source. Future updates will include a docker image. 8 | 9 | ::: 10 | 11 | ### Use a prebuilt image 12 | 13 | Use the [Skyhook package](https://github.com/aerospike/skyhook/pkgs/container/skyhook) to start a container: 14 | 15 | ```sh 16 | docker run -d --name=skyhook -p 6379:6379 ghcr.io/aerospike/skyhook:latest 17 | ``` 18 | 19 | ### Build from source 20 | 21 | Build an image: 22 | 23 | ```sh 24 | docker build -t skyhook . 25 | ``` 26 | 27 | Run as a Docker container: 28 | 29 | ```sh 30 | docker run -d --name=skyhook -p 6379:6379 skyhook 31 | ``` 32 | 33 | The image uses the repository configuration file by default. 34 | [Bind mount](https://docs.docker.com/storage/bind-mounts/) a custom file to configure the server: 35 | 36 | ```sh 37 | docker run -d --name=skyhook -v "$(pwd)"/config/server.yml:/app/server.yml -p 6379:6379 skyhook 38 | ``` 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/TimeCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.handler.NettyResponseWriter 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import io.netty.channel.ChannelHandlerContext 8 | import java.time.Instant 9 | import java.util.concurrent.TimeUnit 10 | 11 | class TimeCommandHandler( 12 | ctx: ChannelHandlerContext 13 | ) : NettyResponseWriter(ctx), CommandHandler { 14 | 15 | override fun handle(cmd: RequestCommand) { 16 | require(cmd.argCount == 1) { BaseListener.argValidationErrorMsg(cmd) } 17 | 18 | val now = Instant.now() 19 | val seconds = now.epochSecond 20 | val microseconds = TimeUnit.NANOSECONDS.toMicros(now.nano.toLong()) 21 | 22 | writeObjectListStr(arrayListOf(seconds, microseconds)) 23 | flushCtxTransactionAware() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.10.0/docker.md: -------------------------------------------------------------------------------- 1 | # Running In Docker 2 | 3 | ## Running on Docker 4 | 5 | :::warning 6 | 7 | This section requires installing docker directly from source. Future updates will include a docker image. 8 | 9 | ::: 10 | 11 | ### Use a prebuilt image 12 | 13 | Use the [Skyhook package](https://github.com/aerospike/skyhook/pkgs/container/skyhook) to start a container: 14 | 15 | ```sh 16 | docker run -d --name=skyhook -p 6379:6379 ghcr.io/aerospike/skyhook:latest 17 | ``` 18 | 19 | ### Build from source 20 | 21 | Build an image: 22 | 23 | ```sh 24 | docker build -t skyhook . 25 | ``` 26 | 27 | Run as a Docker container: 28 | 29 | ```sh 30 | docker run -d --name=skyhook -p 6379:6379 skyhook 31 | ``` 32 | 33 | The image uses the repository configuration file by default. 34 | [Bind mount](https://docs.docker.com/storage/bind-mounts/) a custom file to configure the server: 35 | 36 | ```sh 37 | docker run -d --name=skyhook -v "$(pwd)"/config/server.yml:/app/server.yml -p 6379:6379 skyhook 38 | ``` 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/DelCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.listener.DeleteListener 5 | import com.aerospike.skyhook.command.RequestCommand 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import io.netty.channel.ChannelHandlerContext 8 | 9 | class DelCommandListener( 10 | ctx: ChannelHandlerContext 11 | ) : BaseListener(ctx), DeleteListener { 12 | 13 | override fun handle(cmd: RequestCommand) { 14 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 15 | 16 | val key = createKey(cmd.key) 17 | client.delete(null, this, defaultWritePolicy, key) 18 | } 19 | 20 | override fun onSuccess(key: Key?, existed: Boolean) { 21 | try { 22 | if (existed) { 23 | writeLong(1L) 24 | } else { 25 | writeLong(0L) 26 | } 27 | flushCtxTransactionAware() 28 | } catch (e: Exception) { 29 | closeCtx(e) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/ScanCommand.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.listener.ValueType 5 | import com.aerospike.skyhook.util.RegexUtils 6 | import java.util.* 7 | 8 | class ScanCommand(val cmd: RequestCommand, flagIndex: Int) { 9 | var MATCH: String? = null 10 | var COUNT: Long = defaultCount 11 | var TYPE: ValueType? = null 12 | 13 | val cursor = String(cmd.args[flagIndex - 1]) 14 | 15 | init { 16 | for (i in flagIndex until cmd.args.size step 2) { 17 | setFlag(i) 18 | } 19 | } 20 | 21 | private fun setFlag(i: Int) { 22 | val flagStr = String(cmd.args[i]) 23 | when (flagStr.uppercase(Locale.ENGLISH)) { 24 | "MATCH" -> MATCH = RegexUtils.format(String(cmd.args[i + 1])) 25 | "COUNT" -> COUNT = String(cmd.args[i + 1]).toLong() 26 | "TYPE" -> TYPE = ValueType.valueOf(cmd.args[i + 1]) 27 | else -> throw IllegalArgumentException(flagStr) 28 | } 29 | } 30 | 31 | companion object { 32 | const val zeroCursor = "0" 33 | const val defaultCount = 10L 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/Typed.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | import com.aerospike.client.Value 4 | import java.nio.ByteBuffer 5 | import java.nio.charset.StandardCharsets 6 | 7 | object Typed { 8 | 9 | @Suppress("kotlin:S108") 10 | fun getValue(wireVal: ByteArray): Value { 11 | try { 12 | return Value.DoubleValue(String(wireVal).toDouble()) 13 | } catch (ignore: NumberFormatException) { 14 | } 15 | try { 16 | StandardCharsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(wireVal)) 17 | } catch (ex: CharacterCodingException) { 18 | return Value.BytesValue(wireVal) 19 | } 20 | return Value.StringValue(wireVal.toString(Charsets.UTF_8)) 21 | } 22 | 23 | fun getStringValue(wireVal: ByteArray): Value { 24 | return Value.StringValue(wireVal.toString(Charsets.UTF_8)) 25 | } 26 | 27 | fun getInteger(wireVal: ByteArray): Int { 28 | return String(wireVal).toInt() 29 | } 30 | 31 | fun getLong(wireVal: ByteArray): Long { 32 | return String(wireVal).toLong() 33 | } 34 | 35 | fun getDouble(wireVal: ByteArray): Double { 36 | return String(wireVal).toDouble() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/TypeCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordListener 6 | import com.aerospike.skyhook.command.RequestCommand 7 | import com.aerospike.skyhook.listener.BaseListener 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | class TypeCommandListener( 11 | ctx: ChannelHandlerContext 12 | ) : BaseListener(ctx), RecordListener { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 16 | 17 | val key = createKey(cmd.key) 18 | client.get( 19 | null, this, null, 20 | key, aeroCtx.typeBin 21 | ) 22 | } 23 | 24 | override fun onSuccess(key: Key?, record: Record?) { 25 | if (record == null) { 26 | writeNullString() 27 | flushCtxTransactionAware() 28 | } else { 29 | try { 30 | writeResponse(record.bins[aeroCtx.typeBin]) 31 | flushCtxTransactionAware() 32 | } catch (e: Exception) { 33 | closeCtx(e) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/ExistsCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.listener.ExistsArrayListener 5 | import com.aerospike.skyhook.command.RequestCommand 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import io.netty.channel.ChannelHandlerContext 8 | 9 | class ExistsCommandListener( 10 | ctx: ChannelHandlerContext 11 | ) : BaseListener(ctx), ExistsArrayListener { 12 | 13 | override fun handle(cmd: RequestCommand) { 14 | require(cmd.argCount >= 2) { argValidationErrorMsg(cmd) } 15 | 16 | val keys = getKeys(cmd) 17 | client.exists( 18 | null, this, 19 | null, keys.toTypedArray() 20 | ) 21 | } 22 | 23 | private fun getKeys(cmd: RequestCommand): Set { 24 | return cmd.args.drop(1) 25 | .map { createKey(it) } 26 | .toSet() 27 | } 28 | 29 | override fun onSuccess(keys: Array?, exists: BooleanArray?) { 30 | try { 31 | val count = exists?.count { it } ?: 0 32 | writeLong(count) 33 | flushCtxTransactionAware() 34 | } catch (e: Exception) { 35 | closeCtx(e) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/aerospike/DbsizeCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.aerospike 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.listener.BaseListener 6 | import com.aerospike.skyhook.util.InfoUtils.getNamespaceInfo 7 | import com.aerospike.skyhook.util.InfoUtils.getSetInfo 8 | import io.netty.channel.ChannelHandlerContext 9 | import kotlin.math.floor 10 | 11 | class DbsizeCommandHandler( 12 | ctx: ChannelHandlerContext 13 | ) : BaseListener(ctx), CommandHandler { 14 | 15 | override fun handle(cmd: RequestCommand) { 16 | require(cmd.argCount == 1) { argValidationErrorMsg(cmd) } 17 | 18 | writeLong(getTableRecordsNumber(aeroCtx.namespace, aeroCtx.set)) 19 | flushCtxTransactionAware() 20 | } 21 | 22 | private fun getTableRecordsNumber(ns: String, set: String?): Long { 23 | val allRecords = client.nodes 24 | .map { getSetInfo(ns, set, it) } 25 | .sumOf { it["objects"]!!.toInt() } 26 | val replicationFactor = getNamespaceInfo(ns, client.nodes[0])["effective_replication_factor"]!!.toInt() 27 | return floor(allRecords.toDouble() / replicationFactor).toLong() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 1.8 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: 'temurin' 20 | java-version: 8 21 | 22 | - name: Set up Aerospike Database 23 | uses: reugn/github-action-aerospike@v1 24 | 25 | - name: Cache Gradle packages 26 | uses: actions/cache@v3 27 | with: 28 | path: | 29 | ~/.gradle/caches 30 | ~/.gradle/wrapper 31 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 32 | restore-keys: | 33 | ${{ runner.os }}-gradle- 34 | 35 | - name: Build with Gradle 36 | run: ./gradlew clean build 37 | 38 | - name: Cleanup Gradle Cache 39 | # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. 40 | # Restoring these files from a GitHub Actions cache might cause problems for future builds. 41 | run: | 42 | rm -f ~/.gradle/caches/modules-2/modules-2.lock 43 | rm -f ~/.gradle/caches/modules-2/gc.properties -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/list/LlenCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.list 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.cdt.ListOperation 6 | import com.aerospike.client.listener.RecordListener 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import io.netty.channel.ChannelHandlerContext 10 | 11 | class LlenCommandListener( 12 | ctx: ChannelHandlerContext 13 | ) : BaseListener(ctx), RecordListener { 14 | 15 | override fun handle(cmd: RequestCommand) { 16 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 17 | 18 | val key = createKey(cmd.key) 19 | val operation = ListOperation.size(aeroCtx.bin) 20 | client.operate( 21 | null, this, defaultWritePolicy, 22 | key, operation 23 | ) 24 | } 25 | 26 | override fun onSuccess(key: Key?, record: Record?) { 27 | if (record == null) { 28 | writeEmptyList() 29 | flushCtxTransactionAware() 30 | } else { 31 | try { 32 | writeResponse(record.bins[aeroCtx.bin]) 33 | flushCtxTransactionAware() 34 | } catch (e: Exception) { 35 | closeCtx(e) 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/MapSizeCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.cdt.MapOperation 6 | import com.aerospike.client.listener.RecordListener 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import io.netty.channel.ChannelHandlerContext 10 | 11 | class MapSizeCommandListener( 12 | ctx: ChannelHandlerContext 13 | ) : BaseListener(ctx), RecordListener { 14 | 15 | override fun handle(cmd: RequestCommand) { 16 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 17 | 18 | val key = createKey(cmd.key) 19 | val operation = MapOperation.size(aeroCtx.bin) 20 | 21 | client.operate( 22 | null, this, defaultWritePolicy, 23 | key, operation 24 | ) 25 | } 26 | 27 | override fun onSuccess(key: Key?, record: Record?) { 28 | if (record == null) { 29 | writeLong(0L) 30 | flushCtxTransactionAware() 31 | } else { 32 | try { 33 | writeResponse(record.bins[aeroCtx.bin]) 34 | flushCtxTransactionAware() 35 | } catch (e: Exception) { 36 | closeCtx(e) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/RandomkeyCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordSequenceListener 6 | import com.aerospike.client.policy.ScanPolicy 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import io.netty.channel.ChannelHandlerContext 10 | 11 | class RandomkeyCommandListener( 12 | ctx: ChannelHandlerContext 13 | ) : BaseListener(ctx), RecordSequenceListener { 14 | 15 | private var isEmpty: Boolean = true 16 | 17 | override fun handle(cmd: RequestCommand) { 18 | require(cmd.argCount == 1) { argValidationErrorMsg(cmd) } 19 | 20 | val scanPolicy = ScanPolicy(defaultWritePolicy) 21 | scanPolicy.maxRecords = 1 22 | scanPolicy.includeBinData = false 23 | 24 | client.scanAll( 25 | null, this, scanPolicy, 26 | aeroCtx.namespace, aeroCtx.set 27 | ) 28 | } 29 | 30 | override fun onRecord(key: Key?, record: Record?) { 31 | key?.let { 32 | writeResponse(it.userKey.`object`) 33 | isEmpty = false 34 | } 35 | } 36 | 37 | override fun onSuccess() { 38 | if (isEmpty) writeNullString() 39 | flushCtxTransactionAware() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/AuthCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RequestCommand 4 | import com.aerospike.skyhook.handler.CommandHandler 5 | import com.aerospike.skyhook.handler.NettyResponseWriter 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import com.aerospike.skyhook.pipeline.AerospikeChannelInitializer.Companion.authDetailsAttrKey 8 | import com.aerospike.skyhook.pipeline.AerospikeChannelInitializer.Companion.clientPoolAttrKey 9 | import com.aerospike.skyhook.util.client.AuthDetails 10 | import io.netty.channel.ChannelHandlerContext 11 | 12 | class AuthCommandHandler( 13 | ctx: ChannelHandlerContext, 14 | ) : NettyResponseWriter(ctx), CommandHandler { 15 | 16 | override fun handle(cmd: RequestCommand) { 17 | require(cmd.argCount == 3) { BaseListener.argValidationErrorMsg(cmd) } 18 | 19 | val user = String(cmd.args[1]) 20 | val password = String(cmd.args[2]) 21 | val authDetails = AuthDetails(user, password) 22 | 23 | val client = ctx.channel().attr(clientPoolAttrKey).get().getClient(authDetails) 24 | if (client != null) { 25 | ctx.channel().attr(authDetailsAttrKey).set(authDetails.hashString) 26 | writeOK() 27 | } else { 28 | writeErrorString("Invalid AUTH details") 29 | } 30 | flushCtxTransactionAware() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/TtlCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordListener 6 | import com.aerospike.skyhook.command.RedisCommand 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import io.netty.channel.ChannelHandlerContext 10 | 11 | class TtlCommandListener( 12 | ctx: ChannelHandlerContext 13 | ) : BaseListener(ctx), RecordListener { 14 | 15 | @Volatile 16 | private var m: Long = 1L 17 | 18 | override fun handle(cmd: RequestCommand) { 19 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 20 | 21 | val key = createKey(cmd.key) 22 | if (cmd.command == RedisCommand.PTTL) m = 1000L 23 | client.getHeader(null, this, defaultWritePolicy, key) 24 | } 25 | 26 | override fun onSuccess(key: Key?, record: Record?) { 27 | if (record == null) { 28 | writeLong(-2L) 29 | flushCtxTransactionAware() 30 | } else { 31 | try { 32 | val ttl = if (record.timeToLive == -1) -1L else record.timeToLive * m 33 | writeLong(ttl) 34 | flushCtxTransactionAware() 35 | } catch (e: Exception) { 36 | closeCtx(e) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /website/src/pages/SITE_index.tsx: -------------------------------------------------------------------------------- 1 | import { Redirect } from '@docusaurus/router'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import SkyhookHeroImage from '@site/static/img/hero.jpg'; 4 | import Layout from '@theme/Layout'; 5 | import clsx from 'clsx'; 6 | import React from 'react'; 7 | import styles from './index.module.css'; 8 | 9 | function HomepageHeader() { 10 | const { siteConfig } = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 |

{siteConfig.title}

15 | 16 | 17 |
{siteConfig.tagline}
18 | 19 | 20 |
21 | 22 |
23 |
24 | ); 25 | } 26 | 27 | export default function Home(): JSX.Element { 28 | const { siteConfig } = useDocusaurusContext(); 29 | return ( 30 | //
31 | 36 | 37 | 38 | //
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/MgetCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.BatchRead 4 | import com.aerospike.client.listener.BatchListListener 5 | import com.aerospike.client.policy.BatchPolicy 6 | import com.aerospike.skyhook.command.RequestCommand 7 | import com.aerospike.skyhook.listener.BaseListener 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | class MgetCommandListener( 11 | ctx: ChannelHandlerContext 12 | ) : BaseListener(ctx), BatchListListener { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount >= 2) { argValidationErrorMsg(cmd) } 16 | 17 | val keys = cmd.args.drop(1) 18 | .map { createKey(it) } 19 | .map { BatchRead(it, true) } 20 | client.get(null, this, BatchPolicy(defaultWritePolicy), keys) 21 | } 22 | 23 | override fun onSuccess(records: MutableList?) { 24 | if (records == null) { 25 | writeNullString() 26 | flushCtxTransactionAware() 27 | } else { 28 | try { 29 | writeObjectListStr(records.mapNotNull { 30 | if (it.record != null) { 31 | it.record.bins[aeroCtx.bin] 32 | } else null 33 | }) 34 | flushCtxTransactionAware() 35 | } catch (e: Exception) { 36 | closeCtx(e) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/redis/CommandCommandHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.redis 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import com.aerospike.skyhook.command.RequestCommand 5 | import com.aerospike.skyhook.handler.CommandHandler 6 | import com.aerospike.skyhook.handler.NettyResponseWriter 7 | import com.aerospike.skyhook.listener.BaseListener 8 | import io.netty.channel.ChannelHandlerContext 9 | import java.util.* 10 | 11 | class CommandCommandHandler( 12 | ctx: ChannelHandlerContext 13 | ) : NettyResponseWriter(ctx), CommandHandler { 14 | 15 | override fun handle(cmd: RequestCommand) { 16 | require(cmd.argCount >= 1) { BaseListener.argValidationErrorMsg(cmd) } 17 | 18 | if (cmd.argCount == 1) { 19 | RedisCommand.writeCommand(ctx) 20 | } else { 21 | when (String(cmd.args[1]).uppercase(Locale.ENGLISH)) { 22 | "COUNT" -> { 23 | writeLong(RedisCommand.totalCommands) 24 | } 25 | "INFO" -> { 26 | val commands = cmd.args.drop(2).map { String(it) } 27 | .map { it.lowercase(Locale.ENGLISH) } 28 | RedisCommand.writeCommandInfo(ctx, commands) 29 | } 30 | else -> { 31 | throw IllegalArgumentException(cmd.command.toString()) 32 | } 33 | } 34 | } 35 | flushCtxTransactionAware() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/AppendCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.* 4 | import com.aerospike.client.listener.RecordListener 5 | import com.aerospike.skyhook.command.RequestCommand 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import com.aerospike.skyhook.listener.ValueType 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | class AppendCommandListener( 11 | ctx: ChannelHandlerContext 12 | ) : BaseListener(ctx), RecordListener { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 16 | 17 | val key = createKey(cmd.key) 18 | val ops = arrayOf( 19 | *systemOps(ValueType.STRING), 20 | Operation.append(Bin(aeroCtx.bin, Value.StringValue(String(cmd.args[2])))), 21 | Operation.get(aeroCtx.bin) 22 | ) 23 | client.operate(null, this, defaultWritePolicy, key, *ops) 24 | } 25 | 26 | override fun onSuccess(key: Key?, record: Record?) { 27 | if (record == null) { 28 | writeNullString() 29 | flushCtxTransactionAware() 30 | } else { 31 | try { 32 | val value: String = (record.bins[aeroCtx.bin] as String) 33 | writeLong(value.length) 34 | flushCtxTransactionAware() 35 | } catch (e: Exception) { 36 | closeCtx(e) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/StrlenCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordListener 6 | import com.aerospike.skyhook.command.RequestCommand 7 | import com.aerospike.skyhook.listener.BaseListener 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | class StrlenCommandListener( 11 | ctx: ChannelHandlerContext 12 | ) : BaseListener(ctx), RecordListener, LengthFetcher { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 16 | 17 | val key = createKey(cmd.key) 18 | client.get(null, this, defaultWritePolicy, key) 19 | } 20 | 21 | override fun onSuccess(key: Key?, record: Record?) { 22 | if (record == null) { 23 | writeLong(0L) 24 | flushCtxTransactionAware() 25 | } else { 26 | try { 27 | writeResponse(valueLength(record.bins[aeroCtx.bin])) 28 | flushCtxTransactionAware() 29 | } catch (e: Exception) { 30 | closeCtx(e) 31 | } 32 | } 33 | } 34 | } 35 | 36 | interface LengthFetcher { 37 | 38 | fun valueLength(value: Any?): Long { 39 | return when (value) { 40 | is String -> value.length.toLong() 41 | is ByteArray -> value.size.toLong() 42 | null -> 0L 43 | else -> value.toString().length.toLong() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/config/ServerConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.config 2 | 3 | /** 4 | * Skyhook server configuration properties. 5 | */ 6 | data class ServerConfiguration( 7 | 8 | /** 9 | * The host list to seed the Aerospike cluster. 10 | */ 11 | val hostList: String = "localhost:3000", 12 | 13 | /** 14 | * The Aerospike namespace. 15 | */ 16 | val namespace: String = "test", 17 | 18 | /** 19 | * The Aerospike set name. 20 | */ 21 | val set: String? = "redis", 22 | 23 | /** 24 | * Aerospike Java Client [com.aerospike.client.policy.ClientPolicy] configuration. 25 | */ 26 | val clientPolicy: ClientPolicyConfig = ClientPolicyConfig(), 27 | 28 | /** 29 | * The Aerospike bin name to set values. 30 | */ 31 | val bin: String = "b", 32 | 33 | /** 34 | * The Aerospike bin name to set value type. 35 | */ 36 | val typeBin: String = "t", 37 | 38 | /** 39 | * The Aerospike transaction id bin name. 40 | */ 41 | val transactionIdBin: String = "tid", 42 | 43 | /** 44 | * The server port to bind to. 45 | */ 46 | val redisPort: Int = 6379, 47 | 48 | /** 49 | * The server will bind on unix socket if configured. 50 | */ 51 | val unixSocket: String? = null, 52 | 53 | /** 54 | * The Netty worker group size. 55 | */ 56 | val workerThreads: Int = Runtime.getRuntime().availableProcessors(), 57 | 58 | /** 59 | * The Netty acceptor group size. 60 | */ 61 | val bossThreads: Int = 2, 62 | ) 63 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/hyperlog/PfmergeCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.hyperlog 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordListener 6 | import com.aerospike.client.operation.HLLOperation 7 | import com.aerospike.client.operation.HLLPolicy 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.listener.BaseListener 10 | import io.netty.channel.ChannelHandlerContext 11 | 12 | class PfmergeCommandListener( 13 | ctx: ChannelHandlerContext 14 | ) : BaseListener(ctx), RecordListener { 15 | 16 | override fun handle(cmd: RequestCommand) { 17 | require(cmd.argCount > 3) { argValidationErrorMsg(cmd) } 18 | 19 | val key = createKey(cmd.key) 20 | 21 | val hllValues = cmd.args.drop(2) 22 | .map(::createKey) 23 | .mapNotNull { client.get(null, it)} 24 | .map { it.getHLLValue(aeroCtx.bin) } 25 | 26 | val operationPut = HLLOperation.setUnion(HLLPolicy.Default, aeroCtx.bin, hllValues) 27 | 28 | client.operate(null, this, defaultWritePolicy, key, operationPut) 29 | } 30 | 31 | override fun onSuccess(key: Key?, record: Record?) { 32 | if (record == null) { 33 | writeNullString() 34 | flushCtxTransactionAware() 35 | } else { 36 | try { 37 | writeOK() 38 | flushCtxTransactionAware() 39 | } catch (e: Exception) { 40 | closeCtx(e) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/list/LindexCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.list 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.cdt.ListOperation 6 | import com.aerospike.client.cdt.ListReturnType 7 | import com.aerospike.client.listener.RecordListener 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.listener.BaseListener 10 | import com.aerospike.skyhook.util.Typed 11 | import io.netty.channel.ChannelHandlerContext 12 | 13 | class LindexCommandListener( 14 | ctx: ChannelHandlerContext 15 | ) : BaseListener(ctx), RecordListener { 16 | 17 | override fun handle(cmd: RequestCommand) { 18 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 19 | 20 | val key = createKey(cmd.key) 21 | val index = Typed.getInteger(cmd.args[2]) 22 | 23 | val operation = ListOperation.getByIndex( 24 | aeroCtx.bin, index, 25 | ListReturnType.VALUE 26 | ) 27 | client.operate( 28 | null, this, defaultWritePolicy, 29 | key, operation 30 | ) 31 | } 32 | 33 | override fun onSuccess(key: Key?, record: Record?) { 34 | if (record == null) { 35 | writeEmptyList() 36 | flushCtxTransactionAware() 37 | } else { 38 | try { 39 | writeResponse(record.bins[aeroCtx.bin]) 40 | flushCtxTransactionAware() 41 | } catch (e: Exception) { 42 | closeCtx(e) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | defaults: 9 | run: 10 | working-directory: ./website 11 | 12 | jobs: 13 | checks: 14 | if: github.event_name != 'push' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | - name: Test Build 22 | run: | 23 | if [ -e yarn.lock ]; then 24 | yarn install --frozen-lockfile 25 | elif [ -e package-lock.json ]; then 26 | npm ci 27 | else 28 | npm i 29 | fi 30 | npm run build 31 | gh-release: 32 | if: github.event_name != 'pull_request' 33 | runs-on: ubuntu-latest 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: 18 39 | - uses: webfactory/ssh-agent@v0.5.0 40 | with: 41 | ssh-private-key: ${{ secrets.GH_PAGES_DEPLOY }} 42 | - name: Release to GitHub Pages 43 | env: 44 | USE_SSH: true 45 | GIT_USER: git 46 | run: | 47 | git config --global user.email "joem+deploybot@aerospike.com" 48 | git config --global user.name "Aerospike deploybot" 49 | if [ -e yarn.lock ]; then 50 | yarn install --frozen-lockfile 51 | elif [ -e package-lock.json ]; then 52 | npm ci 53 | else 54 | npm i 55 | fi 56 | npm run deploy 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/HstrlenCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapReturnType 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.listener.key.LengthFetcher 12 | import com.aerospike.skyhook.util.Typed 13 | import io.netty.channel.ChannelHandlerContext 14 | 15 | class HstrlenCommandListener( 16 | ctx: ChannelHandlerContext 17 | ) : BaseListener(ctx), RecordListener, LengthFetcher { 18 | 19 | override fun handle(cmd: RequestCommand) { 20 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 21 | 22 | val key = createKey(cmd.key) 23 | val mapKey: Value = Typed.getValue(cmd.args[2]) 24 | val operation = MapOperation.getByKey( 25 | aeroCtx.bin, mapKey, 26 | MapReturnType.VALUE 27 | ) 28 | client.operate( 29 | null, this, defaultWritePolicy, key, operation 30 | ) 31 | } 32 | 33 | override fun onSuccess(key: Key?, record: Record?) { 34 | if (record == null) { 35 | writeLong(0L) 36 | flushCtxTransactionAware() 37 | } else { 38 | try { 39 | writeResponse(valueLength(record.bins[aeroCtx.bin])) 40 | flushCtxTransactionAware() 41 | } catch (e: Exception) { 42 | closeCtx(e) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/hyperlog/PfaddCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.hyperlog 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordListener 6 | import com.aerospike.client.operation.HLLOperation 7 | import com.aerospike.client.operation.HLLPolicy 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.listener.BaseListener 10 | import com.aerospike.skyhook.util.Typed 11 | import io.netty.channel.ChannelHandlerContext 12 | 13 | open class PfaddCommandListener( 14 | ctx: ChannelHandlerContext 15 | ) : BaseListener(ctx), RecordListener { 16 | 17 | override fun handle(cmd: RequestCommand) { 18 | require(cmd.argCount > 2) { argValidationErrorMsg(cmd) } 19 | 20 | val key = createKey(cmd.key) 21 | 22 | val operation = HLLOperation.add( 23 | HLLPolicy.Default, 24 | aeroCtx.bin, 25 | getValues(cmd), 26 | 16 27 | ) 28 | client.operate(null, this, defaultWritePolicy, key, operation) 29 | } 30 | 31 | protected open fun getValues(cmd: RequestCommand) = 32 | cmd.args.drop(2).map { Typed.getValue(it) } 33 | 34 | override fun onSuccess(key: Key?, record: Record?) { 35 | if (record == null) { 36 | writeNullString() 37 | flushCtxTransactionAware() 38 | } else { 39 | try { 40 | val entitiesWritten = record.getLong(aeroCtx.bin) 41 | writeLong(if (entitiesWritten > 0) 1L else 0L) 42 | flushCtxTransactionAware() 43 | } catch (e: Exception) { 44 | closeCtx(e) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/MapDelCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapReturnType 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.util.Typed 12 | import io.netty.channel.ChannelHandlerContext 13 | 14 | class MapDelCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), RecordListener { 17 | 18 | override fun handle(cmd: RequestCommand) { 19 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 20 | 21 | val key = createKey(cmd.key) 22 | val operation = MapOperation.removeByKeyList( 23 | aeroCtx.bin, 24 | getValues(cmd), MapReturnType.COUNT 25 | ) 26 | client.operate( 27 | null, this, defaultWritePolicy, 28 | key, operation, *systemOps() 29 | ) 30 | } 31 | 32 | private fun getValues(cmd: RequestCommand): List { 33 | return cmd.args.drop(2) 34 | .map { Typed.getValue(it) } 35 | } 36 | 37 | override fun onSuccess(key: Key?, record: Record?) { 38 | if (record == null) { 39 | writeNullArray() 40 | flushCtxTransactionAware() 41 | } else { 42 | try { 43 | writeResponse(record.bins[aeroCtx.bin]) 44 | flushCtxTransactionAware() 45 | } catch (e: Exception) { 46 | closeCtx(e) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/SmergeCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordArrayListener 6 | import com.aerospike.client.policy.BatchPolicy 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import com.aerospike.skyhook.util.IntersectMerge 10 | import com.aerospike.skyhook.util.Merge 11 | import com.aerospike.skyhook.util.UnionMerge 12 | import io.netty.channel.ChannelHandlerContext 13 | 14 | abstract class SmergeBaseCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), RecordArrayListener, Merge { 17 | 18 | override fun handle(cmd: RequestCommand) { 19 | require(cmd.argCount >= 2) { argValidationErrorMsg(cmd) } 20 | 21 | client.get( 22 | null, this, BatchPolicy(defaultWritePolicy), 23 | getKeys(cmd).toTypedArray() 24 | ) 25 | } 26 | 27 | private fun getKeys(cmd: RequestCommand): List { 28 | return cmd.args.drop(1) 29 | .map { createKey(it) } 30 | } 31 | 32 | override fun onSuccess(keys: Array?, records: Array?) { 33 | if (records == null) { 34 | writeEmptyList() 35 | } else { 36 | val values = merge(records.filterNotNull() 37 | .map { it.getMap(aeroCtx.bin) }.map { it.keys }) 38 | writeResponse(values) 39 | } 40 | flushCtxTransactionAware() 41 | } 42 | } 43 | 44 | class SinterCommandListener( 45 | ctx: ChannelHandlerContext 46 | ) : SmergeBaseCommandListener(ctx), IntersectMerge 47 | 48 | class SunionCommandListener( 49 | ctx: ChannelHandlerContext 50 | ) : SmergeBaseCommandListener(ctx), UnionMerge 51 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/SystemCommandsTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | 8 | class SystemCommandsTest() : SkyhookIntegrationTestBase() { 9 | 10 | @Test 11 | fun testPing() { 12 | writeCommand(RedisCommand.PING.name) 13 | val response = readString() 14 | assertEquals("PONG", response) 15 | } 16 | 17 | @Test 18 | fun testEcho() { 19 | writeCommand("${RedisCommand.ECHO.name} abc") 20 | val response = readString() 21 | assertEquals("abc", response) 22 | } 23 | 24 | @Test 25 | fun testCommandInfo() { 26 | writeCommand("${RedisCommand.COMMAND.name} COUNT") 27 | val commands = readLong() 28 | assertTrue { commands > 50 } 29 | } 30 | 31 | @Test 32 | fun testReset() { 33 | writeCommand(RedisCommand.RESET.name) 34 | val response = readString() 35 | assertEquals("RESET", response) 36 | } 37 | 38 | @Test 39 | fun testSave() { 40 | writeCommand(RedisCommand.SAVE.name) 41 | val response = readString() 42 | assertEquals(ok, response) 43 | } 44 | 45 | @Test 46 | fun testBgsave() { 47 | writeCommand(RedisCommand.BGSAVE.name) 48 | val response = readString() 49 | assertEquals(ok, response) 50 | } 51 | 52 | @Test 53 | fun testQuit() { 54 | writeCommand(RedisCommand.QUIT.name) 55 | val response = readString() 56 | assertEquals(ok, response) 57 | } 58 | 59 | @Test 60 | fun testTime() { 61 | writeCommand(RedisCommand.TIME.name) 62 | val response = readStringArray() 63 | assertEquals(2, response.size) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/list/LrangeCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.list 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.cdt.ListOperation 6 | import com.aerospike.client.cdt.ListReturnType 7 | import com.aerospike.client.listener.RecordListener 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.listener.BaseListener 10 | import com.aerospike.skyhook.util.Typed 11 | import io.netty.channel.ChannelHandlerContext 12 | 13 | class LrangeCommandListener( 14 | ctx: ChannelHandlerContext 15 | ) : BaseListener(ctx), RecordListener { 16 | 17 | override fun handle(cmd: RequestCommand) { 18 | require(cmd.argCount == 4) { argValidationErrorMsg(cmd) } 19 | 20 | val key = createKey(cmd.key) 21 | val from = Typed.getInteger(cmd.args[2]) 22 | val to = Typed.getInteger(cmd.args[3]) 23 | 24 | // TODO support negative indexes 25 | require(from >= 0 && to >= 0) { "${cmd.command} negative index" } 26 | require(from <= to) { "${cmd.command} invalid indexes" } 27 | val count = (to - from) + 1 28 | 29 | val operation = ListOperation.getByIndexRange( 30 | aeroCtx.bin, from, 31 | count, ListReturnType.VALUE 32 | ) 33 | client.operate( 34 | null, this, defaultWritePolicy, 35 | key, operation 36 | ) 37 | } 38 | 39 | override fun onSuccess(key: Key?, record: Record?) { 40 | if (record == null) { 41 | writeEmptyList() 42 | flushCtxTransactionAware() 43 | } else { 44 | try { 45 | writeResponse(record.bins[aeroCtx.bin]) 46 | flushCtxTransactionAware() 47 | } catch (e: Exception) { 48 | closeCtx(e) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/TransactionTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertEquals 6 | 7 | class TransactionTest() : SkyhookIntegrationTestBase() { 8 | 9 | @Test 10 | fun testTransaction() { 11 | writeCommand(RedisCommand.MULTI.name) 12 | assertEquals(ok, readString()) 13 | 14 | writeCommand("${RedisCommand.SET.name} key1 val1") 15 | assertEquals("QUEUED", readString()) 16 | 17 | writeCommand("${RedisCommand.GET.name} key1") 18 | assertEquals("QUEUED", readString()) 19 | 20 | writeCommand("NE abc") 21 | assert(readError().isNotEmpty()) 22 | 23 | writeCommand(RedisCommand.PING.name) 24 | assertEquals("QUEUED", readString()) 25 | 26 | writeCommand(RedisCommand.EXEC.name) 27 | assertEquals(3L, readArrayLen()) 28 | 29 | assertEquals(ok, readString()) 30 | assertEquals("val1", readFullBulkString()) 31 | assertEquals("PONG", readString()) 32 | } 33 | 34 | @Test 35 | fun testDiscardTransaction() { 36 | writeCommand(RedisCommand.MULTI.name) 37 | assertEquals(ok, readString()) 38 | 39 | writeCommand("${RedisCommand.SET.name} key1 val1") 40 | assertEquals("QUEUED", readString()) 41 | 42 | writeCommand("${RedisCommand.GET.name} key1") 43 | assertEquals("QUEUED", readString()) 44 | 45 | writeCommand(RedisCommand.DISCARD.name) 46 | assertEquals(ok, readString()) 47 | 48 | writeCommand(RedisCommand.EXEC.name) 49 | assert(readError().isNotEmpty()) 50 | 51 | writeCommand(RedisCommand.PING.name) 52 | assertEquals("PONG", readString()) 53 | } 54 | 55 | @Test 56 | fun testExecWithoutMultiTransaction() { 57 | writeCommand(RedisCommand.EXEC.name) 58 | assert(readError().isNotEmpty()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | /* stylelint-disable docusaurus/copyright-header */ 7 | 8 | /** 9 | * CSS files with the .module.css suffix will be treated as CSS modules 10 | * and scoped locally. 11 | */ 12 | 13 | .heroBanner { 14 | padding: 0; 15 | text-align: left; 16 | position: relative; 17 | overflow: hidden; 18 | background-image: url(../../static/img/hero-bg-right.webp); 19 | background-color: #17104e; 20 | background-repeat: repeat; 21 | background-size: cover; 22 | background-position: right center; 23 | } 24 | .contentBody { 25 | background-color: #17104e; 26 | } 27 | .heroBanner h1 { 28 | font-weight: normal; 29 | font-size: 4rem; 30 | } 31 | .heroBanner h3 { 32 | font-weight: normal; 33 | padding-left: 5%; 34 | } 35 | .heroText { 36 | display: inline-block; 37 | } 38 | .smallA { 39 | width: 64px; 40 | padding-right: 5px; 41 | padding-left: 30px; 42 | display: inline-block; 43 | } 44 | @media screen and (max-width: 966px) { 45 | .heroBanner { 46 | padding: 2rem; 47 | } 48 | .heroBanner h1 { 49 | font-size: 3rem; 50 | } 51 | .heroBanner h3 { 52 | font-size: 1.25rem; 53 | padding-bottom: 10px; 54 | } 55 | .smallA { 56 | width: 48px; 57 | } 58 | } 59 | @media screen and (max-width: 595px) { 60 | .heroBanner { 61 | padding: 1.5rem; 62 | } 63 | .heroBanner h1 { 64 | font-size: 2.5rem; 65 | } 66 | .heroBanner h3 { 67 | font-size: 1.1rem; 68 | padding-bottom: 10px; 69 | } 70 | .smallA { 71 | width: 40px; 72 | } 73 | } 74 | @media screen and (max-width: 496px) { 75 | .heroBanner { 76 | padding: 1.2rem; 77 | } 78 | .heroBanner h1 { 79 | font-size: 1.5rem; 80 | } 81 | .heroBanner h3 { 82 | font-size: 0.7rem; 83 | } 84 | .smallA { 85 | width: 25px; 86 | } 87 | } 88 | @media screen and (max-width: 412px) { 89 | .heroBanner h1 { 90 | font-size: 1.4rem; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/KeysCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.ScanCallback 6 | import com.aerospike.client.exp.Exp 7 | import com.aerospike.client.policy.ScanPolicy 8 | import com.aerospike.client.query.KeyRecord 9 | import com.aerospike.client.query.RegexFlag 10 | import com.aerospike.skyhook.command.RequestCommand 11 | import com.aerospike.skyhook.listener.BaseListener 12 | import com.aerospike.skyhook.util.RegexUtils 13 | import io.netty.channel.ChannelHandlerContext 14 | 15 | class KeysCommandListener( 16 | ctx: ChannelHandlerContext 17 | ) : BaseListener(ctx) { 18 | 19 | private lateinit var regexString: String 20 | private val recordSet: RecordSet by lazy { 21 | RecordSet() 22 | } 23 | 24 | override fun handle(cmd: RequestCommand) { 25 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 26 | regexString = RegexUtils.format(String(cmd.args[1])) 27 | 28 | scan() 29 | writeScanResponse() 30 | } 31 | 32 | private fun scan() { 33 | client.scanAll(buildScanPolicy(), aeroCtx.namespace, aeroCtx.set, callback) 34 | } 35 | 36 | private fun buildScanPolicy(): ScanPolicy { 37 | val scanPolicy = ScanPolicy() 38 | scanPolicy.sendKey = true 39 | scanPolicy.includeBinData = false 40 | 41 | scanPolicy.filterExp = Exp.build( 42 | Exp.regexCompare( 43 | regexString, 44 | RegexFlag.ICASE or RegexFlag.NEWLINE, 45 | Exp.key(Exp.Type.STRING) 46 | ) 47 | ) 48 | return scanPolicy 49 | } 50 | 51 | private fun writeScanResponse() { 52 | writeObjectListStr(recordSet.map { it.key.userKey.`object` as String }) 53 | flushCtxTransactionAware() 54 | } 55 | 56 | private val callback = ScanCallback { key: Key?, record: Record? -> 57 | recordSet.add(KeyRecord(key, record)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/ZrangestoreCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapPolicy 8 | import com.aerospike.client.cdt.MapReturnType 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.util.Typed 11 | import io.netty.channel.ChannelHandlerContext 12 | 13 | class ZrangestoreCommandListener( 14 | ctx: ChannelHandlerContext 15 | ) : ZrangeCommandListener(ctx) { 16 | 17 | override fun handle(cmd: RequestCommand) { 18 | require(cmd.argCount >= 4) { argValidationErrorMsg(cmd) } 19 | 20 | val destKey = createKey(cmd.key) 21 | val sourceKey = createKey(cmd.args[2]) 22 | rangeCommand = RangeCommand(cmd, 5) 23 | validateAndSet() 24 | 25 | val record = client.operate( 26 | defaultWritePolicy, sourceKey, getMapOperation() 27 | ) 28 | 29 | @Suppress("UNCHECKED_CAST") 30 | val putOperation = MapOperation.putItems( 31 | MapPolicy(), 32 | aeroCtx.bin, 33 | (record.bins[aeroCtx.bin] as List>).associate { 34 | Typed.getValue(it.key.toString().toByteArray()) to Value.get(it.value) 35 | } 36 | ) 37 | client.operate( 38 | null, this, defaultWritePolicy, 39 | destKey, putOperation, *systemOps() 40 | ) 41 | } 42 | 43 | override fun getMapReturnType(): Int { 44 | return MapReturnType.KEY_VALUE 45 | } 46 | 47 | override fun onSuccess(key: Key?, record: Record?) { 48 | try { 49 | if (record == null) { 50 | writeLong(0L) 51 | } else { 52 | writeLong(record.getLong(aeroCtx.bin)) 53 | } 54 | flushCtxTransactionAware() 55 | } catch (e: Exception) { 56 | closeCtx(e) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/GetsetCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.* 4 | import com.aerospike.client.listener.RecordListener 5 | import com.aerospike.skyhook.command.RequestCommand 6 | import com.aerospike.skyhook.listener.BaseListener 7 | import com.aerospike.skyhook.util.Typed 8 | import io.netty.channel.ChannelHandlerContext 9 | 10 | open class GetsetCommandListener( 11 | ctx: ChannelHandlerContext 12 | ) : BaseListener(ctx), RecordListener { 13 | 14 | override fun handle(cmd: RequestCommand) { 15 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 16 | 17 | val key = createKey(cmd.key) 18 | val value = Typed.getValue(cmd.args[2]) 19 | val ops = arrayOf( 20 | Operation.get(aeroCtx.bin), 21 | Operation.put(Bin(aeroCtx.bin, value)), 22 | *systemOps() 23 | ) 24 | 25 | client.operate(null, this, updateOnlyPolicy, key, *ops) 26 | } 27 | 28 | override fun writeError(e: AerospikeException?) { 29 | writeNullString() 30 | } 31 | 32 | override fun onSuccess(key: Key?, record: Record?) { 33 | if (record == null) { 34 | writeNullString() 35 | flushCtxTransactionAware() 36 | } else { 37 | try { 38 | writeResponse(record.bins[aeroCtx.bin]) 39 | flushCtxTransactionAware() 40 | } catch (e: Exception) { 41 | closeCtx(e) 42 | } 43 | } 44 | } 45 | } 46 | 47 | class GetdelCommandListener( 48 | ctx: ChannelHandlerContext 49 | ) : GetsetCommandListener(ctx), RecordListener { 50 | 51 | override fun handle(cmd: RequestCommand) { 52 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 53 | 54 | val key = createKey(cmd.key) 55 | val ops = arrayOf( 56 | Operation.get(aeroCtx.bin), 57 | Operation.delete(), 58 | *systemOps() 59 | ) 60 | 61 | client.operate(null, this, updateOnlyPolicy, key, *ops) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/list/ListPopCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.list 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Operation 5 | import com.aerospike.client.Record 6 | import com.aerospike.client.cdt.ListOperation 7 | import com.aerospike.client.listener.RecordListener 8 | import com.aerospike.skyhook.command.RedisCommand 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.util.Typed 12 | import io.netty.channel.ChannelHandlerContext 13 | 14 | class ListPopCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), RecordListener { 17 | 18 | override fun handle(cmd: RequestCommand) { 19 | require(cmd.argCount == 2 || cmd.argCount == 3) { 20 | argValidationErrorMsg(cmd) 21 | } 22 | 23 | val key = createKey(cmd.key) 24 | client.operate( 25 | null, this, defaultWritePolicy, 26 | key, getListOperation(cmd), *systemOps() 27 | ) 28 | } 29 | 30 | private fun getListOperation(cmd: RequestCommand): Operation { 31 | val count = if (cmd.argCount == 2) 1 else Typed.getInteger(cmd.args[2]) 32 | return when (cmd.command) { 33 | RedisCommand.LPOP -> { 34 | ListOperation.popRange(aeroCtx.bin, 0, count) 35 | } 36 | RedisCommand.RPOP -> { 37 | ListOperation.popRange(aeroCtx.bin, -1 * count, count) 38 | } 39 | else -> { 40 | throw IllegalArgumentException(cmd.command.toString()) 41 | } 42 | } 43 | } 44 | 45 | override fun onSuccess(key: Key?, record: Record?) { 46 | if (record == null) { 47 | writeNullArray() 48 | flushCtxTransactionAware() 49 | } else { 50 | try { 51 | writeResponse(record.bins[aeroCtx.bin]) 52 | flushCtxTransactionAware() 53 | } catch (e: Exception) { 54 | closeCtx(e) 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/hyperlog/PfcountCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.hyperlog 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.listener.RecordListener 6 | import com.aerospike.client.operation.HLLOperation 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import io.netty.channel.ChannelHandlerContext 10 | 11 | class PfcountCommandListener( 12 | ctx: ChannelHandlerContext 13 | ) : BaseListener(ctx), RecordListener { 14 | 15 | override fun handle(cmd: RequestCommand) { 16 | require(cmd.argCount > 1) { argValidationErrorMsg(cmd) } 17 | 18 | if (cmd.args.size == 2) { 19 | val key = createKey(cmd.args[1]) 20 | countSingleKey(key) 21 | } else { 22 | val keys = cmd.args.drop(1).map(::createKey) 23 | countMultipleKeys(keys) 24 | } 25 | } 26 | 27 | private fun countSingleKey(key: Key) { 28 | val operation = HLLOperation.getCount(aeroCtx.bin) 29 | client.operate(null, this, defaultWritePolicy, key, operation) 30 | } 31 | 32 | private fun countMultipleKeys(keys: List) { 33 | val hllValuesByKey = keys 34 | .associateWith { client.get(null, it) } 35 | .filterValues { it != null } 36 | .mapValues { it.value.getHLLValue(aeroCtx.bin) } 37 | 38 | if (hllValuesByKey.isEmpty()) { 39 | writeZero() 40 | return 41 | } 42 | 43 | val operation = HLLOperation.getUnionCount(aeroCtx.bin, hllValuesByKey.values.toList()) 44 | 45 | client.operate(null, this, defaultWritePolicy, hllValuesByKey.keys.first(), operation) 46 | } 47 | 48 | override fun onSuccess(key: Key?, record: Record?) { 49 | if (record == null) { 50 | writeZero() 51 | } else { 52 | try { 53 | writeLong(record.getLong(aeroCtx.bin)) 54 | flushCtxTransactionAware() 55 | } catch (e: Exception) { 56 | closeCtx(e) 57 | } 58 | } 59 | } 60 | 61 | private fun writeZero() { 62 | writeLong(0L) 63 | flushCtxTransactionAware() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@algolia/client-search": "^4.15.0", 19 | "@docusaurus/core": "^2.3.1", 20 | "@docusaurus/preset-classic": "^2.3.1", 21 | "@mdx-js/react": "^1.6.22", 22 | "@svgr/webpack": "^6.5.1", 23 | "browserslist": "^4.21.5", 24 | "chokidar": "^3.5.2", 25 | "clsx": "^1.1.1", 26 | "file-loader": "^6.2.0", 27 | "glob-parent": "^6.0.2", 28 | "immer": "^9.0.7", 29 | "prism-react-renderer": "^1.2.1", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "remark-parse": "^10.0.0", 33 | "trim": "^1.0.1", 34 | "url-loader": "^4.1.1", 35 | "webpack": "^5.76.1" 36 | }, 37 | "devDependencies": { 38 | "@docusaurus/module-type-aliases": "^2.3.1", 39 | "@tsconfig/docusaurus": "^1.0.7", 40 | "@types/react": "^18.0.28", 41 | "@types/react-helmet": "^6.1.6", 42 | "@types/react-router-dom": "^5.3.3", 43 | "@typescript-eslint/eslint-plugin": "^4.33.0", 44 | "@typescript-eslint/parser": "^4.33.0", 45 | "eslint": "^8.36.0", 46 | "eslint-config-airbnb": "^19.0.4", 47 | "eslint-config-airbnb-typescript": "^17.0.0", 48 | "eslint-config-prettier": "^8.7.0", 49 | "eslint-plugin-import": "^2.27.5", 50 | "eslint-plugin-jest": "^27.2.1", 51 | "eslint-plugin-jsx-a11y": "^6.7.1", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "eslint-plugin-react": "^7.32.2", 54 | "eslint-plugin-react-hooks": "^4.6.0", 55 | "prettier": "^2.8.4", 56 | "typescript": "^4.9.5" 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.5%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/client/AerospikeClientPoolImpl.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util.client 2 | 3 | import com.aerospike.client.AerospikeClient 4 | import com.aerospike.client.Host 5 | import com.aerospike.client.IAerospikeClient 6 | import com.aerospike.client.policy.ClientPolicy 7 | import com.aerospike.skyhook.config.ServerConfiguration 8 | import com.google.common.cache.Cache 9 | import com.google.common.cache.CacheBuilder 10 | import mu.KotlinLogging 11 | import java.util.* 12 | import javax.inject.Inject 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | class AerospikeClientPoolImpl @Inject constructor( 17 | private val config: ServerConfiguration, 18 | private val clientPolicy: ClientPolicy 19 | ) : AerospikeClientPool { 20 | 21 | companion object { 22 | private val log = KotlinLogging.logger {} 23 | 24 | const val defaultAerospikePort = 3000 25 | private const val clientPoolSize = 8L 26 | } 27 | 28 | private val clientPool: Cache = 29 | CacheBuilder.newBuilder().maximumSize(clientPoolSize).build() 30 | 31 | private val defaultClient: IAerospikeClient by lazy { 32 | createClient(clientPolicy) 33 | } 34 | 35 | override fun getClient(authDetails: AuthDetails): IAerospikeClient? { 36 | val key = authDetails.hashString 37 | return Optional.ofNullable(clientPool.getIfPresent(key)).orElseGet { 38 | val policy = ClientPolicy(clientPolicy) 39 | policy.user = authDetails.user 40 | policy.password = authDetails.password 41 | 42 | try { 43 | val client = createClient(policy) 44 | log.info("Cache a new AerospikeClient") 45 | clientPool.put(key, client) 46 | client 47 | } catch (e: Exception) { 48 | null 49 | } 50 | } 51 | } 52 | 53 | override fun getClient(authDetailsHash: String?): IAerospikeClient { 54 | return authDetailsHash?.let { clientPool.getIfPresent(it) } ?: defaultClient 55 | } 56 | 57 | private fun createClient(policy: ClientPolicy): IAerospikeClient { 58 | return AerospikeClient( 59 | policy, 60 | *Host.parseHosts(config.hostList, defaultAerospikePort) 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/pipeline/AerospikeChannelInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.pipeline 2 | 3 | import com.aerospike.skyhook.config.AerospikeContext 4 | import com.aerospike.skyhook.config.ServerConfiguration 5 | import com.aerospike.skyhook.handler.AerospikeChannelHandler 6 | import com.aerospike.skyhook.util.TransactionState 7 | import com.aerospike.skyhook.util.client.AerospikeClientPool 8 | import io.netty.channel.Channel 9 | import io.netty.channel.ChannelInitializer 10 | import io.netty.handler.codec.redis.RedisArrayAggregator 11 | import io.netty.handler.codec.redis.RedisBulkStringAggregator 12 | import io.netty.handler.codec.redis.RedisDecoder 13 | import io.netty.handler.codec.redis.RedisEncoder 14 | import io.netty.util.AttributeKey 15 | import java.util.concurrent.ExecutorService 16 | import javax.inject.Inject 17 | import javax.inject.Singleton 18 | 19 | @Singleton 20 | class AerospikeChannelInitializer @Inject constructor( 21 | private val config: ServerConfiguration, 22 | private val clientPool: AerospikeClientPool, 23 | private val aerospikeChannelHandler: AerospikeChannelHandler, 24 | private val executorService: ExecutorService 25 | ) : ChannelInitializer() { 26 | 27 | companion object { 28 | val authDetailsAttrKey: AttributeKey = AttributeKey.valueOf("authDetails") 29 | val aeroCtxAttrKey: AttributeKey = AttributeKey.valueOf("aeroCtx") 30 | val clientPoolAttrKey: AttributeKey = AttributeKey.valueOf("clientPool") 31 | val transactionAttrKey: AttributeKey = AttributeKey.valueOf("transactionState") 32 | } 33 | 34 | override fun initChannel(ch: Channel) { 35 | ch.pipeline().addLast( 36 | RedisDecoder(true), RedisBulkStringAggregator(), RedisArrayAggregator(), 37 | RedisEncoder(), aerospikeChannelHandler 38 | ) 39 | 40 | ch.attr(aeroCtxAttrKey).set( 41 | AerospikeContext( 42 | config.namespace, 43 | config.set, 44 | config.bin, 45 | config.typeBin, 46 | config.transactionIdBin 47 | ) 48 | ) 49 | 50 | ch.attr(clientPoolAttrKey).set(clientPool) 51 | ch.attr(transactionAttrKey).set(TransactionState(executorService)) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/util/Intervals.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.util 2 | 3 | import java.util.* 4 | 5 | object Intervals { 6 | 7 | private const val infHighest = "+inf" 8 | private const val infLowest = "-inf" 9 | 10 | private const val lexHighest = "+" 11 | private const val lexLowest = "-" 12 | 13 | private const val scoreExclusive = "(" 14 | private const val lexExclusive = "[" 15 | 16 | fun fromScore(interval: String): Int { 17 | return score(interval, 0, 1) 18 | } 19 | 20 | fun upScore(interval: String): Int { 21 | return score(interval, 1, 0) 22 | } 23 | 24 | private fun score(interval: String, includeShift: Int, excludeShift: Int): Int { 25 | return when (interval.lowercase(Locale.ENGLISH)) { 26 | infHighest -> Int.MAX_VALUE 27 | infLowest -> Int.MIN_VALUE 28 | else -> { 29 | return if (interval.startsWith(scoreExclusive)) { 30 | interval.drop(1).toInt() + excludeShift 31 | } else { 32 | interval.toInt() + includeShift 33 | } 34 | } 35 | } 36 | } 37 | 38 | fun fromLex(interval: String): String { 39 | return lex(interval, true) 40 | } 41 | 42 | fun upLex(interval: String): String { 43 | return lex(interval, false) 44 | } 45 | 46 | private fun lex(interval: String, from: Boolean): String { 47 | return when (interval.lowercase(Locale.ENGLISH)) { 48 | lexHighest -> String(byteArrayOf(127)) 49 | lexLowest -> String(byteArrayOf(0)) 50 | else -> { 51 | return if (interval.startsWith(lexExclusive)) { 52 | if (from) { 53 | fixLexExclusive(interval.drop(1)) 54 | } else { 55 | interval.drop(1) 56 | } 57 | } else { 58 | if (from) { 59 | interval 60 | } else { 61 | fixLexExclusive(interval) 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | private fun fixLexExclusive(str: String): String { 69 | val byteArray = str.toByteArray() 70 | byteArray[byteArray.size - 1]++ 71 | return String(byteArray) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/HexistsCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapReturnType 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.util.Typed 12 | import io.netty.channel.ChannelHandlerContext 13 | 14 | class HexistsCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), RecordListener { 17 | 18 | override fun handle(cmd: RequestCommand) { 19 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 20 | 21 | val key = createKey(cmd.key) 22 | val operation = MapOperation.getByKey( 23 | aeroCtx.bin, Typed.getValue(cmd.args[2]), 24 | MapReturnType.COUNT 25 | ) 26 | client.operate( 27 | null, this, defaultWritePolicy, 28 | key, operation 29 | ) 30 | } 31 | 32 | override fun onSuccess(key: Key?, record: Record?) { 33 | if (record == null) { 34 | writeLong(0L) 35 | } else { 36 | writeResponse(record.bins[aeroCtx.bin]) 37 | } 38 | flushCtxTransactionAware() 39 | } 40 | } 41 | 42 | class SmismemberCommandListener( 43 | ctx: ChannelHandlerContext 44 | ) : BaseListener(ctx) { 45 | 46 | override fun handle(cmd: RequestCommand) { 47 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 48 | 49 | val key = createKey(cmd.key) 50 | val values = getValues(cmd) 51 | writeArrayHeader(values.size.toLong()) 52 | 53 | values.forEach { v -> 54 | val operation = MapOperation.getByKey( 55 | aeroCtx.bin, v, MapReturnType.COUNT 56 | ) 57 | val exists = client.operate( 58 | defaultWritePolicy, 59 | key, operation 60 | )?.getLong(aeroCtx.bin) ?: 0L 61 | writeLong(exists) 62 | } 63 | flushCtxTransactionAware() 64 | } 65 | 66 | private fun getValues(cmd: RequestCommand): List { 67 | return cmd.args.drop(2) 68 | .map { Typed.getValue(it) } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/HincrbyCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapPolicy 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RedisCommand 10 | import com.aerospike.skyhook.command.RequestCommand 11 | import com.aerospike.skyhook.listener.BaseListener 12 | import com.aerospike.skyhook.util.Typed 13 | import io.netty.channel.ChannelHandlerContext 14 | 15 | class HincrbyCommandListener( 16 | ctx: ChannelHandlerContext 17 | ) : BaseListener(ctx), RecordListener { 18 | 19 | @Volatile 20 | private lateinit var command: RedisCommand 21 | 22 | override fun handle(cmd: RequestCommand) { 23 | require(cmd.argCount == 4) { argValidationErrorMsg(cmd) } 24 | 25 | command = cmd.command 26 | val key = createKey(cmd.key) 27 | val operation = MapOperation.increment( 28 | MapPolicy(), aeroCtx.bin, 29 | getMapKey(cmd), getIncrValue(cmd) 30 | ) 31 | client.operate( 32 | null, this, defaultWritePolicy, 33 | key, operation, *systemOps() 34 | ) 35 | } 36 | 37 | private fun getMapKey(cmd: RequestCommand): Value { 38 | return when (cmd.command) { 39 | RedisCommand.ZINCRBY -> Typed.getValue(cmd.args[3]) 40 | else -> Typed.getValue(cmd.args[2]) 41 | } 42 | } 43 | 44 | private fun getIncrValue(cmd: RequestCommand): Value { 45 | return when (cmd.command) { 46 | RedisCommand.ZINCRBY -> Typed.getValue(cmd.args[2]) 47 | else -> Typed.getValue(cmd.args[3]) 48 | } 49 | } 50 | 51 | override fun onSuccess(key: Key?, record: Record?) { 52 | if (record == null) { 53 | writeErrorString("Failed to create a record") 54 | flushCtxTransactionAware() 55 | } else { 56 | try { 57 | when (command) { 58 | RedisCommand.ZINCRBY -> writeBulkString(record.getLong(aeroCtx.bin).toString()) 59 | else -> writeNumeric(record.getDouble(aeroCtx.bin)) 60 | } 61 | flushCtxTransactionAware() 62 | } catch (e: Exception) { 63 | closeCtx(e) 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/SstoreCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapPolicy 8 | import com.aerospike.client.listener.RecordArrayListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.listener.ValueType 12 | import com.aerospike.skyhook.util.IntersectMerge 13 | import com.aerospike.skyhook.util.Merge 14 | import com.aerospike.skyhook.util.Typed 15 | import com.aerospike.skyhook.util.UnionMerge 16 | import io.netty.channel.ChannelHandlerContext 17 | 18 | abstract class SstoreBaseCommandListener( 19 | ctx: ChannelHandlerContext 20 | ) : BaseListener(ctx), RecordArrayListener, Merge { 21 | 22 | @Volatile 23 | private lateinit var key: Key 24 | 25 | override fun handle(cmd: RequestCommand) { 26 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 27 | 28 | key = createKey(cmd.key) 29 | client.get( 30 | null, this, null, 31 | getKeys(cmd).toTypedArray() 32 | ) 33 | } 34 | 35 | private fun getKeys(cmd: RequestCommand): List { 36 | return cmd.args.drop(2) 37 | .map { createKey(it) } 38 | } 39 | 40 | override fun onSuccess(keys: Array?, records: Array?) { 41 | if (records == null) { 42 | writeLong(0L) 43 | } else { 44 | val values = merge(records.filterNotNull() 45 | .map { it.getMap(aeroCtx.bin) }.map { it.keys }) 46 | 47 | val operation = MapOperation.putItems( 48 | MapPolicy(), 49 | aeroCtx.bin, 50 | values.map { 51 | Typed.getValue(it.toString().toByteArray()) to Value.getAsNull() 52 | }.toMap() 53 | ) 54 | client.operate( 55 | defaultWritePolicy, key, *systemOps(ValueType.SET), operation 56 | ) 57 | writeLong(values.size) 58 | } 59 | flushCtxTransactionAware() 60 | } 61 | } 62 | 63 | class SinterstoreCommandListener( 64 | ctx: ChannelHandlerContext 65 | ) : SstoreBaseCommandListener(ctx), IntersectMerge 66 | 67 | class SunionstoreCommandListener( 68 | ctx: ChannelHandlerContext 69 | ) : SstoreBaseCommandListener(ctx), UnionMerge 70 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/HscanCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Operation 5 | import com.aerospike.client.Record 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapReturnType 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import io.netty.channel.ChannelHandlerContext 12 | 13 | open class HscanCommandListener( 14 | ctx: ChannelHandlerContext 15 | ) : BaseListener(ctx), RecordListener { 16 | 17 | @Volatile 18 | protected lateinit var scanCommand: ScanCommand 19 | 20 | override fun handle(cmd: RequestCommand) { 21 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 22 | 23 | val key = createKey(cmd.key) 24 | scanCommand = ScanCommand(cmd, 3) 25 | client.operate( 26 | null, this, defaultWritePolicy, 27 | key, getOperation() 28 | ) 29 | } 30 | 31 | protected open fun getOperation(): Operation { 32 | return MapOperation.getByIndexRange( 33 | aeroCtx.bin, 34 | scanCommand.cursor.toInt(), 35 | scanCommand.COUNT.toInt(), 36 | MapReturnType.KEY_VALUE 37 | ) 38 | } 39 | 40 | override fun onSuccess(key: Key?, record: Record?) { 41 | try { 42 | if (record == null) { 43 | writeArrayHeader(2) 44 | writeSimpleString(ScanCommand.zeroCursor) 45 | writeEmptyList() 46 | } else { 47 | val asList = record.bins[aeroCtx.bin] as List<*> 48 | writeArrayHeader(2) 49 | writeSimpleString(getNextCursor(asList.size)) 50 | writeElementsArray(asList) 51 | } 52 | flushCtxTransactionAware() 53 | } catch (e: Exception) { 54 | closeCtx(e) 55 | } 56 | } 57 | 58 | protected open fun writeElementsArray(list: List<*>) { 59 | writeObjectListStr(list 60 | .map { it as Map.Entry<*, *> } 61 | .map { it.toPair().toList() }.flatten() 62 | ) 63 | } 64 | 65 | private fun getNextCursor(responseSize: Int): String { 66 | return if (responseSize < scanCommand.COUNT) { 67 | ScanCommand.zeroCursor 68 | } else { 69 | (scanCommand.cursor.toInt() + responseSize).toString() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/SaddCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.* 4 | import com.aerospike.client.cdt.MapOperation 5 | import com.aerospike.client.cdt.MapOrder 6 | import com.aerospike.client.cdt.MapPolicy 7 | import com.aerospike.client.cdt.MapWriteFlags 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.listener.ValueType 12 | import com.aerospike.skyhook.util.Typed 13 | import io.netty.channel.ChannelHandlerContext 14 | 15 | open class SaddCommandListener( 16 | ctx: ChannelHandlerContext 17 | ) : BaseListener(ctx), RecordListener { 18 | 19 | @Volatile 20 | protected open var size: Long = 0L 21 | protected open val systemOps: Array = systemOps(ValueType.SET) 22 | protected open val mapPolicy = MapPolicy(MapOrder.UNORDERED, MapWriteFlags.CREATE_ONLY) 23 | 24 | override fun handle(cmd: RequestCommand) { 25 | validate(cmd) 26 | 27 | val key = createKey(cmd.key) 28 | setSize(key) 29 | 30 | val operation = MapOperation.putItems( 31 | mapPolicy, 32 | aeroCtx.bin, 33 | getValues(cmd) 34 | ) 35 | client.operate( 36 | null, this, defaultWritePolicy, 37 | key, *systemOps, operation 38 | ) 39 | } 40 | 41 | protected open fun validate(cmd: RequestCommand) { 42 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 43 | } 44 | 45 | protected open fun setSize(key: Key) { 46 | val getSize = MapOperation.size(aeroCtx.bin) 47 | size = client.operate(defaultWritePolicy, key, getSize) 48 | ?.getLong(aeroCtx.bin) ?: 0L 49 | } 50 | 51 | protected open fun getValues(cmd: RequestCommand): Map { 52 | return cmd.args.drop(2).associate { Typed.getValue(it) to Value.getAsNull() } 53 | } 54 | 55 | override fun writeError(e: AerospikeException?) { 56 | writeLong(0L) 57 | } 58 | 59 | override fun onSuccess(key: Key?, record: Record?) { 60 | if (record == null) { 61 | writeLong(0L) 62 | flushCtxTransactionAware() 63 | } else { 64 | try { 65 | val added = record.getLong(aeroCtx.bin) - size 66 | writeLong(added) 67 | flushCtxTransactionAware() 68 | } catch (e: Exception) { 69 | closeCtx(e) 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/MsetCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Bin 4 | import com.aerospike.client.Key 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.listener.WriteListener 7 | import com.aerospike.skyhook.command.RedisCommand 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.listener.BaseListener 10 | import com.aerospike.skyhook.listener.ValueType 11 | import com.aerospike.skyhook.util.Typed 12 | import io.netty.channel.ChannelHandlerContext 13 | 14 | class MsetCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), WriteListener { 17 | 18 | @Volatile 19 | private lateinit var command: RedisCommand 20 | 21 | @Volatile 22 | private var total: Int = 0 23 | 24 | private val lock = Any() 25 | 26 | /** 27 | * The commands implementation is not atomic. 28 | */ 29 | override fun handle(cmd: RequestCommand) { 30 | require(cmd.argCount >= 3 && cmd.argCount % 2 == 1) { 31 | argValidationErrorMsg(cmd) 32 | } 33 | 34 | command = cmd.command 35 | val values = getValues(cmd) 36 | if (!handleNX(cmd, values.keys.toTypedArray())) return 37 | 38 | total = values.size 39 | values.forEach { (k, v) -> 40 | client.put( 41 | null, this, defaultWritePolicy, k, 42 | Bin(aeroCtx.bin, v), *systemBins(ValueType.STRING) 43 | ) 44 | } 45 | } 46 | 47 | private fun handleNX(cmd: RequestCommand, keys: Array): Boolean { 48 | if (cmd.command == RedisCommand.MSETNX) { 49 | if (!client.exists(null, keys).all { !it }) { 50 | writeLong(0L) 51 | flushCtxTransactionAware() 52 | return false 53 | } 54 | } 55 | return true 56 | } 57 | 58 | private fun getValues(cmd: RequestCommand): Map { 59 | return cmd.args.drop(1).chunked(2) 60 | .associate { (it1, it2) -> createKey(it1) to Typed.getValue(it2) } 61 | } 62 | 63 | override fun onSuccess(key: Key?) { 64 | try { 65 | synchronized(lock) { 66 | total-- 67 | if (total == 0) { 68 | if (command == RedisCommand.MSETNX) { 69 | writeLong(1L) 70 | } else { 71 | writeOK() 72 | } 73 | flushCtxTransactionAware() 74 | } 75 | } 76 | } catch (e: Exception) { 77 | closeCtx(e) 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/config/ClientPolicyConfig.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.config 2 | 3 | import com.aerospike.client.policy.AuthMode 4 | 5 | /** 6 | * Aerospike Java Client [com.aerospike.client.policy.ClientPolicy] configuration properties. 7 | */ 8 | data class ClientPolicyConfig( 9 | 10 | /** 11 | * User authentication to cluster. 12 | * Leave null for clusters running without restricted access. 13 | */ 14 | val user: String? = null, 15 | 16 | /** 17 | * Password authentication to cluster. 18 | * The password will be stored by the client and sent to server in hashed format. 19 | * Leave null for clusters running without restricted access. 20 | */ 21 | val password: String? = null, 22 | 23 | /** 24 | * Expected cluster name. If not null, server nodes must return this cluster name in order to join 25 | * the client's view of the cluster. Should only be set when connecting to servers that support the 26 | * "cluster-name" info command. 27 | */ 28 | val clusterName: String? = null, 29 | 30 | /** 31 | * Authentication mode used when user/password is defined. 32 | */ 33 | val authMode: AuthMode? = null, 34 | 35 | /** 36 | * Initial host connection timeout in milliseconds. 37 | * The timeout when opening a connection to the server host for the first time. 38 | */ 39 | val timeout: Int? = null, 40 | 41 | /** 42 | * Login timeout in milliseconds. The timeout is used when user authentication is enabled 43 | * and a node login is being performed. 44 | */ 45 | val loginTimeout: Int? = null, 46 | 47 | /** 48 | * Minimum number of asynchronous connections allowed per server node. 49 | * Preallocate min connections on client node creation. The client will periodically allocate new connections 50 | * if count falls below min connections. 51 | */ 52 | val asyncMinConnsPerNode: Int? = null, 53 | 54 | /** 55 | * Maximum number of asynchronous connections allowed per server node. 56 | * Transactions will go through retry logic and potentially fail with "ResultCode.NO_MORE_CONNECTIONS" 57 | * if the maximum number of connections would be exceeded. 58 | */ 59 | val asyncMaxConnsPerNode: Int? = null, 60 | 61 | /** 62 | * Throw exception if all seed connections fail on cluster instantiation. 63 | */ 64 | val failIfNotConnected: Boolean? = null, 65 | 66 | /** 67 | * Should use "services-alternate" instead of "services" in info request during cluster tending. 68 | * "services-alternate" returns server configured external IP addresses that client uses to talk to nodes. 69 | */ 70 | val useServicesAlternate: Boolean? = null, 71 | ) 72 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/HsetCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.* 4 | import com.aerospike.client.cdt.MapOperation 5 | import com.aerospike.client.cdt.MapOrder 6 | import com.aerospike.client.cdt.MapPolicy 7 | import com.aerospike.client.cdt.MapWriteFlags 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.listener.ValueType 12 | import com.aerospike.skyhook.util.Typed 13 | import io.netty.channel.ChannelHandlerContext 14 | 15 | class HsetnxCommandListener( 16 | ctx: ChannelHandlerContext 17 | ) : BaseListener(ctx), RecordListener { 18 | 19 | override fun handle(cmd: RequestCommand) { 20 | require(cmd.argCount == 4) { argValidationErrorMsg(cmd) } 21 | 22 | val key = createKey(cmd.key) 23 | val operation = MapOperation.put( 24 | MapPolicy(MapOrder.UNORDERED, MapWriteFlags.CREATE_ONLY), 25 | aeroCtx.bin, 26 | Typed.getValue(cmd.args[2]), 27 | Typed.getValue(cmd.args[3]) 28 | ) 29 | client.operate( 30 | null, this, defaultWritePolicy, 31 | key, *systemOps(ValueType.HASH), operation 32 | ) 33 | } 34 | 35 | override fun writeError(e: AerospikeException?) { 36 | writeLong(0L) 37 | } 38 | 39 | override fun onSuccess(key: Key?, record: Record?) { 40 | if (record == null) { 41 | writeNullString() 42 | flushCtxTransactionAware() 43 | } else { 44 | try { 45 | writeLong(1L) 46 | flushCtxTransactionAware() 47 | } catch (e: Exception) { 48 | closeCtx(e) 49 | } 50 | } 51 | } 52 | } 53 | 54 | open class HsetCommandListener( 55 | ctx: ChannelHandlerContext 56 | ) : SaddCommandListener(ctx) { 57 | 58 | override val systemOps: Array = systemOps(ValueType.HASH) 59 | override val mapPolicy = MapPolicy() 60 | 61 | override fun validate(cmd: RequestCommand) { 62 | require(cmd.argCount >= 4 && cmd.argCount % 2 == 0) { 63 | argValidationErrorMsg(cmd) 64 | } 65 | } 66 | 67 | override fun getValues(cmd: RequestCommand): Map { 68 | return cmd.args.drop(2).chunked(2) 69 | .map { (it1, it2) -> Typed.getValue(it1) to Typed.getValue(it2) } 70 | .toMap() 71 | } 72 | } 73 | 74 | class HmsetCommandListener( 75 | ctx: ChannelHandlerContext 76 | ) : HsetCommandListener(ctx) { 77 | 78 | override fun setSize(key: Key) {} 79 | 80 | override fun onSuccess(key: Key?, record: Record?) { 81 | if (record == null) { 82 | writeNullString() 83 | flushCtxTransactionAware() 84 | } else { 85 | try { 86 | writeOK() 87 | flushCtxTransactionAware() 88 | } catch (e: Exception) { 89 | closeCtx(e) 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/list/ListPushCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.list 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Operation 5 | import com.aerospike.client.Record 6 | import com.aerospike.client.Value 7 | import com.aerospike.client.cdt.ListOperation 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.client.policy.WritePolicy 10 | import com.aerospike.skyhook.command.RedisCommand 11 | import com.aerospike.skyhook.command.RequestCommand 12 | import com.aerospike.skyhook.listener.BaseListener 13 | import com.aerospike.skyhook.listener.ValueType 14 | import com.aerospike.skyhook.util.Typed 15 | import io.netty.channel.ChannelHandlerContext 16 | 17 | class ListPushCommandListener( 18 | ctx: ChannelHandlerContext 19 | ) : BaseListener(ctx), RecordListener { 20 | 21 | private data class OpWritePolicy(val writePolicy: WritePolicy, val op: Operation) 22 | 23 | override fun handle(cmd: RequestCommand) { 24 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 25 | 26 | val key = createKey(cmd.key) 27 | val opPolicy = getOpWritePolicy(cmd) 28 | client.operate( 29 | null, this, opPolicy.writePolicy, 30 | key, *systemOps(ValueType.LIST), opPolicy.op 31 | ) 32 | } 33 | 34 | private fun getOpWritePolicy(cmd: RequestCommand): OpWritePolicy { 35 | val values = getValues(cmd) 36 | return when (cmd.command) { 37 | RedisCommand.LPUSH -> { 38 | OpWritePolicy( 39 | defaultWritePolicy, 40 | ListOperation.insertItems(aeroCtx.bin, 0, values) 41 | ) 42 | } 43 | RedisCommand.LPUSHX -> { 44 | OpWritePolicy( 45 | updateOnlyPolicy, 46 | ListOperation.insertItems(aeroCtx.bin, 0, values) 47 | ) 48 | } 49 | RedisCommand.RPUSH -> { 50 | OpWritePolicy( 51 | defaultWritePolicy, 52 | ListOperation.appendItems(aeroCtx.bin, values) 53 | ) 54 | } 55 | RedisCommand.RPUSHX -> { 56 | OpWritePolicy( 57 | updateOnlyPolicy, 58 | ListOperation.appendItems(aeroCtx.bin, values) 59 | ) 60 | } 61 | else -> { 62 | throw IllegalArgumentException(cmd.command.toString()) 63 | } 64 | } 65 | } 66 | 67 | private fun getValues(cmd: RequestCommand): List { 68 | return cmd.args.drop(2).map { 69 | Typed.getValue(it) 70 | } 71 | } 72 | 73 | override fun onSuccess(key: Key?, record: Record?) { 74 | if (record == null) { 75 | writeNullArray() 76 | flushCtxTransactionAware() 77 | } else { 78 | try { 79 | writeResponse(record.bins[aeroCtx.bin]) 80 | flushCtxTransactionAware() 81 | } catch (e: Exception) { 82 | closeCtx(e) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/ListCommandsTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | 8 | class ListCommandsTest() : SkyhookIntegrationTestBase() { 9 | 10 | private val _key = "list" 11 | 12 | private fun setup(n: Int = 3) { 13 | for (i in 1..n) { 14 | writeCommand("${RedisCommand.RPUSH.name} $_key val$i") 15 | assertEquals(i.toLong(), readLong()) 16 | } 17 | } 18 | 19 | @Test 20 | fun testLpush() { 21 | writeCommand("${RedisCommand.LPUSH.name} $_key val1") 22 | assertEquals(1, readLong()) 23 | writeCommand("${RedisCommand.LPUSHX.name} $_key val2") 24 | assertEquals(2, readLong()) 25 | writeCommand("${RedisCommand.LPUSHX.name} list2 val1") 26 | assertTrue { readError().isNotEmpty() } 27 | writeCommand("${RedisCommand.LINDEX.name} $_key 0") 28 | assertEquals("val2", readFullBulkString()) 29 | } 30 | 31 | @Test 32 | fun testRpush() { 33 | writeCommand("${RedisCommand.RPUSH.name} $_key val1") 34 | assertEquals(1, readLong()) 35 | writeCommand("${RedisCommand.RPUSHX.name} $_key val2") 36 | assertEquals(2, readLong()) 37 | writeCommand("${RedisCommand.RPUSHX.name} list2 val1") 38 | assertTrue { readError().isNotEmpty() } 39 | writeCommand("${RedisCommand.LINDEX.name} $_key 0") 40 | assertEquals("val1", readFullBulkString()) 41 | } 42 | 43 | @Test 44 | fun testLpop() { 45 | setup() 46 | writeCommand("${RedisCommand.LPOP.name} $_key") 47 | val lpopRes = readStringArray() 48 | assertEquals("val1", lpopRes[0]) 49 | writeCommand("${RedisCommand.LPOP.name} $_key 2") 50 | val lpopRes2 = readStringArray() 51 | assertEquals("val2", lpopRes2[0]) 52 | assertEquals("val3", lpopRes2[1]) 53 | } 54 | 55 | @Test 56 | fun testRpop() { 57 | setup() 58 | writeCommand("${RedisCommand.RPOP.name} $_key") 59 | val rpopRes = readStringArray() 60 | assertEquals("val3", rpopRes[0]) 61 | writeCommand("${RedisCommand.RPOP.name} $_key 2") 62 | val rpopRes2 = readStringArray() 63 | assertEquals("val1", rpopRes2[0]) 64 | assertEquals("val2", rpopRes2[1]) 65 | } 66 | 67 | @Test 68 | fun testLlen() { 69 | setup() 70 | writeCommand("${RedisCommand.LLEN.name} $_key") 71 | assertEquals(3, readLong()) 72 | } 73 | 74 | @Test 75 | fun testLindex() { 76 | setup() 77 | writeCommand("${RedisCommand.LINDEX.name} $_key 0") 78 | assertEquals("val1", readFullBulkString()) 79 | writeCommand("${RedisCommand.LINDEX.name} $_key 2") 80 | assertEquals("val3", readFullBulkString()) 81 | } 82 | 83 | @Test 84 | fun testLrange() { 85 | setup() 86 | writeCommand("${RedisCommand.LRANGE.name} $_key 0 3") 87 | val r = readStringArray() 88 | assertEquals("val1", r[0]) 89 | assertEquals("val2", r[1]) 90 | assertEquals("val3", r[2]) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/UnaryCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Bin 4 | import com.aerospike.client.Key 5 | import com.aerospike.client.Operation 6 | import com.aerospike.client.Record 7 | import com.aerospike.client.listener.RecordListener 8 | import com.aerospike.skyhook.command.RedisCommand 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.listener.ValueType 12 | import com.aerospike.skyhook.util.Typed 13 | import io.netty.channel.ChannelHandlerContext 14 | 15 | abstract class UnaryCommandListener( 16 | ctx: ChannelHandlerContext 17 | ) : BaseListener(ctx), RecordListener { 18 | 19 | @Volatile 20 | private lateinit var command: RedisCommand 21 | 22 | override fun handle(cmd: RequestCommand) { 23 | require(cmd.argCount == 2 || cmd.argCount == 3) { 24 | argValidationErrorMsg(cmd) 25 | } 26 | 27 | command = cmd.command 28 | val key = createKey(cmd.key) 29 | val ops = arrayOf( 30 | *systemOps(ValueType.STRING), 31 | getUnaryOperation(cmd), 32 | Operation.get(aeroCtx.bin) 33 | ) 34 | client.operate( 35 | null, this, 36 | defaultWritePolicy, key, *ops 37 | ) 38 | } 39 | 40 | override fun onSuccess(key: Key?, record: Record?) { 41 | if (record == null) { 42 | writeNullString() 43 | flushCtxTransactionAware() 44 | } else { 45 | try { 46 | writeNumeric( 47 | record.getDouble(aeroCtx.bin), 48 | command == RedisCommand.INCRBYFLOAT 49 | ) 50 | flushCtxTransactionAware() 51 | } catch (e: Exception) { 52 | closeCtx(e) 53 | } 54 | } 55 | } 56 | 57 | protected abstract fun getUnaryOperation(cmd: RequestCommand): Operation 58 | } 59 | 60 | class IncrCommandListener( 61 | ctx: ChannelHandlerContext 62 | ) : UnaryCommandListener(ctx) { 63 | 64 | override fun getUnaryOperation(cmd: RequestCommand): Operation { 65 | return when (cmd.command) { 66 | RedisCommand.INCR -> { 67 | Operation.add(Bin(aeroCtx.bin, 1.0)) 68 | } 69 | RedisCommand.INCRBY, RedisCommand.INCRBYFLOAT -> { 70 | Operation.add(Bin(aeroCtx.bin, Typed.getDouble(cmd.args[2]))) 71 | } 72 | else -> { 73 | throw IllegalArgumentException(cmd.command.toString()) 74 | } 75 | } 76 | } 77 | } 78 | 79 | class DecrCommandListener( 80 | ctx: ChannelHandlerContext 81 | ) : UnaryCommandListener(ctx) { 82 | 83 | override fun getUnaryOperation(cmd: RequestCommand): Operation { 84 | return when (cmd.command) { 85 | RedisCommand.DECR -> { 86 | Operation.add(Bin(aeroCtx.bin, -1.0)) 87 | } 88 | RedisCommand.DECRBY -> { 89 | Operation.add(Bin(aeroCtx.bin, -Typed.getDouble(cmd.args[2]))) 90 | } 91 | else -> { 92 | throw IllegalArgumentException(cmd.command.toString()) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/Main.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.config.ServerConfiguration 4 | import com.fasterxml.jackson.core.JsonGenerator 5 | import com.fasterxml.jackson.databind.DeserializationFeature 6 | import com.fasterxml.jackson.databind.ObjectMapper 7 | import com.fasterxml.jackson.databind.SerializationFeature 8 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory 9 | import com.google.inject.Guice 10 | import mu.KotlinLogging 11 | import org.apache.commons.io.FileUtils 12 | import picocli.CommandLine 13 | import java.io.File 14 | import java.nio.charset.Charset 15 | import java.util.concurrent.Callable 16 | import kotlin.system.exitProcess 17 | 18 | /** 19 | * Main entry point that sets up and starts the server. 20 | */ 21 | @CommandLine.Command( 22 | name = "skyhook", 23 | description = ["Redis to Aerospike proxy server"] 24 | ) 25 | class App : Callable { 26 | 27 | companion object { 28 | internal val log = KotlinLogging.logger {} 29 | } 30 | 31 | @CommandLine.Option( 32 | names = ["-f", "--config-file"], 33 | description = ["yaml formatted configuration file"] 34 | ) 35 | private var configFile: File? = null 36 | 37 | @CommandLine.Option( 38 | names = ["-h", "--help"], usageHelp = true, 39 | description = ["display this help and exit"] 40 | ) 41 | private var help: Boolean = false 42 | 43 | override fun call() { 44 | val config = if (configFile != null) { 45 | // Parse the configuration. 46 | val configYaml = FileUtils.readFileToString( 47 | configFile, 48 | Charset.defaultCharset() 49 | ) 50 | 51 | val yamlParser = getYamlParser() 52 | yamlParser.readValue(configYaml, ServerConfiguration::class.java) 53 | } else { 54 | ServerConfiguration() 55 | } 56 | 57 | val injector = Guice.createInjector(SkyhookModule(config)) 58 | val server = injector.getInstance(SkyhookServer::class.java) 59 | 60 | // Add a shutdown hook. 61 | Runtime.getRuntime().addShutdownHook(Thread { server.stop() }) 62 | 63 | // Start the server. 64 | server.start() 65 | } 66 | 67 | private fun getYamlParser(): ObjectMapper { 68 | val mapper = ObjectMapper(YAMLFactory()) 69 | 70 | // Setup deserializer options. 71 | mapper.factory.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET) 72 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 73 | mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) 74 | mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) 75 | 76 | mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) 77 | mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) 78 | mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) 79 | mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) 80 | return mapper 81 | } 82 | } 83 | 84 | fun main(args: Array) { 85 | try { 86 | CommandLine(App()).execute(*args) 87 | } catch (e: Exception) { 88 | App.log.error(e) { 89 | "Server stopped unexpectedly" 90 | } 91 | 92 | exitProcess(1) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/ZpopCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.cdt.MapOperation 7 | import com.aerospike.client.cdt.MapReturnType 8 | import com.aerospike.client.listener.RecordListener 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import io.netty.channel.ChannelHandlerContext 12 | import java.util.* 13 | 14 | abstract class ZpopCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), RecordListener { 17 | 18 | protected var count: Int = 0 19 | 20 | override fun handle(cmd: RequestCommand) { 21 | require(cmd.argCount == 2 || cmd.argCount == 3) { 22 | argValidationErrorMsg(cmd) 23 | } 24 | 25 | val key = createKey(cmd.key) 26 | setCount(cmd) 27 | val record = client.get(defaultWritePolicy, key).bins[aeroCtx.bin] 28 | 29 | client.operate( 30 | null, this, defaultWritePolicy, key, 31 | MapOperation.removeByKeyList(aeroCtx.bin, getKeysToPop(record), MapReturnType.KEY_VALUE), 32 | *systemOps() 33 | ) 34 | } 35 | 36 | private fun setCount(cmd: RequestCommand) { 37 | count = if (cmd.argCount == 3) String(cmd.args[2]).toInt() else 1 38 | } 39 | 40 | @Suppress("UNCHECKED_CAST") 41 | private fun getKeysToPop(data: Any?): List { 42 | val sorted = (data as TreeMap<*, Long>).toList() 43 | .sortedBy { it.second }.map { it.first } 44 | return take(sorted).map { Value.get(it) } 45 | } 46 | 47 | protected abstract fun take(sorted: List): List 48 | 49 | override fun onSuccess(key: Key?, record: Record?) { 50 | if (record == null) { 51 | writeEmptyList() 52 | flushCtxTransactionAware() 53 | } else { 54 | try { 55 | writeResponse(marshalOutput(record.bins[aeroCtx.bin])) 56 | flushCtxTransactionAware() 57 | } catch (e: Exception) { 58 | closeCtx(e) 59 | } 60 | } 61 | } 62 | 63 | @Suppress("UNCHECKED_CAST") 64 | private fun marshalOutput(data: Any?): List { 65 | return sortOutput(data as List>) 66 | .map { it.toPair().toList() }.flatten().map { it.toString() } 67 | } 68 | 69 | protected abstract fun sortOutput(list: List>): List> 70 | } 71 | 72 | class ZpopmaxCommandListener( 73 | ctx: ChannelHandlerContext 74 | ) : ZpopCommandListener(ctx) { 75 | 76 | override fun take(sorted: List): List { 77 | return sorted.takeLast(count) 78 | } 79 | 80 | override fun sortOutput(list: List>): List> { 81 | return list.sortedBy { it.value }.reversed() 82 | } 83 | } 84 | 85 | class ZpopminCommandListener( 86 | ctx: ChannelHandlerContext 87 | ) : ZpopCommandListener(ctx) { 88 | 89 | override fun take(sorted: List): List { 90 | return sorted.take(count) 91 | } 92 | 93 | override fun sortOutput(list: List>): List> { 94 | return list.sortedBy { it.value } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | const lightCodeTheme = require('prism-react-renderer/themes/github'); 2 | const darkCodeTheme = require('prism-react-renderer/themes/dracula'); 3 | 4 | /** @type {import('@docusaurus/types').DocusaurusConfig} */ 5 | module.exports = { 6 | title: 'Skyhook', 7 | tagline: 'Skyhook is a Redis API compatible gateway to Aerospike Database', 8 | url: 'https://aerospike.github.io/', 9 | baseUrl: '/skyhook/', 10 | onBrokenLinks: 'throw', 11 | onBrokenMarkdownLinks: 'throw', 12 | // Recommendation is to have this true but build fails if it is 13 | // trailingSlash: true, 14 | favicon: 'img/favicon.ico', 15 | organizationName: 'aerospike', // Usually your GitHub org/user name. 16 | projectName: 'skyhook', // Usually your repo name. 17 | themeConfig: { 18 | colorMode: { 19 | defaultMode: 'light', 20 | disableSwitch: true, 21 | }, 22 | navbar: { 23 | title: 'Skyhook', 24 | logo: { 25 | alt: 'Skyhook', 26 | src: 'img/logo.png', 27 | }, 28 | items: [ 29 | // { 30 | // type: 'doc', 31 | // docId: 'intro', 32 | // position: 'left', 33 | // label: 'Docs', 34 | // }, 35 | { to: '/blog', label: 'Blog', position: 'left' }, 36 | { type: 'docsVersionDropdown', position: 'right'}, 37 | { 38 | href: 'https://github.com/aerospike/skyhook', 39 | label: 'GitHub', 40 | position: 'right', 41 | }, 42 | ], 43 | }, 44 | footer: { 45 | style: 'dark', 46 | links: [ 47 | // { 48 | // title: 'Docs', 49 | // items: [ 50 | // { 51 | // label: 'Introduction', 52 | // to: '/docs/intro', 53 | // }, 54 | // ], 55 | // }, 56 | { 57 | title: 'Community', 58 | items: [ 59 | { 60 | label: 'Stack Overflow', 61 | href: 'https://stackoverflow.com/questions/tagged/aerospike', 62 | }, 63 | { 64 | label: 'Twitter', 65 | href: 'https://twitter.com/aerospikedb', 66 | }, 67 | ], 68 | }, 69 | { 70 | title: 'More', 71 | items: [ 72 | { 73 | label: 'Blog', 74 | to: '/blog', 75 | }, 76 | { 77 | label: 'GitHub', 78 | href: 'https://github.com/aerospike/skyhook', 79 | }, 80 | ], 81 | }, 82 | ], 83 | copyright: `Copyright © ${new Date().getFullYear()} Aerospike`, 84 | }, 85 | prism: { 86 | theme: darkCodeTheme, 87 | darkTheme: darkCodeTheme, 88 | }, 89 | }, 90 | presets: [ 91 | [ 92 | '@docusaurus/preset-classic', 93 | { 94 | docs: { 95 | sidebarPath: require.resolve('./sidebars.js'), 96 | routeBasePath: '/', 97 | // Please change this to your repo. 98 | editUrl: 'https://github.com/aerospike/skyhook/edit/main/website/', 99 | }, 100 | blog: { 101 | showReadingTime: true, 102 | blogSidebarCount: 0, 103 | // Please change this to your repo. 104 | editUrl: 'https://github.com/aerospike/skyhook/edit/main/website/blog/', 105 | }, 106 | theme: { 107 | customCss: require.resolve('./src/css/custom.css'), 108 | }, 109 | }, 110 | ], 111 | ], 112 | }; 113 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --blue: #3b69e5; 10 | --indigo: #6610f2; 11 | --purple: #6f42c1; 12 | --pink: #e83e8c; 13 | --red: #c22125; 14 | --orange: #fd7e14; 15 | --yellow: #ffc107; 16 | --green: #28a745; 17 | --teal: #20c997; 18 | --cyan: #17a2b8; 19 | --white: #fff; 20 | --gray: #6c757d; 21 | --gray-dark: #343a40; 22 | --primary: #3b69e5; 23 | --secondary: #6c757d; 24 | --success: #28a745; 25 | --info: #17a2b8; 26 | --warning: #ffc107; 27 | --danger: #c22125; 28 | --light: #f8f9fa; 29 | --dark: #0d0b1e; 30 | --ifm-color-primary: #7ec0f9; 31 | --ifm-color-primary-dark: #5aaff7; 32 | --ifm-color-primary-darker: #48a6f6; 33 | --ifm-color-primary-darkest: #128cf4; 34 | --ifm-color-primary-light: #a2d2fb; 35 | --ifm-color-primary-lighter: #b4dafc; 36 | --ifm-color-primary-lightest: #eaf5fe; 37 | --ifm-code-font-size: 95%; 38 | --font-family-sans-serif: 'Lato', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 39 | 'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans', sans-serif, 'Apple Color Emoji', 40 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 41 | --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', 42 | monospace; 43 | } 44 | 45 | /* html[data-theme='dark'] { 46 | --ifm-color-primary: #4e89e8; 47 | --ifm-color-primary-dark: #5a91ea; 48 | --ifm-hero-text-color: white; 49 | --ifm-hero-subtitle: white; 50 | --ifm-heading-color: white; 51 | --ifm-hero-text-color: white; 52 | --ifm-font-color: black; 53 | --ifm-font-color-inverse: white; 54 | } 55 | */ 56 | html { 57 | font-family: sans-serif; 58 | } 59 | 60 | h1, 61 | h2, 62 | h3, 63 | h4 { 64 | font-weight: lighter; 65 | } 66 | h1 { 67 | font-size: 3rem; 68 | } 69 | h2 { 70 | font-size: 2rem; 71 | } 72 | h3 { 73 | font-size: 1.5rem; 74 | } 75 | h4 { 76 | font-size: 1.25rem; 77 | } 78 | 79 | .docusaurus-highlight-code-line { 80 | background-color: rgb(72, 77, 91); 81 | display: block; 82 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 83 | padding: 0 var(--ifm-pre-padding); 84 | } 85 | .navbar__title { 86 | font-size: x-large; 87 | font-weight: lighter; 88 | padding-left: 10px; 89 | } 90 | .navbar__logo { 91 | height: 25px; 92 | } 93 | .navbar__link svg { 94 | display: none; 95 | } 96 | .navbar-sidebar__items svg { 97 | display: none; 98 | } 99 | .footer__link-item svg { 100 | display: none; 101 | } 102 | .footer__col { 103 | padding-left: 12% !important; 104 | } 105 | .container { 106 | margin: auto; 107 | padding: 0; 108 | } 109 | .row { 110 | margin: 0; 111 | } 112 | .dataframe tbody tr th:only-of-type { 113 | vertical-align: middle; 114 | } 115 | .dataframe tbody tr th { 116 | vertical-align: top; 117 | } 118 | .dataframe thead th { 119 | text-align: right; 120 | } 121 | .hiddenTabs { 122 | display: none; 123 | } 124 | #searchScript { 125 | width: 95%; 126 | margin: auto; 127 | padding-bottom: 40px; 128 | } 129 | form.menu__list-item { 130 | margin-left: 15px; 131 | } 132 | @media screen and (max-width: 555px) { 133 | .navbar__title { 134 | display: none; 135 | } 136 | .logoPics { 137 | width: 100%; 138 | margin-bottom: 50px; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/ScanCommandsTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertContains 6 | import kotlin.test.assertEquals 7 | 8 | class ScanCommandsTest() : SkyhookIntegrationTestBase() { 9 | 10 | @Test 11 | fun testScan() { 12 | writeCommand("${RedisCommand.MSET.name} k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k11 v11 k8 v8") 13 | assertEquals(ok, readString()) 14 | writeCommand("${RedisCommand.SADD.name} set a b c") 15 | assertEquals(3L, readLong()) 16 | 17 | writeCommand("${RedisCommand.SCAN.name} 0 COUNT 20 TYPE string MATCH k1*") 18 | Thread.sleep(5000) 19 | val resp = readScanResponse() 20 | assertEquals("0", resp.cursor) 21 | assertEquals(2, resp.elements.size) 22 | assertEquals("k11", resp.elements[0]) 23 | assertEquals("k1", resp.elements[1]) 24 | } 25 | 26 | @Test 27 | fun testHscan() { 28 | writeCommand("${RedisCommand.HSET.name} hash k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k8 v8 k9 v9") 29 | assertEquals(9L, readLong()) 30 | 31 | writeCommand("${RedisCommand.HSCAN.name} hash 0 COUNT 5") 32 | var resp = readScanResponse() 33 | assertEquals("5", resp.cursor) 34 | assertEquals(10, resp.elements.size) 35 | 36 | writeCommand("${RedisCommand.HSCAN.name} hash ${resp.cursor} COUNT 5") 37 | resp = readScanResponse() 38 | assertEquals("0", resp.cursor) 39 | assertEquals(8, resp.elements.size) 40 | } 41 | 42 | @Test 43 | fun testSscan() { 44 | writeCommand("${RedisCommand.SADD.name} set a b c d e f g h i") 45 | assertEquals(9L, readLong()) 46 | 47 | writeCommand("${RedisCommand.SSCAN.name} set 0 COUNT 5") 48 | var resp = readScanResponse() 49 | assertEquals("5", resp.cursor) 50 | assertEquals(5, resp.elements.size) 51 | 52 | writeCommand("${RedisCommand.SSCAN.name} set ${resp.cursor} COUNT 5") 53 | resp = readScanResponse() 54 | assertEquals("0", resp.cursor) 55 | assertEquals(4, resp.elements.size) 56 | } 57 | 58 | @Test 59 | fun testZscan() { 60 | writeCommand("${RedisCommand.ZADD.name} zset 0 a 2 b 1 c 4 d 3 e 6 f 5 g 8 h 7 i") 61 | assertEquals(9L, readLong()) 62 | 63 | writeCommand("${RedisCommand.ZSCAN.name} zset 0 COUNT 5") 64 | var resp = readScanResponse() 65 | assertEquals("5", resp.cursor) 66 | assertEquals(5, resp.elements.size) 67 | assertEquals("a", resp.elements[0]) 68 | assertEquals("c", resp.elements[1]) 69 | assertEquals("b", resp.elements[2]) 70 | assertEquals("e", resp.elements[3]) 71 | assertEquals("d", resp.elements[4]) 72 | 73 | writeCommand("${RedisCommand.ZSCAN.name} zset ${resp.cursor} COUNT 5") 74 | resp = readScanResponse() 75 | assertEquals("0", resp.cursor) 76 | assertEquals(4, resp.elements.size) 77 | assertEquals("g", resp.elements[0]) 78 | assertEquals("f", resp.elements[1]) 79 | assertEquals("i", resp.elements[2]) 80 | assertEquals("h", resp.elements[3]) 81 | } 82 | 83 | @Test 84 | fun testKeys() { 85 | writeCommand("${RedisCommand.MSET.name} k1 v1 k2 v2 k3 v3 k4 v4 k5 v5 k6 v6 k7 v7 k11 v11 k8 v8") 86 | assertEquals(ok, readString()) 87 | 88 | writeCommand("${RedisCommand.KEYS.name} k1*") 89 | Thread.sleep(5000) 90 | val r = readStringArray() 91 | assertContains(r, "k11") 92 | assertContains(r, "k1") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/ExpireCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.AerospikeException 4 | import com.aerospike.client.Key 5 | import com.aerospike.client.listener.DeleteListener 6 | import com.aerospike.client.listener.WriteListener 7 | import com.aerospike.client.policy.WritePolicy 8 | import com.aerospike.skyhook.command.RedisCommand 9 | import com.aerospike.skyhook.command.RequestCommand 10 | import com.aerospike.skyhook.listener.BaseListener 11 | import com.aerospike.skyhook.util.Typed 12 | import io.netty.channel.ChannelHandlerContext 13 | 14 | class ExpireCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), WriteListener, DeleteListener { 17 | 18 | private var del: Boolean = false 19 | 20 | override fun handle(cmd: RequestCommand) { 21 | val key = createKey(cmd.key) 22 | val writePolicy = getPolicy(cmd) 23 | if (del) { 24 | client.delete(null, this, writePolicy, key) 25 | } else { 26 | client.touch(null, this, writePolicy, key) 27 | } 28 | } 29 | 30 | override fun onSuccess(key: Key?) { 31 | try { 32 | writeLong(1L) 33 | flushCtxTransactionAware() 34 | } catch (e: Exception) { 35 | closeCtx(e) 36 | } 37 | } 38 | 39 | override fun onSuccess(key: Key?, existed: Boolean) { 40 | try { 41 | val returnValue = if (existed) 1L else 0L 42 | writeLong(returnValue) 43 | flushCtxTransactionAware() 44 | } catch (e: Exception) { 45 | closeCtx(e) 46 | } 47 | } 48 | 49 | override fun writeError(e: AerospikeException?) { 50 | writeLong(0L) 51 | } 52 | 53 | private fun getPolicy(cmd: RequestCommand): WritePolicy { 54 | val writePolicy = getWritePolicy() 55 | when (cmd.command) { 56 | RedisCommand.EXPIRE -> { 57 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 58 | 59 | writePolicy.expiration = fromMillis(Typed.getLong(cmd.args[2]) * 1000) 60 | } 61 | RedisCommand.PEXPIRE -> { 62 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 63 | 64 | writePolicy.expiration = fromMillis(Typed.getLong(cmd.args[2])) 65 | } 66 | RedisCommand.EXPIREAT -> { 67 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 68 | 69 | writePolicy.expiration = fromTimestamp(Typed.getLong(cmd.args[2]) * 1000) 70 | } 71 | RedisCommand.PEXPIREAT -> { 72 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 73 | 74 | writePolicy.expiration = fromTimestamp(Typed.getLong(cmd.args[2])) 75 | } 76 | RedisCommand.PERSIST -> { 77 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 78 | 79 | writePolicy.expiration = -1 80 | } 81 | else -> { 82 | throw IllegalArgumentException(cmd.command.toString()) 83 | } 84 | } 85 | return writePolicy 86 | } 87 | 88 | private fun fromTimestamp(ts: Long): Int { 89 | return fromMillis(ts - System.currentTimeMillis()) 90 | } 91 | 92 | private fun fromMillis(millis: Long): Int { 93 | val expireSeconds = millis / 1000 94 | if (expireSeconds < 1) { 95 | del = true 96 | return 0 97 | } 98 | return expireSeconds.toInt() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/aerospike/TransactionCommandsHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler.aerospike 2 | 3 | import com.aerospike.client.AerospikeException 4 | import com.aerospike.client.Bin 5 | import com.aerospike.client.Value 6 | import com.aerospike.client.exp.Exp 7 | import com.aerospike.client.policy.WritePolicy 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.handler.CommandHandler 10 | import com.aerospike.skyhook.handler.NettyResponseWriter 11 | import com.aerospike.skyhook.listener.BaseListener 12 | import com.aerospike.skyhook.listener.BaseListener.Companion.argValidationErrorMsg 13 | import com.aerospike.skyhook.pipeline.AerospikeChannelInitializer.Companion.transactionAttrKey 14 | import com.aerospike.skyhook.util.wait 15 | import io.netty.channel.ChannelHandlerContext 16 | 17 | class MultiCommandHandler( 18 | ctx: ChannelHandlerContext, 19 | ) : NettyResponseWriter(ctx), CommandHandler { 20 | 21 | override fun handle(cmd: RequestCommand) { 22 | require(cmd.argCount == 1) { argValidationErrorMsg(cmd) } 23 | 24 | ctx.channel().attr(transactionAttrKey).get().startTransaction() 25 | writeOK() 26 | flushCtx() 27 | } 28 | } 29 | 30 | class DiscardCommandHandler( 31 | ctx: ChannelHandlerContext, 32 | ) : NettyResponseWriter(ctx), CommandHandler { 33 | 34 | override fun handle(cmd: RequestCommand) { 35 | require(cmd.argCount == 1) { argValidationErrorMsg(cmd) } 36 | 37 | ctx.channel().attr(transactionAttrKey).get().clear() 38 | writeOK() 39 | flushCtx() 40 | } 41 | } 42 | 43 | class ExecCommandHandler( 44 | ctx: ChannelHandlerContext, 45 | ) : BaseListener(ctx), CommandHandler { 46 | 47 | override fun handle(cmd: RequestCommand) { 48 | require(cmd.argCount == 1) { argValidationErrorMsg(cmd) } 49 | 50 | if (transactionState.inTransaction) { 51 | if (transactionState.commands.isEmpty()) { 52 | writeEmptyList() 53 | } else { 54 | try { 55 | writeArrayHeader(transactionState.commands.size.toLong()) 56 | for (c in transactionState.commands) { 57 | transactionState.pool.submit { c.command.newHandler(ctx).handle(c) } 58 | synchronized(ctx) { ctx.wait(5000) } 59 | } 60 | } catch (e: Exception) { 61 | writeErrorString("ERR Transaction failed") 62 | } finally { 63 | val writePolicy = transactionClearPolicy(transactionState.transactionId) 64 | val tidBin = Bin(aeroCtx.transactionIdBin, Value.NULL) 65 | for (key in transactionState.keys) { 66 | try { 67 | client.put(writePolicy, key, tidBin) 68 | } catch (e: AerospikeException) { 69 | log.warn { "Exception on clear the transaction id ${transactionState.transactionId}" } 70 | } 71 | } 72 | } 73 | transactionState.clear() 74 | } 75 | } else { 76 | writeErrorString("ERR EXEC without MULTI") 77 | } 78 | flushCtx() 79 | } 80 | 81 | private fun transactionClearPolicy(transactionId: String?): WritePolicy { 82 | val writePolicy = getWritePolicy() 83 | writePolicy.filterExp = Exp.build( 84 | Exp.and( 85 | Exp.binExists(aeroCtx.transactionIdBin), 86 | Exp.eq( 87 | Exp.bin(aeroCtx.transactionIdBin, Exp.Type.STRING), 88 | Exp.`val`(transactionId) 89 | ) 90 | ) 91 | ) 92 | return writePolicy 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/HyperLogCommandsTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertEquals 6 | 7 | class HyperLogCommandsTest() : SkyhookIntegrationTestBase() { 8 | 9 | @Test 10 | fun simpleAdd() { 11 | writeCommand(RedisCommand.PFADD, "ids ABC") 12 | assertEquals(1, readLong()) 13 | writeCommand(RedisCommand.PFCOUNT, "ids") 14 | assertEquals(1, readLong()) 15 | } 16 | 17 | @Test 18 | fun multipleAdd() { 19 | writeCommand(RedisCommand.PFADD, "ids 1 2 3") 20 | assertEquals(1, readLong()) 21 | writeCommand(RedisCommand.PFCOUNT, "ids") 22 | assertEquals(3, readLong()) 23 | } 24 | 25 | @Test 26 | fun duplicateAdd() { 27 | writeCommand(RedisCommand.PFADD, "ids ABC") 28 | assertEquals(1, readLong()) 29 | writeCommand(RedisCommand.PFADD, "ids ABC") 30 | assertEquals(0, readLong()) 31 | writeCommand(RedisCommand.PFADD, "ids ABC ABC") 32 | assertEquals(0, readLong()) 33 | writeCommand(RedisCommand.PFCOUNT, "ids") 34 | assertEquals(1, readLong()) 35 | } 36 | 37 | @Test 38 | fun redisDocumentationExample() { 39 | writeCommand(RedisCommand.PFADD, "hll foo bar zap") 40 | assertEquals(1, readLong()) 41 | writeCommand(RedisCommand.PFADD, "hll zap zap zap") 42 | assertEquals(0, readLong()) 43 | writeCommand(RedisCommand.PFADD, "hll foo bar") 44 | assertEquals(0, readLong()) 45 | writeCommand(RedisCommand.PFCOUNT, "hll") 46 | assertEquals(3, readLong()) 47 | writeCommand(RedisCommand.PFADD, "some-other-hll 1 2 3") 48 | assertEquals(1, readLong()) 49 | writeCommand(RedisCommand.PFCOUNT, "hll some-other-hll") 50 | assertEquals(6, readLong()) 51 | } 52 | 53 | @Test 54 | fun union() { 55 | writeCommand(RedisCommand.PFADD, "a 1 2") 56 | assertEquals(1, readLong()) 57 | writeCommand(RedisCommand.PFADD, "b 2 3") 58 | assertEquals(1, readLong()) 59 | 60 | writeCommand(RedisCommand.PFCOUNT, "a b") 61 | assertEquals(3, readLong()) 62 | writeCommand(RedisCommand.PFCOUNT, "a") 63 | assertEquals(2, readLong()) 64 | writeCommand(RedisCommand.PFCOUNT, "b") 65 | assertEquals(2, readLong()) 66 | } 67 | 68 | @Test 69 | fun countNonExistent() { 70 | writeCommand(RedisCommand.PFCOUNT, "key") 71 | assertEquals(0, readLong()) 72 | writeCommand(RedisCommand.PFADD, "a 1") 73 | assertEquals(1, readLong()) 74 | writeCommand(RedisCommand.PFCOUNT, "key a") 75 | assertEquals(1, readLong()) 76 | writeCommand(RedisCommand.PFCOUNT, "a key") 77 | assertEquals(1, readLong()) 78 | } 79 | 80 | @Test 81 | fun countAllNonExistent() { 82 | writeCommand(RedisCommand.PFCOUNT, "key") 83 | assertEquals(0, readLong()) 84 | writeCommand(RedisCommand.PFCOUNT, "key key2") 85 | assertEquals(0, readLong()) 86 | } 87 | 88 | @Test 89 | fun countMany() { 90 | val n = 10L 91 | (0 until n).forEach { 92 | writeCommand(RedisCommand.PFADD, "key${it} $it") 93 | assertEquals(1, readLong()) 94 | } 95 | val args = (0 until n).joinToString(" ") { "key${it}" } 96 | writeCommand(RedisCommand.PFCOUNT, args) 97 | assertEquals(n, readLong()) 98 | } 99 | 100 | @Test 101 | fun merge() { 102 | writeCommand(RedisCommand.PFADD, "a 1 2") 103 | assertEquals(1, readLong()) 104 | writeCommand(RedisCommand.PFADD, "b 2 3") 105 | assertEquals(1, readLong()) 106 | writeCommand(RedisCommand.PFMERGE, "m a b") 107 | assertEquals(ok, readString()) 108 | writeCommand(RedisCommand.PFCOUNT, "m") 109 | assertEquals(3, readLong()) 110 | } 111 | 112 | @Test 113 | fun mergeNonExistent() { 114 | writeCommand(RedisCommand.PFADD, "a 1 2") 115 | assertEquals(1, readLong()) 116 | writeCommand(RedisCommand.PFMERGE, "m a b") 117 | assertEquals(ok, readString()) 118 | writeCommand(RedisCommand.PFCOUNT, "m") 119 | assertEquals(2, readLong()) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/SkyhookServer.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.config.ServerConfiguration 4 | import com.aerospike.skyhook.pipeline.AerospikeChannelInitializer 5 | import com.aerospike.skyhook.util.SystemUtils 6 | import com.google.inject.name.Named 7 | import io.netty.bootstrap.ServerBootstrap 8 | import io.netty.channel.ChannelOption 9 | import io.netty.channel.EventLoopGroup 10 | import io.netty.channel.epoll.EpollServerDomainSocketChannel 11 | import io.netty.channel.kqueue.KQueueServerDomainSocketChannel 12 | import io.netty.channel.socket.ServerSocketChannel 13 | import io.netty.channel.unix.DomainSocketAddress 14 | import mu.KotlinLogging 15 | import javax.inject.Inject 16 | import javax.inject.Singleton 17 | 18 | @Singleton 19 | class SkyhookServer @Inject constructor( 20 | 21 | /** 22 | * Connect server configuration. 23 | */ 24 | private val config: ServerConfiguration, 25 | 26 | /** 27 | * Initialize channels. 28 | */ 29 | private val channelInitializer: AerospikeChannelInitializer, 30 | 31 | /** 32 | * The event loop group to handle connection requests. 33 | */ 34 | @Named(NETTY_BOSS_GROUP) 35 | private val bossGroup: EventLoopGroup, 36 | 37 | /** 38 | * The event loop group to read and parse incoming requests and write 39 | * responses. 40 | */ 41 | @Named(NETTY_WORKER_GROUP) 42 | private val workerGroup: EventLoopGroup, 43 | 44 | /** 45 | * The ServerSocketChannel. 46 | */ 47 | private val socketChannel: ServerSocketChannel 48 | ) : Server { 49 | 50 | companion object { 51 | /** 52 | * Annotation to get hold of the event loop boss group to handle 53 | * connection requests. 54 | */ 55 | const val NETTY_BOSS_GROUP = 56 | "com.aerospike.skyhook.SkyhookServer." + 57 | "NETTY_BOSS_GROUP" 58 | 59 | /** 60 | * Annotation to get hold of the event loop worker group to read, parse 61 | * incoming requests. 62 | */ 63 | const val NETTY_WORKER_GROUP = 64 | "com.aerospike.skyhook.SkyhookServer." + 65 | "NETTY_WORKER_GROUP" 66 | 67 | private val log = KotlinLogging.logger(this::class.java.name) 68 | } 69 | 70 | /** 71 | * Is the server started. 72 | */ 73 | private var started: Boolean = false 74 | 75 | override fun start() { 76 | if (started) { 77 | return 78 | } 79 | 80 | log.info { "Starting the Server..." } 81 | 82 | if (config.unixSocket == null) { 83 | bindInetSocket() 84 | } else { 85 | bindUnixSocket() 86 | } 87 | 88 | log.info { "Started Netty server with config $config" } 89 | started = true 90 | } 91 | 92 | override fun stop() { 93 | if (!started) { 94 | return 95 | } 96 | 97 | log.info("Shutting down Netty event loops...") 98 | 99 | bossGroup.shutdownGracefully().sync() 100 | workerGroup.shutdownGracefully().sync() 101 | started = false 102 | } 103 | 104 | private fun bindInetSocket() { 105 | val server = initServerBootstrap() 106 | 107 | server.channel(socketChannel.javaClass) 108 | .childOption(ChannelOption.SO_KEEPALIVE, true) 109 | .childOption(ChannelOption.TCP_NODELAY, true) 110 | .bind(config.redisPort).sync() 111 | } 112 | 113 | private fun bindUnixSocket() { 114 | val server = initServerBootstrap() 115 | 116 | val channel = when (SystemUtils.os) { 117 | SystemUtils.OS.LINUX -> EpollServerDomainSocketChannel() 118 | SystemUtils.OS.MAC -> KQueueServerDomainSocketChannel() 119 | else -> throw IllegalArgumentException("Unsupported UNIX Socket") 120 | } 121 | 122 | server.channel(channel.javaClass) 123 | .bind(DomainSocketAddress(config.unixSocket)).sync() 124 | } 125 | 126 | private fun initServerBootstrap(): ServerBootstrap { 127 | val server = ServerBootstrap() 128 | 129 | server.group(bossGroup, workerGroup) 130 | .childHandler(channelInitializer) 131 | .option(ChannelOption.SO_BACKLOG, 128) 132 | return server 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/ZcountCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.* 4 | import com.aerospike.client.cdt.MapOperation 5 | import com.aerospike.client.cdt.MapReturnType 6 | import com.aerospike.client.listener.RecordListener 7 | import com.aerospike.skyhook.command.RequestCommand 8 | import com.aerospike.skyhook.listener.BaseListener 9 | import com.aerospike.skyhook.util.Intervals 10 | import com.aerospike.skyhook.util.Typed 11 | import io.netty.channel.ChannelHandlerContext 12 | 13 | open class ZcountCommandListener( 14 | ctx: ChannelHandlerContext 15 | ) : BaseListener(ctx), RecordListener { 16 | 17 | override fun handle(cmd: RequestCommand) { 18 | require(cmd.argCount == 4) { argValidationErrorMsg(cmd) } 19 | 20 | val key = createKey(cmd.key) 21 | client.operate( 22 | null, this, defaultWritePolicy, 23 | key, getOperation(cmd) 24 | ) 25 | } 26 | 27 | protected open fun getOperation(cmd: RequestCommand): Operation { 28 | return MapOperation.getByValueRange( 29 | aeroCtx.bin, 30 | Value.get(Intervals.fromScore(String(cmd.args[2]))), 31 | Value.get(Intervals.upScore(String(cmd.args[3]))), 32 | MapReturnType.COUNT 33 | ) 34 | } 35 | 36 | override fun writeError(e: AerospikeException?) { 37 | writeLong(0L) 38 | } 39 | 40 | override fun onSuccess(key: Key?, record: Record?) { 41 | if (record == null) { 42 | writeLong(0L) 43 | flushCtxTransactionAware() 44 | } else { 45 | try { 46 | writeLong(record.getLong(aeroCtx.bin)) 47 | flushCtxTransactionAware() 48 | } catch (e: Exception) { 49 | closeCtx(e) 50 | } 51 | } 52 | } 53 | } 54 | 55 | class ZlexcountCommandListener( 56 | ctx: ChannelHandlerContext 57 | ) : ZcountCommandListener(ctx) { 58 | 59 | override fun getOperation(cmd: RequestCommand): Operation { 60 | return MapOperation.getByKeyRange( 61 | aeroCtx.bin, 62 | Value.get(Intervals.fromLex(String(cmd.args[2]))), 63 | Value.get(Intervals.upLex(String(cmd.args[3]))), 64 | MapReturnType.COUNT 65 | ) 66 | } 67 | } 68 | 69 | class ZremrangebyscoreCommandListener( 70 | ctx: ChannelHandlerContext 71 | ) : ZcountCommandListener(ctx) { 72 | 73 | override fun getOperation(cmd: RequestCommand): Operation { 74 | return MapOperation.removeByValueRange( 75 | aeroCtx.bin, 76 | Value.get(Intervals.fromScore(String(cmd.args[2]))), 77 | Value.get(Intervals.upScore(String(cmd.args[3]))), 78 | MapReturnType.COUNT 79 | ) 80 | } 81 | } 82 | 83 | class ZremrangebyrankCommandListener( 84 | ctx: ChannelHandlerContext 85 | ) : ZcountCommandListener(ctx) { 86 | 87 | override fun getOperation(cmd: RequestCommand): Operation { 88 | val from = Typed.getInteger(cmd.args[2]) 89 | val count = getCount(from, cmd) 90 | return MapOperation.removeByRankRange( 91 | aeroCtx.bin, 92 | from, 93 | count, 94 | MapReturnType.COUNT 95 | ) 96 | } 97 | 98 | private fun getCount(from: Int, cmd: RequestCommand): Int { 99 | val to = Typed.getInteger(cmd.args[3]) 100 | return maxOf( 101 | if (to < 0) { 102 | val key = createKey(cmd.key) 103 | val mapSize = client.operate( 104 | null, 105 | key, MapOperation.size(aeroCtx.bin) 106 | ).getInt(aeroCtx.bin) 107 | (mapSize + to + 1) - from 108 | } else { 109 | (to - from) + 1 110 | }, 0 111 | ) 112 | } 113 | } 114 | 115 | class ZremrangebylexCommandListener( 116 | ctx: ChannelHandlerContext 117 | ) : ZcountCommandListener(ctx) { 118 | 119 | override fun getOperation(cmd: RequestCommand): Operation { 120 | return MapOperation.removeByKeyRange( 121 | aeroCtx.bin, 122 | Value.get(Intervals.fromLex(String(cmd.args[2]))), 123 | Value.get(Intervals.upLex(String(cmd.args[3]))), 124 | MapReturnType.COUNT 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/key/GetCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.key 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Operation 5 | import com.aerospike.client.Record 6 | import com.aerospike.client.listener.RecordListener 7 | import com.aerospike.client.policy.WritePolicy 8 | import com.aerospike.skyhook.command.RequestCommand 9 | import com.aerospike.skyhook.listener.BaseListener 10 | import com.aerospike.skyhook.listener.ValueType 11 | import io.netty.channel.ChannelHandlerContext 12 | import java.util.* 13 | 14 | open class GetCommandListener( 15 | ctx: ChannelHandlerContext 16 | ) : BaseListener(ctx), RecordListener { 17 | 18 | override fun handle(cmd: RequestCommand) { 19 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 20 | 21 | val key = createKey(cmd.key) 22 | client.get(null, this, defaultWritePolicy, key) 23 | } 24 | 25 | override fun onSuccess(key: Key?, record: Record?) { 26 | if (record?.bins == null) { 27 | writeNullString() 28 | flushCtxTransactionAware() 29 | } else { 30 | try { 31 | val value = record.bins[aeroCtx.bin] 32 | writeResponse( 33 | when (value) { 34 | is Long -> value.toString() 35 | else -> value 36 | } 37 | ) 38 | flushCtxTransactionAware() 39 | } catch (e: Exception) { 40 | closeCtx(e) 41 | } 42 | } 43 | } 44 | } 45 | 46 | class GetexCommandListener( 47 | ctx: ChannelHandlerContext 48 | ) : GetCommandListener(ctx) { 49 | 50 | private class GetexCommand(val cmd: RequestCommand) { 51 | var EX: Int? = null 52 | var PX: Int? = null 53 | var EXAT: Long? = null 54 | private set 55 | var PXAT: Long? = null 56 | private set 57 | var PERSIST: Boolean? = null 58 | private set 59 | 60 | init { 61 | for (i in 2 until cmd.args.size) { 62 | setFlag(i) 63 | } 64 | validate() 65 | } 66 | 67 | fun buildPolicy(policy: WritePolicy): WritePolicy { 68 | EX?.let { 69 | require(it > 0) { "invalid expiration" } 70 | policy.expiration = it 71 | } 72 | PX?.let { 73 | require(it > 0) { "invalid expiration" } 74 | policy.expiration = Integer.max(it / 1000, 1) 75 | } 76 | EXAT?.let { 77 | val exp = it - (System.currentTimeMillis() / 1000) 78 | require(exp > 0) { "invalid expiration" } 79 | policy.expiration = exp.toInt() 80 | } 81 | PXAT?.let { 82 | val exp = it - System.currentTimeMillis() 83 | require(exp > 0) { "invalid expiration" } 84 | policy.expiration = Integer.max(exp.toInt() / 1000, 1) 85 | } 86 | PERSIST?.let { 87 | policy.expiration = 0 88 | } 89 | return policy 90 | } 91 | 92 | private fun setFlag(i: Int) { 93 | val flagStr = String(cmd.args[i]) 94 | when (flagStr.uppercase(Locale.ENGLISH)) { 95 | "EX" -> EX = String(cmd.args[i + 1]).toInt() 96 | "PX" -> PX = String(cmd.args[i + 1]).toInt() 97 | "EXAT" -> EXAT = String(cmd.args[i + 1]).toLong() 98 | "PXAT" -> PXAT = String(cmd.args[i + 1]).toLong() 99 | "PERSIST" -> PERSIST = true 100 | } 101 | } 102 | 103 | private fun validate() { 104 | require(listOfNotNull(EX, PX, EXAT, PXAT, PERSIST).size <= 1) { 105 | "[EX|PX|EXAT|PXAT|PERSIST]" 106 | } 107 | } 108 | } 109 | 110 | override fun handle(cmd: RequestCommand) { 111 | require(cmd.argCount >= 2) { argValidationErrorMsg(cmd) } 112 | 113 | val getexCommand = GetexCommand(cmd) 114 | val key = createKey(cmd.key) 115 | val ops = arrayOf( 116 | *systemOps(ValueType.STRING), 117 | Operation.get(aeroCtx.bin) 118 | ) 119 | val writePolicy = getexCommand.buildPolicy(getWritePolicy()) 120 | 121 | client.operate(null, this, writePolicy, key, *ops) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/SetCommandsTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import org.junit.jupiter.api.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | 8 | class SetCommandsTest() : SkyhookIntegrationTestBase() { 9 | 10 | private val _key = "set" 11 | 12 | private fun setup(n: Int = 3, key: String = _key) { 13 | for (i in 1..n) { 14 | writeCommand("${RedisCommand.SADD.name} $key val$i") 15 | assertEquals(1, readLong()) 16 | } 17 | } 18 | 19 | @Test 20 | fun testSadd() { 21 | setup(2) 22 | writeCommand("${RedisCommand.SADD.name} $_key val1") 23 | assertEquals(0, readLong()) 24 | } 25 | 26 | @Test 27 | fun testSismember() { 28 | setup(1) 29 | writeCommand("${RedisCommand.SISMEMBER.name} $_key val1") 30 | assertEquals(1, readLong()) 31 | writeCommand("${RedisCommand.SISMEMBER.name} $_key val2") 32 | assertEquals(0, readLong()) 33 | writeCommand("${RedisCommand.SISMEMBER.name} ne val1") 34 | assertEquals(0, readLong()) 35 | } 36 | 37 | @Test 38 | fun testSmismember() { 39 | setup(1) 40 | writeCommand("${RedisCommand.SMISMEMBER.name} $_key val1 val2 val3") 41 | val r = readLongArray() 42 | assertEquals(1, r[0]) 43 | assertEquals(0, r[1]) 44 | assertEquals(0, r[2]) 45 | 46 | writeCommand("${RedisCommand.SMISMEMBER.name} ne val1 val2") 47 | val r2 = readLongArray() 48 | assertEquals(0, r2[0]) 49 | assertEquals(0, r2[1]) 50 | } 51 | 52 | @Test 53 | fun testSmembers() { 54 | setup() 55 | writeCommand("${RedisCommand.SMEMBERS.name} $_key") 56 | val r = readStringArray() 57 | assertEquals("val1", r[0]) 58 | assertEquals("val2", r[1]) 59 | assertEquals("val3", r[2]) 60 | } 61 | 62 | @Test 63 | fun testScard() { 64 | setup() 65 | writeCommand("${RedisCommand.SCARD.name} $_key") 66 | assertEquals(3, readLong()) 67 | writeCommand("${RedisCommand.SCARD.name} set2") 68 | assertEquals(0, readLong()) 69 | } 70 | 71 | @Test 72 | fun testSrem() { 73 | setup(1) 74 | writeCommand("${RedisCommand.SREM.name} $_key val1") 75 | assertEquals(1, readLong()) 76 | writeCommand("${RedisCommand.SREM.name} $_key val2") 77 | assertEquals(0, readLong()) 78 | } 79 | 80 | @Test 81 | fun testSunion() { 82 | setup() 83 | setup(4, "set2") 84 | writeCommand("${RedisCommand.SUNION.name} $_key set2") 85 | val r = readStringArray() 86 | assertTrue { r.size == 4 } 87 | assertTrue { r.contains("val1") } 88 | assertTrue { r.contains("val2") } 89 | assertTrue { r.contains("val3") } 90 | assertTrue { r.contains("val4") } 91 | } 92 | 93 | @Test 94 | fun testSunionstore() { 95 | setup() 96 | setup(4, "set2") 97 | writeCommand("${RedisCommand.SUNIONSTORE.name} union $_key set2") 98 | assertEquals(4, readLong()) 99 | } 100 | 101 | @Test 102 | fun testSinter() { 103 | setup() 104 | setup(4, "set2") 105 | writeCommand("${RedisCommand.SINTER.name} $_key set2") 106 | val r = readStringArray() 107 | assertTrue { r.size == 3 } 108 | assertTrue { r.contains("val1") } 109 | assertTrue { r.contains("val2") } 110 | assertTrue { r.contains("val3") } 111 | } 112 | 113 | @Test 114 | fun testSinterstore() { 115 | setup() 116 | setup(4, "set2") 117 | writeCommand("${RedisCommand.SINTERSTORE.name} inter $_key set2") 118 | assertEquals(3, readLong()) 119 | } 120 | 121 | @Test 122 | fun testSrandmember() { 123 | setup() 124 | writeCommand("${RedisCommand.SRANDMEMBER.name} $_key") 125 | val r = readFullBulkString() 126 | assertTrue { r.startsWith("val") } 127 | 128 | writeCommand("${RedisCommand.SRANDMEMBER.name} $_key 5") 129 | val r2 = readStringArray() 130 | assertTrue { r2.size == 3 } 131 | 132 | writeCommand("${RedisCommand.SRANDMEMBER.name} $_key -5") 133 | val r3 = readStringArray() 134 | assertTrue { r3.size == 5 } 135 | 136 | writeCommand("${RedisCommand.SRANDMEMBER.name} ne") 137 | assertEquals(nullString, readFullBulkString()) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/scan/ScanCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.scan 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Record 5 | import com.aerospike.client.ScanCallback 6 | import com.aerospike.client.cluster.Node 7 | import com.aerospike.client.cluster.Partition 8 | import com.aerospike.client.exp.Exp 9 | import com.aerospike.client.policy.ScanPolicy 10 | import com.aerospike.client.query.KeyRecord 11 | import com.aerospike.client.query.PartitionFilter 12 | import com.aerospike.client.query.RegexFlag 13 | import com.aerospike.skyhook.command.RequestCommand 14 | import com.aerospike.skyhook.listener.BaseListener 15 | import com.aerospike.skyhook.listener.scan.ScanCommand.Companion.zeroCursor 16 | import io.netty.channel.ChannelHandlerContext 17 | 18 | class ScanCommandListener( 19 | ctx: ChannelHandlerContext 20 | ) : BaseListener(ctx) { 21 | 22 | private lateinit var scanCommand: ScanCommand 23 | private var currentPartition = 0 24 | private var count = 0 25 | private val recordSet: RecordSet by lazy { 26 | RecordSet() 27 | } 28 | 29 | override fun handle(cmd: RequestCommand) { 30 | require(cmd.argCount >= 2) { argValidationErrorMsg(cmd) } 31 | 32 | scanCommand = ScanCommand(cmd, 2) 33 | 34 | scanPartition() 35 | writeScanResponse() 36 | } 37 | 38 | private fun writeScanResponse() { 39 | writeArrayHeader(2) 40 | writeSimpleString(getNextCursor()) 41 | writeObjectListStr(recordSet.map { it.key.userKey.`object` as String }) 42 | flushCtxTransactionAware() 43 | } 44 | 45 | private fun getNextCursor(): String { 46 | return if (recordSet.size < scanCommand.COUNT) { 47 | zeroCursor 48 | } else { 49 | recordSet.nextCursor() ?: zeroCursor 50 | } 51 | } 52 | 53 | private fun scanPartition() { 54 | val scanPolicy = buildScanPolicy() 55 | var filter = getPartitionFilter() 56 | while (isScanRequired()) { 57 | client.scanPartitions( 58 | scanPolicy, filter, aeroCtx.namespace, aeroCtx.set, 59 | callback, aeroCtx.bin 60 | ) 61 | resetMaxRecords(scanPolicy) 62 | filter = PartitionFilter.id(++currentPartition) 63 | } 64 | } 65 | 66 | private fun resetMaxRecords(scanPolicy: ScanPolicy) { 67 | scanPolicy.maxRecords = if (scanCommand.COUNT > 0) { 68 | scanCommand.COUNT - count 69 | } else { 70 | scanCommand.COUNT 71 | } 72 | } 73 | 74 | private fun buildScanPolicy(): ScanPolicy { 75 | val scanPolicy = ScanPolicy() 76 | scanPolicy.sendKey = true 77 | scanPolicy.includeBinData = false 78 | scanPolicy.maxRecords = scanCommand.COUNT 79 | val expressions = listOfNotNull( 80 | scanCommand.TYPE?.let { 81 | Exp.eq( 82 | Exp.stringBin(aeroCtx.typeBin), 83 | Exp.`val`(it.str) 84 | ) 85 | }, 86 | scanCommand.MATCH?.let { 87 | Exp.regexCompare( 88 | it, 89 | RegexFlag.ICASE or RegexFlag.NEWLINE, 90 | Exp.key(Exp.Type.STRING) 91 | ) 92 | } 93 | ) 94 | scanPolicy.filterExp = when (expressions.size) { 95 | 0 -> null 96 | 1 -> Exp.build(expressions.first()) 97 | else -> Exp.build(Exp.and(*expressions.toTypedArray())) 98 | } 99 | return scanPolicy 100 | } 101 | 102 | private fun getPartitionFilter(): PartitionFilter? { 103 | if (scanCommand.cursor != zeroCursor) { 104 | val key = Key(aeroCtx.namespace, aeroCtx.set, scanCommand.cursor) 105 | currentPartition = Partition.getPartitionId(key.digest) 106 | return PartitionFilter.after(key) 107 | } 108 | return PartitionFilter.id(currentPartition) 109 | } 110 | 111 | private fun isScanRequired(): Boolean { 112 | return (scanCommand.COUNT == 0L || count < scanCommand.COUNT) && isValidPartition() 113 | } 114 | 115 | private fun isValidPartition(): Boolean { 116 | return currentPartition >= 0 && currentPartition < Node.PARTITIONS 117 | } 118 | 119 | private val callback = ScanCallback { key: Key?, record: Record? -> 120 | recordSet.add(KeyRecord(key, record)) 121 | count++ 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/kotlin/com/aerospike/skyhook/TypeCommandTest.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook 2 | 3 | import com.aerospike.skyhook.command.RedisCommand 4 | import com.aerospike.skyhook.listener.ValueType 5 | import org.junit.jupiter.api.Test 6 | import kotlin.test.assertEquals 7 | 8 | class TypeCommandTest() : SkyhookIntegrationTestBase() { 9 | 10 | @Test 11 | fun testStringType() { 12 | writeCommand("${RedisCommand.SET.name} set abc") 13 | assertEquals(ok, readString()) 14 | writeCommand("${RedisCommand.TYPE.name} set") 15 | assertEquals(ValueType.STRING.str, readFullBulkString()) 16 | 17 | writeCommand("${RedisCommand.APPEND.name} append abc") 18 | assertEquals(3L, readLong()) 19 | writeCommand("${RedisCommand.TYPE.name} append") 20 | assertEquals(ValueType.STRING.str, readFullBulkString()) 21 | 22 | writeCommand("${RedisCommand.SETEX.name} setex 10 abc") 23 | assertEquals(ok, readString()) 24 | writeCommand("${RedisCommand.TYPE.name} setex") 25 | assertEquals(ValueType.STRING.str, readFullBulkString()) 26 | 27 | writeCommand("${RedisCommand.PSETEX.name} psetex 10 abc") 28 | assertEquals(ok, readString()) 29 | writeCommand("${RedisCommand.TYPE.name} psetex") 30 | assertEquals(ValueType.STRING.str, readFullBulkString()) 31 | 32 | writeCommand("${RedisCommand.SETNX.name} setnx abc") 33 | assertEquals(1L, readLong()) 34 | writeCommand("${RedisCommand.TYPE.name} setnx") 35 | assertEquals(ValueType.STRING.str, readFullBulkString()) 36 | 37 | writeCommand("${RedisCommand.MSET.name} mset abc") 38 | assertEquals(ok, readString()) 39 | writeCommand("${RedisCommand.TYPE.name} mset") 40 | assertEquals(ValueType.STRING.str, readFullBulkString()) 41 | 42 | writeCommand("${RedisCommand.MSETNX.name} msetnx abc") 43 | assertEquals(1L, readLong()) 44 | writeCommand("${RedisCommand.TYPE.name} msetnx") 45 | assertEquals(ValueType.STRING.str, readFullBulkString()) 46 | 47 | writeCommand("${RedisCommand.INCR.name} incr") 48 | assertEquals(1L, readLong()) 49 | writeCommand("${RedisCommand.TYPE.name} incr") 50 | assertEquals(ValueType.STRING.str, readFullBulkString()) 51 | 52 | writeCommand("${RedisCommand.INCRBY.name} incrby 3") 53 | assertEquals(3L, readLong()) 54 | writeCommand("${RedisCommand.TYPE.name} incrby") 55 | assertEquals(ValueType.STRING.str, readFullBulkString()) 56 | 57 | writeCommand("${RedisCommand.INCRBYFLOAT.name} incrbyfloat 0.5") 58 | readFullBulkString() 59 | writeCommand("${RedisCommand.TYPE.name} incrbyfloat") 60 | assertEquals(ValueType.STRING.str, readFullBulkString()) 61 | } 62 | 63 | @Test 64 | fun testListType() { 65 | writeCommand("${RedisCommand.RPUSH.name} list val1") 66 | assertEquals(1L, readLong()) 67 | writeCommand("${RedisCommand.TYPE.name} list") 68 | assertEquals(ValueType.LIST.str, readFullBulkString()) 69 | 70 | writeCommand("${RedisCommand.LPUSH.name} list2 val1") 71 | assertEquals(1L, readLong()) 72 | writeCommand("${RedisCommand.TYPE.name} list2") 73 | assertEquals(ValueType.LIST.str, readFullBulkString()) 74 | } 75 | 76 | @Test 77 | fun testHashType() { 78 | writeCommand("${RedisCommand.HSET.name} hash key1 val1") 79 | assertEquals(1L, readLong()) 80 | writeCommand("${RedisCommand.TYPE.name} hash") 81 | assertEquals(ValueType.HASH.str, readFullBulkString()) 82 | 83 | writeCommand("${RedisCommand.HSETNX.name} hash2 key1 val1") 84 | assertEquals(1L, readLong()) 85 | writeCommand("${RedisCommand.TYPE.name} hash2") 86 | assertEquals(ValueType.HASH.str, readFullBulkString()) 87 | 88 | writeCommand("${RedisCommand.HMSET.name} hash3 key1 val1") 89 | assertEquals(ok, readString()) 90 | writeCommand("${RedisCommand.TYPE.name} hash3") 91 | assertEquals(ValueType.HASH.str, readFullBulkString()) 92 | } 93 | 94 | @Test 95 | fun testSetType() { 96 | writeCommand("${RedisCommand.SADD.name} set val1") 97 | assertEquals(1L, readLong()) 98 | writeCommand("${RedisCommand.TYPE.name} set") 99 | assertEquals(ValueType.SET.str, readFullBulkString()) 100 | } 101 | 102 | @Test 103 | fun testZsetType() { 104 | writeCommand("${RedisCommand.ZADD.name} zset 1 val1") 105 | assertEquals(1L, readLong()) 106 | writeCommand("${RedisCommand.TYPE.name} zset") 107 | assertEquals(ValueType.ZSET.str, readFullBulkString()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/handler/AerospikeChannelHandler.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.handler 2 | 3 | import com.aerospike.client.AerospikeException 4 | import com.aerospike.client.ResultCode 5 | import com.aerospike.skyhook.command.RequestCommand 6 | import com.aerospike.skyhook.pipeline.AerospikeChannelInitializer.Companion.transactionAttrKey 7 | import io.netty.channel.ChannelHandler 8 | import io.netty.channel.ChannelHandlerContext 9 | import io.netty.channel.ChannelInboundHandlerAdapter 10 | import io.netty.handler.codec.CodecException 11 | import io.netty.handler.codec.redis.* 12 | import io.netty.util.CharsetUtil 13 | import io.netty.util.ReferenceCountUtil 14 | import mu.KotlinLogging 15 | import javax.inject.Singleton 16 | 17 | @Singleton 18 | @ChannelHandler.Sharable 19 | class AerospikeChannelHandler : ChannelInboundHandlerAdapter() { 20 | 21 | companion object { 22 | private val log = KotlinLogging.logger(this::class.java.name) 23 | } 24 | 25 | override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { 26 | try { 27 | when (msg) { 28 | is ArrayRedisMessage -> { 29 | val arguments: MutableList = ArrayList(msg.children().size) 30 | for (child in msg.children()) { 31 | if (child is FullBulkStringRedisMessage) { 32 | val bytes = ByteArray(child.content().readableBytes()) 33 | val readerIndex = child.content().readerIndex() 34 | child.content().getBytes(readerIndex, bytes) 35 | arguments.add(bytes) 36 | } 37 | } 38 | 39 | val cmd = RequestCommand(arguments) 40 | handleCommand(cmd, ctx) 41 | } 42 | 43 | is AbstractStringRedisMessage -> { 44 | val cmd = RequestCommand( 45 | (msg.content().split(" ") 46 | .map { it.encodeToByteArray() }).toList() 47 | ) 48 | handleCommand(cmd, ctx) 49 | } 50 | 51 | else -> log.warn { "Unsupported message type ${msg.javaClass.simpleName}" } 52 | } 53 | } catch (e: UnsupportedOperationException) { 54 | ctx.write(ErrorRedisMessage(e.message)) 55 | ctx.flush() 56 | } finally { 57 | ReferenceCountUtil.release(msg) 58 | } 59 | } 60 | 61 | /** 62 | * Handle the input command. Listeners are responsible to send the response 63 | * to the client. 64 | */ 65 | private fun handleCommand(cmd: RequestCommand, ctx: ChannelHandlerContext) { 66 | try { 67 | val state = ctx.channel().attr(transactionAttrKey).get() 68 | if (state.inTransaction && !cmd.transactional) { 69 | state.commands.addLast(cmd) 70 | ctx.write(SimpleStringRedisMessage("QUEUED")) 71 | ctx.flush() 72 | } else { 73 | cmd.command.newHandler(ctx).handle(cmd) 74 | } 75 | } catch (e: Exception) { 76 | val msg = when (e) { 77 | is AerospikeException -> { 78 | if (e.resultCode == ResultCode.FILTERED_OUT) 79 | "Transaction error" 80 | else 81 | "Internal error" 82 | } 83 | 84 | else -> e.message 85 | } 86 | log.warn(e) {} 87 | ctx.write(ErrorRedisMessage(msg)) 88 | ctx.flush() 89 | } 90 | } 91 | 92 | private fun printAggregatedRedisResponse(msg: RedisMessage) { 93 | when (msg) { 94 | is SimpleStringRedisMessage -> { 95 | log.debug { msg.content() } 96 | } 97 | 98 | is ErrorRedisMessage -> { 99 | log.debug { msg.content() } 100 | } 101 | 102 | is IntegerRedisMessage -> { 103 | log.debug { msg.value() } 104 | } 105 | 106 | is InlineCommandRedisMessage -> { 107 | log.debug { msg.content() } 108 | } 109 | 110 | is FullBulkStringRedisMessage -> { 111 | log.debug { getString(msg) } 112 | } 113 | 114 | is ArrayRedisMessage -> { 115 | for (child in msg.children()) { 116 | printAggregatedRedisResponse(child) 117 | } 118 | } 119 | 120 | else -> { 121 | throw CodecException("unknown message type: $msg") 122 | } 123 | } 124 | } 125 | 126 | private fun getString(msg: FullBulkStringRedisMessage): String? { 127 | return if (msg.isNull) { 128 | "(null)" 129 | } else msg.content().toString(CharsetUtil.UTF_8) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/MapGetCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Operation 5 | import com.aerospike.client.Record 6 | import com.aerospike.client.Value 7 | import com.aerospike.client.cdt.MapOperation 8 | import com.aerospike.client.cdt.MapReturnType 9 | import com.aerospike.client.listener.RecordListener 10 | import com.aerospike.skyhook.command.RedisCommand 11 | import com.aerospike.skyhook.command.RequestCommand 12 | import com.aerospike.skyhook.listener.BaseListener 13 | import com.aerospike.skyhook.util.Typed 14 | import io.netty.channel.ChannelHandlerContext 15 | 16 | class MapGetCommandListener( 17 | ctx: ChannelHandlerContext 18 | ) : BaseListener(ctx), RecordListener { 19 | 20 | @Volatile 21 | private lateinit var command: RedisCommand 22 | 23 | override fun handle(cmd: RequestCommand) { 24 | command = cmd.command 25 | val key = createKey(cmd.key) 26 | 27 | client.operate( 28 | null, this, defaultWritePolicy, 29 | key, getOperation(cmd) 30 | ) 31 | } 32 | 33 | private fun getValues(cmd: RequestCommand): List { 34 | return cmd.args.drop(2) 35 | .map { Typed.getValue(it) } 36 | } 37 | 38 | private fun getOperation(cmd: RequestCommand): Operation { 39 | return when (cmd.command) { 40 | RedisCommand.HGET -> { 41 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 42 | 43 | val mapKey = Typed.getValue(cmd.args[2]) 44 | MapOperation.getByKey( 45 | aeroCtx.bin, mapKey, 46 | MapReturnType.VALUE 47 | ) 48 | } 49 | RedisCommand.ZRANK -> { 50 | require(cmd.argCount == 3) { argValidationErrorMsg(cmd) } 51 | 52 | val mapKey = Typed.getValue(cmd.args[2]) 53 | MapOperation.getByKey( 54 | aeroCtx.bin, mapKey, 55 | MapReturnType.RANK 56 | ) 57 | } 58 | RedisCommand.HMGET, RedisCommand.ZMSCORE -> { 59 | require(cmd.argCount >= 3) { argValidationErrorMsg(cmd) } 60 | 61 | val mapKeys = getValues(cmd) 62 | MapOperation.getByKeyList( 63 | aeroCtx.bin, mapKeys, 64 | MapReturnType.VALUE 65 | ) 66 | } 67 | RedisCommand.HGETALL -> { 68 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 69 | 70 | MapOperation.getByKeyRange( 71 | aeroCtx.bin, null, null, 72 | MapReturnType.KEY_VALUE 73 | ) 74 | } 75 | RedisCommand.HVALS -> { 76 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 77 | 78 | MapOperation.getByKeyRange( 79 | aeroCtx.bin, null, null, 80 | MapReturnType.VALUE 81 | ) 82 | } 83 | RedisCommand.HKEYS, RedisCommand.SMEMBERS -> { 84 | require(cmd.argCount == 2) { argValidationErrorMsg(cmd) } 85 | 86 | MapOperation.getByKeyRange( 87 | aeroCtx.bin, null, null, 88 | MapReturnType.KEY 89 | ) 90 | } 91 | else -> { 92 | throw IllegalArgumentException(cmd.command.toString()) 93 | } 94 | } 95 | } 96 | 97 | override fun onSuccess(key: Key?, record: Record?) { 98 | if (record == null) { 99 | writeNull() 100 | flushCtxTransactionAware() 101 | } else { 102 | try { 103 | writeResponse(marshalOutput(record.bins[aeroCtx.bin])) 104 | flushCtxTransactionAware() 105 | } catch (e: Exception) { 106 | closeCtx(e) 107 | } 108 | } 109 | } 110 | 111 | private fun writeNull() { 112 | when (command) { 113 | RedisCommand.HGETALL, 114 | RedisCommand.HVALS, 115 | RedisCommand.HKEYS, 116 | RedisCommand.SMEMBERS -> 117 | writeEmptyList() 118 | else -> writeNullString() 119 | } 120 | } 121 | 122 | private fun marshalOutput(data: Any?): Any? { 123 | return when (data) { 124 | is Map<*, *> -> data.toList() 125 | is List<*> -> { 126 | when (data.firstOrNull()) { 127 | is Map.Entry<*, *> -> data.map { it as Map.Entry<*, *> } 128 | .map { it.toPair().toList() }.flatten() 129 | else -> when (command) { 130 | RedisCommand.ZMSCORE -> data.map { it.toString() } 131 | else -> data 132 | } 133 | } 134 | } 135 | -1L -> if (command == RedisCommand.ZRANK) null else data 136 | else -> data 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/kotlin/com/aerospike/skyhook/listener/map/ZaddCommandListener.kt: -------------------------------------------------------------------------------- 1 | package com.aerospike.skyhook.listener.map 2 | 3 | import com.aerospike.client.Key 4 | import com.aerospike.client.Operation 5 | import com.aerospike.client.Record 6 | import com.aerospike.client.Value 7 | import com.aerospike.client.cdt.MapOperation 8 | import com.aerospike.client.cdt.MapOrder 9 | import com.aerospike.client.cdt.MapPolicy 10 | import com.aerospike.client.cdt.MapWriteFlags 11 | import com.aerospike.client.listener.RecordListener 12 | import com.aerospike.skyhook.command.RequestCommand 13 | import com.aerospike.skyhook.listener.BaseListener 14 | import com.aerospike.skyhook.listener.ValueType 15 | import com.aerospike.skyhook.util.Typed 16 | import io.netty.channel.ChannelHandlerContext 17 | import java.util.* 18 | 19 | class ZaddCommandListener( 20 | ctx: ChannelHandlerContext 21 | ) : BaseListener(ctx), RecordListener { 22 | 23 | private class ZaddCommand(val cmd: RequestCommand) { 24 | var XX: Boolean = false 25 | private set 26 | var NX: Boolean = false 27 | private set 28 | var LT: Boolean = false 29 | private set 30 | var GT: Boolean = false 31 | private set 32 | var CH: Boolean = false 33 | private set 34 | var INCR: Boolean = false 35 | private set 36 | 37 | lateinit var values: Map 38 | private set 39 | 40 | init { 41 | for (i in 2 until cmd.args.size) { 42 | if (!setFlag(String(cmd.args[i]))) { 43 | setSortedSetValues(i) 44 | break 45 | } 46 | } 47 | validate() 48 | } 49 | 50 | private fun setFlag(flagStr: String): Boolean { 51 | when (flagStr.uppercase(Locale.ENGLISH)) { 52 | "XX" -> XX = true 53 | "NX" -> NX = true 54 | "LT" -> LT = true 55 | "GT" -> GT = true 56 | "CH" -> CH = true 57 | "INCR" -> INCR = true 58 | else -> return false 59 | } 60 | return true 61 | } 62 | 63 | private fun setSortedSetValues(from: Int) { 64 | values = cmd.args.drop(from).chunked(2).associate { (it1, it2) -> 65 | Typed.getStringValue(it2) to Value.LongValue(Typed.getLong(it1)) 66 | } 67 | } 68 | 69 | private fun validate() { 70 | require(!(NX && XX)) { "[NX|XX]" } 71 | require(!(GT && LT)) { "[GT|LT]" } 72 | require(!LT) { "LT flag not supported" } 73 | require(!GT) { "GT flag not supported" } 74 | require(!CH) { "CH flag not supported" } 75 | } 76 | } 77 | 78 | @Volatile 79 | private var size: Long = 0L 80 | 81 | @Volatile 82 | private lateinit var zaddCommand: ZaddCommand 83 | 84 | override fun handle(cmd: RequestCommand) { 85 | require(cmd.argCount >= 4) { argValidationErrorMsg(cmd) } 86 | 87 | val key = createKey(cmd.key) 88 | zaddCommand = ZaddCommand(cmd) 89 | 90 | val getSize = MapOperation.size(aeroCtx.bin) 91 | size = client.operate(defaultWritePolicy, key, getSize) 92 | ?.getLong(aeroCtx.bin) ?: 0L 93 | 94 | client.operate( 95 | null, this, defaultWritePolicy, 96 | key, *systemOps(ValueType.ZSET), getMapOperation() 97 | ) 98 | } 99 | 100 | private fun getMapOperation(): Operation { 101 | return when { 102 | zaddCommand.INCR -> { 103 | require(zaddCommand.values.size == 1) { "INCR params" } 104 | MapOperation.increment( 105 | getMapPolicy(), 106 | aeroCtx.bin, 107 | zaddCommand.values.keys.first(), 108 | zaddCommand.values.values.first() 109 | ) 110 | } 111 | 112 | else -> { 113 | MapOperation.putItems( 114 | getMapPolicy(), 115 | aeroCtx.bin, 116 | zaddCommand.values 117 | ) 118 | } 119 | } 120 | } 121 | 122 | private fun getMapPolicy(): MapPolicy { 123 | val writeFlag = when { 124 | zaddCommand.XX -> { 125 | MapWriteFlags.UPDATE_ONLY 126 | } 127 | 128 | zaddCommand.NX -> { 129 | MapWriteFlags.CREATE_ONLY 130 | } 131 | 132 | else -> { 133 | MapWriteFlags.DEFAULT 134 | } 135 | } 136 | return MapPolicy(MapOrder.KEY_VALUE_ORDERED, writeFlag) 137 | } 138 | 139 | override fun onSuccess(key: Key?, record: Record?) { 140 | if (record == null) { 141 | writeLong(0L) 142 | flushCtxTransactionAware() 143 | } else { 144 | try { 145 | if (zaddCommand.INCR) { 146 | writeResponse(record.getString(aeroCtx.bin)) 147 | } else { 148 | val added = record.getLong(aeroCtx.bin) - size 149 | writeLong(added) 150 | } 151 | flushCtxTransactionAware() 152 | } catch (e: Exception) { 153 | closeCtx(e) 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /docs/scaling-out.md: -------------------------------------------------------------------------------- 1 | ## Scaling out Skyhook 2 | Skyhook is a stateless application, thus can be scaled with ease to handle higher loads. 3 | There are several ways to split traffic across multiple nodes. Below is some quick documentation on how to scale Skyhook using well known open-source reverse proxy servers and enable SSL/TLS encryption along the way. 4 | 5 | ### Load balancing 6 | Load balancing refers to efficiently distributing incoming network traffic across a group of backend servers. Modern high‑traffic services must serve a huge amount of concurrent requests and respond in a fast and reliable manner. To meet the scale, modern computing best practice generally requires adding more servers. 7 | In this manner, a load balancer performs the following functions: 8 | * Distributes client requests or network load efficiently across multiple servers 9 | * Ensures high availability and reliability by sending requests only to servers that are online 10 | * Provides the flexibility to add or subtract servers on demand 11 | 12 | ### Architecture Diagram 13 | ![](./images/scaling-out-diagram.png) 14 | 15 | ### Using Nginx as HTTP load balancer 16 | 17 | #### Load balancing methods 18 | The following load balancing mechanisms (or methods) are supported in nginx: 19 | * round-robin — requests to the application servers are distributed in a round-robin fashion, 20 | * least-connected — next request is assigned to the server with the least number of active connections, 21 | * ip-hash — a hash-function is used to determine what server should be selected for the next request (based on the client’s IP address). 22 | 23 | #### Default load balancing configuration 24 | The simplest configuration for load balancing with nginx may look like the following: 25 | ```conf 26 | http { 27 | upstream skyhook { 28 | server srv1.skyhook; 29 | server srv2.skyhook; 30 | server srv3.skyhook; 31 | } 32 | server { 33 | listen 80; 34 | 35 | location / { 36 | proxy_pass http://skyhook; 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | In the example above, there are 3 instances of Skyhook running on srv1-srv3. When the load balancing method is not specifically configured, 43 | it defaults to round-robin. All requests are [proxied](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass) to the server group skyhook, and nginx applies HTTP load balancing to distribute the requests. 44 | 45 | You can read more about configuring the Nginx LB from their [official documentation](http://nginx.org/en/docs/http/load_balancing.html). 46 | 47 | ### Using HAProxy as HTTP load balancer 48 | Look into [instructions](https://www.haproxy.com/documentation/hapee/latest/getting-started/installation/) on how to install HAProxy. 49 | 50 | HAProxy's configuration file is /etc/haproxy/haproxy.cfg. This is where you make the changes to define your load balancer. 51 | This [basic configuration](https://gist.github.com/haproxytechblog/38ef4b7d42f16cfe5c30f28ee3304dce) will get you started with a working server. 52 | 53 | ### SSL Termination 54 | When you operate a farm of servers, it can be a tedious task maintaining SSL certificates. 55 | Even using a Let’s Encrypt Certbot to automatically update certificates has its challenges because, 56 | unless you have the ability to dynamically update DNS records as part of the certificate renewal process, 57 | it may necessitate making your web servers directly accessible from the Internet so that Let’s Encrypt servers can verify that you own your domain. 58 | 59 | Enabling SSL on your web servers also costs more CPU usage, since those servers must become involved in encrypting and decrypting messages. 60 | That CPU time could otherwise have been used to do other meaningful work. Web servers can process requests more quickly if they’re not also crunching through encryption algorithms simultaneously. 61 | 62 | The term SSL termination means that you are performing all encryption and decryption at the edge of your network, such as at the load balancer. 63 | The load balancer strips away the encryption and passes the messages in the clear to your servers. You might also hear this called SSL offloading. 64 | 65 | SSL termination has many benefits. These include the following: 66 | * You can maintain certificates in fewer places, making your job easier. 67 | * You don’t need to expose your servers to the Internet for certificate renewal purposes. 68 | * Servers are unburdened from the task of processing encrypted messages, freeing up CPU time. 69 | 70 | ### Enabling SSL with Nginx 71 | To configure an HTTPS server, the ssl parameter must be enabled on [listening sockets](http://nginx.org/en/docs/http/ngx_http_core_module.html#listen) in the [server](http://nginx.org/en/docs/http/ngx_http_core_module.html#server) block, 72 | and the locations of the [server certificate](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate) and [private key](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key) files should be specified. 73 | 74 | See the [official documentation](http://nginx.org/en/docs/http/configuring_https_servers.html) on how to configure HTTPS using Nginx. 75 | 76 | ### Enabling SSL with HAProxy 77 | HAProxy Enterprise supports Transport Layer Security (TLS) for encrypting traffic between itself and clients. You have the option of using or not using TLS between HAProxy Enterprise and your backend servers, if you require end-to-end encryption. 78 | 79 | Read more about [Enabling SSL with HAProxy](https://www.haproxy.com/blog/haproxy-ssl-termination/). 80 | -------------------------------------------------------------------------------- /website/versioned_docs/version-0.9.0/_intro.md: -------------------------------------------------------------------------------- 1 | # Skyhook 2 | 3 | [![Build](https://github.com/aerospike/skyhook/actions/workflows/build.yml/badge.svg)](https://github.com/aerospike/skyhook/actions/workflows/build.yml) 4 | 5 | Skyhook is a Redis API-compatible gateway to the [Aerospike](https://www.aerospike.com/) Database. Use Skyhook to quickly get your Redis client applications up and running on an Aerospike cluster. 6 | 7 | ## Overview 8 | 9 | Skyhook is designed as a standalone server application written in Kotlin, which 10 | accepts Redis protocol commands and projects them to an Aerospike cluster using 11 | the Aerospike Java client under the hood. It uses [Netty](https://netty.io/) as 12 | a non-blocking I/O client-server framework. 13 | 14 | This project is now in **beta**. If you're an enterprise customer feel free to 15 | reach out to our support with feedback and feature requests. 16 | We appreciate feedback from the Aerospike community on 17 | [issues](https://github.com/aerospike/skyhook/issues) 18 | related to Skyhook. 19 | 20 | ## Installation 21 | 22 | ### Prerequisites 23 | 24 | - Java 8 or later 25 | - Aerospike Server version 4.9+ 26 | 27 | ### Installing 28 | 29 | Skyhook is distributed as a jar file which may be downloaded from https://github.com/aerospike/skyhook/releases/latest. 30 | 31 | ### Running 32 | 33 | Usage: 34 | 35 | ```text 36 | % java -jar skyhook-[version]-all.jar -h 37 | 38 | Usage: skyhook [-h] [-f=] 39 | Redis to Aerospike proxy server 40 | -f, --config-file= 41 | yaml formatted configuration file 42 | -h, --help display this help and exit 43 | ``` 44 | 45 | To run the server: 46 | 47 | ```sh 48 | java -jar skyhook-[version]-all.jar -f config/server.yml 49 | ``` 50 | 51 | The configuration file carries all the settings the server needs and is in YAML 52 | format. An example configuration file can be found in the [`config`](https://github.com/aerospike/skyhook/blob/a0199da72222984c8417ccaa6e4a02064ed7224b/config/server.yml) folder of this repository. 53 | If no configuration file is specified, the default settings will be applied. 54 | 55 | ```text 56 | [main] INFO c.a.skyhook.SkyhookServer$Companion - Starting the Server... 57 | ``` 58 | 59 | Now the server is listening to the `config.redisPort` (default: 6379) and is ready to serve. 60 | 61 | If you wish to deploy Skyhook as a cluster of nodes, you can find some example configurations [here](https://aerospike.github.io/skyhook/scaling-out). 62 | 63 | ### Configuration Properties 64 | 65 | The default behavior may be customized by setting the following properties in the configuration file: 66 | 67 | | Property name | Description | Default value | 68 | | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | 69 | | hostList | The host list to seed the Aerospike cluster. | localhost:3000 | 70 | | namespace | The Aerospike namespace. | test | 71 | | set | The Aerospike set name. | redis | 72 | | clientPolicy | The Aerospike Java client [ClientPolicy](https://docs.aerospike.com/apidocs/java/com/aerospike/client/policy/ClientPolicy.html) configuration properties. | ClientPolicyConfig | 73 | | bin | The Aerospike value bin name. | b | 74 | | typeBin | The Aerospike value [type](https://redis.io/topics/data-types) bin name. | t | 75 | | redisPort | The server port to bind to. | 6379 | 76 | | workerThreads[1](#worker-threads) | The Netty worker group size. | number of available cores | 77 | | bossThreads | The Netty acceptor group size. | 2 | 78 | 79 | 1 Used to configure the size of the Aerospike Java Client EventLoops as well. 80 | 81 | ## License 82 | 83 | Licensed under an Apache 2.0 License. 84 | 85 | This is an active open source project. You can contribute to it by trying 86 | Skyhook, providing feedback, reporting bugs, and implementing more Redis 87 | commands. 88 | --------------------------------------------------------------------------------