Skip to content

Commit fe58061

Browse files
authored
Merge pull request #1952 from b0b3rt/grpc-js_compression_support
grpc-js: Allow per-channel request compression from the client and decompression from the server
2 parents ff387c9 + 69428b0 commit fe58061

File tree

13 files changed

+580
-40
lines changed

13 files changed

+580
-40
lines changed

packages/grpc-js/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@
5252
"test": "gulp test",
5353
"check": "gts check src/**/*.ts",
5454
"fix": "gts fix src/*.ts",
55-
"pretest": "npm run generate-types && npm run compile",
55+
"pretest": "npm run generate-types && npm run generate-test-types && npm run compile",
5656
"posttest": "npm run check && madge -c ./build/src",
57-
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ -O src/generated/ --grpcLib ../index channelz.proto"
57+
"generate-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --includeDirs proto/ --include-dirs test/fixtures/ -O src/generated/ --grpcLib ../index channelz.proto",
58+
"generate-test-types": "proto-loader-gen-types --keepCase --longs String --enums String --defaults --oneofs --includeComments --include-dirs test/fixtures/ -O test/generated/ --grpcLib ../../src/index test_service.proto"
5859
},
5960
"dependencies": {
6061
"@grpc/proto-loader": "^0.6.4",
61-
"@types/node": ">=12.12.47"
62+
"@types/node": ">=12.12.47",
63+
"@types/semver": "^7.3.9"
6264
},
6365
"files": [
6466
"src/**/*.ts",

packages/grpc-js/src/channel-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*
1616
*/
1717

18+
import { CompressionAlgorithms } from './compression-algorithms';
19+
1820
/**
1921
* An interface that contains options used when initializing a Channel instance.
2022
*/
@@ -36,6 +38,7 @@ export interface ChannelOptions {
3638
'grpc.enable_http_proxy'?: number;
3739
'grpc.http_connect_target'?: string;
3840
'grpc.http_connect_creds'?: string;
41+
'grpc.default_compression_algorithm'?: CompressionAlgorithms;
3942
'grpc.enable_channelz'?: number;
4043
'grpc-node.max_session_memory'?: number;
4144
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/grpc-js/src/channel.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ import { MaxMessageSizeFilterFactory } from './max-message-size-filter';
4545
import { mapProxyName } from './http_proxy';
4646
import { GrpcUri, parseUri, uriToString } from './uri-parser';
4747
import { ServerSurfaceCall } from './server-call';
48-
import { SurfaceCall } from './call';
4948
import { Filter } from './filter';
5049

5150
import { ConnectivityState } from './connectivity-state';
@@ -333,7 +332,7 @@ export class ChannelImplementation implements Channel {
333332
new CallCredentialsFilterFactory(this),
334333
new DeadlineFilterFactory(this),
335334
new MaxMessageSizeFilterFactory(this.options),
336-
new CompressionFilterFactory(this),
335+
new CompressionFilterFactory(this, this.options),
337336
]);
338337
this.trace('Channel constructed with options ' + JSON.stringify(options, undefined, 2));
339338
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2021 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
export enum CompressionAlgorithms {
19+
identity = 0,
20+
deflate = 1,
21+
gzip = 2
22+
};

packages/grpc-js/src/compression-filter.ts

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,25 @@
1717

1818
import * as zlib from 'zlib';
1919

20-
import { Call, WriteFlags, WriteObject } from './call-stream';
20+
import { Call, WriteObject, WriteFlags } from './call-stream';
2121
import { Channel } from './channel';
22+
import { ChannelOptions } from './channel-options';
23+
import { CompressionAlgorithms } from './compression-algorithms';
24+
import { LogVerbosity } from './constants';
2225
import { BaseFilter, Filter, FilterFactory } from './filter';
26+
import * as logging from './logging';
2327
import { Metadata, MetadataValue } from './metadata';
2428

29+
const isCompressionAlgorithmKey = (key: number): key is CompressionAlgorithms => {
30+
return typeof key === 'number' && typeof CompressionAlgorithms[key] === 'string';
31+
}
32+
33+
type CompressionAlgorithm = keyof typeof CompressionAlgorithms;
34+
35+
type SharedCompressionFilterConfig = {
36+
serverSupportedEncodingHeader?: string;
37+
};
38+
2539
abstract class CompressionHandler {
2640
protected abstract compressMessage(message: Buffer): Promise<Buffer>;
2741
protected abstract decompressMessage(data: Buffer): Promise<Buffer>;
@@ -167,10 +181,45 @@ function getCompressionHandler(compressionName: string): CompressionHandler {
167181
export class CompressionFilter extends BaseFilter implements Filter {
168182
private sendCompression: CompressionHandler = new IdentityHandler();
169183
private receiveCompression: CompressionHandler = new IdentityHandler();
184+
private currentCompressionAlgorithm: CompressionAlgorithm = 'identity';
185+
186+
constructor(channelOptions: ChannelOptions, private sharedFilterConfig: SharedCompressionFilterConfig) {
187+
super();
188+
189+
const compressionAlgorithmKey = channelOptions['grpc.default_compression_algorithm'];
190+
if (compressionAlgorithmKey !== undefined) {
191+
if (isCompressionAlgorithmKey(compressionAlgorithmKey)) {
192+
const clientSelectedEncoding = CompressionAlgorithms[compressionAlgorithmKey] as CompressionAlgorithm;
193+
const serverSupportedEncodings = sharedFilterConfig.serverSupportedEncodingHeader?.split(',');
194+
/**
195+
* There are two possible situations here:
196+
* 1) We don't have any info yet from the server about what compression it supports
197+
* In that case we should just use what the client tells us to use
198+
* 2) We've previously received a response from the server including a grpc-accept-encoding header
199+
* In that case we only want to use the encoding chosen by the client if the server supports it
200+
*/
201+
if (!serverSupportedEncodings || serverSupportedEncodings.includes(clientSelectedEncoding)) {
202+
this.currentCompressionAlgorithm = clientSelectedEncoding;
203+
this.sendCompression = getCompressionHandler(this.currentCompressionAlgorithm);
204+
}
205+
} else {
206+
logging.log(LogVerbosity.ERROR, `Invalid value provided for grpc.default_compression_algorithm option: ${compressionAlgorithmKey}`);
207+
}
208+
}
209+
}
210+
170211
async sendMetadata(metadata: Promise<Metadata>): Promise<Metadata> {
171212
const headers: Metadata = await metadata;
172213
headers.set('grpc-accept-encoding', 'identity,deflate,gzip');
173214
headers.set('accept-encoding', 'identity');
215+
216+
// No need to send the header if it's "identity" - behavior is identical; save the bandwidth
217+
if (this.currentCompressionAlgorithm === 'identity') {
218+
headers.remove('grpc-encoding');
219+
} else {
220+
headers.set('grpc-encoding', this.currentCompressionAlgorithm);
221+
}
222+
174223
return headers;
175224
}
176225

@@ -183,6 +232,19 @@ export class CompressionFilter extends BaseFilter implements Filter {
183232
}
184233
}
185234
metadata.remove('grpc-encoding');
235+
236+
/* Check to see if the compression we're using to send messages is supported by the server
237+
* If not, reset the sendCompression filter and have it use the default IdentityHandler */
238+
const serverSupportedEncodingsHeader = metadata.get('grpc-accept-encoding')[0] as string | undefined;
239+
if (serverSupportedEncodingsHeader) {
240+
this.sharedFilterConfig.serverSupportedEncodingHeader = serverSupportedEncodingsHeader;
241+
const serverSupportedEncodings = serverSupportedEncodingsHeader.split(',');
242+
243+
if (!serverSupportedEncodings.includes(this.currentCompressionAlgorithm)) {
244+
this.sendCompression = new IdentityHandler();
245+
this.currentCompressionAlgorithm = 'identity';
246+
}
247+
}
186248
metadata.remove('grpc-accept-encoding');
187249
return metadata;
188250
}
@@ -192,10 +254,13 @@ export class CompressionFilter extends BaseFilter implements Filter {
192254
* and the output is a framed and possibly compressed message. For this
193255
* reason, this filter should be at the bottom of the filter stack */
194256
const resolvedMessage: WriteObject = await message;
195-
const compress =
196-
resolvedMessage.flags === undefined
197-
? false
198-
: (resolvedMessage.flags & WriteFlags.NoCompress) === 0;
257+
let compress: boolean;
258+
if (this.sendCompression instanceof IdentityHandler) {
259+
compress = false;
260+
} else {
261+
compress = ((resolvedMessage.flags ?? 0) & WriteFlags.NoCompress) === 0;
262+
}
263+
199264
return {
200265
message: await this.sendCompression.writeMessage(
201266
resolvedMessage.message,
@@ -216,8 +281,9 @@ export class CompressionFilter extends BaseFilter implements Filter {
216281

217282
export class CompressionFilterFactory
218283
implements FilterFactory<CompressionFilter> {
219-
constructor(private readonly channel: Channel) {}
284+
private sharedFilterConfig: SharedCompressionFilterConfig = {};
285+
constructor(private readonly channel: Channel, private readonly options: ChannelOptions) {}
220286
createFilter(callStream: Call): CompressionFilter {
221-
return new CompressionFilter();
287+
return new CompressionFilter(this.options, this.sharedFilterConfig);
222288
}
223289
}

packages/grpc-js/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { CallCredentials, OAuth2Client } from './call-credentials';
2626
import { Deadline, StatusObject } from './call-stream';
2727
import { Channel, ChannelImplementation } from './channel';
28+
import { CompressionAlgorithms } from './compression-algorithms';
2829
import { ConnectivityState } from './connectivity-state';
2930
import { ChannelCredentials } from './channel-credentials';
3031
import {
@@ -126,6 +127,7 @@ export {
126127
Status as status,
127128
ConnectivityState as connectivityState,
128129
Propagate as propagate,
130+
CompressionAlgorithms as compressionAlgorithms
129131
// TODO: Other constants as well
130132
};
131133

0 commit comments

Comments
 (0)