Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions script.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"sync"
)
Expand Down Expand Up @@ -60,11 +61,25 @@ func (s *Script) Hash() string {

func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd {
cmd := c.ScriptLoad(ctx, s.src)
if err := cmd.Err(); err == nil {
s.mu.Lock()
s.hash = cmd.Val()
s.mu.Unlock()
if err := cmd.Err(); err != nil {
return cmd
}

s.mu.Lock()
defer s.mu.Unlock()

if !s.serverSHA {
// s.hash is SHA-1(s.src) computed in NewScript, and SCRIPT LOAD returns
// that same digest. A different value is a protocol violation; keeping
// the local one means EVALSHA stays pinned to this script instead of
// running whatever the server holds under the returned digest.
if cmd.Val() != s.hash {
cmd.SetErr(fmt.Errorf("redis: SCRIPT LOAD returned digest %q, want %q", cmd.Val(), s.hash))
}
return cmd
}

s.hash = cmd.Val()
return cmd
}

Expand Down
76 changes: 76 additions & 0 deletions script_load_digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package redis

import (
"context"
"strings"
"testing"
)

// forgedSHA is a well-formed digest that is not the SHA-1 of any script used here.
const forgedSHA = "0123456789abcdef0123456789abcdef01234567"

func TestScriptLoad_KeepsClientComputedDigest(t *testing.T) {
ctx := context.Background()
c := &fakeScripter{hashToReturn: forgedSHA}

s := NewScript("return 1")
want := s.Hash()

if err := s.Load(ctx, c).Err(); err == nil {
t.Fatal("Load() err = nil, want a digest-mismatch error")
}
if got := s.Hash(); got != want {
t.Fatalf("Hash() = %q after Load, want the locally computed %q", got, want)
}

if err := s.Run(ctx, c, []string{"k"}).Err(); err != nil {
t.Fatalf("Run() err: %v", err)
}
if c.lastEvalShaSHA != want {
t.Fatalf("EVALSHA sent %q, want %q", c.lastEvalShaSHA, want)
}
}

func TestScriptLoad_KeepsClientComputedDigest_RO(t *testing.T) {
ctx := context.Background()
c := &fakeScripter{hashToReturn: forgedSHA}

s := NewScript("return 2")
want := s.Hash()
_ = s.Load(ctx, c)

if err := s.RunRO(ctx, c, []string{"k"}).Err(); err != nil {
t.Fatalf("RunRO() err: %v", err)
}
if c.lastEvalShaROSHA != want {
t.Fatalf("EVALSHA_RO sent %q, want %q", c.lastEvalShaROSHA, want)
}
}

func TestScriptLoad_MatchingDigestIsNotAnError(t *testing.T) {
ctx := context.Background()
s := NewScript("return 3")
c := &fakeScripter{hashToReturn: s.Hash()}

cmd := s.Load(ctx, c)
if err := cmd.Err(); err != nil {
t.Fatalf("Load() err: %v", err)
}
if cmd.Val() != s.Hash() {
t.Fatalf("Load() val = %q, want %q", cmd.Val(), s.Hash())
}
}

func TestScriptLoad_ServerSHAAdoptsServerDigest(t *testing.T) {
ctx := context.Background()
hash := strings.Repeat("e", 40)
c := &fakeScripter{hashToReturn: hash}

s := NewScriptServerSHA("return 4")
if err := s.Load(ctx, c).Err(); err != nil {
t.Fatalf("Load() err: %v", err)
}
if s.Hash() != hash {
t.Fatalf("Hash() = %q, want %q", s.Hash(), hash)
}
}
6 changes: 6 additions & 0 deletions script_server_sha_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type fakeScripter struct {
// behavior controls
hashToReturn string

// digests the script helper actually sent with EVALSHA/EVALSHA_RO
lastEvalShaSHA string
lastEvalShaROSHA string

// If set, the first EvalSha/EvalShaRO returns a NOSCRIPT error.
failFirstEvalShaWithNoScr bool
failFirstEvalShaROWithNoScr bool
Expand Down Expand Up @@ -62,6 +66,7 @@ func (f *fakeScripter) Eval(ctx context.Context, script string, keys []string, a
func (f *fakeScripter) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd {
f.mu.Lock()
f.evalShaCalls++
f.lastEvalShaSHA = sha1
callNum := f.evalShaCalls
fail := f.failFirstEvalShaWithNoScr && callNum == 1
f.mu.Unlock()
Expand Down Expand Up @@ -89,6 +94,7 @@ func (f *fakeScripter) EvalRO(ctx context.Context, script string, keys []string,
func (f *fakeScripter) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd {
f.mu.Lock()
f.evalShaROCalls++
f.lastEvalShaROSHA = sha1
callNum := f.evalShaROCalls
fail := f.failFirstEvalShaROWithNoScr && callNum == 1
f.mu.Unlock()
Expand Down
Loading