Skip to content

Commit

Permalink
refactor(pyroscope): switch to in-memory profile handling for fanout …
Browse files Browse the repository at this point in the history
…AppendIngest
  • Loading branch information
marcsanmi committed Jan 31, 2025
1 parent aea9afe commit 43e2818
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 48 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Main (unreleased)

- Add support for pushv1.PusherService Connect API in `pyroscope.receive_http`. (@simonswine)

- Change profile handling in `pyroscope.receive_http` and `pyroscope.write` components to use in-memory processing instead of pipes. (@marcsanmi)

v1.6.1
-----------------

Expand Down
39 changes: 13 additions & 26 deletions internal/component/pyroscope/receive_http/receive_http.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package receive_http

import (
"bytes"
"context"
"errors"
"fmt"
Expand Down Expand Up @@ -239,47 +240,33 @@ func (c *Component) handleIngest(w http.ResponseWriter, r *http.Request) {
}
}

// Create a pipe for each appendable
pipeWriters := make([]io.Writer, len(appendables))
pipeReaders := make([]io.Reader, len(appendables))
for i := range appendables {
pr, pw := io.Pipe()
pipeReaders[i] = pr
pipeWriters[i] = pw
// Read the entire body into memory
// This matches how Append() handles profile data (as RawProfile),
// but means the entire profile will be held in memory
var buf bytes.Buffer
if _, err := io.Copy(&buf, r.Body); err != nil {
level.Error(c.opts.Logger).Log("msg", "Failed to read request body", "err", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
mw := io.MultiWriter(pipeWriters...)

// Create an errgroup with the timeout context
g, ctx := errgroup.WithContext(r.Context())

// Start copying the request body to all pipes
g.Go(func() error {
defer func() {
for _, pw := range pipeWriters {
pw.(io.WriteCloser).Close()
}
}()
_, err := io.Copy(mw, r.Body)
return err
})

// Process each appendable
// Process each appendable with a new reader from the buffer
for i, appendable := range appendables {
g.Go(func() error {
defer pipeReaders[i].(io.ReadCloser).Close()

profile := &pyroscope.IncomingProfile{
Body: io.NopCloser(pipeReaders[i]),
Body: io.NopCloser(bytes.NewReader(buf.Bytes())),
Headers: r.Header.Clone(),
URL: r.URL,
Labels: lbls,
}

err := appendable.Appender().AppendIngest(ctx, profile)
if err != nil {
if err := appendable.Appender().AppendIngest(ctx, profile); err != nil {
level.Error(c.opts.Logger).Log("msg", "Failed to append profile", "appendable", i, "err", err)
return err
}

level.Debug(c.opts.Logger).Log("msg", "Profile appended successfully", "appendable", i)
return nil
})
Expand Down
32 changes: 10 additions & 22 deletions internal/component/pyroscope/write/write.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package write

import (
"bytes"
"context"
"errors"
"fmt"
Expand Down Expand Up @@ -358,33 +359,20 @@ func (e *PyroscopeWriteError) Error() string {

// AppendIngest implements the pyroscope.Appender interface.
func (f *fanOutClient) AppendIngest(ctx context.Context, profile *pyroscope.IncomingProfile) error {
pipeWriters := make([]io.Writer, len(f.config.Endpoints))
pipeReaders := make([]io.Reader, len(f.config.Endpoints))
for i := range f.config.Endpoints {
pr, pw := io.Pipe()
pipeReaders[i] = pr
pipeWriters[i] = pw
// Read the entire body into memory
// This matches how Append() handles profile data (as RawProfile),
// but means the entire profile will be held in memory
var buf bytes.Buffer
if _, err := io.Copy(&buf, profile.Body); err != nil {
return fmt.Errorf("reading profile body: %w", err)
}
mw := io.MultiWriter(pipeWriters...)
bodyBytes := buf.Bytes()

g, ctx := errgroup.WithContext(ctx)

// Start copying the profile body to all pipes
g.Go(func() error {
defer func() {
for _, pw := range pipeWriters {
pw.(io.WriteCloser).Close()
}
}()
_, err := io.Copy(mw, profile.Body)
return err
})

// Send to each endpoint concurrently
for i, endpoint := range f.config.Endpoints {
for _, endpoint := range f.config.Endpoints {
g.Go(func() error {
defer pipeReaders[i].(io.ReadCloser).Close()

u, err := url.Parse(endpoint.URL)
if err != nil {
return fmt.Errorf("parse endpoint URL: %w", err)
Expand All @@ -409,7 +397,7 @@ func (f *fanOutClient) AppendIngest(ctx context.Context, profile *pyroscope.Inco
}
u.RawQuery = query.Encode()

req, err := http.NewRequestWithContext(ctx, "POST", u.String(), pipeReaders[i])
req, err := http.NewRequestWithContext(ctx, "POST", u.String(), bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("create request: %w", err)
}
Expand Down

0 comments on commit 43e2818

Please sign in to comment.