Skip to content

[confighttp] snappy compress, Encode and NewReader inconsistent behavior #10584

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tomatopunk opened this issue Jul 10, 2024 · 14 comments · Fixed by #12911 · May be fixed by #12825
Closed

[confighttp] snappy compress, Encode and NewReader inconsistent behavior #10584

tomatopunk opened this issue Jul 10, 2024 · 14 comments · Fixed by #12911 · May be fixed by #12825
Assignees
Labels
bug Something isn't working

Comments

@tomatopunk
Copy link

tomatopunk commented Jul 10, 2024

Hello team, I hope you are having a good day.

Describe the bug

I am coding on the collect-contrib, about on a new component to receive prometheus_remote_write.

I have encountered a potential bug related to inconsistent behavior in Snappy compression when using encode -> NewReader and NewBufferedWriter -> decode.

When we compress or decompress from a stream, or read all of it and then compress or decompress it, we get different results

Steps to reproduce

What did you expect to see?


package main

import (
	"bytes"
	"fmt"
	"github.com/golang/snappy"
	"io"
)

func main() {
	test1()
	test2()
}

func test1() {
	data := []byte("Hello, World!")
	//encode
	compressed := snappy.Encode(nil, data)

	//decode
	reader := bytes.NewReader(compressed)
	sr := snappy.NewReader(reader)
	sb := new(bytes.Buffer)
	_, err := io.Copy(sb, sr)
	if err != nil {
		println("test1 decode error:", err.Error())
		return
	}

	fmt.Printf("test1 string1: %s, 2: %s", string(data), string(sb.Bytes()))
}

func test2() {
	data := []byte("Hello, World!")

	//encode
	var buf bytes.Buffer
	sw := snappy.NewBufferedWriter(&buf)
	_, err := sw.Write(data)
	if err != nil {
		println("encode error")
		return
	}

	//decode
	byteSlice := buf.Bytes()
	reqBuf, err := snappy.Decode(nil, byteSlice)
	if err != nil {
		println("test2 decode error:", err.Error())
		return
	}

	fmt.Printf("test1 string1: %s, 2: %s", string(data), string(reqBuf))

}

What did you see instead?

maybe should be correctly code/decode by snappy

What version did you use?

main branch

What config did you use?

code:

"snappy": func(body io.ReadCloser) (io.ReadCloser, error) {

Environment

go 1.22

Additional context

In the case of Prometheus and OpenTelemetry collection, Prometheus uses encode and decode, while OpenTelemetry uses NewReader and NewBufferedWriter. However, I believe that whether you decode/encode in stream or read all and then encode/decode, the same protocol should yield the same results, and there should be no parsing failures.

code: https://github.com/prometheus/prometheus/blob/c173cd57c921f582586fc725ad51124728757533/cmd/promtool/metrics.go#L104

I would like to know how to address this bug. Should we consider skipping Snappy? If any fixes are required, I am willing to help resolve this issue.

@tomatopunk tomatopunk added the bug Something isn't working label Jul 10, 2024
@tomatopunk tomatopunk changed the title snappy compress, Encode and NewReader inconsistent behavior [confighttp] snappy compress, Encode and NewReader inconsistent behavior Jul 12, 2024
@jpkrohling jpkrohling self-assigned this Jul 12, 2024
@BinaryFissionGames
Copy link

BinaryFissionGames commented Jul 12, 2024

This mismatch between the reader/writer and encode/decode is called out in the library: https://pkg.go.dev/github.com/golang/snappy#pkg-overview - it explains there is a "stream" format and a "block" format.

Digging a little deeper, it looks like the stream/framed format should be specified as x-snappy-framed, as described here: https://github.com/google/snappy/blob/main/framing_format.txt - I'd expect snappy then to be exclusively for the block/non-framed format, like Prometheus remote write uses.

@jpkrohling
Copy link
Member

Thank you for digging into this, @BinaryFissionGames! I just double-checked, and we are using readers/writers consistently in the Collector code, I believe an OTel Collector as client can compress its data using snappy and a Collector server can read it. I'm closing this issue, but if I'm missing something, let me know and I'll reopen.

reader:

"snappy": func(body io.ReadCloser) (io.ReadCloser, error) {
sr := snappy.NewReader(body)
sb := new(bytes.Buffer)
_, err := io.Copy(sb, sr)
if err != nil {
return nil, err
}
if err = body.Close(); err != nil {
return nil, err
}
return io.NopCloser(sb), nil
},

writer:

snappyPool = &compressor{pool: sync.Pool{New: func() any { return snappy.NewBufferedWriter(nil) }}}
_ writeCloserReset = (*zstd.Encoder)(nil)

@tomatopunk
Copy link
Author

tomatopunk commented Jul 15, 2024

I believe this issue should be reopened.

Actually, we are discussing the need for consistency between streaming and framed encryption/decryption.

For example, stream encryption requires stream decryption, and framed encryption requires framed decryption. However, we are unsure about the correct methods to use for encryption and decryption.

origin:

Package snappy implements the Snappy compression format. It aims for very high speeds and reasonable compression.

There are actually two Snappy formats: block and stream. They are related, but different: trying to decompress block-compressed data as a Snappy stream will fail, and vice versa. The block format is the Decode and Encode functions and the stream format is the Reader and Writer types.

The block format, the more common case, is used when the complete size (the number of bytes) of the original data is known upfront, at the time compression starts. The stream format, also known as the framing format, is for when that isn't always true.

The canonical, C++ implementation is at https://github.com/google/snappy and it only implements the block format.


@tomatopunk
Copy link
Author

In the post, I mentioned to use of reader/write and encode/decode.
Specifically, if we use newBufferWrited to encry and use decode to decry, we will get an error snappy is incorrent

@tomatopunk
Copy link
Author

As I mentioned in this post, a classic example is the otlp-collector and prometheus remote write.
Prometheus remote write use snappy.Encode for compress, while the otlp-collector use snappy.NewReader. As a result, I encountered a snappy error, despite both using snappy.

@tomatopunk
Copy link
Author

so, I suggest either removing snappy decompress in the otlp-collector or replacing the current model with block format.

@jpkrohling
Copy link
Member

Looking at the documentation for that package, we are using what should be called "x-snappy-framed".

I'm not sure it's that easy for us to change from framed to block right now, as it would mean that clients and servers are not compatible anymore. I'd rather do a phased approach, where, once every couple of versions we take one of the following steps:

  1. add "x-snappy-framed" working alongside "snappy" (v0.107.0)
  2. deprecate "snappy" (v0.109.0)
  3. remove "snappy" (framed) (v0.111.0)
  4. add "snappy" (blocked) (v0.113.0)

I believe the risk in breaking clients/servers this way is reduced, and we can then have another snappy support for block format, required by Prometheus remote write. Is it possible to implement framed format in Prometheus remote write? If so, we'd have it working on the next version already, due in a couple of weeks.

@jpkrohling jpkrohling reopened this Jul 15, 2024
@tomatopunk
Copy link
Author

Perhaps we could allow the receive component to choose whether to automatically decompress.

@skandragon
Copy link
Contributor

This is a blocker for prometheus remote write, which I need to implement ASAP.

@skandragon
Copy link
Contributor

This is still a blocker for the in-contrib prometheus-remote-write-receiver. It cannot function today.

@tomatopunk
Copy link
Author

This is still a blocker for the in-contrib prometheus-remote-write-receiver. It cannot function today.

Hello @skandragon,

Yes, this is always a question. Perhaps you could send some emails to the code owners, like @codeboten, to get their opinions. The biggest challenge in solving this issue is finding a willing sponsor to support it.

If I were to implement it, I might choose to extend ServerConfig and modify the newBodyReader method. This could involve either forcibly disabling the Snappy decompression logic or providing a configuration option to specify the decompression type.

func (d *decompressor) newBodyReader(r *http.Request) (io.ReadCloser, error) {
encoding := r.Header.Get(headerContentEncoding)
decoder, ok := d.decoders[encoding]
if !ok {
return nil, fmt.Errorf("unsupported %s: %s", headerContentEncoding, encoding)
}
return decoder(r.Body)
}

func (hss *ServerConfig) ToServer(_ context.Context, host component.Host, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) {

@skandragon
Copy link
Contributor

@tomatopunk I went a different route by adding format detection logic to the confhttp package. Hopefully this will be able to make it in eventually, but until then, I have a branch up and working.

I do not think it is correct to keep using "snappy" content-type incorrectly, and this feels like it will unblock that concern, and ease migration to the correct wire format.

@tomatopunk
Copy link
Author

@tomatopunk I went a different route by adding format detection logic to the confhttp package. Hopefully this will be able to make it in eventually, but until then, I have a branch up and working.

I do not think it is correct to keep using "snappy" content-type incorrectly, and this feels like it will unblock that concern, and ease migration to the correct wire format.

@skandragon , you are right. We should distinguish between the two types of Snappy instead of using the confusing snappy as the Content-Encoding field.

However, both Prometheus 1.0 and 2.0 protocols require Content-Encoding: snappy. Changing the protocol would take longer than adjusting the code for compatibility.

If you want to quickly resolve this issue, you should consider submitting a PR to modify the Prometheus remote write protocol.

https://prometheus.io/docs/specs/prw/remote_write_spec/#definitions

@skandragon
Copy link
Contributor

skandragon commented Apr 11, 2025

@tomatopunk I think the issue is, content-encoding: snappy means, to everyone not the Otel collector, "block snappy". That is the real issue. Prometheus is not going to change, and I do not blame them as they are correct in how they have used that content header and the format of the data they sent.

In any case, the PR I raised seems to have gotten some attention. Let's see where that lands :)

github-merge-queue bot pushed a commit that referenced this issue May 5, 2025
#### Description
This is an alternative PR to #12825.

I'm taking all the commits from that PR and adding the feature gate on
the client side, as requested by reviews. The server behavior of peeking
into the initial bytes to identify the encoding is kept :)

If you've reviewed the previous PR, you can just review the latest
commit!

<!-- Issue number if applicable -->
#### Link to tracking issue
Fixes #10584

---------

Signed-off-by: Arthur Silva Sens <[email protected]>
Co-authored-by: Michael Graff <[email protected]>
Co-authored-by: Alex Boten <[email protected]>
Co-authored-by: Pablo Baeyens <[email protected]>
Co-authored-by: Jonathan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
4 participants