152 lines
7.1 KiB
C++
152 lines
7.1 KiB
C++
// Copyright 2023 Memgraph Ltd.
|
|
//
|
|
// Use of this software is governed by the Business Source License
|
|
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
|
// License, and you may not use this file except in compliance with the Business Source License.
|
|
//
|
|
// As of the Change Date specified in that file, in accordance with
|
|
// the Business Source License, use of this software will be governed
|
|
// by the Apache License, Version 2.0, included in the file
|
|
// licenses/APL.txt.
|
|
|
|
#pragma once
|
|
|
|
#include <type_traits>
|
|
|
|
#include "storage/v3/edge.hpp"
|
|
#include "storage/v3/property_value.hpp"
|
|
#include "storage/v3/transaction.hpp"
|
|
#include "storage/v3/vertex.hpp"
|
|
#include "storage/v3/view.hpp"
|
|
#include "utils/concepts.hpp"
|
|
|
|
namespace memgraph::storage::v3 {
|
|
|
|
inline VertexData *GetDeltaHolder(Vertex *vertex) { return &vertex->second; }
|
|
|
|
inline Edge *GetDeltaHolder(Edge *edge) { return edge; }
|
|
|
|
/// This function iterates through the undo buffers from an object (starting
|
|
/// from the supplied delta) and determines what deltas should be applied to get
|
|
/// the currently visible version of the object. When the function finds a delta
|
|
/// that should be applied it calls the callback function with the delta that
|
|
/// should be applied passed as a parameter to the callback. It is up to the
|
|
/// caller to apply the deltas.
|
|
template <typename TCallback>
|
|
inline void ApplyDeltasForRead(Transaction *transaction, const Delta *delta, View view, const TCallback &callback) {
|
|
// if the transaction is not committed, then its deltas have transaction_id for the timestamp, otherwise they have
|
|
// its commit timestamp set.
|
|
// This allows the transaction to see its changes even though it's committed.
|
|
const auto &commit_info = *transaction->commit_info;
|
|
while (delta != nullptr) {
|
|
const auto &delta_commit_info = *delta->commit_info;
|
|
auto cid = delta->command_id;
|
|
|
|
// For SNAPSHOT ISOLATION -> we can only see the changes which were committed before the start of the current
|
|
// transaction
|
|
//
|
|
// For READ COMMITTED -> we can only see the changes which are committed. Commit timestamps of
|
|
// uncommitted changes are set to the transaction id of the transaction that made the change. Transaction id is
|
|
// always higher than start or commit timestamps so we know if the timestamp is lower than the initial transaction
|
|
// id value, that the change is committed.
|
|
//
|
|
// For READ UNCOMMITTED -> we accept any change.
|
|
if ((transaction->isolation_level == IsolationLevel::SNAPSHOT_ISOLATION && delta_commit_info.is_locally_committed &&
|
|
delta_commit_info.start_or_commit_timestamp.logical_id < transaction->start_timestamp.logical_id) ||
|
|
(transaction->isolation_level == IsolationLevel::READ_COMMITTED && delta_commit_info.is_locally_committed) ||
|
|
(transaction->isolation_level == IsolationLevel::READ_UNCOMMITTED)) {
|
|
break;
|
|
}
|
|
|
|
// We shouldn't undo our newest changes because the user requested a NEW
|
|
// view of the database.
|
|
if (view == View::NEW && delta_commit_info.start_or_commit_timestamp == commit_info.start_or_commit_timestamp &&
|
|
cid <= transaction->command_id) {
|
|
break;
|
|
}
|
|
|
|
// We shouldn't undo our older changes because the user requested a OLD view
|
|
// of the database.
|
|
if (view == View::OLD && delta_commit_info.start_or_commit_timestamp == commit_info.start_or_commit_timestamp &&
|
|
delta->command_id < transaction->command_id) {
|
|
break;
|
|
}
|
|
|
|
// This delta must be applied, call the callback.
|
|
callback(*delta);
|
|
|
|
// Move to the next delta.
|
|
delta = delta->next;
|
|
}
|
|
}
|
|
|
|
/// This function prepares the object for a write. It checks whether there are
|
|
/// any serialization errors in the process (eg. the object can't be written to
|
|
/// from this transaction because it is being written to from another
|
|
/// transaction) and returns a `bool` value indicating whether the caller can
|
|
/// proceed with a write operation.
|
|
template <typename TObj>
|
|
requires utils::SameAsAnyOf<TObj, Edge, Vertex>
|
|
inline bool PrepareForWrite(Transaction *transaction, TObj *object) {
|
|
auto *delta_holder = GetDeltaHolder(object);
|
|
if (delta_holder->delta == nullptr) return true;
|
|
|
|
const auto &delta_commit_info = *delta_holder->delta->commit_info;
|
|
if (delta_commit_info.start_or_commit_timestamp == transaction->commit_info->start_or_commit_timestamp ||
|
|
(delta_commit_info.is_locally_committed &&
|
|
delta_commit_info.start_or_commit_timestamp < transaction->start_timestamp)) {
|
|
return true;
|
|
}
|
|
|
|
transaction->must_abort = true;
|
|
return false;
|
|
}
|
|
|
|
/// This function creates a `DELETE_OBJECT` delta in the transaction and returns
|
|
/// a pointer to the created delta. It doesn't perform any linking of the delta
|
|
/// and is primarily used to create the first delta for an object (that must be
|
|
/// a `DELETE_OBJECT` delta).
|
|
/// @throw std::bad_alloc
|
|
inline Delta *CreateDeleteObjectDelta(Transaction *transaction) {
|
|
return &transaction->deltas.emplace_back(Delta::DeleteObjectTag(), transaction->commit_info.get(), GetNextDeltaId(),
|
|
transaction->command_id);
|
|
}
|
|
|
|
/// This function creates a delta in the transaction for the object and links
|
|
/// the delta into the object's delta list.
|
|
/// @throw std::bad_alloc
|
|
template <typename TObj, class... Args>
|
|
requires utils::SameAsAnyOf<TObj, Edge, Vertex>
|
|
inline void CreateAndLinkDelta(Transaction *transaction, TObj *object, Args &&...args) {
|
|
auto delta = &transaction->deltas.emplace_back(std::forward<Args>(args)..., transaction->commit_info.get(),
|
|
GetNextDeltaId(), transaction->command_id);
|
|
auto *delta_holder = GetDeltaHolder(object);
|
|
|
|
// The operations are written in such order so that both `next` and `prev`
|
|
// chains are valid at all times. The chains must be valid at all times
|
|
// because garbage collection (which traverses the chains) is done
|
|
// concurrently (as well as other execution threads).
|
|
|
|
// 1. We need to set the next delta of the new delta to the existing delta.
|
|
// TODO(antaljanosbenjamin): clang-tidy detects (in my opinion a false positive) issue in
|
|
// `Shard::Accessor::CreateEdge`.
|
|
// NOLINTNEXTLINE(clang-analyzer-core.NullDereference)
|
|
delta->next = delta_holder->delta;
|
|
// 2. We need to set the previous delta of the new delta to the object.
|
|
delta->prev.Set(object);
|
|
// 3. We need to set the previous delta of the existing delta to the new
|
|
// delta. After this point the garbage collector will be able to see the new
|
|
// delta but won't modify it until we are done with all of our modifications.
|
|
if (delta_holder->delta) {
|
|
delta_holder->delta->prev.Set(delta);
|
|
}
|
|
// 4. Finally, we need to set the object's delta to the new delta. The garbage
|
|
// collector and other transactions will acquire the object lock to read the
|
|
// delta from the object. Because the lock is held during the whole time this
|
|
// modification is being done, everybody else will wait until we are fully
|
|
// done with our modification before they read the object's delta value.
|
|
delta_holder->delta = delta;
|
|
}
|
|
|
|
} // namespace memgraph::storage::v3
|