├── .gitattributes
├── .github
└── workflows
│ ├── publish.yml
│ └── tests.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── Dependensies.kt
│ ├── PublishingExtension.kt
│ └── publish.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ktor-simple-cache
├── README.md
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── com
│ │ └── ucasoft
│ │ └── ktor
│ │ └── simpleCache
│ │ ├── CacheOutput.kt
│ │ ├── SimpleCache.kt
│ │ └── SimpleCachePlugin.kt
│ └── test
│ └── kotlin
│ └── com
│ └── ucasoft
│ └── ktor
│ └── simpleCache
│ ├── SimpleCacheTests.kt
│ └── TestApplication.kt
├── ktor-simple-memory-cache
├── README.md
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── com
│ │ └── ucasoft
│ │ └── ktor
│ │ └── simpleMemoryCache
│ │ └── SimpleMemoryCacheProvider.kt
│ └── test
│ └── kotlin
│ └── com
│ └── ucasoft
│ └── ktor
│ └── simpleMemoryCache
│ ├── MemoryCacheTests.kt
│ └── TestApplication.kt
├── ktor-simple-redis-cache
├── README.md
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── com
│ │ └── ucasoft
│ │ └── ktor
│ │ └── simpleRedisCache
│ │ └── SimpleRedisCacheProvider.kt
│ └── test
│ └── kotlin
│ └── com
│ └── ucasoft
│ └── ktor
│ └── simpleRedisCache
│ ├── RedisCacheTests.kt
│ └── TestApplication.kt
└── settings.gradle.kts
/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # Linux start script should use lf
5 | /gradlew text eol=lf
6 |
7 | # These are Windows script files and should use crlf
8 | *.bat text eol=crlf
9 |
10 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: "Publish to Maven"
2 |
3 | on:
4 | workflow_dispatch:
5 | release:
6 | types: [created]
7 |
8 | jobs:
9 | build-and-publish:
10 | runs-on: macos-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - name: "Set Up JDK"
15 | uses: actions/setup-java@v4
16 | with:
17 | distribution: temurin
18 | java-version: 11
19 | cache: gradle
20 |
21 | - name: "Grant execute permission for gradlew"
22 | run: chmod +x gradlew
23 |
24 | - name: "Build with Gradle"
25 | run: ./gradlew build -x :ktor-simple-redis-cache:jvmTest
26 | shell: bash
27 |
28 | - name: "Decode GPG Key"
29 | run: |
30 | echo "${{ secrets.SIGNING_SECRET_KEY_RING_FILE }}" > ~/.gradle/secring.gpg.b64
31 | base64 --decode -i ~/.gradle/secring.gpg.b64 -o ~/.gradle/secring.gpg
32 | shell: bash
33 |
34 | - name: "Publish"
35 | run: ./gradlew publishAllPublicationsToMavenCentralRepository -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} -Psigning.password=${{ secrets.SIGNING_PASSWORD }} -Psigning.secretKeyRingFile=$(echo ~/.gradle/secring.gpg)
36 | env:
37 | MAVEN_TOKEN_USERNAME: ${{ secrets.MAVEN_TOKEN_USERNAME }}
38 | MAVEN_TOKEN_PASSWORD: ${{ secrets.MAVEN_TOKEN_PASSWORD }}
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: "Build and Test"
2 |
3 | on:
4 | pull_request:
5 | branches: [main]
6 |
7 | jobs:
8 | build-and-test:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | checks: write
12 | pull-requests: write
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: "Set Up JDK"
17 | uses: actions/setup-java@v4
18 | with:
19 | distribution: temurin
20 | java-version: 11
21 | cache: gradle
22 |
23 | - name: "Grant execute permission for gradlew"
24 | run: chmod +x gradlew
25 |
26 | - name: "Build with Gradle"
27 | run: ./gradlew build -x allTest
28 | shell: bash
29 |
30 | - name: "Tests"
31 | run: ./gradlew cleanAllTest allTest
32 | shell: bash
33 |
34 | - name: "Tests Report"
35 | uses: dorny/test-reporter@v1
36 | if: success() || failure()
37 | with:
38 | name: jUnit Tests
39 | path: '**/build/test-results/jvmTest/TEST-*.xml'
40 | reporter: java-junit
41 |
42 | - name: "Run Coverage"
43 | if: ${{ github.event_name == 'pull_request' }}
44 | run: ./gradlew koverXmlReport
45 | shell: bash
46 |
47 | - name: "Coverage Report"
48 | uses: mi-kas/kover-report@v1
49 | if: ${{ github.event_name == 'pull_request' }}
50 | with:
51 | path: |
52 | ./ktor-simple-cache/build/reports/kover/report.xml
53 | ./ktor-simple-memory-cache/build/reports/kover/report.xml
54 | ./ktor-simple-redis-cache/build/reports/kover/report.xml
55 | token: ${{ secrets.GITHUB_TOKEN }}
56 | title: "Coverage Report"
57 | update-comment: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Gradle project-specific cache directory
2 | .gradle
3 |
4 | .idea
5 |
6 | # Ignore Gradle build output directory
7 | build
8 |
9 | .kotlin
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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 2023 Sergey Antonov aka Scogun
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ktor Simple Cache
2 |
3 | [](https://sonarcloud.io/summary/new_code?id=Scogun_ktor-simple-cache)  
4 |
5 | This repository hosts a number of libraries for [Ktor](https://ktor.io/) Server to simply add output cache.
6 |
7 | To learn more please refer to the `README`s of individual library.
8 |
9 | | README |
10 | |:---------------------------------------------------------------|
11 | | [ktor-simple-cache](ktor-simple-cache/README.md) |
12 | | [ktor-simple-memory-cache](ktor-simple-memory-cache/README.md) |
13 | | [ktor-simple-redis-cache](ktor-simple-redis-cache/README.md) |
14 |
15 | ## Using in Your Projects
16 |
17 | Use one of simple cache provider library to setup cache during server configuration:
18 |
19 | ```kotlin
20 | install(SimpleCache) {
21 | //cacheProvider {}
22 | }
23 | ```
24 |
25 | ## Thanks
26 |
27 |
28 |
29 | ## Star History
30 |
31 | [](https://star-history.com/#Scogun/ktor-simple-cache&Date)
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | tasks.wrapper {
2 | gradleVersion = "8.12.1"
3 | }
4 |
5 | allprojects {
6 |
7 | group = "com.ucasoft.ktor"
8 |
9 | version = "0.53.4"
10 |
11 | repositories {
12 | mavenCentral()
13 | }
14 |
15 | tasks.withType {
16 | reports {
17 | junitXml.required.set(true)
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Dependensies.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Project
2 |
3 | const val ktorVersion = "3.1.0"
4 | const val kotestVersion = "5.9.1"
5 |
6 | fun Project.ktor(module: String) = "io.ktor:ktor-$module:$ktorVersion"
7 |
8 | fun Project.ktorClient(module: String) = ktor("client-$module")
9 |
10 | fun Project.ktorServer(module: String) = ktor("server-$module")
11 |
12 | fun Project.kotest(module: String, version: String = kotestVersion) = "io.kotest:kotest-$module:$version"
13 |
14 | fun Project.kotestEx(module: String, version: String) = "io.kotest.extensions:kotest-$module:$version"
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/PublishingExtension.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.model.ObjectFactory
2 | import org.gradle.kotlin.dsl.property
3 |
4 | open class PublishingExtension(factory: ObjectFactory) {
5 | val name = factory.property()
6 | val description = factory.property()
7 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/publish.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `maven-publish`
3 | signing
4 | }
5 |
6 | val libraryData = extensions.create("libraryData", PublishingExtension::class)
7 |
8 | val stubJavadoc by tasks.creating(Jar::class) {
9 | archiveClassifier.set("javadoc")
10 | }
11 |
12 | publishing {
13 | publications.configureEach {
14 | if (this is MavenPublication) {
15 | if (name != "kotlinMultiplatform") {
16 | artifact(stubJavadoc)
17 | }
18 | pom {
19 | name.set(libraryData.name)
20 | description.set(libraryData.description)
21 | url.set("https://github.com/Scogun/ktor-simple-cache")
22 | licenses {
23 | license {
24 | name.set("The Apache License, Version 2.0")
25 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
26 | }
27 | }
28 | developers {
29 | developer {
30 | id.set("Scogun")
31 | name.set("Sergey Antonov")
32 | email.set("SAntonov@ucasoft.com")
33 | }
34 | }
35 | scm {
36 | connection.set("scm:git:git://github.com/Scogun/ktor-simple-cache.git")
37 | developerConnection.set("scm:git:ssh://github.com:Scogun/ktor-simple-cache.git")
38 | url.set("https://github.com/Scogun/ktor-simple-cache")
39 | }
40 | }
41 | }
42 | }
43 | repositories {
44 | maven {
45 | name = "MavenCentral"
46 | url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
47 | credentials {
48 | username = System.getenv("MAVEN_TOKEN_USERNAME")
49 | password = System.getenv("MAVEN_TOKEN_PASSWORD")
50 | }
51 | }
52 | }
53 | }
54 |
55 | signing {
56 | sign(publishing.publications)
57 | }
58 |
59 | tasks.withType().configureEach {
60 | val signingTasks = tasks.withType()
61 | mustRunAfter(signingTasks)
62 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scogun/ktor-simple-cache/6212dfb673c759d8bbf369de2a5dcf3ba69242ad/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/ktor-simple-cache/README.md:
--------------------------------------------------------------------------------
1 | # Ktor Simple Cache
2 | Base solution which provides the plugin implementation and abstract class for cache providers.
3 |
4 | [](https://search.maven.org/artifact/com.ucasoft.ktor/ktor-simple-cache/0.53.4/jar)
5 | ## Setup
6 | ### Gradle
7 | ```kotlin
8 | repositories {
9 | mavenCentral()
10 | }
11 |
12 | implementation("com.ucasoft.ktor:ktor-simple-cache:0.53.4")
13 | ```
14 | ## Usage
15 | ```kotlin
16 | install(SimpleCache) {
17 | cacheProvider {
18 | invalidateAt = 10.seconds
19 | }
20 | }
21 |
22 | routing {
23 | cacheOutput(2.seconds) {
24 | get("short-cache") {
25 | call.respond(Random.nextInt())
26 | }
27 | }
28 | cacheOutput {
29 | get("default-cache") {
30 | call.respond(Random.nextInt())
31 | }
32 | }
33 | // Cache key will be built only on listed query keys. Others will be ignored!
34 | // `/based-only-on-id-cache?id=123?time=100` and `/based-only-on-id-cache?id=123?time=200` requests will use similar cache key!
35 | cacheOutput(queryKeys = listOf("id")) {
36 | get("based-only-on-id-cache") {
37 | call.respond(Random.nextInt())
38 | }
39 | }
40 | }
41 | ```
--------------------------------------------------------------------------------
/ktor-simple-cache/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | kotlin("plugin.serialization") apply false
4 | id("org.jetbrains.kotlinx.kover")
5 | id("publish")
6 | }
7 |
8 | kotlin {
9 | jvmToolchain(11)
10 | jvm {
11 | tasks.withType {
12 | useJUnitPlatform()
13 | }
14 | }
15 | linuxX64()
16 | macosX64()
17 | sourceSets {
18 | val commonMain by getting {
19 | dependencies {
20 | implementation(ktorServer("core"))
21 | }
22 | kotlin.srcDir("src/main/kotlin")
23 | }
24 | val jvmTest by getting {
25 | dependencies {
26 | implementation(kotlin("test"))
27 | implementation(ktorServer("test-host"))
28 | implementation(kotest("assertions-core"))
29 | implementation(kotestEx("assertions-ktor", "2.0.0"))
30 | implementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
31 | }
32 | kotlin.srcDir("src/test/kotlin")
33 | }
34 | }
35 | }
36 |
37 | libraryData {
38 | name.set("Ktor Simple Cache")
39 | description.set("Base realization of simple output cache for Ktor server")
40 | }
--------------------------------------------------------------------------------
/ktor-simple-cache/src/main/kotlin/com/ucasoft/ktor/simpleCache/CacheOutput.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleCache
2 |
3 | import io.ktor.server.routing.*
4 | import kotlin.time.Duration
5 |
6 | class CacheOutputSelector : RouteSelector() {
7 |
8 | override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent
9 | }
10 |
11 | fun Route.cacheOutput(invalidateAt: Duration? = null, queryKeys: List = emptyList(), build: Route.() -> Unit) : Route {
12 | val route = createChild(CacheOutputSelector())
13 | route.install(SimpleCachePlugin) {
14 | this.invalidateAt = invalidateAt
15 | this.queryKeys = queryKeys
16 | }
17 | route.build()
18 | return route
19 | }
--------------------------------------------------------------------------------
/ktor-simple-cache/src/main/kotlin/com/ucasoft/ktor/simpleCache/SimpleCache.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleCache
2 |
3 | import io.ktor.server.application.*
4 | import io.ktor.util.*
5 | import kotlinx.coroutines.sync.Mutex
6 | import kotlin.time.Duration
7 | import kotlin.time.Duration.Companion.minutes
8 |
9 | class SimpleCacheConfig {
10 |
11 | var provider: SimpleCacheProvider? = null
12 | }
13 |
14 | class SimpleCache(internal var config: SimpleCacheConfig) {
15 |
16 | companion object : BaseApplicationPlugin {
17 | override val key: AttributeKey = AttributeKey("SimpleCacheHolder")
18 |
19 | override fun install(pipeline: Application, configure: SimpleCacheConfig.() -> Unit): SimpleCache {
20 | return SimpleCache(SimpleCacheConfig().apply(configure))
21 | }
22 | }
23 | }
24 |
25 | abstract class SimpleCacheProvider(config: Config) {
26 |
27 | val invalidateAt = config.invalidateAt
28 |
29 | private val mutex = Mutex()
30 |
31 | abstract suspend fun getCache(key: String): Any?
32 | abstract suspend fun setCache(key: String, content: Any, invalidateAt: Duration?)
33 |
34 | fun handleBadResponse() {
35 | mutex.unlock()
36 | }
37 |
38 | suspend fun loadCache(key: String): Any? {
39 | var cache = getCache(key)
40 | return if (cache == null) {
41 | mutex.lock()
42 | cache = getCache(key)
43 | if (cache == null) {
44 | null
45 | } else {
46 | mutex.unlock()
47 | cache
48 | }
49 | } else {
50 | cache
51 | }
52 | }
53 |
54 | suspend fun saveCache(key: String, content: Any, invalidateAt: Duration?) {
55 | setCache(key, content, invalidateAt)
56 | mutex.unlock()
57 | }
58 |
59 | open class Config protected constructor() {
60 |
61 | var invalidateAt: Duration = 5.minutes
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/ktor-simple-cache/src/main/kotlin/com/ucasoft/ktor/simpleCache/SimpleCachePlugin.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleCache
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.application.*
5 | import io.ktor.server.application.hooks.*
6 | import io.ktor.server.request.*
7 | import io.ktor.server.response.*
8 | import io.ktor.util.*
9 | import kotlin.time.Duration
10 |
11 | class SimpleCachePluginConfig {
12 |
13 | lateinit var queryKeys: List
14 |
15 | var invalidateAt: Duration? = null
16 | }
17 |
18 | val SimpleCachePlugin = createRouteScopedPlugin(name = "SimpleCachePlugin", ::SimpleCachePluginConfig) {
19 | val provider = application.plugin(SimpleCache).config.provider ?: error("Add one cache provider!")
20 | val isResponseFromCacheKey = AttributeKey("isResponseFromCacheKey")
21 | onCall {
22 | val cache = provider.loadCache(buildKey(it.request, pluginConfig.queryKeys))
23 | if (cache != null) {
24 | it.attributes.put(isResponseFromCacheKey, true)
25 | it.respond(cache)
26 | }
27 | }
28 | on(CallFailed) { _, e ->
29 | provider.handleBadResponse()
30 | throw e
31 | }
32 | onCallRespond { call, body ->
33 | if ((call.response.status() ?: HttpStatusCode.OK) >= HttpStatusCode.BadRequest) {
34 | provider.handleBadResponse()
35 | }
36 | else if (!call.attributes.contains(isResponseFromCacheKey)) {
37 | provider.saveCache(buildKey(call.request, pluginConfig.queryKeys), body, pluginConfig.invalidateAt)
38 | }
39 | }
40 | }
41 |
42 | private fun buildKey(request: ApplicationRequest, queryKeys: List): String {
43 | val keys =
44 | if (queryKeys.isEmpty()) request.queryParameters else request.queryParameters.filter { key, _ -> key in queryKeys }
45 | val key = "${request.path()}?${
46 | keys.entries().sortedBy { it.key }
47 | .joinToString("&") { "${it.key}=${it.value.sorted().joinToString(",")}" }
48 | }"
49 | return key.trimEnd('?')
50 | }
--------------------------------------------------------------------------------
/ktor-simple-cache/src/test/kotlin/com/ucasoft/ktor/simpleCache/SimpleCacheTests.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleCache
2 |
3 | import io.kotest.assertions.ktor.client.shouldHaveStatus
4 | import io.kotest.assertions.throwables.shouldThrow
5 | import io.kotest.matchers.collections.shouldBeSingleton
6 | import io.kotest.matchers.collections.shouldContain
7 | import io.kotest.matchers.collections.shouldHaveSize
8 | import io.kotest.matchers.maps.shouldBeEmpty
9 | import io.kotest.matchers.maps.shouldNotBeEmpty
10 | import io.kotest.matchers.nulls.shouldNotBeNull
11 | import io.kotest.matchers.shouldBe
12 | import io.kotest.matchers.shouldNotBe
13 | import io.ktor.client.request.*
14 | import io.ktor.client.statement.*
15 | import io.ktor.http.*
16 | import io.ktor.http.content.*
17 | import io.ktor.server.application.*
18 | import io.ktor.server.testing.*
19 | import kotlinx.coroutines.async
20 | import kotlinx.coroutines.awaitAll
21 | import kotlinx.coroutines.delay
22 | import kotlinx.coroutines.runBlocking
23 | import kotlinx.coroutines.sync.Mutex
24 | import org.junit.jupiter.api.Test
25 | import org.mockito.Answers
26 | import org.mockito.ArgumentMatchers.anyString
27 | import org.mockito.kotlin.*
28 | import kotlin.time.Duration
29 | import kotlin.time.Duration.Companion.minutes
30 |
31 | internal class SimpleCacheTests {
32 |
33 | @Test
34 | fun `no any provider installed`() {
35 | testApplication {
36 |
37 | application(Application::badTestApplication)
38 |
39 | val exception = shouldThrow {
40 | client.get(CHECK_ENDPOINT)
41 | }
42 |
43 | exception.message.shouldBe("Add one cache provider!")
44 | }
45 | }
46 |
47 | @Test
48 | fun `check cache call no set`() {
49 | testApplication {
50 | val provider = buildProvider(mutableMapOf(CHECK_ENDPOINT to TextContent("Test", ContentType.Any)))
51 |
52 | install(SimpleCache) {
53 | testCache(provider)
54 | }
55 |
56 | application {
57 | testApplication()
58 | }
59 |
60 | val response = client.get(CHECK_ENDPOINT)
61 | response.readRawBytes().toString(Charsets.UTF_8).shouldBe("Test")
62 |
63 | verify(provider, times(1)).getCache(eq(CHECK_ENDPOINT))
64 | verify(provider, never()).setCache(anyString(), any(), anyOrNull())
65 | }
66 | }
67 |
68 | @Test
69 | fun `check bad response not cached`() {
70 | testApplication {
71 | val cache = mutableMapOf()
72 | val provider = buildProvider(cache)
73 |
74 | install(SimpleCache) {
75 | testCache(provider)
76 | }
77 |
78 | application(Application::testApplication)
79 |
80 | for (i in 1..2) {
81 | val response = client.get("/bad")
82 | response.shouldHaveStatus(HttpStatusCode.BadRequest)
83 | verify(provider, times(2 * i)).getCache(eq("/bad"))
84 | verify(provider, times(1 * i)).handleBadResponse()
85 | verify(provider, times(0)).setCache(eq("/bad"), any(), anyOrNull())
86 | cache.shouldBeEmpty()
87 | }
88 | }
89 | }
90 |
91 | @Test
92 | fun `check cache work`() {
93 | testApplication {
94 | val cache = mutableMapOf()
95 | val provider = buildProvider(cache)
96 |
97 | install(SimpleCache) {
98 | testCache(provider)
99 | }
100 |
101 | application(Application::testApplication)
102 |
103 | for (i in 0..1) {
104 | val response = client.get(CHECK_ENDPOINT)
105 | response.readRawBytes().toString(Charsets.UTF_8).toIntOrNull().shouldNotBeNull()
106 | verify(provider, times(2 + i)).getCache(eq(CHECK_ENDPOINT))
107 | verify(provider, times(1)).setCache(eq(CHECK_ENDPOINT), any(), anyOrNull())
108 | cache.shouldNotBeEmpty()
109 | }
110 | }
111 | }
112 |
113 | @Test
114 | fun `check cache is concurrency`() {
115 | testApplication {
116 | val provider = buildProvider()
117 |
118 | install(SimpleCache) {
119 | testCache(provider)
120 | }
121 |
122 | application(Application::testApplication)
123 |
124 | runBlocking {
125 | val totalThreads = 1000
126 | val deferred = (1..totalThreads).map {
127 | if (it == 100) {
128 | delay(500)
129 | }
130 | async {
131 | client.get(CHECK_ENDPOINT)
132 | }
133 | }
134 |
135 | val result = deferred.awaitAll().map { it.bodyAsText().toInt() }.groupBy { it }
136 | .map { it.key to it.value.size }
137 | result.shouldBeSingleton {
138 | it.second.shouldBe(totalThreads)
139 | }
140 |
141 | verify(provider, atMost(1100)).getCache(eq(CHECK_ENDPOINT))
142 | verify(provider, times(1)).setCache(eq(CHECK_ENDPOINT), any(), anyOrNull())
143 | }
144 | }
145 | }
146 |
147 | @Test
148 | fun `check bad responses aren not locked`() {
149 |
150 | testApplication {
151 |
152 | install(SimpleCache) {
153 | testCache(buildProvider())
154 | }
155 |
156 | application(Application::testApplication)
157 |
158 | runBlocking {
159 | val totalThreads = 100
160 | val deferred = (1..totalThreads).map {
161 | async {
162 | client.get("/bad")
163 | }
164 | }
165 |
166 | val result = deferred.awaitAll().map { it.bodyAsText() }.groupBy { it }
167 | .map { it.key to it.value.size }
168 | result.shouldBeSingleton {
169 | it.second.shouldBe(totalThreads)
170 | }
171 | }
172 | }
173 | }
174 |
175 | @Test
176 | fun `check route thrown exceptions aren not locked`() {
177 | testApplication {
178 |
179 | install(SimpleCache) {
180 | testCache(buildProvider())
181 | }
182 |
183 | application(Application::testApplication)
184 |
185 | runBlocking {
186 | val totalThreads = 100
187 | val deferred = (1..totalThreads).map {
188 | async {
189 | shouldThrow {
190 | client.get("/exception")
191 | }
192 | }
193 | }
194 |
195 | val result = deferred.awaitAll().map { it.message }.groupBy { it }
196 | .map { it.key to it.value.size }
197 | result.shouldBeSingleton {
198 | it.second.shouldBe(totalThreads)
199 | }
200 | }
201 | }
202 | }
203 |
204 | @Test
205 | fun `check all parameters`() {
206 | val firstCacheKey = "/check?param1=value1¶m2=value2"
207 | val secondCacheKey = "/check?param1=value1¶m2=value2¶m3=value3"
208 | val thirdKey = "/check?param1=value1¶ms=1,2"
209 | val cache = mutableMapOf()
210 | val provider = buildProvider(cache)
211 |
212 | testApplication {
213 |
214 | install(SimpleCache) {
215 | testCache(provider)
216 | }
217 |
218 | application(Application::testApplication)
219 |
220 | val firstResponse = client.get("/check?param2=value2¶m1=value1")
221 | firstResponse.shouldHaveStatus(HttpStatusCode.OK)
222 | verify(provider, times(2)).getCache(eq(firstCacheKey))
223 | verify(provider, times(1)).setCache(eq(firstCacheKey), any(), anyOrNull())
224 | cache.keys.shouldBeSingleton {
225 | it.shouldBe(firstCacheKey)
226 | }
227 |
228 | val secondResponse = client.get("/check?param1=value1¶m2=value2")
229 | secondResponse.shouldHaveStatus(HttpStatusCode.OK)
230 | secondResponse.bodyAsText().toInt().shouldBe(firstResponse.bodyAsText().toInt())
231 | verify(provider, times(3)).getCache(eq(firstCacheKey))
232 | verify(provider, times(1)).setCache(eq(firstCacheKey), any(), anyOrNull())
233 | cache.keys.shouldBeSingleton {
234 | it.shouldBe(firstCacheKey)
235 | }
236 |
237 | val thirdResponse = client.get("/check?param2=value2¶m3=value3¶m1=value1")
238 | thirdResponse.shouldHaveStatus(HttpStatusCode.OK)
239 | thirdResponse.bodyAsText().toInt().shouldNotBe(firstResponse.bodyAsText().toInt())
240 | verify(provider, times(2)).getCache(eq(secondCacheKey))
241 | verify(provider, times(1)).setCache(eq(secondCacheKey), any(), anyOrNull())
242 | cache.keys.shouldHaveSize(2).shouldContain(secondCacheKey)
243 |
244 | val fourthResponse = client.get("/check?params=1¶ms=2¶m1=value1")
245 | fourthResponse.shouldHaveStatus(HttpStatusCode.OK)
246 | fourthResponse.bodyAsText().toInt().shouldNotBe(thirdResponse.bodyAsText().toInt())
247 | verify(provider, times(2)).getCache(eq(thirdKey))
248 | verify(provider, times(1)).setCache(eq(thirdKey), any(), anyOrNull())
249 | cache.keys.shouldHaveSize(3).shouldContain(thirdKey)
250 |
251 | val fifthResponse = client.get("/check?param1=value1¶ms=2¶ms=1")
252 | fifthResponse.shouldHaveStatus(HttpStatusCode.OK)
253 | fifthResponse.bodyAsText().toInt().shouldBe(fourthResponse.bodyAsText().toInt())
254 | verify(provider, times(3)).getCache(eq(thirdKey))
255 | verify(provider, times(1)).setCache(eq(thirdKey), any(), anyOrNull())
256 | cache.keys.shouldHaveSize(3).shouldContain(thirdKey)
257 | }
258 | }
259 |
260 | @Test
261 | fun `check parameters in keys`() {
262 | val cacheOneKey = "/check?param1=value1"
263 | val cacheTwoKeys = "/check?param1=value1¶m3=value3"
264 | val cache = mutableMapOf()
265 | val provider = buildProvider(cache)
266 |
267 | testApplication {
268 |
269 | install(SimpleCache) {
270 | testCache(provider)
271 | }
272 |
273 | application(Application::testApplicationWithKeys)
274 |
275 | val firstResponse = client.get("/check?param2=value2¶m1=value1")
276 | firstResponse.shouldHaveStatus(HttpStatusCode.OK)
277 | verify(provider, times(2)).getCache(eq(cacheOneKey))
278 | verify(provider, times(1)).setCache(eq(cacheOneKey), any(), anyOrNull())
279 | cache.keys.shouldBeSingleton {
280 | it.shouldBe(cacheOneKey)
281 | }
282 |
283 | val secondResponse = client.get("/check?param2=value21¶m1=value1")
284 | secondResponse.shouldHaveStatus(HttpStatusCode.OK)
285 | secondResponse.bodyAsText().toInt().shouldBe(firstResponse.bodyAsText().toInt())
286 | verify(provider, times(3)).getCache(eq(cacheOneKey))
287 | verify(provider, times(1)).setCache(eq(cacheOneKey), any(), anyOrNull())
288 | cache.keys.shouldBeSingleton {
289 | it.shouldBe(cacheOneKey)
290 | }
291 |
292 | val thirdResponse = client.get("/check?param2=value2¶m3=value3¶m1=value1")
293 | thirdResponse.shouldHaveStatus(HttpStatusCode.OK)
294 | thirdResponse.bodyAsText().toInt().shouldNotBe(firstResponse.bodyAsText().toInt())
295 | verify(provider, times(2)).getCache(eq(cacheTwoKeys))
296 | verify(provider, times(1)).setCache(eq(cacheTwoKeys), any(), anyOrNull())
297 | cache.keys.shouldHaveSize(2).shouldContain(cacheTwoKeys)
298 | }
299 | }
300 |
301 | private fun buildProvider(cache: MutableMap = mutableMapOf(), invalidateDuration: Duration = 5.minutes): SimpleCacheProvider {
302 | val provider = mock(defaultAnswer = Answers.CALLS_REAL_METHODS) {
303 | on { invalidateAt } doReturn invalidateDuration
304 | onBlocking { setCache(anyString(), any(), anyOrNull()) } doAnswer {
305 | cache[it.arguments[0].toString()] = it.arguments[1]
306 | }
307 | onBlocking { getCache(anyString()) } doAnswer {
308 | cache[it.arguments[0]]
309 | }
310 | }
311 |
312 | provider::class.java.superclass.getDeclaredField("mutex").also { it.isAccessible = true }.set(provider, Mutex())
313 |
314 | return provider
315 | }
316 |
317 | companion object {
318 | private const val CHECK_ENDPOINT = "/check"
319 | }
320 | }
--------------------------------------------------------------------------------
/ktor-simple-cache/src/test/kotlin/com/ucasoft/ktor/simpleCache/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleCache
2 |
3 | import io.ktor.http.*
4 | import io.ktor.server.application.*
5 | import io.ktor.server.response.*
6 | import io.ktor.server.routing.*
7 | import kotlin.random.Random
8 |
9 | fun Application.badTestApplication() {
10 |
11 | install(SimpleCache) {
12 | }
13 |
14 | routing {
15 | cacheOutput {
16 | get("/check") {
17 | call.respondText("Check response")
18 | }
19 | }
20 | }
21 | }
22 |
23 | fun Application.testApplication() {
24 |
25 | routing {
26 | cacheOutput {
27 | get("/check") {
28 | call.respondText(Random.nextInt().toString())
29 | }
30 | get("/bad") {
31 | call.respond(HttpStatusCode.BadRequest, "Bad request")
32 | }
33 | get("/exception") {
34 | throw RuntimeException("Just an exception")
35 | }
36 | }
37 | }
38 | }
39 |
40 | fun Application.testApplicationWithKeys() {
41 | routing {
42 | cacheOutput(queryKeys = listOf("param1", "param3")) {
43 | get("/check") {
44 | call.respondText(Random.nextInt().toString())
45 | }
46 | }
47 | }
48 | }
49 |
50 | fun SimpleCacheConfig.testCache(
51 | testProvider : SimpleCacheProvider
52 | ){
53 | provider = testProvider
54 | }
--------------------------------------------------------------------------------
/ktor-simple-memory-cache/README.md:
--------------------------------------------------------------------------------
1 | # Ktor Simple Memory Cache
2 | Memory cache provider for Ktor Simple Cache plugin
3 |
4 | [](https://search.maven.org/artifact/com.ucasoft.ktor/ktor-simple-memory-cache/0.53.4/jar)
5 | ## Setup
6 | ### Gradle
7 | ```kotlin
8 | repositories {
9 | mavenCentral()
10 | }
11 |
12 | implementation("com.ucasoft.ktor:ktor-simple-memory-cache:0.53.4")
13 | ```
14 | ## Usage
15 | ```kotlin
16 | install(SimpleCache) {
17 | memoryCache {
18 | invalidateAt = 10.seconds
19 | }
20 | }
21 |
22 | routing {
23 | cacheOutput(2.seconds) {
24 | get("short-cache") {
25 | call.respond(Random.nextInt())
26 | }
27 | }
28 | cacheOutput {
29 | get("default-cache") {
30 | call.respond(Random.nextInt())
31 | }
32 | }
33 | }
34 | ```
--------------------------------------------------------------------------------
/ktor-simple-memory-cache/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | kotlin("plugin.serialization")
4 | id("org.jetbrains.kotlinx.kover")
5 | id("publish")
6 | }
7 |
8 | kotlin {
9 | jvmToolchain(11)
10 | jvm()
11 | linuxX64()
12 | macosX64()
13 | sourceSets {
14 | val commonMain by getting {
15 | dependencies {
16 | implementation(project(":ktor-simple-cache"))
17 | implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
18 | }
19 | kotlin.srcDir("src/main/kotlin")
20 | }
21 | val commonTest by getting {
22 | dependencies {
23 | implementation(kotlin("test"))
24 | implementation(ktorServer("test-host"))
25 | implementation(ktorClient("content-negotiation"))
26 | implementation(ktorServer("content-negotiation"))
27 | implementation(ktor("serialization-kotlinx-json"))
28 | implementation(kotest("assertions-core"))
29 | implementation(kotestEx("assertions-ktor", "2.0.0"))
30 | }
31 | kotlin.srcDir("src/test/kotlin")
32 | }
33 | }
34 | }
35 |
36 | libraryData {
37 | name.set("Ktor Simple Memory Cache")
38 | description.set("Memory cache provider for Simple Cache plugin")
39 | }
--------------------------------------------------------------------------------
/ktor-simple-memory-cache/src/main/kotlin/com/ucasoft/ktor/simpleMemoryCache/SimpleMemoryCacheProvider.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleMemoryCache
2 |
3 | import com.ucasoft.ktor.simpleCache.SimpleCacheConfig
4 | import com.ucasoft.ktor.simpleCache.SimpleCacheProvider
5 | import kotlinx.datetime.Clock
6 | import kotlinx.datetime.Instant
7 | import kotlin.time.Duration
8 |
9 | class SimpleMemoryCacheProvider(config: Config) : SimpleCacheProvider(config) {
10 |
11 | private val cache = mutableMapOf()
12 |
13 | override suspend fun getCache(key: String): Any? {
14 | val `object` = cache[key]
15 | return if (`object`?.isExpired != false) {
16 | null
17 | } else {
18 | `object`.content
19 | }
20 | }
21 |
22 |
23 | override suspend fun setCache(key: String, content: Any, invalidateAt: Duration?) {
24 | cache[key] = SimpleMemoryCacheObject(content, invalidateAt ?: this.invalidateAt)
25 | }
26 |
27 | class Config internal constructor() : SimpleCacheProvider.Config()
28 | }
29 |
30 | private data class SimpleMemoryCacheObject(val content: Any, val duration: Duration, val start: Instant = Clock.System.now()) {
31 |
32 | val isExpired: Boolean
33 | get() = Clock.System.now() - start > duration
34 | }
35 |
36 | fun SimpleCacheConfig.memoryCache(
37 | configure : SimpleMemoryCacheProvider.Config.() -> Unit
38 | ){
39 | provider = SimpleMemoryCacheProvider(SimpleMemoryCacheProvider.Config().apply(configure))
40 | }
--------------------------------------------------------------------------------
/ktor-simple-memory-cache/src/test/kotlin/com/ucasoft/ktor/simpleMemoryCache/MemoryCacheTests.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleMemoryCache
2 |
3 | import io.kotest.assertions.ktor.client.shouldHaveStatus
4 | import io.kotest.matchers.collections.shouldBeSingleton
5 | import io.kotest.matchers.shouldBe
6 | import io.kotest.matchers.shouldNotBe
7 | import io.ktor.client.call.*
8 | import io.ktor.client.request.*
9 | import io.ktor.http.*
10 | import io.ktor.serialization.kotlinx.json.*
11 | import io.ktor.server.application.*
12 | import io.ktor.client.plugins.contentnegotiation.*
13 | import io.ktor.server.testing.*
14 | import kotlinx.coroutines.async
15 | import kotlinx.coroutines.awaitAll
16 | import kotlinx.coroutines.delay
17 | import kotlinx.coroutines.runBlocking
18 | import kotlin.test.Test
19 |
20 | internal class MemoryCacheTests {
21 |
22 | @Test
23 | fun `check cache is concurrency`() {
24 | testApplication {
25 | val jsonClient = client.config {
26 | install(ContentNegotiation) {
27 | json()
28 | }
29 | }
30 |
31 | application(Application::testApplication)
32 |
33 | runBlocking {
34 | val totalThreads = 1000
35 | val deferred = (1..totalThreads).map {
36 | async {
37 | jsonClient.get("long")
38 | }
39 | }
40 |
41 | val result = deferred.awaitAll().map { it.body() }.groupBy { it.id }
42 | .map { it.key to it.value.size }
43 | result.shouldBeSingleton {
44 | it.second.shouldBe(totalThreads)
45 | }
46 | }
47 | }
48 | }
49 |
50 | @Test
51 | fun `test memory cache`() {
52 | testApplication {
53 | val jsonClient = client.config {
54 | install(ContentNegotiation) {
55 | json()
56 | }
57 | }
58 |
59 | application(Application::testApplication)
60 |
61 | val response = jsonClient.get("short")
62 | val longResponse = jsonClient.get("long")
63 |
64 | response.shouldHaveStatus(HttpStatusCode.OK)
65 | longResponse.shouldHaveStatus(HttpStatusCode.OK)
66 |
67 | val firstBody = response.body()
68 |
69 | val secondResponse = jsonClient.get("short")
70 | secondResponse.body().id.shouldBe(firstBody.id)
71 | runBlocking {
72 | delay(3000)
73 | }
74 | val thirdResponse = jsonClient.get("short")
75 | thirdResponse.body().id.shouldNotBe(firstBody.id)
76 |
77 | val secondLongResponse = jsonClient.get("long")
78 | secondLongResponse.body().id.shouldBe(longResponse.body().id)
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/ktor-simple-memory-cache/src/test/kotlin/com/ucasoft/ktor/simpleMemoryCache/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleMemoryCache
2 |
3 | import com.ucasoft.ktor.simpleCache.SimpleCache
4 | import com.ucasoft.ktor.simpleCache.cacheOutput
5 | import io.ktor.serialization.kotlinx.json.*
6 | import io.ktor.server.application.*
7 | import io.ktor.server.plugins.contentnegotiation.*
8 | import io.ktor.server.response.*
9 | import io.ktor.server.routing.*
10 | import kotlinx.serialization.Serializable
11 | import kotlin.random.Random
12 | import kotlin.time.Duration.Companion.seconds
13 |
14 | @Serializable
15 | data class TestResponse(val id: Int = Random.nextInt())
16 |
17 | fun Application.testApplication() {
18 |
19 | install(ContentNegotiation) {
20 | json()
21 | }
22 |
23 | install(SimpleCache) {
24 | memoryCache {
25 | invalidateAt = 10.seconds
26 | }
27 | }
28 |
29 | routing {
30 | cacheOutput(2.seconds) {
31 | get("short") {
32 | call.respond(TestResponse())
33 | }
34 | }
35 | cacheOutput {
36 | get("long") {
37 | call.respond(TestResponse())
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/ktor-simple-redis-cache/README.md:
--------------------------------------------------------------------------------
1 | # Ktor Simple Redis Cache
2 | Redis cache provider for Ktor Simple Cache plugin
3 |
4 | [](https://search.maven.org/artifact/com.ucasoft.ktor/ktor-simple-redis-cache/0.53.4/jar)
5 | ## Setup
6 | ### Gradle
7 | ```kotlin
8 | repositories {
9 | mavenCentral()
10 | }
11 |
12 | implementation("com.ucasoft.ktor:ktor-simple-redis-cache:0.53.4")
13 | ```
14 | ## Usage
15 | ```kotlin
16 | install(SimpleCache) {
17 | redisCache {
18 | invalidateAt = 10.seconds
19 | host = redis.host
20 | port = redis.firstMappedPort
21 | }
22 | }
23 |
24 | routing {
25 | cacheOutput(2.seconds) {
26 | get("short-cache") {
27 | call.respond(Random.nextInt())
28 | }
29 | }
30 | cacheOutput {
31 | get("default-cache") {
32 | call.respond(Random.nextInt())
33 | }
34 | }
35 | }
36 | ```
--------------------------------------------------------------------------------
/ktor-simple-redis-cache/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | kotlin("plugin.serialization")
4 | id("org.jetbrains.kotlinx.kover")
5 | id("publish")
6 | }
7 |
8 | kotlin {
9 | jvmToolchain(11)
10 | jvm {
11 | tasks.withType {
12 | useJUnitPlatform()
13 | }
14 | }
15 | sourceSets {
16 | val jvmMain by getting {
17 | dependencies {
18 | implementation(project(":ktor-simple-cache"))
19 | implementation("redis.clients:jedis:5.1.5")
20 | implementation("com.google.code.gson:gson:2.12.1")
21 | }
22 | kotlin.srcDir("src/main/kotlin")
23 | }
24 | val jvmTest by getting {
25 | dependencies {
26 | implementation("com.redis.testcontainers:testcontainers-redis-junit:1.6.4")
27 | implementation(kotlin("test"))
28 | implementation(ktorServer("test-host"))
29 | implementation(ktorClient("content-negotiation"))
30 | implementation(ktorServer("content-negotiation"))
31 | implementation(ktor("serialization-kotlinx-json"))
32 | implementation(kotest("assertions-core"))
33 | implementation(kotestEx("assertions-ktor", "2.0.0"))
34 | }
35 | kotlin.srcDir("src/test/kotlin")
36 | }
37 | }
38 | }
39 |
40 | libraryData {
41 | name.set("Ktor Simple Redis Cache")
42 | description.set("Redis cache provider for Simple Cache plugin")
43 | }
--------------------------------------------------------------------------------
/ktor-simple-redis-cache/src/main/kotlin/com/ucasoft/ktor/simpleRedisCache/SimpleRedisCacheProvider.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleRedisCache
2 |
3 | import com.google.gson.Gson
4 | import com.ucasoft.ktor.simpleCache.SimpleCacheConfig
5 | import com.ucasoft.ktor.simpleCache.SimpleCacheProvider
6 | import redis.clients.jedis.JedisPooled
7 | import kotlin.time.Duration
8 |
9 | class SimpleRedisCacheProvider(config: Config) : SimpleCacheProvider(config) {
10 |
11 | private val jedis: JedisPooled = JedisPooled(config.host, config.port, config.ssl)
12 |
13 | override suspend fun getCache(key: String): Any? = if (jedis.exists(key)) SimpleRedisCacheObject.fromCache(jedis[key]) else null
14 |
15 | override suspend fun setCache(key: String, content: Any, invalidateAt: Duration?) {
16 | val expired = (invalidateAt ?: this.invalidateAt).inWholeMilliseconds
17 | jedis.psetex(key, expired, SimpleRedisCacheObject.fromObject(content).toString())
18 | }
19 |
20 | class Config internal constructor(): SimpleCacheProvider.Config() {
21 |
22 | var host = "localhost"
23 |
24 | var port = 6379
25 |
26 | var ssl = false
27 | }
28 | }
29 |
30 | private class SimpleRedisCacheObject(val type: String, val content: String) {
31 |
32 | override fun toString() = "$type%#%$content"
33 |
34 | companion object {
35 |
36 | fun fromObject(`object`: Any) = SimpleRedisCacheObject(`object`::class.java.name, Gson().toJson(`object`))
37 |
38 | fun fromCache(cache: String): Any {
39 | val data = cache.split("%#%")
40 | return Gson().fromJson(data.last(), Class.forName(data.first()))
41 | }
42 | }
43 | }
44 |
45 | fun SimpleCacheConfig.redisCache(
46 | configure : SimpleRedisCacheProvider.Config.() -> Unit
47 | ){
48 | provider = SimpleRedisCacheProvider(SimpleRedisCacheProvider.Config().apply(configure))
49 | }
--------------------------------------------------------------------------------
/ktor-simple-redis-cache/src/test/kotlin/com/ucasoft/ktor/simpleRedisCache/RedisCacheTests.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleRedisCache
2 |
3 | import com.redis.testcontainers.RedisContainer
4 | import com.redis.testcontainers.RedisContainer.DEFAULT_IMAGE_NAME
5 | import com.ucasoft.ktor.simpleCache.SimpleCache
6 | import io.kotest.assertions.ktor.client.shouldHaveStatus
7 | import io.kotest.matchers.collections.shouldBeSingleton
8 | import io.kotest.matchers.shouldBe
9 | import io.kotest.matchers.shouldNotBe
10 | import io.ktor.client.call.*
11 | import io.ktor.client.plugins.contentnegotiation.*
12 | import io.ktor.client.request.*
13 | import io.ktor.http.*
14 | import io.ktor.serialization.kotlinx.json.*
15 | import io.ktor.server.application.*
16 | import io.ktor.server.testing.*
17 | import kotlinx.coroutines.async
18 | import kotlinx.coroutines.awaitAll
19 | import kotlinx.coroutines.runBlocking
20 | import org.junit.jupiter.api.AfterAll
21 | import org.junit.jupiter.api.BeforeAll
22 | import org.junit.jupiter.api.Test
23 | import org.testcontainers.junit.jupiter.Container
24 | import kotlin.time.Duration.Companion.seconds
25 |
26 | internal class RedisCacheTests {
27 |
28 | @Test
29 | fun `check cache is concurrency`() {
30 | testApplication {
31 | val jsonClient = client.config {
32 | install(ContentNegotiation) {
33 | json()
34 | }
35 | }
36 |
37 | install(SimpleCache) {
38 | redisCache {
39 | invalidateAt = 10.seconds
40 | this.host = redisContainer.host
41 | this.port = redisContainer.firstMappedPort
42 | }
43 | }
44 |
45 | application(Application::testApplication)
46 |
47 | runBlocking {
48 | val totalThreads = 1000
49 | val deferred = (1..totalThreads).map {
50 | async {
51 | jsonClient.get("/long")
52 | }
53 | }
54 |
55 | val result = deferred.awaitAll().map { it.body() }.groupBy { it.id }
56 | .map { it.key to it.value.size }
57 | result.shouldBeSingleton {
58 | it.second.shouldBe(totalThreads)
59 | }
60 | }
61 | }
62 | }
63 |
64 | @Test
65 | fun `test redis cache`() {
66 | testApplication {
67 | val jsonClient = client.config {
68 | install(ContentNegotiation) {
69 | json()
70 | }
71 | }
72 |
73 | install(SimpleCache) {
74 | redisCache {
75 | invalidateAt = 10.seconds
76 | this.host = redisContainer.host
77 | this.port = redisContainer.firstMappedPort
78 | }
79 | }
80 |
81 | application(Application::testApplication)
82 |
83 | val response = jsonClient.get("/short")
84 | val longResponse = jsonClient.get("/long")
85 |
86 | response.shouldHaveStatus(HttpStatusCode.OK)
87 | longResponse.shouldHaveStatus(HttpStatusCode.OK)
88 |
89 |
90 | val firstBody = response.body()
91 |
92 | val secondResponse = jsonClient.get("/short")
93 | secondResponse.body().id.shouldBe(firstBody.id)
94 | Thread.sleep(3000)
95 | val thirdResponse = jsonClient.get("/short")
96 | thirdResponse.body().shouldNotBe(firstBody.id)
97 |
98 | val secondLongResponse = jsonClient.get("/long")
99 | secondLongResponse.body().id.shouldBe(longResponse.body().id)
100 | }
101 | }
102 |
103 | companion object {
104 |
105 | @Container
106 | val redisContainer = RedisContainer(DEFAULT_IMAGE_NAME)
107 |
108 | @JvmStatic
109 | @BeforeAll
110 | fun setup() {
111 | redisContainer.start()
112 | }
113 |
114 | @JvmStatic
115 | @AfterAll
116 | fun tearDown() {
117 | if (redisContainer.isRunning) {
118 | redisContainer.stop()
119 | }
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/ktor-simple-redis-cache/src/test/kotlin/com/ucasoft/ktor/simpleRedisCache/TestApplication.kt:
--------------------------------------------------------------------------------
1 | package com.ucasoft.ktor.simpleRedisCache
2 |
3 | import com.ucasoft.ktor.simpleCache.cacheOutput
4 | import io.ktor.serialization.kotlinx.json.*
5 | import io.ktor.server.application.*
6 | import io.ktor.server.plugins.contentnegotiation.*
7 | import io.ktor.server.response.*
8 | import io.ktor.server.routing.*
9 | import kotlinx.serialization.Serializable
10 | import kotlin.random.Random
11 | import kotlin.time.Duration.Companion.seconds
12 |
13 | @Serializable
14 | data class TestResponse(val id: Int = Random.nextInt())
15 |
16 | fun Application.testApplication() {
17 |
18 | install(ContentNegotiation) {
19 | json()
20 | }
21 |
22 | routing {
23 | cacheOutput(2.seconds) {
24 | get("short") {
25 | call.respond(TestResponse())
26 | }
27 | }
28 | cacheOutput {
29 | get("long") {
30 | call.respond(TestResponse())
31 | }
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | resolutionStrategy {
3 | plugins {
4 | val kotlinVersion = "2.1.10"
5 | kotlin("multiplatform") version kotlinVersion apply false
6 | kotlin("plugin.serialization") version kotlinVersion apply false
7 | id("org.jetbrains.kotlinx.kover") version "0.9.1" apply false
8 | }
9 | }
10 | }
11 |
12 | plugins {
13 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
14 | }
15 |
16 | rootProject.name = "simple-cache"
17 |
18 | include("ktor-simple-cache")
19 |
20 | include("ktor-simple-memory-cache")
21 |
22 | include("ktor-simple-redis-cache")
23 |
--------------------------------------------------------------------------------