Skip to content

Conversation

@emlimap
Copy link
Contributor

@emlimap emlimap commented Nov 10, 2025

Store Redis cache entries in compact binary wire format to reduce payload size and CPU usage. Uses dns.Msg.PackBuffer with adaptive sync.Pool to minimize allocations.

Changes

  • Binary format: 10-byte header (version, flags, timestamp) + DNS wire bytes
  • Adaptive buffer pooling (2 KiB → 32 KiB cap) prevents repeated allocations
  • Serialization moved outside I/O deadline for accurate network timeouts
  • Backward compatible: auto-detects and decodes legacy JSON entries

Benefits

  • Smaller Redis storage footprint
  • Lower CPU on cache hits/misses
  • Reduced allocations via pooled buffers
  • Safe mixed deployments (old JSON + new binary)

Copy link
Owner

@folbricht folbricht left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few inconsistencies and some unnecessary complexity in this. Since you mentioned you're not comfortable writing Go, would you prefer I make a PR against yours, or just take this code and fix it up?

cache-redis.go Outdated
Comment on lines 172 to 183
if len(valueBytes) >= 1 && valueBytes[0] == '{' {
if jsonErr := json.Unmarshal(valueBytes, &a); jsonErr != nil {
Log.Error("failed to decode cache record from redis (binary and JSON)", "binary_error", err, "json_error", jsonErr)
return nil, false, false
}
} else {
// Try JSON anyway
if jsonErr := json.Unmarshal(valueBytes, &a); jsonErr != nil {
Log.Error("failed to decode cache record from redis", "error", err)
return nil, false, false
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional here looks unnecessary, it'll unmarshal with JSON regardless, if it starts with { or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed it. After removing, a lot of these error messages do appear in the log. Even after I have flushed the Redis DB.

level=ERROR msg="failed to decode cache record from redis" binary_error="failed to unpack DNS message: bad question name: dns: bad rdata" json_error="invalid character '\\x01' looking for beginning of value"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emlimap Do you still see this issue in the latest version? The error suggests the binary decode is failing (the json failure is just it trying to decode the binary format so that's expected to fail, shouldn't even try that)

cache-redis.go Outdated
var _ CacheBackend = (*redisBackend)(nil)

// Buffer pool for dns.Msg.PackBuffer to minimize allocations.
// Pool stores []byte directly (not pointers) and returns the grown buffer
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a pointer is actually slightly more efficient, though that isn't really that important.

cache-redis.go Outdated
binaryFormatVersion = 1
headerSize = 10
flagPrefetchBit = 1 << 0
maxPooledBufCap = 32 << 10 // 32 KiB - cap pooled buffers to prevent unbounded growth
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure the limiter is necessary

cache-redis.go Outdated
packed, err := item.Msg.PackBuffer(buf)
if err != nil {
// Return buffer to pool even on error
packBufPool.Put(buf[:0])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using defer is more common, no need to cover every code-path that returns

cache-redis.go Outdated
}

// Allocate result buffer: header (10 bytes) + packed message
result := make([]byte, headerSize+len(packed))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically all of the advantages of using the pool are wasted here since this allocates a new buffer, then later copies the whole message into it.

@emlimap
Copy link
Contributor Author

emlimap commented Nov 23, 2025

@folbricht I have got AI to make changes based on your comment. Please feel free to make your own PR or take this code and fix it.

@emlimap
Copy link
Contributor Author

emlimap commented Nov 27, 2025

@folbricht The failed to decode messages was because the format being written to Redis was corrupted. This is now fixed. Got Claude code to take your PR comments into account when making the changes.

It also added tests for encoding and decoding. Not sure if it is useful for you. If not, feel free to remove it.

@folbricht
Copy link
Owner

Thank you. The handling of the buffer pool can definitely be improved, so this could be made a little more efficient. I should be able to take care of that in January.

@folbricht folbricht merged commit ff279d5 into folbricht:master Nov 28, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants