├── .docker ├── arm64v8-redi-s.docker └── armhf-redi-s.docker ├── .dockerignore ├── .github └── workflows │ └── swift.yml ├── .gitignore ├── CONTRIBUTORS.txt ├── LICENSE.txt ├── Package.swift ├── README.md └── Sources ├── RedisServer ├── Commands.md ├── Commands │ ├── CommandTable.swift │ ├── CommandType.swift │ ├── ExpirationCommands.swift │ ├── HashCommands.swift │ ├── IntCommands.swift │ ├── KeyCommands.swift │ ├── ListCommands.swift │ ├── PubSubCommands.swift │ ├── RedisCommand.swift │ ├── ServerCommands.swift │ ├── SetCommands.swift │ └── StringCommands.swift ├── Database │ ├── Database.swift │ └── DumpManager.swift ├── Helpers │ ├── RedisLogger.swift │ ├── RedisPattern.swift │ └── Utilities.swift ├── Performance.md ├── README.md ├── Server │ ├── Monitor.swift │ ├── PubSub.swift │ ├── RedisCommandContext.swift │ ├── RedisCommandHandler.swift │ └── RedisServer.swift └── Values │ ├── RedisError.swift │ ├── RedisList.swift │ ├── RedisValue.swift │ └── RedisValueCoding.swift └── redi-s ├── ConfigFile.swift ├── README.md └── main.swift /.docker/arm64v8-redi-s.docker: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | # 3 | # docker run --rm -d --name redi-s helje5/arm64v8-nio-redi-s:latest 4 | # 5 | # Attach w/ new shell 6 | # 7 | # docker exec -it redi-s bash 8 | # 9 | # Hit Redi/S: 10 | # 11 | # redis-benchmark -p 1337 -t SET,GET,RPUSH,INCR -n 500000 -q 12 | # redis-cli -p 1337 13 | # 14 | # To build: 15 | # 16 | # time docker build -t helje5/arm64v8-nio-redi-s:latest -f .docker/arm64v8-redi-s.docker . 17 | # docker push helje5/arm64v8-nio-redi-s:latest 18 | # 19 | 20 | # Build Image 21 | 22 | FROM helje5/arm64v8-swift-dev:4.1.0 AS builder 23 | 24 | LABEL maintainer "Helge Heß " 25 | 26 | ENV DEBIAN_FRONTEND noninteractive 27 | ENV CONFIGURATION release 28 | 29 | USER root 30 | 31 | WORKDIR /src/ 32 | COPY Sources Sources 33 | COPY Package.swift . 34 | 35 | RUN mkdir -p /opt/redi-s/bin 36 | RUN swift build -c ${CONFIGURATION} 37 | RUN cp $(swift build -c ${CONFIGURATION} --show-bin-path)/redi-s \ 38 | /opt/redi-s/bin/ 39 | 40 | 41 | # Deployment Image 42 | 43 | FROM arm64v8/ubuntu:16.04 44 | 45 | LABEL maintainer "Helge Heß " 46 | LABEL description "A 64-bit ARM Redi/S deployment container" 47 | 48 | ENV PORT 1337 49 | 50 | RUN apt-get -q update && apt-get -q -y install \ 51 | libatomic1 libbsd0 libcurl3 libicu55 libxml2 \ 52 | daemontools \ 53 | && rm -r /var/lib/apt/lists/* 54 | 55 | WORKDIR / 56 | 57 | COPY --from=builder /usr/lib/swift/linux/*.so /usr/lib/swift/linux/ 58 | COPY --from=builder /opt/redi-s/bin /opt/redi-s/bin 59 | 60 | EXPOSE $PORT 61 | 62 | WORKDIR /opt/redi-s 63 | 64 | RUN useradd --create-home --shell /bin/bash redi-s 65 | 66 | RUN mkdir -p /opt/redi-s/logs /opt/redi-s/supervise 67 | RUN chown redi-s /opt/redi-s/logs /opt/redi-s/supervise 68 | 69 | RUN bash -c "echo '#!/bin/bash' > run; \ 70 | echo '' >> run; \ 71 | echo echo RUN Started \$\(date\) \>\>logs/run.log >> run; \ 72 | echo '' >> run; \ 73 | echo ./bin/redi-s -p ${PORT} \>\>logs/run.log 2\>\>logs/error.log >> run; \ 74 | echo '' >> run; \ 75 | echo echo RUN Finished \$\(date\) \>\>logs/run.log >> run; \ 76 | echo echo RUN ------------------- \>\>logs/run.log >> run; \ 77 | chmod +x run" 78 | 79 | USER redi-s 80 | 81 | CMD ["supervise", "/opt/redi-s"] 82 | -------------------------------------------------------------------------------- /.docker/armhf-redi-s.docker: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | # 3 | # docker run --rm -d --name redi-s helje5/rpi-nio-redi-s:latest 4 | # 5 | # Attach w/ new shell 6 | # 7 | # docker exec -it redi-s bash 8 | # 9 | # To build: 10 | # 11 | # time docker build -t helje5/rpi-nio-redi-s:latest -f .docker/armhf-redi-s.docker . 12 | # docker push helje5/rpi-nio-redi-s:latest 13 | # 14 | 15 | # Build Image 16 | 17 | FROM helje5/rpi-swift-dev:4.1.0 AS builder 18 | 19 | LABEL maintainer "Helge Heß " 20 | 21 | ENV DEBIAN_FRONTEND noninteractive 22 | 23 | # Release crashes compiler 24 | # - https://github.com/NozeIO/swift-nio-irc/issues/2 25 | ENV CONFIGURATION debug 26 | 27 | USER root 28 | 29 | WORKDIR /src/ 30 | COPY Sources Sources 31 | COPY Package.swift . 32 | 33 | RUN mkdir -p /opt/redi-s/bin 34 | RUN swift build -c ${CONFIGURATION} 35 | RUN cp $(swift build -c ${CONFIGURATION} --show-bin-path)/redi-s \ 36 | /opt/redi-s/bin/ 37 | 38 | 39 | # Deployment Image 40 | 41 | FROM ioft/armhf-ubuntu:16.04 42 | 43 | LABEL maintainer "Helge Heß " 44 | LABEL description "A ARMhf Redi/S deployment container" 45 | 46 | RUN apt-get -q update && apt-get -q -y install \ 47 | libatomic1 libbsd0 libcurl3 libicu55 libxml2 \ 48 | daemontools \ 49 | && rm -r /var/lib/apt/lists/* 50 | 51 | WORKDIR / 52 | 53 | COPY --from=builder /usr/lib/swift/linux/*.so /usr/lib/swift/linux/ 54 | COPY --from=builder /opt/redi-s/bin /opt/redi-s/bin 55 | 56 | EXPOSE 1337 57 | 58 | RUN useradd --create-home --shell /bin/bash redi-s 59 | 60 | RUN mkdir -p /opt/redi-s/logs /opt/redi-s/supervise 61 | RUN chown redi-s /opt/redi-s/logs /opt/redi-s/supervise 62 | 63 | RUN bash -c "echo '#!/bin/bash' > run; \ 64 | echo '' >> run; \ 65 | echo echo RUN Started \$\(date\) \>\>logs/run.log >> run; \ 66 | echo '' >> run; \ 67 | echo ./bin/redi-s \>\>logs/run.log 2\>\>logs/error.log >> run; \ 68 | echo '' >> run; \ 69 | echo echo RUN Finished \$\(date\) \>\>logs/run.log >> run; \ 70 | echo echo RUN ------------------- \>\>logs/run.log >> run; \ 71 | chmod +x run" 72 | 73 | USER redi-s 74 | 75 | CMD ["supervise", "/opt/redi-s"] 76 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .build* 3 | *.xcodeproj 4 | Package.resolved 5 | *.json 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "52 10 * * 1" 8 | 9 | jobs: 10 | linux: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | image: 16 | - swift:5.6.1-bionic 17 | - swift:5.9.2-focal 18 | container: ${{ matrix.image }} 19 | steps: 20 | - name: Checkout Repository 21 | uses: actions/checkout@v3 22 | - name: Build Swift Debug Package 23 | run: swift build -c debug 24 | - name: Build Swift Release Package 25 | run: swift build -c release 26 | nextstep: 27 | runs-on: macos-13 28 | steps: 29 | - name: Select latest available Xcode 30 | uses: maxim-lobanov/setup-xcode@v1 31 | with: 32 | xcode-version: '~15.0' 33 | - name: Checkout Repository 34 | uses: actions/checkout@v3 35 | - name: Build Swift Debug Package 36 | run: swift build -c debug 37 | - name: Build Swift Release Package 38 | run: swift build -c release 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | 69 | *.xcodeproj 70 | .build-linux* 71 | Package.resolved 72 | dump*.json 73 | .swiftpm 74 | 75 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Helge Hess 2 | 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 ZeeZide GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "redi-s", 7 | products: [ 8 | .library (name: "RedisServer", targets: [ "RedisServer" ]), 9 | .executable(name: "redi-s", targets: [ "redi-s" ]), 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/SwiftNIOExtras/swift-nio-redis.git", 13 | from: "0.11.0"), 14 | .package(url: "https://github.com/apple/swift-atomics", from: "1.0.0") 15 | ], 16 | targets: [ 17 | .target(name: "RedisServer", dependencies: [ "NIORedis", "Atomics" ]), 18 | .target(name: "redi-s", dependencies: [ "RedisServer" ]) 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Redi/S 2 | 4 |

5 | 6 | ![Swift4](https://img.shields.io/badge/swift-4-blue.svg) 7 | ![macOS](https://img.shields.io/badge/os-macOS-green.svg?style=flat) 8 | ![tuxOS](https://img.shields.io/badge/os-tuxOS-green.svg?style=flat) 9 | ![Travis](https://travis-ci.org/NozeIO/redi-s.svg?branch=develop) 10 | 11 | Redi/S is a **Redis server implementation** in the 12 | [Swift](https://swift.org) 13 | programming language. 14 | Based on Apple's 15 | [SwiftNIO](https://github.com/apple/swift-nio) 16 | framework. 17 | 18 | What is [Redis](https://redis.io)? Checkout the home page, 19 | but it is an easy to use and very popular Key-Value Store, 20 | w/ PubSub functionality. 21 | 22 | It is not meant to replace the C based [Redis](https://redis.io) server, 23 | but the goal is to make it feature complete and well performing. 24 | 25 | Use cases: 26 | 27 | - Testing of server apps (pull up a Redi/S within your Xcode just for testing). 28 | - As an "embedded" database. Redi/S comes as a regular Swift package you can 29 | directly embed into your own server or application. 30 | - Easy to extend in a safy language. 31 | 32 | 33 | ## Supported Commands 34 | 35 | Redi/S supports a lot, including PubSub and monitoring.
36 | Redi/S supports a lot *not*, including transactions or HyperLogLogs. 37 | 38 | There is a [list of supported commands](Sources/RedisServer/Commands.md). 39 | 40 | Contributions welcome!! A lot of the missing stuff is really easy to add! 41 | 42 | 43 | ## Performance 44 | 45 | Performance differs, e.g. lists are implemented using arrays (hence RPUSH is 46 | okayish, LPUSH is very slow). 47 | But looking at just the simple GET/SET, it is surprisingly close to the 48 | highly optimized C implementation. 49 | 50 | ### 2024-01-30 Swift 5.9.2 51 | 52 | Redi/S (1 NIO thread on M1 Mac Mini): 53 | ``` 54 | helge@M1ni ~ $ redis-benchmark -p 1337 -t SET,GET,RPUSH,INCR -n 500000 -q 55 | WARNING: Could not fetch server CONFIG 56 | SET: 163345.31 requests per second, p50=0.255 msec 57 | GET: 167336.02 requests per second, p50=0.239 msec 58 | INCR: 158780.56 requests per second, p50=0.239 msec 59 | RPUSH: 157480.31 requests per second, p50=0.271 msec 60 | ``` 61 | 62 | Note that more threads end up being worse. Not entirely sure why. 63 | 64 | ### Those Are Older Numbers from 2018 65 | 66 | - using Swift 4.2 on Intel, IIRC 67 | 68 | Redi/S (2 NIO threads on MacPro 3,7 GHz Quad-Core Intel Xeon E5): 69 | ``` 70 | helge@ZeaPro ~ $ redis-benchmark -p 1337 -t SET,GET,RPUSH,INCR -n 500000 -q 71 | SET: 48003.07 requests per second 72 | GET: 48459.00 requests per second 73 | INCR: 43890.45 requests per second 74 | RPUSH: 46087.20 requests per second 75 | ``` 76 | 77 | Redis 4.0.8 (same MacPro 3,7 GHz Quad-Core Intel Xeon E5): 78 | ``` 79 | helge@ZeaPro ~ $ redis-benchmark -t SET,GET,RPUSH,INCR -n 500000 -q 80 | SET: 54884.74 requests per second 81 | GET: 54442.51 requests per second 82 | INCR: 54692.62 requests per second 83 | RPUSH: 54013.18 requests per second 84 | ``` 85 | 86 | Redi/S on RaspberryPi 3B+ 87 | ``` 88 | $ redis-benchmark -h zpi3b.local -p 1337 -t SET,GET,RPUSH,INCR -n 50000 -q 89 | SET: 4119.29 requests per second 90 | GET: 5056.12 requests per second 91 | INCR: 3882.59 requests per second 92 | RPUSH: 3872.07 requests per second 93 | ``` 94 | 95 | There are [Performance notes](Sources/RedisServer/Performance.md), 96 | looking at the specific NIO implementation of Redi/S. 97 | 98 | Persistence is really inefficient, 99 | the databases are just dumped as JSON via Codable. 100 | Easy to fix. 101 | 102 | 103 | ## How to run 104 | 105 | ``` 106 | $ swift build -c release 107 | $ .build/release/redi-s 108 | 2383:M 11 Apr 17:04:16.296 # sSZSsSZSsSZSs Redi/S is starting sSZSsSZSsSZSs 109 | 2383:M 11 Apr 17:04:16.302 # Redi/S bits=64, pid=2383, just started 110 | 2383:M 11 Apr 17:04:16.303 # Configuration loaded 111 | ____ _ _ ______ 112 | | _ \ ___ __| (_) / / ___| Redi/S 64 bit 113 | | |_) / _ \/ _` | | / /\___ \ 114 | | _ < __/ (_| | |/ / ___) | Port: 1337 115 | |_| \_\___|\__,_|_/_/ |____/ PID: 2383 116 | 117 | 2383:M 11 Apr 17:04:16.304 # Server initialized 118 | 2383:M 11 Apr 17:04:16.305 * Ready to accept connections 119 | ``` 120 | 121 | ## Status 122 | 123 | There are a few inefficiencies, the worst being the persistent storage. 124 | Yet generally this seems to work fine. 125 | 126 | The implementation has grown a bit and could use a little refactoring, 127 | specially the database dump parts. 128 | 129 | 130 | ## Playing with the Server 131 | 132 | You'd like to play with this, but never used Redis before? 133 | OK, a small tutorial on what you can do with it. 134 | 135 | First make sure the server runs in one shell: 136 | ``` 137 | $ swift build -c release 138 | $ .build/release/redi-s 139 | 83904:M 12 Apr 16:33:15.159 # sSZSsSZSsSZSs Redi/S is starting sSZSsSZSsSZSs 140 | 83904:M 12 Apr 16:33:15.169 # Redi/S bits=64, pid=83904, just started 141 | 83904:M 12 Apr 16:33:15.170 # Configuration loaded 142 | ____ _ _ ______ 143 | | _ \ ___ __| (_) / / ___| Redi/S 64 bit 144 | | |_) / _ \/ _` | | / /\___ \ 145 | | _ < __/ (_| | |/ / ___) | Port: 1337 146 | |_| \_\___|\__,_|_/_/ |____/ PID: 83904 147 | 148 | 83904:M 12 Apr 16:33:15.176 # Server initialized 149 | 83904:M 12 Apr 16:33:15.178 * Ready to accept connections 150 | ``` 151 | 152 | Notice how the server says: "Port 1337". This is the port the server is running 153 | on. 154 | 155 | ### Via telnet/netcat 156 | 157 | You can directly connect to the server and issue Redis commands (the server 158 | is then running the connection in `telnet mode`, which is different to the 159 | regular RESP protocol): 160 | 161 | ``` 162 | $ nc localhost 1337 163 | KEYS * 164 | *0 165 | SET theanswer 42 166 | +OK 167 | GET theanswer 168 | $2 169 | 42 170 | ``` 171 | 172 | Redis is a key/value store. That is, it acts like a big Dictionary that 173 | can be modified from multiple processes. Above we list the available 174 | `KEYS`, then we set the key `theanswer` to the value 42, and retrieve it. 175 | (Redis provides [great documentation](https://redis.io/commands) 176 | on the available commands, Redi/S implements many, but not all of them). 177 | 178 | ### Via redis-cli 179 | 180 | Redis provides a tool called `redis-cli`, which is a much more convenient 181 | way to access the server. 182 | On macOS you can install that using `brew install redis` (which also gives 183 | you the real server), 184 | on Ubuntu you can grab it via `sudo apt-get install redis-tools`. 185 | 186 | The same thing we did in `telnet` above: 187 | 188 | ``` 189 | $ redis-cli -p 1337 190 | 127.0.0.1:1337> KEYS * 191 | 1) "theanswer" 192 | 127.0.0.1:1337> SET theanswer 42 193 | OK 194 | 127.0.0.1:1337> GET theanswer 195 | "42" 196 | ``` 197 | 198 | ### Key Expiration 199 | 200 | Redis is particularily useful for HTTP session stores, and for caches. 201 | When setting a key, you can set an "expiration" (in seconds, milliseconds, 202 | or Unix timestamps): 203 | 204 | ``` 205 | 127.0.0.1:1337> EXPIRE theanswer 10 206 | (integer) 1 207 | 127.0.0.1:1337> TTL theanswer 208 | (integer) 6 209 | 127.0.0.1:1337> GET theanswer 210 | "42" 211 | 127.0.0.1:1337> TTL theanswer 212 | (integer) -2 213 | 127.0.0.1:1337> GET theanswer 214 | (nil) 215 | ``` 216 | 217 | We are using "strings" here. In Redis "strings" are actually "Data" objects, 218 | i.e. binary arrays of bytes (this is even true for bytes!). 219 | For example in a web application, you could use the "session-id" you generate, 220 | serialize your session into a Data object, and then store it like 221 | `SET session-id TTL 600`. 222 | 223 | ### Key Generation 224 | 225 | But how do we generate keys (e.g. session-ids) in a distributed setting? 226 | As usual there are many ways to do this. 227 | For example you could use a Redis integer key which provides atomic increment 228 | and decrement operations: 229 | 230 | ``` 231 | 127.0.0.1:1337> SET idsequence 0 232 | OK 233 | 127.0.0.1:1337> INCR idsequence 234 | (integer) 1 235 | 127.0.0.1:1337> INCR idsequence 236 | (integer) 2 237 | ``` 238 | 239 | Or if you generate keys on the client side, you can validate that they are 240 | unique using [SETNX](https://redis.io/commands/setnx). For example: 241 | 242 | ``` 243 | 127.0.0.1:1337> SETNX mykey 10 244 | (integer) 1 245 | ``` 246 | 247 | And another client will get 248 | 249 | ``` 250 | 127.0.0.1:1337> SETNX mykey 10 251 | (integer) 0 252 | ``` 253 | 254 | ### Simple Lists 255 | 256 | Redis cannot only store string (read: Data) values, it can also store 257 | lists, sets and hashes (dictionaries). 258 | As well as some other datatypes: 259 | [Data Types Intro](https://redis.io/topics/data-types-intro). 260 | 261 | ``` 262 | 127.0.0.1:1337> RPUSH chatchannel "Hi guys!" 263 | (integer) 1 264 | 127.0.0.1:1337> RPUSH chatchannel "How is it going?" 265 | (integer) 2 266 | 127.0.0.1:1337> LLEN chatchannel 267 | (integer) 2 268 | 127.0.0.1:1337> LRANGE chatchannel 0 -1 269 | 1) "Hi guys!" 270 | 2) "How is it going?" 271 | 127.0.0.1:1337> RPOP chatchannel 272 | "How is it going?" 273 | 127.0.0.1:1337> RPOP chatchannel 274 | "Hi guys!" 275 | 127.0.0.1:1337> RPOP chatchannel 276 | (nil) 277 | ``` 278 | 279 | ### Monitoring 280 | 281 | Assume you want to debug what's going on on your Redis server. 282 | You can do this by connecting w/ a fresh client and put that into 283 | "monitoring" mode. The Redis server will echo all commands it receives 284 | to that monitor: 285 | 286 | ``` 287 | $ redis-cli -p 1337 288 | 127.0.0.1:1337> MONITOR 289 | OK 290 | ``` 291 | 292 | Some other client: 293 | 294 | ``` 295 | 127.0.0.1:1337> hmset user:1000 username antirez birthyear 1976 verified 1 296 | OK 297 | 127.0.0.1:1337> hmget user:1000 username verified 298 | 1) "antirez" 299 | 2) "1" 300 | ``` 301 | 302 | The monitor will print: 303 | 304 | ``` 305 | 1523545069.071390 [0 127.0.0.1:60904] "hmset" "user:1000" "username" "antirez" "birthyear" "1976" "verified" "1" 306 | 1523545087.016070 [0 127.0.0.1:60904] "hmget" "user:1000" "username" "verified" 307 | ``` 308 | 309 | ### Publish/Subscribe 310 | 311 | Redis includes a simple publish/subscribe server. 312 | Any numbers of clients can subscribe to any numbers of channels. 313 | Other clients can then push "messages" to a channel, and all 314 | subscribed clients will receive them. 315 | 316 | One client: 317 | ``` 318 | 127.0.0.1:1337> PSUBSCRIBE thermostats* 319 | Reading messages... (press Ctrl-C to quit) 320 | 1) psubscribe 321 | 2) "thermostats*" 322 | 3) (integer) 1 323 | ``` 324 | 325 | Another client (the reply contains the number of consumers): 326 | 327 | ``` 328 | 127.0.0.1:1337> PUBLISH thermostats:kitchen "temperature set to 42℃" 329 | (integer) 1 330 | ``` 331 | 332 | The subscribed client will get: 333 | ``` 334 | 1) message 335 | 2) "thermostats:kitchen" 336 | 3) "temperatur set to 4242℃" 337 | ``` 338 | 339 | > Note: PubSub is separate to the key-value store. You cannot watch keys using 340 | > that! (there are blocking list operations for producer/consumer scenarios, 341 | > but those are not yet supported by Redi/S) 342 | 343 | 344 | ### Benchmarking 345 | 346 | Redis tools also include a tool called `redis-benchmark` which can be, 347 | similar to `apache-bench` or `wrk` be used to measure the server performance. 348 | 349 | For example, to exercise the server with half a million SET, GET, RPUSH and INCR 350 | requests each: 351 | 352 | ``` 353 | $ redis-benchmark -p 1337 -t SET,GET,RPUSH,INCR -n 500000 -q 354 | SET: 43192.81 requests per second 355 | GET: 46253.47 requests per second 356 | INCR: 38952.95 requests per second 357 | RPUSH: 39305.09 requests per second 358 | ``` 359 | 360 | 361 | ## Who 362 | 363 | Brought to you by 364 | [ZeeZide](http://zeezide.de). 365 | We like 366 | [feedback](https://twitter.com/ar_institute), 367 | GitHub stars, 368 | cool [contract work](http://zeezide.com/en/services/services.html), 369 | presumably any form of praise you can think of. 370 | 371 | There is a `#swift-nio` channel on the 372 | [swift-server Slack](https://t.co/W1vfsb9JAB). 373 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands.md: -------------------------------------------------------------------------------- 1 | # Redi/S - Supported Commands 2 | 3 | [Redis Commands(https://redis.io/commands/) 4 | 5 | - [ ] [ASKING](https://redis.io/commands/asking) 6 | - [ ] [AUTH](https://redis.io/commands/auth) 7 | - [ ] [BGREWRITEAOF](https://redis.io/commands/bgrewriteaof) 8 | - [x] [BGSAVE](https://redis.io/commands/bgsave) 9 | - [ ] [CLIENT](https://redis.io/commands/client) 10 | - [x] CLIENT LIST 11 | - [ ] CLIENT KILL 12 | - [x] CLIENT GETNAME 13 | - [x] CLIENT SETNAME 14 | - [ ] CLIENT PAUSE 15 | - [ ] CLIENT REPLY 16 | - [ ] [CLUSTER](https://redis.io/commands/cluster) 17 | - [x] [COMMAND](https://redis.io/commands/command) 18 | - [x] COMMAND COUNT 19 | - [ ] COMMAND GETKEYS 20 | - [ ] COMMAND INFO 21 | - [ ] [CONFIG](https://redis.io/commands/config) 22 | - [x] [DBSIZE](https://redis.io/commands/dbsize) 23 | - [ ] [DEBUG](https://redis.io/commands/debug) 24 | - [x] [DEL](https://redis.io/commands/del) 25 | - [ ] [DISCARD](https://redis.io/commands/discard) 26 | - [ ] [DUMP](https://redis.io/commands/dump) 27 | - [x] [ECHO](https://redis.io/commands/echo) 28 | - [ ] [EVAL](https://redis.io/commands/eval) 29 | - [ ] [EVALSHA](https://redis.io/commands/evalsha) 30 | - [ ] [EXEC](https://redis.io/commands/exec) 31 | - [x] [EXISTS](https://redis.io/commands/exists) 32 | - [x] [EXPIRE](https://redis.io/commands/expire) 33 | - [x] [EXPIREAT](https://redis.io/commands/expireat) 34 | - [ ] [FLUSHALL](https://redis.io/commands/flushall) 35 | - [ ] [FLUSHDB](https://redis.io/commands/flushdb) 36 | - [ ] HOST 37 | - [ ] [INFO](https://redis.io/commands/info) 38 | - [x] [KEYS](https://redis.io/commands/keys) 39 | - [x] [LASTSAVE](https://redis.io/commands/lastsave) 40 | - [ ] [LATENCY](https://redis.io/commands/latency) 41 | - [ ] [MEMORY](https://redis.io/commands/memory) 42 | - [ ] [MIGRATE](https://redis.io/commands/migrate) 43 | - [ ] [MODULE](https://redis.io/commands/module) 44 | - [x] [MONITOR](https://redis.io/commands/monitor) 45 | - [ ] [MOVE](https://redis.io/commands/move) 46 | - [ ] [MULTI](https://redis.io/commands/multi) 47 | - [ ] [OBJECT](https://redis.io/commands/object) 48 | - [ ] [PERSIST](https://redis.io/commands/persist) 49 | - [x] [PEXPIRE](https://redis.io/commands/pexpire) 50 | - [x] [PEXPIREAT](https://redis.io/commands/pexpireat) 51 | - [x] [PING](https://redis.io/commands/ping) 52 | - [ ] [POST](https://redis.io/commands/post) 53 | - [ ] [PSYNC](https://redis.io/commands/psync) 54 | - [x] [PTTL](https://redis.io/commands/pttl) 55 | - [x] [QUIT](https://redis.io/commands/quit) 56 | - [ ] [RANDOMKEY](https://redis.io/commands/randomkey) 57 | - [ ] [READONLY](https://redis.io/commands/readonly) 58 | - [ ] [READWRITE](https://redis.io/commands/readwrite) 59 | - [x] [RENAME](https://redis.io/commands/rename) 60 | - [x] [RENAMENX](https://redis.io/commands/renamenx) 61 | - [ ] [REPLCONF](https://redis.io/commands/replconf) 62 | - [ ] [RESTORE](https://redis.io/commands/restore) 63 | - [ ] [ROLE](https://redis.io/commands/role) 64 | - [x] [SAVE](https://redis.io/commands/save) 65 | - [ ] [SCAN](https://redis.io/commands/scan) 66 | - [ ] [SCRIPT](https://redis.io/commands/script) 67 | - [x] [SELECT](https://redis.io/commands/select) 68 | - [ ] [SHUTDOWN](https://redis.io/commands/shutdown) 69 | - [ ] [SLAVEOF](https://redis.io/commands/slaveof) 70 | - [ ] [SLOWLOG](https://redis.io/commands/slowlog) 71 | - [ ] [SORT](https://redis.io/commands/sort) 72 | - [x] [SWAPDB](https://redis.io/commands/swapdb) 73 | - [ ] [SYNC](https://redis.io/commands/sync) 74 | - [ ] [TIME](https://redis.io/commands/time) 75 | - [ ] [TOUCH](https://redis.io/commands/touch) 76 | - [x] [TTL](https://redis.io/commands/ttl) 77 | - [x] [TYPE](https://redis.io/commands/type) 78 | - [ ] [UNLINK](https://redis.io/commands/unlink) 79 | - [ ] [UNWATCH](https://redis.io/commands/unwatch) 80 | - [ ] [WAIT](https://redis.io/commands/wait) 81 | - [ ] [WATCH](https://redis.io/commands/watch) 82 | - PubSub 83 | - [x] [PSUBSCRIBE](https://redis.io/commands/psubscribe) 84 | - [x] [PUBLISH](https://redis.io/commands/publish) 85 | - [x] [PUBSUB](https://redis.io/commands/pubsub) 86 | - [x] [PUNSUBSCRIBE](https://redis.io/commands/punsubscribe) 87 | - [x] [SUBSCRIBE](https://redis.io/commands/subscribe) 88 | - [x] [UNSUBSCRIBE](https://redis.io/commands/unsubscribe) 89 | - Strings (aka Data) 90 | - [x] [APPEND](https://redis.io/commands/append) 91 | - [x] [DECR](https://redis.io/commands/decr) 92 | - [x] [DECRBY](https://redis.io/commands/decrby) 93 | - [x] [GET](https://redis.io/commands/get) 94 | - [x] [GETRANGE](https://redis.io/commands/getrange) 95 | - [x] [GETSET](https://redis.io/commands/getset) 96 | - [x] [INCR](https://redis.io/commands/incr) 97 | - [x] [INCRBY](https://redis.io/commands/incrby) 98 | - [ ] [INCRBYFLOAT](https://redis.io/commands/incrbyfloat) 99 | - [x] [MGET](https://redis.io/commands/mget) 100 | - [x] [MSET](https://redis.io/commands/mset) 101 | - [x] [MSETNX](https://redis.io/commands/msetnx) 102 | - [x] [PSETEX](https://redis.io/commands/psetex) 103 | - [x] [SET](https://redis.io/commands/set) 104 | - [x] [SETEX](https://redis.io/commands/setex) 105 | - [x] [SETNX](https://redis.io/commands/setnx) 106 | - [x] [SETRANGE](https://redis.io/commands/setrange) 107 | - [x] [STRLEN](https://redis.io/commands/strlen) 108 | - [x] [SUBSTR](https://redis.io/commands/substr) 109 | - Lists 110 | - [ ] [BLPOP](https://redis.io/commands/blpop) 111 | - [ ] [BRPOP](https://redis.io/commands/brpop) 112 | - [ ] [BRPOPLPUSH](https://redis.io/commands/brpoplpush) 113 | - [x] [LINDEX](https://redis.io/commands/lindex) 114 | - [ ] [LINSERT](https://redis.io/commands/linsert) 115 | - [x] [LLEN](https://redis.io/commands/llen) 116 | - [x] [LPOP](https://redis.io/commands/lpop) 117 | - [x] [LPUSH](https://redis.io/commands/lpush) 118 | - [x] [LPUSHX](https://redis.io/commands/lpushx) 119 | - [x] [LRANGE](https://redis.io/commands/lrange) 120 | - [ ] [LREM](https://redis.io/commands/lrem) 121 | - [x] [LSET](https://redis.io/commands/lset) 122 | - [ ] [LTRIM](https://redis.io/commands/ltrim) 123 | - [x] [RPOP](https://redis.io/commands/rpop) 124 | - [ ] [RPOPLPUSH](https://redis.io/commands/rpoplpush) 125 | - [x] [RPUSH](https://redis.io/commands/rpush) 126 | - [x] [RPUSHX](https://redis.io/commands/rpushx) 127 | - Hashes 128 | - [x] [HDEL](https://redis.io/commands/hdel) 129 | - [x] [HEXISTS](https://redis.io/commands/hexists) 130 | - [x] [HGET](https://redis.io/commands/hget) 131 | - [x] [HGETALL](https://redis.io/commands/hgetall) 132 | - [x] [HINCRBY](https://redis.io/commands/hincrby) 133 | - [ ] [HINCRBYFLOAT](https://redis.io/commands/hincrbyfloat) 134 | - [x] [HKEYS](https://redis.io/commands/hkeys) 135 | - [x] [HLEN](https://redis.io/commands/hlen) 136 | - [x] [HMGET](https://redis.io/commands/hmget) 137 | - [x] [HMSET](https://redis.io/commands/hmset) 138 | - [ ] [HSCAN](https://redis.io/commands/hscan) 139 | - [x] [HSET](https://redis.io/commands/hset) 140 | - [x] [HSETNX](https://redis.io/commands/hsetnx) 141 | - [x] [HSTRLEN](https://redis.io/commands/hstrlen) 142 | - [x] [HVALS](https://redis.io/commands/hvals) 143 | - Sets 144 | - [x] [SADD](https://redis.io/commands/sadd) 145 | - [x] [SCARD](https://redis.io/commands/scard) 146 | - [x] [SDIFF](https://redis.io/commands/sdiff) 147 | - [x] [SDIFFSTORE](https://redis.io/commands/sdiffstore) 148 | - [x] [SINTER](https://redis.io/commands/sinter) 149 | - [x] [SINTERSTORE](https://redis.io/commands/sinterstore) 150 | - [x] [SISMEMBER](https://redis.io/commands/sismember) 151 | - [x] [SMEMBERS](https://redis.io/commands/smembers) 152 | - [ ] [SMOVE](https://redis.io/commands/smove) 153 | - [ ] [SPOP](https://redis.io/commands/spop) 154 | - [ ] [SRANDMEMBER](https://redis.io/commands/srandmember) 155 | - [x] [SREM](https://redis.io/commands/srem) 156 | - [ ] [SSCAN](https://redis.io/commands/sscan) 157 | - [x] [SUNION](https://redis.io/commands/sunion) 158 | - [x] [SUNIONSTORE](https://redis.io/commands/sunionstore) 159 | - Sorted Sets 160 | - [ ] [ZADD](https://redis.io/commands/zadd) 161 | - [ ] [ZCARD](https://redis.io/commands/zcard) 162 | - [ ] [ZCOUNT](https://redis.io/commands/zcount) 163 | - [ ] [ZINCRBY](https://redis.io/commands/zincrby) 164 | - [ ] [ZINTERSTORE](https://redis.io/commands/zinterstore) 165 | - [ ] [ZLEXCOUNT](https://redis.io/commands/zlexcount) 166 | - [ ] [ZRANGE](https://redis.io/commands/zrange) 167 | - [ ] [ZRANGEBYLEX](https://redis.io/commands/zrangebylex) 168 | - [ ] [ZRANGEBYSCORE](https://redis.io/commands/zrangebyscore) 169 | - [ ] [ZRANK](https://redis.io/commands/zrank) 170 | - [ ] [ZREM](https://redis.io/commands/zrem) 171 | - [ ] [ZREMRANGEBYLEX](https://redis.io/commands/zremrangebylex) 172 | - [ ] [ZREMRANGEBYRANK](https://redis.io/commands/zremrangebyrank) 173 | - [ ] [ZREMRANGEBYSCORE](https://redis.io/commands/zremrangebyscore) 174 | - [ ] [ZREVRANGE](https://redis.io/commands/zrevrange) 175 | - [ ] [ZREVRANGEBYLEX](https://redis.io/commands/zrevrangebylex) 176 | - [ ] [ZREVRANGEBYSCORE](https://redis.io/commands/zrevrangebyscore) 177 | - [ ] [ZREVRANK](https://redis.io/commands/zrevrank) 178 | - [ ] [ZSCAN](https://redis.io/commands/zscan) 179 | - [ ] [ZSCORE](https://redis.io/commands/zscore) 180 | - [ ] [ZUNIONSTORE](https://redis.io/commands/zunionstore) 181 | - Bit Fields 182 | - [ ] [GETBIT](https://redis.io/commands/getbit) 183 | - [ ] [BITCOUNT](https://redis.io/commands/bitcount) 184 | - [ ] [BITFIELD](https://redis.io/commands/bitfield) 185 | - [ ] [BITOP](https://redis.io/commands/bitop) 186 | - [ ] [BITPOS](https://redis.io/commands/bitpos) 187 | - [ ] [SETBIT](https://redis.io/commands/setbit) 188 | - HyperLogLog 189 | - [ ] [PFADD](https://redis.io/commands/pfadd) 190 | - [ ] [PFCOUNT](https://redis.io/commands/pfcount) 191 | - [ ] [PFDEBUG](https://redis.io/commands/pfdebug) 192 | - [ ] [PFMERGE](https://redis.io/commands/pfmerge) 193 | - [ ] [PFSELFTEST](https://redis.io/commands/pfselftest) 194 | - Geo 195 | - [ ] [GEOADD](https://redis.io/commands/geoadd) 196 | - [ ] [GEODIST](https://redis.io/commands/geodist) 197 | - [ ] [GEOHASH](https://redis.io/commands/geohash) 198 | - [ ] [GEOPOS](https://redis.io/commands/geopos) 199 | - [ ] [GEORADIUS](https://redis.io/commands/georadius) 200 | - [ ] [GEORADIUSBYMEMBER](https://redis.io/commands/georadiusbymember) 201 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/CommandTable.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import NIO 17 | import NIORedis 18 | 19 | public typealias RedisCommandTable = [ RedisCommand ] 20 | 21 | extension RedisServer { 22 | 23 | static let defaultCommandTable : RedisCommandTable = [ 24 | // command is funny in that arity is 0 25 | Command(name : "COMMAND", 26 | type : .optionalValue(Commands.COMMAND), // FIXME: multivalue! 27 | flags : [ .loading, .stale ]), 28 | 29 | Command(name : "PING", 30 | type : .optionalValue(Commands.PING), 31 | flags : [ .stale, .fast ]), 32 | Command(name : "ECHO", 33 | type : .singleValue(Commands.PING), 34 | flags : [ .stale, .fast ]), 35 | 36 | Command(name : "QUIT", 37 | type : .noArguments(Commands.QUIT), 38 | flags : [ .admin ]), 39 | Command(name : "MONITOR", 40 | type : .noArguments(Commands.MONITOR), 41 | flags : [ .admin ]), 42 | Command(name : "SAVE", 43 | type : .noArguments(Commands.SAVE), 44 | flags : [ .admin ]), 45 | Command(name : "BGSAVE", 46 | type : .noArguments(Commands.SAVE), 47 | flags : [ .admin ]), 48 | Command(name : "LASTSAVE", 49 | type : .noArguments(Commands.LASTSAVE), 50 | flags : [ .admin ]), 51 | Command(name : "CLIENT", 52 | type : .oneOrMoreValues(Commands.CLIENT), 53 | flags : [ .admin, .noscript ]), 54 | 55 | Command(name : "PUBLISH", 56 | type : .keyValue(Commands.PUBLISH), 57 | flags : [ .pubsub, .loading, .stale, .fast ]), 58 | Command(name : "SUBSCRIBE", 59 | type : .keys(Commands.SUBSCRIBE), 60 | flags : [ .pubsub, .noscript, .loading, .stale ]), 61 | Command(name : "UNSUBSCRIBE", 62 | type : .keys(Commands.UNSUBSCRIBE), 63 | flags : [ .pubsub, .noscript, .loading, .stale ]), 64 | Command(name : "PSUBSCRIBE", 65 | type : .oneOrMoreValues(Commands.PSUBSCRIBE), 66 | flags : [ .pubsub, .noscript, .loading, .stale ]), 67 | Command(name : "PUNSUBSCRIBE", 68 | type : .oneOrMoreValues(Commands.PUNSUBSCRIBE), 69 | flags : [ .pubsub, .noscript, .loading, .stale ]), 70 | Command(name : "PUBSUB", 71 | type : .oneOrMoreValues(Commands.PUBSUB), 72 | flags : [ .pubsub, .random, .loading, .stale ]), 73 | 74 | Command(name : "SELECT", 75 | type : .singleValue(Commands.SELECT), 76 | flags : [ .loading, .fast ]), 77 | Command(name : "SWAPDB", 78 | type : .valueValue(Commands.SWAPDB), 79 | flags : [ .write, .fast ]), 80 | 81 | // MARK: - Generic Commands 82 | 83 | Command(name : "DBSIZE", 84 | type : .noArguments(Commands.DBSIZE), 85 | flags : [ .readonly, .fast ]), 86 | Command(name : "KEYS", 87 | type : .singleValue(Commands.KEYS), 88 | flags : [ .readonly, .sortForScript ]), 89 | Command(name : "DEL", 90 | type : .keys(Commands.DEL), 91 | flags : [ .write ]), 92 | Command(name : "EXISTS", 93 | type : .keys(Commands.EXISTS), 94 | flags : [ .readonly, .fast ]), 95 | Command(name : "TYPE", 96 | type : .key(Commands.TYPE), 97 | flags : [ .readonly, .fast ]), 98 | Command(name : "RENAME", 99 | type : .keyKey(Commands.RENAME), 100 | flags : [ .write ]), 101 | Command(name : "RENAMENX", 102 | type : .keyKey(Commands.RENAMENX), 103 | flags : [ .write ]), 104 | 105 | // MARK: - Expiration Commands 106 | 107 | Command(name : "PERSIST", 108 | type : .key(Commands.PERSIST), 109 | flags : [ .write, .fast ]), 110 | Command(name : "EXPIRE", 111 | type : .keyValue(Commands.EXPIRE), 112 | flags : [ .write, .fast ]), 113 | Command(name : "PEXPIRE", 114 | type : .keyValue(Commands.EXPIRE), 115 | flags : [ .write, .fast ]), 116 | Command(name : "EXPIREAT", 117 | type : .keyValue(Commands.EXPIRE), 118 | flags : [ .write, .fast ]), 119 | Command(name : "PEXPIREAT", 120 | type : .keyValue(Commands.EXPIRE), 121 | flags : [ .write, .fast ]), 122 | Command(name : "TTL", 123 | type : .key(Commands.TTL), 124 | flags : [ .readonly, .fast ]), 125 | Command(name : "PTTL", 126 | type : .key(Commands.TTL), 127 | flags : [ .readonly, .fast ]), 128 | 129 | // MARK: - String Commands 130 | 131 | Command(name : "SET", 132 | type : .keyValueOptions(Commands.SET), 133 | flags : [ .write, .denyoom ]), 134 | Command(name : "GETSET", 135 | type : .keyValue(Commands.GETSET), 136 | flags : [ .write, .denyoom ]), 137 | Command(name : "SETNX", 138 | type : .keyValue(Commands.SETNX), 139 | flags : [ .write, .denyoom, .fast ]), 140 | Command(name : "SETEX", 141 | type : .keyValueValue(Commands.SETEX), 142 | flags : [ .write, .denyoom, .fast ]), 143 | Command(name : "PSETEX", 144 | type : .keyValueValue(Commands.SETEX), 145 | flags : [ .write, .denyoom, .fast ]), 146 | Command(name : "APPEND", 147 | type : .keyValue(Commands.APPEND), 148 | flags : [ .write, .denyoom ]), 149 | Command(name : "SETRANGE", 150 | type : .keyIndexValue(Commands.SETRANGE), 151 | flags : [ .write, .denyoom ]), 152 | Command(name : "GET", 153 | type : .key(Commands.GET), 154 | flags : [ .readonly, .fast ]), 155 | Command(name : "STRLEN", 156 | type : .key(Commands.STRLEN), 157 | flags : [ .readonly, .fast ]), 158 | Command(name : "GETRANGE", 159 | type : .keyRange(Commands.GETRANGE), 160 | flags : [ .readonly ]), 161 | Command(name : "SUBSTR", // same like GETRANGE 162 | type : .keyRange(Commands.GETRANGE), 163 | flags : [ .readonly ]), 164 | Command(name : "MGET", 165 | type : .keys(Commands.MGET), 166 | flags : [ .readonly, .fast ]), 167 | Command(name : "MSET", 168 | type : .keyValueMap(Commands.MSET), 169 | flags : [ .write, .denyoom ]), 170 | Command(name : "MSETNX", 171 | type : .keyValueMap(Commands.MSETNX), 172 | flags : [ .write, .denyoom ]), 173 | 174 | // MARK: - Integer String Commands 175 | 176 | Command(name : "INCR", 177 | type : .key(Commands.INCR), 178 | flags : [ .write, .denyoom, .fast ]), 179 | Command(name : "DECR", 180 | type : .key(Commands.DECR), 181 | flags : [ .write, .denyoom, .fast ]), 182 | Command(name : "INCRBY", 183 | type : .keyValue(Commands.INCRBY), 184 | flags : [ .write, .denyoom, .fast ]), 185 | Command(name : "DECRBY", 186 | type : .keyValue(Commands.DECRBY), 187 | flags : [ .write, .denyoom, .fast ]), 188 | 189 | // MARK: - List Commands 190 | 191 | Command(name : "LLEN", 192 | type : .key(Commands.LLEN), 193 | flags : [ .readonly, .fast ]), 194 | Command(name : "LRANGE", 195 | type : .keyRange(Commands.LRANGE), 196 | flags : [ .readonly ]), 197 | Command(name : "LINDEX", 198 | type : .keyIndex(Commands.LINDEX), 199 | flags : [ .readonly ]), 200 | Command(name : "LSET", 201 | type : .keyIndexValue(Commands.LSET), 202 | flags : [ .write, .denyoom ]), 203 | Command(name : "RPUSH", 204 | type : .keyValues(Commands.RPUSH), 205 | flags : [ .write, .denyoom, .fast ]), 206 | Command(name : "LPUSH", 207 | type : .keyValues(Commands.LPUSH), 208 | flags : [ .write, .denyoom /*, .fast - not really :-) */ ]), 209 | Command(name : "RPUSHX", 210 | type : .keyValue(Commands.RPUSHX), 211 | flags : [ .write, .denyoom, .fast ]), 212 | Command(name : "LPUSHX", 213 | type : .keyValue(Commands.LPUSHX), 214 | flags : [ .write, .denyoom /*, .fast - not really :-) */ ]), 215 | Command(name : "RPOP", 216 | type : .key(Commands.RPOP), 217 | flags : [ .write, .fast ]), 218 | Command(name : "LPOP", 219 | type : .key(Commands.LPOP), 220 | flags : [ .write, /*, .fast - not really :-) */ ]), 221 | 222 | // MARK: - Hash Commands 223 | 224 | Command(name : "HLEN", 225 | type : .key(Commands.HLEN), 226 | flags : [ .readonly, .fast ]), 227 | Command(name : "HGETALL", 228 | type : .key(Commands.HGETALL), 229 | flags : [ .readonly, .fast ]), 230 | Command(name : "HGET", 231 | type : .keyValue(Commands.HGET), 232 | flags : [ .readonly, .fast ]), 233 | Command(name : "HEXISTS", 234 | type : .keyValue(Commands.HEXISTS), 235 | flags : [ .readonly, .fast ]), 236 | Command(name : "HSTRLEN", 237 | type : .keyValue(Commands.HSTRLEN), 238 | flags : [ .readonly, .fast ]), 239 | Command(name : "HKEYS", 240 | type : .key(Commands.HKEYS), 241 | flags : [ .readonly, .sortForScript ]), 242 | Command(name : "HVALS", 243 | type : .key(Commands.HVALS), 244 | flags : [ .readonly, .sortForScript ]), 245 | Command(name : "HSET", 246 | type : .keyValueValue(Commands.HSET), 247 | flags : [ .write, .denyoom, .fast ]), 248 | Command(name : "HSETNX", 249 | type : .keyValueValue(Commands.HSET), 250 | flags : [ .write, .denyoom, .fast ]), 251 | Command(name : "HINCRBY", 252 | type : .keyValueValue(Commands.HINCRBY), 253 | flags : [ .write, .denyoom, .fast ]), 254 | Command(name : "HMSET", 255 | type : .keyValues(Commands.HMSET), 256 | flags : [ .write, .denyoom, .fast ]), 257 | Command(name : "HDEL", 258 | type : .keyValues(Commands.HDEL), 259 | flags : [ .write, .fast ]), 260 | Command(name : "HMGET", 261 | type : .keyValues(Commands.HMGET), 262 | flags : [ .readonly, .fast ]), 263 | 264 | // MARK: - Set Commands 265 | 266 | Command(name : "SCARD", 267 | type : .key(Commands.SCARD), 268 | flags : [ .readonly, .fast ]), 269 | Command(name : "SMEMBERS", 270 | type : .key(Commands.SMEMBERS), 271 | flags : [ .readonly, .fast ]), 272 | Command(name : "SISMEMBER", 273 | type : .keyKey(Commands.SISMEMBER), 274 | flags : [ .readonly, .fast ]), 275 | Command(name : "SADD", 276 | type : .keys(Commands.SADD), 277 | flags : [ .write, .denyoom, .fast ]), 278 | Command(name : "SREM", 279 | type : .keys(Commands.SREM), 280 | flags : [ .write, .denyoom, .fast ]), 281 | Command(name : "SDIFF", 282 | type : .keys(Commands.SDIFF), 283 | flags : [ .readonly, .sortForScript ]), 284 | Command(name : "SINTER", 285 | type : .keys(Commands.SINTER), 286 | flags : [ .readonly, .sortForScript ]), 287 | Command(name : "SUNION", 288 | type : .keys(Commands.SUNION), 289 | flags : [ .readonly, .sortForScript ]), 290 | Command(name : "SDIFFSTORE", 291 | type : .keys(Commands.SDIFFSTORE), 292 | flags : [ .write, .denyoom ]), 293 | Command(name : "SINTERSTORE", 294 | type : .keys(Commands.SINTERSTORE), 295 | flags : [ .write, .denyoom ]), 296 | Command(name : "SUNIONSTORE", 297 | type : .keys(Commands.SUNIONSTORE), 298 | flags : [ .write, .denyoom ]), 299 | ] 300 | } 301 | 302 | 303 | // MARK: - Implementations 304 | 305 | enum Commands { 306 | 307 | typealias CommandContext = RedisCommandContext // TODO: drop this 308 | typealias Context = RedisCommandContext 309 | 310 | } 311 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/CommandType.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | import NIORedis 17 | import Foundation 18 | 19 | /** 20 | * Redis commands are typed by number of arguments etc. Checkout the 21 | * documentation for the `COMMAND` command for all the details: 22 | * 23 | * https://redis.io/commands/command 24 | * 25 | * CommandType represents the "reflection" information, 26 | * and adds the *implementation* of the command (in the form of a closure). 27 | * 28 | * (In a language w/ proper runtime information, you would just derive all that 29 | * via reflection. In Swift we have to type it down). 30 | */ 31 | public enum CommandType { 32 | 33 | public typealias Context = RedisCommandContext 34 | 35 | // This ain't no beauty, but apart from hardcoding this seems to be the 36 | // best option? Recommendations are welcome! 37 | 38 | case noArguments ( ( Context ) throws -> Void ) 39 | case optionalValue ( ( RESPValue?, Context ) throws -> Void ) 40 | case singleValue ( ( RESPValue, Context ) throws -> Void ) 41 | case valueValue ( ( RESPValue, RESPValue, Context ) throws -> Void ) 42 | case oneOrMoreValues( ( ArraySlice, Context ) throws -> Void ) 43 | case key ( ( Data, Context ) throws -> Void ) 44 | case keyKey ( ( Data, Data, Context ) throws -> Void ) 45 | case keyValue ( ( Data, RESPValue, Context ) throws -> Void ) 46 | case keyValueValue ( ( Data, RESPValue, RESPValue, 47 | Context ) throws -> Void ) 48 | case keyValueOptions( ( Data, RESPValue, ArraySlice, 49 | Context ) throws -> Void ) 50 | case keyValues ( ( Data, ArraySlice, 51 | Context ) throws -> Void ) 52 | case keyRange ( ( Data, Int, Int, Context ) throws -> Void ) 53 | case keyIndex ( ( Data, Int, Context ) throws -> Void ) 54 | case keyIndexValue ( ( Data, Int, RESPValue, Context ) throws -> Void ) 55 | case keys ( ( ContiguousArray, Context ) throws -> Void ) 56 | case keyValueMap ( ( ContiguousArray< ( Data, RESPValue )>, 57 | Context ) throws -> Void ) 58 | 59 | public struct ArgumentSpecification { 60 | 61 | enum Arity : RESPEncodable { 62 | case fix (Int) 63 | case minimum(Int) 64 | 65 | func toRESPValue() -> RESPValue { 66 | switch self { 67 | case .fix (let value): return .integer(value + 1) 68 | case .minimum(let value): return .integer(-(value + 1)) 69 | } 70 | } 71 | } 72 | 73 | let arity : Arity 74 | let firstKey : Int 75 | let lastKey : Int 76 | let step : Int 77 | 78 | init(arity: Arity, 79 | firstKey: Int = 0, lastKey: Int? = nil, step: Int? = nil) 80 | { 81 | self.arity = arity 82 | self.firstKey = firstKey 83 | self.lastKey = lastKey ?? firstKey 84 | self.step = step ?? (firstKey != 0 ? 1 : 0) 85 | } 86 | 87 | init(argumentCount fixCount: Int) { 88 | self.init(arity: .fix(fixCount), firstKey: 1, lastKey: 1) 89 | } 90 | init(minimumArgumentCount fixCount: Int) { 91 | self.init(arity: .minimum(fixCount), firstKey: 1, lastKey: 1) 92 | } 93 | } 94 | 95 | /// Essentially the `Mirror` for the command type. 96 | var keys : ArgumentSpecification { 97 | switch self { 98 | case .noArguments: return ArgumentSpecification(argumentCount: 0) 99 | case .key: return ArgumentSpecification(argumentCount: 1) 100 | 101 | case .keyKey, .keyValue, .keyIndex: 102 | return ArgumentSpecification(argumentCount: 2) 103 | case .keyRange, .keyIndexValue, .keyValueValue: 104 | return ArgumentSpecification(argumentCount: 3) 105 | 106 | case .keyValues: 107 | return ArgumentSpecification(minimumArgumentCount: 2) 108 | 109 | case .optionalValue: return ArgumentSpecification(arity: .minimum(0)) 110 | case .singleValue: return ArgumentSpecification(arity: .fix(1)) 111 | case .valueValue: return ArgumentSpecification(arity: .fix(2)) 112 | case .oneOrMoreValues: return ArgumentSpecification(arity: .minimum(1)) 113 | 114 | case .keys: 115 | return ArgumentSpecification(arity: .minimum(1), 116 | firstKey: 1, lastKey: -1) 117 | 118 | case .keyValueOptions: 119 | return ArgumentSpecification(minimumArgumentCount: 2) 120 | 121 | case .keyValueMap: 122 | return ArgumentSpecification(arity: .minimum(2), 123 | firstKey: 1, lastKey: -1, step: 2) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/ExpirationCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | import NIORedis 17 | 18 | extension Commands { 19 | 20 | static func TTL(key: Data, in ctx: CommandContext) throws { 21 | let inMilliseconds = ctx.command.name.hasPrefix("P") 22 | 23 | enum Result { 24 | case keyMissing // -2 25 | case noExpire // -1 26 | case deadline(Date) 27 | } 28 | 29 | let result : Result = ctx.readInDatabase { db in 30 | guard db[key] != nil else { return .keyMissing } 31 | guard let deadline = db[expiration: key] else { return .noExpire } 32 | return .deadline(deadline) 33 | } 34 | 35 | switch result { 36 | case .deadline(let deadline): 37 | let now = Date() 38 | let ttl = deadline.timeIntervalSince(now) 39 | let result = ttl >= 0 40 | ? (inMilliseconds ? Int(ttl * 1000.0) : Int(ttl)) 41 | : 0 42 | ctx.write(result) 43 | case .keyMissing: ctx.write(-2) 44 | case .noExpire: ctx.write(-1) 45 | } 46 | } 47 | 48 | static func PERSIST(key: Data, in ctx: CommandContext) throws { 49 | let result : Bool = ctx.writeInDatabase { db in 50 | guard db.removeExpiration(forKey: key) != nil else { return false } 51 | return db[key] != nil 52 | } 53 | return ctx.write(result) 54 | } 55 | 56 | static func EXPIRE(key: Data, value: RESPValue, 57 | in ctx: CommandContext) throws 58 | { 59 | guard let intValue = value.intValue else { throw RedisError.notAnInteger } 60 | 61 | let now = Date() 62 | let deadline : Date 63 | 64 | switch ctx.command.name { 65 | case "EXPIRE": 66 | deadline = Date(timeIntervalSinceNow: TimeInterval(intValue)) 67 | case "PEXPIRE": 68 | deadline = Date(timeIntervalSinceNow: TimeInterval(intValue) / 1000.0) 69 | 70 | case "EXPIREAT": 71 | deadline = Date(timeIntervalSince1970: TimeInterval(intValue)) 72 | case "PEXPIREAT": 73 | deadline = Date(timeIntervalSince1970: TimeInterval(intValue) / 1000.0) 74 | 75 | default: fatalError("Internal inconsistency, unexpected cmd: \(ctx)") 76 | } 77 | 78 | let didDeadlinePass = deadline < now 79 | 80 | let didSet : Bool = ctx.writeInDatabase { db in 81 | if didDeadlinePass { 82 | return db.removeValue(forKey: key) != nil 83 | } 84 | if db[key] == nil { 85 | return false 86 | } 87 | 88 | db[expiration: key] = deadline 89 | return true 90 | } 91 | return ctx.write(didSet) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/HashCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct NIO.ByteBuffer 16 | import enum NIORedis.RESPValue 17 | import struct Foundation.Data 18 | 19 | extension Commands { 20 | 21 | static func HLEN(key: Data, in ctx: CommandContext) throws { 22 | ctx.get(key) { value in 23 | guard let value = value else { return ctx.write(0) } 24 | guard case .hash(let hash) = value else { throw RedisError.wrongType } 25 | ctx.write(hash.count) 26 | } 27 | } 28 | 29 | static func HGETALL(key: Data, in ctx: CommandContext) throws { 30 | ctx.get(key) { value in 31 | guard let value = value else { return ctx.write([]) } 32 | guard case .hash = value else { throw RedisError.wrongType } 33 | ctx.write(value) 34 | } 35 | } 36 | 37 | static func HSTRLEN(key: Data, field: RESPValue, 38 | in ctx: CommandContext) throws 39 | { 40 | guard let fieldKey = field.keyValue else { throw RedisError.syntaxError } 41 | 42 | ctx.get(key) { value in 43 | guard let value = value else { return ctx.write(0) } 44 | guard case .hash(let hash) = value else { throw RedisError.wrongType } 45 | guard let fieldValue = hash[fieldKey] else { return ctx.write(0) } 46 | ctx.write(fieldValue.readableBytes) 47 | } 48 | } 49 | 50 | static func HGET(key: Data, field: RESPValue, in ctx: CommandContext) throws { 51 | guard let fieldKey = field.keyValue else { throw RedisError.syntaxError } 52 | 53 | ctx.get(key) { value in 54 | guard let value = value else { return ctx.write(.bulkString(nil)) } 55 | guard case .hash(let hash) = value else { throw RedisError.wrongType } 56 | ctx.write(.bulkString(hash[fieldKey])) 57 | } 58 | } 59 | 60 | static func HEXISTS(key: Data, field: RESPValue, 61 | in ctx: CommandContext) throws 62 | { 63 | guard let fieldKey = field.keyValue else { throw RedisError.syntaxError } 64 | 65 | ctx.get(key) { value in 66 | guard let value = value else { return ctx.write(false) } 67 | guard case .hash(let hash) = value else { throw RedisError.wrongType } 68 | ctx.write(hash[fieldKey] != nil) 69 | } 70 | } 71 | 72 | static func HKEYS(key: Data, in ctx: CommandContext) throws { 73 | ctx.get(key) { value in 74 | guard let value = value else { return ctx.write([]) } 75 | guard case .hash(let hash) = value else { throw RedisError.wrongType } 76 | 77 | ctx.eventLoop.execute { 78 | let keys = hash.keys.lazy.map { RESPValue(bulkString: $0) } 79 | ctx.write(.array(ContiguousArray(keys))) 80 | } 81 | } 82 | } 83 | 84 | static func HVALS(key: Data, in ctx: CommandContext) throws { 85 | ctx.get(key) { value in 86 | guard let value = value else { return ctx.write([]) } 87 | guard case .hash(let hash) = value else { throw RedisError.wrongType } 88 | 89 | ctx.eventLoop.execute { 90 | let vals = hash.values.lazy.map { RESPValue.bulkString($0) } 91 | ctx.write(.array(ContiguousArray(vals))) 92 | } 93 | } 94 | } 95 | 96 | static func HSET(key: Data, field: RESPValue, value: RESPValue, 97 | in ctx: CommandContext) throws 98 | { 99 | guard let fieldKey = field.keyValue else { throw RedisError.syntaxError } 100 | guard let bb = value.byteBuffer else { throw RedisError.syntaxError } 101 | let isNX = ctx.command.name == "HSETNX" 102 | 103 | let isNew : Bool = try ctx.writeInDatabase { db in 104 | let isNew : Bool 105 | 106 | if let oldValue = db[key] { 107 | guard case .hash(var hash) = oldValue else { 108 | throw RedisError.wrongType 109 | } 110 | 111 | isNew = hash[fieldKey] == nil 112 | if isNew || !isNX { 113 | hash[fieldKey] = bb 114 | db[key] = .hash(hash) 115 | } 116 | } 117 | else { 118 | db[key] = .hash([fieldKey: bb]) 119 | isNew = true 120 | } 121 | return isNew 122 | } 123 | 124 | ctx.write(isNew) 125 | } 126 | 127 | static func HINCRBY(key: Data, field: RESPValue, value: RESPValue, 128 | in ctx: CommandContext) throws 129 | { 130 | guard let fieldKey = field.keyValue else { throw RedisError.syntaxError } 131 | guard let intValue = value.intValue else { throw RedisError.notAnInteger } 132 | 133 | let result : Int = try ctx.writeInDatabase { db in 134 | 135 | let result : Int 136 | 137 | if let oldValue = db[key] { 138 | guard case .hash(var hash) = oldValue else { 139 | throw RedisError.wrongType 140 | } 141 | 142 | if let bb = hash[fieldKey] { 143 | guard let oldInt = bb.stringAsInteger else { 144 | throw RedisError.notAnInteger 145 | } 146 | result = oldInt + intValue 147 | } 148 | else { 149 | result = 0 + intValue 150 | } 151 | 152 | hash[fieldKey] = ByteBuffer.makeFromIntAsString(result) 153 | db[key] = .hash(hash) 154 | } 155 | else { 156 | result = 0 + intValue 157 | db[key] = .hash([fieldKey: ByteBuffer.makeFromIntAsString(result)]) 158 | } 159 | 160 | return result 161 | } 162 | 163 | ctx.write(result) 164 | } 165 | 166 | static func HMSET(key: Data, values: ArraySlice, 167 | in ctx: CommandContext) throws 168 | { 169 | guard values.count % 2 == 0 else { 170 | throw RedisError.wrongNumberOfArguments(command: "HMSET") 171 | } 172 | 173 | let newValues = try values.convertToRedisHash() 174 | 175 | try ctx.writeInDatabase { db in 176 | if let oldValue = db[key] { 177 | guard case .hash(var oldDict) = oldValue else { 178 | throw RedisError.wrongType 179 | } 180 | 181 | oldDict.merge(newValues) { $1 } 182 | db[key] = .hash(oldDict) 183 | } 184 | else { 185 | db[key] = .hash(newValues) 186 | } 187 | 188 | } 189 | 190 | ctx.write(RESPValue.ok) 191 | } 192 | 193 | static func HDEL(key: Data, fields: ArraySlice, 194 | in ctx: CommandContext) throws 195 | { 196 | guard !fields.isEmpty else { return ctx.write([]) } 197 | 198 | let keys : [ Data ] = try fields.lazy.map { 199 | guard let key = $0.keyValue else { 200 | throw RedisError.syntaxError 201 | } 202 | return key 203 | } 204 | 205 | let delCount : Int = try ctx.writeInDatabase { db in 206 | guard let value = db[key] else { return 0 } 207 | guard case .hash(var hash) = value else { throw RedisError.wrongType } 208 | 209 | var delCount = 0 210 | 211 | for field in keys { 212 | if hash.removeValue(forKey: field) != nil { 213 | delCount += 1 214 | } 215 | } 216 | if delCount > 0 { 217 | db[key] = .hash(hash) 218 | } 219 | return delCount 220 | } 221 | 222 | ctx.write(delCount) 223 | } 224 | 225 | static func HMGET(key: Data, fields: ArraySlice, 226 | in ctx: CommandContext) throws 227 | { 228 | guard !fields.isEmpty else { return ctx.write([]) } 229 | 230 | let keys : [ Data ] = try fields.lazy.map { 231 | guard let key = $0.keyValue else { 232 | throw RedisError.syntaxError 233 | } 234 | return key 235 | } 236 | 237 | ctx.get(key) { value in 238 | let nilValue = RESPValue.bulkString(nil) 239 | guard let value = value else { 240 | return ctx.write(Array(repeating: nilValue, count: keys.count)) 241 | } 242 | 243 | guard case .hash(let hash) = value else { 244 | return ctx.write(RedisError.wrongType) 245 | } 246 | 247 | ctx.eventLoop.execute { 248 | var results = ContiguousArray() 249 | results.reserveCapacity(keys.count) 250 | for key in keys { 251 | results.append(RESPValue.bulkString(hash[key])) 252 | } 253 | ctx.write(RESPValue.array(results)) 254 | } 255 | } 256 | } 257 | 258 | } 259 | 260 | extension ArraySlice where Element == RESPValue { 261 | // ArraySlice to please Swift 4.0, which has IndexDistance 262 | 263 | func convertToRedisHash() throws -> Dictionary { 264 | guard count % 2 == 0 else { 265 | throw RedisError.wrongNumberOfArguments(command: nil) 266 | } 267 | 268 | let pairCount = count / 2 269 | var dict = Dictionary(minimumCapacity: pairCount + 1) 270 | 271 | var i = startIndex 272 | while i < endIndex { 273 | guard let key = self[i].keyValue else { 274 | throw RedisError.syntaxError 275 | } 276 | i = i.advanced(by: 1) 277 | 278 | let value = self[i] 279 | i = i.advanced(by: 1) 280 | 281 | switch value { 282 | case .bulkString(.some(let cs)), .simpleString(let cs): 283 | dict[key] = cs 284 | default: 285 | throw RedisError.syntaxError 286 | } 287 | } 288 | return dict 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/IntCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | import enum NIORedis.RESPValue 17 | 18 | extension Commands { 19 | 20 | static func workOnInt(_ key: Data, in ctx: CommandContext, 21 | _ op: ( Int ) -> Int) throws 22 | { 23 | let result = try ctx.writeInDatabase { db in 24 | return try db.intOp(key, op) 25 | } 26 | ctx.write(result) 27 | } 28 | 29 | static func INCR(key: Data, in ctx: CommandContext) throws { 30 | try workOnInt(key, in: ctx) { $0 + 1 } 31 | } 32 | static func DECR(key: Data, in ctx: CommandContext) throws { 33 | try workOnInt(key, in: ctx) { $0 - 1 } 34 | } 35 | 36 | static func INCRBY(key: Data, value: RESPValue, 37 | in ctx: CommandContext) throws 38 | { 39 | guard let intValue = value.intValue else { throw RedisError.notAnInteger } 40 | try workOnInt(key, in: ctx) { $0 + intValue } 41 | } 42 | static func DECRBY(key: Data, value: RESPValue, 43 | in ctx: CommandContext) throws 44 | { 45 | guard let intValue = value.intValue else { throw RedisError.notAnInteger } 46 | try workOnInt(key, in: ctx) { $0 - intValue } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/KeyCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import enum NIORedis.RESPValue 16 | import struct NIORedis.RESPError 17 | import struct NIO.ByteBuffer 18 | import struct Foundation.Data 19 | 20 | fileprivate enum TypeValues { 21 | static let none = RESPValue(simpleString: "none") 22 | static let string = RESPValue(simpleString: "string") 23 | static let list = RESPValue(simpleString: "list") 24 | static let set = RESPValue(simpleString: "set") 25 | static let hash = RESPValue(simpleString: "hash") 26 | } 27 | 28 | 29 | extension Commands { 30 | 31 | static func KEYS(pattern v: RESPValue, in ctx: CommandContext) throws { 32 | var bb : ByteBuffer 33 | switch v { 34 | case .simpleString(let cs), .bulkString(.some(let cs)): bb = cs 35 | default: throw RedisError.syntaxError 36 | } 37 | 38 | guard let pattern = RedisPattern(bb) else { 39 | let s = bb.readString(length: bb.readableBytes) 40 | throw RedisError.patternNotImplemented(s) 41 | } 42 | 43 | let keys = ctx.readInDatabase { db in db.keys } 44 | 45 | if case .matchAll = pattern { 46 | let values = ContiguousArray( 47 | keys.lazy.map { RESPValue(bulkString: $0) }) 48 | return ctx.write(.array(values)) 49 | } 50 | 51 | var values = ContiguousArray() 52 | for key in keys { 53 | guard pattern.match(key) else { continue } 54 | values.append(RESPValue(bulkString: key)) 55 | } 56 | 57 | ctx.write(.array(values)) 58 | } 59 | 60 | static func DBSIZE(_ ctx: CommandContext) throws { 61 | ctx.write(ctx.readInDatabase { db in db.count }) 62 | } 63 | 64 | static func TYPE(key: Data, in ctx: CommandContext) throws { 65 | ctx.get(key) { value in 66 | ctx.eventLoop.execute { 67 | guard let value = value else { return ctx.write(TypeValues.none) } 68 | 69 | switch value { 70 | case .string: ctx.write(TypeValues.string) 71 | case .list: ctx.write(TypeValues.list) 72 | case .set: ctx.write(TypeValues.set) 73 | case .hash: ctx.write(TypeValues.hash) 74 | case .clear: fatalError("use of .clear case") 75 | } 76 | } 77 | } 78 | } 79 | 80 | static func DEL(keys: ContiguousArray, in ctx: CommandContext) throws { 81 | ctx.writeInDatabase { db in 82 | 83 | var count = 0 84 | for key in keys { 85 | if db.removeValue(forKey: key) != nil { 86 | count += 1 87 | } 88 | } 89 | 90 | ctx.write(count) 91 | } 92 | } 93 | 94 | static func RENAME(oldKey: Data, newKey: Data, 95 | in ctx: CommandContext) throws 96 | { 97 | let isSame = oldKey == newKey 98 | 99 | try ctx.writeInDatabase { db in 100 | if isSame { 101 | guard db[oldKey] != nil else { throw RedisError.noSuchKey } 102 | } 103 | else { 104 | if !db.renameKey(oldKey, to: newKey) { throw RedisError.noSuchKey } 105 | } 106 | } 107 | 108 | ctx.write(RESPValue.ok) 109 | } 110 | 111 | static func RENAMENX(oldKey: Data, newKey: Data, 112 | in ctx: CommandContext) throws 113 | { 114 | let isSame = oldKey == newKey 115 | 116 | let didExist : Bool = try ctx.writeInDatabase { db in 117 | if isSame { 118 | guard db[oldKey] != nil else { throw RedisError.noSuchKey } 119 | return true 120 | } 121 | 122 | if db[newKey] != nil { 123 | return true 124 | } 125 | 126 | if !db.renameKey(oldKey, to: newKey) { throw RedisError.noSuchKey } 127 | return false 128 | } 129 | ctx.write(didExist ? 0 : 1) 130 | } 131 | 132 | static func EXISTS(keys: ContiguousArray, 133 | in ctx: CommandContext) throws 134 | { 135 | let count : Int = ctx.readInDatabase { db in 136 | var count = 0 137 | for key in keys { 138 | if db[key] != nil { count += 1 } 139 | } 140 | return count 141 | } 142 | ctx.write(count) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/ListCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | import struct NIO.ByteBuffer 17 | import NIORedis 18 | 19 | extension Commands { 20 | 21 | static func LLEN(key: Data, in ctx: CommandContext) throws { 22 | let count = try ctx.readInDatabase { db in try db.listCount(key: key) } 23 | ctx.write(count) 24 | } 25 | 26 | static func LINDEX(key: Data, index: Int, in ctx: CommandContext) throws { 27 | ctx.get(key) { value in 28 | guard let value = value else { return ctx.write(.bulkString(nil)) } 29 | guard case .list(let list) = value else { throw RedisError.wrongType } 30 | 31 | let count = list.count 32 | if count == 0 { return ctx.write(.bulkString(nil)) } 33 | 34 | let cindex = index < 0 ? (count + index) : index 35 | if cindex < 0 || cindex >= count { return ctx.write(.bulkString(nil)) } 36 | 37 | ctx.write(list[cindex]) 38 | } 39 | } 40 | 41 | static func LRANGE(key: Data, start: Int, stop: Int, in ctx: CommandContext) 42 | throws 43 | { 44 | ctx.get(key) { value in 45 | guard let value = value else { return ctx.write([]) } 46 | guard case .list(let list) = value else { throw RedisError.wrongType } 47 | 48 | let count = list.count 49 | if count == 0 { return ctx.write([]) } 50 | 51 | let range = list.rangeForRedisRange(start: start, stop: stop) 52 | 53 | ctx.write(list[range].toRESPValue()) 54 | } 55 | } 56 | 57 | static func LSET(key: Data, index: Int, value: RESPValue, 58 | in ctx: CommandContext) throws 59 | { 60 | guard let bb = value.byteBuffer else { throw RedisError.wrongType } 61 | 62 | try ctx.writeInDatabase { db in 63 | try db.lset(key: key, index: index, value: bb) 64 | } 65 | ctx.write(RESPValue.ok) 66 | } 67 | 68 | @inline(__always) 69 | static func pop(key: Data, left : Bool, in ctx: CommandContext) throws { 70 | let v = try ctx.writeInDatabase { db in 71 | return try db.listPop(key: key, left: left) 72 | } 73 | ctx.write(.bulkString(v)) 74 | } 75 | 76 | @_specialize(where T == ArraySlice) 77 | @inline(__always) 78 | static func push(key : Data, values: T, 79 | left : Bool, createIfMissing : Bool = true, 80 | in ctx: CommandContext) throws 81 | where T.Element == RESPValue 82 | { 83 | let valueCount : Int 84 | #if swift(>=4.1) 85 | valueCount = values.count 86 | #else 87 | if let c = values.count as? Int { valueCount = c } 88 | else { fatalError("non-int count") } 89 | #endif 90 | 91 | if valueCount == 0 { 92 | let result = try ctx.writeInDatabase { db in try db.listCount(key: key) } 93 | return ctx.write(result) 94 | } 95 | else if valueCount == 1 { 96 | switch values[values.startIndex] { 97 | case .simpleString(let bb), .bulkString(.some(let bb)): 98 | let count = try ctx.writeInDatabase { db in 99 | return try db.listPush(key: key, value: bb, left: left, 100 | createIfMissing: createIfMissing) 101 | } 102 | return ctx.write(count) 103 | default: throw RedisError.wrongType 104 | } 105 | } 106 | 107 | guard let byteBuffers = values.extractRedisList(reverse: left) else { 108 | throw RedisError.wrongType 109 | } 110 | 111 | let count = try ctx.writeInDatabase { db in 112 | try db.listPush(key: key, values: byteBuffers, left: left, 113 | createIfMissing: createIfMissing) 114 | } 115 | ctx.write(count) 116 | } 117 | 118 | static func RPOP(key: Data, in ctx: CommandContext) throws { 119 | try pop(key: key, left: false, in: ctx) 120 | } 121 | static func LPOP(key: Data, in ctx: CommandContext) throws { 122 | try pop(key: key, left: true, in: ctx) 123 | } 124 | 125 | static func RPUSH(key: Data, values: ArraySlice, 126 | in ctx: CommandContext) throws 127 | { 128 | try push(key: key, values: values, left: false, in: ctx) 129 | } 130 | static func LPUSH(key: Data, values: ArraySlice, 131 | in ctx: CommandContext) throws 132 | { 133 | try push(key: key, values: values, left: true, in: ctx) 134 | } 135 | 136 | static func RPUSHX(key: Data, value: RESPValue, 137 | in ctx: CommandContext) throws 138 | { 139 | try push(key: key, values: [ value ], 140 | left: false, createIfMissing: false, in: ctx) 141 | } 142 | static func LPUSHX(key: Data, value: RESPValue, 143 | in ctx: CommandContext) throws 144 | { 145 | try push(key: key, values: [ value ], 146 | left: true, createIfMissing: false, in: ctx) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/PubSubCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Dispatch 16 | import NIO 17 | import enum NIORedis.RESPValue 18 | import struct Foundation.Data 19 | 20 | extension Commands { 21 | static func PUBLISH(channel: Data, message value: RESPValue, 22 | in ctx: CommandContext) throws 23 | { 24 | guard let message = value.byteBuffer else { throw RedisError.wrongType } 25 | 26 | let pubsub = ctx.handler.server.pubSub 27 | pubsub.Q.async { 28 | let count = pubsub.publish(channel, message) 29 | ctx.write(count) 30 | } 31 | } 32 | 33 | static func SUBSCRIBE(channelKeys: ContiguousArray, 34 | in ctx: CommandContext) throws 35 | { 36 | guard !channelKeys.isEmpty else { throw RedisError.syntaxError } 37 | 38 | let subscribeCmd = RESPValue(simpleString: "subscribe") 39 | 40 | var subscribedChannels = ctx.handler.subscribedChannels ?? Set() 41 | 42 | let pubsub = ctx.handler.server.pubSub 43 | pubsub.Q.async { 44 | for key in channelKeys { 45 | if !subscribedChannels.contains(key) { 46 | subscribedChannels.insert(key) 47 | pubsub.subscribe(key, handler: ctx.handler) 48 | } 49 | 50 | ctx.eventLoop.execute { 51 | if ctx.handler.subscribedChannels == nil { 52 | ctx.handler.subscribedChannels = Set() 53 | } 54 | ctx.handler.subscribedChannels!.insert(key) 55 | let count = ctx.handler.subscribedChannels!.count 56 | ctx.write([ subscribeCmd, 57 | RESPValue(bulkString: key), 58 | RESPValue.integer(count) ].toRESPValue()) 59 | } 60 | } 61 | } 62 | } 63 | 64 | static func UNSUBSCRIBE(channelKeys: ContiguousArray, 65 | in ctx: CommandContext) throws 66 | { 67 | guard !channelKeys.isEmpty else { throw RedisError.syntaxError } 68 | 69 | let subscribeCmd = RESPValue(simpleString: "unsubscribe") 70 | 71 | var subscribedChannels = ctx.handler.subscribedChannels ?? Set() 72 | 73 | let pubsub = ctx.handler.server.pubSub 74 | pubsub.Q.async { 75 | for key in channelKeys { 76 | if subscribedChannels.contains(key) { 77 | subscribedChannels.remove(key) 78 | pubsub.unsubscribe(key, handler: ctx.handler) 79 | } 80 | 81 | ctx.eventLoop.execute { 82 | ctx.handler.subscribedChannels?.remove(key) 83 | let count = ctx.handler.subscribedChannels?.count ?? 0 84 | ctx.write([ subscribeCmd, 85 | RESPValue(bulkString: key), 86 | RESPValue.integer(count) ].toRESPValue()) 87 | } 88 | } 89 | } 90 | } 91 | 92 | static func PSUBSCRIBE(patternValues: ArraySlice, 93 | in ctx: CommandContext) throws 94 | { 95 | // sigh: lots of WET 96 | guard !patternValues.isEmpty else { throw RedisError.syntaxError } 97 | guard let patterns = patternValues.extractByteBuffersAndPatterns() else { 98 | throw RedisError.wrongType 99 | } 100 | 101 | let subscribeCmd = RESPValue(simpleString: "psubscribe") 102 | 103 | var subscribedPatterns = ctx.handler.subscribedPatterns ?? Set() 104 | 105 | let pubsub = ctx.handler.server.pubSub 106 | pubsub.Q.async { 107 | for ( bb, key ) in patterns { 108 | if !subscribedPatterns.contains(key) { 109 | subscribedPatterns.insert(key) 110 | pubsub.subscribe(key, handler: ctx.handler) 111 | } 112 | 113 | ctx.eventLoop.execute { 114 | if ctx.handler.subscribedPatterns == nil { 115 | ctx.handler.subscribedPatterns = Set() 116 | } 117 | ctx.handler.subscribedPatterns!.insert(key) 118 | let count = ctx.handler.subscribedPatterns!.count 119 | ctx.write([ subscribeCmd, 120 | RESPValue.bulkString(bb), 121 | RESPValue.integer(count) ].toRESPValue()) 122 | } 123 | } 124 | } 125 | } 126 | 127 | static func PUNSUBSCRIBE(patternValues: ArraySlice, 128 | in ctx: CommandContext) throws 129 | { 130 | // sigh: lots of WET 131 | guard !patternValues.isEmpty else { throw RedisError.syntaxError } 132 | guard let patterns = patternValues.extractByteBuffersAndPatterns() else { 133 | throw RedisError.wrongType 134 | } 135 | 136 | let subscribeCmd = RESPValue(simpleString: "punsubscribe") 137 | 138 | var subscribedPatterns = ctx.handler.subscribedPatterns ?? Set() 139 | 140 | let pubsub = ctx.handler.server.pubSub 141 | pubsub.Q.async { 142 | for ( bb, key ) in patterns { 143 | if subscribedPatterns.contains(key) { 144 | subscribedPatterns.remove(key) 145 | pubsub.unsubscribe(key, handler: ctx.handler) 146 | } 147 | 148 | ctx.eventLoop.execute { 149 | ctx.handler.subscribedPatterns?.remove(key) 150 | let count = ctx.handler.subscribedPatterns?.count ?? 0 151 | ctx.write([ subscribeCmd, 152 | RESPValue.bulkString(bb), 153 | RESPValue.integer(count) ].toRESPValue()) 154 | } 155 | } 156 | } 157 | } 158 | 159 | static func PUBSUB(values: ArraySlice, 160 | in ctx: CommandContext) throws 161 | { 162 | guard let subcmd = values.first?.stringValue?.uppercased() else { 163 | throw RedisError.syntaxError 164 | } 165 | 166 | let argIdx = values.startIndex.advanced(by: 1) 167 | let args = values[argIdx..() 216 | 217 | for channel in channels { 218 | guard let loop2Sub = pubSub.channelToEventLoopToSubscribers[channel], 219 | !loop2Sub.isEmpty else { 220 | channelCountPairs.append(RESPValue(bulkString: channel)) 221 | channelCountPairs.append(.integer(0)) 222 | continue 223 | } 224 | 225 | let count = loop2Sub.values.reduce(0) { $0 + $1.count } 226 | channelCountPairs.append(RESPValue(bulkString: channel)) 227 | channelCountPairs.append(.integer(count)) 228 | } 229 | 230 | ctx.write(.array(channelCountPairs)) 231 | } 232 | } 233 | 234 | static func channelList(pattern: RedisPattern? = nil, 235 | in ctx: CommandContext) 236 | { 237 | let pubSub = ctx.handler.server.pubSub 238 | pubSub.Q.async { 239 | var channels = ContiguousArray() 240 | 241 | for ( channel, loop2Sub ) in pubSub.channelToEventLoopToSubscribers { 242 | guard !loop2Sub.isEmpty else { continue } 243 | if let pattern = pattern { 244 | if !pattern.match(channel) { continue } 245 | } 246 | 247 | var isActive = false 248 | for sub in loop2Sub.values { 249 | if !sub.isEmpty { 250 | isActive = true 251 | break 252 | } 253 | } 254 | if isActive { channels.append(RESPValue(bulkString: channel)) } 255 | } 256 | 257 | ctx.write(.array(channels)) 258 | } 259 | } 260 | } 261 | 262 | 263 | extension Collection where Element == RESPValue { 264 | 265 | func extractByteBuffersAndPatterns() 266 | -> ContiguousArray<(ByteBuffer, RedisPattern)>? 267 | { 268 | var patterns = ContiguousArray<(ByteBuffer, RedisPattern)>() 269 | #if swift(>=4.1) 270 | patterns.reserveCapacity(count) 271 | #else 272 | if let count = count as? Int { patterns.reserveCapacity(count) } 273 | #endif 274 | 275 | for item in self { 276 | guard let bb = item.byteBuffer else { return nil } 277 | guard let pattern = RedisPattern(bb) else { return nil } 278 | patterns.append( ( bb, pattern ) ) 279 | } 280 | return patterns 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/RedisCommand.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | import NIORedis 17 | import struct Foundation.Data 18 | 19 | public struct RedisCommand : RESPEncodable { 20 | 21 | public let name : String 22 | public let type : CommandType 23 | public let flags : Flags 24 | 25 | public struct Flags : OptionSet, RESPEncodable, CustomStringConvertible { 26 | 27 | public let rawValue : Int 28 | 29 | public init(rawValue: Int) { self.rawValue = rawValue } 30 | 31 | /// command may result in modifications 32 | public static let write = Flags(rawValue: 1 << 0) 33 | 34 | /// command will never modify keys 35 | public static let readonly = Flags(rawValue: 1 << 1) 36 | 37 | /// reject command if currently OOM 38 | public static let denyoom = Flags(rawValue: 1 << 2) 39 | 40 | /// server admin command 41 | public static let admin = Flags(rawValue: 1 << 3) 42 | 43 | /// pubsub-related command 44 | public static let pubsub = Flags(rawValue: 1 << 4) 45 | 46 | /// deny this command from scripts 47 | public static let noscript = Flags(rawValue: 1 << 5) 48 | 49 | /// command has random results, dangerous for scripts 50 | public static let random = Flags(rawValue: 1 << 6) 51 | 52 | /// allow command while database is loading 53 | public static let loading = Flags(rawValue: 1 << 7) 54 | 55 | /// allow command while replica has stale data 56 | public static let stale = Flags(rawValue: 1 << 8) 57 | 58 | public static let fast = Flags(rawValue: 1 << 9) 59 | 60 | public static let sortForScript = Flags(rawValue: 1 << 10) 61 | 62 | public var stringArray : [ String ] { 63 | var values = [ String ]() 64 | if contains(.write) { values.append("write") } 65 | if contains(.readonly) { values.append("readonly") } 66 | if contains(.denyoom) { values.append("denyoom") } 67 | if contains(.admin) { values.append("admin") } 68 | if contains(.pubsub) { values.append("pubsub") } 69 | if contains(.noscript) { values.append("noscript") } 70 | if contains(.random) { values.append("random") } 71 | if contains(.loading) { values.append("loading") } 72 | if contains(.stale) { values.append("stale") } 73 | if contains(.fast) { values.append("fast") } 74 | if contains(.sortForScript) { values.append("sort_for_script") } 75 | return values 76 | } 77 | public func toRESPValue() -> RESPValue { 78 | return stringArray.toRESPValue() 79 | } 80 | public var description : String { 81 | return "" 82 | } 83 | } 84 | 85 | public func toRESPValue() -> RESPValue { 86 | let keys = type.keys 87 | return [ 88 | name.lowercased(), 89 | keys.arity, 90 | flags, 91 | keys.firstKey, keys.lastKey, keys.step 92 | ].toRESPValue() 93 | } 94 | } 95 | 96 | 97 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/ServerCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Dispatch 16 | import struct NIO.ByteBufferAllocator 17 | import struct NIO.NIOAny 18 | import enum NIORedis.RESPValue 19 | import struct Foundation.Data 20 | 21 | extension Commands { 22 | 23 | static func COMMAND(value: RESPValue?, in ctx: CommandContext) throws { 24 | let commandTable = ctx.handler.server.commandTable 25 | 26 | if let value = value { 27 | // TODO: Only supports one parameter here. 28 | guard let s = value.stringValue else { 29 | throw RedisError.unknownSubcommand // TBD? ProtocolError? 30 | } 31 | 32 | switch s.uppercased() { 33 | case "COUNT": 34 | ctx.write(commandTable.count) 35 | case "LIST": 36 | // TODO: [FILTERBY ] 37 | ctx.write(commandTable.map(\.name)) 38 | case "DOCS": 39 | throw RedisError.unsupportedSubcommand 40 | case "GETKEYS": 41 | throw RedisError.unsupportedSubcommand 42 | case "GETKEYSANDFLAGS": 43 | throw RedisError.unsupportedSubcommand 44 | case "INFO": 45 | throw RedisError.unsupportedSubcommand 46 | default: 47 | throw RedisError.unknownSubcommand 48 | } 49 | } 50 | else { 51 | ctx.write(commandTable) 52 | } 53 | } 54 | 55 | static func SELECT(value: RESPValue?, in ctx: CommandContext) throws { 56 | guard let dbIndex = value?.intValue else { 57 | throw RedisError.invalidDBIndex 58 | } 59 | guard dbIndex >= 0 && dbIndex < ctx.databases.databases.count else { 60 | throw RedisError.dbIndexOutOfRange 61 | } 62 | 63 | ctx.handler.databaseIndex = dbIndex 64 | 65 | ctx.write(RESPValue.ok) 66 | } 67 | 68 | static func SWAPDB(swap from: RESPValue, with to: RESPValue, 69 | in ctx: CommandContext) throws 70 | { 71 | guard let fromIndex = from.intValue, let toIndex = to.intValue else { 72 | throw RedisError.invalidDBIndex 73 | } 74 | 75 | let dbs = ctx.databases 76 | let count = dbs.databases.count 77 | 78 | guard fromIndex >= 0 && fromIndex < count 79 | && toIndex >= 0 && toIndex < count else { 80 | throw RedisError.dbIndexOutOfRange 81 | } 82 | guard fromIndex != toIndex else { 83 | return ctx.write(RESPValue.ok) 84 | } 85 | 86 | do { 87 | let lock = dbs.context.lock 88 | lock.lockForWriting() 89 | defer { lock.unlock() } 90 | 91 | let other = dbs.databases[fromIndex] 92 | dbs.databases[fromIndex] = dbs.databases[toIndex] 93 | dbs.databases[toIndex] = other 94 | } 95 | 96 | ctx.write(RESPValue.ok) 97 | } 98 | 99 | static func PING(value: RESPValue?, in ctx: CommandContext) throws { 100 | guard let value = value else { 101 | return ctx.write(RESPValue(simpleString: "PONG")) 102 | } 103 | ctx.write(value) 104 | } 105 | 106 | static func MONITOR(_ ctx: CommandContext) throws { 107 | let client = ctx.handler 108 | guard !client.isMonitoring.load(ordering: .relaxed) else { 109 | return ctx.write(RESPValue.ok) 110 | } 111 | 112 | client.isMonitoring.store(true, ordering: .relaxed) 113 | client.server.monitors.wrappingIncrement(ordering: .relaxed) 114 | ctx.write(RESPValue.ok) 115 | } 116 | 117 | static func QUIT(_ ctx: CommandContext) throws { 118 | ctx.context.channel.close(mode: .input, promise: nil) 119 | 120 | ctx.context.writeAndFlush(NIOAny(RESPValue.ok)) 121 | .whenComplete { _ in 122 | ctx.context.channel.close(promise: nil) 123 | } 124 | } 125 | 126 | static func SAVE(_ ctx: CommandContext) throws { 127 | let async = ctx.command.name.uppercased() == "BGSAVE" 128 | let server = ctx.handler.server 129 | 130 | try ctx.writeInDatabase { _ in 131 | try server.dumpManager.saveDump(of: ctx.databases, to: server.dumpURL, 132 | asynchronously: async) 133 | ctx.write(RESPValue.ok) 134 | } 135 | } 136 | 137 | static func LASTSAVE(_ ctx: CommandContext) throws { 138 | ctx.handler.server.dumpManager.getLastSave { stamp, _ in 139 | ctx.write(Int(stamp.timeIntervalSince1970)) 140 | } 141 | } 142 | 143 | 144 | // MARK: - Client 145 | 146 | static func CLIENT(values: ArraySlice, 147 | in ctx: CommandContext) throws 148 | { 149 | guard let subcmd = values.first?.stringValue?.uppercased() else { 150 | throw RedisError.syntaxError 151 | } 152 | let args = values[1.. 0 else { return ctx.write("") } // Never 188 | 189 | var result = ByteBufferAllocator().buffer(capacity: 1024) 190 | func yield(_ info: RedisCommandHandler.ClientInfo) { 191 | listQueue.async { 192 | assert(count > 0) 193 | count -= 1 194 | 195 | result.writeString(info.redisClientLogLine) 196 | result.writeInteger(10 as UInt8) 197 | 198 | if count == 0 { 199 | ctx.write(.bulkString(result)) 200 | } 201 | } 202 | } 203 | 204 | // This could be improved by grouping the clients by eventLoop and only 205 | // issue a single statistics collector. 206 | for client in clients { 207 | if let eventLoop = client.eventLoop { 208 | eventLoop.execute { 209 | yield(client.getClientInfo()) 210 | } 211 | } 212 | else { 213 | yield(client.getClientInfo()) 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | 221 | fileprivate extension RedisCommandHandler.ClientInfo { 222 | 223 | var redisClientLogLine : String { 224 | var ms = "id=\(id)" 225 | 226 | if let v = addr { ms += " addr=\(v)" } 227 | if let v = name { ms += " name=\(v)" } 228 | 229 | ms += " age=\(Int64(age)) idle=\(Int64(idle))" 230 | // flags 231 | ms += " db=\(db)" 232 | 233 | if let v = cmd?.lowercased() { ms += " cmd=\(v)" } 234 | return ms 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/SetCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | import struct NIO.ByteBuffer 17 | import NIORedis 18 | 19 | extension Commands { 20 | 21 | static func SCARD(key: Data, in ctx: CommandContext) throws { 22 | ctx.get(key) { value in 23 | guard let value = value else { return ctx.write(0) } 24 | guard case .set(let set) = value else { throw RedisError.wrongType } 25 | 26 | ctx.write(set.count) 27 | } 28 | } 29 | 30 | static func SMEMBERS(key: Data, in ctx: CommandContext) throws { 31 | ctx.get(key) { value in 32 | guard let value = value else { return ctx.write([]) } 33 | guard case .set(let set) = value else { throw RedisError.wrongType } 34 | 35 | ctx.eventLoop.execute { 36 | ctx.write(set.toRESPValue()) 37 | } 38 | } 39 | } 40 | 41 | static func SISMEMBER(key: Data, member: Data, 42 | in ctx: CommandContext) throws 43 | { 44 | ctx.get(key) { value in 45 | guard let value = value else { return ctx.write(false) } 46 | guard case .set(let set) = value else { throw RedisError.wrongType } 47 | 48 | ctx.write(set.contains(member)) 49 | } 50 | } 51 | 52 | static func SADD(keys: ContiguousArray, in ctx: CommandContext) throws { 53 | guard let key = keys.first else { throw RedisError.syntaxError } 54 | let members = keys[1.., in ctx: CommandContext) throws { 63 | guard let key = keys.first else { throw RedisError.syntaxError } 64 | let members = keys[1.., 73 | in ctx: CommandContext, 74 | _ op: @escaping (inout Set, Set) -> Void) throws 75 | { 76 | guard !keys.isEmpty else { return ctx.write([]) } 77 | 78 | let baseKey = keys[0] 79 | 80 | let result = try ctx.writeInDatabase { db in 81 | return try db.setOp(baseKey, against: keys[1.., 87 | in ctx: CommandContext, 88 | _ op: @escaping (inout Set, Set) -> Void) 89 | throws 90 | { 91 | guard keys.count > 1 else { throw RedisError.syntaxError } 92 | 93 | let destination = keys[0] 94 | let baseKey = keys[1] 95 | 96 | let count : Int 97 | let db = ctx.database 98 | 99 | do { 100 | let lock = ctx.databases.context.lock 101 | lock.lockForWriting() 102 | defer { lock.unlock() } 103 | 104 | let result = try db.setOp(baseKey, against: keys[2.., in ctx: CommandContext) throws 113 | { 114 | try setop(keys: keys, in: ctx) { $0.subtract($1) } 115 | } 116 | static func SDIFFSTORE(keys: ContiguousArray, 117 | in ctx: CommandContext) throws 118 | { 119 | try setopStore(keys: keys, in: ctx) { $0.subtract($1) } 120 | } 121 | 122 | static func SINTER(keys: ContiguousArray, in ctx: CommandContext) throws 123 | { 124 | try setop(keys: keys, in: ctx) { $0.formIntersection($1) } 125 | } 126 | static func SINTERSTORE(keys: ContiguousArray, 127 | in ctx: CommandContext) throws 128 | { 129 | try setopStore(keys: keys, in: ctx) { $0.formIntersection($1) } 130 | } 131 | 132 | static func SUNION(keys: ContiguousArray, in ctx: CommandContext) throws 133 | { 134 | try setop(keys: keys, in: ctx) { $0.formUnion($1) } 135 | } 136 | static func SUNIONSTORE(keys: ContiguousArray, 137 | in ctx: CommandContext) throws 138 | { 139 | try setopStore(keys: keys, in: ctx) { $0.formUnion($1) } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/RedisServer/Commands/StringCommands.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Date 16 | import struct Foundation.Data 17 | import struct Foundation.TimeInterval 18 | import NIORedis 19 | import NIO 20 | 21 | extension Commands { 22 | 23 | static func APPEND(_ key: Data, _ value: RESPValue, in ctx: CommandContext) 24 | throws 25 | { 26 | guard var bb = value.byteBuffer else { throw RedisError.wrongType } 27 | 28 | let result : Int = try ctx.writeInDatabase { ( db : Databases.Database ) in 29 | if let oldValue : RedisValue = db[key] { 30 | guard case .string(var s) = oldValue else { throw RedisError.wrongType } 31 | s.writeBuffer(&bb) 32 | db[key] = .string(s) 33 | return s.readableBytes 34 | } 35 | else { 36 | db[key] = .string(bb) 37 | return bb.readableBytes 38 | } 39 | 40 | } 41 | ctx.write(result) 42 | } 43 | 44 | fileprivate enum KeyOverrideBehaviour { 45 | case always 46 | case ifMissing 47 | case ifExisting 48 | } 49 | fileprivate enum SetReturnStyle { 50 | case ok 51 | case bool 52 | } 53 | 54 | @inline(__always) 55 | fileprivate 56 | static func set(_ key : Data, 57 | _ value : RESPValue, 58 | keyOverride : KeyOverrideBehaviour = .always, 59 | expiration : TimeInterval? = nil, 60 | result : SetReturnStyle, 61 | in ctx : CommandContext) throws 62 | { 63 | guard let redisValue = RedisValue(string: value) else { 64 | throw RedisError.wrongType 65 | } 66 | 67 | let db = ctx.database 68 | 69 | let doSet : Bool 70 | do { 71 | let lock = ctx.databases.context.lock 72 | lock.lockForWriting() 73 | defer { lock.unlock() } 74 | 75 | switch keyOverride { 76 | case .always: doSet = true 77 | case .ifMissing: doSet = db[key] == nil // NX 78 | case .ifExisting: doSet = db[key] != nil // XX 79 | } 80 | 81 | if doSet { 82 | db[key] = redisValue 83 | 84 | if let expiration = expiration { 85 | db[expiration: key] = Date(timeIntervalSinceNow: expiration) 86 | } 87 | else { // YES, SET removes the expiration! 88 | _ = db.removeExpiration(forKey: key) 89 | } 90 | } 91 | } 92 | 93 | if result == .ok { 94 | ctx.write(doSet ? RESPValue.ok : RESPValue.init(bulkString: nil)) 95 | } 96 | else { 97 | ctx.write(doSet) 98 | } 99 | } 100 | 101 | static func SET(_ key: Data, _ value: RESPValue, 102 | _ opts: ArraySlice, in ctx: CommandContext) 103 | throws 104 | { 105 | // [EX seconds] [PX milliseconds] [NX|XX] 106 | var keyOverride : KeyOverrideBehaviour? = nil 107 | var expiration : TimeInterval? = nil 108 | 109 | // Report: slice fails on indexing (4.0) 110 | // TODO => wrong. We just need to use proper indices (start at `startIndex` 111 | // and then advance) 112 | let opts = ContiguousArray(opts) 113 | var i = opts.startIndex 114 | let count = opts.count 115 | while i < count { 116 | guard let s = opts[i].stringValue, !s.isEmpty else { 117 | throw RedisError.syntaxError 118 | } 119 | 120 | switch s { 121 | case "EX", "PX": 122 | guard expiration == nil, (i + 1 < count), 123 | let v = opts[i + 1].intValue 124 | else { 125 | throw RedisError.syntaxError 126 | } 127 | if s == "PX" { expiration = TimeInterval(v) / 1000.0 } 128 | else { expiration = TimeInterval(v) } 129 | i += 1 130 | 131 | case "NX", "XX": 132 | guard keyOverride == nil else { throw RedisError.syntaxError } 133 | keyOverride = s == "NX" ? .ifMissing : .ifExisting 134 | 135 | default: throw RedisError.syntaxError 136 | } 137 | 138 | i += 1 139 | } 140 | 141 | try set(key, value, 142 | keyOverride: keyOverride ?? .always, 143 | expiration: expiration, result: .ok, 144 | in: ctx) 145 | } 146 | 147 | static func SETNX(_ key: Data, _ value: RESPValue, 148 | in ctx: CommandContext) throws 149 | { 150 | try set(key, value, keyOverride: .ifMissing, result: .bool, in: ctx) 151 | } 152 | 153 | static func SETEX(_ key: Data, _ seconds: RESPValue, _ value: RESPValue, 154 | in ctx: CommandContext) throws 155 | { 156 | let inMilliseconds = ctx.command.name.hasPrefix("P") 157 | guard let v = seconds.intValue else { throw RedisError.notAnInteger } 158 | 159 | let timeout = inMilliseconds ? (TimeInterval(v) / 1000.0) : TimeInterval(v) 160 | try set(key, value, expiration: timeout, result: .ok, in: ctx) 161 | } 162 | 163 | 164 | static func MSET(pairs: ContiguousArray< ( Data, RESPValue )>, 165 | in ctx: CommandContext) 166 | throws 167 | { 168 | let redisPairs = try pairs.lazy.map { 169 | ( pair : ( Data, RESPValue ) ) -> ( Data, RedisValue ) in 170 | 171 | guard let redisValue = RedisValue(string: pair.1) else { 172 | throw RedisError.wrongType 173 | } 174 | return ( pair.0, redisValue ) 175 | } 176 | 177 | ctx.writeInDatabase { db in 178 | for ( key, value ) in redisPairs { 179 | db[key] = value 180 | } 181 | 182 | ctx.write(RESPValue.ok) 183 | } 184 | } 185 | 186 | static func MSETNX(pairs: ContiguousArray< ( Data, RESPValue )>, 187 | in ctx: CommandContext) 188 | throws 189 | { 190 | let redisPairs = try pairs.lazy.map { 191 | ( pair : ( Data, RESPValue ) ) -> ( Data, RedisValue ) in 192 | 193 | guard let redisValue = RedisValue(string: pair.1) else { 194 | throw RedisError.wrongType 195 | } 196 | return ( pair.0, redisValue ) 197 | } 198 | 199 | let result : Bool = ctx.writeInDatabase { db in 200 | for ( key, _ ) in redisPairs { 201 | if db[key] != nil { return false } 202 | } 203 | 204 | for ( key, value ) in redisPairs { 205 | db[key] = value 206 | } 207 | 208 | return true 209 | } 210 | ctx.write(result) 211 | } 212 | 213 | static func GETSET(_ key: Data, _ value: RESPValue, in ctx: CommandContext) 214 | throws 215 | { 216 | guard let redisValue = RedisValue(string: value) else { 217 | throw RedisError.wrongType 218 | } 219 | 220 | ctx.writeInDatabase { db in 221 | 222 | let value = db[key] 223 | db[key] = redisValue 224 | db[expiration: key] = nil 225 | 226 | ctx.eventLoop.execute { 227 | guard let value = value else { return ctx.write(.bulkString(nil)) } 228 | ctx.write(value) 229 | } 230 | } 231 | } 232 | 233 | static func SETRANGE(key: Data, index: Int, value: RESPValue, 234 | in ctx: CommandContext) throws 235 | { 236 | guard index >= 0 else { throw RedisError.indexOutOfRange } 237 | guard var bb = value.byteBuffer else { throw RedisError.wrongType } 238 | 239 | let result : Int = try ctx.writeInDatabase { db in 240 | var s : ByteBuffer 241 | 242 | if let value = db[key] { 243 | guard case .string(let olds) = value else { 244 | throw RedisError.wrongType 245 | } 246 | s = olds 247 | } 248 | else { 249 | let size = index + bb.readableBytes + 1 250 | let alloc = ByteBufferAllocator() 251 | s = alloc.buffer(capacity: size) 252 | } 253 | 254 | if index > s.readableBytes { // if index > count, 0-padded!!! 255 | let countToWrite = index - s.readableBytes 256 | s.writeRepeatingByte(0, count: countToWrite) 257 | } 258 | 259 | s.moveWriterIndex(to: s.readerIndex + index) 260 | s.writeBuffer(&bb) 261 | 262 | db[key] = .string(s) 263 | return s.readableBytes 264 | } 265 | 266 | ctx.write(result) 267 | } 268 | 269 | 270 | // MARK: - Read Commands 271 | 272 | static func GET(_ key: Data, in ctx: CommandContext) throws { 273 | ctx.get(key) { value in 274 | guard let value = value else { return ctx.write(.bulkString(nil)) } 275 | guard case .string(_) = value else { throw RedisError.wrongType } 276 | ctx.write(value) 277 | } 278 | } 279 | 280 | static func STRLEN(_ key: Data, in ctx: CommandContext) throws { 281 | ctx.get(key) { value in 282 | guard let value = value else { return ctx.write(0) } 283 | guard case .string(let s) = value else { throw RedisError.wrongType } 284 | ctx.write(s.readableBytes) 285 | } 286 | } 287 | 288 | static func GETRANGE(key: Data, start: Int, stop: Int, 289 | in ctx: CommandContext) throws 290 | { 291 | ctx.get(key) { value in 292 | guard let value = value else { return ctx.write("") } 293 | guard case .string(let s) = value else { throw RedisError.wrongType } 294 | 295 | let count = s.readableBytes 296 | if count == 0 { return ctx.write("") } 297 | 298 | let range = s.rangeForRedisRange(start: start, stop: stop) 299 | if range.isEmpty { return ctx.write("") } 300 | 301 | let from = s.readerIndex + range.lowerBound 302 | guard let slice = s.getSlice(at: from, length: range.count) else { 303 | throw RedisError.indexOutOfRange 304 | } 305 | 306 | ctx.write(slice) 307 | } 308 | } 309 | 310 | static func MGET(keys: ContiguousArray, in ctx: CommandContext) throws { 311 | let count = keys.count 312 | if count == 0 { return ctx.write([]) } 313 | 314 | let values : ContiguousArray = ctx.readInDatabase { db in 315 | 316 | var values = ContiguousArray() 317 | values.reserveCapacity(count) 318 | 319 | for key in keys { 320 | if let value = db[key] { 321 | if case .string(let s) = value { values.append(.bulkString(s)) } 322 | else { values.append(.bulkString(nil)) } 323 | } 324 | else { 325 | values.append(.bulkString(nil)) 326 | } 327 | } 328 | return values 329 | } 330 | 331 | ctx.write(.array(values)) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Sources/RedisServer/Database/DumpManager.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | protocol DumpManager { 16 | 17 | func saveDump(of databases: Databases, to url: URL, asynchronously: Bool) 18 | throws 19 | 20 | func loadDumpIfAvailable(url: URL, configuration: RedisServer.Configuration) 21 | -> Databases 22 | 23 | func getLastSave(_ cb: @escaping ( Date, TimeInterval ) -> Void) 24 | } 25 | 26 | 27 | // MARK: - Hackish, slow and wasteful store 28 | 29 | import Dispatch 30 | import Foundation 31 | 32 | extension CodingUserInfoKey { 33 | static let dbContext = 34 | CodingUserInfoKey(rawValue: "de.zeezide.nio.redis.dbs.context")! 35 | } 36 | 37 | extension Decoder { 38 | 39 | var dbContext : Databases.Context? { 40 | return userInfo[CodingUserInfoKey.dbContext] as? Databases.Context 41 | } 42 | 43 | } 44 | 45 | 46 | enum RedisDumpError : Swift.Error { 47 | case missingDatabaseContext 48 | case internalError 49 | case unexpectedValueType(String) 50 | } 51 | 52 | class SimpleJSONDumpManager : DumpManager { 53 | 54 | let Q = DispatchQueue(label: "de.zeezide.nio.redisd.dump") 55 | let logger : RedisLogger 56 | 57 | weak var server : RedisServer? // FIXME: this is not great, cleanup 58 | 59 | private var lastSave = Date.distantPast 60 | private var lastSaveDuration : TimeInterval = 0 61 | 62 | private var scheduledDate : Date? 63 | private var workItem : DispatchWorkItem? 64 | 65 | init(server : RedisServer) { 66 | self.server = server 67 | self.logger = server.logger 68 | } 69 | 70 | 71 | // MARK: - Database Triggered Saves 72 | 73 | func _scheduleSave(in ti: TimeInterval) { // Q: own 74 | let now = Date() 75 | let deadline = now.addingTimeInterval(ti) 76 | 77 | if let oldDate = scheduledDate, oldDate < deadline { return } 78 | 79 | scheduledDate = nil 80 | workItem?.cancel() 81 | workItem = nil 82 | 83 | workItem = DispatchWorkItem() { [weak self] in 84 | guard let me = self else { return } 85 | me.scheduledDate = nil 86 | me.workItem = nil // hope we are arc'ed :-> 87 | 88 | if let server = me.server, let dbs = server.databases { 89 | do { 90 | // reset counter 91 | do { 92 | let dbContext = dbs.context 93 | dbContext.lock.lockForWriting() 94 | defer { dbContext.lock.unlock() } 95 | 96 | for db in dbs.databases { 97 | db.changeCountSinceLastSave = 0 98 | } 99 | } 100 | 101 | let ( lastSave, diff ) = try me._saveDump(of: dbs, to: server.dumpURL) 102 | me.lastSave = lastSave 103 | me.lastSaveDuration = diff 104 | } 105 | catch { 106 | me.logger.error("scheduled save failed:", error) 107 | } 108 | } 109 | } 110 | 111 | let walltime = DispatchWallTime(date: deadline) 112 | 113 | scheduledDate = deadline 114 | Q.asyncAfter(wallDeadline: walltime, execute: workItem!) 115 | } 116 | 117 | 118 | // MARK: - Command Triggered Operations 119 | 120 | func getLastSave(_ cb: @escaping ( Date, TimeInterval ) -> Void) { 121 | Q.async { 122 | cb(self.lastSave, self.lastSaveDuration) 123 | } 124 | } 125 | 126 | func saveDump(of databases: Databases, to url: URL, asynchronously: Bool) 127 | throws 128 | { 129 | if !asynchronously { 130 | let ( lastSave, diff ) = try self._saveDump(of: databases, to: url) 131 | Q.async { 132 | self.lastSave = lastSave 133 | self.lastSaveDuration = diff 134 | } 135 | } 136 | else { 137 | Q.async { 138 | do { 139 | let ( lastSave, diff ) = try self._saveDump(of: databases, to: url) 140 | self.lastSave = lastSave 141 | self.lastSaveDuration = diff 142 | } 143 | catch { 144 | self.logger.error("asynchronous save failed:", error) 145 | } 146 | } 147 | } 148 | } 149 | 150 | func _saveDump(of databases: Databases, to url: URL) throws 151 | -> ( Date, TimeInterval ) 152 | { 153 | let start = Date() 154 | 155 | do { 156 | let encoder = JSONEncoder() 157 | let data = try encoder.encode(databases) 158 | try data.write(to: url, options: .atomic) 159 | } 160 | 161 | let done = Date() 162 | return ( done, done.timeIntervalSince(start) ) 163 | } 164 | 165 | func makeFreshDatabases(with context: Databases.Context) 166 | -> Databases 167 | { 168 | return Databases(context: context) 169 | 170 | } 171 | 172 | func loadDumpIfAvailable(url: URL, configuration: RedisServer.Configuration) 173 | -> Databases 174 | { 175 | let start = Date() 176 | 177 | // FIXME: The dump manager should manage the dumping and counting. The 178 | // DB should just report changes. 179 | let dbContext = 180 | Databases.Context(savePoints: configuration.savePoints ?? [], 181 | onSavePoint: { [weak self] db, savePoint in 182 | guard let me = self else { return } 183 | // Careful: running in DB thread 184 | me.Q.async { 185 | me._scheduleSave(in: savePoint.delay) 186 | } 187 | }) 188 | 189 | let fm = FileManager() 190 | 191 | guard let data = fm.contents(atPath: url.path), data.count > 2 else { 192 | return makeFreshDatabases(with: dbContext) 193 | } 194 | 195 | let decoder = JSONDecoder() 196 | decoder.userInfo[CodingUserInfoKey.dbContext] = dbContext 197 | 198 | let dbs : Databases 199 | do { 200 | dbs = try decoder.decode(Databases.self, from: data) 201 | } 202 | catch { 203 | logger.error("failed to decode dump:", url.path, error) 204 | return makeFreshDatabases(with: dbContext) 205 | } 206 | 207 | let diff = Date().timeIntervalSince(start) 208 | let diffs = timeDiffFormatter.string(from: NSNumber(value: diff)) ?? "-" 209 | logger.log("DB loaded from disk: \(diffs) seconds") 210 | 211 | do { 212 | let lock = dbs.context.lock 213 | lock.lockForWriting() 214 | defer { lock.unlock() } 215 | 216 | for db in dbs.databases { 217 | db.scheduleExpiration(Date()) 218 | } 219 | } 220 | 221 | return dbs 222 | } 223 | 224 | } 225 | 226 | fileprivate let timeDiffFormatter : NumberFormatter = { 227 | let nf = NumberFormatter() 228 | nf.locale = Locale(identifier: "en_US") 229 | nf.minimumIntegerDigits = 1 230 | nf.minimumFractionDigits = 3 231 | nf.maximumFractionDigits = 3 232 | return nf 233 | }() 234 | -------------------------------------------------------------------------------- /Sources/RedisServer/Helpers/RedisLogger.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | public protocol RedisLogger { 16 | 17 | typealias LogLevel = RedisLogLevel 18 | 19 | func primaryLog(_ logLevel: LogLevel, _ msgfunc: () -> String, 20 | _ values: [ Any? ] ) 21 | } 22 | 23 | public extension RedisLogger { 24 | 25 | @inlinable 26 | func error(_ msg: @autoclosure () -> String, _ values: Any?...) { 27 | primaryLog(.Error, msg, values) 28 | } 29 | @inlinable 30 | func warn (_ msg: @autoclosure () -> String, _ values: Any?...) { 31 | primaryLog(.Warn, msg, values) 32 | } 33 | @inlinable 34 | func log (_ msg: @autoclosure () -> String, _ values: Any?...) { 35 | primaryLog(.Log, msg, values) 36 | } 37 | @inlinable 38 | func info (_ msg: @autoclosure () -> String, _ values: Any?...) { 39 | primaryLog(.Info, msg, values) 40 | } 41 | @inlinable 42 | func trace(_ msg: @autoclosure () -> String, _ values: Any?...) { 43 | primaryLog(.Trace, msg, values) 44 | } 45 | } 46 | 47 | public enum RedisLogLevel : Int8 { 48 | case Error 49 | case Warn 50 | case Log 51 | case Info 52 | case Trace 53 | 54 | var logStamp : String { 55 | switch self { 56 | case .Error: return "!" 57 | case .Warn: return "#" 58 | case .Info: return "-" 59 | case .Trace: return "." 60 | case .Log: return "*" 61 | } 62 | } 63 | 64 | var logPrefix : String { 65 | switch self { 66 | case .Error: return "ERROR: " 67 | case .Warn: return "WARN: " 68 | case .Info: return "INFO: " 69 | case .Trace: return "Trace: " 70 | case .Log: return "" 71 | } 72 | } 73 | } 74 | 75 | 76 | // MARK: - Simple Default Logger 77 | 78 | import struct Foundation.Date 79 | import class Foundation.DateFormatter 80 | 81 | #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) 82 | import Darwin 83 | #else 84 | import Glibc 85 | #endif 86 | 87 | fileprivate let redisLogDateFmt : DateFormatter = { 88 | let formatter = DateFormatter() 89 | formatter.dateFormat = "dd MMM HH:mm:ss.SSS" 90 | return formatter 91 | }() 92 | 93 | private let pid = getpid() 94 | 95 | public struct RedisPrintLogger : RedisLogger { 96 | 97 | public let logLevel : LogLevel 98 | 99 | @inlinable 100 | public init(logLevel: LogLevel = .Log) { 101 | self.logLevel = logLevel 102 | } 103 | 104 | public func primaryLog(_ logLevel : LogLevel, 105 | _ msgfunc : () -> String, 106 | _ values : [ Any? ] ) 107 | { 108 | guard logLevel.rawValue <= self.logLevel.rawValue else { return } 109 | 110 | let now = Date() 111 | 112 | let prefix = 113 | "\(pid):M \(redisLogDateFmt.string(from: now)) \(logLevel.logStamp) " 114 | let s = msgfunc() 115 | 116 | if values.isEmpty { 117 | print("\(prefix)\(s)") 118 | } 119 | else { 120 | var ms = "" 121 | appendValues(values, to: &ms) 122 | print("\(prefix)\(s)\(ms)") 123 | } 124 | } 125 | 126 | func appendValues(_ values: [ Any? ], to ms: inout String) { 127 | for v in values { 128 | ms += " " 129 | 130 | if let v = v as? CustomStringConvertible { ms += v.description } 131 | else if let v = v as? String { ms += v } 132 | else if let v = v { ms += "\(v)" } 133 | else { ms += "" } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/RedisServer/Helpers/RedisPattern.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct Foundation.Data 16 | import struct NIO.ByteBuffer 17 | 18 | /** 19 | * A very incomplete implementation of the Pattern that can be used in the 20 | * `KEYS` command. 21 | * 22 | * This one only does: 23 | * - match all: `*` 24 | * - prefix, suffix, infix: `*str`, `str*`, `*str*` 25 | * - exact match: `str` 26 | */ 27 | enum RedisPattern : Hashable { 28 | 29 | case matchAll 30 | case prefix(Data) 31 | case suffix(Data) 32 | case infix(Data) 33 | case exact(Data) 34 | 35 | init?(_ s: ByteBuffer) { 36 | guard let p = RedisPattern.parse(s) else { return nil } 37 | self = p 38 | } 39 | 40 | func match(_ data: Data) -> Bool { 41 | switch self { 42 | case .matchAll: return true 43 | case .exact (let match): return data == match 44 | case .prefix(let match): return data.hasPrefix(match) 45 | case .suffix(let match): return data.hasSuffix(match) 46 | case .infix (let match): return data.contains (match) 47 | } 48 | } 49 | 50 | #if swift(>=4.1) 51 | #else 52 | var hashValue: Int { // lolz 53 | switch self { 54 | case .matchAll: return 1337 55 | case .exact (let match): return match.hashValue 56 | case .prefix(let match): return match.hashValue 57 | case .suffix(let match): return match.hashValue 58 | case .infix (let match): return match.hashValue 59 | } 60 | } 61 | 62 | static func ==(lhs: RedisPattern, rhs: RedisPattern) -> Bool { 63 | switch ( lhs, rhs ) { 64 | case ( .matchAll, .matchAll ): return true 65 | case ( .exact (let lhs), .exact (let rhs) ): return lhs == rhs 66 | case ( .prefix(let lhs), .prefix(let rhs) ): return lhs == rhs 67 | case ( .suffix(let lhs), .suffix(let rhs) ): return lhs == rhs 68 | case ( .infix (let lhs), .infix (let rhs) ): return lhs == rhs 69 | default: return false 70 | } 71 | } 72 | #endif 73 | 74 | private static func parse(_ s: ByteBuffer) -> RedisPattern? { 75 | return s.withUnsafeReadableBytes { bptr in 76 | let cStar : UInt8 = 42 // * 77 | let cBackslash : UInt8 = 92 // \ 78 | let cCaret : UInt8 = 94 // ^ 79 | let cQMark : UInt8 = 63 // ? 80 | let cLBrack : UInt8 = 91 // [ 81 | 82 | if bptr.count == 0 { return .exact(Data()) } 83 | if bptr.count == 1 && bptr[0] == cStar { return .matchAll } 84 | 85 | var hasLeadingStar = false 86 | var hasTrailingStar = false 87 | var i = 0 88 | let count = bptr.count 89 | 90 | while i < count { 91 | if bptr[i] == cBackslash { i += 2; continue } 92 | 93 | switch bptr[i] { 94 | case cCaret, cQMark, cLBrack: // no support for kewl stuff 95 | return nil 96 | 97 | case cStar: 98 | if i == 0 { hasLeadingStar = true } 99 | else if i == (bptr.count - 1) { hasTrailingStar = true } 100 | else { return nil } 101 | 102 | default: break 103 | } 104 | i += 1 105 | } 106 | 107 | switch ( hasLeadingStar, hasTrailingStar ) { 108 | case ( false, false ): 109 | return .exact(s.getData(at: s.readerIndex, length: count)!) 110 | 111 | case ( true, false ): 112 | return .suffix(s.getData(at: s.readerIndex + 1, length: count - 1)!) 113 | 114 | case ( false, true ): 115 | return .prefix(s.getData(at: s.readerIndex, length: count - 1)!) 116 | 117 | case ( true, true ): 118 | return .infix(s.getData(at: s.readerIndex + 1, length: count - 2)!) 119 | } 120 | } 121 | } 122 | 123 | } 124 | 125 | extension Data { 126 | 127 | var bytesStr : String { 128 | return self.map { String($0) }.joined(separator: " ") 129 | } 130 | 131 | func hasPrefix(_ other: Data) -> Bool { 132 | return starts(with: other) 133 | } 134 | 135 | func hasSuffix(_ other: Data) -> Bool { 136 | guard count >= other.count else { return false } 137 | return other.starts(with: suffix(other.count)) 138 | } 139 | 140 | func contains(_ other: Data) -> Bool { 141 | return range(of: other) != nil 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /Sources/RedisServer/Helpers/Utilities.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Dispatch 16 | import Foundation 17 | 18 | extension DispatchWallTime { 19 | 20 | init(date: Date) { 21 | // TBD: is this sound? hm. 22 | let ti = date.timeIntervalSince1970 23 | let secs = Int(ti) 24 | let nsecs = Int((ti - TimeInterval(secs)) * 1_000_000_000) 25 | self.init(timespec: timespec(tv_sec: secs, tv_nsec: nsecs)) 26 | } 27 | 28 | } 29 | 30 | final class RWLock { 31 | 32 | private var lock = pthread_rwlock_t() 33 | 34 | public init() { 35 | pthread_rwlock_init(&lock, nil) 36 | } 37 | deinit { 38 | pthread_rwlock_destroy(&lock) 39 | } 40 | 41 | @inline(__always) 42 | func lockForReading() { 43 | pthread_rwlock_rdlock(&lock) 44 | } 45 | 46 | @inline(__always) 47 | func lockForWriting() { 48 | pthread_rwlock_wrlock(&lock) 49 | } 50 | 51 | @inline(__always) 52 | func unlock() { 53 | pthread_rwlock_unlock(&lock) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Sources/RedisServer/Performance.md: -------------------------------------------------------------------------------- 1 | # Redi/S - Performance 2 | 3 | Questions on any of that? 4 | Either Twitter [@helje5](https://twitter.com/helje5), 5 | or join the `#swift-nio` channel on the 6 | [swift-server Slack](https://t.co/W1vfsb9JAB). 7 | 8 | 9 | ## Todos 10 | 11 | There are still a few things which could be easily optimized a lot regardless of 12 | bigger architectural changes: 13 | 14 | - integer backed store for strings (INCR/DECR) 15 | - do proper in-place modifications for sets 16 | 17 | 18 | ## Copy on Write 19 | 20 | The current implementation is based around Swift's value types. 21 | The idea is/was to make heavy use of the Copy-on-Write features and thereby 22 | unblock the database thread as quickly as possible. 23 | 24 | For example if we deliver a result, we only grab the result in the locked DB context, 25 | all the rendering and socket delivery is happening in a NIO eventloop thread. 26 | 27 | The same goes for persistence. We can grab the current value of the database 28 | dictionary and persist that, w/o any extra locking 29 | (though C Redis is much more efficient w/ the fork approach ...) 30 | 31 | > There is another flaw here. The "copy" will happen in the database scope, 32 | > which obviously is sub-optimal. (Redis CoW by forking the process is much 33 | > more performant ...) 34 | 35 | 36 | ## Data Structures 37 | 38 | Redi/S is using just regular Swift datastructures 39 | (and is therefore also a test of the scalability of those). 40 | 41 | Most importantly this currently uses Array's for lists! 🤦‍♀️ 42 | Means: 43 | RPUSH is reasonably fast, but occasionally requires a realloc/copy. 44 | LPUSH is very slow. 45 | 46 | Plan: To make LPUSH faster we could use the NIO.CircularBuffer. 47 | [If we get some more methods](https://github.com/apple/swift-nio/issues/279) 48 | on it. 49 | 50 | The real fix is to use proper lists etc. 51 | But if we approach this, we also need to reconsider CoW. 52 | 53 | 54 | ## Concurrency 55 | 56 | How many eventloop threads are the sweet spot? 57 | 58 | - Is it 1, avoiding all synchronization overhead? 59 | - Is it `System.coreCount`, putting all CPUs to work? 60 | - Is it `System.coreCount / 2`, excluding hyper-threads? 61 | 62 | We benchmarked the server on 63 | a 13" MBP - 2 Cores, 4 hyperthreads, 64 | and on 65 | a MacPro 2013 - 4 Cores, 8 hyperthreads. 66 | 67 | Surprisingly *2* seems to be the sweet spot. 68 | Not quite sure yet why. 69 | Is that when the worker thread is saturated? It doesn't seems so. 70 | 71 | Running the MT-aware version on a single eventloop thread halves the 72 | performance. 73 | 74 | Notably running a SingleThread optimized version still reached ~75% of the 75 | dual thread variant (but at a lower CPU load). 76 | 77 | 78 | ## Tested Optimizations 79 | 80 | Trying to improve performance, we've tested a few setups we thought might 81 | do the trick. 82 | 83 | ### Command Name as Data 84 | 85 | This version uses a Swift `String` to represent command names. 86 | That appears to be wasteful (because a Swift string is an expensive Unicode 87 | String), 88 | but actually seems to have no measurable performance impact. 89 | 90 | We tested a branch in which the command-name is wrapped in a plain `Data` 91 | and used that as a key. 92 | 93 | Potential follow up: 94 | Command lookup seems to play no significant role, 95 | but one thing we might try is to wrap the ByteBuffer in a small struct 96 | w/ an efficient and targetted, case-insensitive hash. 97 | 98 | ### Avoid NIO Pipeline for non-BB 99 | 100 | The "idea" in NIO is that you form a pipeline of handlers. 101 | At the base of that pipeline is the socket, which pushes and receives 102 | `ByteBuffer`s from that pipeline. 103 | The handlers can then perform a set of transformations. 104 | And one thing they can do, is parse the `ByteBuffer`s into higher level 105 | objects. 106 | 107 | This is what we did originally (0.5.0) release: 108 | 109 | ``` 110 | Socket 111 | =(BB)=> 112 | NIORedis.RESPChannelHandler 113 | =(RESPValue)=> 114 | RedisServer.RedisCommandHandler 115 | <=(RESPValue) 116 | NIORedis.RESPChannelHandler 117 | <=(BB)= 118 | Socket 119 | ``` 120 | 121 | When values travel the NIO pipeline, they are boxed in `NIOAny` objects. 122 | Crazy enough just this boxing has a very high overhead for non-ByteBuffer 123 | objects, i.e. putting `RESPValue`s in and out of `NIOAny` while passing 124 | them from the parser to the command handler, takes about *9%* of the runtime 125 | (at least in a sample below ...). 126 | 127 | To workaround that, `RedisCommandHandler` is now a *subclass* 128 | of `RESPChannelHandler`. 129 | This way we never wrap non-ByteBuffer objects in `NIOAny` and the pipeline 130 | looks like this: 131 | 132 | ``` 133 | Socket 134 | =(BB)=> 135 | RedisServer.RedisCommandHandler : NIORedis.RESPChannelHandler 136 | <=(BB)= 137 | Socket 138 | ``` 139 | 140 | We do not have a completely idle system for more exact performance testing, 141 | but this seems to lead to a 3-10% speedup (measurements vary quite a bit). 142 | 143 | Follow-up: 144 | - get `MemoryLayout.size` down to max 24, and we can avoid a malloc 145 | - but `ByteBuffer` (and `Data`) are already 24 146 | - made `RESPError` class backed in swift-nio-redis. Reduces size of 147 | `RESPValue` from 49 to 25 bytes (still 1 byte too much) 148 | - @weissi suggest backing `RESPValue` w/ a class storage as well, 149 | we might try that. Though it takes away yet another Swift feature (enums) 150 | for the sake of performance. 151 | 152 | 153 | ### Worker Sync Variants 154 | 155 | #### GCD DispatchQueue for synchronization 156 | 157 | Originally the project used a `DispatchQueue` to synchronize access to the 158 | in-memory databases. 159 | 160 | The overhead of this is pretty high, so we switched to a RWLock for a ~10% speedup. 161 | But you don't lock a NIO thread you say?! 162 | Well, this is all very fast in-memory database access which in *this specific case* 163 | is actually faster than the capturing a dispatch block and submitting that to a queue 164 | (which also involves a lock ...) 165 | 166 | #### NIO.EventLoop instead of GCD 167 | 168 | We wondered whether a `NIO.EventLoop` might be faster then a `DispatchQueue` 169 | as the single threaded synchronization point for the worker thread 170 | (`loop.execute` replacing `queue.async`). 171 | 172 | There is no measurable difference. GCD is a tinsy bit faster. 173 | 174 | #### Single Threaded 175 | 176 | Also tested a version with no threading at all (Node.js/Noze.io style). 177 | That is, not just lowering the thread-count to 1, but taking out all `.async` 178 | and `.execute` calls. 179 | 180 | This is surprisingly fast, the synchronization overhead of `EventLoop.execute` 181 | and `DispatchQueue.async` is very high. 182 | 183 | Running a single-thread optimized version still reached ~75% of the 184 | dual thread variant (but at a lower CPU load). 185 | 186 | Follow up: 187 | If we would take out CoW data structures, which wouldn't be necessary anymore 188 | in the single-threaded setup, it sounds quite likely that this might go faster 189 | than the threaded variant. 190 | 191 | 192 | ## Instruments 193 | 194 | I've been running Instruments on Redi/S. With SwiftNIO 1.3.1. 195 | Below annotated callstacks. 196 | 197 | Notes: 198 | - just `NIOAny` boxing (passing RESPValues in the NIO pipeline) has an overhead 199 | of *9%*! 200 | - this probably implies that just directly embedding NIORedis into 201 | RedisServer would lead to that speedup. 202 | - from `flush` to `Posix.write` takes NIO another 10% 203 | 204 | ### Single Threaded 205 | 206 | This is the single threaded version, to remove synchronization overhead 207 | from the picture. 208 | 209 | ``` 210 | redis-benchmark -p 1337 -t get -n 1000000 -q 211 | ``` 212 | 213 | - Selector.whenReady: 98.4% 214 | - KQueue.kevent 2.1% 215 | - handleEvent 95.4% 216 | - readFromSocket 89.8% 217 | - Posix.read 8.7% 218 | - RedisChannel.read() 77.2% 219 | - decodedValue(_:in:) 71.2% 220 | - 1.3% alloc/dealloc 221 | - decodedValue(:in:) 68.8% 222 | - wrapInbountOut: 1.8% 223 | - RedisCommandHandler: 66.2% (parsing ~11%) 224 | - unwrapInboundIn: 1.7% 225 | - parseCommandCall: 4.7% 226 | - Dealloc 1.3% 227 | - stringValue 1.3% (getString) 228 | - Uppercased 0.7% 229 | - callCommand: 55.3% 230 | - Alloc/dealloc 2% 231 | - withKeyValue 51.6% 232 | - release_Dealloc - 1.6% 233 | - Data init, still using alloc! 0.2% 234 | - Commands.GET 48.4% 235 | - ctx.write(46.8%) 236 | - writeAndFlush 45% 237 | - RedisChannelHandler.write 8% 238 | - Specialised RedisChannelHandler.write 6.7% 239 | - unwrapOutboundIn 2.6% 240 | - wrapOutboundOut 0.6% 241 | - ctx.write 2.8% 242 | - Unwrap 2.5% 243 | - Flush 36.2% 244 | - pendingWritesManager 32.7% 245 | - Posix.write 26.3% 246 | - NIOAny 1.2% 247 | - Allocated-boxed-opaque-existential 248 | 249 | ### Multi Threaded w/ GCD Worker Thread 250 | 251 | - Instruments crashed once, so numbers are not 100% exact, but very close 252 | 253 | ``` 254 | redis-benchmark -p 1337 -t set -n something -q 255 | ``` 256 | 257 | - GCD: worker queue 17.3% 258 | - GCD overhead till callout: 3% 259 | - worker closure: 14.3% 260 | - SET: 13.8%, 12.8% in closure 261 | - ~2% own code 262 | - 11% in: 263 | - 5% nativeUpdateValue(_:forKey:) 264 | - 1.3% nativeRemoveObject(forKey:) 265 | - 4.7% SelectableEventLoop.execute (malloc + locks!) 266 | - Summary: raw database ops: 5.3%, write-sync 4.7%, GCD sync 3%+, own ~2% 267 | - EventLoops: 82.3%, .run 81.4% 268 | - PriorityQueue:4.8% 269 | - alloc/free 2.1% 270 | - invoke 271 | - READ PATH - 37.9% 272 | - selector.whenReady 36.1% 273 | - KQueue.kevent(6.9%) 274 | - handleEvent (28.7%) 275 | - readComplete 2.1% 276 | - flush 1.4% **** removed flush in cmdhandler 277 | - readFromSocket(25%) 278 | - socket.read 5.3% 279 | - Posix.read 4.9% 280 | - alloc 0.7% 281 | - invokeChannelRead 18.2% 282 | - RedisChannel.read 17.6% (Overhead: Parser=>Cmd: 5.2%) ** 283 | - 0.4% alloc, 0.3% unwrap 284 | - BB.withUnsafeRB 16.6% (Parser) 285 | - decoded(value:in) 14.9% 286 | - dealloc 0.5%, ContArray.reserveCap 0.2% 287 | - decoded(value:in:) 13.5% (recursive top-level array!) 288 | - wrapInboundOut 0.7% 289 | - fireChannelRead 12.6% 290 | - RedisCmdHandler 12.4% ** 291 | - unwrapInboundIn 1.1% 292 | - parseCmdCall 2.1% 293 | - RESPValue.stringValue 0.6% 294 | - dealloc 0.6% 295 | - upper 0.4% 296 | - hash 0.1% 297 | - callCommand 6.7% 298 | - RESPValue.keyValue 1.4% 299 | - BB.readData(length:) DOES AN alloc? 300 | - the release closure! 301 | - Commands.SET 4.8% 302 | - ContArray.init 0.2% 303 | - runInDB 3.3% (pure sync overhead) 304 | - WRITE PATH - 31.1% (dispatch back from DB thread) 305 | - Commands.set 30.4% 306 | - cmdctx.write 30% (29.6% specialized) - 1.2% own rendering overhead 307 | - writeAndFlush 28.5% 308 | - flush 18.7% 309 | - socket flush 17.9% 310 | - Posix.write 14% 311 | - write 9.6% 312 | - RedisChannelHandler.write 9.6% 313 | - specialised 8.7% ??? 314 | - ByteBuffer.write - 3% 315 | - unwrapOutboundIn - 1.4% 316 | - ctx.write 1.2% (bubble up) 317 | - integer write 1% (buffer.write(integer:endianess:as:) **** 318 | - NIOAny 0.8% 319 | - 1.5% dealloc 320 | -------------------------------------------------------------------------------- /Sources/RedisServer/README.md: -------------------------------------------------------------------------------- 1 |

Redi/S - RedisServer Module 2 | 4 |

5 | 6 | RedisServer is a regular Swift package. You can import and run the server 7 | as part of your own application process. 8 | Or write custom frontends for it. 9 | 10 | ## Using Swift Package Manager 11 | 12 | ```swift 13 | // swift-tools-version:4.0 14 | 15 | import PackageDescription 16 | 17 | let package = Package( 18 | name: "MyRedisServer", 19 | dependencies: [ 20 | .package(url: "https://github.com/NozeIO/redi-s.git", 21 | from: "0.5.0") 22 | ], 23 | targets: [ 24 | .target(name: "MyRedisServer", 25 | dependencies: [ "RedisServer" ]) 26 | ] 27 | ) 28 | ``` 29 | 30 | ## Start Server 31 | 32 | The server can be configured by passing in a `Configuration` object, 33 | but the trivial server looks like this: 34 | 35 | ```swift 36 | import RedisServer 37 | 38 | let server = RedisServer() 39 | server.listenAndWait() 40 | ``` 41 | 42 | Also checkout our [redi-s example frontend](../redi-s/README.md). 43 | 44 | 45 | ### Who 46 | 47 | Brought to you by 48 | [ZeeZide](http://zeezide.de). 49 | We like 50 | [feedback](https://twitter.com/ar_institute), 51 | GitHub stars, 52 | cool [contract work](http://zeezide.com/en/services/services.html), 53 | presumably any form of praise you can think of. 54 | -------------------------------------------------------------------------------- /Sources/RedisServer/Server/Monitor.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import class Foundation.NumberFormatter 16 | import class Foundation.NSNumber 17 | import struct Foundation.Locale 18 | import struct Foundation.Date 19 | 20 | fileprivate let redisMonitorTimestampFormat : NumberFormatter = { 21 | let nf = NumberFormatter() 22 | nf.locale = Locale(identifier: "en_US") 23 | nf.minimumFractionDigits = 6 24 | nf.maximumFractionDigits = 6 25 | return nf 26 | }() 27 | 28 | internal extension RedisCommandHandler.MonitorInfo { 29 | 30 | var redisClientLogLine : String { 31 | let now = Date().timeIntervalSince1970 32 | 33 | var logStr = redisMonitorTimestampFormat.string(from: NSNumber(value: now)) 34 | ?? "-" 35 | 36 | logStr += " [\(db) " 37 | 38 | if let addr = addr { 39 | switch addr { 40 | case .v4(let addr4): logStr += "\(addr4.host):\(addr.port ?? 0)" 41 | case .v6(let addr6): 42 | if addr6.host.hasPrefix(":") { 43 | logStr += "[\(addr6.host)]:\(addr.port ?? 0)" 44 | } 45 | else { logStr += "\(addr6.host):\(addr.port ?? 0)" } 46 | default: logStr += addr.description 47 | } 48 | } 49 | else { logStr += "-" } 50 | logStr += "]" 51 | 52 | if case .array(.some(let callList)) = call { 53 | for v in callList { 54 | logStr += " " 55 | if let s = v.stringValue { logStr += "\"\(s)\"" } 56 | else if let i = v.intValue { logStr += String(i) } 57 | else { logStr += " ?" } 58 | } 59 | } 60 | else { 61 | logStr += " unexpected value type" 62 | } 63 | 64 | return logStr 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/RedisServer/Server/PubSub.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Dispatch 16 | import struct Foundation.Data 17 | import struct NIO.ByteBuffer 18 | import enum NIORedis.RESPValue 19 | 20 | class PubSub { 21 | 22 | typealias SubscriberList = ContiguousArray 23 | typealias LoopSubscribersMap = [ ObjectIdentifier : SubscriberList ] 24 | 25 | let Q : DispatchQueue 26 | var channelToEventLoopToSubscribers = [ Data : LoopSubscribersMap ]() 27 | var patternToEventLoopToSubscribers = [ RedisPattern : LoopSubscribersMap ]() 28 | 29 | let subscriberCapacity = 128 30 | 31 | init(Q: DispatchQueue) { 32 | self.Q = Q 33 | } 34 | 35 | // MARK: - Publish 36 | 37 | func publish(_ channel: Data, _ message: ByteBuffer) -> Int { 38 | var count = 0 39 | 40 | let messagePayload : RESPValue = { 41 | var messageList = ContiguousArray() 42 | messageList.reserveCapacity(3) 43 | messageList.append(RESPValue(simpleString: "message")) 44 | messageList.append(RESPValue(bulkString: channel)) 45 | messageList.append(.bulkString(message)) 46 | return .array(messageList) 47 | }() 48 | 49 | func notifySubscribers(_ map: LoopSubscribersMap) { 50 | for ( _, subscribers ) in map { 51 | guard !subscribers.isEmpty else { continue } 52 | 53 | guard let loop = subscribers[0].eventLoop else { 54 | assert(subscribers[0].eventLoop != nil, "subscriber without loop?!") 55 | continue 56 | } 57 | count += subscribers.count 58 | 59 | loop.execute { 60 | for subscriber in subscribers { 61 | subscriber.handleNotification(messagePayload) 62 | } 63 | } 64 | } 65 | } 66 | 67 | if let exact = channelToEventLoopToSubscribers[channel] { 68 | notifySubscribers(exact) 69 | } 70 | 71 | for ( pattern, loopToSubscribers ) in patternToEventLoopToSubscribers { 72 | guard pattern.match(channel) else { continue } 73 | notifySubscribers(loopToSubscribers) 74 | } 75 | 76 | return count 77 | } 78 | 79 | 80 | // MARK: - Subscribe/Unsubscribe 81 | 82 | func subscribe(_ channel: Data, handler: RedisCommandHandler) { 83 | subscribe(channel, registry: &channelToEventLoopToSubscribers, 84 | handler: handler) 85 | } 86 | 87 | func subscribe(_ pattern: RedisPattern, handler: RedisCommandHandler) { 88 | subscribe(pattern, registry: &patternToEventLoopToSubscribers, 89 | handler: handler) 90 | } 91 | 92 | func unsubscribe(_ channel: Data, handler: RedisCommandHandler) { 93 | unsubscribe(channel, registry: &channelToEventLoopToSubscribers, 94 | handler: handler) 95 | } 96 | 97 | func unsubscribe(_ pattern: RedisPattern, handler: RedisCommandHandler) { 98 | unsubscribe(pattern, registry: &patternToEventLoopToSubscribers, 99 | handler: handler) 100 | } 101 | 102 | @_specialize(where Key == Data) 103 | @_specialize(where Key == RedisPattern) 104 | private func subscribe(_ key: Key, 105 | registry: inout [ Key : LoopSubscribersMap ], 106 | handler: RedisCommandHandler) 107 | { 108 | guard let loop = handler.eventLoop else { 109 | assert(handler.eventLoop != nil, "try to operate on handler w/o loop") 110 | return 111 | } 112 | let loopID = ObjectIdentifier(loop) 113 | 114 | if var loopToSubscribers = registry[key] { 115 | if case nil = loopToSubscribers[loopID]?.append(handler) { 116 | loopToSubscribers[loopID] = 117 | ContiguousArray(handler, capacity: subscriberCapacity) 118 | } 119 | registry[key] = loopToSubscribers 120 | } 121 | else { 122 | registry[key] = 123 | [ loopID : ContiguousArray(handler, capacity: subscriberCapacity) ] 124 | } 125 | } 126 | 127 | @_specialize(where Key == Data) 128 | @_specialize(where Key == RedisPattern) 129 | private func unsubscribe(_ key: Key, 130 | registry: inout [ Key : LoopSubscribersMap ], 131 | handler: RedisCommandHandler) 132 | { 133 | guard let loop = handler.eventLoop else { 134 | assert(handler.eventLoop != nil, "try to operate on handler w/o loop") 135 | return 136 | } 137 | let loopID = ObjectIdentifier(loop) 138 | 139 | guard var loopToSubscribers = registry[key] else { return } 140 | guard var subscribers = loopToSubscribers[loopID] else { return } 141 | guard let idx = subscribers.firstIndex(where: { $0 === handler }) else { 142 | return 143 | } 144 | 145 | subscribers.remove(at: idx) 146 | if subscribers.isEmpty { 147 | loopToSubscribers.removeValue(forKey: loopID) 148 | if loopToSubscribers.isEmpty { 149 | registry.removeValue(forKey: key) 150 | } 151 | else { 152 | registry[key] = loopToSubscribers 153 | } 154 | } 155 | else { 156 | loopToSubscribers[loopID] = subscribers 157 | registry[key] = loopToSubscribers 158 | } 159 | } 160 | } 161 | 162 | fileprivate extension ContiguousArray where Element == RedisCommandHandler { 163 | 164 | init(_ e: Element, capacity: Int) { 165 | self.init(repeating: e, count: 1) 166 | reserveCapacity(capacity) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/RedisServer/Server/RedisCommandContext.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | import NIORedis 17 | import struct Foundation.Data 18 | 19 | /** 20 | * The environment commands need to run. 21 | */ 22 | public struct RedisCommandContext { 23 | 24 | let command : RedisCommand 25 | let handler : RedisCommandHandler 26 | let context : ChannelHandlerContext 27 | let databases : Databases 28 | 29 | 30 | // MARK: - Convenience Accessors 31 | 32 | var database : Databases.Database { 33 | return databases[handler.databaseIndex] 34 | } 35 | 36 | var eventLoop : EventLoop { 37 | return context.eventLoop 38 | } 39 | 40 | 41 | // MARK: - Database Synchronization 42 | 43 | func readInDatabase(_ cb: ( Databases.Database ) throws -> T) throws -> T { 44 | let db = database 45 | 46 | do { 47 | let lock = databases.context.lock 48 | lock.lockForReading() 49 | defer { lock.unlock() } 50 | 51 | return try cb(db) 52 | } 53 | catch let error as RedisError { throw error } 54 | catch { fatalError("unexpected error: \(error)") } 55 | } 56 | func readInDatabase(_ cb: ( Databases.Database ) -> T) -> T { 57 | let db = database 58 | 59 | let lock = databases.context.lock 60 | lock.lockForReading() 61 | defer { lock.unlock() } 62 | 63 | return cb(db) 64 | } 65 | 66 | func writeInDatabase(_ cb: (Databases.Database) throws -> T) throws -> T { 67 | let db = database 68 | 69 | do { 70 | let lock = databases.context.lock 71 | lock.lockForWriting() 72 | defer { lock.unlock() } 73 | 74 | return try cb(db) 75 | } 76 | catch let error as RedisError { throw error } 77 | catch { fatalError("unexpected error: \(error)") } 78 | } 79 | 80 | func writeInDatabase(_ cb: ( Databases.Database ) -> T) -> T { 81 | let db = database 82 | 83 | let lock = databases.context.lock 84 | lock.lockForWriting() 85 | defer { lock.unlock() } 86 | 87 | return cb(db) 88 | } 89 | 90 | func get(_ key: Data, _ cb: ( RedisValue? ) throws -> Void) { 91 | let db = database 92 | let loop = eventLoop 93 | 94 | let value : RedisValue? 95 | 96 | do { 97 | let lock = databases.context.lock 98 | lock.lockForReading() 99 | defer { lock.unlock() } 100 | 101 | value = db[key] 102 | } 103 | 104 | assert(loop.inEventLoop) 105 | 106 | do { return try cb(value) } 107 | catch let error as RedisError { self.write(error) } 108 | catch { fatalError("unexpected error: \(error)") } 109 | } 110 | 111 | 112 | // MARK: - Write output 113 | 114 | @_specialize(where T == Int) 115 | @_specialize(where T == String) 116 | func write(_ value: T, flush: Bool = true) { 117 | let context = self.context 118 | let handler = self.handler 119 | 120 | if eventLoop.inEventLoop { 121 | handler.write(context: context, value: value.toRESPValue(), promise: nil) 122 | if flush { context.channel.flush() } 123 | } 124 | else { 125 | eventLoop.execute { 126 | handler.write(context: context, value: value.toRESPValue(), 127 | promise: nil) 128 | if flush { context.channel.flush() } 129 | } 130 | } 131 | } 132 | 133 | func write(_ value: RESPValue, flush: Bool = true) { 134 | let context = self.context 135 | let handler = self.handler 136 | 137 | if eventLoop.inEventLoop { 138 | handler.write(context: context, value: value, promise: nil) 139 | if flush { context.channel.flush() } 140 | } 141 | else { 142 | eventLoop.execute { 143 | handler.write(context: context, value: value, promise: nil) 144 | if flush { context.channel.flush() } 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/RedisServer/Server/RedisCommandHandler.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | import NIORedis 17 | import class Atomics.ManagedAtomic 18 | import struct Foundation.Data 19 | import struct Foundation.Date 20 | import struct Foundation.TimeInterval 21 | 22 | /** 23 | * Redis commands are sent as plain RESP arrays. For example 24 | * 25 | * SET answer 42 26 | * 27 | * Arrives as: 28 | * 29 | * [ "SET", "answer", 42 ] 30 | * 31 | */ 32 | final class RedisCommandHandler : RESPChannelHandler { 33 | // See [Avoid NIO Pipeline](Performance.md#avoid-nio-pipeline-for-non-bb) 34 | // for the reason why this is a subclass (instead of a consumer of the 35 | // RESPChannelHandler producer. 36 | 37 | public typealias Context = RedisCommandContext 38 | public typealias Command = RedisCommand 39 | 40 | let id : Int 41 | let creationDate = Date() 42 | let server : RedisServer // intentional cycle! 43 | let commandMap : [ String : Command ] 44 | 45 | var channel : Channel? 46 | var eventLoop : EventLoop? 47 | var remoteAddress : SocketAddress? 48 | 49 | var lastActivity = Date() 50 | var lastCommand : String? 51 | var name : String? 52 | var databaseIndex = 0 53 | var isMonitoring = ManagedAtomic(false) 54 | 55 | var subscribedChannels : Set? 56 | var subscribedPatterns : Set? 57 | 58 | init(id: Int, server: RedisServer) { 59 | self.id = id 60 | self.server = server 61 | self.commandMap = server.commandMap 62 | super.init() 63 | } 64 | 65 | 66 | // MARK: - Channel Activation 67 | 68 | override public func channelActive(context: ChannelHandlerContext) { 69 | eventLoop = context.eventLoop 70 | remoteAddress = context.remoteAddress 71 | channel = context.channel 72 | 73 | super.channelActive(context: context) 74 | } 75 | 76 | override public func channelInactive(context: ChannelHandlerContext) { 77 | if let channels = subscribedChannels, !channels.isEmpty { 78 | subscribedChannels = nil 79 | 80 | server.pubSub.Q.async { 81 | for channel in channels { 82 | self.server.pubSub.unsubscribe(channel, handler: self) 83 | } 84 | } 85 | } 86 | if let patterns = subscribedPatterns, !patterns.isEmpty { 87 | subscribedPatterns = nil 88 | 89 | server.pubSub.Q.async { 90 | for pattern in patterns { 91 | self.server.pubSub.unsubscribe(pattern, handler: self) 92 | } 93 | } 94 | } 95 | 96 | super.channelInactive(context: context) 97 | 98 | server.Q.async { 99 | self.server._unregisterClient(self) 100 | } 101 | self.channel = nil 102 | } 103 | 104 | 105 | // MARK: - PubSub 106 | 107 | func handleNotification(_ payload: RESPValue) { 108 | guard let channel = channel else { 109 | assert(self.channel != nil, "notification, but channel is gone?") 110 | return 111 | } 112 | 113 | channel.writeAndFlush(payload, promise: nil) 114 | } 115 | 116 | 117 | // MARK: - Reading 118 | 119 | override public func channelRead(context: ChannelHandlerContext, 120 | value: RESPValue) 121 | { 122 | lastActivity = Date() 123 | do { 124 | let ( command, args ) = try parseCommandCall(value) 125 | 126 | if server.monitors.load(ordering: .relaxed) > 0 { 127 | let info = MonitorInfo(db: databaseIndex, addr: remoteAddress, 128 | call: value) 129 | server.notifyMonitors(info: info) 130 | } 131 | 132 | lastCommand = command.name 133 | 134 | guard let dbs = server.databases else { 135 | assert(server.databases != nil, "server has no databases?!") 136 | throw RedisError.internalServerError 137 | } 138 | 139 | let cmdctx = RedisCommandContext(command : command, 140 | handler : self, 141 | context : context, 142 | databases : dbs) 143 | try callCommand(command, with: args, in: cmdctx) 144 | } 145 | catch let error as RESPError { 146 | self.write(context: context, value: error.toRESPValue(), promise: nil) 147 | } 148 | catch let error as RESPEncodable { 149 | self.write(context: context, value: error.toRESPValue(), promise: nil) 150 | } 151 | catch { 152 | let respError = RESPError(message: "\(error)") 153 | self.write(context: context, value: respError.toRESPValue(), promise: nil) 154 | } 155 | } 156 | 157 | override public func errorCaught(context: ChannelHandlerContext, error: Error) { 158 | super.errorCaught(context: context, error: error) 159 | server.logger.error("Channel", error) 160 | context.close(promise: nil) 161 | } 162 | 163 | 164 | // MARK: - Command Parsing and Invocation 165 | 166 | private func parseCommandCall(_ respValue: RESPValue) throws 167 | -> ( Command, ContiguousArray) 168 | { 169 | guard case .array(.some(let commandList)) = respValue else { 170 | // RESPError(code: "ERR", message: "unknown command \'$4\'") 171 | throw RESPError(message: "invalid command \(respValue)") 172 | } 173 | 174 | guard let commandName = commandList.first?.stringValue else { 175 | throw RESPError(message: "missing command \(respValue)") 176 | } 177 | 178 | guard let command = commandMap[commandName.uppercased()] else { 179 | throw RESPError(message: "unknown command \(commandName)") 180 | } 181 | 182 | server.logger.trace("Parsed command:", commandName, commandList, 183 | "\n ", command) 184 | 185 | guard isArgumentCountValid(commandList.count, for: command) else { 186 | throw RESPError(message: 187 | "wrong number of arguments for '\(commandName)' command") 188 | } 189 | 190 | return ( command, commandList ) 191 | } 192 | 193 | private func isArgumentCountValid(_ countIncludingCommand: Int, 194 | for command: Command) -> Bool 195 | { 196 | switch command.type.keys.arity { 197 | case .fix(let count): 198 | return (count + 1) == countIncludingCommand 199 | 200 | case .minimum(let minimumCount): 201 | return countIncludingCommand > minimumCount 202 | } 203 | } 204 | 205 | private func callCommand(_ command : Command, 206 | with arguments : ContiguousArray, 207 | in ctx : Context) throws 208 | { 209 | // Note: Argument counts are validated. Safe to access the values. 210 | let firstKeyIndex = command.type.keys.firstKey 211 | 212 | // This Nice Not Is. Ideas welcome, in a proper language we would just 213 | // reflect. In a not so proper language we would use macros to hide the 214 | // non-sense. But in Swift, hm. 215 | switch command.type { 216 | case .noArguments(let cb): 217 | try cb(ctx) 218 | 219 | case .singleValue(let cb): 220 | try cb(arguments[1], ctx) 221 | 222 | case .valueValue(let cb): 223 | try cb(arguments[1], arguments[2], ctx) 224 | 225 | case .optionalValue(let cb): 226 | if arguments.count > 2 { 227 | throw RESPError(message: "wrong number of arguments for " 228 | + "'\(command.name.lowercased())' command") 229 | } 230 | try cb(arguments.count > 1 ? arguments[1] : nil, ctx) 231 | 232 | case .oneOrMoreValues(let cb): 233 | try cb(arguments[1..=4.1) 312 | let keys = keysOpt.compactMap( { $0 }) 313 | #else 314 | let keys = keysOpt.flatMap( { $0 }) 315 | #endif 316 | guard keysOpt.count == keys.count else { 317 | throw RESPError(message: "Protocol error: expected keys.") 318 | } 319 | 320 | try cb(ContiguousArray(keys), ctx) 321 | 322 | case .keyValueMap(let cb): 323 | let count = arguments.count 324 | guard count % 2 == 1 else { 325 | throw RESPError(message: 326 | "wrong number of arguments for '\(command.name)'") 327 | } 328 | 329 | let step = command.type.keys.step 330 | var values = ContiguousArray<( Data, RESPValue )>() 331 | values.reserveCapacity(count + 1) 332 | 333 | for i in stride(from: firstKeyIndex, to: count, by: step) { 334 | guard let key = arguments[i].keyValue else { 335 | throw RESPError(message: "Protocol error: expected key.") 336 | } 337 | values.append( ( key, arguments[i + 1] ) ) 338 | } 339 | 340 | try cb(values, ctx) 341 | } 342 | 343 | } 344 | 345 | struct MonitorInfo { 346 | let db : Int 347 | let addr : SocketAddress? 348 | let call : RESPValue 349 | } 350 | 351 | struct ClientInfo { 352 | let id : Int 353 | let addr : SocketAddress? 354 | let name : String? 355 | let age : TimeInterval 356 | let idle : TimeInterval 357 | // flags 358 | let db : Int 359 | let cmd : String? 360 | } 361 | 362 | func getClientInfo() -> ClientInfo { 363 | let now = Date() 364 | return ClientInfo( 365 | id : id, 366 | addr : remoteAddress, 367 | name : name, 368 | age : now.timeIntervalSince(creationDate), 369 | idle : now.timeIntervalSince(lastActivity), 370 | db : databaseIndex, 371 | cmd : lastCommand 372 | ) 373 | } 374 | 375 | } 376 | 377 | fileprivate extension RESPValue { 378 | 379 | @inline(__always) 380 | func withSafeKeyValue(_ cb: ( Data? ) throws -> Void) rethrows { 381 | // SR-7378 382 | switch self { 383 | case .simpleString(let cs), .bulkString(.some(let cs)): 384 | #if false // this does not work, because key `Data` leaves scope 385 | try cs.withVeryUnsafeBytes { ptr in 386 | let ip = ptr.baseAddress!.advanced(by: cs.readerIndex) 387 | let data = Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: ip), 388 | count: cs.readableBytes, 389 | deallocator: .none) 390 | try cb(data) 391 | } 392 | #else 393 | let data = cs.getData(at: cs.readerIndex, length: cs.readableBytes) 394 | try cb(data) 395 | #endif 396 | 397 | default: 398 | try cb(nil) 399 | } 400 | } 401 | } 402 | 403 | -------------------------------------------------------------------------------- /Sources/RedisServer/Server/RedisServer.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import class Dispatch.DispatchQueue 16 | import struct Foundation.URL 17 | import struct Foundation.TimeInterval 18 | import class Foundation.FileManager 19 | import class Foundation.JSONDecoder 20 | import class Atomics.ManagedAtomic 21 | import enum NIORedis.RESPValue 22 | import NIO 23 | 24 | public let DefaultRedisPort = 6379 25 | 26 | open class RedisServer { 27 | 28 | public typealias Command = RedisCommand 29 | 30 | open class Configuration { 31 | 32 | public struct SavePoint { 33 | public let delay : TimeInterval 34 | public let changeCount : Int 35 | 36 | public init(delay: TimeInterval, changeCount: Int) { 37 | self.delay = delay 38 | self.changeCount = changeCount 39 | } 40 | } 41 | 42 | open var host : String? = nil // "127.0.0.1" 43 | open var port : Int = DefaultRedisPort 44 | 45 | open var alwaysShowLog : Bool = true 46 | 47 | open var dbFilename : String = "dump.json" 48 | open var savePoints : [ SavePoint ]? = nil 49 | 50 | open var eventLoopGroup : EventLoopGroup? = nil 51 | 52 | open var commands : RedisCommandTable = RedisServer.defaultCommandTable 53 | open var logger : RedisLogger = RedisPrintLogger(logLevel: .Log) 54 | 55 | public init() {} 56 | } 57 | 58 | public let configuration : Configuration 59 | public let group : EventLoopGroup 60 | public let logger : RedisLogger 61 | public let commandTable : RedisCommandTable 62 | public let dumpURL : URL 63 | 64 | let commandMap : [ String : Command ] 65 | var dumpManager : DumpManager! // oh my. init-mess 66 | var databases : Databases? 67 | 68 | let Q = DispatchQueue(label: "de.zeezide.nio.redisd.clients") 69 | let clientID = ManagedAtomic(0) 70 | var clients = [ ObjectIdentifier : RedisCommandHandler ]() 71 | var monitors = ManagedAtomic(0) 72 | let pubSub : PubSub 73 | 74 | public init(configuration: Configuration = Configuration()) { 75 | self.configuration = configuration 76 | 77 | self.group = configuration.eventLoopGroup 78 | ?? MultiThreadedEventLoopGroup(numberOfThreads: 2) 79 | self.commandTable = configuration.commands 80 | self.logger = configuration.logger 81 | self.dumpURL = URL(fileURLWithPath: configuration.dbFilename) 82 | 83 | pubSub = PubSub(Q: Q) 84 | 85 | commandMap = Dictionary(grouping: commandTable, 86 | by: { $0.name.uppercased() }) 87 | .mapValues({ $0.first! }) 88 | 89 | self.dumpManager = SimpleJSONDumpManager(server: self) 90 | } 91 | 92 | 93 | public func stopOnSigInt() { 94 | logger.warn("Received SIGINT scheduling shutdown...") 95 | 96 | Q.async { // Safe? Unsafe. No idea. Probably not :-) 97 | // TODO: I think the proper trick is to use a pipe here. 98 | if let dbs = self.databases { 99 | do { 100 | self.logger.warn("User requested shutdown...") 101 | try self.dumpManager.saveDump(of: dbs, to: self.dumpURL, 102 | asynchronously: false) 103 | } 104 | catch { 105 | self.logger.error("failed to save database:", error) 106 | } 107 | } 108 | self.logger.warn("Redis is now ready to exit, bye bye...") 109 | exit(0) 110 | } 111 | } 112 | 113 | var serverChannel : Channel? 114 | 115 | open func listenAndWait() { 116 | listen() 117 | 118 | do { 119 | try serverChannel?.closeFuture.wait() // no close, no exit 120 | } 121 | catch { 122 | logger.error("failed to wait on server:", error) 123 | } 124 | } 125 | 126 | open func listen() { 127 | let bootstrap = makeBootstrap() 128 | 129 | do { 130 | logStartupOnPort(configuration.port) 131 | 132 | loadDumpIfAvailable() 133 | 134 | let address : SocketAddress 135 | 136 | if let host = configuration.host { 137 | address = try SocketAddress 138 | .makeAddressResolvingHost(host, port: configuration.port) 139 | } 140 | else { 141 | var addr = sockaddr_in() 142 | addr.sin_port = in_port_t(configuration.port).bigEndian 143 | address = SocketAddress(addr, host: "*") 144 | } 145 | 146 | serverChannel = try bootstrap.bind(to: address) 147 | .wait() 148 | 149 | if let addr = serverChannel?.localAddress { 150 | logSetupOnAddress(addr) 151 | } 152 | else { 153 | logger.warn("server reported no local addres?") 154 | } 155 | } 156 | catch let error as NIO.IOError { 157 | logger.error("failed to start server, errno:", error.errnoCode, "\n", 158 | error.localizedDescription) 159 | } 160 | catch { 161 | logger.error("failed to start server:", type(of:error), error) 162 | } 163 | } 164 | 165 | func logStartupOnPort(_ port: Int) { 166 | if configuration.alwaysShowLog { 167 | let title = "Redi/S \(MemoryLayout.size * 8) bit" 168 | let line1 = "Port: \(port)" 169 | let line2 = "PID: \(getpid())" 170 | 171 | let logo = """ 172 | ____ _ _ ______ 173 | | _ \\ ___ __| (_) / / ___| \(title) 174 | | |_) / _ \\/ _` | | / /\\___ \\ 175 | | _ < __/ (_| | |/ / ___) | \(line1) 176 | |_| \\_\\___|\\__,_|_/_/ |____/ \(line2)\n 177 | """ 178 | print(logo) 179 | } 180 | 181 | } 182 | func logSetupOnAddress(_ address: SocketAddress) { 183 | logger.log("Ready to accept connections") 184 | if !configuration.alwaysShowLog { 185 | logger.warn("Redi/S running on:", address) 186 | } 187 | } 188 | 189 | 190 | // MARK: - Load Database Dump 191 | 192 | func loadDumpIfAvailable() { 193 | defer { logger.warn("Server initialized") } 194 | 195 | databases = dumpManager.loadDumpIfAvailable(url: dumpURL, 196 | configuration: configuration) 197 | } 198 | 199 | 200 | // MARK: - Client Registry 201 | 202 | func _registerClient(_ client: RedisCommandHandler) { // Q! 203 | clients[ObjectIdentifier(client)] = client 204 | } 205 | 206 | func _unregisterClient(_ client: RedisCommandHandler) { // Q! 207 | let oid = ObjectIdentifier(client) 208 | clients.removeValue(forKey: oid) 209 | if client.isMonitoring.load(ordering: .relaxed) { 210 | monitors.wrappingDecrement(ordering: .relaxed) 211 | } 212 | } 213 | 214 | 215 | // MARK: - Monitors 216 | 217 | func notifyMonitors(info: RedisCommandHandler.MonitorInfo) { 218 | // 1522931848.230484 [0 127.0.0.1:60376] "SET" "a" "10" 219 | 220 | let logPacket : RESPValue = { 221 | let logStr = info.redisClientLogLine 222 | let bb = ByteBuffer(string: logStr) 223 | return RESPValue.simpleString(bb) 224 | }() 225 | 226 | Q.async { 227 | for ( _, client ) in self.clients { 228 | guard client.isMonitoring.load(ordering: .relaxed) else { continue } 229 | guard let channel = client.channel else { continue } 230 | channel.writeAndFlush(logPacket, promise: nil) 231 | } 232 | } 233 | } 234 | 235 | 236 | // MARK: - Bootstrap 237 | 238 | open func makeBootstrap() -> ServerBootstrap { 239 | let clientID = self.clientID 240 | 241 | let reuseAddrOpt = ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), 242 | SO_REUSEADDR) 243 | let bootstrap = ServerBootstrap(group: group) 244 | // Specify backlog and enable SO_REUSEADDR for the server itself 245 | .serverChannelOption(ChannelOptions.backlog, value: 256) 246 | .serverChannelOption(reuseAddrOpt, value: 1) 247 | 248 | // Set the handlers that are applied to the accepted Channels 249 | .childChannelInitializer { channel in 250 | channel.pipeline 251 | .addHandler(BackPressureHandler() /* Oh well :-) */, 252 | name: "com.apple.nio.backpressure") 253 | .flatMap { 254 | let cid = clientID.wrappingIncrementThenLoad(ordering: .relaxed) 255 | let handler = RedisCommandHandler(id: cid, server: self) 256 | 257 | self.Q.async { 258 | self._registerClient(handler) 259 | } 260 | 261 | return channel.pipeline 262 | .addHandler(handler, name:"de.zeezide.nio.redis.server.client") 263 | } 264 | } 265 | 266 | // Enable TCP_NODELAY and SO_REUSEADDR for the accepted Channels 267 | .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), 268 | value: 1) 269 | .childChannelOption(reuseAddrOpt, value: 1) 270 | .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1) 271 | 272 | return bootstrap 273 | } 274 | 275 | } 276 | -------------------------------------------------------------------------------- /Sources/RedisServer/Values/RedisError.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIORedis 16 | 17 | enum RedisError : Swift.Error, RESPEncodable { 18 | 19 | case wrongType 20 | case noSuchKey 21 | case indexOutOfRange 22 | case notAnInteger 23 | case unknownSubcommand 24 | case unsupportedSubcommand 25 | case syntaxError 26 | case dbIndexOutOfRange 27 | case invalidDBIndex 28 | case wrongNumberOfArguments(command: String?) 29 | case expectedKey 30 | case internalServerError 31 | case patternNotImplemented(String?) 32 | 33 | var code : String { 34 | switch self { 35 | case .wrongType: return "WRONGTYPE" 36 | case .expectedKey: return "Protocol error" 37 | case .internalServerError, 38 | .patternNotImplemented: return "500" 39 | default: return "ERR" 40 | } 41 | } 42 | 43 | var reason : String { 44 | switch self { 45 | case .noSuchKey: return "no such key" 46 | case .indexOutOfRange: return "index out of range" 47 | case .syntaxError: return "syntax error" 48 | case .dbIndexOutOfRange: return "DB index is out of range" 49 | case .invalidDBIndex: return "invalid DB index" 50 | case .expectedKey: return "expected key." 51 | case .internalServerError: return "internal server error" 52 | 53 | case .patternNotImplemented(let s): 54 | return "pattern not implemented \(s ?? "-")" 55 | 56 | case .wrongType: 57 | return "Operation against a key holding the wrong kind of value" 58 | case .notAnInteger: 59 | return "value is not an integer or out of range" 60 | case .unknownSubcommand: 61 | return "Unknown subcommand or wrong number of arguments." 62 | case .unsupportedSubcommand: 63 | return "The subcommand is known, but unsupported." 64 | 65 | case .wrongNumberOfArguments(let command): 66 | if let command = command { 67 | return "wrong number of arguments for: \(command.uppercased())" 68 | } 69 | else { 70 | return "wrong number of arguments" 71 | } 72 | } 73 | } 74 | 75 | func toRESPValue() -> RESPValue { 76 | return RESPValue(errorCode: code, message: reason) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Sources/RedisServer/Values/RedisList.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import struct NIO.ByteBuffer 16 | import enum NIORedis.RESPValue 17 | 18 | typealias RedisList = ContiguousArray 19 | 20 | /// Amount of additional storage to alloc when pushing elements 21 | fileprivate let RedisExtraListCapacity = 128 22 | 23 | fileprivate extension ContiguousArray { 24 | 25 | @inline(__always) 26 | mutating func redisReserveExtraCapacity(_ newCount: Int) { 27 | if capacity >= (count + newCount) { return } 28 | 29 | let reserveCount : Int 30 | switch count + newCount { // rather arbitrary 31 | case 0...100: reserveCount = 128 32 | case 101...1000: reserveCount = 2048 33 | case 1001...16384: reserveCount = 16384 34 | default: 35 | #if true // this keeps the performance from degrading 36 | reserveCount = (count + newCount) * 2 37 | #else 38 | reserveCount = count + newCount + 2048 39 | #endif 40 | } 41 | reserveCapacity(reserveCount) 42 | } 43 | 44 | } 45 | fileprivate extension Array { 46 | 47 | @inline(__always) 48 | mutating func redisReserveExtraCapacity(_ newCount: Int) { 49 | if capacity >= (count + newCount) { return } 50 | reserveCapacity(count + newCount + RedisExtraListCapacity) 51 | } 52 | 53 | } 54 | 55 | 56 | extension RedisValue { 57 | // this is of course all non-sense, it should be a proper list 58 | 59 | mutating func lset(_ value: ByteBuffer, at idx: Int) -> Bool { 60 | guard case .list(var list) = self else { return false } 61 | 62 | self = .clear 63 | list[idx] = value 64 | self = .list(list) 65 | return true 66 | } 67 | 68 | mutating func lpop() -> ByteBuffer? { 69 | guard case .list(var list) = self else { return nil } 70 | guard !list.isEmpty else { return nil } 71 | 72 | self = .clear 73 | let bb = list.remove(at: 0) 74 | self = .list(list) 75 | return bb 76 | } 77 | mutating func rpop() -> ByteBuffer? { 78 | guard case .list(var list) = self else { return nil } 79 | self = .clear 80 | let bb = list.popLast() 81 | self = .list(list) 82 | return bb 83 | } 84 | 85 | @discardableResult 86 | mutating func rpush(_ value: ByteBuffer) -> Int? { 87 | guard case .list(var list) = self else { return nil } 88 | self = .clear 89 | list.redisReserveExtraCapacity(1) 90 | list.append(value) 91 | self = .list(list) 92 | return list.count 93 | } 94 | 95 | @discardableResult 96 | mutating func rpush(_ items: T) -> Int? 97 | where T.Element == ByteBuffer 98 | { 99 | guard case .list(var list) = self else { return nil } 100 | self = .clear 101 | 102 | #if swift(>=4.1) 103 | let newCount = items.count 104 | #else 105 | let newCount = (items.count as? Int) ?? 1 106 | #endif 107 | list.redisReserveExtraCapacity(newCount) 108 | 109 | list.append(contentsOf: items) 110 | self = .list(list) 111 | return list.count 112 | } 113 | 114 | @discardableResult 115 | mutating func rpush(_ items: RedisList) -> Int? { 116 | guard case .list(var list) = self else { return nil } 117 | self = .clear 118 | 119 | let newCount = items.count 120 | list.redisReserveExtraCapacity(newCount) 121 | 122 | list.append(contentsOf: items) 123 | self = .list(list) 124 | return list.count 125 | } 126 | 127 | @discardableResult 128 | mutating func lpush(_ value: ByteBuffer) -> Int? { 129 | guard case .list(var list) = self else { return nil } 130 | self = .clear 131 | list.redisReserveExtraCapacity(1) 132 | list.insert(value, at: 0) 133 | self = .list(list) 134 | return list.count 135 | } 136 | 137 | @discardableResult 138 | mutating func lpush(_ reversedItems: RedisList) -> Int? { 139 | guard case .list(let list) = self else { return nil } 140 | self = .clear 141 | var newList = RedisList() 142 | newList.reserveCapacity(reversedItems.count + list.count) 143 | newList.append(contentsOf: reversedItems) 144 | newList.append(contentsOf: list) 145 | self = .list(newList) 146 | return newList.count 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /Sources/RedisServer/Values/RedisValue.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import NIO 16 | import NIORedis 17 | import Foundation 18 | 19 | /** 20 | * For an overview on Redis datatypes, check out: 21 | * 22 | * [Data types](https://redis.io/topics/data-types) 23 | * 24 | * We try to stick to `ByteBuffer`, to avoid unnecessary copies. But for stuff 25 | * which needs to be Hashable, we can't :-) 26 | * 27 | * NOTE: Do not confuse RedisValue's with RESPValue's. RedisValues are the 28 | * values the Redis database can store. 29 | * RESPValues are the value types supported by the Redis-write protocol. 30 | * Those are converted into each other, but are very much distinct! 31 | */ 32 | enum RedisValue { 33 | 34 | /// A binary safe string. 35 | case string(RedisString) 36 | 37 | /// A list of strings. For use w/ LPUSH, RPUSH, LRANGE etc. 38 | case list (RedisList) // FIXME: oh no ;-) 39 | 40 | /// A set of strings. For use w/ SADD, SINTER, SPOP etc 41 | case set (Set) 42 | 43 | /// A map between string fields and string values. 44 | /// Common commands: HMSET, HGETALL, HSET, HGETALL. 45 | case hash ([ Data : RedisString ]) 46 | 47 | /// Helper, do not use. 48 | case clear 49 | } 50 | 51 | #if false // TODO: Redis stores integers as actual integers (PERF) 52 | enum RedisString { 53 | case buffer (ByteBuffer) 54 | case integer(Int64) 55 | } 56 | #else 57 | typealias RedisString = ByteBuffer 58 | #endif 59 | 60 | fileprivate let sharedAllocator = ByteBufferAllocator() 61 | 62 | extension RedisValue : RESPEncodable { 63 | 64 | init?(_ value: RESPValue) { 65 | switch value { 66 | case .simpleString(let v): self = .string(v) 67 | case .bulkString(.none): return nil 68 | case .bulkString(.some(let v)): self = .string(v) 69 | case .integer, .array, .error: return nil 70 | } 71 | } 72 | 73 | init(_ value: Int) { // FIXME: speed, and use .int backing store 74 | self = .string(ByteBuffer.makeFromIntAsString(value)) 75 | } 76 | 77 | public var intValue : Int? { // FIXME: speed, and use .int backing store 78 | guard case .string(let bb) = self else { return nil } 79 | return bb.stringAsInteger 80 | } 81 | 82 | func toRESPValue() -> RESPValue { 83 | switch self { 84 | case .string(let v): return .bulkString(v) 85 | case .list (let items): return items.toRESPValue() 86 | case .set (let items): return items.toRESPValue() 87 | case .hash (let hash): return hash.toRESPValue() 88 | case .clear: fatalError("use of .clear case") 89 | } 90 | } 91 | 92 | } 93 | 94 | extension RedisValue { 95 | 96 | init?(string value: RESPValue) { 97 | guard let bb = value.byteBuffer else { return nil } 98 | self = .string(bb) 99 | } 100 | 101 | init?(list value: RESPValue) { 102 | guard case .array(.some(let items)) = value else { return nil } 103 | self.init(list: items) 104 | } 105 | 106 | init?(list value: T) where T.Element == RESPValue { 107 | var list = RedisList() 108 | #if swift(>=4.1) 109 | list.reserveCapacity(value.count) 110 | #else 111 | if let count = value.count as? Int { 112 | list.reserveCapacity(count) 113 | } 114 | #endif 115 | 116 | for item in value { 117 | guard let bb = item.byteBuffer else { return nil } 118 | list.append(bb) 119 | } 120 | self = .list(list) 121 | } 122 | 123 | } 124 | 125 | 126 | extension Dictionary where Element == ( key: Data, value: ByteBuffer ) { 127 | 128 | public func toRESPValue() -> RESPValue { 129 | var array = ContiguousArray() 130 | array.reserveCapacity(count * 2 + 1) 131 | 132 | for ( key, value ) in self { 133 | array.append(RESPValue(bulkString: key)) 134 | array.append(.bulkString(value)) 135 | } 136 | 137 | return .array(array) 138 | } 139 | 140 | } 141 | 142 | extension Collection where Element == Data { 143 | 144 | public func toRESPValue() -> RESPValue { 145 | var array = ContiguousArray() 146 | #if swift(>=4.1) 147 | array.reserveCapacity(count) 148 | #else 149 | if let count = count as? Int { array.reserveCapacity(count) } 150 | #endif 151 | 152 | for data in self { 153 | array.append(RESPValue(bulkString: data)) 154 | } 155 | 156 | return .array(array) 157 | } 158 | 159 | } 160 | extension Collection where Element == ByteBuffer { 161 | 162 | public func toRESPValue() -> RESPValue { 163 | var array = ContiguousArray() 164 | #if swift(>=4.1) 165 | array.reserveCapacity(count) 166 | #else 167 | if let count = count as? Int { array.reserveCapacity(count) } 168 | #endif 169 | 170 | for bb in self { 171 | array.append(.bulkString(bb)) 172 | } 173 | 174 | return .array(array) 175 | } 176 | 177 | } 178 | 179 | extension Collection where Element == RESPValue { 180 | 181 | func extractByteBuffers(reverse: Bool = false) 182 | -> ContiguousArray? 183 | { 184 | if reverse { return lazy.reversed().extractByteBuffers() } 185 | 186 | var byteBuffers = ContiguousArray() 187 | #if swift(>=4.1) 188 | byteBuffers.reserveCapacity(count) 189 | #else 190 | if let count = count as? Int { byteBuffers.reserveCapacity(count) } 191 | #endif 192 | 193 | for item in self { 194 | guard let bb = item.byteBuffer else { return nil } 195 | byteBuffers.append(bb) 196 | } 197 | return byteBuffers 198 | } 199 | 200 | func extractRedisList(reverse: Bool = false) -> RedisList? { 201 | if reverse { return lazy.reversed().extractRedisList() } 202 | 203 | var byteBuffers = RedisList() 204 | #if swift(>=4.1) 205 | byteBuffers.reserveCapacity(count) 206 | #else 207 | if let count = count as? Int { byteBuffers.reserveCapacity(count) } 208 | #endif 209 | 210 | for item in self { 211 | guard let bb = item.byteBuffer else { return nil } 212 | byteBuffers.append(bb) 213 | } 214 | return byteBuffers 215 | } 216 | } 217 | 218 | 219 | extension ByteBuffer { 220 | 221 | static func makeFromIntAsString(_ value: Int) -> ByteBuffer { 222 | return ByteBuffer(string: String(value)) 223 | } 224 | var stringAsInteger: Int? { 225 | guard readableBytes > 0 else { return nil } 226 | 227 | // FIXME: faster parsing (though the backing store should be int) 228 | guard let s = getString(at: readerIndex, length: readableBytes) else { 229 | return nil 230 | } 231 | return Int(s) 232 | } 233 | 234 | func rangeForRedisRange(start: Int, stop: Int) -> Range { 235 | let count = self.readableBytes 236 | if count == 0 { return 0..<0 } 237 | 238 | var fromIndex = start < 0 ? (count + start) : start 239 | if fromIndex >= count { return 0..<0 } 240 | else if fromIndex < 0 { fromIndex = 0 } 241 | 242 | var toIndex = stop < 0 ? (count + stop) : stop 243 | if toIndex >= count { toIndex = count - 1 } 244 | 245 | if fromIndex > toIndex { return 0..<0 } 246 | 247 | toIndex += 1 248 | return fromIndex.. Range { 256 | #if swift(>=4.1) 257 | let count = self.count 258 | #else 259 | let count = self.count as! Int 260 | #endif 261 | if count == 0 { return 0..<0 } 262 | 263 | var fromIndex = start < 0 ? (count + start) : start 264 | if fromIndex >= count { return 0..<0 } 265 | else if fromIndex < 0 { fromIndex = 0 } 266 | 267 | var toIndex = stop < 0 ? (count + stop) : stop 268 | if toIndex >= count { toIndex = count - 1 } 269 | 270 | if fromIndex > toIndex { return 0..<0 } 271 | 272 | toIndex += 1 273 | return fromIndex.. ByteBuffer { 23 | let data = try decode(Data.self, forKey: key) 24 | return ByteBuffer(data: data) 25 | } 26 | 27 | func decodeByteBufferArray(forKey key: Key) throws 28 | -> ContiguousArray 29 | { 30 | let datas = try decode(Array.self, forKey: key) 31 | var buffers = ContiguousArray() 32 | buffers.reserveCapacity(datas.count + 1) 33 | 34 | for data in datas { 35 | buffers.append(ByteBuffer(data: data)) 36 | } 37 | return buffers 38 | } 39 | 40 | func decodeByteBufferHash(forKey key: Key) throws -> [ Data : ByteBuffer ] { 41 | let datas = try decode(Dictionary.self, forKey: key) 42 | var buffers = [ Data : ByteBuffer ]() 43 | buffers.reserveCapacity(datas.count + 1) 44 | 45 | for ( key, data ) in datas { 46 | buffers[key] = ByteBuffer(data: data) 47 | } 48 | return buffers 49 | } 50 | } 51 | 52 | extension RedisValue : Codable { 53 | 54 | enum CodingKeys: CodingKey { 55 | case type, value 56 | } 57 | 58 | init(from decoder: Decoder) throws { 59 | let container = try decoder.container(keyedBy: CodingKeys.self) 60 | let type = try container.decode(String.self, forKey: .type) 61 | 62 | switch type { 63 | case "string": 64 | self = .string(try container.decodeByteBuffer(forKey: .value)) 65 | 66 | case "list": 67 | self = .list(try container.decodeByteBufferArray(forKey: .value)) 68 | 69 | case "set": 70 | self = .set(try container.decode(Set.self, forKey: .value)) 71 | 72 | case "hash": 73 | self = .hash(try container.decodeByteBufferHash(forKey: .value)) 74 | 75 | default: 76 | assertionFailure("unexpected dump value type: \(type)") 77 | throw RedisDumpError.unexpectedValueType(type) 78 | } 79 | } 80 | 81 | func encode(to encoder: Encoder) throws { 82 | var container = encoder.container(keyedBy: CodingKeys.self) 83 | 84 | switch self { 85 | case .string(let value): 86 | try container.encode("string", forKey: .type) 87 | try container.encode(value, forKey: .value) 88 | 89 | case .list(let value): 90 | try container.encode("list", forKey: .type) 91 | try container.encode(Array(value), forKey: .value) 92 | 93 | case .set(let value): 94 | try container.encode("set", forKey: .type) 95 | try container.encode(value, forKey: .value) 96 | 97 | case .hash(let value): 98 | try container.encode("hash", forKey: .type) 99 | try container.encode(value, forKey: .value) 100 | 101 | case .clear: 102 | assertionFailure("cannot dump transient .clear type") 103 | throw RedisDumpError.internalError 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/redi-s/ConfigFile.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import RedisServer 16 | import Foundation 17 | 18 | extension RedisServer.Configuration.SavePoint { 19 | 20 | /** 21 | * Parse config format: 22 | * save 900 1 - after 900 sec (15 min) if at least 1 key changed 23 | * save 300 10 - after 300 sec (5 min) if at least 10 keys changed 24 | * save 60 10000 - after 60 sec if at least 10000 keys changed 25 | */ 26 | init?(_ s: String) { 27 | guard !s.isEmpty else { return nil } 28 | 29 | let ts = s.trimmingCharacters(in: CharacterSet.whitespaces) 30 | 31 | #if swift(>=4.1) 32 | let comps = ts.components(separatedBy: CharacterSet.whitespaces) 33 | .compactMap { $0.isEmpty ? nil : $0 } 34 | #else 35 | let comps = ts.components(separatedBy: CharacterSet.whitespaces) 36 | .flatMap { $0.isEmpty ? nil : $0 } 37 | #endif 38 | 39 | guard comps.count == 2 else { return nil } 40 | 41 | guard let intervalInMS = Int(comps[0]), let count = Int(comps[1]) else { 42 | return nil 43 | } 44 | 45 | self.init(delay: TimeInterval(intervalInMS) / 1000.0, 46 | changeCount: count) 47 | } 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Sources/redi-s/README.md: -------------------------------------------------------------------------------- 1 |

Redi/S - Server Executable 2 | 4 |

5 | 6 | `redi-s` is a very small executable based on the 7 | [RedisServer](../RedisServer/) 8 | module. 9 | The actual functionality is contained in the module, 10 | the tool just parses command line options and starts the server. 11 | 12 | ## How to build 13 | 14 | If you care about performance, do a `release` build: 15 | 16 | ```shell 17 | $ swift build -c release 18 | ``` 19 | 20 | ## How to run 21 | 22 | ``` 23 | $ .build/release/redi-s 24 | 2383:M 11 Apr 17:04:16.296 # sSZSsSZSsSZSs Redi/S is starting sSZSsSZSsSZSs 25 | 2383:M 11 Apr 17:04:16.302 # Redi/S bits=64, pid=2383, just started 26 | 2383:M 11 Apr 17:04:16.303 # Configuration loaded 27 | ____ _ _ ______ 28 | | _ \ ___ __| (_) / / ___| Redi/S 64 bit 29 | | |_) / _ \/ _` | | / /\___ \ 30 | | _ < __/ (_| | |/ / ___) | Port: 1337 31 | |_| \_\___|\__,_|_/_/ |____/ PID: 2383 32 | 33 | 2383:M 11 Apr 17:04:16.304 # Server initialized 34 | 2383:M 11 Apr 17:04:16.305 * Ready to accept connections 35 | ``` 36 | 37 | Options: 38 | 39 | - `-h` / `--help`, print help 40 | - `-p` / `--port`, select a port, e.g. `-p 8888` 41 | 42 | ## TODO 43 | 44 | - [ ] load configuration file 45 | 46 | ### Who 47 | 48 | Brought to you by 49 | [ZeeZide](http://zeezide.de). 50 | We like 51 | [feedback](https://twitter.com/ar_institute), 52 | GitHub stars, 53 | cool [contract work](http://zeezide.com/en/services/services.html), 54 | presumably any form of praise you can think of. 55 | -------------------------------------------------------------------------------- /Sources/redi-s/main.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the swift-nio-redis open source project 4 | // 5 | // Copyright (c) 2018-2024 ZeeZide GmbH. and the swift-nio-redis project authors 6 | // Licensed under Apache License v2.0 7 | // 8 | // See LICENSE.txt for license information 9 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 10 | // 11 | // SPDX-License-Identifier: Apache-2.0 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import RedisServer 16 | import Foundation 17 | 18 | 19 | // MARK: - Handle Commandline Arguments 20 | 21 | func help() { 22 | let cmd = CommandLine.arguments.first ?? "redis-server" 23 | print("Usage: \(cmd) -h or --help") 24 | print() 25 | print("Examples:") 26 | print(" \(cmd) (run the server with default conf)") 27 | print(" \(cmd) -p 1337") 28 | } 29 | 30 | let args = CommandLine.arguments.dropFirst() 31 | if args.contains("--help") || args.contains("-h") { 32 | help() 33 | exit(0) 34 | } 35 | 36 | let cmdLinePort : Int? = { 37 | guard let idx = 38 | args.firstIndex(where: { [ "-p", "--port" ].contains($0) }) else 39 | { 40 | return nil 41 | } 42 | guard (idx + 1) < args.endIndex, let port = UInt16(args[idx + 1]) else { 43 | print("Missing or invalid value for", args[idx], "argument") 44 | exit(42) 45 | } 46 | return Int(port) 47 | }() 48 | 49 | 50 | // MARK: - Setup Configuration 51 | 52 | let logger = RedisPrintLogger() 53 | 54 | logger.warn("sSZSsSZSsSZSs Redi/S is starting sSZSsSZSsSZSs") 55 | logger.warn("Redi/S" 56 | + " bits=\(MemoryLayout.size * 8)," 57 | + " pid=\(getpid())," 58 | + " just started") 59 | 60 | 61 | let configuration = RedisServer.Configuration() 62 | configuration.logger = logger 63 | configuration.port = cmdLinePort ?? 1337 64 | 65 | configuration.savePoints = { 66 | typealias SavePoint = RedisServer.Configuration.SavePoint 67 | return [ SavePoint(delay: 10, changeCount: 100) ] 68 | }() 69 | 70 | logger.warn("Configuration loaded") 71 | 72 | // MARK: - Run Server 73 | 74 | let server = RedisServer(configuration: configuration) 75 | defer { try! server.group.syncShutdownGracefully() } 76 | 77 | signal(SIGINT) { // Safe? Unsafe. No idea :-) 78 | s in server.stopOnSigInt() 79 | } 80 | 81 | server.listenAndWait() 82 | --------------------------------------------------------------------------------