package rooms import ( "context" "encoding/json" "errors" "fmt" "math/rand" "log" "github.com/redis/go-redis/v9" ) type PersonId int func (p PersonId) MarshalBinary() ([]byte, error) { bytes, err := json.Marshal(p) return bytes, err } func (p *PersonId) UnmarshalBinary(data []byte) error { err := json.Unmarshal(data, p) return err } // TODO move to rooms i guess func RandomPersonId() PersonId { randInt := rand.Int() if randInt == 0 { randInt = 1 } return PersonId(randInt) } type Person struct { Id PersonId Name string PasswordHash string } // well, it seems that i'd better do marshalling into bytes then // see https://github.com/redis/go-redis/issues/2512 func (r *Room) MarshalBinary() (data []byte, err error) { return json.Marshal(r) } func (r *Room) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, r) } // let's check whether it will be possible to save nested structs type RoomManager interface { Get(roomName string) (Room, bool, error) Save(room Room) error Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error } const roomRedisPrefix = "room" func roomNameToRedisId(roomName string) string { return fmt.Sprintf("%s:%s", roomRedisPrefix, roomName) } type RedisRM struct { Rdb *redis.Client } func (redisRM RedisRM) Get(roomName string) (Room, bool, error) { var readRoom Room err := redisRM.Rdb.Get(context.TODO(), roomNameToRedisId(roomName)).Scan(&readRoom) if err == redis.Nil { return Room{}, false, nil } if err != nil { log.Printf("error reading room with id %s : %s", roomName, err) return Room{}, false, err } return readRoom, true, nil } var maxRetries int = 20 func (redisRM RedisRM) Update(ctx context.Context, roomName string, f func(fromRoom Room) (toRoom Room)) error { // transactional function roomKey := roomNameToRedisId(roomName) txf := func (tx *redis.Tx) error { var savedRoom Room err := tx.Get(ctx, roomKey).Scan(&savedRoom) if err != nil { return err } savedRoom.InitMaps() room := f(savedRoom) _, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error { pipe.Set(ctx, roomKey, &room, 0) return nil }) return err } for i := 0; i < maxRetries; i++ { err := redisRM.Rdb.Watch(ctx, txf, roomKey) if err == nil { return nil // success } if err == redis.TxFailedErr { // optimistic lock will keep spinning continue } // non tx errror are returned, including redis.Nil return err } return errors.New("update reached maximum amount of retries") } func (redisRM RedisRM) Save(room Room) error { err := redisRM.Rdb.Set(context.TODO(), roomNameToRedisId(room.Name), &room, 0).Err() // maybe even set expiration? return err }