Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
arachne::coordination::operational_ledger Class Reference

#include <include/arachne/coordinator.hpp>

Collaboration diagram for arachne::coordination::operational_ledger:

Classes

struct  implementation

Public Member Functions

 operational_ledger (std::filesystem::path database_path, std::optional< std::filesystem::path > legacy_inbox_root=std::nullopt)
 ~operational_ledger ()
 operational_ledger (const operational_ledger &)=delete
operational_ledgeroperator= (const operational_ledger &)=delete
 operational_ledger (operational_ledger &&)=delete
operational_ledgeroperator= (operational_ledger &&)=delete
envelope_record intake (const intake_request &request)
envelope_record transition (std::string_view envelope_id, cocoon_status next, std::string_view actor_ref, std::string_view reason={})
envelope_record get (std::string_view envelope_id) const
std::vector< envelope_recordlist (cocoon_status status) const
std::vector< state_eventhistory (std::string_view envelope_id) const
void capture_inbox_baseline (const std::filesystem::path &inbox_root)
std::vector< verification_issueverify_inbox (const std::filesystem::path &inbox_root) const
bool retire_queued_payload (std::string_view envelope_id, const std::filesystem::path &internal_queue_root, std::optional< std::filesystem::path > legacy_inbox_root=std::nullopt)
accumulation_state accumulation () const
bool should_integrate (const accumulation_policy &policy) const
bool claim_logical_run (std::string_view run_id, std::string_view graph_domain, std::string_view logical_date, std::string_view configuration_sha256, bool retry_failed=false, bool resume_running=false)
void bind_product_run_inputs (std::string_view run_id, const std::vector< std::string > &envelope_ids)
std::vector< envelope_recordproduct_run_inputs (std::string_view run_id) const
void finish_integrated_product_run (std::string_view run_id, std::string_view manifest_ref)
void finish_run (std::string_view run_id, std::string_view status, std::string_view manifest_ref={})
const std::filesystem::path & path () const noexcept

Private Attributes

implementationimpl_
std::filesystem::path path_
std::optional< std::filesystem::path > legacy_inbox_root_

Detailed Description

Definition at line 81 of file coordinator.hpp.

Constructor & Destructor Documentation

◆ operational_ledger() [1/3]

arachne::coordination::operational_ledger::operational_ledger ( std::filesystem::path database_path,
std::optional< std::filesystem::path > legacy_inbox_root = std::nullopt )
explicit

Definition at line 564 of file coordinator.cpp.

568 : impl_(nullptr) {
569 reject_symlink_components(database_path, "operational ledger path");
570 path_ = weakly_canonical_or_absolute(database_path);
571 if (legacy_inbox_root) {
572 reject_symlink_components(*legacy_inbox_root, "legacy inbox path");
575 throw std::invalid_argument(
576 "operational ledger must remain outside the legacy inbox"
577 );
578 }
579 }
580 if (path_.has_parent_path()) {
581 std::filesystem::create_directories(path_.parent_path());
582 }
584}
std::optional< std::filesystem::path > legacy_inbox_root_
void reject_symlink_components(const std::filesystem::path &path, std::string_view context)
std::filesystem::path weakly_canonical_or_absolute(const std::filesystem::path &path)
bool path_is_within(const std::filesystem::path &path, const std::filesystem::path &possible_parent)

References impl_.

◆ ~operational_ledger()

arachne::coordination::operational_ledger::~operational_ledger ( )

Definition at line 586 of file coordinator.cpp.

586{ delete impl_; }

References impl_.

◆ operational_ledger() [2/3]

arachne::coordination::operational_ledger::operational_ledger ( const operational_ledger & )
delete

◆ operational_ledger() [3/3]

arachne::coordination::operational_ledger::operational_ledger ( operational_ledger && )
delete

Member Function Documentation

◆ accumulation()

accumulation_state arachne::coordination::operational_ledger::accumulation ( ) const
nodiscard

Definition at line 1083 of file coordinator.cpp.

1086 {
1087 return false;
1088 }
1089 if (error || std::filesystem::is_symlink(status)
1090 || !std::filesystem::is_regular_file(status)) {
1091 throw std::runtime_error(
1092 "queued payload is not a removable regular file"
1093 );
1094 }
1095 const auto retained = fingerprint_regular_file(
1096 envelope.payload_ref, std::numeric_limits<std::uintmax_t>::max(),
1097 "queued payload"
1098 );
1099 if (retained.sha256 != envelope.payload_sha256
1100 || retained.byte_length != envelope.byte_length) {
1101 throw std::runtime_error(
file_fingerprint fingerprint_regular_file(const std::filesystem::path &path, std::uintmax_t max_bytes, std::string_view context)

◆ bind_product_run_inputs()

void arachne::coordination::operational_ledger::bind_product_run_inputs ( std::string_view run_id,
const std::vector< std::string > & envelope_ids )

Definition at line 1241 of file coordinator.cpp.

1249 {
1250 date_was_claimed = true;
1251 prior_configuration = column_text(prior_date.get(), 0);
1252 const std::string status = column_text(prior_date.get(), 1);
1253 if (status == "running" || status == "succeeded") {
1254 execute(database, "COMMIT;");
1255 return false;
1256 }
1257 } else if (result != SQLITE_DONE) {
1258 throw_sqlite(database, "inspect logical-date guard");
1259 }
1260 }
1261 if (date_was_claimed) {
1262 if (prior_configuration != configuration_sha256) {
1263 throw std::logic_error(
1264 "failed logical run used a different configuration"
1265 );
1266 }
1267 if (!retry_failed) {
1268 execute(database, "COMMIT;");
1269 return false;
1270 }
1271 }
1272
1273 const std::string started_at = utc_timestamp();
1274 auto statement = prepare(database, R"SQL(
1275INSERT INTO runs(
1276 run_id, graph_domain, logical_date, configuration_sha256, status,
1277 attempt_count, started_at
1278) VALUES (?, ?, ?, ?, 'running', 1, ?)
1279)SQL");
1280 bind_text(database, statement.get(), 1, run_id);
1281 bind_text(database, statement.get(), 2, graph_domain);
1282 bind_text(database, statement.get(), 3, logical_date);
1283 bind_text(database, statement.get(), 4, configuration_sha256);
1284 bind_text(database, statement.get(), 5, started_at);
1285 step_done(database, statement.get());
1286 auto attempt = prepare(database, R"SQL(
1287INSERT INTO run_attempts(run_id, attempt, status, started_at)
1288VALUES (?, 1, 'running', ?)
1289)SQL");
1290 bind_text(database, attempt.get(), 1, run_id);
1291 bind_text(database, attempt.get(), 2, started_at);
1292 step_done(database, attempt.get());
1293 execute(database, "COMMIT;");
1294 return true;
1295 } catch (...) {
1296 execute(database, "ROLLBACK;");
1297 throw;
1298 }
1299}
1300
1302 const std::string_view run_id, const std::vector<std::string>& envelope_ids
1303) {
1304 if (!is_safe_identifier(run_id)) {
1305 throw std::invalid_argument("run_id must be a safe stable identifier");
1306 }
1307 if (envelope_ids.empty()) {
1308 throw std::invalid_argument("product run requires at least one input");
1309 }
1310 std::vector<std::string> requested = envelope_ids;
1311 std::ranges::sort(requested);
1312 if (std::ranges::adjacent_find(requested) != requested.end()) {
1313 throw std::invalid_argument("product run inputs must be unique");
1314 }
1315
1316 sqlite3* database = impl_->database.get();
1317 execute(database, "BEGIN IMMEDIATE;");
1318 try {
1319 {
1320 auto run = prepare(database, R"SQL(
1321SELECT graph_domain, status FROM runs WHERE run_id=?
1322)SQL");
1323 bind_text(database, run.get(), 1, run_id);
1324 if (sqlite3_step(run.get()) != SQLITE_ROW) {
1325 throw std::logic_error("product run is unknown");
1326 }
1327 if (column_text(run.get(), 0) != "product_graph"
1328 || column_text(run.get(), 1) != "running") {
1329 throw std::logic_error(
1330 "product run inputs require a running product_graph run"
1331 );
1332 }
void bind_product_run_inputs(std::string_view run_id, const std::vector< std::string > &envelope_ids)
std::string column_text(sqlite3_stmt *statement, int column)
void step_done(sqlite3 *database, sqlite3_stmt *statement)
void throw_sqlite(sqlite3 *database, std::string_view context)
void execute(sqlite3 *database, std::string_view sql)
statement_handle prepare(sqlite3 *database, std::string_view sql)
void bind_text(sqlite3 *database, sqlite3_stmt *statement, int index, std::string_view value)

◆ capture_inbox_baseline()

void arachne::coordination::operational_ledger::capture_inbox_baseline ( const std::filesystem::path & inbox_root)

Definition at line 903 of file coordinator.cpp.

904 {
905 sqlite3* database = impl_->database.get();
906 auto statement = prepare(database, R"SQL(
907SELECT sequence, envelope_id, from_status, to_status, actor_ref, reason,
908 occurred_at
909FROM state_events WHERE envelope_id = ? ORDER BY sequence
910)SQL");
911 bind_text(database, statement.get(), 1, envelope_id);
912 std::vector<state_event> result;
913 while (sqlite3_step(statement.get()) == SQLITE_ROW) {
914 result.push_back(
915 { sqlite3_column_int64(statement.get(), 0),
916 column_text(statement.get(), 1),
917 cocoon_status_from_string(column_text(statement.get(), 2)),
918 cocoon_status_from_string(column_text(statement.get(), 3)),
919 column_text(statement.get(), 4), column_text(statement.get(), 5),
920 column_text(statement.get(), 6) }
921 );
922 }
923 return result;
924}
925
927 const std::filesystem::path& inbox_root
928) {
929 reject_symlink_components(inbox_root, "legacy inbox path");
930 const auto root = weakly_canonical_or_absolute(inbox_root);
931 if (root == root.root_path()) {
932 throw std::invalid_argument("legacy inbox must be a scoped directory");
933 }
934 sqlite3* database = impl_->database.get();
935 execute(database, "BEGIN IMMEDIATE;");
936 try {
937 std::vector<std::filesystem::path> paths;
938 for (const auto& entry :
939 std::filesystem::recursive_directory_iterator(root)) {
940 std::error_code status_error;
941 const auto status = entry.symlink_status(status_error);
942 if (status_error || std::filesystem::is_symlink(status)) {
943 throw std::runtime_error(
944 "legacy inbox baseline encountered an unsafe link"
945 );
946 }
947 if (std::filesystem::is_regular_file(status)) {
948 paths.push_back(entry.path());
949 }
950 }
951 std::ranges::sort(paths);
952 for (const auto& path : paths) {
953 const auto relative_path = path.lexically_relative(root);
954 if (relative_path.empty() || relative_path.is_absolute()
955 || !path_is_within(path, root)) {
956 throw std::runtime_error(
957 "legacy inbox baseline path escaped its root"
958 );
959 }
960 const auto relative = relative_path.generic_string();
void capture_inbox_baseline(const std::filesystem::path &inbox_root)
const std::filesystem::path & path() const noexcept
cocoon_status cocoon_status_from_string(const std::string_view value)

References impl_.

Referenced by anonymous_namespace{main.cpp}::command_inbox_baseline().

Here is the caller graph for this function:

◆ claim_logical_run()

bool arachne::coordination::operational_ledger::claim_logical_run ( std::string_view run_id,
std::string_view graph_domain,
std::string_view logical_date,
std::string_view configuration_sha256,
bool retry_failed = false,
bool resume_running = false )
nodiscard

Definition at line 1115 of file coordinator.cpp.

1121 {
1122 throw_sqlite(database, "read accumulation state");
1123 }
1124 accumulation_state result;
1125 result.accepted_count
1126 = static_cast<std::size_t>(sqlite3_column_int64(statement.get(), 0));
1127 result.accepted_bytes
1128 = static_cast<std::uintmax_t>(sqlite3_column_int64(statement.get(), 1));
1129 const auto oldest = sqlite3_column_int64(statement.get(), 2);
1130 if (oldest > 0) {
1131 result.oldest_age = std::chrono::seconds(
1132 std::max<std::int64_t>(0, unix_seconds() - oldest)
1133 );
1134 }
1135 return result;
1136}
1137
1139 const accumulation_policy& policy
1140) const {
1141 const auto state = accumulation();
1142 return (policy.accepted_count > 0
1143 && state.accepted_count >= policy.accepted_count)
1144 || (policy.accepted_bytes > 0
1145 && state.accepted_bytes >= policy.accepted_bytes)
1146 || (policy.oldest_age.count() > 0
1147 && state.oldest_age >= policy.oldest_age);
1148}
1149
1151 const std::string_view run_id, const std::string_view graph_domain,
1152 const std::string_view logical_date,
1153 const std::string_view configuration_sha256, const bool retry_failed,
1154 const bool resume_running
1155) {
1157 run_id, graph_domain, logical_date, configuration_sha256
1158 );
1159 sqlite3* database = impl_->database.get();
1160 execute(database, "BEGIN IMMEDIATE;");
1161 try {
1162 int prior_attempts = 0;
1163 std::string prior_status;
1164 {
1165 auto existing = prepare(database, R"SQL(
1166SELECT graph_domain, logical_date, configuration_sha256, status, attempt_count
1167FROM runs WHERE run_id = ?
1168)SQL");
1169 bind_text(database, existing.get(), 1, run_id);
1170 const int result = sqlite3_step(existing.get());
1171 if (result == SQLITE_ROW) {
1172 if (column_text(existing.get(), 0) != graph_domain
1173 || column_text(existing.get(), 1) != logical_date
1174 || column_text(existing.get(), 2) != configuration_sha256) {
1175 throw std::logic_error(
1176 "run_id is already bound to different run inputs"
1177 );
1178 }
1179 prior_status = column_text(existing.get(), 3);
1180 prior_attempts = sqlite3_column_int(existing.get(), 4);
1181 } else if (result != SQLITE_DONE) {
1182 throw_sqlite(database, "inspect existing run");
1183 }
1184 }
1185 if (!prior_status.empty()) {
1186 if (prior_status == "running") {
1187 execute(database, "COMMIT;");
1188 return resume_running;
1189 }
1190 if (prior_status != "failed" || !retry_failed) {
1191 execute(database, "COMMIT;");
1192 return false;
1193 }
1194 {
1195 auto competing = prepare(database, R"SQL(
1196SELECT 1 FROM runs
1197WHERE graph_domain=? AND logical_date=? AND run_id<>?
1198 AND status IN ('running','succeeded') LIMIT 1
1199)SQL");
1200 bind_text(database, competing.get(), 1, graph_domain);
1201 bind_text(database, competing.get(), 2, logical_date);
1202 bind_text(database, competing.get(), 3, run_id);
1203 const int result = sqlite3_step(competing.get());
1204 if (result == SQLITE_ROW) {
1205 execute(database, "COMMIT;");
1206 return false;
1207 }
1208 if (result != SQLITE_DONE) {
1209 throw_sqlite(database, "inspect competing logical run");
1210 }
1211 }
1212 const std::string started_at = utc_timestamp();
1213 const int next_attempt = prior_attempts + 1;
1214 auto retry = prepare(database, R"SQL(
1215UPDATE runs SET status='running', attempt_count=?, manifest_ref='',
1216 started_at=?, finished_at=NULL WHERE run_id=? AND status='failed'
1217)SQL");
1218 if (sqlite3_bind_int(retry.get(), 1, next_attempt) != SQLITE_OK) {
1219 throw_sqlite(database, "bind retry attempt");
1220 }
1221 bind_text(database, retry.get(), 2, started_at);
1222 bind_text(database, retry.get(), 3, run_id);
1223 step_done(database, retry.get());
1224 auto attempt = prepare(database, R"SQL(
1225INSERT INTO run_attempts(run_id, attempt, status, started_at)
1226VALUES (?, ?, 'running', ?)
1227)SQL");
1228 bind_text(database, attempt.get(), 1, run_id);
1229 if (sqlite3_bind_int(attempt.get(), 2, next_attempt) != SQLITE_OK) {
1230 throw_sqlite(database, "bind retry attempt");
1231 }
1232 bind_text(database, attempt.get(), 3, started_at);
1233 step_done(database, attempt.get());
1234 execute(database, "COMMIT;");
1235 return true;
1236 }
1237
1238 bool date_was_claimed = false;
1239 std::string prior_configuration;
accumulation_state accumulation() const
bool should_integrate(const accumulation_policy &policy) const
bool claim_logical_run(std::string_view run_id, std::string_view graph_domain, std::string_view logical_date, std::string_view configuration_sha256, bool retry_failed=false, bool resume_running=false)
void validate_run_claim(std::string_view run_id, std::string_view graph_domain, std::string_view logical_date, std::string_view configuration_sha256)

◆ finish_integrated_product_run()

void arachne::coordination::operational_ledger::finish_integrated_product_run ( std::string_view run_id,
std::string_view manifest_ref )

Definition at line 1360 of file coordinator.cpp.

1368 : requested) {
1369 const auto envelope = get(envelope_id);
1370 switch (envelope.status) {
1376 break;
1382 throw std::logic_error(
1383 "product run input is not eligible for materialization"
1384 );
1385 }
1386 auto insert = prepare(database, R"SQL(
1387INSERT INTO run_inputs(run_id, envelope_id, payload_sha256)
1388VALUES (?, ?, ?)
1389)SQL");
1390 bind_text(database, insert.get(), 1, run_id);
1391 bind_text(database, insert.get(), 2, envelope_id);
1392 bind_text(database, insert.get(), 3, envelope.payload_sha256);
1393 step_done(database, insert.get());
1394 }
1395 execute(database, "COMMIT;");
1396 } catch (...) {
1397 execute(database, "ROLLBACK;");
1398 throw;
1399 }
1400}
1401
1402std::vector<envelope_record>
1403operational_ledger::product_run_inputs(const std::string_view run_id) const {
1404 if (!is_safe_identifier(run_id)) {
1405 throw std::invalid_argument("run_id must be a safe stable identifier");
1406 }
1407 sqlite3* database = impl_->database.get();
1408 {
1409 auto run = prepare(database, "SELECT 1 FROM runs WHERE run_id=?");
1410 bind_text(database, run.get(), 1, run_id);
1411 if (sqlite3_step(run.get()) != SQLITE_ROW) {
1412 throw std::out_of_range("unknown run");
1413 }
1414 }
1415 auto statement = prepare(database, R"SQL(
1416SELECT e.envelope_id, e.payload_ref, e.payload_sha256, e.format_version,
1417 e.submission_ref, e.title, s.status, s.accepted_by, e.supersedes,
1418 e.byte_length, i.payload_sha256
1419FROM run_inputs i
1420JOIN envelopes e USING(envelope_id)
1421JOIN envelope_state s USING(envelope_id)
1422WHERE i.run_id=? ORDER BY e.created_at, e.envelope_id
1423)SQL");
1424 bind_text(database, statement.get(), 1, run_id);
1425 std::vector<envelope_record> result;
1426 while (sqlite3_step(statement.get()) == SQLITE_ROW) {
1427 if (column_text(statement.get(), 2)
1428 != column_text(statement.get(), 10)) {
1429 throw std::logic_error("bound product input hash changed");
1430 }
1431 result.emplace_back(read_envelope(statement.get(), path_));
1432 }
1433 return result;
1434}
1435
1437 const std::string_view run_id, const std::string_view manifest_ref
1438) {
1439 if (!is_safe_identifier(run_id)) {
1440 throw std::invalid_argument("run_id must be a safe stable identifier");
1441 }
1442 if (manifest_ref.empty()) {
1443 throw std::invalid_argument(
1444 "integrated product run requires a run manifest reference"
1445 );
1446 }
1447 sqlite3* database = impl_->database.get();
1448 execute(database, "BEGIN IMMEDIATE;");
1449 try {
1450 std::string run_status;
1451 std::string prior_manifest;
1452 int attempt_number = 0;
1453 {
1454 auto run = prepare(database, R"SQL(
1455SELECT graph_domain, status, attempt_count, manifest_ref
1456FROM runs WHERE run_id=?
1457)SQL");
1458 bind_text(database, run.get(), 1, run_id);
1459 if (sqlite3_step(run.get()) != SQLITE_ROW) {
1460 throw std::logic_error("product run is unknown");
1461 }
1462 if (column_text(run.get(), 0) != "product_graph") {
1463 throw std::logic_error("run is not a product_graph run");
1464 }
1465 run_status = column_text(run.get(), 1);
1466 attempt_number = sqlite3_column_int(run.get(), 2);
1467 prior_manifest = column_text(run.get(), 3);
1468 }
1469
1470 std::vector<std::pair<std::string, cocoon_status>> inputs;
1471 {
1472 auto query = prepare(database, R"SQL(
1473SELECT i.envelope_id, s.status
1474FROM run_inputs i JOIN envelope_state s USING(envelope_id)
1475WHERE i.run_id=? ORDER BY i.envelope_id
1476)SQL");
1477 bind_text(database, query.get(), 1, run_id);
1478 while (sqlite3_step(query.get()) == SQLITE_ROW) {
1479 inputs.emplace_back(
1480 column_text(query.get(), 0),
1481 cocoon_status_from_string(column_text(query.get(), 1))
1482 );
1483 }
1484 }
void finish_integrated_product_run(std::string_view run_id, std::string_view manifest_ref)
envelope_record get(std::string_view envelope_id) const
std::vector< envelope_record > product_run_inputs(std::string_view run_id) const
envelope_record read_envelope(sqlite3_stmt *statement, const std::filesystem::path &ledger_path)

◆ finish_run()

void arachne::coordination::operational_ledger::finish_run ( std::string_view run_id,
std::string_view status,
std::string_view manifest_ref = {} )

Definition at line 1486 of file coordinator.cpp.

1488 : inputs) {
1489 (void)envelope_id;
1490 if (status != cocoon_status::processing
1491 && status != cocoon_status::integrated) {
1492 throw std::logic_error(
1493 "product run input is neither processing nor integrated"
1494 );
1495 }
1496 }
1497 if (run_status == "succeeded") {
1498 if (prior_manifest != manifest_ref
1499 || std::ranges::any_of(inputs, [](const auto& input) {
1500 return input.second != cocoon_status::integrated;
1501 })) {
1502 throw std::logic_error(
1503 "completed product run differs from reconciliation request"
1504 );
1505 }
1506 execute(database, "COMMIT;");
1507 return;
1508 }
1509 if (run_status != "running") {
1510 throw std::logic_error(
1511 "only a running product run can confirm integration"
1512 );
1513 }
1514
1515 const std::string finished_at = utc_timestamp();
1516 for (const auto& [envelope_id, status] : inputs) {
1517 if (status == cocoon_status::integrated) {
1518 continue;
1519 }
1520 auto update = prepare(database, R"SQL(
1521UPDATE envelope_state SET status='integrated', updated_at=?
1522WHERE envelope_id=? AND status='processing'
1523)SQL");
1524 bind_text(database, update.get(), 1, finished_at);
1525 bind_text(database, update.get(), 2, envelope_id);
1526 step_done(database, update.get());
1527 if (sqlite3_changes(database) != 1) {
1528 throw std::logic_error(
1529 "product input changed during activation reconciliation"
1530 );
1531 }
1532 auto event = prepare(database, R"SQL(
1533INSERT INTO state_events(
1534 envelope_id, from_status, to_status, actor_ref, reason, occurred_at
1535) VALUES (?, 'processing', 'integrated', 'arachne:product-reconcile', ?, ?)
1536)SQL");

◆ get()

envelope_record arachne::coordination::operational_ledger::get ( std::string_view envelope_id) const
nodiscard

Definition at line 856 of file coordinator.cpp.

◆ history()

std::vector< state_event > arachne::coordination::operational_ledger::history ( std::string_view envelope_id) const
nodiscard

Definition at line 885 of file coordinator.cpp.

888 {
889 sqlite3* database = impl_->database.get();
890 auto statement = prepare(
891 database,
892 std::string(envelope_select)
893 + " WHERE s.status = ? ORDER BY e.created_at, e.envelope_id"
894 );
895 bind_text(database, statement.get(), 1, to_string(status));
896 std::vector<envelope_record> result;
897 while (sqlite3_step(statement.get()) == SQLITE_ROW) {
898 result.emplace_back(read_envelope(statement.get(), path_));
899 }
900 return result;
901}
std::string_view to_string(const cocoon_status status) noexcept

References impl_.

◆ intake()

envelope_record arachne::coordination::operational_ledger::intake ( const intake_request & request)
nodiscard

Definition at line 588 of file coordinator.cpp.

588 {
589 reject_symlink_components(request.source_path, "submission path");
590 reject_symlink_components(request.inbox_root, "inbox path");
591 const auto source = weakly_canonical_or_absolute(request.source_path);
592 const auto inbox = weakly_canonical_or_absolute(request.inbox_root);
593 if (request.submission_ref.empty() || request.title.empty()) {
594 throw std::invalid_argument(
595 "submission reference and title are required"
596 );
597 }
598 if (inbox == inbox.root_path() || !inbox.has_parent_path()) {
599 throw std::invalid_argument("inbox must be a scoped directory");
600 }
601 if (path_is_within(path_, inbox)) {
602 throw std::invalid_argument(
603 "operational ledger must remain outside the internal batch queue"
604 );
605 }
608 || path_is_within(*legacy_inbox_root_, inbox))) {
609 throw std::invalid_argument(
610 "internal queue and external legacy inbox must not overlap"
611 );
612 }
613 std::filesystem::create_directories(inbox);
614 reject_symlink_components(inbox, "inbox path");
615 const auto source_fingerprint = fingerprint_regular_file(
616 source, request.max_payload_bytes, "submission"
617 );
618 const std::string& payload_hash = source_fingerprint.sha256;
619 const std::string envelope_id = "env_"
621 "batch_envelope_v1\n" + payload_hash + "\n"
622 + request.submission_ref
623 )
624 .substr(0, 32);
625
626 try {
627 auto existing = get(envelope_id);
628 if (existing.payload_sha256 != payload_hash
629 || existing.submission_ref != request.submission_ref
630 || existing.title != request.title
631 || existing.supersedes != request.supersedes) {
632 throw std::logic_error("stable envelope identity collision");
633 }
634 if (!path_is_within(existing.payload_ref, inbox)) {
635 throw std::runtime_error(
636 "registered cocoon payload escaped the internal queue"
637 );
638 }
640 existing.payload_ref, "registered cocoon payload"
641 );
642 std::error_code retained_error;
643 const auto retained_status = std::filesystem::symlink_status(
644 existing.payload_ref, retained_error
645 );
646 if (retained_status.type() == std::filesystem::file_type::not_found
647 || retained_error == std::errc::no_such_file_or_directory) {
648 if (existing.status == cocoon_status::integrated) {
649 // Successful internal-queue cleanup is intentionally durable:
650 // an identical resubmission does not recreate retired bytes.
651 return existing;
652 }
653 throw std::runtime_error(
654 "queued cocoon payload disappeared before integration"
655 );
656 }
657 const auto retained = fingerprint_regular_file(
658 existing.payload_ref, request.max_payload_bytes,
659 "registered cocoon payload"
660 );
661 if (retained.sha256 != existing.payload_sha256
662 || retained.byte_length != existing.byte_length) {
663 throw std::runtime_error(
664 "registered internal queue payload was modified"
665 );
666 }
667 if (existing.status == cocoon_status::received) {
668 const auto after_validation = fingerprint_regular_file(
669 existing.payload_ref, request.max_payload_bytes,
670 "registered cocoon payload"
671 );
672 if (after_validation.sha256 != retained.sha256
673 || after_validation.byte_length != retained.byte_length) {
674 throw std::runtime_error(
675 "registered inbox payload changed during validation"
676 );
677 }
678 constexpr auto target = cocoon_status::waiting_approval;
679 try {
680 return transition(
681 envelope_id, target, "arachne:intake",
682 "opaque payload received and queued for explicit approval"
683 );
684 } catch (const std::logic_error&) {
685 existing = get(envelope_id);
686 if (existing.status == target) {
687 return existing;
688 }
689 throw;
690 }
691 }
692 return existing;
693 } catch (const std::out_of_range&) { }
694
695 std::filesystem::path payload_ref;
696 file_fingerprint retained;
697 if (path_is_within(source, inbox)) {
698 payload_ref = source;
699 retained = source_fingerprint;
700 } else {
701 auto normalized_request = request;
702 normalized_request.inbox_root = inbox;
703 payload_ref = choose_destination(normalized_request, payload_hash);
704 const auto staging_root
705 = inbox.parent_path() / ".arachne-intake-staging";
706 reject_symlink_components(staging_root, "intake staging path");
707 std::filesystem::create_directories(staging_root);
708 reject_symlink_components(staging_root, "intake staging path");
709 const auto sequence = intake_stage_sequence.fetch_add(1);
710 const auto staging = staging_root
711 / (envelope_id + "-" + std::to_string(unix_seconds()) + "-"
712 + std::to_string(sequence) + ".part");
713 try {
714 std::filesystem::copy_file(
715 source, staging, std::filesystem::copy_options::none
716 );
717 const auto staged = fingerprint_regular_file(
718 staging, request.max_payload_bytes, "staged inbox copy"
719 );
720 if (staged.sha256 != payload_hash
721 || staged.byte_length != source_fingerprint.byte_length) {
722 throw std::runtime_error("inbox copy hash or size mismatch");
723 }
724 // A same-filesystem hard link publishes without an overwrite race.
725 // Removing the staging name cannot modify the queued payload link.
726 std::filesystem::create_hard_link(staging, payload_ref);
727 if (!std::filesystem::remove(staging)) {
728 throw std::runtime_error("cannot retire intake staging link");
729 }
730 } catch (...) {
731 std::error_code ignored;
732 std::filesystem::remove(staging, ignored);
733 throw;
734 }
735 retained = fingerprint_regular_file(
736 payload_ref, request.max_payload_bytes, "retained inbox payload"
737 );
738 if (retained.sha256 != payload_hash
739 || retained.byte_length != source_fingerprint.byte_length) {
740 throw std::runtime_error(
741 "retained inbox payload changed during publication"
742 );
743 }
744 }
745
746 const auto final_fingerprint = fingerprint_regular_file(
747 payload_ref, request.max_payload_bytes, "retained inbox payload"
748 );
749 if (final_fingerprint.sha256 != retained.sha256
750 || final_fingerprint.byte_length != retained.byte_length) {
751 throw std::runtime_error(
752 "retained inbox payload changed during mechanical validation"
753 );
754 }
755 retained = final_fingerprint;
756 const auto created_at = utc_timestamp();
757 sqlite3* database = impl_->database.get();
758 execute(database, "BEGIN IMMEDIATE;");
759 try {
760 auto statement = prepare(database, R"SQL(
761INSERT INTO envelopes(
762 envelope_id, payload_ref, payload_sha256, format_version,
763 submission_ref, title, supersedes, byte_length, created_at
764) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
765)SQL");
766 bind_text(database, statement.get(), 1, envelope_id);
767 bind_text(
768 database, statement.get(), 2,
770 );
771 bind_text(database, statement.get(), 3, payload_hash);
772 if (sqlite3_bind_int(statement.get(), 4, 0) != SQLITE_OK) {
773 throw_sqlite(database, "bind format version");
774 }
775 bind_text(database, statement.get(), 5, request.submission_ref);
776 bind_text(database, statement.get(), 6, request.title);
777 bind_optional(database, statement.get(), 7, request.supersedes);
778 if (sqlite3_bind_int64(
779 statement.get(), 8,
780 static_cast<sqlite3_int64>(retained.byte_length)
781 )
782 != SQLITE_OK) {
783 throw_sqlite(database, "bind payload length");
784 }
785 bind_text(database, statement.get(), 9, created_at);
786 step_done(database, statement.get());
787
788 auto state_statement = prepare(database, R"SQL(
789INSERT INTO envelope_state(envelope_id, status, accepted_by, updated_at)
790VALUES (?, 'received', NULL, ?)
791)SQL");
792 bind_text(database, state_statement.get(), 1, envelope_id);
793 bind_text(database, state_statement.get(), 2, created_at);
794 step_done(database, state_statement.get());
795 execute(database, "COMMIT;");
796 } catch (...) {
797 execute(database, "ROLLBACK;");
envelope_record transition(std::string_view envelope_id, cocoon_status next, std::string_view actor_ref, std::string_view reason={})
void bind_optional(sqlite3 *database, sqlite3_stmt *statement, int index, const std::optional< std::string > &value)
std::filesystem::path choose_destination(const intake_request &request, std::string_view payload_hash)
std::string portable_payload_reference(const std::filesystem::path &payload, const std::filesystem::path &ledger_path)
std::string sha256(std::span< const std::byte > bytes)
Definition crypto.cpp:209

References impl_, arachne::coordination::integrated, arachne::coordination::received, arachne::crypto::sha256(), arachne::coordination::intake_request::submission_ref, arachne::coordination::intake_request::title, and arachne::coordination::waiting_approval.

Referenced by anonymous_namespace{main.cpp}::command_intake().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ list()

std::vector< envelope_record > arachne::coordination::operational_ledger::list ( cocoon_status status) const
nodiscard

Definition at line 869 of file coordinator.cpp.

875 {
876 sqlite3* database = impl_->database.get();
877 auto statement = prepare(
878 database, std::string(envelope_select) + " WHERE e.envelope_id = ?"
879 );
880 bind_text(database, statement.get(), 1, envelope_id);
881 if (sqlite3_step(statement.get()) != SQLITE_ROW) {
882 throw std::out_of_range("unknown envelope");

◆ operator=() [1/2]

operational_ledger & arachne::coordination::operational_ledger::operator= ( const operational_ledger & )
delete

◆ operator=() [2/2]

operational_ledger & arachne::coordination::operational_ledger::operator= ( operational_ledger && )
delete

◆ path()

const std::filesystem::path & arachne::coordination::operational_ledger::path ( ) const
nodiscardnoexcept

Definition at line 1538 of file coordinator.cpp.

◆ product_run_inputs()

std::vector< envelope_record > arachne::coordination::operational_ledger::product_run_inputs ( std::string_view run_id) const
nodiscard

Definition at line 1335 of file coordinator.cpp.

1336 {
1337 auto inputs = prepare(database, R"SQL(
1338SELECT envelope_id, payload_sha256 FROM run_inputs
1339WHERE run_id=? ORDER BY envelope_id
1340)SQL");
1341 bind_text(database, inputs.get(), 1, run_id);
1342 while (sqlite3_step(inputs.get()) == SQLITE_ROW) {
1343 existing.emplace_back(
1344 column_text(inputs.get(), 0), column_text(inputs.get(), 1)
1345 );
1346 }
1347 }
1348
1349 if (!existing.empty()) {
1350 if (existing.size() != requested.size()) {
1351 throw std::logic_error(
1352 "running product run is already bound to different inputs"
1353 );
1354 }
1355 for (std::size_t index = 0; index < requested.size(); ++index) {
1356 const auto envelope = get(requested[index]);
1357 if (existing[index].first != requested[index]
1358 || existing[index].second != envelope.payload_sha256) {

◆ retire_queued_payload()

bool arachne::coordination::operational_ledger::retire_queued_payload ( std::string_view envelope_id,
const std::filesystem::path & internal_queue_root,
std::optional< std::filesystem::path > legacy_inbox_root = std::nullopt )
nodiscard

Definition at line 1007 of file coordinator.cpp.

1009 {
1010 result.push_back(
1011 { path, "recorded legacy inbox path escapes its root" }
1012 );
1013 continue;
1014 }
1015 std::error_code status_error;
1016 const auto status = std::filesystem::symlink_status(path, status_error);
1017 if (status_error || std::filesystem::is_symlink(status)
1018 || !std::filesystem::is_regular_file(status)) {
1019 result.push_back({ path, "pre-existing inbox file is missing" });
1020 continue;
1021 }
1022 const auto current = fingerprint_regular_file(
1023 path, std::numeric_limits<std::uintmax_t>::max(),
1024 "legacy inbox file"
1025 );
1026 if (current.byte_length != expected_size) {
1027 result.push_back({ path, "pre-existing inbox file size changed" });
1028 continue;
1029 }
1030 if (current.sha256 != expected_hash) {
1031 result.push_back({ path, "pre-existing inbox file hash changed" });
1032 }
1033 }
1034 return result;
1035}
1036
1038 const std::string_view envelope_id,
1039 const std::filesystem::path& internal_queue_root,
1040 std::optional<std::filesystem::path> legacy_inbox_root
1041) {
1042 reject_symlink_components(internal_queue_root, "internal queue path");
1043 const auto queue = weakly_canonical_or_absolute(internal_queue_root);
1044 if (queue == queue.root_path()) {
1045 throw std::invalid_argument(
1046 "internal queue must be a scoped directory"
1047 );
1048 }
1049 std::optional<std::filesystem::path> legacy = legacy_inbox_root_;
1050 if (legacy_inbox_root) {
1051 reject_symlink_components(*legacy_inbox_root, "legacy inbox path");
1052 const auto supplied = weakly_canonical_or_absolute(*legacy_inbox_root);
1053 if (legacy && supplied != *legacy) {
1054 throw std::invalid_argument(
1055 "cleanup legacy-inbox boundary differs from ledger "
1056 "configuration"
1057 );
1058 }
1059 legacy = supplied;
1060 }
1061 if (legacy
1062 && (path_is_within(queue, *legacy) || path_is_within(*legacy, queue))) {
1063 throw std::invalid_argument(
1064 "internal queue and external legacy inbox must be disjoint"
1065 );
1066 }
1067 const auto envelope = get(envelope_id);
1068 if (envelope.status != cocoon_status::integrated) {
1069 throw std::logic_error(
1070 "queued payload may be retired only after full integration success"
1071 );
1072 }
1073 reject_symlink_components(envelope.payload_ref, "queued payload path");
1074 if (!path_is_within(envelope.payload_ref, queue)) {
1075 throw std::invalid_argument(
1076 "refusing to retire a payload outside the internal queue"
1077 );
1078 }
1079 if (legacy) {
1080 reject_inbox_deletion_target(envelope.payload_ref, *legacy);
1081 }
bool retire_queued_payload(std::string_view envelope_id, const std::filesystem::path &internal_queue_root, std::optional< std::filesystem::path > legacy_inbox_root=std::nullopt)
void reject_inbox_deletion_target(const std::filesystem::path &target, const std::filesystem::path &inbox_root)

◆ should_integrate()

bool arachne::coordination::operational_ledger::should_integrate ( const accumulation_policy & policy) const
nodiscard

Definition at line 1103 of file coordinator.cpp.

1105 {
1106 throw std::runtime_error(
1107 "cannot retire integrated queue payload: " + error.message()
1108 );
1109 }
1110 return true;
1111}
1112
1113accumulation_state operational_ledger::accumulation() const {

◆ transition()

envelope_record arachne::coordination::operational_ledger::transition ( std::string_view envelope_id,
cocoon_status next,
std::string_view actor_ref,
std::string_view reason = {} )
nodiscard

Definition at line 799 of file coordinator.cpp.

802 :intake",
803 "opaque payload received and queued for explicit approval"
804 );
805}
806
807envelope_record operational_ledger::transition(
808 const std::string_view envelope_id, const cocoon_status next,
809 const std::string_view actor_ref, const std::string_view reason
810) {
811 if (actor_ref.empty()) {
812 throw std::invalid_argument(
813 "state transition requires an actor reference"
814 );
815 }
816 sqlite3* database = impl_->database.get();
817 execute(database, "BEGIN IMMEDIATE;");
818 try {
819 auto current_statement = prepare(database, R"SQL(
820SELECT status FROM envelope_state WHERE envelope_id = ?
821)SQL");
822 bind_text(database, current_statement.get(), 1, envelope_id);
823 if (sqlite3_step(current_statement.get()) != SQLITE_ROW) {
824 throw std::out_of_range("unknown envelope");
825 }
826 const auto current = cocoon_status_from_string(
827 column_text(current_statement.get(), 0)
828 );
829 if (!can_transition(current, next)) {
830 throw std::logic_error(
831 "invalid cocoon transition from "
832 + std::string(to_string(current)) + " to "
833 + std::string(to_string(next))
834 );
835 }
836 const auto occurred_at = utc_timestamp();
837 auto update = prepare(database, R"SQL(
838UPDATE envelope_state
839SET status = ?, accepted_by = CASE WHEN ? = 'accepted' THEN ? ELSE accepted_by END,
840 updated_at = ?
841WHERE envelope_id = ?
842)SQL");
843 bind_text(database, update.get(), 1, to_string(next));
844 bind_text(database, update.get(), 2, to_string(next));
845 if (next == cocoon_status::accepted) {
846 bind_text(database, update.get(), 3, actor_ref);
847 } else if (sqlite3_bind_null(update.get(), 3) != SQLITE_OK) {
848 throw_sqlite(database, "bind accepted by");
849 }
850 bind_text(database, update.get(), 4, occurred_at);
851 bind_text(database, update.get(), 5, envelope_id);
852 step_done(database, update.get());
853
envelope_record intake(const intake_request &request)

◆ verify_inbox()

std::vector< verification_issue > arachne::coordination::operational_ledger::verify_inbox ( const std::filesystem::path & inbox_root) const
nodiscard

Definition at line 962 of file coordinator.cpp.

977 {
978 throw_sqlite(database, "bind inbox file size");
979 }
980 step_done(database, statement.get());
981 }
982 execute(database, "COMMIT;");
983 } catch (...) {
984 execute(database, "ROLLBACK;");
985 throw;
986 }
987}
988
989std::vector<verification_issue> operational_ledger::verify_inbox(
990 const std::filesystem::path& inbox_root
991) const {
992 reject_symlink_components(inbox_root, "legacy inbox path");
993 const auto root = weakly_canonical_or_absolute(inbox_root);
994 sqlite3* database = impl_->database.get();
995 auto statement = prepare(database, R"SQL(
996SELECT relative_path, sha256, byte_length FROM inbox_baseline
997WHERE inbox_root = ? ORDER BY relative_path
998)SQL");
999 bind_text(database, statement.get(), 1, root.string());
1000 std::vector<verification_issue> result;
1001 while (sqlite3_step(statement.get()) == SQLITE_ROW) {
1002 const std::filesystem::path relative = column_text(statement.get(), 0);
1003 const auto expected_hash = column_text(statement.get(), 1);
1004 const auto expected_size = static_cast<std::uintmax_t>(
1005 sqlite3_column_int64(statement.get(), 2)
std::vector< verification_issue > verify_inbox(const std::filesystem::path &inbox_root) const

Member Data Documentation

◆ impl_

implementation* arachne::coordination::operational_ledger::impl_
private

◆ legacy_inbox_root_

std::optional<std::filesystem::path> arachne::coordination::operational_ledger::legacy_inbox_root_
private

Definition at line 143 of file coordinator.hpp.

◆ path_

std::filesystem::path arachne::coordination::operational_ledger::path_
private

Definition at line 142 of file coordinator.hpp.


The documentation for this class was generated from the following files: