// Copyright 2023 The LevelDB-Go and Pebble Authors. All rights reserved. Use // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. package record // RotationHelper is a type used to inform the decision of rotating a record log // file. // // The assumption is that multiple records can be coalesced into a single record // (called a snapshot). Starting a new file, where the first record is a // snapshot of the current state is referred to as "rotating" the log. // // Normally we rotate files when a certain file size is reached. But in certain // cases (e.g. contents become very large), this can result in too frequent // rotation. This helper contains logic to impose extra conditions on the // rotation. // // The rotation helper uses "size" as a unit-less estimation that is correlated // with the on-disk size of a record or snapshot. type RotationHelper struct { // lastSnapshotSize is the size of the last snapshot. lastSnapshotSize int64 // sizeSinceLastSnapshot is the sum of sizes of records applied since the last // snapshot. sizeSinceLastSnapshot int64 lastRecordSize int64 } // AddRecord makes the rotation helper aware of a new record. func (rh *RotationHelper) AddRecord(recordSize int64) { rh.sizeSinceLastSnapshot += recordSize rh.lastRecordSize = recordSize } // ShouldRotate returns whether we should start a new log file (with a snapshot). // Does not need to be called if other rotation factors (log file size) are not // satisfied. func (rh *RotationHelper) ShouldRotate(nextSnapshotSize int64) bool { // The primary goal is to ensure that when reopening a log file, the number of // edits that need to be replayed on top of the snapshot is "sane" while // keeping the rotation frequency as low as possible. // // For the purposes of this description, we assume that the log is mainly // storing a collection of "entries", with edits adding or removing entries. // Consider the following cases: // // - The number of live entries is roughly stable: after writing the snapshot // (with S entries), we require that there be enough edits such that the // cumulative number of entries in those edits, E, be greater than S. This // will ensure that at most 50% of data written out is due to rotation. // // - The number of live entries K in the DB is shrinking drastically, say from // S to S/10: After this shrinking, E = 0.9S, and so if we used the previous // snapshot entry count, S, as the threshold that needs to be exceeded, we // will further delay the snapshot writing. Which means on reopen we will // need to replay 0.9S edits to get to a version with 0.1S entries. It would // be better to create a new snapshot when E exceeds the number of entries in // the current version. // // - The number of live entries L in the DB is growing; say the last snapshot // had S entries, and now we have 10S entries, so E = 9S. If we required // that E is at least the current number of entries, we would further delay // writing a new snapshot (which is not desirable). // // The logic below uses the min of the last snapshot size count and the size // count in the current version. return rh.sizeSinceLastSnapshot > rh.lastSnapshotSize || rh.sizeSinceLastSnapshot > nextSnapshotSize } // Rotate makes the rotation helper aware that we are rotating to a new snapshot // (to which we will apply the latest edit). func (rh *RotationHelper) Rotate(snapshotSize int64) { rh.lastSnapshotSize = snapshotSize rh.sizeSinceLastSnapshot = rh.lastRecordSize } // DebugInfo returns the last snapshot size and size of the edits since the last // snapshot; used for testing and debugging. func (rh *RotationHelper) DebugInfo() (lastSnapshotSize int64, sizeSinceLastSnapshot int64) { return rh.lastSnapshotSize, rh.sizeSinceLastSnapshot }