From 95baa10d338599734f80ef651f7004858b2d5350 Mon Sep 17 00:00:00 2001 From: lacklacklack Date: Fri, 22 May 2026 20:40:33 +0200 Subject: [PATCH 1/5] Add compression to replicator --- rel/overlay/etc/default.ini | 7 ++ src/couch_replicator/COMPRESSION_CONFIG.md | 32 +++++ .../src/couch_replicator_httpc.erl | 110 +++++++++++++++++- 3 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/couch_replicator/COMPRESSION_CONFIG.md diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index ff148b2714..b585c7cc38 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -724,6 +724,13 @@ partitioned||* = true ; *.example.com:443:[2001:db8::1]:443 ;connect_to = +; Compression settings for replication +;compress_requests = true +;compress_min_size = 1024 +;compression_algorithm = gzip +;accept_encodings = gzip, deflate + + ; Some socket options that might boost performance in some scenarios: ; {nodelay, boolean()} ; {sndbuf, integer()} diff --git a/src/couch_replicator/COMPRESSION_CONFIG.md b/src/couch_replicator/COMPRESSION_CONFIG.md new file mode 100644 index 0000000000..0dfd05b886 --- /dev/null +++ b/src/couch_replicator/COMPRESSION_CONFIG.md @@ -0,0 +1,32 @@ +# CouchDB Replicator Compression + +## Overview + +The replicator now supports configurable HTTP compression to reduce bandwidth during replication. + +## Configuration + +```ini +[replicator] +; Enable compression (default: true) +compress_requests = true + +; Minimum body size to compress in bytes (default: 1024) +compress_min_size = 1024 + +; Algorithm: gzip (default), deflate +compression_algorithm = gzip + +; Accept these encodings in responses +accept_encodings = gzip, deflate +``` + +## Algorithms + +- **gzip** (default): Best compatibility, built-in to Erlang +- **deflate**: Built-in to Erlang, slightly faster than gzip + +## Statistics + +- `couch_replicator.requests.compressed` - Total compressed requests +- `couch_replicator.requests.compressed.{algorithm}` - Per-algorithm stats \ No newline at end of file diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl index 7f4f43afd5..abd954aeb2 100644 --- a/src/couch_replicator/src/couch_replicator_httpc.erl +++ b/src/couch_replicator/src/couch_replicator_httpc.erl @@ -21,6 +21,7 @@ -export([stop_http_worker/0]). -export([full_url/2]). + -import(couch_util, [ get_value/2, get_value/3 @@ -40,6 +41,80 @@ % where we may end up processing an unbounded number of messages. -define(MAX_DISCARDED_MESSAGES, 100). +should_compress_request(Body) when is_binary(Body) -> + MinSize = config:get_integer("replicator", "compress_min_size", 1024), + byte_size(Body) >= MinSize; +should_compress_request(Body) when is_list(Body) -> + should_compress_request(iolist_to_binary(Body)); +should_compress_request(_) -> + false. + +get_compression_algorithm() -> + % Supported: gzip (default), deflate + Algorithm = config:get("replicator", "compression_algorithm", "gzip"), + case Algorithm of + "gzip" -> gzip; + "deflate" -> deflate; + _ -> + couch_log:warning( + "couch_replicator_httpc: Unknown compression algorithm ~p, using gzip", + [Algorithm] + ), + gzip + end. + +compress_body(Body) when is_binary(Body) -> + compress_body_with_algorithm(Body, get_compression_algorithm()); +compress_body(Body) when is_list(Body) -> + compress_body(iolist_to_binary(Body)); +compress_body(Body) -> + Body. + +compress_body_with_algorithm(Body, gzip) -> + zlib:gzip(Body); +compress_body_with_algorithm(Body, deflate) -> + zlib:compress(Body). + +get_content_encoding(gzip) -> "gzip"; +get_content_encoding(deflate) -> "deflate". + +decompress_body(Headers, Body) -> + case lists:keyfind("Content-Encoding", 1, Headers) of + {"Content-Encoding", Encoding} -> + decompress_body_with_encoding(Encoding, Body); + _ -> + Body + end. + +decompress_body_with_encoding("gzip", Body) -> + try + zlib:gunzip(Body) + catch + error:data_error -> + couch_log:warning( + "couch_replicator_httpc: Failed to decompress gzip response, using original", + [] + ), + Body + end; +decompress_body_with_encoding("deflate", Body) -> + try + zlib:uncompress(Body) + catch + error:data_error -> + couch_log:warning( + "couch_replicator_httpc: Failed to decompress deflate response, using original", + [] + ), + Body + end; +decompress_body_with_encoding(Other, Body) -> + couch_log:warning( + "couch_replicator_httpc: Unknown content encoding ~p, using original body", + [Other] + ), + Body. + setup(Db) -> #httpdb{ httpc_pool = nil, @@ -108,11 +183,35 @@ stop_http_worker() -> send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) -> Method = get_value(method, Params, get), - UserHeaders = get_value(headers, Params, []), - Headers1 = merge_headers(BaseHeaders, UserHeaders), + UserHeaders0 = get_value(headers, Params, []), + % Accept multiple compression algorithms + AcceptEncodings = config:get("replicator", "accept_encodings", "gzip, deflate, zstd"), + UserHeaders1 = case lists:keyfind("Accept-Encoding", 1, UserHeaders0) of + false -> [{"Accept-Encoding", AcceptEncodings} | UserHeaders0]; + _ -> UserHeaders0 + end, + Body0 = get_value(body, Params, []), + CompressEnabled = config:get_boolean("replicator", "compress_requests", true), + {Body, UserHeaders2} = case CompressEnabled andalso should_compress_request(Body0) of + true -> + Algorithm = get_compression_algorithm(), + CompressedBody = compress_body(Body0), + ContentEncoding = get_content_encoding(Algorithm), + UpdatedHeaders = case lists:keyfind("Content-Encoding", 1, UserHeaders1) of + false -> [{"Content-Encoding", ContentEncoding} | UserHeaders1]; + _ -> UserHeaders1 + end, + % Track compression algorithm usage + couch_stats:increment_counter([couch_replicator, requests, compressed]), + couch_stats:increment_counter([couch_replicator, requests, compressed, Algorithm]), + {CompressedBody, UpdatedHeaders}; + false -> + {Body0, UserHeaders1} + end, + + Headers1 = merge_headers(BaseHeaders, UserHeaders2), {Headers2, HttpDb} = couch_replicator_auth:update_headers(HttpDb0, Headers1), Url0 = full_url(HttpDb, Params), - Body = get_value(body, Params, []), case get_value(path, Params) == "_changes" of true -> Timeout = infinity; @@ -182,6 +281,9 @@ process_response({error, connection_closing}, Worker, HttpDb, Params, _Cb) -> process_response({ibrowse_req_id, ReqId}, Worker, HttpDb, Params, Callback) -> process_stream_response(ReqId, Worker, HttpDb, Params, Callback); process_response({ok, Code, Headers, Body}, Worker, HttpDb, Params, Callback) -> + % Decompress body if it's gzip compressed + DecompressedBody = decompress_body(Headers, Body), + case list_to_integer(Code) of R when R =:= 301; R =:= 302; R =:= 303 -> backoff_success(HttpDb, Params), @@ -195,7 +297,7 @@ process_response({ok, Code, Headers, Body}, Worker, HttpDb, Params, Callback) -> backoff_success(HttpDb, Params), couch_stats:increment_counter([couch_replicator, responses, success]), EJson = - case Body of + case DecompressedBody of <<>> -> null; Json -> From 7581daad8556f020f35d04fbb8464ac39cec1f89 Mon Sep 17 00:00:00 2001 From: lacklacklack Date: Mon, 22 Jun 2026 16:45:32 +0200 Subject: [PATCH 2/5] Add test and metrics --- .../priv/stats_descriptions.cfg | 13 +++ .../src/couch_replicator_httpc.erl | 7 +- .../couch_replicator_compression_tests.erl | 89 +++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl diff --git a/src/couch_replicator/priv/stats_descriptions.cfg b/src/couch_replicator/priv/stats_descriptions.cfg index 10821d8851..bea544b6c6 100644 --- a/src/couch_replicator/priv/stats_descriptions.cfg +++ b/src/couch_replicator/priv/stats_descriptions.cfg @@ -146,3 +146,16 @@ {type, counter}, {desc, <<"number of times DNS overrides were applied to replication requests">>} ]}. + +{[couch_replicator, requests_compressed], [ + {type, counter}, + {desc, <<"number of HTTP requests compressed by the replicator">>} +]}. +{[couch_replicator, requests_compressed, gzip], [ + {type, counter}, + {desc, <<"number of HTTP requests compressed with gzip by the replicator">>} +]}. +{[couch_replicator, requests_compressed, deflate], [ + {type, counter}, + {desc, <<"number of HTTP requests compressed with deflate by the replicator">>} +]}. diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl index abd954aeb2..4617e101bb 100644 --- a/src/couch_replicator/src/couch_replicator_httpc.erl +++ b/src/couch_replicator/src/couch_replicator_httpc.erl @@ -192,7 +192,8 @@ send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) -> end, Body0 = get_value(body, Params, []), CompressEnabled = config:get_boolean("replicator", "compress_requests", true), - {Body, UserHeaders2} = case CompressEnabled andalso should_compress_request(Body0) of + ShouldCompress = should_compress_request(Body0), + {Body, UserHeaders2} = case CompressEnabled andalso ShouldCompress of true -> Algorithm = get_compression_algorithm(), CompressedBody = compress_body(Body0), @@ -202,8 +203,8 @@ send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) -> _ -> UserHeaders1 end, % Track compression algorithm usage - couch_stats:increment_counter([couch_replicator, requests, compressed]), - couch_stats:increment_counter([couch_replicator, requests, compressed, Algorithm]), + couch_stats:increment_counter([couch_replicator, requests_compressed]), + couch_stats:increment_counter([couch_replicator, requests_compressed, Algorithm]), {CompressedBody, UpdatedHeaders}; false -> {Body0, UserHeaders1} diff --git a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl new file mode 100644 index 0000000000..af32e040a2 --- /dev/null +++ b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl @@ -0,0 +1,89 @@ +% 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_compress_http_requests, ?TIMEOUT_EUNIT) + ] + } + }. + +should_compress_http_requests({_Ctx, {Source, Target}}) -> + config:set("replicator", "compress_min_size", "10", false), + config:set("replicator", "compress_requests", "true", false), + config:set("replicator", "compression_algorithm", "gzip", false), + InitialCompressed = couch_stats:sample([couch_replicator, requests_compressed]), + InitialGzip = couch_stats:sample([couch_replicator, requests_compressed, gzip]), + ?assertEqual(0, InitialCompressed), + ?assertEqual(0, InitialGzip), + populate_db(Source, ?DOCS_COUNT), + replicate(Source, Target), + compare_dbs(Source, Target), + FinalCompressed = couch_stats:sample([couch_replicator, requests_compressed]), + FinalGzip = couch_stats:sample([couch_replicator, requests_compressed, gzip]), + ?assertEqual(?DOCS_COUNT, FinalCompressed, + io_lib:format("Expected ~p compressed requests, got: ~p", [?DOCS_COUNT, FinalCompressed])), + ?assertEqual(?DOCS_COUNT, FinalGzip, + io_lib:format("Expected ~p gzip requests, got: ~p", [?DOCS_COUNT, FinalGzip])), + ?assertEqual(FinalCompressed, FinalGzip, + "All compressed requests should use gzip algorithm"), + config:delete("replicator", "compress_min_size", false), + config:delete("replicator", "compress_requests", false), + config:delete("replicator", "compression_algorithm", false). + +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) -> + 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} + ]}, + {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). From f1fe550b2e5f54d3edaf7908066cdf1713101b7e Mon Sep 17 00:00:00 2001 From: lacklacklack Date: Thu, 25 Jun 2026 16:59:14 +0200 Subject: [PATCH 3/5] Resolve comments: gzip only, simplify _bulk_docs body handling --- rel/overlay/etc/default.ini | 7 +- src/couch_replicator/COMPRESSION_CONFIG.md | 34 ++---- .../priv/stats_descriptions.cfg | 8 -- .../src/couch_replicator_api_wrap.erl | 59 +++++++--- .../src/couch_replicator_httpc.erl | 110 +----------------- .../couch_replicator_compression_tests.erl | 34 +++--- 6 files changed, 75 insertions(+), 177 deletions(-) diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index b585c7cc38..939bea21b6 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -724,11 +724,12 @@ partitioned||* = true ; *.example.com:443:[2001:db8::1]:443 ;connect_to = -; Compression settings for replication -;compress_requests = true +; Compress outbound replication request bodies (_bulk_docs, _revs_diff) with gzip. +; Disabled by default. Only gzip is supported. Enable only when talking to CouchDB +; servers that support gzip Content-Encoding on inbound requests. +;compress_requests = false ;compress_min_size = 1024 ;compression_algorithm = gzip -;accept_encodings = gzip, deflate ; Some socket options that might boost performance in some scenarios: diff --git a/src/couch_replicator/COMPRESSION_CONFIG.md b/src/couch_replicator/COMPRESSION_CONFIG.md index 0dfd05b886..a7e59c8c3d 100644 --- a/src/couch_replicator/COMPRESSION_CONFIG.md +++ b/src/couch_replicator/COMPRESSION_CONFIG.md @@ -1,32 +1,16 @@ -# CouchDB Replicator Compression +# CouchDB Replicator Request Compression -## Overview +The replicator can optionally gzip-compress outbound request bodies for +`_bulk_docs` and `_revs_diff`. This reduces bandwidth during replication. +CouchDB already supports `Content-Encoding: gzip` on inbound requests, so no +server-side changes are needed. -The replicator now supports configurable HTTP compression to reduce bandwidth during replication. - -## Configuration +Compression is disabled by default. Relevant `[replicator]` config keys: ```ini [replicator] -; Enable compression (default: true) -compress_requests = true - -; Minimum body size to compress in bytes (default: 1024) -compress_min_size = 1024 - -; Algorithm: gzip (default), deflate -compression_algorithm = gzip - -; Accept these encodings in responses -accept_encodings = gzip, deflate +compress_requests = false +compress_min_size = 1024 ; minimum body size in bytes before compressing ``` -## Algorithms - -- **gzip** (default): Best compatibility, built-in to Erlang -- **deflate**: Built-in to Erlang, slightly faster than gzip - -## Statistics - -- `couch_replicator.requests.compressed` - Total compressed requests -- `couch_replicator.requests.compressed.{algorithm}` - Per-algorithm stats \ No newline at end of file +Metric: `couch_replicator.requests_compressed.gzip` — number of gzip-compressed requests sent. diff --git a/src/couch_replicator/priv/stats_descriptions.cfg b/src/couch_replicator/priv/stats_descriptions.cfg index bea544b6c6..546b8af38e 100644 --- a/src/couch_replicator/priv/stats_descriptions.cfg +++ b/src/couch_replicator/priv/stats_descriptions.cfg @@ -147,15 +147,7 @@ {desc, <<"number of times DNS overrides were applied to replication requests">>} ]}. -{[couch_replicator, requests_compressed], [ - {type, counter}, - {desc, <<"number of HTTP requests compressed by the replicator">>} -]}. {[couch_replicator, requests_compressed, gzip], [ {type, counter}, {desc, <<"number of HTTP requests compressed with gzip by the replicator">>} ]}. -{[couch_replicator, requests_compressed, deflate], [ - {type, counter}, - {desc, <<"number of HTTP requests compressed with deflate by the replicator">>} -]}. diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl index 9364757d6c..b10288a36c 100644 --- a/src/couch_replicator/src/couch_replicator_api_wrap.erl +++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl @@ -171,13 +171,14 @@ ensure_full_commit(#httpdb{} = Db) -> get_missing_revs(#httpdb{} = Db, IdRevs) -> JsonBody = {[{Id, couch_doc:revs_to_strs(Revs)} || {Id, Revs} <- IdRevs]}, + {Body, ExtraHeaders} = maybe_compress(?JSON_ENCODE(JsonBody)), 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}) -> @@ -477,7 +478,7 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) -> % Note: nginx and other servers don't like PUT/POST requests without % a Content-Length header, so we can't do a chunked transfer encoding % and JSON encode each doc only before sending it through the socket. - {Docs, Len} = lists:mapfoldl( + {Docs, _} = lists:mapfoldl( fun (#doc{} = Doc, Acc) -> Json = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, [revs, attachments])), @@ -485,32 +486,27 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) -> (Doc, Acc) -> {Doc, Acc + iolist_size(Doc)} end, - byte_size(Prefix) + byte_size(Suffix) + length(DocList) - 1, + 0, DocList ), - BodyFun = fun - (eof) -> - eof; - ([]) -> - {ok, Suffix, eof}; - ([prefix | Rest]) -> - {ok, Prefix, Rest}; - ([Doc]) -> - {ok, Doc, []}; - ([Doc | RestDocs]) -> - {ok, [Doc, ","], RestDocs} - end, + % Collect the full body into a binary so we can optionally gzip it. + % Content-Length is required (nginx etc. reject chunked PUT/POST). + RawBody = iolist_to_binary( + [Prefix, lists:join(",", Docs), Suffix] + ), + {Body, ExtraHeaders} = maybe_compress(RawBody), Headers = [ - {"Content-Length", Len}, + {"Content-Length", byte_size(Body)}, {"Content-Type", "application/json"}, {"X-Couch-Full-Commit", FullCommit} + | ExtraHeaders ], send_req( HttpDb, [ {method, post}, {path, "_bulk_docs"}, - {body, {BodyFun, [prefix | Docs]}}, + {body, Body}, {headers, Headers} ], fun @@ -1052,6 +1048,33 @@ header_value(Key, Headers, Default) -> _ -> Default end. + +%% Compress Body with gzip if enabled and body is large enough. +%% Returns {Body, ExtraHeaders} where ExtraHeaders may contain Content-Encoding. +maybe_compress(Body) when is_binary(Body) -> + case config:get_boolean("replicator", "compress_requests", false) of + true -> + Algorithm = config:get("replicator", "compression_algorithm", "gzip"), + MinSize = config:get_integer("replicator", "compress_min_size", 1024), + case byte_size(Body) >= MinSize of + true -> compress_with(Algorithm, Body); + false -> {Body, []} + end; + false -> + {Body, []} + end. + +compress_with("gzip", Body) -> + Compressed = zlib:gzip(Body), + couch_stats:increment_counter([couch_replicator, requests_compressed]), + couch_stats:increment_counter([couch_replicator, requests_compressed, gzip]), + {Compressed, [{"Content-Encoding", "gzip"}]}; +compress_with(Other, Body) -> + couch_log:warning( + "~p: unsupported compression_algorithm ~p, skipping compression", + [?MODULE, Other] + ), + {Body, []}. % 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. diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl index 4617e101bb..4dd576b343 100644 --- a/src/couch_replicator/src/couch_replicator_httpc.erl +++ b/src/couch_replicator/src/couch_replicator_httpc.erl @@ -41,80 +41,6 @@ % where we may end up processing an unbounded number of messages. -define(MAX_DISCARDED_MESSAGES, 100). -should_compress_request(Body) when is_binary(Body) -> - MinSize = config:get_integer("replicator", "compress_min_size", 1024), - byte_size(Body) >= MinSize; -should_compress_request(Body) when is_list(Body) -> - should_compress_request(iolist_to_binary(Body)); -should_compress_request(_) -> - false. - -get_compression_algorithm() -> - % Supported: gzip (default), deflate - Algorithm = config:get("replicator", "compression_algorithm", "gzip"), - case Algorithm of - "gzip" -> gzip; - "deflate" -> deflate; - _ -> - couch_log:warning( - "couch_replicator_httpc: Unknown compression algorithm ~p, using gzip", - [Algorithm] - ), - gzip - end. - -compress_body(Body) when is_binary(Body) -> - compress_body_with_algorithm(Body, get_compression_algorithm()); -compress_body(Body) when is_list(Body) -> - compress_body(iolist_to_binary(Body)); -compress_body(Body) -> - Body. - -compress_body_with_algorithm(Body, gzip) -> - zlib:gzip(Body); -compress_body_with_algorithm(Body, deflate) -> - zlib:compress(Body). - -get_content_encoding(gzip) -> "gzip"; -get_content_encoding(deflate) -> "deflate". - -decompress_body(Headers, Body) -> - case lists:keyfind("Content-Encoding", 1, Headers) of - {"Content-Encoding", Encoding} -> - decompress_body_with_encoding(Encoding, Body); - _ -> - Body - end. - -decompress_body_with_encoding("gzip", Body) -> - try - zlib:gunzip(Body) - catch - error:data_error -> - couch_log:warning( - "couch_replicator_httpc: Failed to decompress gzip response, using original", - [] - ), - Body - end; -decompress_body_with_encoding("deflate", Body) -> - try - zlib:uncompress(Body) - catch - error:data_error -> - couch_log:warning( - "couch_replicator_httpc: Failed to decompress deflate response, using original", - [] - ), - Body - end; -decompress_body_with_encoding(Other, Body) -> - couch_log:warning( - "couch_replicator_httpc: Unknown content encoding ~p, using original body", - [Other] - ), - Body. - setup(Db) -> #httpdb{ httpc_pool = nil, @@ -183,36 +109,11 @@ stop_http_worker() -> send_ibrowse_req(#httpdb{headers = BaseHeaders} = HttpDb0, Params) -> Method = get_value(method, Params, get), - UserHeaders0 = get_value(headers, Params, []), - % Accept multiple compression algorithms - AcceptEncodings = config:get("replicator", "accept_encodings", "gzip, deflate, zstd"), - UserHeaders1 = case lists:keyfind("Accept-Encoding", 1, UserHeaders0) of - false -> [{"Accept-Encoding", AcceptEncodings} | UserHeaders0]; - _ -> UserHeaders0 - end, - Body0 = get_value(body, Params, []), - CompressEnabled = config:get_boolean("replicator", "compress_requests", true), - ShouldCompress = should_compress_request(Body0), - {Body, UserHeaders2} = case CompressEnabled andalso ShouldCompress of - true -> - Algorithm = get_compression_algorithm(), - CompressedBody = compress_body(Body0), - ContentEncoding = get_content_encoding(Algorithm), - UpdatedHeaders = case lists:keyfind("Content-Encoding", 1, UserHeaders1) of - false -> [{"Content-Encoding", ContentEncoding} | UserHeaders1]; - _ -> UserHeaders1 - end, - % Track compression algorithm usage - couch_stats:increment_counter([couch_replicator, requests_compressed]), - couch_stats:increment_counter([couch_replicator, requests_compressed, Algorithm]), - {CompressedBody, UpdatedHeaders}; - false -> - {Body0, UserHeaders1} - end, - - Headers1 = merge_headers(BaseHeaders, UserHeaders2), + UserHeaders = get_value(headers, Params, []), + Headers1 = merge_headers(BaseHeaders, UserHeaders), {Headers2, HttpDb} = couch_replicator_auth:update_headers(HttpDb0, Headers1), Url0 = full_url(HttpDb, Params), + Body = get_value(body, Params, []), case get_value(path, Params) == "_changes" of true -> Timeout = infinity; @@ -282,9 +183,6 @@ process_response({error, connection_closing}, Worker, HttpDb, Params, _Cb) -> process_response({ibrowse_req_id, ReqId}, Worker, HttpDb, Params, Callback) -> process_stream_response(ReqId, Worker, HttpDb, Params, Callback); process_response({ok, Code, Headers, Body}, Worker, HttpDb, Params, Callback) -> - % Decompress body if it's gzip compressed - DecompressedBody = decompress_body(Headers, Body), - case list_to_integer(Code) of R when R =:= 301; R =:= 302; R =:= 303 -> backoff_success(HttpDb, Params), @@ -298,7 +196,7 @@ process_response({ok, Code, Headers, Body}, Worker, HttpDb, Params, Callback) -> backoff_success(HttpDb, Params), couch_stats:increment_counter([couch_replicator, responses, success]), EJson = - case DecompressedBody of + case Body of <<>> -> null; Json -> diff --git a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl index af32e040a2..cee9a0c3ae 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl @@ -26,32 +26,32 @@ compression_test_() -> fun couch_replicator_test_helper:test_setup/0, fun couch_replicator_test_helper:test_teardown/1, [ - ?TDEF_FE(should_compress_http_requests, ?TIMEOUT_EUNIT) + ?TDEF_FE(should_not_compress_by_default, ?TIMEOUT_EUNIT), + ?TDEF_FE(should_compress_when_enabled, ?TIMEOUT_EUNIT) ] } }. -should_compress_http_requests({_Ctx, {Source, Target}}) -> - config:set("replicator", "compress_min_size", "10", false), +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", "compress_requests", "true", false), + config:set("replicator", "compress_min_size", "10", false), config:set("replicator", "compression_algorithm", "gzip", false), - InitialCompressed = couch_stats:sample([couch_replicator, requests_compressed]), - InitialGzip = couch_stats:sample([couch_replicator, requests_compressed, gzip]), - ?assertEqual(0, InitialCompressed), - ?assertEqual(0, InitialGzip), + Before = couch_stats:sample([couch_replicator, requests_compressed, gzip]), populate_db(Source, ?DOCS_COUNT), replicate(Source, Target), - compare_dbs(Source, Target), - FinalCompressed = couch_stats:sample([couch_replicator, requests_compressed]), - FinalGzip = couch_stats:sample([couch_replicator, requests_compressed, gzip]), - ?assertEqual(?DOCS_COUNT, FinalCompressed, - io_lib:format("Expected ~p compressed requests, got: ~p", [?DOCS_COUNT, FinalCompressed])), - ?assertEqual(?DOCS_COUNT, FinalGzip, - io_lib:format("Expected ~p gzip requests, got: ~p", [?DOCS_COUNT, FinalGzip])), - ?assertEqual(FinalCompressed, FinalGzip, - "All compressed requests should use gzip algorithm"), - config:delete("replicator", "compress_min_size", false), + compare_dbs(Source, Target), + After = couch_stats:sample([couch_replicator, requests_compressed, gzip]), + ?assert(After > Before), config:delete("replicator", "compress_requests", false), + config:delete("replicator", "compress_min_size", false), config:delete("replicator", "compression_algorithm", false). populate_db(DbName, Count) -> From a37a5fbc2e7bd8879a17a6efc2c509fa1bc28da5 Mon Sep 17 00:00:00 2001 From: lacklacklack Date: Mon, 20 Jul 2026 10:04:11 +0200 Subject: [PATCH 4/5] Resolve new minor comments, refactor --- rel/overlay/etc/default.ini | 9 +-- src/couch_replicator/COMPRESSION_CONFIG.md | 16 ---- .../src/couch_replicator_api_wrap.erl | 81 +++++++++++-------- .../src/couch_replicator_httpc.erl | 1 - .../couch_replicator_compression_tests.erl | 23 +++--- 5 files changed, 65 insertions(+), 65 deletions(-) delete mode 100644 src/couch_replicator/COMPRESSION_CONFIG.md diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index 939bea21b6..53f63a493b 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -724,12 +724,11 @@ partitioned||* = true ; *.example.com:443:[2001:db8::1]:443 ;connect_to = -; Compress outbound replication request bodies (_bulk_docs, _revs_diff) with gzip. -; Disabled by default. Only gzip is supported. Enable only when talking to CouchDB -; servers that support gzip Content-Encoding on inbound requests. -;compress_requests = false +; Compress outbound replication request bodies (_bulk_docs, _revs_diff). +; Accepted values: none (default, disabled), gzip. +; Enable gzip only when the target supports Content-Encoding: gzip on inbound requests. +;request_compression = none ;compress_min_size = 1024 -;compression_algorithm = gzip ; Some socket options that might boost performance in some scenarios: diff --git a/src/couch_replicator/COMPRESSION_CONFIG.md b/src/couch_replicator/COMPRESSION_CONFIG.md deleted file mode 100644 index a7e59c8c3d..0000000000 --- a/src/couch_replicator/COMPRESSION_CONFIG.md +++ /dev/null @@ -1,16 +0,0 @@ -# CouchDB Replicator Request Compression - -The replicator can optionally gzip-compress outbound request bodies for -`_bulk_docs` and `_revs_diff`. This reduces bandwidth during replication. -CouchDB already supports `Content-Encoding: gzip` on inbound requests, so no -server-side changes are needed. - -Compression is disabled by default. Relevant `[replicator]` config keys: - -```ini -[replicator] -compress_requests = false -compress_min_size = 1024 ; minimum body size in bytes before compressing -``` - -Metric: `couch_replicator.requests_compressed.gzip` — number of gzip-compressed requests sent. diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl index b10288a36c..7b413ef6bb 100644 --- a/src/couch_replicator/src/couch_replicator_api_wrap.erl +++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl @@ -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,7 +175,8 @@ ensure_full_commit(#httpdb{} = Db) -> get_missing_revs(#httpdb{} = Db, IdRevs) -> JsonBody = {[{Id, couch_doc:revs_to_strs(Revs)} || {Id, Revs} <- IdRevs]}, - {Body, ExtraHeaders} = maybe_compress(?JSON_ENCODE(JsonBody)), + RawBody = ?JSON_ENCODE(JsonBody), + {Body, ExtraHeaders} = maybe_compress(RawBody), send_req( Db, [ @@ -478,7 +483,7 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) -> % Note: nginx and other servers don't like PUT/POST requests without % a Content-Length header, so we can't do a chunked transfer encoding % and JSON encode each doc only before sending it through the socket. - {Docs, _} = lists:mapfoldl( + {Docs, Len} = lists:mapfoldl( fun (#doc{} = Doc, Acc) -> Json = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, [revs, attachments])), @@ -486,21 +491,33 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) -> (Doc, Acc) -> {Doc, Acc + iolist_size(Doc)} end, - 0, + byte_size(Prefix) + byte_size(Suffix) + length(DocList) - 1, DocList ), - % Collect the full body into a binary so we can optionally gzip it. - % Content-Length is required (nginx etc. reject chunked PUT/POST). - RawBody = iolist_to_binary( - [Prefix, lists:join(",", Docs), Suffix] - ), - {Body, ExtraHeaders} = maybe_compress(RawBody), - Headers = [ - {"Content-Length", byte_size(Body)}, + BodyFun = fun + (eof) -> + eof; + ([]) -> + {ok, Suffix, eof}; + ([prefix | Rest]) -> + {ok, Prefix, Rest}; + ([Doc]) -> + {ok, Doc, []}; + ([Doc | RestDocs]) -> + {ok, [Doc, ","], RestDocs} + end, + Headers0 = [ {"Content-Type", "application/json"}, {"X-Couch-Full-Commit", FullCommit} - | ExtraHeaders ], + {Body, Headers} = + case compress_requests(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, [ @@ -1049,33 +1066,33 @@ header_value(Key, Headers, Default) -> Default end. -%% Compress Body with gzip if enabled and body is large enough. +%% Returns true if compression is enabled and Body is large enough to compress. +compress_requests(BodySize) -> + case config:get("replicator", "request_compression", ?COMPRESS_NONE) of + ?COMPRESS_NONE -> + false; + _ -> + MinSize = config:get_integer("replicator", "compress_min_size", ?COMPRESS_MIN_SIZE), + BodySize >= MinSize + end. + +%% 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(Body) when is_binary(Body) -> - case config:get_boolean("replicator", "compress_requests", false) of + case compress_requests(byte_size(Body)) of true -> - Algorithm = config:get("replicator", "compression_algorithm", "gzip"), - MinSize = config:get_integer("replicator", "compress_min_size", 1024), - case byte_size(Body) >= MinSize of - true -> compress_with(Algorithm, Body); - false -> {Body, []} - end; + gzip_request_body(Body, []); false -> {Body, []} end. -compress_with("gzip", Body) -> - Compressed = zlib:gzip(Body), - couch_stats:increment_counter([couch_replicator, requests_compressed]), - couch_stats:increment_counter([couch_replicator, requests_compressed, gzip]), - {Compressed, [{"Content-Encoding", "gzip"}]}; -compress_with(Other, Body) -> - couch_log:warning( - "~p: unsupported compression_algorithm ~p, skipping compression", - [?MODULE, Other] - ), - {Body, []}. - % 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. normalize_db(#httpdb{} = HttpDb) -> diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl index 4dd576b343..7f4f43afd5 100644 --- a/src/couch_replicator/src/couch_replicator_httpc.erl +++ b/src/couch_replicator/src/couch_replicator_httpc.erl @@ -21,7 +21,6 @@ -export([stop_http_worker/0]). -export([full_url/2]). - -import(couch_util, [ get_value/2, get_value/3 diff --git a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl index cee9a0c3ae..59a25be191 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl @@ -41,18 +41,19 @@ should_not_compress_by_default({_Ctx, {Source, Target}}) -> ?assertEqual(Before, After). should_compress_when_enabled({_Ctx, {Source, Target}}) -> - config:set("replicator", "compress_requests", "true", false), + config:set("replicator", "request_compression", "gzip", false), config:set("replicator", "compress_min_size", "10", false), - config:set("replicator", "compression_algorithm", "gzip", false), - 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), - config:delete("replicator", "compress_requests", false), - config:delete("replicator", "compress_min_size", false), - config:delete("replicator", "compression_algorithm", 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. populate_db(DbName, Count) -> Docs = lists:map( From 204f0bb477d25bb5cfe1d837021360b267f55897 Mon Sep 17 00:00:00 2001 From: lacklacklack Date: Mon, 20 Jul 2026 10:23:28 +0200 Subject: [PATCH 5/5] Add per job compression --- .../include/couch_replicator_api_wrap.hrl | 3 +- .../src/couch_replicator_api_wrap.erl | 23 +++++------ .../src/couch_replicator_parse.erl | 11 +++++- .../couch_replicator_compression_tests.erl | 38 ++++++++++++++++++- 4 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/couch_replicator/include/couch_replicator_api_wrap.hrl b/src/couch_replicator/include/couch_replicator_api_wrap.hrl index 6d6ad527cb..71adb556d6 100644 --- a/src/couch_replicator/include/couch_replicator_api_wrap.hrl +++ b/src/couch_replicator/include/couch_replicator_api_wrap.hrl @@ -28,5 +28,6 @@ http_connections, first_error_timestamp = nil, proxy_url, - auth_context = nil + auth_context = nil, + request_compression = "none" }). diff --git a/src/couch_replicator/src/couch_replicator_api_wrap.erl b/src/couch_replicator/src/couch_replicator_api_wrap.erl index 7b413ef6bb..5a7a6a6b85 100644 --- a/src/couch_replicator/src/couch_replicator_api_wrap.erl +++ b/src/couch_replicator/src/couch_replicator_api_wrap.erl @@ -176,7 +176,7 @@ 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(RawBody), + {Body, ExtraHeaders} = maybe_compress(Db, RawBody), send_req( Db, [ @@ -511,7 +511,7 @@ update_docs(#httpdb{} = HttpDb, DocList, Options, UpdateType) -> {"X-Couch-Full-Commit", FullCommit} ], {Body, Headers} = - case compress_requests(Len) of + case compress_requests(HttpDb, Len) of true -> FullBody = iolist_to_binary([Prefix, lists:join(",", Docs), Suffix]), gzip_request_body(FullBody, Headers0); @@ -1066,15 +1066,12 @@ header_value(Key, Headers, Default) -> Default end. -%% Returns true if compression is enabled and Body is large enough to compress. -compress_requests(BodySize) -> - case config:get("replicator", "request_compression", ?COMPRESS_NONE) of - ?COMPRESS_NONE -> - false; - _ -> - MinSize = config:get_integer("replicator", "compress_min_size", ?COMPRESS_MIN_SIZE), - BodySize >= MinSize - 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) -> + 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}. @@ -1085,8 +1082,8 @@ gzip_request_body(Body, Headers) -> %% Compress Body if compression is enabled and body meets minimum size. %% Returns {Body, ExtraHeaders} where ExtraHeaders may contain Content-Encoding. -maybe_compress(Body) when is_binary(Body) -> - case compress_requests(byte_size(Body)) of +maybe_compress(#httpdb{} = HttpDb, Body) when is_binary(Body) -> + case compress_requests(HttpDb, byte_size(Body)) of true -> gzip_request_body(Body, []); false -> diff --git a/src/couch_replicator/src/couch_replicator_parse.erl b/src/couch_replicator/src/couch_replicator_parse.erl index 000108acd5..8713a61f41 100644 --- a/src/couch_replicator/src/couch_replicator_parse.erl +++ b/src/couch_replicator/src/couch_replicator_parse.erl @@ -54,6 +54,7 @@ default_options() -> {checkpoint_interval, cfg_int("checkpoint_interval", 30000)}, {use_checkpoints, cfg_boolean("use_checkpoints", true)}, {use_bulk_get, cfg_boolean("use_bulk_get", true)}, + {request_compression, cfg_str("request_compression", "none")}, {ibrowse_options, cfg_ibrowse_opts()}, {socket_options, cfg_sock_opts()} ]. @@ -216,7 +217,8 @@ parse_rep_db({Props}, Proxy, Options) -> timeout = get_value(connection_timeout, Options), http_connections = get_value(http_connections, Options), retries = get_value(retries, Options), - proxy_url = ProxyURL + proxy_url = ProxyURL, + request_compression = get_value(request_compression, Options, "none") }, couch_replicator_utils:normalize_basic_auth(HttpDb); parse_rep_db(<<"http://", _/binary>> = Url, Proxy, Options) -> @@ -284,6 +286,9 @@ cfg_int(Var, Default) -> cfg_boolean(Var, Default) -> config:get_boolean("replicator", Var, Default). +cfg_str(Var, Default) -> + config:get("replicator", Var, Default). + cfg_atoms(Cfg, Default) -> case cfg(Cfg) of undefined -> @@ -388,6 +393,8 @@ convert_options([{<<"since_seq">>, V} | R]) -> [{since_seq, V} | convert_options(R)]; convert_options([{<<"use_checkpoints">>, V} | R]) -> [{use_checkpoints, V} | convert_options(R)]; +convert_options([{<<"request_compression">>, V} | R]) when is_binary(V) -> + [{request_compression, binary_to_list(V)} | convert_options(R)]; convert_options([{<<"use_bulk_get">>, V} | _R]) when not is_boolean(V) -> throw({bad_request, <<"parameter `use_bulk_get` must be a boolean">>}); convert_options([{<<"use_bulk_get">>, V} | R]) -> @@ -774,6 +781,7 @@ t_parse_sock_opts(_) -> {connection_timeout, 30000}, {http_connections, 20}, {ibrowse_options, []}, + {request_compression, "none"}, {retries, 5}, {socket_options, [ {priority, 3}, @@ -819,6 +827,7 @@ t_parse_ibrowse_opts(_) -> {ibrowse_options, [ {prefer_ipv6, true} ]}, + {request_compression, "none"}, {retries, 5}, {socket_options, [ {keepalive, true}, diff --git a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl index 59a25be191..e90e75c373 100644 --- a/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl +++ b/src/couch_replicator/test/eunit/couch_replicator_compression_tests.erl @@ -27,7 +27,9 @@ compression_test_() -> 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_when_enabled, ?TIMEOUT_EUNIT), + ?TDEF_FE(should_compress_per_job, ?TIMEOUT_EUNIT), + ?TDEF_FE(job_compression_overrides_global_disabled, ?TIMEOUT_EUNIT) ] } }. @@ -55,6 +57,36 @@ should_compress_when_enabled({_Ctx, {Source, Target}}) -> 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) -> @@ -72,12 +104,16 @@ populate_db(DbName, Count) -> 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).