147 lines
4.1 KiB
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
|
|
}
|