Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ sections to include in release notes:

### Removed

- Schema version from restart checkpoint format; model version is sufficient (#338)

### Git SHA

## **SIPNET 2.1.0 - "Nitrogen Cycle, Methane, and Restart"**
Expand Down
19 changes: 9 additions & 10 deletions docs/developer-guide/restart-checkpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ On resume, SIPNET executes:
3. Validate compatibility checks and restart boundary checks
4. Continue run from resumed climate input

## Restart Schema v1.0 Overview
## Restart Checkpoint Overview

Checkpoint format is ASCII text with one key/value per line:

- header: `SIPNET_RESTART 1.0`
- header: `SIPNET_RESTART`
- metadata: `meta_info.model_version`, `meta_info.build_info`, `meta_info.checkpoint_utc_epoch`, `meta_info.processed_steps`
- schema layout guard metadata: `schema_layout.envi_size`, `schema_layout.trackers_size`, `schema_layout.phenology_trackers_size`, `schema_layout.event_trackers_size`
Comment thread
re2zero marked this conversation as resolved.
- mode flags: `flags.*`
Expand All @@ -44,7 +44,6 @@ Example checkpoint content is exercised in
On load, SIPNET enforces the following. Lines that start with (warning) log a warning and do not error.

- magic header match
- schema version match
- model numeric version match
- `schema_layout.*` values exactly match the expected struct sizes for the running build
- (warning) build info mismatch
Expand Down Expand Up @@ -75,20 +74,20 @@ If you add saved state or change an existing saved payload:

1. Update the serialized payload type and restart read/write logic in `src/sipnet/restart.c`.
2. Update the `RESTART_SCHEMA_LAYOUT_*` constants, static asserts, and runtime schema-layout validation.
3. Update restart docs/tests and bump `RESTART_SCHEMA_VERSION`.
3. Update restart docs/tests.

## Struct Drift Guards

Restart schema v1.0 includes compile-time and runtime drift guards so struct layout changes cannot silently pass:
Restart checkpoints include compile-time and runtime drift guards so struct layout changes cannot silently pass:

- Compile-time guards: `_Static_assert` checks in `src/sipnet/restart.c` for `Envi`, `Trackers`, `PhenologyTrackers`, `EventTrackers`, and expected number of model flags in `Context`.
- Runtime guards: `schema_layout.*` fields in each checkpoint are validated on load.
- Test guardrails: `tests/sipnet/test_restart_infrastructure/testRestartMVP.c` verifies schema layout keys are present and rejects tampered values.

## Schema Bump Checklist
## Schema Changes Checklist

When intentionally changing the restart schema version:
When intentionally changing the restart checkpoint format:

1. Update `src/sipnet/restart.c` in all schema touchpoints: `RESTART_SCHEMA_VERSION`, `RESTART_SCHEMA_LAYOUT_*`, `_Static_assert` layout guards, and checkpoint read/write + required-key validation logic.
2. Update restart examples/fixtures to the new header and key set, including the restart fixtures in `tests/sipnet/test_restart_infrastructure/testRestartMVP.c`.
3. Update docs that name schema version or key expectations: `docs/developer-guide/restart-checkpoint.md` and `docs/user-guide/running-sipnet.md`.
1. Update `src/sipnet/restart.c` in all schema touchpoints: `RESTART_SCHEMA_LAYOUT_*`, `_Static_assert` layout guards, and checkpoint read/write + required-key validation logic.
2. Update restart examples/fixtures to the new key set, including the restart fixtures in `tests/sipnet/test_restart_infrastructure/testRestartMVP.c`.
3. Update docs that name key expectations: `docs/developer-guide/restart-checkpoint.md` and `docs/user-guide/running-sipnet.md`.
49 changes: 19 additions & 30 deletions src/sipnet/restart.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,13 @@
#include "version.h"

#define RESTART_MAGIC "SIPNET_RESTART"
#define RESTART_SCHEMA_VERSION "1.0"
#define RESTART_FLOAT_EPSILON 1e-8

/*
* When the serialized restart contract changes, update:
* - Number of elements of changed payload struct (#defs immediately below)
* - RESTART_SCHEMA_LAYOUT_* constants and related runtime checks
* - restart read/write logic, docs, and restart tests
* - RESTART_SCHEMA_VERSION
*/

#define MODEL_VERSION_BUFFER_SIZE 32
Expand All @@ -49,19 +47,19 @@
(8 * NUM_EVENT_TRACKERS_FIELDS)

_Static_assert(sizeof(Envi) == RESTART_SCHEMA_LAYOUT_ENVI_SIZE,
"Restart schema 1.0 drift: Envi changed; bump restart schema "
"version and update schema_layout.* checks");
"Restart schema drift: Envi changed; update schema_layout.* "
"checks");
_Static_assert(sizeof(Trackers) == RESTART_SCHEMA_LAYOUT_TRACKERS_SIZE,
"Restart schema 1.0 drift: serialized trackers payload changed; "
"bump restart schema version and update schema_layout.* checks");
_Static_assert(
sizeof(PhenologyTrackers) == RESTART_SCHEMA_LAYOUT_PHENOLOGY_TRACKERS_SIZE,
"Restart schema 1.0 drift: PhenologyTrackers changed; bump restart "
"schema version and update schema_layout.* checks");
"Restart schema drift: serialized trackers payload changed; "
"update schema_layout.* checks");
_Static_assert(sizeof(PhenologyTrackers) ==
RESTART_SCHEMA_LAYOUT_PHENOLOGY_TRACKERS_SIZE,
"Restart schema drift: PhenologyTrackers changed; update "
"schema_layout.* checks");
_Static_assert(sizeof(EventTrackers) ==
RESTART_SCHEMA_LAYOUT_EVENT_TRACKERS_SIZE,
"Restart schema 1.0 drift: EventTrackers changed; bump restart "
"schema version and update schema_layout.* checks");
"Restart schema drift: EventTrackers changed; update "
"schema_layout.* checks");

#define NUM_CLIMATE_SIGNATURE_FIELDS 4
typedef struct RestartClimateSignature {
Expand Down Expand Up @@ -91,8 +89,8 @@ typedef struct RestartContextModelFlags {
static RestartContextModelFlags modelFlags;

_Static_assert(sizeof(RestartContextModelFlags) == NUM_CONTEXT_MODEL_FLAGS * 4,
"Restart schema 1.0 drift: Model flags changed; bump restart "
"schema version and update schema_layout.* checks");
"Restart schema drift: Model flags changed; update "
"schema_layout.* checks");

typedef enum StateFieldType {
FT_LONGLONG = 0,
Expand Down Expand Up @@ -387,10 +385,9 @@ static void validateSchemaLayoutValue(const char *restartIn, const char *key,
const char *value, long long expected) {
long long parsed = parseIntStrict(restartIn, key, value);
if (parsed != expected) {
logError(
"Restart schema layout mismatch in %s: key=%s found=%lld expected=%lld "
"(schema %s)\n",
restartIn, key, parsed, expected, RESTART_SCHEMA_VERSION);
logError("Restart schema layout mismatch in %s: key=%s found=%lld "
"expected=%lld\n",
restartIn, key, parsed, expected);
exit(EXIT_CODE_BAD_RESTART_PARAMETER);
}
}
Expand Down Expand Up @@ -542,21 +539,13 @@ static void readRestartState(const char *restartIn, RestartState *state,
}
checkLineLength(firstLine, strlen(firstLine), restartIn, in);

char magic[64];
char schemaVersion[16];
if (sscanf(firstLine, "%63s %15s", magic, schemaVersion) != 2) {
parseError(restartIn, "invalid header line", NULL);
}
// Strip trailing newline/carriage return for exact comparison
firstLine[strcspn(firstLine, "\r\n")] = '\0';
Comment on lines +542 to +543

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch


if (strcmp(magic, RESTART_MAGIC) != 0) {
if (strcmp(firstLine, RESTART_MAGIC) != 0) {
logError("Restart file %s has invalid magic header\n", restartIn);
exit(EXIT_CODE_BAD_RESTART_PARAMETER);
}
if (strcmp(schemaVersion, RESTART_SCHEMA_VERSION) != 0) {
logError("Restart schema mismatch in %s: found %s expected %s\n", restartIn,
schemaVersion, RESTART_SCHEMA_VERSION);
exit(EXIT_CODE_BAD_RESTART_PARAMETER);
}

int meanLength = meanNPP->length;
int *seenMeanValues = (int *)calloc((size_t)meanLength, sizeof(int));
Expand Down Expand Up @@ -727,7 +716,7 @@ static void writeRestartState(const char *restartOut, const RestartState *state,
FILE *out = openFile(restartOut, "w");

// Magic header
fprintf(out, "%s %s\n", RESTART_MAGIC, RESTART_SCHEMA_VERSION);
fprintf(out, "%s\n", RESTART_MAGIC);

// Schema batches
writeKeysBatch(out, state->metaPF, NUM_META_FIELDS);
Expand Down
37 changes: 1 addition & 36 deletions tests/sipnet/test_restart_infrastructure/testRestartMVP.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#include "utils/tUtils.h"

#define CHECKPOINT_FILE "run.restart"
#define RESTART_MAGIC_LINE "SIPNET_RESTART 1.0"
#define RESTART_MAGIC_LINE "SIPNET_RESTART"
#define MAX_LINE_LENGTH 1028

static int prepRunFiles(const char *climFile, const char *eventFile) {
Expand Down Expand Up @@ -475,40 +475,6 @@ static int testModelVersionMismatchFails(void) {
return status;
}

static int testSchemaMismatchFails(void) {
int status = 0;
int stepStatus = 0;
int rc;

logTest("Starting testSchemaMismatchFails\n");

runShell("rm -f run.out events.out run.restart *.log");

stepStatus = prepRunFiles("restart_segment1.clim", "events_segment1.in");
if (stepStatus) {
logTest("Failed to prepare files for schema mismatch test segment 1\n");
return stepStatus;
}
status |= (runModel("restart_seg1.in", "schema_mismatch_seg1.log") != 0);
status |= replaceFirstOccurrence(CHECKPOINT_FILE, "SIPNET_RESTART ",
"SIPNET_RESTART 1");

stepStatus = prepRunFiles("restart_segment2.clim", "events_segment2.in");
if (stepStatus) {
logTest("Failed to prepare files for schema mismatch test segment 2\n");
return status | stepStatus;
}

rc = runModel("restart_seg2.in", "schema_mismatch_seg2.log");
status |= (rc != EXIT_CODE_BAD_RESTART_PARAMETER);

if (status) {
logTest("testSchemaMismatchFails failed (rc=%d)\n", rc);
}

return status;
}

static int testSchemaLayoutMismatchFails(void) {
int status = 0;
int stepStatus = 0;
Expand Down Expand Up @@ -762,7 +728,6 @@ int run(void) {
status |= testRestartNotNearMidnightWarns();
status |= testMissingFinalNewlineCheckpointSucceeds();
status |= testModelVersionMismatchFails();
status |= testSchemaMismatchFails();
status |= testSchemaLayoutMismatchFails();
status |= testMeanValueIndexOutOfRangeFails();
status |= testNonFiniteRestartValuesFail();
Expand Down
Loading