mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
de4b4d7258
- [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
233 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|