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>
208 lines
9.1 KiB
C
208 lines
9.1 KiB
C
/*
|
||
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);
|
||
}
|