-
Notifications
You must be signed in to change notification settings - Fork 63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scripting API (eval, evalSha, scriptExists, scriptLoad) #269
Conversation
delete sealed mark from Input and Output traits
@@ -8,8 +8,8 @@ import scala.util.matching.Regex | |||
import zio.Chunk | |||
import zio.duration.Duration | |||
|
|||
sealed trait Input[-A] { | |||
private[redis] def encode(data: A): Chunk[RespValue.BulkString] | |||
trait Input[-A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please leave it sealed. Input and output aren't really data encoders, but "shapes" of command inputs and outputs, hence no need for flexibility there. There's an issue for making data encoding more flexible (see #214).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, add redisencoder, redisdecoder according to #214
@@ -3,8 +3,7 @@ package zio.redis | |||
import zio.Chunk | |||
import zio.duration._ | |||
|
|||
sealed trait Output[+A] { | |||
|
|||
trait Output[+A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please leave it sealed.
import zio.ZIO | ||
import zio.redis.Input.EvalInput | ||
import zio.redis._ | ||
import Scripting._ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this to trait.
|
||
private[redis] object Scripting { | ||
|
||
final def evalCommand[K: Input, A: Input, R: Output]: RedisCommand[Script[K, A], R] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename to eval
. This will change as soon as you adjust input.
|
||
trait Scripting { | ||
|
||
case class Script[K, A](lua: String, keys: Seq[K], args: Seq[A]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not use Chunk
instead of Seq
. Also, make it sealed. More important, this isn't really an option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, i don't understand that comment((
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my english broke, I wanted to say let's use Chunk
instead of Seq
and let's avoid using Seq
.
}, | ||
testM("return custom data type") { | ||
|
||
case class CustomData(count: Long, avg: Long, pair: (Int, String)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make it final.
def encode(data: Iterable[A]): Chunk[RespValue.BulkString] = | ||
data.foldLeft(Chunk.empty: Chunk[RespValue.BulkString])((acc, a) => acc ++ input.encode(a)) | ||
} | ||
|
||
case class EvalInput[K, A](keysInput: Input[K], argsInput: Input[A]) extends Input[Script[K, A]] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct. What you have in Script
should be specified as an input, i.e.:
- string script
- varargs keys
- varargs args
Also, be consistent and make it final.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully, I understood you right and did how you wanted)
return sealed state for traits input, output add test for errors
def encode(data: Iterable[A]): Chunk[RespValue.BulkString] = | ||
data.foldLeft(Chunk.empty: Chunk[RespValue.BulkString])((acc, a) => acc ++ input.encode(a)) | ||
} | ||
|
||
case object EvalInput extends Input[(String, Seq[Chunk[Byte]], Seq[Chunk[Byte]])] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please no Seq
.
|
||
trait Scripting { | ||
|
||
sealed case class Script[K, A](lua: String, keys: Seq[K], args: Seq[A]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in my previous comments, this isn't an option, and it isn't even necessary anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script has been deleted. IMHO this class showed relation between keys, args and lua script, that's why i wrote it)
|
||
import java.nio.charset.StandardCharsets | ||
|
||
trait RedisEncoder[A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rename to Encoder
.
|
||
import zio.IO | ||
|
||
trait RedisDecoder[A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rename to Decoder
.
@quelgar this PR introduces the notion of codecs you mentioned in #214. Now, it doesn't propagates them throughout the API, but we might as well settle upon this API now and propagate in a separate ticket. One thing I'm not sure is |
fix linter errors delete script case class replace all seq on chunks
add script debug add script exists add script flush add script kill add script load
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anatolysergeev Please run sbt prepare
as well.
@quelgar Just a reminder to take a look at codec part :).
|
||
import zio.IO | ||
|
||
trait Decoder[A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am unsure about this. RespValue
should not leak in the client code. If we're allowing custom decoders (and we should), I think they should work on byte chunks.
|
||
import zio.Chunk | ||
|
||
trait Encoder[A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add summoner here.
* You have to write decoder that would convert | ||
* redis protocol value to a suitable type for your app | ||
* | ||
* @since 2.6.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd remove @since
as it refers to Redis version, which doesn't really matches the lib version.
final val Eval: RedisCommand[(String, Chunk[Chunk[Byte]], Chunk[Chunk[Byte]]), RespValue] = | ||
RedisCommand("EVAL", EvalInput, RespValueOutput) | ||
|
||
final val EvalSHA: RedisCommand[(String, Chunk[Chunk[Byte]], Chunk[Chunk[Byte]]), RespValue] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename to EvalSha
.
import zio.test._ | ||
|
||
trait ScriptingSpec extends BaseSpec { | ||
val scriptingSpec: Spec[Annotations with RedisExecutor, TestFailure[Any], TestSuccess] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lots of tests seems to be ignored, what's the problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test for flash command need specific test environment to run, due to tests constancy i wrote ignore.
with other tests i was straggling to wrote them that they would work like i want. For example test for command kill i don;t understand why but don't want to kill the script((, test for debug when you start command yes or sync block everything in redis and don't want to turn off debug mode.
I think that debug, kill should be run on in specific environment, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, in that case i propose shipping the testable commands in this PR and the other through follow up pull requests, i.e. once the env is figured out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok
Yeah, I can't help thinking we're missing an opportunity to simplify here. The fact there's commands here that use the Can we pull |
That was my first implementation. But @mijicd proposed to do Encoder. And i rewrote Decoder too. Main points were consistency with Input and save ADT for Output. I agree that for contributors to this library it would harder to understand why there are 2 "similar" services, but for users i don't think that would be a problem. I have more worries about that or mb i don't understand something
User should implement their own Resp deserialization or how that would work? |
@anatolysergeev Well, the original implementation unsealed the output and exposed it in user-land. I don't think we should do it, and I recalled an issue that proposed encoder / decoder mechanism. If we're aiming to simplify, then let's propose combinators to the existing input / output as @quelgar wrote. |
@anatolysergeev i believe i answered that one. Originally, input and output where envisioned as command shapes. Since there's a finite amount of those, it makes sense to seal them. However, now we're effectively giving them more power. I'd say try to keep them sealed, if we need to unseal, we can do that. |
@anatolysergeev sounds reasonable to me I think for sealed trait Input[A] {
self =>
final def contramap[B](f: B => A): Input[B] = new Input {
def encode(data: B) = self.encode(f(b))
}
} On the point " |
add contramap to Input delete decoder/encoder
I haven't done methods that would be catch failures(
|
@anatolysergeev I am sorry for my way overdue response. My workload took its toll. After a discussion with John, I realized that
|
Hello @mijicd . I am sorry for my long response too and my inactivity. I've read your comment and don't see the solution with string domain for "eval" appropriate and don't want to do it just to close the issue. |
@anatolysergeev Please don't remove input / output. As originally discussed, they're intended to specify shapes of commands. However, we need arbitrary input / output, that could be polymorphic. In the user land (API functions), this parameter would be type parameterized to require a schema. So yes, I propose digging into zio-schema API (it's been released), and drafting the solution. I do have a sketch of it, so if you're willing to wait until the weekend, we could have a pairing session. WDYT? |
I think that a great idea and i'm very excited about that. But if you mean in "pair session" an online conversation i should warn you that i'm not confident in my English speaking skills, all in all i would try my best)) |
# Conflicts: # redis/src/main/scala/zio/redis/Input.scala # redis/src/main/scala/zio/redis/Output.scala # redis/src/main/scala/zio/redis/RedisCommand.scala # redis/src/main/scala/zio/redis/api/Hashes.scala # redis/src/main/scala/zio/redis/api/Keys.scala # redis/src/main/scala/zio/redis/api/Lists.scala # redis/src/main/scala/zio/redis/api/Sets.scala # redis/src/main/scala/zio/redis/api/SortedSets.scala # redis/src/main/scala/zio/redis/api/Streams.scala # redis/src/main/scala/zio/redis/package.scala # redis/src/test/scala/zio/redis/ApiSpec.scala # redis/src/test/scala/zio/redis/OutputSpec.scala
# Conflicts: # redis/src/main/scala/zio/redis/api/Hashes.scala # redis/src/main/scala/zio/redis/api/Keys.scala # redis/src/main/scala/zio/redis/api/Sets.scala # redis/src/main/scala/zio/redis/api/SortedSets.scala # redis/src/main/scala/zio/redis/api/Streams.scala
add result builder support for mGet method clean some unnecessary explicit types in tests
# Conflicts: # redis/src/main/scala/zio/redis/ResultBuilder.scala
val encodedKeys = keys.foldLeft[Chunk[BulkString]](Chunk.empty)((cur, next) => cur ++ inputK.encode(next)) | ||
val encodedArgs = args.foldLeft[Chunk[BulkString]](Chunk.empty)((cur, next) => cur ++ inputV.encode(next)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't
val encodedKeys = keys.foldLeft[Chunk[BulkString]](Chunk.empty)((cur, next) => cur ++ inputK.encode(next)) | |
val encodedArgs = args.foldLeft[Chunk[BulkString]](Chunk.empty)((cur, next) => cur ++ inputV.encode(next)) | |
val encodedKeys = keys.flatMap(inputK.encode) | |
val encodedArgs = args.flatMap(inputV.encode) |
trait Implicits { | ||
implicit val bytesEncoder: Input[Chunk[Byte]] = ByteInput | ||
implicit val booleanInput: Input[Boolean] = BoolInput | ||
implicit val stringInput: Input[String] = StringInput | ||
implicit val longInput: Input[Long] = LongInput | ||
} | ||
|
||
object Implicits extends Implicits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be used only in tests. If that's the case, please define them there instead.
args: Chunk[A] | ||
): ResultOutputBuilder = new ResultOutputBuilder { | ||
def returning[R: Output]: ZIO[RedisExecutor, RedisError, R] = { | ||
val command = RedisCommand(Eval, EvalInput(implicitly[Input[K]], implicitly[Input[A]]), implicitly[Output[R]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If possible, add summoners for Input and Output. It'll make the call site cleaner.
|
||
final case class CustomData(count: Long, avg: Long, pair: (Int, String)) | ||
|
||
object CustomData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not nest items not directly related to CustomData in its companion object.
private val tryDecodeLong: RespValue => Long = { | ||
case RespValue.Integer(value) => value | ||
case other => throw ProtocolError(s"$other isn't a integer type") | ||
} | ||
private val tryDecodeString: RespValue => String = { | ||
case s @ RespValue.BulkString(_) => s.asString | ||
case other => throw ProtocolError(s"$other isn't a string type") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You already have these in LongOutput and StringOutput. Please check the rest implicits you defined here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's true. But Outputs required codecs and we don't have schema codecs implicit in the Output.map
. I had a long thought about that, i can not figure out how better to handle this.
- Codecs are serialize, deserialize that we put into cache ignoring redis protocol, but here we work with redis protocol
- It seems not very good to have initialization of codec in few place (now RedisExacutor has dependency of it)
- It looks like we should do something with
.map
method because inside it we actually has required implicit of codec, but how to get it from there 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One of idea was to add new .mapWithCodec
- not sure about a name))
But i don't like that solution, it's looks like a crunch to me 🤔
final def mapWithCodec[B](f: Codec => A => B): Output[B] =
new Output[B] {
protected def tryDecode(respValue: RespValue)(implicit codec: Codec): B = f(codec)(self.tryDecode(respValue))
}
implicit val decoder: Output[CustomData] = RespValueOutput.mapWithCodec { implicit codec =>
{
case RespValue.Array(elements) =>
val count = LongOutput.unsafeDecode(elements(0))
val avg = LongOutput.unsafeDecode(elements(1))
val pair = elements(2) match {
case RespValue.Array(elements) =>
(LongOutput.unsafeDecode(elements(0)).toInt, StringOutput.unsafeDecode(elements(1)))
case other => throw ProtocolError(s"$other isn't an array type")
}
CustomData(count, avg, pair)
case other => throw ProtocolError(s"$other isn't an array type")
}
}
Issue #158
That is a try to implement scripting API for redis.
This PR have debatable changes: removing sealed state for traits Input and Output. I couldn't find an answer for questions "why they are should be sealed?", "why we don't want to give the opportunity for customers of this library to implement their own encoders and decoders".
In test I've written an example how that could be easy to implement own decoder for customer if traits wouldn't be sealed.
Thanks, I'm waiting for approval to continue coding in this approach or for comments how better it should be implemented.