m_blacklist.insert(fid);
}
+void
+BeesContext::temp_transid_insert(const BeesFileId &fid, const uint64_t transid)
+{
+ unique_lock<mutex> lock(m_temp_transid_mutex);
+ // First write since reset wins: later writes join the same or a later
+ // transaction, so the earliest sample bounds every extent this temp
+ // file can still be creating.
+ m_temp_transid.emplace(fid, transid);
+}
+
+void
+BeesContext::temp_transid_erase(const BeesFileId &fid)
+{
+ unique_lock<mutex> lock(m_temp_transid_mutex);
+ m_temp_transid.erase(fid);
+}
+
+uint64_t
+BeesContext::temp_transid_min() const
+{
+ unique_lock<mutex> lock(m_temp_transid_mutex);
+ uint64_t rv = 0;
+ for (const auto &[fid, transid] : m_temp_transid) {
+ (void)fid;
+ if (!rv || transid < rv) {
+ rv = transid;
+ }
+ }
+ return rv;
+}
+
void
BeesContext::blacklist_erase(const BeesFileId &fid)
{
BeesRoots::effective_transid_max()
{
const auto raw = transid_max();
- return raw > m_min_transid_age ? raw - m_min_transid_age : 0;
+ auto rv = raw > m_min_transid_age ? raw - m_min_transid_age : 0;
+
+ // Hold the crawler below any transid in which a live temp file is
+ // still creating extents. Those extents are ordinary btrfs extents
+ // and the scan would pick them up like any other, but they are only
+ // half-built: refs are still being added as the dedupes that consume
+ // the temp file complete. An extent scanned mid-construction has an
+ // incomplete ref list, so it fails as a dedupe dst and contributes an
+ // incomplete src ref map. loop.transid-age-min can paper over this
+ // by making every extent wait a fixed number of transids; this is the
+ // exact bound, and costs nothing once the temp files are reset.
+ const auto temp_floor = m_ctx->temp_transid_min();
+ if (temp_floor) {
+ // Strictly below: the crawl window must not include the transid
+ // the temp file's extents can belong to.
+ const auto limit = temp_floor - 1;
+ if (limit < rv) {
+ rv = limit;
+ BEESCOUNT(crawl_temp_transid_hold);
+ }
+ }
+ return rv;
}
bool
THROW_CHECK2(runtime_error, static_cast<int>(m_mode), static_cast<int>(want), m_mode == want);
}
+void
+BeesTempFile::note_data_transid()
+{
+ if (m_data_transid) {
+ return;
+ }
+ // Extents this temp file is about to create belong to the running
+ // transaction or a later one, so the transid sampled at the first
+ // write bounds them from below. The extent crawler must not advance
+ // into that range: those extents are still acquiring refs, and an
+ // extent scanned mid-construction has an incomplete ref list -- it
+ // fails as a dedupe dst and offers an incomplete src ref map.
+ m_data_transid = m_ctx->roots()->transid_max();
+ m_ctx->temp_transid_insert(BeesFileId(m_fd), m_data_transid);
+}
+
void
BeesTempFile::reset()
{
// A reset begins a new mutation cycle: either family may be used next.
m_mode = Mode::none;
+ // The old contents are gone, so this file no longer holds back the
+ // extent crawler. See note_data_transid().
+ m_ctx->temp_transid_erase(BeesFileId(m_fd));
+ m_data_transid = 0;
// Always leave first block empty to avoid creating a file with an inline extent
resize(reserved());
}
THROW_CHECK1(invalid_argument, src, src.size() > 0);
+ note_data_transid();
+
// FIEMAP used to give us garbage data, e.g. distinct adjacent
// extents merged into a single entry in the FIEMAP output.
// FIEMAP didn't stop giving us garbage data, we just stopped
BEESNOTE("appending " << len << " bytes to " << name_fd(m_fd) << " at " << to_hex(m_end_offset));
BEESTRACE("appending " << len << " bytes to " << name_fd(m_fd) << " at " << to_hex(m_end_offset));
+ note_data_transid();
pwrite_or_die(m_fd, buf, len, m_end_offset);
m_end_offset += len;
BEESCOUNT(tmp_append);
/// Extend or truncate the file to @p new_end_offset bytes via ftruncate().
void resize(off_t new_end_offset);
+ /// Transid sampled at the first write since the last reset, or 0
+ /// when this file holds no data. Published to the context so the
+ /// extent crawler can stay below it.
+ uint64_t m_data_transid = 0;
+
+ /// Sample and publish m_data_transid if not already set.
+ void note_data_transid();
+
/**
* Mutation discipline used since the last reset(). A TempFile may be
* driven by exactly one family of methods between resets: @c modern
mutable mutex m_blacklist_mutex;
set<BeesFileId> m_blacklist; ///< Files excluded from deduplication.
+ mutable mutex m_temp_transid_mutex;
+ /// Earliest transid at which each live temp file materialized data.
+ /// Empty when no temp file currently holds data.
+ map<BeesFileId, uint64_t> m_temp_transid;
/// Timer recording total daemon uptime (used in status output).
Timer m_total_timer;
/// Return true if @p fid is on the blacklist.
bool is_blacklisted(const BeesFileId &fid) const;
+ /// Record that temp file @p fid materialized data at @p transid.
+ /// Idempotent: the first transid recorded since the last reset wins,
+ /// because that is the earliest transaction its extents can belong to.
+ void temp_transid_insert(const BeesFileId &fid, uint64_t transid);
+ /// Forget @p fid's transid; called when the temp file is reset.
+ void temp_transid_erase(const BeesFileId &fid);
+ /// Earliest transid among live temp files, or 0 when none hold data.
+ uint64_t temp_transid_min() const;
+
/// Return the per-inode Exclusion mutex for @p inode (creates if absent).
shared_ptr<Exclusion> get_inode_mutex(uint64_t inode);