1616 *
1717 */
1818
19+ use std:: { future:: Future , io} ;
20+
1921use crate :: {
2022 parseable:: PARSEABLE ,
2123 storage:: { self , ObjectStorageError , StorageMetadata } ,
@@ -33,11 +35,108 @@ pub async fn get_metadata(
3335 Ok ( serde_json:: from_slice :: < StorageMetadata > ( & metadata) ?)
3436}
3537
38+ /// Persist authoritative metadata and refresh its best-effort local staging copy.
39+ /// Success lets callers publish the committed change to live RBAC state.
3640pub async fn put_metadata (
3741 metadata : & StorageMetadata ,
3842 tenant_id : & Option < String > ,
3943) -> Result < ( ) , ObjectStorageError > {
40- storage:: put_remote_metadata ( metadata, tenant_id) . await ?;
41- storage:: put_staging_metadata ( metadata, tenant_id) ?;
44+ put_metadata_with_writers (
45+ storage:: put_remote_metadata ( metadata, tenant_id) ,
46+ || storage:: put_staging_metadata ( metadata, tenant_id) ,
47+ tenant_id,
48+ )
49+ . await
50+ }
51+
52+ async fn put_metadata_with_writers (
53+ remote_write : impl Future < Output = Result < ( ) , ObjectStorageError > > ,
54+ staging_write : impl FnOnce ( ) -> io:: Result < ( ) > ,
55+ tenant_id : & Option < String > ,
56+ ) -> Result < ( ) , ObjectStorageError > {
57+ remote_write. await ?;
58+ // A staging failure cannot undo the durable commit. Returning an error here
59+ // would leave callers' live authorization state behind the source of truth.
60+ if let Err ( error) = staging_write ( ) {
61+ tracing:: error!(
62+ error = %error,
63+ tenant_id = ?tenant_id,
64+ "Failed to refresh staging metadata after durable metadata write"
65+ ) ;
66+ }
4267 Ok ( ( ) )
4368}
69+
70+ #[ cfg( test) ]
71+ mod tests {
72+ use std:: cell:: Cell ;
73+
74+ use super :: * ;
75+
76+ #[ tokio:: test]
77+ async fn staging_failure_after_durable_success_returns_success ( ) {
78+ let durable_written = Cell :: new ( false ) ;
79+ let staging_attempted = Cell :: new ( false ) ;
80+ let result = put_metadata_with_writers (
81+ async {
82+ durable_written. set ( true ) ;
83+ Ok ( ( ) )
84+ } ,
85+ || {
86+ assert ! (
87+ durable_written. get( ) ,
88+ "durable metadata must be written first"
89+ ) ;
90+ staging_attempted. set ( true ) ;
91+ Err ( io:: Error :: new (
92+ io:: ErrorKind :: PermissionDenied ,
93+ "staging is read-only" ,
94+ ) )
95+ } ,
96+ & None ,
97+ )
98+ . await ;
99+
100+ assert ! ( staging_attempted. get( ) ) ;
101+ assert ! (
102+ result. is_ok( ) ,
103+ "callers must publish durable changes to live authorization: {result:?}"
104+ ) ;
105+ }
106+
107+ #[ tokio:: test]
108+ async fn durable_failure_is_returned_without_attempting_staging ( ) {
109+ let result = put_metadata_with_writers (
110+ async { Err ( ObjectStorageError :: Custom ( "durable write failed" . into ( ) ) ) } ,
111+ || panic ! ( "staging must not be changed after a failed durable write" ) ,
112+ & None ,
113+ )
114+ . await ;
115+
116+ assert ! (
117+ matches!( result, Err ( ObjectStorageError :: Custom ( message) ) if message == "durable write failed" )
118+ ) ;
119+ }
120+
121+ #[ tokio:: test]
122+ async fn successful_writes_run_durable_before_staging ( ) {
123+ let durable_written = Cell :: new ( false ) ;
124+ let staging_written = Cell :: new ( false ) ;
125+ let result = put_metadata_with_writers (
126+ async {
127+ durable_written. set ( true ) ;
128+ Ok ( ( ) )
129+ } ,
130+ || {
131+ assert ! ( durable_written. get( ) ) ;
132+ staging_written. set ( true ) ;
133+ Ok ( ( ) )
134+ } ,
135+ & None ,
136+ )
137+ . await ;
138+
139+ assert ! ( result. is_ok( ) ) ;
140+ assert ! ( staging_written. get( ) ) ;
141+ }
142+ }
0 commit comments