owpengram-server/internal/store/redisstore/code_cas.go

147 lines
4.1 KiB
Go

package redisstore
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"telesrv/internal/store"
)
const getPhoneCodeSnapshotScript = `
local raw = redis.call('GET', KEYS[1])
if not raw then
return ''
end
local decoded, record = pcall(cjson.decode, raw)
if not decoded or type(record) ~= 'table'
or tonumber(record.Version or 0) ~= tonumber(ARGV[1])
or (record.Revision or '') == '' then
redis.call('DEL', KEYS[1])
return ''
end
return raw
`
const compareAndUpdatePhoneCodeScript = `
local raw = redis.call('GET', KEYS[1])
if not raw then
return 0
end
local decoded, record = pcall(cjson.decode, raw)
if not decoded or type(record) ~= 'table'
or tonumber(record.Version or 0) ~= tonumber(ARGV[1])
or (record.Revision or '') == '' then
redis.call('DEL', KEYS[1])
return 0
end
if (record.Purpose or '') ~= '' or record.Revision ~= ARGV[2] then
return 0
end
redis.call('SET', KEYS[1], ARGV[3], 'KEEPTTL')
return 1
`
const compareAndDeletePhoneCodeScript = `
local raw = redis.call('GET', KEYS[1])
if not raw then
return 0
end
local decoded, record = pcall(cjson.decode, raw)
if not decoded or type(record) ~= 'table'
or tonumber(record.Version or 0) ~= tonumber(ARGV[1])
or (record.Revision or '') == '' then
redis.call('DEL', KEYS[1])
return 0
end
if (record.Purpose or '') ~= '' or record.Revision ~= ARGV[2] then
return 0
end
redis.call('DEL', KEYS[1])
return 1
`
func (s *CodeStore) GetSnapshot(ctx context.Context, hash string) (store.PhoneCodeSnapshot, bool, error) {
value, err := s.c.Eval(
ctx,
getPhoneCodeSnapshotScript,
[]string{codeKey(hash)},
store.PhoneCodeVersionCurrent,
).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return store.PhoneCodeSnapshot{}, false, nil
}
return store.PhoneCodeSnapshot{}, false, fmt.Errorf("redis get phone code snapshot: %w", err)
}
raw, ok := value.(string)
if !ok {
return store.PhoneCodeSnapshot{}, false, fmt.Errorf("redis get phone code snapshot: unexpected result %T", value)
}
if raw == "" {
return store.PhoneCodeSnapshot{}, false, nil
}
var record store.PhoneCode
if err := json.Unmarshal([]byte(raw), &record); err != nil {
return store.PhoneCodeSnapshot{}, false, fmt.Errorf("redis get phone code snapshot decode: %w", err)
}
if record.Version != store.PhoneCodeVersionCurrent || record.Revision == "" {
return store.PhoneCodeSnapshot{}, false, fmt.Errorf("redis get phone code snapshot returned invalid version/revision")
}
return store.PhoneCodeSnapshot{Record: record, Revision: record.Revision}, true, nil
}
func (s *CodeStore) CompareAndUpdate(ctx context.Context, hash, expectedRevision string, next store.PhoneCode) (bool, error) {
if expectedRevision == "" || next.Version != store.PhoneCodeVersionCurrent || next.Purpose != "" {
return false, nil
}
revision, err := store.NewPhoneCodeRevisionToken()
if err != nil {
return false, err
}
next.Revision = revision
raw, err := json.Marshal(next)
if err != nil {
return false, fmt.Errorf("marshal compare-and-update phone code: %w", err)
}
value, err := s.c.Eval(
ctx,
compareAndUpdatePhoneCodeScript,
[]string{codeKey(hash)},
store.PhoneCodeVersionCurrent,
expectedRevision,
string(raw),
).Result()
if err != nil {
return false, fmt.Errorf("redis compare-and-update phone code: %w", err)
}
return redisCASApplied(value, "compare-and-update phone code")
}
func (s *CodeStore) CompareAndDelete(ctx context.Context, hash, expectedRevision string) (bool, error) {
if expectedRevision == "" {
return false, nil
}
value, err := s.c.Eval(
ctx,
compareAndDeletePhoneCodeScript,
[]string{codeKey(hash)},
store.PhoneCodeVersionCurrent,
expectedRevision,
).Result()
if err != nil {
return false, fmt.Errorf("redis compare-and-delete phone code: %w", err)
}
return redisCASApplied(value, "compare-and-delete phone code")
}
func redisCASApplied(value any, operation string) (bool, error) {
number, ok := value.(int64)
if !ok || (number != 0 && number != 1) {
return false, fmt.Errorf("redis %s: unexpected result %v (%T)", operation, value, value)
}
return number == 1, nil
}