Skip to content

Commit 60a0172

Browse files
pdillingerfacebook-github-bot
authored andcommitted
Compression API clarifcations/minor fixes (facebook#13775)
Summary: * A number of comments clarifying contracts, etc. * Make ReleaseWorkingArea public instead of protected because there are some limited cases where a wrapper implementation might want to call it directly * Check non-empty dictionary precondition on MaybeCloneForDict * Expand testing of wrapped WorkingAreas * Random documentation improvement in block_builder.cc Pull Request resolved: facebook#13775 Test Plan: existing and expanded tests and assertions Reviewed By: hx235 Differential Revision: D78304550 Pulled By: pdillinger fbshipit-source-id: e5f064e8405a5a49be123ee13145cb3626bbbfbf
1 parent b7cd1fd commit 60a0172

5 files changed

Lines changed: 70 additions & 10 deletions

File tree

include/rocksdb/advanced_compression.h

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ struct FilterBuildingContext;
5050
// a number of built-in CompressionTypes that ignore any dictionary block in
5151
// the file; therefore they cannot accommodate dictionary compression in the
5252
// future without a schema change / extension.)
53+
//
54+
// Exceptions MUST NOT propagate out of overridden functions into RocksDB,
55+
// because RocksDB is not exception-safe. This could cause undefined behavior
56+
// including data loss, unreported corruption, deadlocks, and more.
5357
class Compressor {
5458
public:
5559
Compressor() = default;
@@ -134,15 +138,15 @@ class Compressor {
134138
// EXTENSIBLE or reinterpret_cast-able by custom Compressor implementations
135139
struct WorkingArea {};
136140

137-
protected:
138141
// To allow for flexible re-use / reclaimation, we have explicit Get and
139142
// Release functions, and usually wrap in a special RAII smart pointer.
140143
// For example, a WorkingArea could be saved/recycled in thread-local or
141144
// core-local storage, or heap managed, etc., though an explicit WorkingArea
142145
// is only advised for repeated compression (by a single thread).
146+
// ReleaseWorkingArea() in not intended to be called directly, but used by
147+
// ManagedWorkingArea.
143148
virtual void ReleaseWorkingArea(WorkingArea*) {}
144149

145-
public:
146150
using ManagedWorkingArea =
147151
ManagedPtr<WorkingArea, Compressor, &Compressor::ReleaseWorkingArea>;
148152

@@ -221,6 +225,10 @@ class Compressor {
221225
// decompressed into part of a single buffer allocated to hold a block's
222226
// uncompressed contents along with an in-memory object representation of the
223227
// block (to reduce fragmentation and other overheads of separate objects).
228+
//
229+
// Exceptions MUST NOT propagate out of overridden functions into RocksDB,
230+
// because RocksDB is not exception-safe. This could cause undefined behavior
231+
// including data loss, unreported corruption, deadlocks, and more.
224232
class Decompressor {
225233
public:
226234
Decompressor() = default;
@@ -278,6 +286,9 @@ class Decompressor {
278286
// supported for this kind of Decompressor. Corruption - dictionary is
279287
// malformed (though many implementations will accept any data as a
280288
// dictionary)
289+
//
290+
// RocksDB promises not to call this function with an empty dictionary slice
291+
// (equivalent to no dictionary).
281292
virtual Status MaybeCloneForDict(const Slice& /*serialized_dict*/,
282293
std::unique_ptr<Decompressor>* /*out*/) {
283294
return Status::NotSupported(
@@ -339,6 +350,10 @@ class Decompressor {
339350
// (because that would break backward compatibility, potential quiet
340351
// corruption)
341352
// TODO: consider adding optional streaming compression support (low priority)
353+
//
354+
// Exceptions MUST NOT propagate out of overridden functions into RocksDB,
355+
// because RocksDB is not exception-safe. This could cause undefined behavior
356+
// including data loss, unreported corruption, deadlocks, and more.
342357
class CompressionManager
343358
: public std::enable_shared_from_this<CompressionManager>,
344359
public Customizable {
@@ -466,6 +481,10 @@ class CompressorWrapper : public Compressor {
466481
return wrapped_->ObtainWorkingArea();
467482
}
468483

484+
// NOTE: Don't need to override ReleaseWorkingArea() here because
485+
// ManagedWorkingArea takes care of calling it on the Compressor that created
486+
// the WorkingArea.
487+
469488
Status CompressBlock(Slice uncompressed_data, std::string* compressed_output,
470489
CompressionType* out_compression_type,
471490
ManagedWorkingArea* working_area) override {
@@ -491,6 +510,10 @@ class DecompressorWrapper : public Decompressor {
491510
wrapped_->ReleaseWorkingArea(wa);
492511
}
493512

513+
// NOTE: Don't need to override ReleaseWorkingArea() here because
514+
// ManagedWorkingArea takes care of calling it on the Decompressor that
515+
// created the WorkingArea.
516+
494517
ManagedWorkingArea ObtainWorkingArea(CompressionType preferred) override {
495518
return wrapped_->ObtainWorkingArea(preferred);
496519
}

table/block_based/block_builder.cc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@
2121
// An entry for a particular key-value pair has the form:
2222
// shared_bytes: varint32
2323
// unshared_bytes: varint32
24-
// value_length: varint32
24+
// value_length: varint32 (NOTE1)
2525
// key_delta: char[unshared_bytes]
2626
// value: char[value_length]
27-
// shared_bytes == 0 for restart points.
27+
// shared_bytes == 0 (explicitly stored) for restart points.
2828
//
2929
// The trailer of the block has the form:
3030
// restarts: uint32[num_restarts]
3131
// num_restarts: uint32
3232
// restarts[i] contains the offset within the block of the ith restart point.
33+
//
34+
// NOTE1: omitted for format_version >= 4 index blocks, because the value is
35+
// composed of one (shared_bytes > 0) or two (shared_bytes == 0) varints, whose
36+
// length is self-describing.
3337

3438
#include "table/block_based/block_builder.h"
3539

util/compression.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,8 @@ class BuiltinDecompressorV2WithDict : public BuiltinDecompressorV2 {
746746

747747
Status BuiltinDecompressorV2::MaybeCloneForDict(
748748
const Slice& dict, std::unique_ptr<Decompressor>* out) {
749+
// Check RocksDB-promised precondition
750+
assert(dict.size() > 0);
749751
// Because of unfortunate decisions in handling built-in compression types,
750752
// all the compression types before ZSTD that do not actually support
751753
// dictionary compression pretend to support it. Specifically, we have to be

util/compression.h

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -240,15 +240,25 @@ struct DecompressorDict {
240240

241241
private:
242242
void Populate(Decompressor& from_decompressor, Slice dict) {
243-
Status s = from_decompressor.MaybeCloneForDict(dict, &decompressor_);
244-
if (decompressor_ == nullptr) {
243+
if (UNLIKELY(dict.empty())) {
245244
dict_str_ = {};
246245
dict_allocation_ = {};
247-
assert(!s.ok());
248-
decompressor_ = std::make_unique<FailureDecompressor>(std::move(s));
246+
// Appropriately reject bad files with empty dictionary block.
247+
// It is longstanding not to write an empty dictionary block:
248+
// https://github.com/facebook/rocksdb/blame/10.2.fb/table/block_based/block_based_table_builder.cc#L1841
249+
decompressor_ = std::make_unique<FailureDecompressor>(
250+
Status::Corruption("Decompression dictionary is empty"));
249251
} else {
250-
assert(s.ok());
251-
assert(decompressor_->GetSerializedDict() == dict);
252+
Status s = from_decompressor.MaybeCloneForDict(dict, &decompressor_);
253+
if (decompressor_ == nullptr) {
254+
dict_str_ = {};
255+
dict_allocation_ = {};
256+
assert(!s.ok());
257+
decompressor_ = std::make_unique<FailureDecompressor>(std::move(s));
258+
} else {
259+
assert(s.ok());
260+
assert(decompressor_->GetSerializedDict() == dict);
261+
}
252262
}
253263

254264
memory_usage_ = sizeof(struct DecompressorDict);

util/compression_test.cc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,6 +1124,27 @@ TEST_F(DBCompressionTest, CompressionManagerWrapper) {
11241124
out_compression_type, working_area);
11251125
}
11261126
}
1127+
1128+
// Also check WorkingArea handling
1129+
struct MyWorkingArea : public WorkingArea {
1130+
explicit MyWorkingArea(ManagedWorkingArea&& wrapped)
1131+
: wrapped_(std::move(wrapped)) {}
1132+
ManagedWorkingArea wrapped_;
1133+
};
1134+
ManagedWorkingArea ObtainWorkingArea() override {
1135+
ManagedWorkingArea rv{
1136+
new MyWorkingArea{CompressorWrapper::ObtainWorkingArea()}, this};
1137+
if (GetPreferredCompressionType() == kZSTD) {
1138+
// ZSTD should always use WorkingArea, so this is our chance to ensure
1139+
// CompressorWrapper::ObtainWorkingArea() is properly connected
1140+
assert(rv.get() != nullptr);
1141+
}
1142+
return rv;
1143+
}
1144+
1145+
void ReleaseWorkingArea(WorkingArea* wa) override {
1146+
delete static_cast<MyWorkingArea*>(wa);
1147+
}
11271148
};
11281149
struct MyManager : public CompressionManagerWrapper {
11291150
using CompressionManagerWrapper::CompressionManagerWrapper;

0 commit comments

Comments
 (0)