Skip to content

Commit d8f8b04

Browse files
authored
Merge pull request #987 from profunktor/lua-script-extensions
Introduce lua scripting extensions for convenience
2 parents 72369b3 + 92f3a4a commit d8f8b04

File tree

6 files changed

+183
-0
lines changed

6 files changed

+183
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2018-2021 ProfunKtor
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dev.profunktor.redis4cats.extensions
18+
19+
import cats.effect.kernel.{ Resource, Sync }
20+
import cats.syntax.all._
21+
import cats.{ ApplicativeThrow, Functor }
22+
import dev.profunktor.redis4cats.algebra.Scripting
23+
import dev.profunktor.redis4cats.effects.ScriptOutputType
24+
import io.lettuce.core.RedisNoScriptException
25+
26+
object luaScripting {
27+
28+
final case class LuaScript(contents: String, sha: String)
29+
30+
object LuaScript {
31+
32+
def make[F[_]: Functor](redis: Scripting[F, _, _])(contents: String): F[LuaScript] =
33+
redis.digest(contents).map(sha => LuaScript(contents, sha))
34+
35+
/** Helper to load a lua script from resources/lua/{resourceName}. The path to the lua scripts can be configured.
36+
* @param redis
37+
* redis commands
38+
* @param resourceName
39+
* filename of the lua script
40+
* @param pathToScripts
41+
* path to the lua scripts
42+
* @return
43+
* [[LuaScript]]
44+
*/
45+
def loadFromResources[F[_]: Sync](redis: Scripting[F, _, _])(
46+
resourceName: String,
47+
pathToScripts: String = "lua"
48+
): F[LuaScript] =
49+
Resource
50+
.fromAutoCloseable(
51+
Sync[F].blocking(
52+
scala.io.Source.fromResource(resource = s"$pathToScripts/$resourceName")
53+
)
54+
)
55+
.evalMap(fileSrc => Sync[F].blocking(fileSrc.mkString))
56+
.use(make(redis))
57+
58+
}
59+
60+
implicit class LuaScriptingExtensions[F[_]: ApplicativeThrow, K, V](redis: Scripting[F, K, V]) {
61+
62+
/** Evaluate the cached lua script via it's sha. If the script is not cached, fallback to evaluating the script
63+
* directly.
64+
* @param luaScript
65+
* the lua script with its content and sha
66+
* @param output
67+
* output of script
68+
* @param keys
69+
* keys to script
70+
* @param values
71+
* values to script
72+
* @return
73+
* ScriptOutputType
74+
*/
75+
def evalLua(
76+
luaScript: LuaScript,
77+
output: ScriptOutputType[V],
78+
keys: List[K],
79+
values: List[V]
80+
): F[output.R] =
81+
redis
82+
.evalSha(
83+
luaScript.sha,
84+
output,
85+
keys,
86+
values
87+
)
88+
.recoverWith { case _: RedisNoScriptException =>
89+
redis.eval(luaScript.contents, output, keys, values)
90+
}
91+
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
local key = KEYS[1]
2+
local field = ARGV[1]
3+
local value = ARGV[2]
4+
local ttl = tonumber(ARGV[3])
5+
6+
local numFieldsSet = redis.call('hset', key, field, value)
7+
redis.call('expire', key, ttl)
8+
return numFieldsSet

modules/tests/src/test/scala/dev/profunktor/redis4cats/RedisClusterSpec.scala

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ class RedisClusterSpec extends Redis4CatsFunSuite(true) with TestScenarios {
4242

4343
test("cluster: scripts")(withRedis(scriptsScenario))
4444

45+
test("cluster: scripts lua extensions")(withRedis(scriptingLuaExtensionsScenario))
46+
4547
test("cluster: functions")(withRedis(functionsScenario))
4648

4749
test("cluster: hyperloglog api")(withRedis(hyperloglogScenario))

modules/tests/src/test/scala/dev/profunktor/redis4cats/RedisSpec.scala

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class RedisSpec extends Redis4CatsFunSuite(false) with TestScenarios {
4848

4949
test("scripts")(withRedis(scriptsScenario))
5050

51+
test("scripts lua extensions")(withRedis(scriptingLuaExtensionsScenario))
52+
5153
test("functions")(withRedis(functionsScenario))
5254

5355
test("hyperloglog api")(withRedis(hyperloglogScenario))

modules/tests/src/test/scala/dev/profunktor/redis4cats/TestScenarios.scala

+35
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,41 @@ trait TestScenarios { self: FunSuite =>
664664
} yield ()
665665
}
666666

667+
def scriptingLuaExtensionsScenario(redis: RedisCommands[IO, String, String]): IO[Unit] = {
668+
import dev.profunktor.redis4cats.extensions.luaScripting._
669+
670+
for {
671+
// hsetAndExpire.lua is an example script
672+
hsetAndExpire <- LuaScript.loadFromResources[IO](redis)("hsetAndExpire.lua")
673+
674+
_ <- redis.hGet(key = "luaExt", field = "x").map(assertEquals(_, None))
675+
_ <- redis
676+
.evalLua(
677+
hsetAndExpire,
678+
ScriptOutputType.Integer[String],
679+
keys = List("luaExt"),
680+
values = List("x", "42", "10")
681+
)
682+
.map(assertEquals(_, 1L, "1 field, 'x', should be set for key=luaExt"))
683+
_ <- redis.hGet(key = "luaExt", field = "x").map(assertEquals(_, "42".some))
684+
firstTtl <- redis.ttl("luaExt")
685+
_ <- IO(assert(firstTtl.map(_.toSeconds).exists(ttl => ttl > 0 && ttl <= 10)))
686+
687+
_ <- redis
688+
.evalLua(
689+
hsetAndExpire,
690+
ScriptOutputType.Integer[String],
691+
keys = List("luaExt"),
692+
values = List("y", "84", "20")
693+
)
694+
.map(assertEquals(_, 1L, "1 field, 'y', should be set for key=luaExt"))
695+
_ <- redis.hGet(key = "luaExt", field = "y").map(assertEquals(_, "84".some))
696+
secondTtl <- redis.ttl("luaExt")
697+
_ <- IO(assert(secondTtl.map(_.toSeconds).exists(ttl => ttl > 0 && ttl <= 20)))
698+
_ <- IO(assert(firstTtl < secondTtl))
699+
} yield ()
700+
}
701+
667702
def functionsScenario(redis: RedisCommands[IO, String, String]): IO[Unit] = {
668703
val myFunc =
669704
"""#!lua name=mylib

site/docs/effects/scripting.md

+43
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,46 @@ commandsApi.use { redis => // ScriptCommands[IO, String, String]
4545
```
4646

4747
The return type depends on the `ScriptOutputType` you pass and needs to suite the result of the Lua script itself. Possible values are `Integer`, `Value` (for decoding the result using the value codec), `Multi` (for many values) and `Status` (maps to `Unit` in Scala). Scripts can be cached for better performance using `scriptLoad` and then executed via `evalSha`, see the [redis docs]((https://redis.io/commands#scripting)) for details.
48+
49+
### Lua Scripting Extensions
50+
51+
Redis4cats provides useful extensions to the Lua scripting; methods for loading scripts and executing them by their SHA1 are provided.
52+
53+
Suppose you have the following Lua script saved under the project's `resources` folder, `mymodule/src/main/resources/lua/hsetAndExpire.lua)`:
54+
55+
```lua
56+
local key = KEYS[1]
57+
local field = ARGV[1]
58+
local value = ARGV[2]
59+
local ttl = tonumber(ARGV[3])
60+
61+
local numFieldsSet = redis.call('hset', key, field, value)
62+
redis.call('expire', key, ttl)
63+
return numFieldsSet
64+
```
65+
66+
Then you can load it into Redis and execute via:
67+
68+
```scala mdoc:silent
69+
import dev.profunktor.redis4cats.extensions.luaScripting._
70+
71+
commandsApi.use { redis => // ScriptCommands[IO, String, String]
72+
for {
73+
hsetAndExpire <- LuaScript.loadFromResources[IO](redis)("hsetAndExpire.lua")
74+
value = "42"
75+
ttl = "10"
76+
_ <- redis.evalLua(
77+
hsetAndExpire,
78+
ScriptOutputType.Integer[String],
79+
keys = List("mySetKey"),
80+
values = List("myField", value, ttl)
81+
)
82+
} yield ()
83+
}
84+
```
85+
86+
The extension api provides the following methods:
87+
- `LuaScript.make` to create a `LuaScript` instance from a string
88+
- `LuaScript.loadFromResources` to load a Lua script from resources, path is configurable and defaults to `lua/`
89+
- `evalLua` method as a shortcut for `evalSha` then falling back to `eval` if the script is not loaded yet
90+

0 commit comments

Comments
 (0)