diff options
Diffstat (limited to 'src/mongo/db/timeseries/bucket_catalog.cpp')
| -rw-r--r-- | src/mongo/db/timeseries/bucket_catalog.cpp | 204 |
1 files changed, 113 insertions, 91 deletions
diff --git a/src/mongo/db/timeseries/bucket_catalog.cpp b/src/mongo/db/timeseries/bucket_catalog.cpp index c9d3bc2f76a..10e0e2df903 100644 --- a/src/mongo/db/timeseries/bucket_catalog.cpp +++ b/src/mongo/db/timeseries/bucket_catalog.cpp @@ -38,7 +38,6 @@ #include "mongo/db/commands/server_status.h" #include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/operation_context.h" -#include "mongo/db/timeseries/metadata.h" #include "mongo/db/timeseries/timeseries_options.h" #include "mongo/platform/compiler.h" #include "mongo/stdx/thread.h" @@ -47,14 +46,11 @@ namespace mongo { namespace { +void normalizeArray(BSONArrayBuilder* builder, const BSONObj& obj); +void normalizeObject(BSONObjBuilder* builder, const BSONObj& obj); + const auto getBucketCatalog = ServiceContext::declareDecoration<BucketCatalog>(); MONGO_FAIL_POINT_DEFINE(hangTimeseriesDirectModificationBeforeWriteConflict); -MONGO_FAIL_POINT_DEFINE(hangWaitingForConflictingPreparedBatch); - -Mutex _bucketIdGenLock = - MONGO_MAKE_LATCH(HierarchicalAcquisitionLevel(0), "bucket_catalog_internal::_bucketIdGenLock"); -PseudoRandom _bucketIdGenPRNG(SecureRandom().nextInt64()); -AtomicWord<uint64_t> _bucketIdGenCounter{static_cast<uint64_t>(_bucketIdGenPRNG.nextInt64())}; uint8_t numDigits(uint32_t num) { uint8_t numDigits = 0; @@ -65,6 +61,81 @@ uint8_t numDigits(uint32_t num) { return numDigits; } +void normalizeArray(BSONArrayBuilder* builder, const BSONObj& obj) { + for (auto& arrayElem : obj) { + if (arrayElem.type() == BSONType::Array) { + BSONArrayBuilder subArray = builder->subarrayStart(); + normalizeArray(&subArray, arrayElem.Obj()); + } else if (arrayElem.type() == BSONType::Object) { + BSONObjBuilder subObject = builder->subobjStart(); + normalizeObject(&subObject, arrayElem.Obj()); + } else { + builder->append(arrayElem); + } + } +} + +void normalizeObject(BSONObjBuilder* builder, const BSONObj& obj) { + // BSONObjIteratorSorted provides an abstraction similar to what this function does. However it + // is using a lexical comparison that is slower than just doing a binary comparison of the field + // names. That is all we need here as we are looking to create something that is binary + // comparable no matter of field order provided by the user. + + // Helper that extracts the necessary data from a BSONElement that we can sort and re-construct + // the same BSONElement from. + struct Field { + BSONElement element() const { + return BSONElement(fieldName.rawData() - 1, // Include type byte before field name + fieldName.size() + 1, // Include null terminator after field name + totalSize); + } + bool operator<(const Field& rhs) const { + return fieldName < rhs.fieldName; + } + StringData fieldName; + int totalSize; + }; + + // Put all elements in a buffer, sort it and then continue normalize in sorted order + auto num = obj.nFields(); + static constexpr std::size_t kNumStaticFields = 16; + boost::container::small_vector<Field, kNumStaticFields> fields; + fields.resize(num); + BSONObjIterator bsonIt(obj); + int i = 0; + while (bsonIt.more()) { + auto elem = bsonIt.next(); + fields[i++] = {elem.fieldNameStringData(), elem.size()}; + } + auto it = fields.begin(); + auto end = fields.end(); + std::sort(it, end); + for (; it != end; ++it) { + auto elem = it->element(); + if (elem.type() == BSONType::Array) { + BSONArrayBuilder subArray(builder->subarrayStart(elem.fieldNameStringData())); + normalizeArray(&subArray, elem.Obj()); + } else if (elem.type() == BSONType::Object) { + BSONObjBuilder subObject(builder->subobjStart(elem.fieldNameStringData())); + normalizeObject(&subObject, elem.Obj()); + } else { + builder->append(elem); + } + } +} + +void normalizeTopLevel(BSONObjBuilder* builder, const BSONElement& elem) { + if (elem.type() == BSONType::Array) { + BSONArrayBuilder subArray(builder->subarrayStart(elem.fieldNameStringData())); + normalizeArray(&subArray, elem.Obj()); + } else if (elem.type() == BSONType::Object) { + BSONObjBuilder subObject(builder->subobjStart(elem.fieldNameStringData())); + normalizeObject(&subObject, elem.Obj()); + } else { + builder->append(elem); + } +} + OperationId getOpId(OperationContext* opCtx, BucketCatalog::CombineWithInsertsFromOtherClients combine) { switch (combine) { @@ -84,7 +155,7 @@ BSONObj buildControlMinTimestampDoc(StringData timeField, Date_t roundedTime) { } std::pair<OID, Date_t> generateBucketId(const Date_t& time, const TimeseriesOptions& options) { - OID oid; + OID bucketId = OID::gen(); // We round the measurement timestamp down to the nearest minute, hour, or day depending on the // granularity. We do this for two reasons. The first is so that if measurements come in @@ -94,35 +165,32 @@ std::pair<OID, Date_t> generateBucketId(const Date_t& time, const TimeseriesOpti // what the bucket timestamp will be, so we can route measurements to the right shard chunk. auto roundedTime = timeseries::roundTimestampToGranularity(time, options.getGranularity()); int64_t const roundedSeconds = durationCount<Seconds>(roundedTime.toDurationSinceEpoch()); - oid.setTimestamp(roundedSeconds); - - // Now, if we used the standard OID generation method for the remaining bytes we could end up - // with lots of bucket OID collisions. Consider the case where we have the granularity set to - // 'Hours'. This means we will round down to the nearest day, so any bucket generated on the - // same machine on the same day will have the same timestamp portion and unique instance portion - // of the OID. Only the increment would differ. Since we only use 3 bytes for the increment - // portion, we run a serious risk of overflow if we are generating lots of buckets. + bucketId.setTimestamp(roundedSeconds); + + // Now, if we stopped here we could end up with bucket OID collisions. Consider the case where + // we have the granularity set to 'Hours'. This means we will round down to the nearest day, so + // any bucket generated on the same machine on the same day will have the same timestamp portion + // and unique instance portion of the OID. Only the increment will differ. Since we only use 3 + // bytes for the increment portion, we run a serious risk of overflow if we are generating lots + // of buckets. // - // To address this, we'll instead use a PRNG to generate the rest of the bytes. With 8 bytes of - // randomness, we should have a pretty low chance of collisions. The limit of the birthday - // paradox converges to roughly the square root of the size of the space, so we would need a few - // billion buckets with the same timestamp to expect collisions. In the rare case that we do get - // a collision, we can (and do) simply regenerate the bucket _id at a higher level. - uint64_t bits = BigEndian<uint64_t>::store(_bucketIdGenCounter.addAndFetch(1)); - - OID::InstanceUnique instance; - const auto instanceBuf = static_cast<uint8_t*>(instance.bytes); - std::memcpy(instanceBuf, &bits, OID::kInstanceUniqueSize); - - OID::Increment increment; - const auto incrementBuf = static_cast<uint8_t*>(increment.bytes); - uint8_t* bitsBuf = (uint8_t*)&bits; - std::memcpy(incrementBuf, &(bitsBuf)[OID::kInstanceUniqueSize], OID::kIncrementSize); - - oid.setInstanceUnique(instance); - oid.setIncrement(increment); + // To address this, we'll take the difference between the actual timestamp and the rounded + // timestamp and add it to the instance portion of the OID to ensure we can't have a collision. + // for timestamps generated on the same machine. + // + // This leaves open the possibility that in the case of step-down/step-up, we could get a + // collision if the old primary and the new primary have unique instance bits that differ by + // less than the maximum rounding difference. This is quite unlikely though, and can be resolved + // by restarting the new primary. It remains an open question whether we can fix this in a + // better way. + // TODO (SERVER-61412): Avoid time-series bucket OID collisions after election + auto instance = bucketId.getInstanceUnique(); + uint32_t sum = DataView(reinterpret_cast<char*>(instance.bytes)).read<uint32_t>(1) + + (durationCount<Seconds>(time.toDurationSinceEpoch()) - roundedSeconds); + DataView(reinterpret_cast<char*>(instance.bytes)).write<uint32_t>(sum, 1); + bucketId.setInstanceUnique(instance); - return {oid, roundedTime}; + return {bucketId, roundedTime}; } Status getTimeseriesBucketClearedError(const OID& bucketId, @@ -202,12 +270,6 @@ void BucketCatalog::ExecutionStatsController::incNumMeasurementsCommitted(long l _globalStats->numMeasurementsCommitted.fetchAndAddRelaxed(increment); } -void BucketCatalog::ExecutionStatsController::incNumMeasurementsGroupCommitted( - long long increment) { - _collectionStats->numMeasurementsGroupCommitted.fetchAndAddRelaxed(increment); - _globalStats->numMeasurementsGroupCommitted.fetchAndAddRelaxed(increment); -} - class BucketCatalog::Bucket { public: friend class BucketCatalog; @@ -684,11 +746,7 @@ Status BucketCatalog::prepareCommit(std::shared_ptr<WriteBatch> batch) { _useBucketInState(&stripe, stripeLock, batch->bucket().id, BucketState::kPrepared); if (batch->finished()) { - // Someone may have aborted it while we were waiting. Since we have the prepared batch, we - // should now be able to fully abort the bucket. - if (bucket) { - _abort(&stripe, stripeLock, batch, getBatchStatus()); - } + // Someone may have aborted it while we were waiting. return getBatchStatus(); } else if (!bucket) { _abort(&stripe, stripeLock, batch, getTimeseriesBucketClearedError(batch->bucket().id)); @@ -853,8 +911,6 @@ void BucketCatalog::_appendExecutionStatsToBuilder(const ExecutionStats* stats, if (commits) { builder->appendNumber("avgNumMeasurementsPerCommit", measurementsCommitted / commits); } - builder->appendNumber("numMeasurementsGroupCommitted", - stats->numMeasurementsGroupCommitted.load()); } @@ -867,16 +923,6 @@ void BucketCatalog::appendGlobalExecutionStats(BSONObjBuilder* builder) const { _appendExecutionStatsToBuilder(&_globalExecutionStats, builder); } -void BucketCatalog::resetBucketOIDCounter() { - stdx::lock_guard lk{_bucketIdGenLock}; - _bucketIdGenCounter.store(static_cast<uint64_t>(_bucketIdGenPRNG.nextInt64())); -} - -void BucketCatalog::reportMeasurementsGroupCommitted(const NamespaceString& ns, int64_t count) { - auto stats = _getExecutionStats(ns); - stats.incNumMeasurementsGroupCommitted(count); -} - BucketCatalog::BucketMetadata::BucketMetadata(BSONElement elem, const StringData::ComparatorInterface* comparator) : _metadataElement(elem), _comparator(comparator) { @@ -884,7 +930,7 @@ BucketCatalog::BucketMetadata::BucketMetadata(BSONElement elem, BSONObjBuilder objBuilder; // We will get an object of equal size, just with reordered fields. objBuilder.bb().reserveBytes(_metadataElement.size()); - timeseries::metadata::normalize(_metadataElement, objBuilder); + normalizeTopLevel(&objBuilder, _metadataElement); _metadata = objBuilder.obj(); } // Updates the BSONElement to refer to the copied BSONObj. @@ -899,9 +945,8 @@ const BSONObj& BucketCatalog::BucketMetadata::toBSON() const { return _metadata; } -boost::optional<StringData> BucketCatalog::BucketMetadata::getMetaField() const { - return _metadataElement ? boost::make_optional(_metadataElement.fieldNameStringData()) - : boost::none; +StringData BucketCatalog::BucketMetadata::getMetaField() const { + return StringData(_metadataElement.fieldName()); } const StringData::ComparatorInterface* BucketCatalog::BucketMetadata::getComparator() const { @@ -1006,10 +1051,6 @@ void BucketCatalog::_waitToCommitBatch(Stripe* stripe, const std::shared_ptr<Wri } } - // We only hit this failpoint when there are conflicting prepared batches on the same - // bucket. - hangWaitingForConflictingPreparedBatch.pauseWhileSet(); - // We have to wait for someone else to finish. current->getResult().getStatus().ignore(); // We don't care about the result. } @@ -1067,7 +1108,7 @@ void BucketCatalog::_abort(Stripe* stripe, // user is doing with it, but we need to keep the bucket around until // that batch is finished. if (auto& prepared = bucket->_preparedBatch) { - if (batch && prepared == batch) { + if (prepared == batch) { // We own the prepared batch, so we can go ahead and abort it and remove the bucket. prepared->_abort(status); prepared.reset(); @@ -1078,8 +1119,6 @@ void BucketCatalog::_abort(Stripe* stripe, if (doRemove) { [[maybe_unused]] bool removed = _removeBucket(stripe, stripeLock, bucket); - } else { - _setBucketState(bucket->id(), BucketState::kCleared); } } @@ -1123,31 +1162,14 @@ BucketCatalog::Bucket* BucketCatalog::_allocateBucket(Stripe* stripe, const CreationInfo& info) { _expireIdleBuckets(stripe, stripeLock, info.stats, info.closedBuckets); + auto [bucketId, roundedTime] = generateBucketId(info.time, info.options); - // In rare cases duplicate bucket _id fields can be generated in the same stripe and fail to be - // inserted. We will perform a limited number of retries to minimize the probability of - // collision. - auto maxRetries = gTimeseriesInsertMaxRetriesOnDuplicates.load(); - OID bucketId; - Date_t roundedTime; - stdx::unordered_map<OID, std::unique_ptr<Bucket>, OID::Hasher>::iterator it; - bool inserted = false; - for (int retryAttempts = 0; !inserted && retryAttempts < maxRetries; ++retryAttempts) { - std::tie(bucketId, roundedTime) = generateBucketId(info.time, info.options); - std::tie(it, inserted) = stripe->allBuckets.try_emplace( - bucketId, std::make_unique<Bucket>(bucketId, info.stripe)); - if (!inserted) { - resetBucketOIDCounter(); - } - } - uassert(6130900, - "Unable to insert documents due to internal OID generation collision. Increase the " - "value of server parameter 'timeseriesInsertMaxRetriesOnDuplicates' and try again", - inserted); - + auto [it, inserted] = + stripe->allBuckets.try_emplace(bucketId, std::make_unique<Bucket>(bucketId, info.stripe)); + tassert(6130900, "Expected bucket to be inserted", inserted); Bucket* bucket = it->second.get(); stripe->openBuckets[info.key] = bucket; - _initializeBucketState(it->first); + _initializeBucketState(bucketId); if (info.openedDuetoMetadata) { info.stats.incNumBucketsOpenedDueToMetadata(); |
