Select Git revision
transaction.h
-
Timm Fitschen authoredTimm Fitschen authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
transaction.h 19.10 KiB
/*
* This file is a part of the CaosDB Project.
*
* Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
* Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#ifndef CAOSDB_TRANSACTION_H
#define CAOSDB_TRANSACTION_H
#include "boost/log/core/record.hpp" // for record
#include "boost/log/sources/record_ostream.hpp" // for basic_record_o...
#include "boost/preprocessor/seq/limits/enum_256.hpp" // for BOOST_PP_SEQ_E...
#include "boost/preprocessor/seq/limits/size_256.hpp" // for BOOST_PP_SEQ_S...
#include "caosdb/entity.h" // for Entity
#include "caosdb/logging.h"
#include "caosdb/entity/v1alpha1/main.grpc.pb.h" // for EntityTransactionSe...
#include "caosdb/entity/v1alpha1/main.pb.h" // for Entity, RetrieveReq...
#include "caosdb/transaction_status.h" // for TransactionStatus
#include "caosdb/status_code.h" // for StatusCode
#include "google/protobuf/util/json_util.h" // for MessageToJsonString, Jso...
#include <iterator>
// IWYU pragma: no_include <ext/alloc_traits.h>
#include <memory> // for shared_ptr, unique_ptr
#include <stdexcept>
#include <string> // for string
#include <vector> // for vector
/**
* Do all necessary checks and assure that another retrieval (by id or by
* query) can be added as a sub-request to a transaction.
*/
#define ASSERT_CAN_ADD_RETRIEVAL \
if (!IsStatus(TransactionStatus::INITIAL()) && \
!IsStatus(TransactionStatus::GO_ON())) { \
return StatusCode::TRANSACTION_STATUS_ERROR; \
} \
switch (this->transaction_type) { \
case NONE: \
this->transaction_type = TransactionType::READ_ONLY; \
break; \
case READ_ONLY: \
break; \
case MIXED_READ_AND_WRITE: \
break; \
default: \
CAOSDB_LOG_ERROR_AND_RETURN_STATUS( \
logger_name, StatusCode::TRANSACTION_TYPE_ERROR, \
"You cannot add a retrieval to this transaction because it has the " \
"wrong TransactionType.") \
}
/**
* Do all necessary checks and assure that another retrieval (by id or by
* query) can be added as a sub-request to a transaction.
*/
#define ASSERT_CAN_ADD_QUERY \
ASSERT_CAN_ADD_RETRIEVAL \
if (this->has_query) { \
CAOSDB_LOG_ERROR_AND_RETURN_STATUS( \
logger_name, StatusCode::UNSUPPORTED_FEATURE, \
"Currently the number of queries which can be processed in a single " \
"transaction is limitted to one."); \
}
/**
* Do all necessary checks and assure that another deletion can be added as a
* sub-request to a transaction.
*/
#define ASSERT_CAN_ADD_DELETION \
if (!IsStatus(TransactionStatus::INITIAL()) && \
!IsStatus(TransactionStatus::GO_ON())) { \
return StatusCode::TRANSACTION_STATUS_ERROR; \
} \
switch (this->transaction_type) { \
case NONE: \
this->transaction_type = TransactionType::DELETE; \
case DELETE: \
case MIXED_WRITE: \
case MIXED_READ_AND_WRITE: \
break; \
default: \
CAOSDB_LOG_ERROR_AND_RETURN_STATUS( \
logger_name, StatusCode::TRANSACTION_TYPE_ERROR, \
"You cannot add a deletion to this transaction because it has the " \
"wrong TransactionType.") \
}
/**
* Do all necessary checks and assure that another insertion can be added as a
* sub-request to a transaction.
*/
#define ASSERT_CAN_ADD_INSERTION \
if (!IsStatus(TransactionStatus::INITIAL()) && \
!IsStatus(TransactionStatus::GO_ON())) { \
return StatusCode::TRANSACTION_STATUS_ERROR; \
} \
switch (this->transaction_type) { \
case NONE: \
this->transaction_type = TransactionType::INSERT; \
case INSERT: \
case MIXED_WRITE: \
case MIXED_READ_AND_WRITE: \
break; \
default: \
CAOSDB_LOG_ERROR_AND_RETURN_STATUS( \
logger_name, StatusCode::TRANSACTION_TYPE_ERROR, \
"You cannot add an insertion to this transaction because it has the " \
"wrong TransactionType.") \
}
/**
* Do all necessary checks and assure that another update can be added as a
* sub-request to a transaction.
*/
#define ASSERT_CAN_ADD_UPDATE \
if (!IsStatus(TransactionStatus::INITIAL()) && \
!IsStatus(TransactionStatus::GO_ON())) { \
return StatusCode::TRANSACTION_STATUS_ERROR; \
} \
switch (this->transaction_type) { \
case NONE: \
this->transaction_type = TransactionType::INSERT; \
case INSERT: \
case MIXED_WRITE: \
case MIXED_READ_AND_WRITE: \
break; \
default: \
CAOSDB_LOG_ERROR_AND_RETURN_STATUS( \
logger_name, StatusCode::TRANSACTION_TYPE_ERROR, \
"You cannot add an update to this transaction because it has the " \
"wrong TransactionType.") \
} \
if (!entity->HasId()) { \
CAOSDB_LOG_ERROR_AND_RETURN_STATUS( \
logger_name, StatusCode::ORIGINAL_ENTITY_MISSING_ID, \
"You cannot update this entity without any id. Probably you did not " \
"retrieve it first? Entity updates should always start with the " \
"retrieval of the existing entity which may then be changed.") \
}
/**
* @brief Creation and execution of transactions.
* @author Timm Fitschen
* @date 2021-08-05
*/
namespace caosdb::transaction {
using caosdb::entity::Entity;
using ProtoEntity = caosdb::entity::v1alpha1::Entity;
using caosdb::entity::v1alpha1::EntityTransactionService;
using caosdb::entity::v1alpha1::FileDownloadResponse;
using caosdb::entity::v1alpha1::FileTransmissionService;
using caosdb::entity::v1alpha1::FileUploadRequest;
using caosdb::entity::v1alpha1::FileUploadResponse;
using caosdb::entity::v1alpha1::IdResponse;
using caosdb::entity::v1alpha1::MultiTransactionRequest;
using caosdb::entity::v1alpha1::MultiTransactionResponse;
using caosdb::entity::v1alpha1::RegisterFileDownloadRequest;
using caosdb::entity::v1alpha1::RegisterFileDownloadResponse;
using caosdb::entity::v1alpha1::RegisterFileUploadResponse;
using caosdb::transaction::TransactionStatus;
using WrappedResponseCase =
caosdb::entity::v1alpha1::TransactionResponse::WrappedResponseCase;
class Transaction;
static const std::string logger_name = "caosdb::transaction";
/**
* Abstract base class for the results of a Transaction.
*/
class ResultSet {
class iterator;
public:
virtual ~ResultSet() = default;
[[nodiscard]] virtual auto Size() const noexcept -> int = 0;
[[nodiscard]] virtual auto At(const int index) const -> const Entity & = 0;
auto begin() const -> iterator;
auto end() const -> iterator;
private:
class iterator : public std::iterator<std::output_iterator_tag, Entity> {
public:
explicit iterator(const ResultSet *result_set, int index = 0);
auto operator*() const -> const Entity &;
auto operator++() -> iterator &;
auto operator++(int) -> iterator;
auto operator!=(const iterator &rhs) const -> bool;
private:
int current_index = 0;
const ResultSet *result_set;
};
};
/**
* Container with results of a transaction.
*
* In contrast to UniqueResult, this one can also hold multiple entities or zero
* entities.
*/
class MultiResultSet : public ResultSet {
public:
~MultiResultSet() = default;
explicit MultiResultSet(std::vector<std::unique_ptr<Entity>> result_set);
[[nodiscard]] inline auto Size() const noexcept -> int override {
return this->entities.size();
}
[[nodiscard]] inline auto At(const int index) const
-> const Entity & override {
return *(this->entities.at(index));
}
std::vector<std::unique_ptr<Entity>> entities;
};
/**
* Container with the single result of a transaction.
*
* In contrast to MultiResultSet, this one guarantees to hold exactly one
* entity.
*/
class UniqueResult : public ResultSet {
public:
~UniqueResult() = default;
explicit inline UniqueResult(ProtoEntity *protoEntity)
: entity(new Entity(protoEntity)){};
explicit inline UniqueResult(IdResponse *idResponse)
: entity(new Entity(idResponse)){};
[[nodiscard]] auto GetEntity() const -> const Entity &;
[[nodiscard]] inline auto Size() const noexcept -> int override { return 1; }
[[nodiscard]] inline auto At(const int index) const
-> const Entity & override {
if (index != 0) {
throw std::out_of_range("Index out of range. Length is 1.");
}
return *(this->entity);
}
private:
std::unique_ptr<Entity> entity;
};
// TODO(fspreck) One possibility for count-queries: Transaction gets a
// GetQueryCount function that returns exactly this. In case of a
// single FIND Query this will be equivalent to the Size of the
// ResultSet, but it may be different in case of Query + RetrieveById,
// and in case of COUNT queries which have an empty ResultSet. We have
// to think about how to extend this to transactions with more than
// one query in the future. My suggestion is to treat a transaction
// with more than one query as the union of the query results in which
// case the query count would return the cumulative number of
// results. This would even allow the combination of FIND, FIND
// unique, and COUNT, albeit a little strangely.
/**
* @brief Create a transaction via `CaosDBConnection.createTransaction()`
*/
class Transaction {
public:
auto UploadFile(FileUploadResponse *response,
const std::string ®istration_id) -> void;
auto RegisterUploadFile(RegisterFileUploadResponse *response) -> void;
auto DownloadFile(FileDownloadResponse *response,
const RegisterFileDownloadResponse ®istration_response)
-> void;
auto RegisterDownloadFile(const RegisterFileDownloadRequest &request,
RegisterFileDownloadResponse *response) -> void;
/**
* The transaction type restricts the kind of sub-transaction which may be
* added to a transaction (insertion, update, deletion, retrieval).
*
* @note MIXED_READ_AND_WRITE and MIXED_WRITE transaction are not supported
* yet.
*/
enum TransactionType {
NONE, //!< Unspecified or not specified yet.
READ_ONLY, //!< Only retrievals (by id, by query)
INSERT, //!< Only insertions
UPDATE, //!< Only updates
DELETE, //!< Only deletions
MIXED_WRITE, //!< Only insertions, deletions, updates
MIXED_READ_AND_WRITE //!< all kind of transaction.
};
Transaction(std::shared_ptr<EntityTransactionService::Stub> entity_service,
std::shared_ptr<FileTransmissionService::Stub> file_service);
/**
* Add an entity id to this transaction for retrieval.
*
* The retrieval is being processed when the Execute() or
* ExecuteAsynchronously() methods of this transaction are called.
*/
auto RetrieveById(const std::string &id) noexcept -> StatusCode;
/**
* Add all entity ids to this transaction for retrieval.
*/
template <class InputIterator>
inline auto RetrieveById(InputIterator begin, InputIterator end) noexcept
-> StatusCode;
/**
* Add a query to this transaction.
*
* Currently, only FIND and COUNT queries are supported.
*/
auto Query(const std::string &query) noexcept -> StatusCode;
/**
* Add the entity to this transaction for an insertion.
*
* The insertion is being processed when the Execute() or
* ExecuteAsynchronously() methods of this transaction are called.
*
* Changing the entity afterwards results in undefined behavior.
*/
auto InsertEntity(Entity *entity) noexcept -> StatusCode;
/**
* Add the entity to this transaction for an update.
*
* The update is being processed when the Execute() or
* ExecuteAsynchronously() methods of this transaction are called.
*
* Changing the entity afterwards results in undefined behavior.
*/
auto UpdateEntity(Entity *entity) noexcept -> StatusCode;
/**
* Add an entity id to this transaction for deletion.
*
* The deletion is being processed when the Execute() or
* ExecuteAsynchronously() methods of this transaction are called.
*/
auto DeleteById(const std::string &id) noexcept -> StatusCode;
inline auto IsStatus(const TransactionStatus &status) const noexcept -> bool {
return this->status.GetCode() == status.GetCode();
};
/**
* Execute this transaction in blocking mode and return the status.
*/
auto Execute() -> TransactionStatus;
/**
* Execute this transaction in non-blocking mode and return immediately.
*
* A client may request the current status at any time via GetStatus().
*
* Use WaitForIt() to join the back-ground execution of this transaction.
*/
auto ExecuteAsynchronously() noexcept -> StatusCode;
/**
* Join the background execution and return the status when the execution
* terminates.
*
* Use this after ExecuteAsynchronously().
*/
[[nodiscard]] auto WaitForIt() const noexcept -> TransactionStatus;
/**
* Return the current status of the transaction.
*/
[[nodiscard]] inline auto GetStatus() const -> TransactionStatus {
return this->status;
}
[[nodiscard]] inline auto GetResultSet() const -> const ResultSet & {
const ResultSet *result_set = this->result_set.get();
return *result_set;
}
/**
* Return the result of a count query
*
* Only meaningful if there was exactly one COUNT query executed in
* this transaction. In all other cases, the return value will be
* -1.
*/
[[nodiscard]] inline auto GetCountResult() const -> int {
return query_count;
}
/**
* Return the number of sub-requests in this transaction.
*
* This is meant for debugging because the number of sub-requests is a
* GRPC-API detail.
*/
[[nodiscard]] inline auto GetRequestCount() const -> int {
return this->request->requests_size();
}
/**
* Get a JSON representation of the respone.
*
* For debugging.
*/
inline auto ResponseToString() const -> const std::string {
google::protobuf::util::JsonOptions options;
std::string out;
google::protobuf::util::MessageToJsonString(*this->response, &out, options);
return out;
}
/**
* Get a JSON representation of the request.
*
* For debugging.
*/
inline auto RequestToString() const -> const std::string {
google::protobuf::util::JsonOptions options;
std::string out;
google::protobuf::util::MessageToJsonString(*this->request, &out, options);
return out;
}
private:
bool has_query = false;
TransactionType transaction_type = TransactionType::NONE;
mutable std::unique_ptr<ResultSet> result_set;
mutable TransactionStatus status = TransactionStatus::INITIAL();
std::shared_ptr<EntityTransactionService::Stub> entity_service;
std::shared_ptr<FileTransmissionService::Stub> file_service;
MultiTransactionRequest *request;
mutable MultiTransactionResponse *response;
std::string error_message;
mutable long query_count;
};
template <class InputIterator>
inline auto Transaction::RetrieveById(InputIterator begin,
InputIterator end) noexcept
-> StatusCode {
ASSERT_CAN_ADD_RETRIEVAL
auto next = begin;
while (next != end) {
auto *sub_request = this->request->add_requests();
sub_request->mutable_retrieve_request()->set_id(*next);
next = std::next(next);
}
this->status = TransactionStatus::GO_ON();
return this->status.GetCode();
}
} // namespace caosdb::transaction
#endif