Files
LithosAnanake/kernel/src/log_region.c
T
rajamesandClaude Opus 5.5 4da370f492 feat(log): LOG-FLUSH -- a VM's log records reach Artemis's log ring; and the ring keeps its order when it grows
FABRIC-3 SXXXII.4's deferred LOG-FLUSH, built to the 2026-10-14 rulings
(SXXXVIII): everything a VM logs, at or above the log level, goes into its
own buffer; it goes to Artemis on LOG-FLUSH, when full, and at once on an
ERROR, as one kernel-Hermes LOG_BATCH message she writes on her own turn,
the source being the sender the kernel stamped.  Records that cannot go yet
are counted and reported with the next flush.  LOG-SHOW (Hera only) reads
the ring back -- it had no reader.

Found by the first test that filled it: ring growth overwrote the oldest
record and put blank slots inside the live range.  Fixed test first.

Three ISAs: POST 1052 passed; new parity baseline word_count=526
hash=0x82f4ae7cb2e2d424 (deliberate).  CLAUDE.md: Phase 8 PKI closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:10:01 -04:00

208 lines
9.1 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* log_region.c - Growable per-VM log-persistence ring on Artemis's disk.
* See starkernel/log_region.h for the format/interface design.
*
* Deliberately never calls log_message() (or anything documented to call
* it) -- this runs inside Artemis's own dictionary context during message
* delivery (LOG-APPEND, dispatched by a sender's MSG-SEND/MSG-TICK); a log
* call here would append to Artemis's own buffer and eventually flush back
* through this same path.
*/
#include "starkernel/log_region.h"
#include <stddef.h>
#include <string.h>
#include "block_subsystem.h" /* blk_meta_zone_read/write, compute_crc64 */
static uint64_t log_region_ctrl_compute_crc(const log_region_ctrl_t *ctrl) {
size_t crc_span = offsetof(log_region_ctrl_t, hdr_crc);
return compute_crc64((const uint8_t *)ctrl, crc_span);
}
/* Reads the control header; returns 0 and *out populated only if magic,
* version, and CRC all check out. Any other result (blank, foreign,
* corrupt, or a read failure) is treated identically by the caller: "not
* yet initialized," matching genesis_marker_read()'s own discipline. */
static int log_region_ctrl_read(log_region_ctrl_t *out) {
if (blk_meta_zone_read(LOG_REGION_DEVBLOCK_FROM_TOP_BASE, (uint8_t *)out) != 0) return -1;
if (LOG_REGION_GET_MAGIC(out->magic) != (uint32_t)(LOG_REGION_MAGIC & 0xFFFFFFFFull)) return -1;
if (LOG_REGION_GET_VERSION(out->magic) != LOG_REGION_VERSION_0) return -1;
uint64_t want_crc = log_region_ctrl_compute_crc(out);
if (want_crc != out->hdr_crc) return -1;
return 0;
}
static int log_region_ctrl_write(log_region_ctrl_t *ctrl) {
ctrl->hdr_crc = log_region_ctrl_compute_crc(ctrl);
return blk_meta_zone_write(LOG_REGION_DEVBLOCK_FROM_TOP_BASE, (const uint8_t *)ctrl) == 0 ? 0 : -1;
}
static void log_region_ctrl_init_fresh(log_region_ctrl_t *ctrl) {
memset(ctrl, 0, sizeof(*ctrl));
ctrl->magic = LOG_REGION_PACK(LOG_REGION_VERSION_0);
ctrl->devblocks = LOG_REGION_INITIAL_DEVBLOCKS;
ctrl->head_slot = 0;
ctrl->tail_slot = 0;
ctrl->record_count = 0;
}
/* Reads/writes the devblock holding slot_index (LOG_SLOTS_PER_DEVBLOCK
* slots per devblock), one devblock_from_top past the control header for
* every LOG_SLOTS_PER_DEVBLOCK slots. */
static uint32_t log_region_slot_devblock_from_top(uint32_t slot_index) {
return LOG_REGION_DEVBLOCK_FROM_TOP_BASE + 1u + (slot_index / LOG_SLOTS_PER_DEVBLOCK);
}
static int log_region_read_slot(uint32_t slot_index, log_slot_t *out) {
uint8_t devblock_buf[4096];
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK;
if (blk_meta_zone_read(dft, devblock_buf) != 0) return -1;
memcpy(out, devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, sizeof(*out));
return 0;
}
static int log_region_write_slot(uint32_t slot_index, const log_slot_t *slot) {
uint8_t devblock_buf[4096];
uint32_t dft = log_region_slot_devblock_from_top(slot_index);
uint32_t slot_in_devblock = slot_index % LOG_SLOTS_PER_DEVBLOCK;
/* Read-modify-write: blk_meta_zone_*() only operates at whole-devblock
* granularity, and a devblock holds LOG_SLOTS_PER_DEVBLOCK slots, so a
* single-slot write must preserve its siblings. A read failure on a
* never-yet-written devblock (blank fence content) is not fatal here --
* proceed with a zeroed buffer: blank fence content is
* indistinguishable from absent, and is treated as absent. */
if (blk_meta_zone_read(dft, devblock_buf) != 0) {
memset(devblock_buf, 0, sizeof(devblock_buf));
}
memcpy(devblock_buf + (size_t)slot_in_devblock * LOG_SLOT_SIZE, slot, sizeof(*slot));
return blk_meta_zone_write(dft, devblock_buf) == 0 ? 0 : -1;
}
int log_region_append(uint8_t level, uint64_t timestamp,
const char *source, uint32_t source_len,
const char *msg, uint32_t msg_len) {
log_region_ctrl_t ctrl;
if (log_region_ctrl_read(&ctrl) != 0) {
log_region_ctrl_init_fresh(&ctrl);
}
uint32_t total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
if (ctrl.record_count >= total_slots) {
if (ctrl.devblocks < LOG_REGION_MAX_DEVBLOCKS) {
/* Grow: new slots appear at indices [old_total .. new_total-1].
* The ring is full here, so head == tail, and in ring order the
* records run head..old_total-1 then 0..head-1. The new slots
* must come after the newest record and before the oldest, so
* the records from head on move up by the slots added, and the
* next write goes into the gap they leave (or, with head at 0,
* simply at old_total). Until 2026-10-14 nothing moved and
* tail stayed put: the next write overwrote the oldest record,
* and the blank new slots read back as records inside the live
* range (FABRIC-3.md SXXXVIII -- first exercised by LOG-FLUSH). */
uint32_t old_total = total_slots, added, i;
ctrl.devblocks += LOG_REGION_GROWTH_INCREMENT;
if (ctrl.devblocks > LOG_REGION_MAX_DEVBLOCKS) ctrl.devblocks = LOG_REGION_MAX_DEVBLOCKS;
total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
added = total_slots - old_total;
if (ctrl.head_slot == 0) {
ctrl.tail_slot = old_total;
} else {
for (i = old_total; i-- > ctrl.head_slot; ) {
log_slot_t moving;
if (log_region_read_slot(i, &moving) != 0) return -1;
if (log_region_write_slot(i + added, &moving) != 0) return -1;
}
ctrl.tail_slot = ctrl.head_slot;
ctrl.head_slot += added;
}
}
if (ctrl.record_count >= total_slots) {
/* Still full (growth ceiling already reached) -- evict oldest,
* unless doing so would discard higher-priority history for a
* lower-priority newcomer (FABRIC-3.md §XXXII.4, 2026-09-15:
* plain FIFO eviction here was flagged as level-blind -- a
* flood of INFO/DEBUG noise could push out real ERROR/WARN
* records). Read the oldest slot's own level before evicting:
* if it's ERROR/WARN and the incoming record is not, protect
* it -- drop the incoming record instead of evicting real
* history. Any other combination (oldest is already low
* priority, or the incoming record is itself ERROR/WARN)
* evicts exactly as before. A read failure on the oldest slot
* (should not happen -- it was written by this same function)
* falls back to the prior unconditional-eviction behavior
* rather than wedging the ring. */
log_slot_t oldest;
int oldest_protected = 0;
if (log_region_read_slot(ctrl.head_slot, &oldest) == 0) {
oldest_protected = (oldest.level <= LOG_REGION_PROTECTED_MAX_LEVEL);
}
int incoming_protected = (level <= LOG_REGION_PROTECTED_MAX_LEVEL);
if (oldest_protected && !incoming_protected) {
return 1; /* dropped by design, not a failure -- see header */
}
ctrl.head_slot = (ctrl.head_slot + 1u) % total_slots;
ctrl.record_count--;
}
}
log_slot_t slot;
memset(&slot, 0, sizeof(slot));
slot.timestamp = timestamp;
slot.level = level;
uint32_t src_n = source_len;
if (src_n > LOG_SLOT_SOURCE_MAX) src_n = LOG_SLOT_SOURCE_MAX;
memcpy(slot.source, source, src_n);
uint32_t msg_n = msg_len;
if (msg_n > LOG_SLOT_MSG_MAX) msg_n = LOG_SLOT_MSG_MAX;
memcpy(slot.msg, msg, msg_n);
slot.msg_len = (uint16_t)msg_n;
if (log_region_write_slot(ctrl.tail_slot, &slot) != 0) return -1;
ctrl.tail_slot = (ctrl.tail_slot + 1u) % total_slots;
if (ctrl.record_count < total_slots) ctrl.record_count++;
return log_region_ctrl_write(&ctrl);
}
int log_region_read_recent(uint32_t back, log_slot_t *out) {
log_region_ctrl_t ctrl;
uint32_t total_slots;
if (!out) return -1;
if (log_region_ctrl_read(&ctrl) != 0) return -1;
if (back >= ctrl.record_count) return 1;
total_slots = ctrl.devblocks * LOG_SLOTS_PER_DEVBLOCK;
if (total_slots == 0) return 1;
return log_region_read_slot((ctrl.tail_slot + total_slots - 1u - back) % total_slots, out);
}