diff --git a/include/silk/util/error.h b/include/silk/util/error.h new file mode 100644 index 0000000..8cd16c3 --- /dev/null +++ b/include/silk/util/error.h @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace silk +{ + +/** + * Rich error description that travels alongside an errno-style code returned by a function. + * An Error is a stack of frames: the top frame is the most recent, the bottom is the innermost + * failure that started the chain. All frame storage (headers + message bytes) lives in a single + * arena owned by the Error. + * + * Errors are normally produced through the RETURN_ERROR / CHECK_ERROR / CHECK_BOOL macros, + * which push a frame and capture the call-site file and line. Functions take an Error * error + * out-parameter and the caller declares the Error on its own stack: + * + * Error error; + * int r = doWork(&error); + * if (r) { + * logger->log(error.format()); + * } + * + * Callers must hand in a clean Error. The macros only push: any pre-existing frames remain + * underneath the new top frame. Use clear between independent uses of the same Error + * (e.g., a loop dispatching independent operations). + * + * To walk the stack, follow the chain from top via next: + * + * for (const Error::Frame * frame = error.top(); frame; frame = error.next(frame)) { + * use(frame->code, frame->file, frame->line, frame->message()); + * } + * + * Frame pointers stay valid until the next push/clear/move/destroy on the owning Error. + */ +class Error +{ +public: + /** + * One frame stored in the arena. The message bytes follow inline immediately after the header + * and are always NUL-terminated (the byte one past the last message byte is '\0'), so callers + * may pass message().data() to C APIs expecting a NUL-terminated string. + */ + struct Frame + { + uint32_t nextOffset; // arena offset of the next-deeper frame, or SENTINEL at the bottom + uint32_t msgLen; + const char * file; + int code; + int line; + + /** Inline message bytes following this header (NUL-terminated; the NUL is not counted). */ + std::string_view message() const noexcept { return std::string_view(reinterpret_cast(this + 1), msgLen); } + }; + static_assert(sizeof(Frame) == 24); + + /** Maximum length of a single frame's message in bytes; longer messages are truncated. */ + static constexpr size_t MAX_MESSAGE_LEN = 512; + + /** Construct an empty Error with no frames; no heap allocation until the first push. */ + Error() noexcept = default; + + // non-copyable + Error(const Error &) = delete; + Error & operator=(const Error &) = delete; + + // moveable; source is left empty (no frames, freshly default-constructed state) + Error(Error && other) noexcept + : arena(std::move(other.arena)) + , topOffset(std::exchange(other.topOffset, SENTINEL)) + { + } + Error & operator=(Error && other) noexcept + { + arena = std::move(other.arena); + topOffset = std::exchange(other.topOffset, SENTINEL); + return *this; + } + + /** True if no frames have been pushed yet. */ + bool empty() const noexcept { return topOffset == SENTINEL; } + + /** errno-style code of the top frame; 0 if empty. */ + int code() const noexcept { return topOffset == SENTINEL ? 0 : frameAt(topOffset)->code; } + + /** Pointer to the top of the stack (most recent frame); nullptr if empty. */ + const Frame * top() const noexcept { return topOffset == SENTINEL ? nullptr : frameAt(topOffset); } + + /** Pointer to the next-deeper frame; nullptr if frame is the innermost. */ + const Frame * next(const Frame * frame) const noexcept { return frame->nextOffset == SENTINEL ? nullptr : frameAt(frame->nextOffset); } + + /** Push one frame on top of the existing stack with a plain message. Returns code. */ + int push(int code, const char * file, int line, std::string_view message) noexcept; + + /** printf-style variant of push: vsnprintf's format + varargs into the frame's message. */ + int pushf(int code, const char * file, int line, const char * format, ...) noexcept __attribute__((format(printf, 5, 6))); + + /** + * Render the stack as one human-readable string. The top frame prints first; deeper frames are + * prefixed with "caused by: ". Each frame is :: (errno=[: + * ]). Best-effort: returns whatever was built so far if allocation fails partway. + */ + std::string format() const noexcept; + + /** Discard all frames. */ + void clear() noexcept + { + arena.clear(); + topOffset = SENTINEL; + } + +private: + /** Marker stored in topOffset / Frame::nextOffset to indicate "no frame". */ + static constexpr uint32_t SENTINEL = UINT32_MAX; + + /** Reserve space for a new top frame; returns a pointer to the message slot, or nullptr on OOM. */ + char * reserve(int code, const char * file, int line, size_t msgLen) noexcept; + + /** Interpret arena bytes at offset as a Frame. */ + const Frame * frameAt(uint32_t offset) const noexcept { return reinterpret_cast(arena.data() + offset); } + + // + // State. + // + + std::vector arena; + uint32_t topOffset = SENTINEL; +}; + +} // namespace silk + +/** + * Unconditional return-and-push: push a fresh frame on top of *error and return code. Captures + * the call-site file and line. Use when an error condition is detected directly (not propagated + * from a callee returning a code): + * + * if (input == nullptr) { + * RETURN_ERROR(EINVAL, error, "null input: idx=%d", idx); + * } + */ +#define RETURN_ERROR(code, error, msg, ...) return (error)->push##__VA_OPT__(f)((code), __FILE__, __LINE__, msg __VA_OPT__(, ) __VA_ARGS__) + +/** + * Failure propagation: if r is non-zero, push a new frame on top of *error and return r. + * Works uniformly for callees that populate *error (the existing stack is preserved underneath) + * and for callees that do not (the frame becomes the leaf). Callers must hand in a clean Error; + * see the class doc. + * + * int r = volume->allocateLsn(&lsn); + * CHECK_ERROR(r, error, "could not allocate LSN"); + * + * int r = store->getRowBlockReference(rbn, time, rowFunctions, &reference, error); + * CHECK_ERROR(r, error, "could not get row-block reference: pgId=%u", rbn.pgId); + */ +#define CHECK_ERROR(code, error, msg, ...) \ + do \ + { \ + if ((code) != 0) [[unlikely]] \ + { \ + return (error)->push##__VA_OPT__(f)((code), __FILE__, __LINE__, msg __VA_OPT__(, ) __VA_ARGS__); \ + } \ + } while (0) + +/** + * Boolean failure: if cond is false, push a fresh frame on top of *error and return code. + * + * NEVER embed a function call as cond -- capture into a bool b temp first: + * bool b = record.addRow(&context, recordRow); + * CHECK_BOOL(b, ENOSPC, error, "could not append row"); + */ +#define CHECK_BOOL(cond, code, error, msg, ...) \ + do \ + { \ + if (!(cond)) [[unlikely]] \ + { \ + return (error)->push##__VA_OPT__(f)((code), __FILE__, __LINE__, msg __VA_OPT__(, ) __VA_ARGS__); \ + } \ + } while (0) diff --git a/src/util/error.cpp b/src/util/error.cpp new file mode 100644 index 0000000..34ad5cc --- /dev/null +++ b/src/util/error.cpp @@ -0,0 +1,123 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace silk +{ + +// std::vector::data comes from ::operator new, which on hosted implementations is +// aligned to __STDCPP_DEFAULT_NEW_ALIGNMENT__. We need that to be at least alignof(Frame) so that +// Frames placed at offset 0 are aligned; record-padding inside reserve keeps subsequent +// Frames aligned as well. +static_assert(__STDCPP_DEFAULT_NEW_ALIGNMENT__ >= alignof(Error::Frame), "arena base must satisfy Frame alignment"); + +char * Error::reserve(int code, const char * file, int line, size_t msgLen) noexcept +{ + // Record size is the Frame header + message + 1 byte reserved for vsnprintf's NUL terminator + // (unused on plain-message paths). Pad to 8 bytes so consecutive Frames stay 8-aligned. + size_t recordSize = sizeof(Frame) + msgLen + 1; + size_t alignedRecordSize = alignUp(recordSize, 8); + + size_t newOffset = arena.size(); + if (newOffset >= SENTINEL) + { + return nullptr; + } + + try + { + arena.resize(newOffset + alignedRecordSize); + } + catch (...) + { + return nullptr; + } + + Frame * frame = std::construct_at( + reinterpret_cast(arena.data() + newOffset), Frame{topOffset, static_cast(msgLen), file, code, line}); + + topOffset = static_cast(newOffset); + return reinterpret_cast(frame + 1); +} + +int Error::push(int code, const char * file, int line, std::string_view message) noexcept +{ + size_t msgLen = std::min(message.size(), MAX_MESSAGE_LEN); + + char * dest = reserve(code, file, line, msgLen); + if (dest) + { + std::memcpy(dest, message.data(), msgLen); + dest[msgLen] = '\0'; + } + return code; +} + +int Error::pushf(int code, const char * file, int line, const char * format, ...) noexcept +{ + char buffer[MAX_MESSAGE_LEN + 1]; + + std::va_list args; + va_start(args, format); + int written = std::vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + + if (written < 0) + { + written = 0; + } + + return push(code, file, line, std::string_view(buffer, static_cast(written))); +} + +std::string Error::format() const noexcept +{ + std::string result; + try + { + bool first = true; + for (const Frame * frame = top(); frame; frame = next(frame)) + { + if (!first) + { + result += "\n caused by: "; + } + first = false; + + if (frame->file) + { + result += frame->file; + result += ':'; + result += std::to_string(frame->line); + result += ": "; + } + + std::string_view message = frame->message(); + result.append(message.data(), message.size()); + + result += " (errno="; + result += std::to_string(frame->code); + const char * description = strerrordesc_np(frame->code); + if (description) + { + result += ": "; + result += description; + } + result += ')'; + } + } + catch (...) + { + } + return result; +} + +} // namespace silk diff --git a/src/util/tests/error-test.cpp b/src/util/tests/error-test.cpp new file mode 100644 index 0000000..37ac6bb --- /dev/null +++ b/src/util/tests/error-test.cpp @@ -0,0 +1,274 @@ +#include + +#include + +#include +#include +#include +#include + +namespace silk +{ +namespace +{ + +int returnLeaf(Error * error, int * macroLine) +{ + *macroLine = __LINE__ + 1; + RETURN_ERROR(EIO, error, "disk failure"); + return 0; +} + +int returnLeafFormatted(Error * error, int * macroLine) +{ + *macroLine = __LINE__ + 1; + RETURN_ERROR(EIO, error, "could not open %s: errno=%d", "foo.txt", 42); + return 0; +} + +int checkErrorPropagates(int innerCode, Error * error, int * macroLine) +{ + int r = innerCode; + *macroLine = __LINE__ + 1; + CHECK_ERROR(r, error, "could not allocate LSN"); + return 0; +} + +int checkErrorFormatted(int innerCode, Error * error, int * macroLine) +{ + int r = innerCode; + *macroLine = __LINE__ + 1; + CHECK_ERROR(r, error, "could not insert record: pgId=%u", 7u); + return 0; +} + +int checkErrorStacks(Error * error, int * macroLine) +{ + int r = error->push(EIO, "log.cpp", 11, "ring buffer full"); + *macroLine = __LINE__ + 1; + CHECK_ERROR(r, error, "log write rejected at lsn=%lu", 42UL); + return 0; +} + +int checkBoolPropagates(bool cond, Error * error, int * macroLine) +{ + *macroLine = __LINE__ + 1; + CHECK_BOOL(cond, ENOSPC, error, "could not append row"); + return 0; +} + +bool endsWith(std::string_view text, std::string_view suffix) +{ + if (text.size() < suffix.size()) + { + return false; + } + return text.substr(text.size() - suffix.size()) == suffix; +} + +} + +TEST(ErrorTest, DefaultConstructedIsEmpty) +{ + Error error; + bool empty = error.empty(); + ASSERT_TRUE(empty); + + int code = error.code(); + ASSERT_EQ(code, 0); + + const Error::Frame * top = error.top(); + ASSERT_EQ(top, nullptr); +} + +TEST(ErrorTest, ClearDiscardsFrames) +{ + Error error; + error.push(EIO, "demo.cpp", 1, "disk failure"); + + error.clear(); + + bool empty = error.empty(); + ASSERT_TRUE(empty); +} + +TEST(ReturnErrorTest, WritesLeafAndReturnsCode) +{ + Error error; + int macroLine = 0; + int r = returnLeaf(&error, ¯oLine); + ASSERT_EQ(r, EIO); + + int code = error.code(); + ASSERT_EQ(code, EIO); + + const Error::Frame * top = error.top(); + ASSERT_NE(top, nullptr); + + int topCode = top->code; + ASSERT_EQ(topCode, EIO); + + std::string_view topMessage = top->message(); + ASSERT_EQ(topMessage, "disk failure"); + + int topLine = top->line; + ASSERT_EQ(topLine, macroLine); + + const char * topFile = top->file; + ASSERT_TRUE(endsWith(topFile, "error-test.cpp")); + + const Error::Frame * deeper = error.next(top); + ASSERT_EQ(deeper, nullptr); +} + +TEST(ReturnErrorTest, FormatsMessage) +{ + Error error; + int macroLine = 0; + int r = returnLeafFormatted(&error, ¯oLine); + ASSERT_EQ(r, EIO); + + const Error::Frame * top = error.top(); + std::string_view message = top->message(); + ASSERT_EQ(message, "could not open foo.txt: errno=42"); + + int line = top->line; + ASSERT_EQ(line, macroLine); +} + +TEST(CheckErrorTest, FallsThroughOnSuccess) +{ + Error error; + int macroLine = 0; + int r = checkErrorPropagates(0, &error, ¯oLine); + ASSERT_EQ(r, 0); + + bool empty = error.empty(); + ASSERT_TRUE(empty); +} + +TEST(CheckErrorTest, PropagatesLeafFailure) +{ + Error error; + int macroLine = 0; + int r = checkErrorPropagates(EIO, &error, ¯oLine); + ASSERT_EQ(r, EIO); + + const Error::Frame * top = error.top(); + ASSERT_NE(top, nullptr); + + int code = top->code; + ASSERT_EQ(code, EIO); + + std::string_view message = top->message(); + ASSERT_EQ(message, "could not allocate LSN"); + + int line = top->line; + ASSERT_EQ(line, macroLine); + + const Error::Frame * deeper = error.next(top); + ASSERT_EQ(deeper, nullptr); +} + +TEST(CheckErrorTest, FormatsLeafFailure) +{ + Error error; + int macroLine = 0; + int r = checkErrorFormatted(EBADF, &error, ¯oLine); + ASSERT_EQ(r, EBADF); + + const Error::Frame * top = error.top(); + std::string_view message = top->message(); + ASSERT_EQ(message, "could not insert record: pgId=7"); + + int line = top->line; + ASSERT_EQ(line, macroLine); +} + +TEST(CheckErrorTest, StacksOnTopOfExistingFrame) +{ + Error error; + int macroLine = 0; + int r = checkErrorStacks(&error, ¯oLine); + ASSERT_EQ(r, EIO); + + const Error::Frame * top = error.top(); + ASSERT_NE(top, nullptr); + + int topCode = top->code; + ASSERT_EQ(topCode, EIO); + + std::string_view topMessage = top->message(); + ASSERT_EQ(topMessage, "log write rejected at lsn=42"); + + int topLine = top->line; + ASSERT_EQ(topLine, macroLine); + + const Error::Frame * bottom = error.next(top); + ASSERT_NE(bottom, nullptr); + + int bottomCode = bottom->code; + ASSERT_EQ(bottomCode, EIO); + + std::string_view bottomMessage = bottom->message(); + ASSERT_EQ(bottomMessage, "ring buffer full"); + + const Error::Frame * endOfChain = error.next(bottom); + ASSERT_EQ(endOfChain, nullptr); +} + +TEST(CheckBoolTest, FallsThroughOnTrue) +{ + Error error; + int macroLine = 0; + int r = checkBoolPropagates(true, &error, ¯oLine); + ASSERT_EQ(r, 0); + + bool empty = error.empty(); + ASSERT_TRUE(empty); +} + +TEST(CheckBoolTest, PropagatesOnFalse) +{ + Error error; + int macroLine = 0; + int r = checkBoolPropagates(false, &error, ¯oLine); + ASSERT_EQ(r, ENOSPC); + + const Error::Frame * top = error.top(); + int code = top->code; + ASSERT_EQ(code, ENOSPC); + + std::string_view message = top->message(); + ASSERT_EQ(message, "could not append row"); + + int line = top->line; + ASSERT_EQ(line, macroLine); +} + +TEST(FormatTest, SingleFrameRendersAllFields) +{ + Error error; + error.push(EIO, "demo.cpp", 42, "disk failure"); + + std::string formatted = error.format(); + std::string expected = std::string("demo.cpp:42: disk failure (errno=") + std::to_string(EIO) + ": " + std::strerror(EIO) + ")"; + ASSERT_EQ(formatted, expected); +} + +TEST(FormatTest, StackRendersTopFirstWithCausedBy) +{ + Error error; + error.push(ENOSPC, "log.cpp", 201, "ring buffer full"); + error.push(ENOSPC, "volume.cpp", 89, "could not allocate LSN"); + error.push(EIO, "store.cpp", 142, "could not insert record: pgId=7"); + + std::string formatted = error.format(); + std::string expected = std::string("store.cpp:142: could not insert record: pgId=7 (errno=") + std::to_string(EIO) + ": " + + std::strerror(EIO) + ")\n caused by: volume.cpp:89: could not allocate LSN (errno=" + std::to_string(ENOSPC) + ": " + + std::strerror(ENOSPC) + ")\n caused by: log.cpp:201: ring buffer full (errno=" + std::to_string(ENOSPC) + ": " + + std::strerror(ENOSPC) + ")"; + ASSERT_EQ(formatted, expected); +} + +} // namespace silk