Skip to content

Add support for compression on the client #707

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

Merged
merged 17 commits into from
Jan 29, 2020
Merged

Conversation

glbrntt
Copy link
Collaborator

@glbrntt glbrntt commented Jan 24, 2020

Motivation:

Compression support is an optional part of the gRPC specification but
ideally should be supported. Users have also requested this feature
(#611).

Since introducing compression without breaking API is difficult we
should do this before we tag 1.0.0.

Modifications:

  • Add support for gzip and deflate compression on the client.
  • Introduce new configuration for supporting compression.
  • Add interop tests for the client.
  • Compression is enabled at the connection level (compression used for requests and compression methods to advertise to the server which it may use for responses)
  • Request compression may be disabled at the RPC level or at the message level for streaming requests.

Result:

Client supports compression.

Motivation:

Compression support is an optional part of the gRPC specification but
ideally should be supported. Users have also requested this feature
(grpc#611).

Since introducing compression without breaking API is difficult we
should do this before we tag 1.0.0.

Modifications:

- Add support for gzip and deflate compression on the client.
- Introduce new configuration for supporting compression.
- Add interop tests for the client.

Result:

Client supports compression.
@glbrntt glbrntt requested a review from MrMage January 24, 2020 16:01
Copy link
Collaborator

@MrMage MrMage left a comment

Choose a reason for hiding this comment

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

Looks good so far. A couple notes:

  • I have not reviewed Zlib.swift, especially not wrt memory safety and leakage
  • It looks like the interop tests don't decompress anything; I assume that's expected because the other endpoint would be responsible for that?
  • You could also add tests for Zlib, but I guess that is integration-tested by CompressorTests.

@@ -52,6 +52,7 @@ public final class ServerStreamingCall<RequestMessage: Message, ResponseMessage:
path: path,
host: connection.configuration.target.host,
requestID: requestID,
encoding: connection.configuration.compression,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we unify the naming of encoding vs compression?

/// `sendMessage(_:disableCompression)`, `sendMessage(_:disableCompression:promise)`,
/// `sendMessages(_:disableCompression)` or `sendMessages(_:disableCompression:promise)`.
///
/// Note that any disabling compression has no effect if compression is disabled on the
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: "any disabling compression" sounds weird

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops, yes, I don't think the any is meant to be there.

@@ -46,18 +46,24 @@ public struct CallOptions {
/// messages associated with the call.
public var requestIDHeader: String?

/// Disables request compression on this RPC. Ignored if compression is disabled at the
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: "on this call"?

public internal(set) var outbound: CompressionAlgorithm?

/// The set of compression algorithms advertised to the remote peer that they may use.
public internal(set) var inbound: [CompressionAlgorithm]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not super happy with these names, neither "requests/responses" nor "outbound/inbound". How about "accepted" and "outbound"? Or something more analogous to the "accept-encoding" headers? Not 100% sure myself. Thinking about this more, maybe the names are actually good.

Also, does gRPC support using different compressions in both directions on one stream? Or is this more like the "preferred" outbound method?

Overall, I'm a bit confused by this so far 😅

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agh...naming here is tricky. To the user requests/responses seems more natural for the init than inbound/outbound, but then responses doesn't quite fit the "accept-encoding" part. Also compression.requests/compression.responses isn't as clear as compression.outbound, compression.inbound.

The init could be more explicit:

init(forRequests outbound: CompressionAlgorithm?, acceptableForResponses inbound: [CompressionAlgorithm]?) {
  // ...
}

...

MessageEncoding(forRequests: .gzip, acceptableForResponses: [.gzip, .identity])

I'm still not certain how this API should be spelt! Thoughts?

Also, does gRPC support using different compressions in both directions on one stream? Or is this more like the "preferred" outbound method?

Compression for the request and response stream are independent and may be different.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One other thing I've been considering recently which might make things easier for us and users is some sort of client bootstrap: we can be more descriptive with names and add to it without breaking API (with structs we need a new init if we add a property).

let bootstrap = GRPCConnectionBootstrap(group: group)  // hmm sounds familiar...
  .requestCompression(.gzip)
  .acceptableResponseCompression(.gzip, .deflate, .identity)

Anyway, that's a story for another time!

Copy link
Collaborator

Choose a reason for hiding this comment

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

MessageEncoding(forRequests: .gzip, acceptableForResponses: [.gzip, .identity]) SGTM. I also like the Bootstrap idea.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll leave the bootstrap for another day since that's a fairly large change.

public static let none = MessageEncoding(requests: nil, responses: [.identity])

/// Accept all supported compression on responses, do not compress requests.
public static let onlyResponses = MessageEncoding(requests: nil, responses: CompressionAlgorithm.all)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: responsesOnly (up to you)

}

extension ClientConnection.Configuration.MessageEncoding {
var acceptEncoding: String {
Copy link
Collaborator

Choose a reason for hiding this comment

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

acceptEncodingHeader?

Copy link
Collaborator Author

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

Thanks for the review @MrMage, I'll fix your nits etc when I push up another revision.

Just wanted to check you saw Sources/GRPC/Compression/Zlib.swift? (I noticed there were no comments on it and GitHub hid the diff for me.)

/// `sendMessage(_:disableCompression)`, `sendMessage(_:disableCompression:promise)`,
/// `sendMessages(_:disableCompression)` or `sendMessages(_:disableCompression:promise)`.
///
/// Note that any disabling compression has no effect if compression is disabled on the
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops, yes, I don't think the any is meant to be there.

public internal(set) var outbound: CompressionAlgorithm?

/// The set of compression algorithms advertised to the remote peer that they may use.
public internal(set) var inbound: [CompressionAlgorithm]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agh...naming here is tricky. To the user requests/responses seems more natural for the init than inbound/outbound, but then responses doesn't quite fit the "accept-encoding" part. Also compression.requests/compression.responses isn't as clear as compression.outbound, compression.inbound.

The init could be more explicit:

init(forRequests outbound: CompressionAlgorithm?, acceptableForResponses inbound: [CompressionAlgorithm]?) {
  // ...
}

...

MessageEncoding(forRequests: .gzip, acceptableForResponses: [.gzip, .identity])

I'm still not certain how this API should be spelt! Thoughts?

Also, does gRPC support using different compressions in both directions on one stream? Or is this more like the "preferred" outbound method?

Compression for the request and response stream are independent and may be different.

public internal(set) var outbound: CompressionAlgorithm?

/// The set of compression algorithms advertised to the remote peer that they may use.
public internal(set) var inbound: [CompressionAlgorithm]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One other thing I've been considering recently which might make things easier for us and users is some sort of client bootstrap: we can be more descriptive with names and add to it without breaking API (with structs we need a new init if we add a property).

let bootstrap = GRPCConnectionBootstrap(group: group)  // hmm sounds familiar...
  .requestCompression(.gzip)
  .acceptableResponseCompression(.gzip, .deflate, .identity)

Anyway, that's a story for another time!

Copy link
Collaborator

@MrMage MrMage left a comment

Choose a reason for hiding this comment

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

Looks good overall, have given Zlib.swift a cursory look overall now. (No time to study the Zlib docs.)

self.end()
}

/// Decompresses the data in `input` into the `output` buffer.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wait, shouldn't this method be about compressing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, thanks!

@glbrntt
Copy link
Collaborator Author

glbrntt commented Jan 28, 2020

Missed this before:

  • It looks like the interop tests don't decompress anything; I assume that's expected because the other endpoint would be responsible for that?

They do decompress in the ServerCompressed{Unary,Streaming} tests (client sends uncompressed data, server responds with compressed data).

  • You could also add tests for Zlib, but I guess that is integration-tested by CompressorTests.

That's right. I didn't think there was much value in adding them.

Copy link
Collaborator

@MrMage MrMage left a comment

Choose a reason for hiding this comment

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

LGTM

@glbrntt
Copy link
Collaborator Author

glbrntt commented Jan 29, 2020

@MrMage just pushed up a small patch, the compression documentation isn't clear in some places but working on the server part of this made me realise that something wasn't quite right. Here's the change: ffe5bd4

@MrMage
Copy link
Collaborator

MrMage commented Jan 29, 2020 via email

@glbrntt glbrntt merged commit 151b627 into grpc:nio Jan 29, 2020
@glbrntt glbrntt deleted the gb-compression branch January 29, 2020 12:54
@glbrntt glbrntt added the ⚠️ semver/major Breaks existing public API. label Feb 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⚠️ semver/major Breaks existing public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants