├── 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 | 
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 | [](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 |
--------------------------------------------------------------------------------