@@ -49,10 +49,22 @@ namespace privateer {
4949 }
5050 }
5151
52- // byte-compares an existing block file against data
53- result<bool > file_equals (fs::path const &path, std::span<std::byte const > data) {
52+ // what a block name already holds against the content being published
53+ enum struct existing_block : int {
54+ absent, // the name has no file
55+ equal, // the file holds this content, so it is already published
56+ differs, // the file holds other content under the same name
57+ };
58+
59+ // Byte-compares the file under path against data. A name with no file
60+ // is absent rather than an error, because writing one is what a
61+ // publisher does about it.
62+ result<existing_block> compare_block (fs::path const &path, std::span<std::byte const > data) {
5463 int const fd = ::open (path.c_str (), O_RDONLY | O_CLOEXEC );
5564 if (fd < 0 ) {
65+ if (errno == ENOENT ) {
66+ return existing_block::absent;
67+ }
5668 return fail_errno (errc::io_error, " open block for the dedup compare" );
5769 }
5870 struct stat st {};
@@ -63,7 +75,7 @@ namespace privateer {
6375 }
6476 if (std::cmp_not_equal (st.st_size , data.size ())) {
6577 ::close (fd);
66- return false ;
78+ return existing_block::differs ;
6779 }
6880 std::vector<std::byte> buffer (std::min<size_t >(data.size (), size_t {1 } << 20 ));
6981 size_t offset = 0 ;
@@ -80,14 +92,22 @@ namespace privateer {
8092 }
8193 if (got == 0 || std::memcmp (buffer.data (), data.data () + offset, static_cast <size_t >(got)) != 0 ) {
8294 ::close (fd);
83- return false ;
95+ return existing_block::differs ;
8496 }
8597 offset += static_cast <size_t >(got);
8698 }
8799 ::close (fd);
88- return true ;
100+ return existing_block::equal ;
89101 }
90102
103+ // How many times publish resolves the name before it gives up. Each
104+ // extra round needs another publisher to take the name in the window
105+ // between this one's compare and its link, and something to unlink
106+ // that file again before the next compare. A directory entry that
107+ // cannot be opened at all, a dangling symlink in a tampered store,
108+ // exhausts the rounds and reports io_error.
109+ constexpr unsigned publish_attempts = 4 ;
110+
91111 } // namespace
92112
93113 result<block_store> block_store::create (fs::path const &segment_dir, bool durable) {
@@ -147,28 +167,45 @@ namespace privateer {
147167 if (name.size == 0 || data.empty ()) {
148168 return fail (errc::invalid_argument, " publish needs a name and content" );
149169 }
150- auto staged = staged_file::create_in (shard_path (name));
151- if (!staged) {
152- return std::unexpected{staged.error ()};
153- }
154- if (auto written = staged->write (data); !written) {
155- return std::unexpected{written.error ()};
156- }
157- auto published = staged->publish (to_hex (name), publish_mode::fail_if_exists);
158- if (!published) {
159- return std::unexpected{published.error ()};
160- }
161- if (*published) {
162- return true ;
163- }
164- auto equal = file_equals (block_path (name), data);
165- if (!equal) {
166- return std::unexpected{equal.error ()};
167- }
168- if (!*equal) {
169- return fail (errc::hash_collision, " existing block differs under the same name" );
170+ fs::path const path = block_path (name);
171+ for (unsigned attempt = 0 ; attempt < publish_attempts; ++attempt) {
172+ // A file already under the name carries the content the name
173+ // stands for, so the compare is the whole publication and the
174+ // block is not written at all. Content that repeats is the
175+ // common case on a workload that copies a structure and changes
176+ // parts of it.
177+ auto const existing = compare_block (path, data);
178+ if (!existing) {
179+ return std::unexpected{existing.error ()};
180+ }
181+ switch (*existing) {
182+ case existing_block::equal:
183+ return false ;
184+ case existing_block::differs:
185+ return fail (errc::hash_collision, " existing block differs under the same name" );
186+ case existing_block::absent:
187+ break ; // the write below is what resolves it
188+ }
189+ auto staged = staged_file::create_in (shard_path (name));
190+ if (!staged) {
191+ return std::unexpected{staged.error ()};
192+ }
193+ if (auto written = staged->write (data); !written) {
194+ return std::unexpected{written.error ()};
195+ }
196+ auto const published = staged->publish (to_hex (name), publish_mode::fail_if_exists);
197+ if (!published) {
198+ return std::unexpected{published.error ()};
199+ }
200+ if (*published) {
201+ return true ;
202+ }
203+ // The atomic link is what resolves a race on one name, and this
204+ // caller lost it. The next round compares against the winner's
205+ // file, which is the dedup answer; only an unlink of that file
206+ // inside the same window sends the round back to the write.
170207 }
171- return false ;
208+ return fail (errc::io_error, " the block name neither opens nor accepts a link " ) ;
172209 }
173210
174211 result<> block_store::make_durable (std::span<block_digest const > names, sync_fan_out const &fan_out) {
0 commit comments