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
4 changes: 3 additions & 1 deletion .github/workflows/durability-certification-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
GF_CERT_SEED: "7560"
GF_CERT_HISTORIES: ${{ github.event.inputs.histories || '64' }}
GF_CERT_OPS: ${{ github.event.inputs.ops || '32' }}
GRAPHFORGE_CERT_RUNNER: blacksmith-4vcpu-ubuntu-2404
GRAPHFORGE_CERT_FILESYSTEM_CLASS: hosted-linux-filesystem
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # master as of 2026-08-05
Expand All @@ -61,4 +63,4 @@ jobs:
name: durability-certification-evidence-${{ github.sha }}
path: ${{ runner.temp }}/durability-certification-evidence
if-no-files-found: error
retention-days: 1
retention-days: 14
43 changes: 41 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ jobs:
name: native-oracle-windows-${{ github.sha }}
path: ${{ runner.temp }}/durability-certification-evidence
if-no-files-found: error
retention-days: 1
retention-days: 14

- name: Run Windows injected publication error recovery test
shell: bash
Expand Down Expand Up @@ -977,7 +977,7 @@ jobs:
name: native-oracle-macos-${{ github.sha }}
path: ${{ runner.temp }}/durability-certification-evidence
if-no-files-found: error
retention-days: 1
retention-days: 14

- name: Run macOS injected publication error recovery test
run: >-
Expand All @@ -997,6 +997,43 @@ jobs:
project_checkpoints::tests::checkpoint_read_guard_unlocks_before_a_cloned_descriptor_closes
--no-fail-fast -- --exact --nocapture

native-durability-aggregate:
name: Native Durability Aggregate
runs-on: blacksmith-4vcpu-ubuntu-2404
needs:
- changes
- windows-graphforge-storage-locks
- macos-graphforge-storage-durability
if: always() && needs.changes.outputs.rust == 'true'
timeout-minutes: 10
steps:
- name: Checkout aggregate implementation
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download exact-SHA Windows oracle
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: native-oracle-windows-${{ github.sha }}
path: native/windows
- name: Download exact-SHA macOS oracle
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: native-oracle-macos-${{ github.sha }}
path: native/macos
- name: Validate and aggregate native durability evidence
run: >-
python3 scripts/ci/durability-certification-gate.py aggregate-native
--expected-sha "${{ github.sha }}"
--windows native/windows/native-oracle-windows.json
--macos native/macos/native-oracle-macos.json
--output native/native-durability-aggregate.json
- name: Upload exact-SHA native durability aggregate
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-durability-aggregate-${{ github.sha }}
path: native/native-durability-aggregate.json
if-no-files-found: error
retention-days: 14

bazel-bootstrap:
# Job display name kept for continuity; this is the authoritative Rust
# compile/test path under CI Gate after #4 cutover.
Expand Down Expand Up @@ -1280,6 +1317,7 @@ jobs:
- concurrency-matrix
- windows-graphforge-storage-locks
- macos-graphforge-storage-durability
- native-durability-aggregate
- bazel-bootstrap
steps:
- name: Checkout gate implementation
Expand All @@ -1300,4 +1338,5 @@ jobs:
"${{ needs.concurrency-matrix.result }}"
"${{ needs.windows-graphforge-storage-locks.result }}"
"${{ needs.macos-graphforge-storage-durability.result }}"
"${{ needs.native-durability-aggregate.result }}"
"${{ needs.bazel-bootstrap.result }}"
177 changes: 177 additions & 0 deletions crates/graphforge-api/src/durability_certification_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,52 @@

#![cfg(test)]

use std::collections::HashMap;

use arrow::array::{Array, StringArray};
use graphforge_storage::project_certification::{
CERT_CONTRACT, CERT_SEED, WRITE_SKEW_CLASSIFICATION, evidence_summary, run_certification_suite,
};
use graphforge_storage::{ProjectRetentionLimits, ProjectRetentionPolicy};
use uuid::Uuid;

use crate::{
CancellationToken, CheckpointRequest, GraphForge, GraphForgeOptions, OperationId,
ProjectWriteMode, PropValue, WriteContext,
};

const READ_NAMES: &str = "MATCH (n:CertPerson) RETURN n.name AS name ORDER BY name";

fn uuid7(seed: u8) -> Uuid {
let mut bytes = [0_u8; 16];
bytes[0] = seed;
bytes[6] = 0x70;
bytes[8] = 0x80;
Uuid::from_bytes(bytes)
}

fn context(seed: u8) -> WriteContext {
WriteContext {
operation_uuid: OperationId(uuid7(seed)),
actor_uuid: None,
}
}

fn names(graph: &GraphForge) -> Vec<String> {
let result = graph.execute(READ_NAMES).expect("certification query");
let mut names = Vec::new();
for batch in result.batches {
if batch.num_rows() == 0 {
continue;
}
let values = batch
.column_by_name("name")
.and_then(|array| array.as_any().downcast_ref::<StringArray>())
.expect("certification name column");
names.extend((0..values.len()).map(|row| values.value(row).to_owned()));
}
names
}

#[test]
fn seeded_certification_suite_is_clean_at_required_budget() {
Expand Down Expand Up @@ -39,3 +82,137 @@ fn write_skew_classification_remains_honest() {
assert_eq!(WRITE_SKEW_CLASSIFICATION, "allowed_documented_not_ssi");
assert!(!WRITE_SKEW_CLASSIFICATION.contains("serializable"));
}

#[test]
fn production_histories_observe_pinned_and_reopened_state_in_every_write_mode() {
for (mode_index, mode) in [
ProjectWriteMode::SingleWriter,
ProjectWriteMode::QueuedWriter,
ProjectWriteMode::OptimisticMultiWriter,
]
.into_iter()
.enumerate()
{
let project = tempfile::tempdir().expect("certification project");
let path = project.path().to_str().expect("utf-8 project path");
let options = GraphForgeOptions {
write_mode: mode,
..GraphForgeOptions::default()
};
let writer =
GraphForge::new_with_options(Some(path), options.clone()).expect("writer open");
let pinned = GraphForge::new_with_options(Some(path), options).expect("pinned open");
assert!(names(&pinned).is_empty());

let seed = u8::try_from(mode_index + 1).expect("bounded mode index");
let tx = writer.begin_transaction(context(seed)).expect("begin");
tx.stage_add_node(
uuid7(seed + 16),
"CertPerson",
HashMap::from([("name".into(), PropValue::Str(format!("mode-{mode_index}")))]),
)
.expect("stage production node");
tx.validate(&writer)
.expect("validate production transaction");
let receipt = tx.commit(&writer).expect("commit production transaction");

// A facade is a pinned reader. It must not drift after a later commit.
assert!(names(&pinned).is_empty());
let reopened = GraphForge::new(Some(path)).expect("fresh reopen");
assert_eq!(
reopened.project_open_recovery().selected_generation_uuid,
receipt.generation_uuid
);
assert_eq!(names(&reopened), [format!("mode-{mode_index}")]);

reopened
.checkpoint(CheckpointRequest {
name: format!("mode-{mode_index}"),
description: Some("production certification root".into()),
idempotency_key: OperationId(uuid7(seed + 32)),
actor_uuid: None,
})
.expect("checkpoint production generation");
let reachability = reopened
.inspect_project_reachability(
ProjectRetentionPolicy::default(),
ProjectRetentionLimits::default(),
)
.expect("inspect production reachability");
assert!(
reachability
.checkpoint_roots
.contains(&receipt.generation_uuid)
);
let preview = reopened
.preview_project_cleanup(
ProjectRetentionPolicy::default(),
ProjectRetentionLimits::default(),
)
.expect("preview production cleanup");
assert_eq!(preview.selected_generation_uuid, receipt.generation_uuid);
}
}

#[test]
fn production_history_certifies_cancel_drop_and_idempotent_retry() {
let project = tempfile::tempdir().expect("certification project");
let path = project.path().to_str().expect("utf-8 project path");
let graph = GraphForge::new_with_options(
Some(path),
GraphForgeOptions {
write_mode: ProjectWriteMode::QueuedWriter,
..GraphForgeOptions::default()
},
)
.expect("queued writer open");

{
let dropped = graph.begin_transaction(context(80)).expect("begin dropped");
dropped
.stage_add_node(
uuid7(81),
"CertPerson",
HashMap::from([("name".into(), PropValue::Str("dropped".into()))]),
)
.expect("stage dropped");
}
assert!(names(&GraphForge::new(Some(path)).expect("reopen after drop")).is_empty());

let cancelled = graph
.begin_transaction(context(82))
.expect("begin cancelled");
cancelled
.stage_add_node(
uuid7(83),
"CertPerson",
HashMap::from([("name".into(), PropValue::Str("cancelled".into()))]),
)
.expect("stage cancelled");
let token = CancellationToken::new();
token.cancel();
let error = cancelled
.commit_with_cancellation(&graph, Some(token))
.expect_err("pre-admission cancellation must fail");
assert_eq!(error.code(), "GF_CANCELLED");
assert!(names(&GraphForge::new(Some(path)).expect("reopen after cancel")).is_empty());

let retry_context = context(84);
let node = uuid7(85);
let properties = HashMap::from([("name".into(), PropValue::Str("durable".into()))]);
let first = graph
.begin_transaction(retry_context.clone())
.expect("begin first");
first
.stage_add_node(node, "CertPerson", properties.clone())
.expect("stage first");
let first_receipt = first.commit(&graph).expect("commit first");
let retry = graph.begin_transaction(retry_context).expect("begin retry");
retry
.stage_add_node(node, "CertPerson", properties)
.expect("stage retry");
let retry_receipt = retry.commit(&graph).expect("commit retry");
assert_eq!(first_receipt.generation_uuid, retry_receipt.generation_uuid);
let reopened = GraphForge::new(Some(path)).expect("reopen durable retry");
assert_eq!(names(&reopened), ["durable"]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { tableFromIPC } from "apache-arrow";
import { GraphForge } from "../index.js";

const OP_COMMIT = "018f0f4e-7b8c-7000-8000-000000007501";
const OP_ROLLBACK = "018f0f4e-7b8c-7000-8000-000000007502";
const OP_DROP = "018f0f4e-7b8c-7000-8000-000000007503";
const OP_SEED = "018f0f4e-7b8c-7000-8000-000000007504";
const OP_CERT = "018f0f4e-7b8c-7000-8000-000000007560";
const OP_CERT_CHECKPOINT = "018f0f4e-7b8c-7000-8000-000000007562";
const NODE_BULK = "018f0f4e-7b8c-7000-8000-000000007511";
const NODE_GHOST = "018f0f4e-7b8c-7000-8000-000000007512";
const NODE_CERT = "018f0f4e-7b8c-7000-8000-000000007561";
Expand Down Expand Up @@ -95,5 +97,17 @@ test("certification observations agree across reopen", async () => {
const reopened = new GraphForge(root);
const again = reopened.projectOpenRecovery();
assert.equal(again.selectedGenerationUuid, generation);
const names = tableFromIPC(
reopened.execute("MATCH (n:Person) RETURN n.name AS name ORDER BY name"),
);
assert.deepEqual([...names.getChild("name").toArray()], ["Cert"]);
await reopened.checkpoint({
name: "certification",
idempotencyKey: OP_CERT_CHECKPOINT,
});
const reachability = reopened.inspectProjectReachability();
assert.ok(reachability.checkpointRoots.includes(generation));
const cleanup = reopened.previewProjectCleanup();
assert.equal(cleanup.selectedGenerationUuid, generation);
});
});
12 changes: 12 additions & 0 deletions crates/graphforge-bindings-py/tests/transaction_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
OP_DROP = "018f0f4e-7b8c-7000-8000-000000007503"
OP_CLI = "018f0f4e-7b8c-7000-8000-000000007504"
OP_CERT = "018f0f4e-7b8c-7000-8000-000000007560"
OP_CERT_CHECKPOINT = "018f0f4e-7b8c-7000-8000-000000007562"
NODE_BULK = "018f0f4e-7b8c-7000-8000-000000007511"
NODE_GHOST = "018f0f4e-7b8c-7000-8000-000000007512"
NODE_CERT = "018f0f4e-7b8c-7000-8000-000000007561"
Expand Down Expand Up @@ -125,6 +126,17 @@ def check_certification_observation_parity(project: Path) -> None:
reopened = g.GraphForge(str(project))
again = reopened.project_open_recovery()
assert again["selected_generation_uuid"] == generation
names = (
reopened.execute("MATCH (n:Person) RETURN n.name AS name ORDER BY name")
.column("name")
.to_pylist()
)
assert "Cert" in names
reopened.checkpoint(name="certification", idempotency_key=OP_CERT_CHECKPOINT)
reachability = reopened.inspect_project_reachability()
assert generation in reachability["checkpoint_roots"]
cleanup = reopened.preview_project_cleanup()
assert cleanup["selected_generation_uuid"] == generation
code, stdout, _stderr = _cli_execute(
[
"gf",
Expand Down
Loading