Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions category/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,16 @@ add_library(
"event/event_iterator.h"
"event/event_iterator_inline.h"
"event/event_metadata.h"
"event/event_recorder.cpp"
"event/event_recorder.h"
"event/event_recorder.hpp"
"event/event_recorder_inline.h"
"event/event_ring.c"
"event/event_ring.h"
"event/event_ring_util.c"
"event/event_ring_util.h"
"event/owned_event_ring.cpp"
"event/owned_event_ring.hpp"
"event/test_event_ctypes.h"
"event/test_event_ctypes_metadata.c"
# fiber
Expand Down
120 changes: 120 additions & 0 deletions category/core/event/event_recorder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (C) 2025 Category Labs, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#include <category/core/assert.h>
#include <category/core/config.hpp>
#include <category/core/event/event_recorder.h>
#include <category/core/event/event_recorder.hpp>
#include <category/core/event/event_ring.h>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <span>
#include <tuple>

#include <string.h>

MONAD_NAMESPACE_BEGIN

std::tuple<monad_event_descriptor *, std::byte *, uint64_t>
EventRecorder::setup_record_error_event(
uint16_t event_type, monad_event_record_error_type error_type,
size_t header_payload_size,
std::span<std::span<std::byte const> const> trailing_payload_bufs,
size_t original_payload_size)
{
monad_event_record_error *error_payload;
size_t error_payload_size;

switch (error_type) {
case MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB:
[[fallthrough]];
case MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE:
// When an event cannot be recorded due to its payload size, we still
// record the first 8 KiB of that payload; it may help with diagnosing
// the cause of the overflow, which is a condition that is not expected
// in normal operation
error_payload_size = RECORD_ERROR_TRUNCATED_SIZE;
break;
default:
error_payload_size = sizeof *error_payload;
break;
}

uint64_t seqno;
uint8_t *payload_buf;
monad_event_descriptor *const event = monad_event_recorder_reserve(
&recorder_, error_payload_size, &seqno, &payload_buf);
MONAD_ASSERT(event != nullptr, "non-overflow reservation must succeed");

event->event_type = 1;
error_payload = reinterpret_cast<monad_event_record_error *>(payload_buf);
error_payload->error_type = error_type;
error_payload->dropped_event_type = event_type;
error_payload->requested_payload_size = original_payload_size;

switch (error_type) {
case MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB:
[[fallthrough]];
case MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE: {
// In these cases, the payload area is set up like this:
//
// .----------------.-----------------------.---------------.
// | *error_payload | event header (type T) | truncated VLT |
// .----------------.-----------------------.---------------.
//
// The intention here is for the reader to be able to see some of
// the event that was discarded; we never expect these errors to
// happen, so recording as much information as possible may be
// important for debugging how our assumptions were wrong.
//
// The event header is written by the call site: we pass a pointer
// to it in the return value, and the caller writes to it as though
// the recording did not fail. The code below is responsible for
// writing the "variable-length trailing" (VLT) data, which is the
// only part that is truncated; an earlier assertion ensures that
// RECORD_ERROR_TRUNCATED_SIZE is large enough that the event
// header is never truncated. We do include the event header's size
// in the `error_payload->truncated_payload_size` field, however.
error_payload->truncated_payload_size =
RECORD_ERROR_TRUNCATED_SIZE - sizeof(*error_payload);
size_t const truncated_vlt_offset =
sizeof(*error_payload) + header_payload_size;
size_t residual_size =
RECORD_ERROR_TRUNCATED_SIZE - truncated_vlt_offset;
void *p = payload_buf + truncated_vlt_offset;
for (std::span<std::byte const> buf : trailing_payload_bufs) {
size_t const copy_len = std::min(residual_size, size(buf));
p = mempcpy(p, data(buf), copy_len);
residual_size -= copy_len;
if (residual_size == 0) {
break;
}
}
} break;

default:
error_payload->truncated_payload_size = 0;
break;
}

return {
event,
reinterpret_cast<std::byte *>(payload_buf) + sizeof *error_payload,
seqno};
}

MONAD_NAMESPACE_END
226 changes: 226 additions & 0 deletions category/core/event/event_recorder.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Copyright (C) 2025 Category Labs, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#pragma once

/**
* @file
*
* This file defines a C++ interface for recording to event rings
*/

#include <category/core/assert.h>
#include <category/core/config.hpp>
#include <category/core/event/event_recorder.h>
#include <category/core/event/event_ring.h>

#include <array>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <span>
#include <tuple>
#include <type_traits>
#include <utility>

#include <string.h>

MONAD_NAMESPACE_BEGIN

/// C++ event recording works in three steps: (1) reserving descriptor and
/// payload buffer space in an event ring, then (2) the user performs zero-copy
/// typed initialization of the payload directly in ring memory, then (3) the
/// result is committed to the event ring; this type connects all three steps
template <typename T>
struct ReservedEvent
{
monad_event_descriptor *event;
T *payload;
uint64_t seqno;
};

/// An interface to an event recorder that represents "reserve" and "commit"
/// semantics as the ReservedEvent<T> template type, and has convenient support
/// for capturing recording errors
class EventRecorder
{
public:
explicit EventRecorder(monad_event_recorder const &recorder) noexcept
: recorder_{recorder}
{
}

/// Reserve resources to record an event; T is the type of the "header"
/// payload, and U... is a variadic sequence of trailing payload buffers
/// of type `std::span<std::byte const>`, e.g., TXN_LOG records the log
/// header structure `struct monad_exec_txn_log` and two variadic byte
/// sequences (for topics and log data)
template <
typename T, typename EventEnum,
std::same_as<std::span<std::byte const>>... U>
requires std::is_enum_v<EventEnum>
[[nodiscard]] ReservedEvent<T> reserve_event(EventEnum, U...);

/// Commit the previously reserved event resources to the event ring
template <typename T>
void commit(ReservedEvent<T> const &);

static constexpr size_t RECORD_ERROR_TRUNCATED_SIZE = 1UL << 13;

protected:
alignas(64) monad_event_recorder recorder_;

/// Helper for creating a RECORD_ERROR event in place of the requested
/// event, which could not be recorded
std::tuple<monad_event_descriptor *, std::byte *, uint64_t>
setup_record_error_event(
uint16_t event_type, monad_event_record_error_type,
size_t header_payload_size,
std::span<std::span<std::byte const> const> payload_bufs,
size_t original_payload_size);
};

template <
typename T, typename EventEnum,
std::same_as<std::span<std::byte const>>... U>
requires std::is_enum_v<EventEnum>
ReservedEvent<T>
EventRecorder::reserve_event(EventEnum event_type, U... trailing_bufs)
{
// This is checking that, in the event of a recorder error, we could still
// fit the entire header event type T and the error reporting type in the
// maximum "truncated buffer" size allocated to report errors
static_assert(
sizeof(T) + sizeof(monad_event_record_error) <=
RECORD_ERROR_TRUNCATED_SIZE);

// This function does the following:
//
// - Reserves an event descriptor
//
// - Reserves payload buffer space to hold the event payload data type,
// which is a fixed-size, C-layout-compatible structure of type `T`;
// the caller will later initialize this memory, constructing their T
// instance within it
//
// - Also reserves (as part of the above allocation) payload buffer space
// for variable-length arrays that follow the `T` object in the event
// payload. For example, the topics and log data arrays for TXN_LOG
// are variable-length data that is copied immediately following the
// main `T = monad_c_eth_txn_log` payload structure; in this kind of
// event, the payload type `monad_c_eth_txn_log` is called the "header"
//
// All variable-length trailing data segments are passed to this function
// via the variadic list of arguments. They are treated as unstructured
// data and have type `std::span<std::byte const>`. After payload space is
// reserved for these byte arrays, they are also memcpy'd immediately.
//
// Events that do not have variable-length trailing data also use this
// function, with an empty `U` parameter pack.
//
// The reason variable-length data is memcpy'd immediately but the fixed
// sized part of the event payload (of type `T`) is not, is best explained
// by example. Consider this C++ type that models an Ethereum log:
//
// struct Log
// {
// byte_string data{};
// std::vector<bytes32_t> topics{};
// Address address{};
// }
//
// This type is not trivially copyable, but the underlying array elements
// in the `data` and `topics` array can be trivially copied.
//
// The corresponding C-layout-compatible type describing the log,
// `T = monad_c_eth_txn_log`, has to be manually initialized by the caller,
// so this function returns a `monad_c_eth_txn_log *` pointing to the
// payload buffer space for the caller to perform zero-copy initialization.
//
// We need to know the total size of the variable-length trailing data in
// order to reserve enough space for it; since the caller always knows what
// this data is, this function asks for the complete span rather than just
// the size, and also does the memcpy now. This simplifies the recording
// calls, and also the handling of the RECORD_ERROR type, which writes
// diagnostic truncated payloads on overflow

size_t const payload_size = (size(trailing_bufs) + ... + sizeof(T));
if (payload_size > std::numeric_limits<uint32_t>::max()) [[unlikely]] {
std::array<std::span<std::byte const>, sizeof...(trailing_bufs)> const
trailing_bufs_array = {trailing_bufs...};
auto const [event, header_buf, seqno] = setup_record_error_event(
std::to_underlying(event_type),
MONAD_EVENT_RECORD_ERROR_OVERFLOW_4GB,
sizeof(T),
trailing_bufs_array,
payload_size);
return {event, reinterpret_cast<T *>(header_buf), seqno};
}
if (payload_size >=
recorder_.payload_buf_mask + 1 - 2 * MONAD_EVENT_WINDOW_INCR) {
// The payload is smaller than the maximum possible size, but still
// cannot fit entirely in the event ring's payload buffer. For example,
// suppose we tried to allocate 300 MiB from a 256 MiB payload buffer.
//
// The event ring C API does not handle this as a special case;
// instead, the payload buffer's normal ring buffer expiration logic
// allows the allocation to "succeed" but it appears as expired
// immediately upon allocation (for the expiration logic, see the
// "Sliding window buffer" section of event_recorder.md).
//
// We treat this as a formal error so that the operator will know
// to allocate a (much) larger event ring buffer.
std::array<std::span<std::byte const>, sizeof...(trailing_bufs)> const
trailing_bufs_array = {trailing_bufs...};
auto const [event, header_buf, seqno] = setup_record_error_event(
std::to_underlying(event_type),
MONAD_EVENT_RECORD_ERROR_OVERFLOW_EXPIRE,
sizeof(T),
trailing_bufs_array,
payload_size);
return {event, reinterpret_cast<T *>(header_buf), seqno};
}

uint64_t seqno;
uint8_t *payload_buf;
monad_event_descriptor *const event = monad_event_recorder_reserve(
&recorder_, payload_size, &seqno, &payload_buf);
MONAD_DEBUG_ASSERT(event != nullptr);
if constexpr (sizeof...(trailing_bufs) > 0) {
// Copy the variable-length trailing buffers; GCC issues a false
// positive warning about this memcpy that must be disabled
#if !defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wstringop-overflow"
#pragma GCC diagnostic ignored "-Warray-bounds"
#endif
void *p = payload_buf + sizeof(T);
((p = mempcpy(p, data(trailing_bufs), size(trailing_bufs))), ...);
#if !defined(__clang__)
#pragma GCC diagnostic pop
#endif
}
event->event_type = std::to_underlying(event_type);
return {event, reinterpret_cast<T *>(payload_buf), seqno};
}

template <typename T>
void EventRecorder::commit(ReservedEvent<T> const &r)
{
monad_event_recorder_commit(r.event, r.seqno);
}

MONAD_NAMESPACE_END
Loading
Loading