Skip to content

Commit de46fea

Browse files
committed
Implement the ability to downgrade CouchDB versions
Currently, CouchDB on-disk header record allows appending new fields to it in such a way that newer versions can upgrade themselves from the old versions easily if the ?LATEST_DISK_VERSION stays the same. However, it was not possible to perform a downgrade back to an old version in case something went wrong. While in general it may not be safe to downgrade in such cases, it may be possible for some features, so allow for such an option and make it configurable. This is essentially a counterpart to this feature: ``` % As long the changes are limited to new header fields (with inline % defaults) added to the end of the record, then there is no need to increment % the disk revision number. ``` The release notes of future releases may indicate which version are downgrade safe. Then, to perform a downgrade users would enable the downgrade flag, downgrade, and then reset it back to default (prohibit = true). Implementation-wise, it's pretty basic, if the size of the new tuple is larger than the old one, it's a downgrade. Then, only use as many tuple fields as current (aka the "old" version) knows about, everything else is discarded.
1 parent 1a2dfc9 commit de46fea

File tree

2 files changed

+77
-11
lines changed

2 files changed

+77
-11
lines changed

rel/overlay/etc/default.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ view_index_dir = {{view_index_dir}}
120120
; this setting.
121121
;cfile_skip_ioq = false
122122

123+
; CouchDB will not normally allow a database downgrade to a previous version if
124+
; the new version added new fields to the database file header structure. You
125+
; can disable this protection by setting this to false. We recommend
126+
; re-enabling the protection after you have performed the downgrade but it is
127+
; not mandatory to do so.
128+
; Note that some future versions of CouchDB might not support downgrade at all,
129+
; whatever value this is set to. In those cases CouchDB will refuse to
130+
; downgrade or even open the databases in question.
131+
;prohibit_downgrade = true
132+
123133
[purge]
124134
; Allowed maximum number of documents in one purge request
125135
;max_document_id_number = 100

src/couch/src/couch_bt_engine_header.erl

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,16 @@ upgrade_tuple(Old) when is_record(Old, db_header) ->
204204
Old;
205205
upgrade_tuple(Old) when is_tuple(Old) ->
206206
NewSize = record_info(size, db_header),
207-
if
208-
tuple_size(Old) < NewSize -> ok;
209-
true -> erlang:error({invalid_header_size, Old})
210-
end,
211-
{_, New} = lists:foldl(
212-
fun(Val, {Idx, Hdr}) ->
213-
{Idx + 1, setelement(Idx, Hdr, Val)}
207+
Upgrade = tuple_size(Old) < NewSize,
208+
ProhibitDowngrade = config:get_boolean("couchdb", "prohibit_downgrade", true),
209+
OldKVs =
210+
case {Upgrade, ProhibitDowngrade} of
211+
{true, AnyBool} when is_boolean(AnyBool) -> tuple_to_list(Old);
212+
{false, true} -> error({invalid_header_size, Old});
213+
{false, false} -> lists:sublist(tuple_to_list(Old), NewSize)
214214
end,
215-
{1, #db_header{}},
216-
tuple_to_list(Old)
217-
),
215+
FoldFun = fun(Val, {Idx, Hdr}) -> {Idx + 1, setelement(Idx, Hdr, Val)} end,
216+
{_, New} = lists:foldl(FoldFun, {1, #db_header{}}, OldKVs),
218217
if
219218
is_record(New, db_header) -> ok;
220219
true -> erlang:error({invalid_header_extension, {Old, New}})
@@ -338,7 +337,7 @@ latest(_Else) ->
338337
undefined.
339338

340339
-ifdef(TEST).
341-
-include_lib("eunit/include/eunit.hrl").
340+
-include_lib("couch/include/couch_eunit.hrl").
342341

343342
mk_header(Vsn) ->
344343
{
@@ -478,4 +477,61 @@ get_epochs_from_old_header_test() ->
478477
Vsn5Header = mk_header(5),
479478
?assertEqual(undefined, epochs(Vsn5Header)).
480479

480+
tuple_uprade_test_() ->
481+
{
482+
foreach,
483+
fun() ->
484+
Ctx = test_util:start_couch(),
485+
config:set("couchdb", "prohibit_downgrade", "true", false),
486+
Ctx
487+
end,
488+
fun(Ctx) ->
489+
config:delete("couchdb", "prohibit_downgrade", false),
490+
test_util:stop_couch(Ctx)
491+
end,
492+
[
493+
?TDEF_FE(t_upgrade_tuple_same_size),
494+
?TDEF_FE(t_upgrade_tuple),
495+
?TDEF_FE(t_downgrade_default),
496+
?TDEF_FE(t_downgrade_allowed)
497+
]
498+
}.
499+
500+
t_upgrade_tuple_same_size(_) ->
501+
Hdr = #db_header{disk_version = ?LATEST_DISK_VERSION},
502+
Hdr1 = upgrade_tuple(Hdr),
503+
?assertEqual(Hdr, Hdr1).
504+
505+
t_upgrade_tuple(_) ->
506+
Hdr = {db_header, ?LATEST_DISK_VERSION, 101},
507+
Hdr1 = upgrade_tuple(Hdr),
508+
?assertMatch(
509+
#db_header{
510+
disk_version = ?LATEST_DISK_VERSION,
511+
update_seq = 101,
512+
purge_infos_limit = 1000
513+
},
514+
Hdr1
515+
).
516+
517+
t_downgrade_default(_) ->
518+
Junk = lists:duplicate(50, x),
519+
Hdr = list_to_tuple([db_header, ?LATEST_DISK_VERSION] ++ Junk),
520+
% Not allowed by default
521+
?assertError({invalid_header_size, _}, upgrade_tuple(Hdr)).
522+
523+
t_downgrade_allowed(_) ->
524+
Junk = lists:duplicate(50, x),
525+
Hdr = list_to_tuple([db_header, ?LATEST_DISK_VERSION, 42] ++ Junk),
526+
config:set("couchdb", "prohibit_downgrade", "false", false),
527+
Hdr1 = upgrade_tuple(Hdr),
528+
?assert(is_record(Hdr1, db_header)),
529+
?assertMatch(
530+
#db_header{
531+
disk_version = ?LATEST_DISK_VERSION,
532+
update_seq = 42
533+
},
534+
Hdr1
535+
).
536+
481537
-endif.

0 commit comments

Comments
 (0)