Skip to content

Unenforced discard limits #620

Open
@emcfarlane

Description

An internal discard limit is applied to limit discarding bytes from a connection on close. It's currently at 4MiB:

discardLimit = 1024 * 1024 * 4 // 4MiB

However when connect receives a large message payload it discards the entire message to read the max size and report an error :

  • connect-go/envelope.go

    Lines 253 to 257 in 734ea94

    _, err := io.CopyN(io.Discard, r.reader, size)
    if err != nil && !errors.Is(err, io.EOF) {
    return errorf(CodeUnknown, "read enveloped message: %w", err)
    }
    return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", size, r.readMaxBytes)
  • connect-go/protocol_connect.go

    Lines 1087 to 1091 in 734ea94

    discardedBytes, err := io.Copy(io.Discard, u.reader)
    if err != nil {
    return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", u.readMaxBytes, err)
    }
    return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", bytesRead+discardedBytes, u.readMaxBytes)

This avoids the discard limit and will read up to the max message size (max uint32, and potentially more in connect unary?). This also can read much more than the connect.WithReadMaxBytes limited, although it won't keep the total payload in memory.

To Reproduce

To showcase I wrote a quick test to time a large payload with a server that has a 2MiB limit. Each iteration takes around 4.5s locally. Altering the bounds to error and not discard reduce this to less than 1s (presumably the time to marshal and start sending the large payload).

func TestShowDiscard(t *testing.T) {
	mux := http.NewServeMux()
	mux.Handle(
		pingv1connect.NewPingServiceHandler(
			&ExamplePingServer{},
			connect.WithReadMaxBytes(2*1024*1024), // 2 MiB
		),
	)
	server := httptest.NewServer(mux)
	t.Cleanup(server.Close)

	httpClient := server.Client()
	httpTransport, ok := httpClient.Transport.(*http.Transport)
	assert.True(t, ok)
	httpTransport.DisableCompression = true

	clients := []struct {
		name string
		opts []connect.ClientOption
	}{{
		name: "connect",
		opts: []connect.ClientOption{},
	}, {
		name: "grpc",
		opts: []connect.ClientOption{
			connect.WithGRPC(),
		},
	}, {
		name: "grpcweb",
		opts: []connect.ClientOption{
			connect.WithGRPCWeb(),
		},
	}}

	bigPayload := strings.Repeat("a", math.MaxUint32-10)
	msg := &pingv1.PingRequest{Text: bigPayload}
	size := proto.Size(msg)
	t.Log("size", size)
	if size > math.MaxUint32 {
		t.Fatal("message too big")
	}
	for _, client := range clients {
		now := time.Now()
		t.Log("start", client.name, now)
		client := pingv1connect.NewPingServiceClient(
			httpClient,
			server.URL,
			connect.WithClientOptions(client.opts...),
		)
		if _, err := client.Ping(
			context.Background(), connect.NewRequest(&pingv1.PingRequest{Text: bigPayload}),
		); err != nil {
			t.Log(err) // resource_exhausted: message size 4294967291 is larger than configured max 2097152
		}
		t.Log("duration", time.Since(now))
	}
}

Open questions

  1. Should the discard limits be applied to message discarding?
  2. Does the number of bytes read affect the discard limit?

Additional Info
Opened #606 to explore a solution, but needs clarification on how discard limits should be calculated.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions