-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add compression to replicator #6013
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
base: main
Are you sure you want to change the base?
Changes from all commits
95baa10
7581daa
f1fe550
a37a5fb
204f0bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -57,6 +57,10 @@ | |
| -define(MAX_URL_LEN, 7000). | ||
| -define(MIN_URL_LEN, 200). | ||
|
|
||
| -define(COMPRESS_MIN_SIZE, 1024). | ||
| -define(COMPRESS_NONE, "none"). | ||
| -define(COMPRESS_GZIP, "gzip"). | ||
|
|
||
| db_uri(#httpdb{url = Url}) -> | ||
| couch_util:url_strip_password(Url). | ||
|
|
||
|
|
@@ -171,13 +175,15 @@ ensure_full_commit(#httpdb{} = Db) -> | |
|
|
||
| get_missing_revs(#httpdb{} = Db, IdRevs) -> | ||
| JsonBody = {[{Id, couch_doc:revs_to_strs(Revs)} || {Id, Revs} <- IdRevs]}, | ||
| RawBody = ?JSON_ENCODE(JsonBody), | ||
| {Body, ExtraHeaders} = maybe_compress(Db, RawBody), | ||
| send_req( | ||
| Db, | ||
| [ | ||
| {method, post}, | ||
| {path, "_revs_diff"}, | ||
| {body, ?JSON_ENCODE(JsonBody)}, | ||
| {headers, [{"Content-Type", "application/json"}]} | ||
| {body, Body}, | ||
| {headers, [{"Content-Type", "application/json"} | ExtraHeaders]} | ||
| ], | ||
| fun | ||
| (200, _, {Props}) -> | ||
|
|
@@ -500,17 +506,24 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) -> | |
| ([Doc | RestDocs]) -> | ||
| {ok, [Doc, ","], RestDocs} | ||
| end, | ||
| Headers = [ | ||
| {"Content-Length", Len}, | ||
| Headers0 = [ | ||
| {"Content-Type", "application/json"}, | ||
| {"X-Couch-Full-Commit", FullCommit} | ||
| ], | ||
| {Body, Headers} = | ||
| case compress_requests(HttpDb, Len) of | ||
| true -> | ||
| FullBody = iolist_to_binary([Prefix, lists:join(",", Docs), Suffix]), | ||
| gzip_request_body(FullBody, Headers0); | ||
| false -> | ||
| {{BodyFun, [prefix | Docs]}, [{"Content-Length", Len} | Headers0]} | ||
| end, | ||
| send_req( | ||
| HttpDb, | ||
| [ | ||
| {method, post}, | ||
| {path, "_bulk_docs"}, | ||
| {body, {BodyFun, [prefix | Docs]}}, | ||
| {body, Body}, | ||
| {headers, Headers} | ||
| ], | ||
| fun | ||
|
|
@@ -1052,6 +1065,30 @@ header_value(Key, Headers, Default) -> | |
| _ -> | ||
| Default | ||
| end. | ||
|
|
||
| %% Returns true if compression is enabled for HttpDb and Body is large enough. | ||
| compress_requests(#httpdb{request_compression = ?COMPRESS_NONE}, _BodySize) -> | ||
| false; | ||
| compress_requests(#httpdb{}, BodySize) -> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we're missing a compression algorithm check here. If we get ?NONE we catch the first clause but here if we get a gzzzip and over minsize we'd end up compressing. We should probably check explicitly, keeping a shape of the code where we could easily add zstd or other compression methods. If we get a bogus method we could try falling back to ?NONE but not the new gzip default. |
||
| MinSize = config:get_integer("replicator", "compress_min_size", ?COMPRESS_MIN_SIZE), | ||
| BodySize >= MinSize. | ||
|
|
||
| %% Compress Body with gzip, prepend Content-Length and Content-Encoding headers. | ||
| %% Returns {CompressedBody, Headers}. | ||
| gzip_request_body(Body, Headers) -> | ||
| Compressed = zlib:gzip(Body), | ||
| couch_stats:increment_counter([couch_replicator, requests_compressed, gzip]), | ||
| {Compressed, [{"Content-Length", byte_size(Compressed)}, {"Content-Encoding", "gzip"} | Headers]}. | ||
|
|
||
| %% Compress Body if compression is enabled and body meets minimum size. | ||
| %% Returns {Body, ExtraHeaders} where ExtraHeaders may contain Content-Encoding. | ||
| maybe_compress(#httpdb{} = HttpDb, Body) when is_binary(Body) -> | ||
| case compress_requests(HttpDb, byte_size(Body)) of | ||
| true -> | ||
| gzip_request_body(Body, []); | ||
| false -> | ||
| {Body, []} | ||
| end. | ||
|
|
||
| % Normalize an #httpdb{} or #db{} record such that it can be used for | ||
| % comparisons. This means remove things like pids and also sort options / props. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| % Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
| % use this file except in compliance with the License. You may obtain a copy of | ||
| % the License at | ||
| % | ||
| % http://www.apache.org/licenses/LICENSE-2.0 | ||
| % | ||
| % Unless required by applicable law or agreed to in writing, software | ||
| % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
| % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
| % License for the specific language governing permissions and limitations under | ||
| % the License. | ||
|
|
||
| -module(couch_replicator_compression_tests). | ||
|
|
||
| -include_lib("couch/include/couch_eunit.hrl"). | ||
| -include_lib("couch/include/couch_db.hrl"). | ||
|
|
||
| -define(DOCS_COUNT, 10). | ||
| -define(TIMEOUT_EUNIT, 30). | ||
|
|
||
| compression_test_() -> | ||
| { | ||
| "Replication compression tests", | ||
| { | ||
| foreach, | ||
| fun couch_replicator_test_helper:test_setup/0, | ||
| fun couch_replicator_test_helper:test_teardown/1, | ||
| [ | ||
| ?TDEF_FE(should_not_compress_by_default, ?TIMEOUT_EUNIT), | ||
| ?TDEF_FE(should_compress_when_enabled, ?TIMEOUT_EUNIT), | ||
| ?TDEF_FE(should_compress_per_job, ?TIMEOUT_EUNIT), | ||
| ?TDEF_FE(job_compression_overrides_global_disabled, ?TIMEOUT_EUNIT) | ||
| ] | ||
| } | ||
| }. | ||
|
|
||
| should_not_compress_by_default({_Ctx, {Source, Target}}) -> | ||
| Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| populate_db(Source, ?DOCS_COUNT), | ||
| replicate(Source, Target), | ||
| compare_dbs(Source, Target), | ||
| After = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| ?assertEqual(Before, After). | ||
|
|
||
| should_compress_when_enabled({_Ctx, {Source, Target}}) -> | ||
| config:set("replicator", "request_compression", "gzip", false), | ||
| config:set("replicator", "compress_min_size", "10", false), | ||
| try | ||
| Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| populate_db(Source, ?DOCS_COUNT), | ||
| replicate(Source, Target), | ||
| compare_dbs(Source, Target), | ||
| After = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| ?assert(After > Before) | ||
| after | ||
| config:delete("replicator", "request_compression", false), | ||
| config:delete("replicator", "compress_min_size", false) | ||
| end. | ||
|
|
||
| should_compress_per_job({_Ctx, {Source, Target}}) -> | ||
| % global config is none (default), but job sets gzip | ||
| config:set("replicator", "compress_min_size", "10", false), | ||
| try | ||
| Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| populate_db(Source, ?DOCS_COUNT), | ||
| replicate_with_options(Source, Target, [{<<"request_compression">>, <<"gzip">>}]), | ||
| compare_dbs(Source, Target), | ||
| After = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| ?assert(After > Before) | ||
| after | ||
| config:delete("replicator", "compress_min_size", false) | ||
| end. | ||
|
|
||
| job_compression_overrides_global_disabled({_Ctx, {Source, Target}}) -> | ||
| % global config is gzip, but job disables it | ||
| config:set("replicator", "request_compression", "gzip", false), | ||
| config:set("replicator", "compress_min_size", "10", false), | ||
| try | ||
| Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| populate_db(Source, ?DOCS_COUNT), | ||
| replicate_with_options(Source, Target, [{<<"request_compression">>, <<"none">>}]), | ||
| compare_dbs(Source, Target), | ||
| After = couch_stats:sample([couch_replicator, requests_compressed, gzip]), | ||
| ?assertEqual(Before, After) | ||
| after | ||
| config:delete("replicator", "request_compression", false), | ||
| config:delete("replicator", "compress_min_size", false) | ||
| end. | ||
|
|
||
| populate_db(DbName, Count) -> | ||
| Docs = lists:map( | ||
| fun(I) -> | ||
| Id = iolist_to_binary(io_lib:format("doc~p", [I])), | ||
| Data = list_to_binary(lists:duplicate(100, $x)), | ||
| {[ | ||
| {<<"_id">>, Id}, | ||
| {<<"value">>, I}, | ||
| {<<"data">>, Data} | ||
| ]} | ||
| end, | ||
| lists:seq(1, Count) | ||
| ), | ||
| {ok, _} = fabric:update_docs(DbName, Docs, [?ADMIN_CTX]), | ||
| ok. | ||
|
|
||
| replicate(Source, Target) -> | ||
| replicate_with_options(Source, Target, []). | ||
|
|
||
| replicate_with_options(Source, Target, ExtraOptions) -> | ||
| SourceUrl = couch_replicator_test_helper:cluster_db_url(Source), | ||
| TargetUrl = couch_replicator_test_helper:cluster_db_url(Target), | ||
| RepObject = {[ | ||
| {<<"source">>, SourceUrl}, | ||
| {<<"target">>, TargetUrl}, | ||
| {<<"continuous">>, false} | ||
| | ExtraOptions | ||
| ]}, | ||
| {ok, _} = couch_replicator_test_helper:replicate(RepObject). | ||
|
|
||
| compare_dbs(Source, Target) -> | ||
| {ok, SourceInfo} = fabric:get_db_info(Source), | ||
| {ok, TargetInfo} = fabric:get_db_info(Target), | ||
| SourceDocCount = couch_util:get_value(doc_count, SourceInfo), | ||
| TargetDocCount = couch_util:get_value(doc_count, TargetInfo), | ||
| ?assertEqual(SourceDocCount, TargetDocCount), | ||
| ?assertEqual(?DOCS_COUNT, TargetDocCount). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to be careful here. ?JSON_ENCODE/1 may also return iodata not just binaries and maybe compress handles binary with a guard. The fix would be in maybe_compress to drop the guard and use iolist_size instead