Skip to main content

fidl_fuchsia_pkg_rewrite_ext/
transaction.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::EditTransactionError;
6use crate::rule::Rule;
7use flex_client::ProxyHasDomain;
8use flex_fuchsia_pkg_rewrite as rewrite;
9use flex_fuchsia_pkg_rewrite::{EditTransactionProxy, EngineProxy};
10use std::future::Future;
11use zx_status as zx;
12
13const RETRY_ATTEMPTS: usize = 100;
14
15/// A helper for managing the editing of rewrite rules.
16pub struct EditTransaction {
17    transaction: EditTransactionProxy,
18}
19
20impl EditTransaction {
21    /// Removes all dynamically configured rewrite rules, leaving only any
22    /// statically configured rules.
23    pub fn reset_all(&self) -> Result<(), EditTransactionError> {
24        self.transaction.reset_all().map_err(EditTransactionError::from)
25    }
26
27    /// Returns a vector of all dynamic (editable) rewrite rules. The
28    /// vector will reflect any changes made to the rewrite rules so far in
29    /// this transaction.
30    pub async fn list_dynamic(&self) -> Result<Vec<Rule>, EditTransactionError> {
31        let (iter, iter_server_end) =
32            self.transaction.domain().create_proxy::<rewrite::RuleIteratorMarker>();
33        self.transaction.list_dynamic(iter_server_end)?;
34
35        let mut rules = Vec::new();
36        loop {
37            let chunk = iter.next().await?;
38            if chunk.is_empty() {
39                break;
40            }
41
42            for rule in chunk {
43                rules.push(Rule::try_from(rule)?);
44            }
45        }
46
47        Ok(rules)
48    }
49
50    /// Adds a rewrite rule with highest priority. If `rule` already exists, this
51    /// API will prioritize it over other rules.
52    pub async fn add(&self, rule: Rule) -> Result<(), EditTransactionError> {
53        self.transaction
54            .add(&rule.into())
55            .await?
56            .map_err(|err| EditTransactionError::AddError(zx::Status::from_raw(err)))
57    }
58}
59
60/// Perform a rewrite rule edit transaction, retrying as necessary if another edit transaction runs
61/// concurrently.
62///
63/// The given callback `cb` should perform the needed edits to the state of the rewrite rules but
64/// not attempt to `commit()` the transaction. `do_transaction` will internally attempt to commit
65/// the transaction and trigger a retry if necessary.
66pub async fn do_transaction<T, R>(engine: &EngineProxy, cb: T) -> Result<(), EditTransactionError>
67where
68    T: Fn(EditTransaction) -> R,
69    R: Future<Output = Result<EditTransaction, EditTransactionError>>,
70{
71    // Make a reasonable effort to retry the edit after a concurrent edit, but don't retry forever.
72    for _ in 0..RETRY_ATTEMPTS {
73        let (transaction, transaction_server_end) =
74            engine.domain().create_proxy::<rewrite::EditTransactionMarker>();
75
76        let () = engine
77            .start_edit_transaction(transaction_server_end)
78            .map_err(EditTransactionError::from)?;
79
80        let transaction = cb(EditTransaction { transaction }).await?;
81
82        let response =
83            transaction.transaction.commit().await.map_err(EditTransactionError::from)?;
84
85        // Retry edit transaction on concurrent edit
86        return match response.map_err(zx::Status::from_raw) {
87            Ok(()) => Ok(()),
88            Err(zx::Status::UNAVAILABLE) => {
89                continue;
90            }
91            Err(status) => Err(EditTransactionError::CommitError(status)),
92        };
93    }
94
95    Err(EditTransactionError::CommitError(zx::Status::UNAVAILABLE))
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use assert_matches::assert_matches;
102    use flex_fuchsia_pkg_rewrite::{
103        EditTransactionRequest, EngineMarker, EngineRequest, RuleIteratorRequest,
104    };
105    use fuchsia_async as fasync;
106    use futures::TryStreamExt;
107    use std::sync::atomic::{AtomicUsize, Ordering};
108    use std::sync::{Arc, Mutex};
109
110    #[derive(Debug, PartialEq)]
111    enum Event {
112        ResetAll,
113        ListDynamic,
114        IteratorNext,
115        Add(Rule),
116        CommitFailed,
117        Commit,
118    }
119
120    struct Engine {
121        engine: EngineProxy,
122        events: Arc<Mutex<Vec<Event>>>,
123        #[cfg(feature = "fdomain")]
124        _client: Arc<flex_client::Client>,
125    }
126
127    macro_rules! rule {
128        ($host_match:expr => $host_replacement:expr,
129         $path_prefix_match:expr => $path_prefix_replacement:expr) => {
130            Rule::new($host_match, $host_replacement, $path_prefix_match, $path_prefix_replacement)
131                .unwrap()
132        };
133    }
134
135    impl Engine {
136        fn new() -> Self {
137            Self::with_fail_attempts(0, zx::Status::OK)
138        }
139
140        fn with_fail_attempts(mut fail_attempts: usize, fail_status: zx::Status) -> Self {
141            #[cfg(feature = "fdomain")]
142            let client = fdomain_local::local_client_empty();
143            #[cfg(not(feature = "fdomain"))]
144            let client = flex_client::fidl::ZirconClient;
145            let events = Arc::new(Mutex::new(Vec::new()));
146            let events_task = Arc::clone(&events);
147
148            let (engine, mut engine_stream) = client.create_proxy_and_stream::<EngineMarker>();
149
150            fasync::Task::local(async move {
151                while let Some(req) = engine_stream.try_next().await.unwrap() {
152                    match req {
153                        EngineRequest::StartEditTransaction { transaction, control_handle: _ } => {
154                            let mut tx_stream = transaction.into_stream();
155
156                            while let Some(req) = tx_stream.try_next().await.unwrap() {
157                                match req {
158                                    EditTransactionRequest::ResetAll { control_handle: _ } => {
159                                        events_task.lock().unwrap().push(Event::ResetAll);
160                                    }
161                                    EditTransactionRequest::ListDynamic {
162                                        iterator,
163                                        control_handle: _,
164                                    } => {
165                                        events_task.lock().unwrap().push(Event::ListDynamic);
166                                        let mut stream = iterator.into_stream();
167
168                                        let mut rules = vec![
169                                            rule!("fuchsia.com" => "example.com", "/" => "/"),
170                                            rule!("fuchsia.com" => "mycorp.com", "/" => "/"),
171                                        ]
172                                        .into_iter();
173
174                                        while let Some(req) = stream.try_next().await.unwrap() {
175                                            let RuleIteratorRequest::Next { responder } = req;
176                                            events_task.lock().unwrap().push(Event::IteratorNext);
177
178                                            if let Some(rule) = rules.next() {
179                                                responder.send(&[rule.into()]).unwrap();
180                                            } else {
181                                                responder.send(&[]).unwrap();
182                                            }
183                                        }
184                                    }
185                                    EditTransactionRequest::Add { rule, responder } => {
186                                        events_task
187                                            .lock()
188                                            .unwrap()
189                                            .push(Event::Add(rule.try_into().unwrap()));
190                                        responder.send(Ok(())).unwrap();
191                                    }
192                                    EditTransactionRequest::Commit { responder } => {
193                                        if fail_attempts > 0 {
194                                            fail_attempts -= 1;
195                                            events_task.lock().unwrap().push(Event::CommitFailed);
196                                            responder.send(Err(fail_status.into_raw())).unwrap();
197                                        } else {
198                                            events_task.lock().unwrap().push(Event::Commit);
199                                            responder.send(Ok(())).unwrap();
200                                        }
201                                    }
202                                }
203                            }
204                        }
205                        _ => {
206                            panic!("unexpected reqest: {:?}", req);
207                        }
208                    }
209                }
210            })
211            .detach();
212
213            Self {
214                engine,
215                events,
216                #[cfg(feature = "fdomain")]
217                _client: client,
218            }
219        }
220
221        fn take_events(&self) -> Vec<Event> {
222            self.events.lock().unwrap().drain(..).collect()
223        }
224    }
225
226    #[fuchsia::test]
227    async fn test_do_transaction_empty_always_commits() {
228        let engine = Engine::new();
229
230        do_transaction(&engine.engine, |transaction| async { Ok(transaction) }).await.unwrap();
231
232        assert_eq!(engine.take_events(), vec![Event::Commit]);
233    }
234
235    #[fuchsia::test]
236    async fn test_do_transaction_reset_all() {
237        let engine = Engine::new();
238
239        do_transaction(&engine.engine, |transaction| async {
240            transaction.reset_all()?;
241            Ok(transaction)
242        })
243        .await
244        .unwrap();
245
246        assert_eq!(engine.take_events(), vec![Event::ResetAll, Event::Commit]);
247    }
248
249    #[fuchsia::test]
250    async fn test_do_transaction_list_dynamic() {
251        let engine = Engine::new();
252
253        do_transaction(&engine.engine, |transaction| async {
254            let rules = transaction.list_dynamic().await?;
255            assert_eq!(
256                rules,
257                vec![
258                    rule!("fuchsia.com" => "example.com", "/" => "/"),
259                    rule!("fuchsia.com" => "mycorp.com", "/" => "/"),
260                ]
261            );
262            Ok(transaction)
263        })
264        .await
265        .unwrap();
266
267        assert_eq!(
268            engine.take_events(),
269            // We should get three iterators. The first two get the rules, the last gets nothing.
270            vec![
271                Event::ListDynamic,
272                Event::IteratorNext,
273                Event::IteratorNext,
274                Event::IteratorNext,
275                Event::Commit
276            ]
277        );
278    }
279
280    #[fuchsia::test]
281    async fn test_do_transaction_add() {
282        let engine = Engine::new();
283
284        let attempts = Arc::new(AtomicUsize::new(0));
285        do_transaction(&engine.engine, |transaction| async {
286            attempts.fetch_add(1, Ordering::SeqCst);
287            transaction.add(rule!("foo.com" => "bar.com", "/" => "/")).await?;
288            transaction.add(rule!("baz.com" => "boo.com", "/" => "/")).await?;
289            Ok(transaction)
290        })
291        .await
292        .unwrap();
293
294        assert_eq!(attempts.load(Ordering::SeqCst), 1);
295        assert_eq!(
296            engine.take_events(),
297            vec![
298                Event::Add(rule!("foo.com" => "bar.com", "/" => "/")),
299                Event::Add(rule!("baz.com" => "boo.com", "/" => "/")),
300                Event::Commit,
301            ],
302        );
303    }
304
305    #[fuchsia::test]
306    async fn test_do_transaction_closure_error_does_not_commit() {
307        let engine = Engine::new();
308
309        let attempts = Arc::new(AtomicUsize::new(0));
310        let err = do_transaction(&engine.engine, |_transaction| async {
311            attempts.fetch_add(1, Ordering::SeqCst);
312            Err(EditTransactionError::AddError(zx::Status::INTERNAL))
313        })
314        .await
315        .unwrap_err();
316
317        assert_eq!(attempts.load(Ordering::SeqCst), 1);
318        assert_matches!(err, EditTransactionError::AddError(zx::Status::INTERNAL));
319        assert_eq!(engine.take_events(), vec![]);
320    }
321
322    #[fuchsia::test]
323    async fn test_do_transaction_retries_commit_errors() {
324        let engine = Engine::with_fail_attempts(5, zx::Status::UNAVAILABLE);
325
326        let attempts = Arc::new(AtomicUsize::new(0));
327        do_transaction(&engine.engine, |transaction| async {
328            attempts.fetch_add(1, Ordering::SeqCst);
329            Ok(transaction)
330        })
331        .await
332        .unwrap();
333
334        assert_eq!(attempts.load(Ordering::SeqCst), 6);
335        assert_eq!(
336            engine.take_events(),
337            vec![
338                Event::CommitFailed,
339                Event::CommitFailed,
340                Event::CommitFailed,
341                Event::CommitFailed,
342                Event::CommitFailed,
343                Event::Commit,
344            ]
345        );
346    }
347
348    #[fuchsia::test]
349    async fn test_do_transaction_eventually_gives_up() {
350        let engine = Engine::with_fail_attempts(RETRY_ATTEMPTS + 1, zx::Status::UNAVAILABLE);
351
352        let attempts = Arc::new(AtomicUsize::new(0));
353        let err = do_transaction(&engine.engine, |transaction| async {
354            attempts.fetch_add(1, Ordering::SeqCst);
355            Ok(transaction)
356        })
357        .await
358        .unwrap_err();
359
360        assert_eq!(attempts.load(Ordering::SeqCst), RETRY_ATTEMPTS);
361        assert_matches!(err, EditTransactionError::CommitError(zx::Status::UNAVAILABLE));
362        assert_eq!(
363            engine.take_events(),
364            (0..RETRY_ATTEMPTS).map(|_| Event::CommitFailed).collect::<Vec<_>>(),
365        );
366    }
367
368    #[fuchsia::test]
369    async fn test_do_transaction_does_not_retry_other_errors() {
370        let engine = Engine::with_fail_attempts(5, zx::Status::INTERNAL);
371
372        let attempts = Arc::new(AtomicUsize::new(0));
373        let err = do_transaction(&engine.engine, |transaction| async {
374            attempts.fetch_add(1, Ordering::SeqCst);
375            Ok(transaction)
376        })
377        .await
378        .unwrap_err();
379
380        assert_eq!(attempts.load(Ordering::SeqCst), 1);
381        assert_matches!(err, EditTransactionError::CommitError(zx::Status::INTERNAL));
382        assert_eq!(engine.take_events(), vec![Event::CommitFailed,]);
383    }
384}