|
|
… |
47 unchanged lines |
| 48 |
48 |
|
} |
| 49 |
49 |
|
let lookup = *CREDENTIALS.lock().unwrap_or_else(|p| p.into_inner()); |
| 50 |
50 |
|
let given = lookup.and_then(|f| f(url)); |
| 51 |
|
- |
Some(CloudStore::open_with(url, given.as_ref()).map(|s| Box::new(s) as Box<dyn Store>)) |
|
51 |
+ |
Some(shared(url, given).map(|s| Box::new(Shared(s)) as Box<dyn Store>)) |
| 52 |
52 |
|
} |
| 53 |
53 |
|
|
|
54 |
+ |
/// One client per store and credentials, kept for the life of the process |
|
55 |
+ |
/// (oxygenhub#234): a server opens a repository for every request, and a |
|
56 |
+ |
/// new client each time meant a new runtime, new connections and signing |
|
57 |
+ |
/// in to the provider again before the first byte. |
|
58 |
+ |
type OpenStores = std::collections::HashMap<(String, Option<Credentials>), Arc<CloudStore>>; |
|
59 |
+ |
|
|
60 |
+ |
fn shared(url: &str, given: Option<Credentials>) -> Result<Arc<CloudStore>> { |
|
61 |
+ |
static OPEN: Mutex<Option<OpenStores>> = Mutex::new(None); |
|
62 |
+ |
let key = (url.to_string(), given); |
|
63 |
+ |
let mut open = OPEN.lock().unwrap_or_else(|p| p.into_inner()); |
|
64 |
+ |
let map = open.get_or_insert_with(Default::default); |
|
65 |
+ |
if let Some(s) = map.get(&key) { |
|
66 |
+ |
return Ok(s.clone()); |
|
67 |
+ |
} |
|
68 |
+ |
let store = Arc::new(CloudStore::open_with(url, key.1.as_ref())?); |
|
69 |
+ |
map.insert(key, store.clone()); |
|
70 |
+ |
Ok(store) |
|
71 |
+ |
} |
|
72 |
+ |
|
|
73 |
+ |
/// A shared [`CloudStore`] as a [`Store`]. |
|
74 |
+ |
struct Shared(Arc<CloudStore>); |
|
75 |
+ |
|
|
76 |
+ |
impl Store for Shared { |
|
77 |
+ |
fn describe(&self) -> String { |
|
78 |
+ |
self.0.describe() |
|
79 |
+ |
} |
|
80 |
+ |
fn has(&self, key: &str) -> Result<bool> { |
|
81 |
+ |
self.0.has(key) |
|
82 |
+ |
} |
|
83 |
+ |
fn get(&self, key: &str) -> Result<Vec<u8>> { |
|
84 |
+ |
self.0.get(key) |
|
85 |
+ |
} |
|
86 |
+ |
fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> { |
|
87 |
+ |
self.0.get_range(key, range) |
|
88 |
+ |
} |
|
89 |
+ |
fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> { |
|
90 |
+ |
self.0.put_new(key, bytes) |
|
91 |
+ |
} |
|
92 |
+ |
fn list(&self, prefix: &str) -> Result<Vec<Entry>> { |
|
93 |
+ |
self.0.list(prefix) |
|
94 |
+ |
} |
|
95 |
+ |
fn delete(&self, key: &str) -> Result<()> { |
|
96 |
+ |
self.0.delete(key) |
|
97 |
+ |
} |
|
98 |
+ |
fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> { |
|
99 |
+ |
self.0.read_ref(key) |
|
100 |
+ |
} |
|
101 |
+ |
fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> { |
|
102 |
+ |
self.0.write_ref(key, bytes, expected) |
|
103 |
+ |
} |
|
104 |
+ |
} |
|
105 |
+ |
|
|
106 |
+ |
static OPENS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
|
107 |
+ |
static CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
|
108 |
+ |
static MICROS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
|
109 |
+ |
|
|
110 |
+ |
/// What the cloud stores have done since the process started: clients |
|
111 |
+ |
/// opened, requests made and the time spent in them, for a server to show |
|
112 |
+ |
/// (a hub's /health). |
|
113 |
+ |
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
|
114 |
+ |
pub struct Stats { |
|
115 |
+ |
pub opens: u64, |
|
116 |
+ |
pub calls: u64, |
|
117 |
+ |
pub millis: u64, |
|
118 |
+ |
} |
|
119 |
+ |
|
|
120 |
+ |
pub fn stats() -> Stats { |
|
121 |
+ |
use std::sync::atomic::Ordering::Relaxed; |
|
122 |
+ |
Stats { |
|
123 |
+ |
opens: OPENS.load(Relaxed), |
|
124 |
+ |
calls: CALLS.load(Relaxed), |
|
125 |
+ |
millis: MICROS.load(Relaxed) / 1000, |
|
126 |
+ |
} |
|
127 |
+ |
} |
|
128 |
+ |
|
| 54 |
129 |
|
/// Credentials a program keeps itself and hands over, instead of the |
| 55 |
130 |
|
/// provider's usual places: a hub keeps each repository's sealed. When a |
| 56 |
131 |
|
/// store is opened with them, nothing from the environment or the |
| 57 |
132 |
|
/// provider's files is used. |
| 58 |
|
- |
#[derive(Clone, PartialEq, Eq)] |
|
133 |
+ |
#[derive(Clone, PartialEq, Eq, Hash)] |
| 59 |
134 |
|
pub enum Credentials { |
| 60 |
135 |
|
/// An S3 access key. |
| 61 |
136 |
|
Aws { |
|
|
… |
194 unchanged lines |
| 256 |
331 |
|
prefix: String, |
| 257 |
332 |
|
name: String, |
| 258 |
333 |
|
hint: &'static str, |
| 259 |
|
- |
/// Always there until the store is dropped. |
|
334 |
+ |
/// Always there until the store is dropped: two workers, so the |
|
335 |
+ |
/// requests of a shared store run side by side. |
| 260 |
336 |
|
runtime: Option<tokio::runtime::Runtime>, |
| 261 |
|
- |
/// One call at a time: the runtime runs on the calling thread. |
| 262 |
|
- |
turn: Mutex<()>, |
| 263 |
337 |
|
} |
| 264 |
338 |
|
|
| 265 |
339 |
|
impl Drop for CloudStore { |
|
|
… |
53 unchanged lines |
| 319 |
393 |
|
)), |
| 320 |
394 |
|
_ => None, |
| 321 |
395 |
|
}; |
| 322 |
|
- |
let runtime = tokio::runtime::Builder::new_current_thread() |
|
396 |
+ |
OPENS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
|
397 |
+ |
let runtime = tokio::runtime::Builder::new_multi_thread() |
|
398 |
+ |
.worker_threads(2) |
|
399 |
+ |
.thread_name("oxy-strata") |
| 323 |
400 |
|
.enable_all() |
| 324 |
401 |
|
.build() |
| 325 |
402 |
|
.map_err(|e| Error::Backend(format!("starting the HTTP runtime: {e}")))?; |
|
|
… |
25 unchanged lines |
| 351 |
428 |
|
name: parsed.plain(), |
| 352 |
429 |
|
hint, |
| 353 |
430 |
|
runtime: Some(runtime), |
| 354 |
|
- |
turn: Mutex::new(()), |
| 355 |
431 |
|
}) |
| 356 |
432 |
|
} |
| 357 |
433 |
|
|
|
|
… |
5 unchanged lines |
| 363 |
439 |
|
F: Future + Send, |
| 364 |
440 |
|
F::Output: Send, |
| 365 |
441 |
|
{ |
| 366 |
|
- |
let _turn = self.turn.lock().unwrap_or_else(|p| p.into_inner()); |
| 367 |
442 |
|
let runtime = self |
| 368 |
443 |
|
.runtime |
| 369 |
444 |
|
.as_ref() |
| 370 |
445 |
|
.expect("the runtime lives as long as the store"); |
| 371 |
|
- |
if tokio::runtime::Handle::try_current().is_ok() { |
| 372 |
|
- |
return std::thread::scope(|s| { |
|
446 |
+ |
let started = std::time::Instant::now(); |
|
447 |
+ |
let out = if tokio::runtime::Handle::try_current().is_ok() { |
|
448 |
+ |
std::thread::scope(|s| { |
| 373 |
449 |
|
s.spawn(|| runtime.block_on(f)) |
| 374 |
450 |
|
.join() |
| 375 |
451 |
|
.unwrap_or_else(|p| std::panic::resume_unwind(p)) |
| 376 |
|
- |
}); |
| 377 |
|
- |
} |
| 378 |
|
- |
runtime.block_on(f) |
|
452 |
+ |
}) |
|
453 |
+ |
} else { |
|
454 |
+ |
runtime.block_on(f) |
|
455 |
+ |
}; |
|
456 |
+ |
CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
|
457 |
+ |
MICROS.fetch_add( |
|
458 |
+ |
started.elapsed().as_micros() as u64, |
|
459 |
+ |
std::sync::atomic::Ordering::Relaxed, |
|
460 |
+ |
); |
|
461 |
+ |
out |
| 379 |
462 |
|
} |
| 380 |
463 |
|
|
| 381 |
464 |
|
fn path(&self, key: &str) -> Result<ObjectPath> { |
|
|
… |
220 unchanged lines |
| 602 |
685 |
|
|
| 603 |
686 |
|
#[cfg(test)] |
| 604 |
687 |
|
mod tests { |
|
688 |
+ |
|
|
689 |
+ |
/// One client per store and credentials (oxygenhub#234): opening the |
|
690 |
+ |
/// same again shares it, and only a new one counts as opened. |
|
691 |
+ |
#[test] |
|
692 |
+ |
fn a_store_opened_again_shares_its_client() { |
|
693 |
+ |
let creds = |k: &str| Credentials::Aws { |
|
694 |
+ |
access_key_id: k.into(), |
|
695 |
+ |
secret_access_key: "s".into(), |
|
696 |
+ |
session_token: None, |
|
697 |
+ |
}; |
|
698 |
+ |
let url = "s3://oxy-shared-test/repos/a?region=eu-west-2"; |
|
699 |
+ |
let before = super::stats().opens; |
|
700 |
+ |
let a = super::shared(url, Some(creds("one"))).unwrap(); |
|
701 |
+ |
let b = super::shared(url, Some(creds("one"))).unwrap(); |
|
702 |
+ |
assert!(std::sync::Arc::ptr_eq(&a, &b)); |
|
703 |
+ |
let other_keys = super::shared(url, Some(creds("two"))).unwrap(); |
|
704 |
+ |
assert!( |
|
705 |
+ |
!std::sync::Arc::ptr_eq(&a, &other_keys), |
|
706 |
+ |
"other credentials, another client" |
|
707 |
+ |
); |
|
708 |
+ |
assert!( |
|
709 |
+ |
super::stats().opens - before >= 2, |
|
710 |
+ |
"two clients opened (other tests may open more)" |
|
711 |
+ |
); |
|
712 |
+ |
} |
|
713 |
+ |
|
| 605 |
714 |
|
#[tokio::test] |
| 606 |
715 |
|
async fn a_store_can_be_used_from_inside_a_runtime() { |
| 607 |
716 |
|
// As a server's handler would: blocking on the store's own runtime |
|
|
… |
83 unchanged lines |