-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
When using scripts, there is the universal problem of managing the loading and handling of the scripts. It would be good if there was some kind of wrapper built in to handle this. I've written this for myself, and I'm curious if you would be interested in a PR to implement something similar to it so that it's automatically included in the package
The core principle of what I've done below is when trying to execute the script
- See if we have the sha, if not upload the script to redis
- Attempt execution and return results if it succeeds
- If execution returns no script then, add the script again. This handles redis restarts
- Execute again now that the script has been re-uploaded and return the results
I designed this in the way that was most convenient and expedient for me but I think there's a valid discussion around how you'd want it to work in the package, if included.
My core questions are
- Would you be interested in adding something like this
- If so, do you have an opinion on how it would need to be designed
- Would you accept a PR integrating this logic somewhere in the code
I also want to say that my code is good enough for our use case but I would obviously understand if you'd want to design it differently for integration
package redis
import (
"context"
"fmt"
"strings"
"sync"
)
type script struct {
lua string
sha *string
mu sync.RWMutex
}
var testScript = script{
lua: "return 'hello'",
}
func loadScriptSafe(ctx context.Context, script string) (string, error) {
redisSha, err := RedisClient.ScriptLoad(ctx, script).Result()
if err != nil {
return "", err
}
if len(redisSha) != 40 {
return "", fmt.Errorf("returned value is not a SHA")
}
return redisSha, nil
}
func loadScriptIfMissing(ctx context.Context, script *script) (string, error) {
script.mu.RLock()
localSha := script.sha
script.mu.RUnlock()
if localSha != nil {
return *localSha, nil
}
redisSha, err := loadScriptSafe(ctx, script.lua)
if err != nil {
return "", err
}
script.mu.Lock()
script.sha = &redisSha
script.mu.Unlock()
return redisSha, nil
}
func execScript(ctx context.Context, script *script, keys []string, args ...any) (any, error) {
// Case 1: We just started and don't have the script sha
// Case 2: Redis restarted and it doesn't have the script sha
// Case 1: Make sure we have the script sha
var err error
var sha string
sha, err = loadScriptIfMissing(ctx, script)
if err != nil {
return nil, err
}
var result any
result, err = RedisClient.EvalSha(ctx, sha, keys, args).Result()
if err == nil {
return result, nil
}
if !strings.Contains(err.Error(), "NOSCRIPT") {
return nil, err
}
// Case 2: Redis crashed and it doesn't have the script sha
_, err = loadScriptSafe(ctx, script.lua)
if err != nil {
return nil, err
}
return RedisClient.EvalSha(ctx, sha, keys, args).Result()
}
func execTestScript(ctx context.Context) (string, error) {
result, err := execScript(ctx, &testScript, nil)
if err != nil {
return "", err
}
if resultVal, ok := result.(string); ok {
return resultVal, nil
} else {
return "", fmt.Errorf("received unexpected value from script evaluation")
}
}