Files
traefikoidc/internal/cache/backends/resp.go
T
lukaszraczylo de4b4d7258 fix(cache): remove sync.Pool for Yaegi compatibility (#121)
- [x] Remove sync.Pool implementation that causes reflection panics
- [x] Replace pool-based NewRESPWriter with direct instantiation
- [x] Replace pool-based NewRESPReader with direct instantiation
- [x] Convert Release() methods to no-ops for API compatibility
- [x] Add documentation explaining sync.Pool removal for Yaegi
- [x] Remove "sync" import

Resolves #120
2026-01-19 17:52:31 +00:00

233 lines
5.3 KiB
Go

package backends
import (
"bufio"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
// RESP (REdis Serialization Protocol) implementation
// Pure Go implementation compatible with Yaegi interpreter (no unsafe package)
//
// NOTE: sync.Pool was intentionally removed for Yaegi compatibility.
// Yaegi (Traefik's Go interpreter) has issues with sync.Pool and reflection
// that cause "reflect: call of reflect.Value.Field on zero Value" panics.
// See: https://github.com/lukaszraczylo/traefikoidc/issues/120
var (
ErrInvalidRESP = errors.New("invalid RESP response")
ErrNilResponse = errors.New("nil response")
)
// RESPWriter writes RESP protocol messages
type RESPWriter struct {
w io.Writer
}
// NewRESPWriter creates a new RESP writer
func NewRESPWriter(w io.Writer) *RESPWriter {
return &RESPWriter{w: w}
}
// Release is a no-op for API compatibility (pooling removed for Yaegi compatibility)
func (w *RESPWriter) Release() {
// No-op: pooling removed for Yaegi compatibility
}
// WriteCommand writes a Redis command in RESP array format
// Example: SET key value EX 3600 -> *5\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n$2\r\nEX\r\n$4\r\n3600\r\n
func (w *RESPWriter) WriteCommand(args ...string) error {
// Write array header
if _, err := fmt.Fprintf(w.w, "*%d\r\n", len(args)); err != nil {
return err
}
// Write each argument as bulk string
for _, arg := range args {
if _, err := fmt.Fprintf(w.w, "$%d\r\n%s\r\n", len(arg), arg); err != nil {
return err
}
}
return nil
}
// RESPReader reads RESP protocol messages
type RESPReader struct {
r *bufio.Reader
}
// NewRESPReader creates a new RESP reader
func NewRESPReader(r io.Reader) *RESPReader {
return &RESPReader{
r: bufio.NewReaderSize(r, 4096),
}
}
// Release is a no-op for API compatibility (pooling removed for Yaegi compatibility)
func (r *RESPReader) Release() {
// No-op: pooling removed for Yaegi compatibility
}
// ReadResponse reads a RESP response and returns the parsed value
func (r *RESPReader) ReadResponse() (interface{}, error) {
typeByte, err := r.r.ReadByte()
if err != nil {
return nil, err
}
switch typeByte {
case '+': // Simple string
return r.readSimpleString()
case '-': // Error
return nil, r.readError()
case ':': // Integer
return r.readInteger()
case '$': // Bulk string
return r.readBulkString()
case '*': // Array
return r.readArray()
default:
return nil, fmt.Errorf("%w: unknown type byte '%c'", ErrInvalidRESP, typeByte)
}
}
// readSimpleString reads a simple string (+OK\r\n)
func (r *RESPReader) readSimpleString() (string, error) {
line, err := r.readLine()
if err != nil {
return "", err
}
return line, nil
}
// readError reads an error message (-Error message\r\n)
func (r *RESPReader) readError() error {
line, err := r.readLine()
if err != nil {
return err
}
return errors.New(line)
}
// readInteger reads an integer (:1000\r\n)
func (r *RESPReader) readInteger() (int64, error) {
line, err := r.readLine()
if err != nil {
return 0, err
}
return strconv.ParseInt(line, 10, 64)
}
// readBulkString reads a bulk string ($6\r\nfoobar\r\n or $-1\r\n for nil)
func (r *RESPReader) readBulkString() (interface{}, error) {
line, err := r.readLine()
if err != nil {
return nil, err
}
length, err := strconv.Atoi(line)
if err != nil {
return nil, fmt.Errorf("%w: invalid bulk string length", ErrInvalidRESP)
}
// -1 indicates nil bulk string
if length == -1 {
return nil, ErrNilResponse
}
// Read exactly 'length' bytes plus \r\n
buf := make([]byte, length+2)
if _, err := io.ReadFull(r.r, buf); err != nil {
return nil, err
}
// Verify \r\n terminator
if buf[length] != '\r' || buf[length+1] != '\n' {
return nil, fmt.Errorf("%w: missing CRLF after bulk string", ErrInvalidRESP)
}
return string(buf[:length]), nil
}
// readArray reads an array (*2\r\n...\r\n or *-1\r\n for nil)
func (r *RESPReader) readArray() (interface{}, error) {
line, err := r.readLine()
if err != nil {
return nil, err
}
length, err := strconv.Atoi(line)
if err != nil {
return nil, fmt.Errorf("%w: invalid array length", ErrInvalidRESP)
}
// -1 indicates nil array
if length == -1 {
return nil, ErrNilResponse
}
// Read each element
result := make([]interface{}, length)
for i := 0; i < length; i++ {
elem, err := r.ReadResponse()
if err != nil {
return nil, err
}
result[i] = elem
}
return result, nil
}
// readLine reads a line terminated by \r\n
func (r *RESPReader) readLine() (string, error) {
line, err := r.r.ReadString('\n')
if err != nil {
return "", err
}
// Remove \r\n
line = strings.TrimSuffix(line, "\r\n")
if !strings.HasSuffix(line+"\r\n", "\r\n") {
return "", fmt.Errorf("%w: missing CRLF", ErrInvalidRESP)
}
return line, nil
}
// RESPString extracts a string from RESP response
func RESPString(resp interface{}) (string, error) {
if resp == nil {
return "", ErrNilResponse
}
switch v := resp.(type) {
case string:
return v, nil
case []byte:
return string(v), nil
default:
return "", fmt.Errorf("expected string, got %T", resp)
}
}
// RESPInt extracts an integer from RESP response
func RESPInt(resp interface{}) (int64, error) {
if resp == nil {
return 0, ErrNilResponse
}
switch v := resp.(type) {
case int64:
return v, nil
case int:
return int64(v), nil
default:
return 0, fmt.Errorf("expected integer, got %T", resp)
}
}