field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit ed34cead9b1269f09d0bbd19715957c990e6380a
parent 875959a92e2d342e02016d3fde6f2843d633c868
Author: triesap <tyson@radroots.org>
Date:   Sun, 20 Sep 2026 04:10:22 +0000

media: preserve shared inbound cache ownership

- Inspect all context references before physical cache deletion
- Hold existing mutation fences through bounded scan and unlink
- Retain bytes on unknown ownership or exhausted reconciliation budgets
- Verify concurrency, quotas, native consumers and interrupted cleanup

Diffstat:
MTeraFFI/provenance.json | 54+++++++++++++++++++++++++++---------------------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 68++++++++++++++++++++++++++++++++++++++++++++++----------------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 68++++++++++++++++++++++++++++++++++++++++++++++----------------------
MTeraFFI/source/aarch64-apple-ios.json | 68++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcore/crates/tera_core/src/runtime/mod.rs | 3++-
Mcore/crates/tera_core/src/runtime/product_surface/media.rs | 24+++++++++++++++++-------
Mcore/crates/tera_core/src/runtime/product_surface/today.rs | 59++++++++++++++++++++++++++++++++++++++---------------------
Acore/crates/tera_core/src/runtime/product_surface/today_media_collection.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/today_media_collection_budget_tests.rs | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs | 322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcore/crates/tera_core/src/runtime/product_surface/today_media_retrieval.rs | 18++++++++++++------
Mcore/crates/tera_core/src/runtime/product_surface/today_scope_tests.rs | 1+
Mcore/crates/tera_core/src/runtime/product_surface/today_submission_overlay.rs | 2+-
Acore/crates/tera_core/src/runtime/today_projection.rs | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrelease/provenance.json | 4++--
Mtest-fixtures/legacy-identifiers.v1.json | 8++++++++
17 files changed, 946 insertions(+), 133 deletions(-)

diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -22,9 +22,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76332680, + "bytes": 76473944, "path": "TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "543ad9663043f92abada345b92b0b67c61223a74c680cbea075b65fcb075196b" + "sha256": "ca3a03f9a5405ecf9b9c9a5c19f69ff2c48e2270336f09a2843e04129de3d362" }, { "bytes": 80633, @@ -37,9 +37,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76405744, + "bytes": 76544496, "path": "TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "b4159f302d5b4a21de1bb24fb614bafd920d2d374e7b6af07c88f8cdcd6bd2b1" + "sha256": "4e2e628e002429b4068098d2db57d5292958d2cf430cb630471e1699d6913825" }, { "bytes": 49312, @@ -77,34 +77,34 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 21899968, + "bytes": 21967872, "path": "native/aarch64-apple-darwin/libtera_ffi.dylib", - "sha256": "e8ec716720de96cc7450be3edb93bf109075f129be6aa81d93c07b330984c091" + "sha256": "891e67a07e2ec4e72d78679276d51111c850086c95e00a8600ce087def6b2fa8" }, { - "bytes": 76332680, + "bytes": 76473944, "path": "native/aarch64-apple-ios-sim/libtera_ffi.a", - "sha256": "543ad9663043f92abada345b92b0b67c61223a74c680cbea075b65fcb075196b" + "sha256": "ca3a03f9a5405ecf9b9c9a5c19f69ff2c48e2270336f09a2843e04129de3d362" }, { - "bytes": 76405744, + "bytes": 76544496, "path": "native/aarch64-apple-ios/libtera_ffi.a", - "sha256": "b4159f302d5b4a21de1bb24fb614bafd920d2d374e7b6af07c88f8cdcd6bd2b1" + "sha256": "4e2e628e002429b4068098d2db57d5292958d2cf430cb630471e1699d6913825" }, { - "bytes": 101576, + "bytes": 102750, "path": "source/aarch64-apple-darwin.json", - "sha256": "37d5f64f2f7172ba7efd570c1306810b3fc5393643fd692df85f717bbde2c969" + "sha256": "fe4aa12b24ed7fb71c0382e537cdf7696389ef63bcd310857546d383c148e1c6" }, { - "bytes": 101420, + "bytes": 102594, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "9d2a8de1234b6829ffe7df6793049ca2968977de837e7dcb0f410b9d8d296728" + "sha256": "923ef1485204e843e5e192aee606b8df389880a03cb01cb3a290c261329244dc" }, { - "bytes": 101416, + "bytes": 102590, "path": "source/aarch64-apple-ios.json", - "sha256": "16415cad4036e47de345680770d0b9acc8b858c224818c17db0e05212c54f6b6" + "sha256": "b5bfdb21f49a0954a6ac08fcea42363067fa3d348297d2a8b567bfa936e0197e" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "31064aa6fb8085ae69af38ded58537fc76ee4554" + "tree": "6661f9a8c9bd760a3f24a90125c05450b1c7d530" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -139,9 +139,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76332680, + "bytes": 76473944, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64-simulator/libtera_ffi.a", - "sha256": "543ad9663043f92abada345b92b0b67c61223a74c680cbea075b65fcb075196b" + "sha256": "ca3a03f9a5405ecf9b9c9a5c19f69ff2c48e2270336f09a2843e04129de3d362" }, { "bytes": 80633, @@ -154,9 +154,9 @@ "sha256": "3ed9b7ece2f86e5b5d1ddc6ecbb97deb49c46ca2ea91a930eaec1b580232345c" }, { - "bytes": 76405744, + "bytes": 76544496, "path": "Tera/Frameworks/TeraFFI.xcframework/ios-arm64/libtera_ffi.a", - "sha256": "b4159f302d5b4a21de1bb24fb614bafd920d2d374e7b6af07c88f8cdcd6bd2b1" + "sha256": "4e2e628e002429b4068098d2db57d5292958d2cf430cb630471e1699d6913825" }, { "bytes": 643709, @@ -174,19 +174,19 @@ "sha256": "7b3f92a57ebcc7873349403243098d5dcf993b2dbdab9ed84fea4a50c20baab0" }, { - "bytes": 101576, + "bytes": 102750, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "37d5f64f2f7172ba7efd570c1306810b3fc5393643fd692df85f717bbde2c969" + "sha256": "fe4aa12b24ed7fb71c0382e537cdf7696389ef63bcd310857546d383c148e1c6" }, { - "bytes": 101420, + "bytes": 102594, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "9d2a8de1234b6829ffe7df6793049ca2968977de837e7dcb0f410b9d8d296728" + "sha256": "923ef1485204e843e5e192aee606b8df389880a03cb01cb3a290c261329244dc" }, { - "bytes": 101416, + "bytes": 102590, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "16415cad4036e47de345680770d0b9acc8b858c224818c17db0e05212c54f6b6" + "sha256": "b5bfdb21f49a0954a6ac08fcea42363067fa3d348297d2a8b567bfa936e0197e" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "31064aa6fb8085ae69af38ded58537fc76ee4554" -manifest_sha256 = "bbd348afc051ecac6a621c02d039441dfda73b05d7070ba943a8707664ea33fb" +source_tree = "6661f9a8c9bd760a3f24a90125c05450b1c7d530" +manifest_sha256 = "b9e71bb1b065bd36ccfae2ab232edefdbf8372131b0facf8d308baf7df7379de" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -633,10 +633,10 @@ "sha256": "c25bafe3b7228d8c9dc680ba30aa87d3e1f5fe1468484c920d29739de895d5f8" }, "core/crates/tera_core/src/runtime/mod.rs": { - "bytes": 9303, - "git_blob": "d8878a07ae7fd3fe2c2ef9ab2b16fd55b612671d", + "bytes": 9341, + "git_blob": "b4d9aa62acc77fd18be249badba9a3b786e6a992", "mode": "100644", - "sha256": "415e60d13dfb1e5dfe4dead67a3f4a24d0c91d7160dda19fb21147a7e45df984" + "sha256": "aca22ae842ed48de7305edf958125fe48c91f66f0bf6bfb54f0e91e8fc763245" }, "core/crates/tera_core/src/runtime/mutation_admission.rs": { "bytes": 2148, @@ -789,10 +789,10 @@ "sha256": "af6606fa0c20f63756f9bf462cba1c681c201892b720c1135d71a17123b13ae7" }, "core/crates/tera_core/src/runtime/product_surface/media.rs": { - "bytes": 75981, - "git_blob": "89d2d07cb9af04c9cce3a7371e5519a3720444ec", + "bytes": 76591, + "git_blob": "c746fdaee5012d4a456eb67493fcab783650f66f", "mode": "100644", - "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" + "sha256": "12ce0eb395f9a8600ae9c555378804e39551d9a324ae751763170f25b241099c" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { "bytes": 8202, @@ -1173,10 +1173,10 @@ "sha256": "ec18505ac5efb7bfa1b6c351178aa7f6b15d8272d87dc567711acdd65ca372b7" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 133627, - "git_blob": "ad2bc57a5823079b49036f80a17a480fd070f318", + "bytes": 134482, + "git_blob": "be86114e236914ca3a290815142075604d7068e9", "mode": "100644", - "sha256": "2eddec5f72e483f09498f8252abf3f558b064e2f80b8ed0aa4594a516a18fe56" + "sha256": "5036fd7817c074848eab8f8189b8c3aee0a92dd30c357df448e5b7dccf4d6d38" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 13548, @@ -1220,11 +1220,29 @@ "mode": "100644", "sha256": "d5da0dc2e13a5a72731c3efc1b4fd02459acc542a94c5a38380dfa1b9e9520da" }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection.rs": { + "bytes": 4257, + "git_blob": "9f07064837c40b42fa54443ff1ca553e641dc696", + "mode": "100644", + "sha256": "f84ce1cf66d86e38ef508bf7c4d14a679d89875cc100e0895be22daf9fc28fc6" + }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection_budget_tests.rs": { + "bytes": 5636, + "git_blob": "2730144367c477f7e063c89b2f90ba8d5945faf7", + "mode": "100644", + "sha256": "3be886e04c99abb891bff97fd3bc3f4c43071246926fd8ae4e781ef2e6060a30" + }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs": { + "bytes": 10348, + "git_blob": "0630f332dc310018e47de7af0679e1246d4153c2", + "mode": "100644", + "sha256": "1c50eee0acab54e9a41104981827893505e273fe81a2dcefc8bdb7b388f39534" + }, "core/crates/tera_core/src/runtime/product_surface/today_media_retrieval.rs": { - "bytes": 8538, - "git_blob": "df5a02041a03f2ccfc9b34ec4e46b77d6f1366a3", + "bytes": 8726, + "git_blob": "5998af928f14e9d196705399556cb53861c7393b", "mode": "100644", - "sha256": "fe2f479a3130c23bfebfa9db38febe79a67383fb7a1dda899c195b43cc83472e" + "sha256": "fe7d0381ef8886e3735fd85a2624bd853e5395722eccb83e40f4b4923ae9ca59" }, "core/crates/tera_core/src/runtime/product_surface/today_media_visibility.rs": { "bytes": 1125, @@ -1275,16 +1293,16 @@ "sha256": "25fdb0267fba678ab8952d94e5d4a278f6ee062c32b2fc65b9ad5c6ec6e4300e" }, "core/crates/tera_core/src/runtime/product_surface/today_scope_tests.rs": { - "bytes": 14312, - "git_blob": "3a4ba55dcabe39685b15b92ba56c0f28d100eb5f", + "bytes": 14330, + "git_blob": "e20beb537408309a820a66d3b596cee04fb483fb", "mode": "100644", - "sha256": "7f6c67b88c110898e4ca0c00f58b9c180c77c1809c824ae342e257ea2c891f3b" + "sha256": "55e8165abfdb8a583c526032a4a168e05f0d87ff8d289c064d8923d258be3ce7" }, "core/crates/tera_core/src/runtime/product_surface/today_submission_overlay.rs": { - "bytes": 5917, - "git_blob": "dedf4ed211a06500a2fdcab5e1926e8fbcc8aa3b", + "bytes": 5923, + "git_blob": "3c4ef72bff08b4af9bbcd36472fd46b875ecf18f", "mode": "100644", - "sha256": "2a8805194c0099d369302a183339a65ff73e371b0d0a1930e735cd582fcb8dab" + "sha256": "95f6f3f684ea0d442fcc9a4cc215ad570de75a81bcd586fa312f870a6e971309" }, "core/crates/tera_core/src/runtime/product_surface/today_sync.rs": { "bytes": 10240, @@ -1346,6 +1364,12 @@ "mode": "100644", "sha256": "aa9592858c72bd6b0be8ea76818ef22aa3fa1b3f26def4289b64192622e1895a" }, + "core/crates/tera_core/src/runtime/today_projection.rs": { + "bytes": 2787, + "git_blob": "809eacf53a7d0b3c5f451b54cd5a2be7565b4f77", + "mode": "100644", + "sha256": "4d3135fe4973108574292d72316be819e98172a52b3591cd10fb7a67f693653e" + }, "core/crates/tera_core/tests/durable_runtime.rs": { "bytes": 3854, "git_blob": "92a1635ccbed873e6feec429b062a39893089364", @@ -2163,13 +2187,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 141951, - "git_blob": "9f8976ef731b206d2b7699640d400cfa08f8e9e9", + "bytes": 142235, + "git_blob": "ce738b9018d2977c3e93935b748f65873d2a0420", "mode": "100644", - "sha256": "80bdf7c1efc05d83260c08201b8d05ecf2d8780ae477239ec05fddc1bb5024a4" + "sha256": "37ff8e8ed45e6990bb3d2da4b8a7cddff2c9a99f5168de94c59d9e916b4ca333" } }, "policy": "staged_inputs", - "tree": "31064aa6fb8085ae69af38ded58537fc76ee4554" + "tree": "6661f9a8c9bd760a3f24a90125c05450b1c7d530" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -629,10 +629,10 @@ "sha256": "c25bafe3b7228d8c9dc680ba30aa87d3e1f5fe1468484c920d29739de895d5f8" }, "core/crates/tera_core/src/runtime/mod.rs": { - "bytes": 9303, - "git_blob": "d8878a07ae7fd3fe2c2ef9ab2b16fd55b612671d", + "bytes": 9341, + "git_blob": "b4d9aa62acc77fd18be249badba9a3b786e6a992", "mode": "100644", - "sha256": "415e60d13dfb1e5dfe4dead67a3f4a24d0c91d7160dda19fb21147a7e45df984" + "sha256": "aca22ae842ed48de7305edf958125fe48c91f66f0bf6bfb54f0e91e8fc763245" }, "core/crates/tera_core/src/runtime/mutation_admission.rs": { "bytes": 2148, @@ -785,10 +785,10 @@ "sha256": "af6606fa0c20f63756f9bf462cba1c681c201892b720c1135d71a17123b13ae7" }, "core/crates/tera_core/src/runtime/product_surface/media.rs": { - "bytes": 75981, - "git_blob": "89d2d07cb9af04c9cce3a7371e5519a3720444ec", + "bytes": 76591, + "git_blob": "c746fdaee5012d4a456eb67493fcab783650f66f", "mode": "100644", - "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" + "sha256": "12ce0eb395f9a8600ae9c555378804e39551d9a324ae751763170f25b241099c" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { "bytes": 8202, @@ -1169,10 +1169,10 @@ "sha256": "ec18505ac5efb7bfa1b6c351178aa7f6b15d8272d87dc567711acdd65ca372b7" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 133627, - "git_blob": "ad2bc57a5823079b49036f80a17a480fd070f318", + "bytes": 134482, + "git_blob": "be86114e236914ca3a290815142075604d7068e9", "mode": "100644", - "sha256": "2eddec5f72e483f09498f8252abf3f558b064e2f80b8ed0aa4594a516a18fe56" + "sha256": "5036fd7817c074848eab8f8189b8c3aee0a92dd30c357df448e5b7dccf4d6d38" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 13548, @@ -1216,11 +1216,29 @@ "mode": "100644", "sha256": "d5da0dc2e13a5a72731c3efc1b4fd02459acc542a94c5a38380dfa1b9e9520da" }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection.rs": { + "bytes": 4257, + "git_blob": "9f07064837c40b42fa54443ff1ca553e641dc696", + "mode": "100644", + "sha256": "f84ce1cf66d86e38ef508bf7c4d14a679d89875cc100e0895be22daf9fc28fc6" + }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection_budget_tests.rs": { + "bytes": 5636, + "git_blob": "2730144367c477f7e063c89b2f90ba8d5945faf7", + "mode": "100644", + "sha256": "3be886e04c99abb891bff97fd3bc3f4c43071246926fd8ae4e781ef2e6060a30" + }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs": { + "bytes": 10348, + "git_blob": "0630f332dc310018e47de7af0679e1246d4153c2", + "mode": "100644", + "sha256": "1c50eee0acab54e9a41104981827893505e273fe81a2dcefc8bdb7b388f39534" + }, "core/crates/tera_core/src/runtime/product_surface/today_media_retrieval.rs": { - "bytes": 8538, - "git_blob": "df5a02041a03f2ccfc9b34ec4e46b77d6f1366a3", + "bytes": 8726, + "git_blob": "5998af928f14e9d196705399556cb53861c7393b", "mode": "100644", - "sha256": "fe2f479a3130c23bfebfa9db38febe79a67383fb7a1dda899c195b43cc83472e" + "sha256": "fe7d0381ef8886e3735fd85a2624bd853e5395722eccb83e40f4b4923ae9ca59" }, "core/crates/tera_core/src/runtime/product_surface/today_media_visibility.rs": { "bytes": 1125, @@ -1271,16 +1289,16 @@ "sha256": "25fdb0267fba678ab8952d94e5d4a278f6ee062c32b2fc65b9ad5c6ec6e4300e" }, "core/crates/tera_core/src/runtime/product_surface/today_scope_tests.rs": { - "bytes": 14312, - "git_blob": "3a4ba55dcabe39685b15b92ba56c0f28d100eb5f", + "bytes": 14330, + "git_blob": "e20beb537408309a820a66d3b596cee04fb483fb", "mode": "100644", - "sha256": "7f6c67b88c110898e4ca0c00f58b9c180c77c1809c824ae342e257ea2c891f3b" + "sha256": "55e8165abfdb8a583c526032a4a168e05f0d87ff8d289c064d8923d258be3ce7" }, "core/crates/tera_core/src/runtime/product_surface/today_submission_overlay.rs": { - "bytes": 5917, - "git_blob": "dedf4ed211a06500a2fdcab5e1926e8fbcc8aa3b", + "bytes": 5923, + "git_blob": "3c4ef72bff08b4af9bbcd36472fd46b875ecf18f", "mode": "100644", - "sha256": "2a8805194c0099d369302a183339a65ff73e371b0d0a1930e735cd582fcb8dab" + "sha256": "95f6f3f684ea0d442fcc9a4cc215ad570de75a81bcd586fa312f870a6e971309" }, "core/crates/tera_core/src/runtime/product_surface/today_sync.rs": { "bytes": 10240, @@ -1342,6 +1360,12 @@ "mode": "100644", "sha256": "aa9592858c72bd6b0be8ea76818ef22aa3fa1b3f26def4289b64192622e1895a" }, + "core/crates/tera_core/src/runtime/today_projection.rs": { + "bytes": 2787, + "git_blob": "809eacf53a7d0b3c5f451b54cd5a2be7565b4f77", + "mode": "100644", + "sha256": "4d3135fe4973108574292d72316be819e98172a52b3591cd10fb7a67f693653e" + }, "core/crates/tera_core/tests/durable_runtime.rs": { "bytes": 3854, "git_blob": "92a1635ccbed873e6feec429b062a39893089364", @@ -2159,13 +2183,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 141951, - "git_blob": "9f8976ef731b206d2b7699640d400cfa08f8e9e9", + "bytes": 142235, + "git_blob": "ce738b9018d2977c3e93935b748f65873d2a0420", "mode": "100644", - "sha256": "80bdf7c1efc05d83260c08201b8d05ecf2d8780ae477239ec05fddc1bb5024a4" + "sha256": "37ff8e8ed45e6990bb3d2da4b8a7cddff2c9a99f5168de94c59d9e916b4ca333" } }, "policy": "staged_inputs", - "tree": "31064aa6fb8085ae69af38ded58537fc76ee4554" + "tree": "6661f9a8c9bd760a3f24a90125c05450b1c7d530" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -629,10 +629,10 @@ "sha256": "c25bafe3b7228d8c9dc680ba30aa87d3e1f5fe1468484c920d29739de895d5f8" }, "core/crates/tera_core/src/runtime/mod.rs": { - "bytes": 9303, - "git_blob": "d8878a07ae7fd3fe2c2ef9ab2b16fd55b612671d", + "bytes": 9341, + "git_blob": "b4d9aa62acc77fd18be249badba9a3b786e6a992", "mode": "100644", - "sha256": "415e60d13dfb1e5dfe4dead67a3f4a24d0c91d7160dda19fb21147a7e45df984" + "sha256": "aca22ae842ed48de7305edf958125fe48c91f66f0bf6bfb54f0e91e8fc763245" }, "core/crates/tera_core/src/runtime/mutation_admission.rs": { "bytes": 2148, @@ -785,10 +785,10 @@ "sha256": "af6606fa0c20f63756f9bf462cba1c681c201892b720c1135d71a17123b13ae7" }, "core/crates/tera_core/src/runtime/product_surface/media.rs": { - "bytes": 75981, - "git_blob": "89d2d07cb9af04c9cce3a7371e5519a3720444ec", + "bytes": 76591, + "git_blob": "c746fdaee5012d4a456eb67493fcab783650f66f", "mode": "100644", - "sha256": "3cdca11226903c23bed81dc796338061922df620163b8f06edeb8bc278794a00" + "sha256": "12ce0eb395f9a8600ae9c555378804e39551d9a324ae751763170f25b241099c" }, "core/crates/tera_core/src/runtime/product_surface/model.rs": { "bytes": 8202, @@ -1169,10 +1169,10 @@ "sha256": "ec18505ac5efb7bfa1b6c351178aa7f6b15d8272d87dc567711acdd65ca372b7" }, "core/crates/tera_core/src/runtime/product_surface/today.rs": { - "bytes": 133627, - "git_blob": "ad2bc57a5823079b49036f80a17a480fd070f318", + "bytes": 134482, + "git_blob": "be86114e236914ca3a290815142075604d7068e9", "mode": "100644", - "sha256": "2eddec5f72e483f09498f8252abf3f558b064e2f80b8ed0aa4594a516a18fe56" + "sha256": "5036fd7817c074848eab8f8189b8c3aee0a92dd30c357df448e5b7dccf4d6d38" }, "core/crates/tera_core/src/runtime/product_surface/today_admission_tests.rs": { "bytes": 13548, @@ -1216,11 +1216,29 @@ "mode": "100644", "sha256": "d5da0dc2e13a5a72731c3efc1b4fd02459acc542a94c5a38380dfa1b9e9520da" }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection.rs": { + "bytes": 4257, + "git_blob": "9f07064837c40b42fa54443ff1ca553e641dc696", + "mode": "100644", + "sha256": "f84ce1cf66d86e38ef508bf7c4d14a679d89875cc100e0895be22daf9fc28fc6" + }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection_budget_tests.rs": { + "bytes": 5636, + "git_blob": "2730144367c477f7e063c89b2f90ba8d5945faf7", + "mode": "100644", + "sha256": "3be886e04c99abb891bff97fd3bc3f4c43071246926fd8ae4e781ef2e6060a30" + }, + "core/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs": { + "bytes": 10348, + "git_blob": "0630f332dc310018e47de7af0679e1246d4153c2", + "mode": "100644", + "sha256": "1c50eee0acab54e9a41104981827893505e273fe81a2dcefc8bdb7b388f39534" + }, "core/crates/tera_core/src/runtime/product_surface/today_media_retrieval.rs": { - "bytes": 8538, - "git_blob": "df5a02041a03f2ccfc9b34ec4e46b77d6f1366a3", + "bytes": 8726, + "git_blob": "5998af928f14e9d196705399556cb53861c7393b", "mode": "100644", - "sha256": "fe2f479a3130c23bfebfa9db38febe79a67383fb7a1dda899c195b43cc83472e" + "sha256": "fe7d0381ef8886e3735fd85a2624bd853e5395722eccb83e40f4b4923ae9ca59" }, "core/crates/tera_core/src/runtime/product_surface/today_media_visibility.rs": { "bytes": 1125, @@ -1271,16 +1289,16 @@ "sha256": "25fdb0267fba678ab8952d94e5d4a278f6ee062c32b2fc65b9ad5c6ec6e4300e" }, "core/crates/tera_core/src/runtime/product_surface/today_scope_tests.rs": { - "bytes": 14312, - "git_blob": "3a4ba55dcabe39685b15b92ba56c0f28d100eb5f", + "bytes": 14330, + "git_blob": "e20beb537408309a820a66d3b596cee04fb483fb", "mode": "100644", - "sha256": "7f6c67b88c110898e4ca0c00f58b9c180c77c1809c824ae342e257ea2c891f3b" + "sha256": "55e8165abfdb8a583c526032a4a168e05f0d87ff8d289c064d8923d258be3ce7" }, "core/crates/tera_core/src/runtime/product_surface/today_submission_overlay.rs": { - "bytes": 5917, - "git_blob": "dedf4ed211a06500a2fdcab5e1926e8fbcc8aa3b", + "bytes": 5923, + "git_blob": "3c4ef72bff08b4af9bbcd36472fd46b875ecf18f", "mode": "100644", - "sha256": "2a8805194c0099d369302a183339a65ff73e371b0d0a1930e735cd582fcb8dab" + "sha256": "95f6f3f684ea0d442fcc9a4cc215ad570de75a81bcd586fa312f870a6e971309" }, "core/crates/tera_core/src/runtime/product_surface/today_sync.rs": { "bytes": 10240, @@ -1342,6 +1360,12 @@ "mode": "100644", "sha256": "aa9592858c72bd6b0be8ea76818ef22aa3fa1b3f26def4289b64192622e1895a" }, + "core/crates/tera_core/src/runtime/today_projection.rs": { + "bytes": 2787, + "git_blob": "809eacf53a7d0b3c5f451b54cd5a2be7565b4f77", + "mode": "100644", + "sha256": "4d3135fe4973108574292d72316be819e98172a52b3591cd10fb7a67f693653e" + }, "core/crates/tera_core/tests/durable_runtime.rs": { "bytes": 3854, "git_blob": "92a1635ccbed873e6feec429b062a39893089364", @@ -2159,13 +2183,13 @@ "sha256": "b052a73a824e8f8b26af2646a4758f13655e04de9551a8271890d3cf2b63209d" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 141951, - "git_blob": "9f8976ef731b206d2b7699640d400cfa08f8e9e9", + "bytes": 142235, + "git_blob": "ce738b9018d2977c3e93935b748f65873d2a0420", "mode": "100644", - "sha256": "80bdf7c1efc05d83260c08201b8d05ecf2d8780ae477239ec05fddc1bb5024a4" + "sha256": "37ff8e8ed45e6990bb3d2da4b8a7cddff2c9a99f5168de94c59d9e916b4ca333" } }, "policy": "staged_inputs", - "tree": "31064aa6fb8085ae69af38ded58537fc76ee4554" + "tree": "6661f9a8c9bd760a3f24a90125c05450b1c7d530" } } diff --git a/core/crates/tera_core/src/runtime/mod.rs b/core/crates/tera_core/src/runtime/mod.rs @@ -8,6 +8,7 @@ mod mutation_admission; pub mod product_surface; pub mod sdk; pub mod store; +mod today_projection; use chrono::Utc; use radroots_identity::PublicKey; @@ -32,7 +33,7 @@ pub struct TeraRuntime { lifecycle: lifecycle::RuntimeLifecycle, pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>, pub(crate) store_public_key: Option<PublicKey>, - today_projection_lock: tokio::sync::Mutex<()>, + today_projection_lock: today_projection::TodayProjectionFence, #[cfg(feature = "mobile-social")] mutations: mutation_admission::MutationAdmission, #[cfg(feature = "mobile-social")] diff --git a/core/crates/tera_core/src/runtime/product_surface/media.rs b/core/crates/tera_core/src/runtime/product_surface/media.rs @@ -1126,25 +1126,35 @@ pub(crate) async fn write_verified_artifact( } #[cfg(feature = "mobile-social")] -pub(crate) async fn remove_artifact_files( +pub(crate) fn remove_artifact_files( directory: &Path, artifact_id: Phase1MediaArtifactId, ) -> Result<(), Phase1InboundMediaError> { - ensure_cache_directory(directory).await?; + // Complete physical deletion while the caller still owns its mutation + // guards. Tokio filesystem workers may outlive a cancelled caller; do not + // dispatch unlink or sync to a worker that could escape that fence. + match std::fs::symlink_metadata(directory) { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { + return Err(Phase1InboundMediaError::CorruptArtifact); + } + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(_) => return Err(Phase1InboundMediaError::CacheIo), + } for extension in MEDIA_CACHE_EXTENSIONS { let path = artifact_path(directory, artifact_id, extension)?; - match tokio::fs::symlink_metadata(&path).await { + match std::fs::symlink_metadata(&path) { Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { return Err(Phase1InboundMediaError::CorruptArtifact); } - Ok(_) => tokio::fs::remove_file(path) - .await - .map_err(|_| Phase1InboundMediaError::CacheIo)?, + Ok(_) => std::fs::remove_file(path).map_err(|_| Phase1InboundMediaError::CacheIo)?, Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} Err(_) => return Err(Phase1InboundMediaError::CacheIo), } } - sync_cache_directory(directory).await + std::fs::File::open(directory) + .and_then(|directory| directory.sync_all()) + .map_err(|_| Phase1InboundMediaError::CacheIo) } #[cfg(feature = "mobile-social")] diff --git a/core/crates/tera_core/src/runtime/product_surface/today.rs b/core/crates/tera_core/src/runtime/product_surface/today.rs @@ -94,6 +94,10 @@ mod submission_overlay; #[path = "today_media_retrieval.rs"] mod media_retrieval; +#[cfg(feature = "mobile-social")] +#[path = "today_media_collection.rs"] +mod media_collection; + #[path = "today_media_visibility.rs"] mod media_visibility; @@ -374,13 +378,16 @@ impl TeraRuntime { ) .await? .is_none_or(|document| document.value() != encoded); + let document = ProjectionDocument::new(key, encoded)?; + let write = self.today_projection_lock.begin_write(); ProjectionStore::put_projection_document( storage, projection_id.clone(), generation, - ProjectionDocument::new(key, encoded)?, + document, ) .await?; + write.complete(); let source_position = if event_status.raw_events() == 0 { None @@ -582,7 +589,7 @@ impl TeraRuntime { })?; if changed { state = trial; - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; } Ok(changed) } @@ -606,7 +613,7 @@ impl TeraRuntime { media.fail(failure.clone()) })?; if changed { - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; } Ok(changed) } @@ -644,7 +651,7 @@ impl TeraRuntime { invalidate_artifact_references(&mut trial, *artifact_id); } state = trial; - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; Ok(evicted) } @@ -667,7 +674,7 @@ impl TeraRuntime { .ok_or(TodayError::ProjectionMissing)?; let changed = state.media_cache.touch(artifact_id, observed_at_unix_ms)?; if changed { - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; } Ok(changed) } @@ -693,11 +700,12 @@ impl TeraRuntime { let cache_changed = state.media_cache.invalidate_artifact(artifact_id); let references_changed = invalidate_artifact_references(&mut state, artifact_id); if cache_changed || references_changed { - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; } #[cfg(feature = "mobile-social")] if let Some(directory) = self.inbound_media_directory.as_deref() { - super::media::remove_artifact_files(directory, artifact_id).await?; + media_collection::collect(self, directory, &[artifact_id], &_guard, &_projection) + .await?; } Ok(cache_changed || references_changed) } @@ -727,13 +735,11 @@ impl TeraRuntime { for_each_media_mut(&mut state, |media| { media.invalidate(); }); - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; } #[cfg(feature = "mobile-social")] if let Some(directory) = self.inbound_media_directory.as_deref() { - for artifact_id in &removed { - super::media::remove_artifact_files(directory, *artifact_id).await?; - } + media_collection::collect(self, directory, &removed, &_guard, &_projection).await?; } Ok(removed) } @@ -797,7 +803,7 @@ impl TeraRuntime { state.overlays.remove(&key); } state.content_generation = content_generation(&state)?; - store_state(storage, context, generation, &state).await + store_state(self, storage, context, generation, &state).await } } @@ -1054,6 +1060,7 @@ fn verified_receipt( } async fn persist_media_state( + runtime: &TeraRuntime, storage: &dyn radroots_storage::Storage, context: &LocalNetwork, generation: ProjectionGeneration, @@ -1062,7 +1069,7 @@ async fn persist_media_state( refresh_thread_profiles(state); validate_media_state(state)?; state.content_generation = content_generation(state)?; - store_state(storage, context, generation, state).await + store_state(runtime, storage, context, generation, state).await } fn refresh_thread_profiles(state: &mut TodayProjectionState) { @@ -1489,24 +1496,34 @@ async fn load_state( }; let (state, migrated) = decode_state_document(document.value())?; if migrated { - store_state(storage, context, generation, &state).await?; + // Legacy migration creates an empty cache and cannot introduce file + // ownership. Keep that invariant explicit for this unfenced read repair. + if state.media_cache.status()?.artifacts != 0 { + return Err(TodayError::CorruptProjection); + } + ProjectionStore::put_projection_document( + storage, + projection_id()?, + generation, + ProjectionDocument::new(projection_document_key(context), encode(&state)?)?, + ) + .await?; } Ok(Some(state)) } async fn store_state( + runtime: &TeraRuntime, storage: &dyn radroots_storage::Storage, context: &LocalNetwork, generation: ProjectionGeneration, state: &TodayProjectionState, ) -> Result<(), TodayError> { - ProjectionStore::put_projection_document( - storage, - projection_id()?, - generation, - ProjectionDocument::new(projection_document_key(context), encode(state)?)?, - ) - .await?; + let id = projection_id()?; + let document = ProjectionDocument::new(projection_document_key(context), encode(state)?)?; + let write = runtime.today_projection_lock.begin_write(); + ProjectionStore::put_projection_document(storage, id, generation, document).await?; + write.complete(); Ok(()) } diff --git a/core/crates/tera_core/src/runtime/product_surface/today_media_collection.rs b/core/crates/tera_core/src/runtime/product_surface/today_media_collection.rs @@ -0,0 +1,111 @@ +//! Physical cache deletion requires absence across every context, not merely +//! invalidation in the requesting context. Incomplete proof retains bytes. +use std::{collections::BTreeSet, path::Path}; + +use radroots_storage::projection::document_query::{ + ProjectionDocumentGenerations, ProjectionDocumentQuery, +}; +use tokio::sync::MutexGuard; + +use super::*; + +const MAX_CANDIDATES: usize = 64; +const PAGE_ROWS: u16 = 32; +const MAX_PAGES: usize = 64; +const MAX_DOCUMENT_BYTES: usize = 64 * 1024 * 1024; + +/// Both guards must belong to this runtime, acquired file-first, and remain +/// held through unlink. A live inventory is only complete under that fence. +/// Excess candidates and incomplete/unknown ownership are retained for a later +/// reconciliation; a quota never authorizes deletion of ambiguous ownership. +pub(super) async fn collect( + runtime: &TeraRuntime, + directory: &Path, + candidates: &[Phase1MediaArtifactId], + _files: &MutexGuard<'_, ()>, + _projection: &MutexGuard<'_, ()>, +) -> Result<(), TodayError> { + if !runtime.today_projection_lock.can_collect() { + return Ok(()); + } + let storage = runtime + .client + .storage() + .map_err(|_| TodayError::RuntimeUnavailable)?; + let Some(unreferenced) = unreferenced(storage, candidates).await? else { + return Ok(()); + }; + if !runtime.today_projection_lock.can_collect() { + return Ok(()); + } + for artifact in unreferenced { + super::super::media::remove_artifact_files(directory, artifact)?; + } + Ok(()) +} + +async fn unreferenced( + storage: &dyn radroots_storage::Storage, + candidates: &[Phase1MediaArtifactId], +) -> Result<Option<BTreeSet<Phase1MediaArtifactId>>, TodayError> { + let mut remaining: BTreeSet<_> = candidates.iter().take(MAX_CANDIDATES).copied().collect(); + if remaining.is_empty() { + return Ok(Some(remaining)); + } + let mut query = ProjectionDocumentQuery::new( + projection_id()?, + ProjectionDocumentGenerations::All, + PAGE_ROWS, + )?; + let mut bytes = 0_usize; + for _ in 0..MAX_PAGES { + let page = match ProjectionStore::query_projection_documents(storage, query.clone()).await { + Ok(page) => page, + Err(_) => return Ok(None), + }; + for record in page.records() { + let Some(document) = record.document() else { + return Ok(None); + }; + bytes = bytes.saturating_add(document.value().len()); + if bytes > MAX_DOCUMENT_BYTES || record.generation() != projection_generation()? { + return Ok(None); + } + let Some(mut state) = current_ownership(document.value(), record.key()) else { + return Ok(None); + }; + // Validation binds every verified card/profile receipt to this + // cache. Cache entries without a visible card still own bytes. + remaining.retain(|artifact| !state.media_cache.invalidate_artifact(*artifact)); + } + let Some(cursor) = page.next_cursor() else { + return Ok(Some(remaining)); + }; + query = query.with_cursor(cursor)?; + } + Ok(None) +} + +fn current_ownership(bytes: &[u8], key: &str) -> Option<TodayProjectionState> { + // Do not migrate or discard unknown fields while proving absence. Exact + // canonical encoding also rejects duplicate keys and unrecognized nested + // fields, which a permissive application decoder could otherwise ignore. + let state: TodayProjectionState = serde_json::from_slice(bytes).ok()?; + if state.schema_version != TODAY_PROJECTION_DOCUMENT_SCHEMA_VERSION + || state.content_generation == 0 + || content_generation(&state).ok()? != state.content_generation + || validate_media_state(&state).is_err() + || encode(&state).ok()?.as_slice() != bytes + { + return None; + } + let mut digest = Sha256::new(); + digest.update(PROJECTION_DOCUMENT_KEY_DOMAIN); + digest.update(state.context_id.as_bytes()); + digest.update(state.context_generation.to_be_bytes()); + (key == format!("context.{}", hex::encode(digest.finalize()))).then_some(state) +} + +#[cfg(test)] +#[path = "today_media_collection_tests.rs"] +mod tests; diff --git a/core/crates/tera_core/src/runtime/product_surface/today_media_collection_budget_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_media_collection_budget_tests.rs @@ -0,0 +1,167 @@ +use super::*; + +#[tokio::test] +async fn uncertain_database_write_retains_files_even_after_later_success() { + for sqlite in [false, true] { + let f = Fixture::backend(sqlite).await; + f.file().await; + // Model backend work whose caller was cancelled before acknowledgement. + // The owned file can still be referenced by that late completion. + let projection = f.runtime.today_projection_lock.lock().await; + let write = f.runtime.today_projection_lock.begin_write(); + drop(projection); + f.collect().await.unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + drop(write); + f.collect().await.unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + f.state(2, true).await; // A late successful backend completion. + f.state(2, false).await; // Ordinary later writes cannot clear uncertainty. + assert!( + f.candidates() + .await + .unwrap() + .contains(&f.receipt.artifact_id()) + ); + f.collect().await.unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + f.runtime.shutdown().await.unwrap(); + } +} + +#[test] +fn collection_cannot_dispatch_file_work_beyond_its_mutation_fence() { + use std::{ + future::Future, + task::{Context, Poll, Waker}, + time::Duration, + }; + + let executor = tokio::runtime::Builder::new_current_thread() + .enable_all() + .max_blocking_threads(1) + .build() + .unwrap(); + let f = executor.block_on(Fixture::new()); + executor.block_on(f.file()); + let (ready, started) = std::sync::mpsc::channel(); + let (release, blocked) = std::sync::mpsc::channel(); + let worker = executor.spawn_blocking(move || { + ready.send(()).unwrap(); + blocked.recv().unwrap(); + }); + started.recv_timeout(Duration::from_secs(5)).unwrap(); + let completed = { + let _entered = executor.enter(); + let mut future = Box::pin(f.collect()); + matches!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())), + Poll::Ready(Ok(())) + ) + // Dropping the caller must not leave dispatched filesystem mutations + // that could race the next owner after these guards are released. + }; + std::fs::write(f.path(), &f.bytes).unwrap(); + executor.block_on(f.state(2, true)); + release.send(()).unwrap(); + executor.block_on(worker).unwrap(); + executor.block_on(f.runtime.shutdown()).unwrap(); + executor.shutdown_timeout(Duration::from_secs(5)); + assert!( + completed, + "physical collection escaped its synchronous fence" + ); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); +} + +#[tokio::test] +async fn payload_budget_retains_bytes_before_decoding_another_document() { + let f = Fixture::new().await; + let mut state = f.empty.clone(); + // These opaque source IDs remain bounded by the storage document limit. + // Their contents are irrelevant to the cache ownership being inspected. + state.quarantined_source_ids = vec!["a".repeat(14 * 1024 * 1024)]; + for generation in 2..=6 { + state.context_generation = generation; + persist_media_state( + &f.runtime, + f.runtime.client.storage().unwrap(), + &context(None, generation), + projection_generation().unwrap(), + &mut state, + ) + .await + .unwrap(); + } + f.file().await; + assert!(f.candidates().await.is_none()); + f.collect().await.unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); +} + +#[tokio::test] +async fn quota_eviction_in_one_context_preserves_another_contexts_bytes() { + let f = Fixture::new().await; + f.file().await; + f.state(1, true).await; + f.state(2, true).await; + let bytes = b"different cached content"; + let hash = BlobHash::digest(bytes).to_hex(); + let url = format!("https://media.example/{hash}.png"); + let reference = Phase1StructuralMediaReference::new( + &url, + Some(hash), + Some("image/png".into()), + Some(2), + Some(3), + Some(bytes.len() as u64), + None, + ) + .unwrap(); + let receipt = Phase1VerifiedMediaReceipt::from_commitment( + &reference, + BlobUrl::parse(&url).unwrap(), + &ByteCommitment::from_bytes(bytes, MediaType::parse("image/png").unwrap()), + 2, + 3, + f.receipt.configuration(), + 12, + ) + .unwrap(); + let files = f.runtime.inbound_media_lock.lock().await; + let projection = f.runtime.today_projection_lock.lock().await; + let storage = f.runtime.client.storage().unwrap(); + let selected = context(None, 1); + let mut state = load_state(storage, &selected, projection_generation().unwrap()) + .await + .unwrap() + .unwrap(); + let evicted = state + .media_cache + .admit(&receipt, Phase1MediaCachePolicy::new(1_024, 1).unwrap(), 12) + .unwrap(); + assert_eq!(evicted, vec![f.receipt.artifact_id()]); + persist_media_state( + &f.runtime, + storage, + &selected, + projection_generation().unwrap(), + &mut state, + ) + .await + .unwrap(); + collect(&f.runtime, f.directory(), &evicted, &files, &projection) + .await + .unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + assert_eq!( + f.runtime + .phase1_media_cache_status(&context(None, 2)) + .await + .unwrap() + .artifacts, + 1 + ); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs @@ -0,0 +1,322 @@ +use std::sync::Arc; + +use radroots_blossom::{BlobUrl, MediaType, Sha256 as BlobHash, descriptor::ByteCommitment}; + +use super::super::tests::context; +use super::*; + +#[path = "today_media_collection_budget_tests.rs"] +mod budgets; + +struct Fixture { + root: tempfile::TempDir, + runtime: Arc<TeraRuntime>, + empty: TodayProjectionState, + receipt: Phase1VerifiedMediaReceipt, + bytes: Vec<u8>, +} + +impl Fixture { + async fn new() -> Self { + Self::backend(false).await + } + + async fn backend(sqlite: bool) -> Self { + let root = tempfile::tempdir().unwrap(); + let mut runtime = if sqlite { + use crate::runtime::{ + builder::RuntimeBuilder, + store::{MobileUserStoreConfig, ProtectedDataAvailability}, + }; + let config = MobileUserStoreConfig::from_encoded( + root.path(), + &super::super::tests::keys().public_key().to_string(), + &"87".repeat(32), + 2_000_000_000_000, + ProtectedDataAvailability::Available, + ) + .unwrap(); + std::fs::create_dir_all(config.owner_directory()).unwrap(); + RuntimeBuilder::new(config).build().await.unwrap() + } else { + TeraRuntime::test_memory().unwrap() + }; + runtime.inbound_media_directory = Some(root.path().join("inbound_media.v1")); + let selected = context(None, 1); + runtime + .phase1_today_page(&selected, TodayPageRequest::first(20, 2_000_000_200, "UTC")) + .await + .unwrap(); + let empty = load_state( + runtime.client.storage().unwrap(), + &selected, + projection_generation().unwrap(), + ) + .await + .unwrap() + .unwrap(); + let bytes = b"cache ownership fixture".to_vec(); + let hash = BlobHash::digest(&bytes).to_hex(); + let url = format!("https://media.example/{hash}.png"); + let reference = Phase1StructuralMediaReference::new( + &url, + Some(hash), + Some("image/png".into()), + Some(2), + Some(3), + Some(bytes.len() as u64), + None, + ) + .unwrap(); + let receipt = Phase1VerifiedMediaReceipt::from_commitment( + &reference, + BlobUrl::parse(&url).unwrap(), + &ByteCommitment::from_bytes(&bytes, MediaType::parse("image/png").unwrap()), + 2, + 3, + Phase1MediaConfigurationFingerprint::new([9; 32]).unwrap(), + 10, + ) + .unwrap(); + Self { + root, + runtime: Arc::new(runtime), + empty, + receipt, + bytes, + } + } + + fn directory(&self) -> &Path { + self.runtime.inbound_media_directory.as_deref().unwrap() + } + + fn path(&self) -> std::path::PathBuf { + self.directory() + .join(format!("{}.png", self.receipt.artifact_id().to_hex())) + } + + async fn file(&self) { + super::super::super::media::write_verified_artifact( + self.directory(), + &self.receipt, + &self.bytes, + ) + .await + .unwrap(); + } + + async fn state(&self, generation: u64, owned: bool) { + let selected = context(None, generation); + let mut state = self.empty.clone(); + state.context_generation = generation; + if owned { + state + .media_cache + .admit(&self.receipt, Phase1MediaCachePolicy::default(), 11) + .unwrap(); + } + persist_media_state( + &self.runtime, + self.runtime.client.storage().unwrap(), + &selected, + projection_generation().unwrap(), + &mut state, + ) + .await + .unwrap(); + } + + async fn candidates(&self) -> Option<BTreeSet<Phase1MediaArtifactId>> { + unreferenced( + self.runtime.client.storage().unwrap(), + &[self.receipt.artifact_id()], + ) + .await + .unwrap() + } + + async fn collect(&self) -> Result<(), TodayError> { + let files = self.runtime.inbound_media_lock.lock().await; + let projection = self.runtime.today_projection_lock.lock().await; + collect( + &self.runtime, + self.directory(), + &[self.receipt.artifact_id()], + &files, + &projection, + ) + .await + } +} + +#[tokio::test] +async fn invalidating_one_context_preserves_another_then_last_owner_allows_cleanup() { + for sqlite in [false, true] { + shared_context_cleanup(Fixture::backend(sqlite).await).await; + } +} + +async fn shared_context_cleanup(f: Fixture) { + f.file().await; + f.state(1, true).await; + f.state(2, true).await; + assert!( + f.runtime + .phase1_invalidate_media_artifact(&context(None, 1), f.receipt.artifact_id()) + .await + .unwrap() + ); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + assert_eq!( + f.runtime + .phase1_media_cache_status(&context(None, 2)) + .await + .unwrap() + .artifacts, + 1 + ); + f.runtime + .phase1_invalidate_media_configuration( + &context(None, 2), + Phase1MediaConfigurationFingerprint::new([8; 32]).unwrap(), + ) + .await + .unwrap(); + assert!(!f.path().exists()); + f.collect().await.unwrap(); // Interrupted/repeated cleanup is idempotent. + f.runtime.shutdown().await.unwrap(); +} + +#[tokio::test] +async fn complete_scan_reaches_owner_beyond_one_thousand_records() { + let f = Fixture::new().await; + for generation in 1..=1_025 { + f.state(generation, false).await; + } + // Select the last ordered document, not the last insertion. + let selected = (1..=1_025) + .max_by_key(|g| projection_document_key(&context(None, *g))) + .unwrap(); + f.state(selected, true).await; + assert!(f.candidates().await.unwrap().is_empty()); + f.state(selected, false).await; + assert_eq!( + f.candidates().await.unwrap(), + BTreeSet::from([f.receipt.artifact_id()]) + ); +} + +#[tokio::test] +async fn exhausted_inventory_budget_retains_unreferenced_bytes() { + let f = Fixture::new().await; + for generation in 1..=(MAX_PAGES * usize::from(PAGE_ROWS) + 1) as u64 { + f.state(generation, false).await; + } + f.file().await; + assert!(f.candidates().await.is_none()); + f.collect().await.unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); +} + +#[tokio::test] +async fn unknown_generation_corrupt_and_noncanonical_documents_never_authorize_deletion() { + for kind in 0..4 { + let f = Fixture::new().await; + f.file().await; + let mut bytes = encode(&f.empty).unwrap(); + let mut generation = projection_generation().unwrap(); + let mut key = projection_document_key(&context(None, 1)); + match kind { + 0 => generation = ProjectionGeneration::new([55; 32]).unwrap(), + 1 => bytes = b"corrupt".to_vec(), + 2 => { + bytes.pop(); + bytes.extend_from_slice(b",\"futureOwnership\":true}"); + } + _ => key = "unknown.owner".into(), + } + ProjectionStore::put_projection_document( + f.runtime.client.storage().unwrap(), + projection_id().unwrap(), + generation, + ProjectionDocument::new(key, bytes).unwrap(), + ) + .await + .unwrap(); + assert!(f.candidates().await.is_none(), "case {kind}"); + f.collect().await.unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + } +} + +#[tokio::test] +async fn reference_admitted_while_collector_waits_is_observed_before_unlink() { + let f = Fixture::new().await; + f.file().await; + f.state(1, true).await; + let projection = f.runtime.today_projection_lock.lock().await; + let runtime = f.runtime.clone(); + let artifact = f.receipt.artifact_id(); + let task = tokio::spawn(async move { + runtime + .phase1_invalidate_media_artifact(&context(None, 1), artifact) + .await + }); + // Synchronize on the first lock's acquisition; no timing assumption about + // task scheduling or sleeps. The collector cannot acquire the second lock. + tokio::time::timeout(std::time::Duration::from_secs(2), async { + while f.runtime.inbound_media_lock.try_lock().is_ok() { + tokio::task::yield_now().await; + } + }) + .await + .unwrap(); + assert!(!task.is_finished()); + f.state(2, true).await; + drop(projection); + task.await.unwrap().unwrap(); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); +} + +#[tokio::test] +async fn interrupted_unlink_retains_ownership_and_retry_only_removes_unreferenced_file() { + let f = Fixture::new().await; + f.file().await; + let artifact = f.receipt.artifact_id(); + let obstructed = f.directory().join(format!("{}.gif", artifact.to_hex())); + let external = f.root.path().join("external"); + std::fs::write(&external, b"unowned").unwrap(); + std::os::unix::fs::symlink(&external, &obstructed).unwrap(); + assert!(f.collect().await.is_err()); + assert_eq!(std::fs::read(&external).unwrap(), b"unowned"); + assert_eq!(std::fs::read(f.path()).unwrap(), f.bytes); + std::fs::remove_file(obstructed).unwrap(); + f.state(2, true).await; + f.collect().await.unwrap(); + assert!(f.path().exists()); + f.state(2, false).await; + f.collect().await.unwrap(); + assert!(!f.path().exists()); +} + +#[tokio::test] +async fn candidate_limit_retains_excess_and_empty_batch_never_needs_storage() { + let f = Fixture::new().await; + let ids: Vec<_> = (0..=MAX_CANDIDATES) + .map(|i| Phase1MediaArtifactId::from_sha256(BlobHash::digest(&i.to_be_bytes()))) + .collect(); + let result = unreferenced(f.runtime.client.storage().unwrap(), &ids) + .await + .unwrap() + .unwrap(); + assert_eq!(result.len(), MAX_CANDIDATES); + assert!(!result.contains(&ids[MAX_CANDIDATES])); + assert!( + unreferenced(f.runtime.client.storage().unwrap(), &[]) + .await + .unwrap() + .unwrap() + .is_empty() + ); +} diff --git a/core/crates/tera_core/src/runtime/product_surface/today_media_retrieval.rs b/core/crates/tera_core/src/runtime/product_surface/today_media_retrieval.rs @@ -30,7 +30,7 @@ impl TeraRuntime { Ok(artifact) => { state = media_visibility::current_state(self, context).await?; if state.media_cache.touch(artifact_id, observed_at_unix_ms)? { - persist_media_state(storage, context, generation, &mut state).await?; + persist_media_state(self, storage, context, generation, &mut state).await?; } Ok(Some(artifact)) } @@ -38,8 +38,15 @@ impl TeraRuntime { state = media_visibility::current_state(self, context).await?; state.media_cache.invalidate_artifact(artifact_id); invalidate_artifact_references(&mut state, artifact_id); - persist_media_state(storage, context, generation, &mut state).await?; - let _ = super::super::media::remove_artifact_files(directory, artifact_id).await; + persist_media_state(self, storage, context, generation, &mut state).await?; + let _ = media_collection::collect( + self, + directory, + &[artifact_id], + &_guard, + &_projection, + ) + .await; Err(error.into()) } } @@ -202,9 +209,8 @@ impl TeraRuntime { return Err(error); } }; - for artifact_id in evicted { - super::super::media::remove_artifact_files(directory, artifact_id).await?; - } + let _projection = self.today_projection_lock.lock().await; + media_collection::collect(self, directory, &evicted, &_guard, &_projection).await?; Ok(artifact) } } diff --git a/core/crates/tera_core/src/runtime/product_surface/today_scope_tests.rs b/core/crates/tera_core/src/runtime/product_surface/today_scope_tests.rs @@ -398,6 +398,7 @@ async fn corrupt_snapshot_order_and_legacy_unbound_cache_fail_closed() { ); assert_eq!(decode_state(&old_bytes).unwrap(), unbound); store_state( + &runtime, runtime.client.storage().unwrap(), &selected, projection_generation().unwrap(), diff --git a/core/crates/tera_core/src/runtime/product_surface/today_submission_overlay.rs b/core/crates/tera_core/src/runtime/product_surface/today_submission_overlay.rs @@ -78,7 +78,7 @@ impl TeraRuntime { } state.overlays.insert(key, overlay); state.content_generation = content_generation(&state)?; - store_state(storage, context, generation, &state).await?; + store_state(self, storage, context, generation, &state).await?; Ok(true) } } diff --git a/core/crates/tera_core/src/runtime/today_projection.rs b/core/crates/tera_core/src/runtime/today_projection.rs @@ -0,0 +1,98 @@ +//! Serializes Today state and retains cache bytes after uncertain backend writes. +//! +//! Dropping a storage future need not cancel work already dispatched to SQLite. +//! Only a fresh runtime after storage shutdown can discard this uncertainty. + +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use tokio::sync::{Mutex, MutexGuard}; + +#[derive(Default)] +pub(super) struct TodayProjectionFence { + mutex: Mutex<()>, + pending: AtomicUsize, + uncertain: AtomicBool, +} + +impl TodayProjectionFence { + pub(super) async fn lock(&self) -> MutexGuard<'_, ()> { + self.mutex.lock().await + } + + /// Call while holding the Today mutex, before dispatching an ownership write. + pub(super) fn begin_write(&self) -> WritePermit<'_> { + self.pending.fetch_add(1, Ordering::AcqRel); + WritePermit { + fence: self, + completed: false, + } + } + + #[cfg(any(feature = "mobile-social", test))] + pub(super) fn can_collect(&self) -> bool { + self.pending.load(Ordering::Acquire) == 0 && !self.uncertain.load(Ordering::Acquire) + } +} + +pub(super) struct WritePermit<'a> { + fence: &'a TodayProjectionFence, + completed: bool, +} + +impl WritePermit<'_> { + pub(super) fn complete(mut self) { + self.completed = true; + } +} + +impl Drop for WritePermit<'_> { + fn drop(&mut self) { + if !self.completed { + self.fence.uncertain.store(true, Ordering::Release); + } + self.fence.pending.fetch_sub(1, Ordering::Release); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + future::{Future, pending}, + task::{Context, Poll, Waker}, + }; + + #[test] + fn cancelled_dispatched_write_retains_uncertainty_after_later_success() { + let fence = TodayProjectionFence::default(); + let write = || async { + let permit = fence.begin_write(); + pending::<()>().await; + permit.complete(); + }; + drop(write()); // An unpolled future never dispatched a write. + assert!(fence.can_collect()); + let mut future = Box::pin(write()); + assert!(matches!( + future + .as_mut() + .poll(&mut Context::from_waker(Waker::noop())), + Poll::Pending + )); + assert!(!fence.can_collect()); + drop(future); + fence.begin_write().complete(); + assert!(!fence.can_collect()); + } + + #[test] + fn all_acknowledged_writes_must_finish_before_collection() { + let fence = TodayProjectionFence::default(); + let first = fence.begin_write(); + let second = fence.begin_write(); + first.complete(); + assert!(!fence.can_collect()); + second.complete(); + assert!(fence.can_collect()); + } +} diff --git a/release/provenance.json b/release/provenance.json @@ -2,7 +2,7 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "7b3f92a57ebcc7873349403243098d5dcf993b2dbdab9ed84fea4a50c20baab0", - "ffi_provenance_sha256": "bbd348afc051ecac6a621c02d039441dfda73b05d7070ba943a8707664ea33fb", + "ffi_provenance_sha256": "b9e71bb1b065bd36ccfae2ab232edefdbf8372131b0facf8d308baf7df7379de", "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", "sbom_sha256": "a54785d33587ad36c2098709da6511623373a178ff351ef7274b8623437c1373", @@ -22,7 +22,7 @@ "lib_revision": "b6667541b08b4e578199c6612cf51b7402071f5e", "source_date_epoch": 1787871027, "swift_package_lock_sha256": "d5c9240384b50f006ee6e9eff886a3775546280e90a05cbe781eaa296632da26", - "tera_ffi_source_tree": "31064aa6fb8085ae69af38ded58537fc76ee4554", + "tera_ffi_source_tree": "6661f9a8c9bd760a3f24a90125c05450b1c7d530", "xcode_package_lock_sha256": "b6806f710734f8e628d7a7d7b97826d89eea78fba32fa9b464f513bcadd411e2" }, "version": "0.1.0-alpha" diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -4261,6 +4261,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_media_collection_tests.rs", + "count": 1 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_media_visibility_tests.rs", "count": 1 }, @@ -5211,6 +5215,10 @@ "count": 2 }, { + "path": "core/crates/tera_core/src/runtime/product_surface/today_media_collection.rs", + "count": 2 + }, + { "path": "core/crates/tera_core/src/runtime/product_surface/today_sync.rs", "count": 1 },