The single global repo lock becomes one lock per marketplace, and refresh_pins takes each marketplace's lock only while setting its pins, so a slow fetch no longer queues installs and refreshes of other marketplaces. refresh_marketplace now takes the lock first and reads the settings store under it (a closure, not a copy captured earlier); a marketplace removed meanwhile gets no cache and no snapshot. Removing a marketplace deletes its snapshot and cache under the same lock (remove_marketplace_cache). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -322,7 +322,8 @@ pub(crate) mod ops {
|
|||||||
branch: None,
|
branch: None,
|
||||||
account_id: None,
|
account_id: None,
|
||||||
});
|
});
|
||||||
let snap = crate::marketplace::refresh_marketplace(&mgr, &settings, "m1").await;
|
let snap =
|
||||||
|
crate::marketplace::refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
|
|
||||||
let repo = crate::marketplace::git::cache_path(data.path(), "m1");
|
let repo = crate::marketplace::git::cache_path(data.path(), "m1");
|
||||||
let tree = GitTree::open(&repo, &head).unwrap();
|
let tree = GitTree::open(&repo, &head).unwrap();
|
||||||
@@ -568,21 +569,23 @@ async fn snapshot_blocking(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Make each cache's pin refs exactly the commits installs reference, so a
|
/// Make each cache's pin refs exactly the commits installs reference, so a
|
||||||
/// pinned version can never be garbage-collected away. Under the repo lock
|
/// pinned version can never be garbage-collected away. Each marketplace's
|
||||||
/// (pre-flight F11): a concurrent fetch writes refs in the same repos.
|
/// pins are set under that marketplace's repo lock only (pre-flight F11, PR
|
||||||
|
/// review #5): a concurrent fetch writes refs in the same repo.
|
||||||
pub(crate) async fn refresh_pins(state: &AppState) {
|
pub(crate) async fn refresh_pins(state: &AppState) {
|
||||||
let settings = state.settings_store.get();
|
let settings = state.settings_store.get();
|
||||||
let pins = mk::pins_by_marketplace(&settings, &state.projects_store.list());
|
let pins = mk::pins_by_marketplace(&settings, &state.projects_store.list());
|
||||||
let root = state.marketplace.data_root().to_path_buf();
|
let root = state.marketplace.data_root().to_path_buf();
|
||||||
let ids: Vec<String> = settings.marketplaces.iter().map(|m| m.id.clone()).collect();
|
for m in &settings.marketplaces {
|
||||||
let _repo_guard = state.marketplace.repo_lock().lock().await;
|
let lock = state.marketplace.repo_lock(&m.id);
|
||||||
|
let _repo_guard = lock.lock().await;
|
||||||
|
let repo = git::cache_path(&root, &m.id);
|
||||||
|
let commits = pins.get(&m.id).cloned().unwrap_or_default();
|
||||||
|
let id = m.id.clone();
|
||||||
let _ = tokio::task::spawn_blocking(move || {
|
let _ = tokio::task::spawn_blocking(move || {
|
||||||
for id in ids {
|
|
||||||
let repo = git::cache_path(&root, &id);
|
|
||||||
if !repo.exists() {
|
if !repo.exists() {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
let commits = pins.get(&id).cloned().unwrap_or_default();
|
|
||||||
if let Err(e) = git::set_pins(&repo, &commits) {
|
if let Err(e) = git::set_pins(&repo, &commits) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"Could not update the pinned commits of marketplace {}: {}",
|
"Could not update the pinned commits of marketplace {}: {}",
|
||||||
@@ -590,28 +593,14 @@ pub(crate) async fn refresh_pins(state: &AppState) {
|
|||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forget a marketplace's snapshot and delete its cache, under the repo lock.
|
/// Forget a marketplace's snapshot and delete its cache, under its repo lock.
|
||||||
pub(crate) async fn remove_cache(state: &AppState, marketplace_id: &str) {
|
pub(crate) async fn remove_cache(state: &AppState, marketplace_id: &str) {
|
||||||
state.marketplace.remove_snapshot(marketplace_id);
|
mk::remove_marketplace_cache(&state.marketplace, marketplace_id).await;
|
||||||
let path = git::cache_path(state.marketplace.data_root(), marketplace_id);
|
|
||||||
let _repo_guard = state.marketplace.repo_lock().lock().await;
|
|
||||||
let _ = tokio::task::spawn_blocking(move || {
|
|
||||||
if path.exists() {
|
|
||||||
if let Err(e) = std::fs::remove_dir_all(&path) {
|
|
||||||
log::warn!(
|
|
||||||
"Could not delete the marketplace cache {}: {}",
|
|
||||||
path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_new_account(
|
fn save_new_account(
|
||||||
@@ -665,7 +654,7 @@ pub async fn refresh_marketplaces(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|m| marketplace_id.as_deref().is_none_or(|id| id == m.id))
|
.filter(|m| marketplace_id.as_deref().is_none_or(|id| id == m.id))
|
||||||
{
|
{
|
||||||
mk::refresh_marketplace(&state.marketplace, &settings, &m.id).await;
|
mk::refresh_marketplace(&state.marketplace, &|| state.settings_store.get(), &m.id).await;
|
||||||
}
|
}
|
||||||
refresh_pins(&state).await;
|
refresh_pins(&state).await;
|
||||||
list_marketplace_snapshots(state).await
|
list_marketplace_snapshots(state).await
|
||||||
@@ -696,7 +685,8 @@ pub async fn add_marketplace(
|
|||||||
|
|
||||||
let mut trial = settings.clone();
|
let mut trial = settings.clone();
|
||||||
trial.marketplaces.push(m.clone());
|
trial.marketplaces.push(m.clone());
|
||||||
let snap = mk::refresh_marketplace(&state.marketplace, &trial, &m.id).await;
|
// Not yet in the store: the trial settings stand in for it.
|
||||||
|
let snap = mk::refresh_marketplace(&state.marketplace, &|| trial.clone(), &m.id).await;
|
||||||
let failure = snap.fetch_error.clone().or_else(|| {
|
let failure = snap.fetch_error.clone().or_else(|| {
|
||||||
snap.head_commit
|
snap.head_commit
|
||||||
.is_none()
|
.is_none()
|
||||||
|
|||||||
@@ -311,10 +311,14 @@ pub fn run() {
|
|||||||
// Failures are logged, not toasted — the Marketplace tab shows them.
|
// Failures are logged, not toasted — the Marketplace tab shows them.
|
||||||
{
|
{
|
||||||
let settings = settings_store_setup.get();
|
let settings = settings_store_setup.get();
|
||||||
|
let settings_store = settings_store_setup.clone();
|
||||||
let marketplace = marketplace_setup.clone();
|
let marketplace = marketplace_setup.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
for m in &settings.marketplaces {
|
for m in &settings.marketplaces {
|
||||||
let snap = crate::marketplace::refresh_marketplace(&marketplace, &settings, &m.id).await;
|
// Reads the store again under the lock: one removed
|
||||||
|
// since startup is skipped (PR review #6).
|
||||||
|
let current = || settings_store.get();
|
||||||
|
let snap = crate::marketplace::refresh_marketplace(&marketplace, ¤t, &m.id).await;
|
||||||
if let Some(e) = snap.fetch_error {
|
if let Some(e) = snap.fetch_error {
|
||||||
log::warn!("Marketplace \"{}\" could not be refreshed at startup: {}", m.name, e);
|
log::warn!("Marketplace \"{}\" could not be refreshed at startup: {}", m.name, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ pub struct MarketplaceManager {
|
|||||||
snapshots: Mutex<HashMap<String, MarketplaceSnapshot>>,
|
snapshots: Mutex<HashMap<String, MarketplaceSnapshot>>,
|
||||||
reports: Mutex<HashMap<String, SyncReport>>,
|
reports: Mutex<HashMap<String, SyncReport>>,
|
||||||
gh_login_cancel: tokio::sync::Mutex<Option<oneshot::Sender<()>>>,
|
gh_login_cancel: tokio::sync::Mutex<Option<oneshot::Sender<()>>>,
|
||||||
/// Serialises writers of the bare caches (fetch, pins, cache removal) so
|
/// One lock per marketplace cache, serialising its writers (fetch, pins,
|
||||||
/// concurrent refreshes never race on gix ref locks (pre-flight F11a).
|
/// cache removal) so they never race on gix ref locks (pre-flight F11a),
|
||||||
repo_lock: tokio::sync::Mutex<()>,
|
/// without one marketplace's fetch holding up another (PR review #5).
|
||||||
|
repo_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
|
||||||
/// One lock per project, held for a whole `sync_project`, so a start sync
|
/// One lock per project, held for a whole `sync_project`, so a start sync
|
||||||
/// and Apply now never run `sync.sh` in one container at once (F11b).
|
/// and Apply now never run `sync.sh` in one container at once (F11b).
|
||||||
sync_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
|
sync_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
|
||||||
@@ -62,7 +63,7 @@ impl MarketplaceManager {
|
|||||||
snapshots: Mutex::new(HashMap::new()),
|
snapshots: Mutex::new(HashMap::new()),
|
||||||
reports: Mutex::new(HashMap::new()),
|
reports: Mutex::new(HashMap::new()),
|
||||||
gh_login_cancel: tokio::sync::Mutex::new(None),
|
gh_login_cancel: tokio::sync::Mutex::new(None),
|
||||||
repo_lock: tokio::sync::Mutex::new(()),
|
repo_locks: Mutex::new(HashMap::new()),
|
||||||
sync_locks: Mutex::new(HashMap::new()),
|
sync_locks: Mutex::new(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,10 +72,16 @@ impl MarketplaceManager {
|
|||||||
&self.data_root
|
&self.data_root
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hold while writing to any marketplace cache (fetch, `git::set_pins`,
|
/// The marketplace's cache lock. Hold it while writing to that cache
|
||||||
/// removing a cache).
|
/// (fetch, `git::set_pins`, removing it) and never drop it mid-fetch: a
|
||||||
pub fn repo_lock(&self) -> &tokio::sync::Mutex<()> {
|
/// blocking fetch keeps running after its future is cancelled.
|
||||||
&self.repo_lock
|
pub fn repo_lock(&self, marketplace_id: &str) -> Arc<tokio::sync::Mutex<()>> {
|
||||||
|
self.repo_locks
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.entry(marketplace_id.to_string())
|
||||||
|
.or_default()
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The project's sync lock; see `sync_project`.
|
/// The project's sync lock; see `sync_project`.
|
||||||
@@ -240,14 +247,23 @@ fn failed_snapshot(
|
|||||||
snap
|
snap
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh one marketplace: resolve the credential, fetch (blocking task, under
|
/// Refresh one marketplace: resolve the credential, fetch (blocking task),
|
||||||
/// the repo lock), parse the catalog at head and store the snapshot. On failure
|
/// parse the catalog at head and store the snapshot. On failure the previous
|
||||||
/// the previous items and head are kept and `fetch_error` is set.
|
/// items and head are kept and `fetch_error` is set.
|
||||||
|
///
|
||||||
|
/// Everything runs under the marketplace's repo lock, and `current_settings`
|
||||||
|
/// (the settings store as it is *now*, not a copy taken before the lock) is
|
||||||
|
/// read only once the lock is held: a marketplace removed meanwhile gets no
|
||||||
|
/// cache and no snapshot (PR review #6), since its removal deletes both
|
||||||
|
/// under the same lock.
|
||||||
pub async fn refresh_marketplace(
|
pub async fn refresh_marketplace(
|
||||||
mgr: &MarketplaceManager,
|
mgr: &MarketplaceManager,
|
||||||
settings: &AppSettings,
|
current_settings: &(dyn Fn() -> AppSettings + Sync),
|
||||||
marketplace_id: &str,
|
marketplace_id: &str,
|
||||||
) -> MarketplaceSnapshot {
|
) -> MarketplaceSnapshot {
|
||||||
|
let lock = mgr.repo_lock(marketplace_id);
|
||||||
|
let _repo_guard = lock.lock().await;
|
||||||
|
let settings = current_settings();
|
||||||
let Some(m) = settings
|
let Some(m) = settings
|
||||||
.marketplaces
|
.marketplaces
|
||||||
.iter()
|
.iter()
|
||||||
@@ -275,15 +291,12 @@ pub async fn refresh_marketplace(
|
|||||||
|
|
||||||
let repo = git::cache_path(mgr.data_root(), &m.id);
|
let repo = git::cache_path(mgr.data_root(), &m.id);
|
||||||
let (url, branch) = (m.url.clone(), m.branch.clone());
|
let (url, branch) = (m.url.clone(), m.branch.clone());
|
||||||
let joined = {
|
let joined = tokio::task::spawn_blocking(move || {
|
||||||
let _repo_guard = mgr.repo_lock.lock().await;
|
|
||||||
tokio::task::spawn_blocking(move || {
|
|
||||||
let head = git::fetch(&repo, &url, branch.as_deref(), cred)?;
|
let head = git::fetch(&repo, &url, branch.as_deref(), cred)?;
|
||||||
let items = parse_at(&repo, &head).map_err(git::FetchError::Other)?;
|
let items = parse_at(&repo, &head).map_err(git::FetchError::Other)?;
|
||||||
Ok::<_, git::FetchError>((head, items))
|
Ok::<_, git::FetchError>((head, items))
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
};
|
|
||||||
|
|
||||||
match joined {
|
match joined {
|
||||||
Ok(Ok((head, items))) => {
|
Ok(Ok((head, items))) => {
|
||||||
@@ -306,6 +319,28 @@ pub async fn refresh_marketplace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Forget a marketplace's snapshot and delete its cache, under its repo lock,
|
||||||
|
/// so a refresh already under way either finishes first (and is then
|
||||||
|
/// deleted) or sees the marketplace gone and stores nothing.
|
||||||
|
pub async fn remove_marketplace_cache(mgr: &MarketplaceManager, marketplace_id: &str) {
|
||||||
|
let lock = mgr.repo_lock(marketplace_id);
|
||||||
|
let _repo_guard = lock.lock().await;
|
||||||
|
mgr.remove_snapshot(marketplace_id);
|
||||||
|
let path = git::cache_path(mgr.data_root(), marketplace_id);
|
||||||
|
let _ = tokio::task::spawn_blocking(move || {
|
||||||
|
if path.exists() {
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(&path) {
|
||||||
|
log::warn!(
|
||||||
|
"Could not delete the marketplace cache {}: {}",
|
||||||
|
path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
fn item_changed(repo: &Path, inst: &MarketplaceInstall, head: &str) -> Result<bool, String> {
|
fn item_changed(repo: &Path, inst: &MarketplaceInstall, head: &str) -> Result<bool, String> {
|
||||||
let old = GitTree::open(repo, &inst.commit)?;
|
let old = GitTree::open(repo, &inst.commit)?;
|
||||||
let new = GitTree::open(repo, head)?;
|
let new = GitTree::open(repo, head)?;
|
||||||
@@ -498,7 +533,7 @@ mod tests {
|
|||||||
let data = tempfile::tempdir().unwrap();
|
let data = tempfile::tempdir().unwrap();
|
||||||
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
|
|
||||||
let snap = refresh_marketplace(&mgr, &settings_with(&fx.url()), "m1").await;
|
let snap = refresh_marketplace(&mgr, &|| settings_with(&fx.url()), "m1").await;
|
||||||
|
|
||||||
assert_eq!(snap.fetch_error, None);
|
assert_eq!(snap.fetch_error, None);
|
||||||
assert_eq!(snap.head_commit.as_deref(), Some(c1.as_str()));
|
assert_eq!(snap.head_commit.as_deref(), Some(c1.as_str()));
|
||||||
@@ -530,11 +565,11 @@ mod tests {
|
|||||||
let data = tempfile::tempdir().unwrap();
|
let data = tempfile::tempdir().unwrap();
|
||||||
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
let settings = settings_with(&url);
|
let settings = settings_with(&url);
|
||||||
let first = refresh_marketplace(&mgr, &settings, "m1").await;
|
let first = refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
assert_eq!(first.fetch_error, None);
|
assert_eq!(first.fetch_error, None);
|
||||||
|
|
||||||
drop(fx); // the source repository disappears (offline, deleted, …)
|
drop(fx); // the source repository disappears (offline, deleted, …)
|
||||||
let second = refresh_marketplace(&mgr, &settings, "m1").await;
|
let second = refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
|
|
||||||
assert!(second.fetch_error.is_some(), "expected a fetch error");
|
assert!(second.fetch_error.is_some(), "expected a fetch error");
|
||||||
assert_eq!(second.head_commit.as_deref(), Some(c1.as_str()));
|
assert_eq!(second.head_commit.as_deref(), Some(c1.as_str()));
|
||||||
@@ -550,10 +585,11 @@ mod tests {
|
|||||||
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
let settings = settings_with(&fx.url());
|
let settings = settings_with(&fx.url());
|
||||||
|
|
||||||
let guard = mgr.repo_lock().lock().await;
|
let lock = mgr.repo_lock("m1");
|
||||||
|
let guard = lock.lock().await;
|
||||||
let blocked = tokio::time::timeout(
|
let blocked = tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(300),
|
std::time::Duration::from_millis(300),
|
||||||
refresh_marketplace(&mgr, &settings, "m1"),
|
refresh_marketplace(&mgr, &|| settings.clone(), "m1"),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(blocked.is_err(), "refresh must not fetch while the repo lock is held");
|
assert!(blocked.is_err(), "refresh must not fetch while the repo lock is held");
|
||||||
@@ -563,10 +599,102 @@ mod tests {
|
|||||||
);
|
);
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
||||||
let snap = refresh_marketplace(&mgr, &settings, "m1").await;
|
let snap = refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
assert_eq!(snap.head_commit.as_deref(), Some(c1.as_str()));
|
assert_eq!(snap.head_commit.as_deref(), Some(c1.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repo_locks_are_per_marketplace() {
|
||||||
|
let mgr = MarketplaceManager::new(std::env::temp_dir());
|
||||||
|
let a1 = mgr.repo_lock("a");
|
||||||
|
let a2 = mgr.repo_lock("a");
|
||||||
|
let b = mgr.repo_lock("b");
|
||||||
|
assert!(Arc::ptr_eq(&a1, &a2), "one lock per marketplace");
|
||||||
|
assert!(!Arc::ptr_eq(&a1, &b), "marketplaces do not block each other");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PR review #5: a long fetch of one marketplace must not hold up work
|
||||||
|
/// (another refresh, pins, installs) on a different one.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_busy_marketplace_does_not_block_another() {
|
||||||
|
let Some(fx) = GitFixture::new() else { return };
|
||||||
|
let c1 = fx.with_all_kinds();
|
||||||
|
let data = tempfile::tempdir().unwrap();
|
||||||
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
|
let settings = settings_with(&fx.url());
|
||||||
|
|
||||||
|
let other = mgr.repo_lock("some-other-marketplace");
|
||||||
|
let _busy = other.lock().await;
|
||||||
|
let snap = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(20),
|
||||||
|
refresh_marketplace(&mgr, &|| settings.clone(), "m1"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("m1 must not wait for another marketplace's lock");
|
||||||
|
assert_eq!(snap.head_commit.as_deref(), Some(c1.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PR review #6: a refresh that was already under way when the
|
||||||
|
/// marketplace was removed must not recreate its cache or snapshot.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_refresh_of_a_removed_marketplace_leaves_nothing_behind() {
|
||||||
|
let Some(fx) = GitFixture::new() else { return };
|
||||||
|
fx.with_all_kinds();
|
||||||
|
let data = tempfile::tempdir().unwrap();
|
||||||
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
|
let current = Mutex::new(settings_with(&fx.url()));
|
||||||
|
let read_current = || current.lock().unwrap().clone();
|
||||||
|
|
||||||
|
let lock = mgr.repo_lock("m1");
|
||||||
|
let guard = lock.lock().await;
|
||||||
|
let refresh = refresh_marketplace(&mgr, &read_current, "m1");
|
||||||
|
tokio::pin!(refresh);
|
||||||
|
// The refresh starts, then waits for the lock…
|
||||||
|
assert!(
|
||||||
|
tokio::time::timeout(std::time::Duration::from_millis(100), &mut refresh)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
// …while the marketplace is removed from settings.
|
||||||
|
current.lock().unwrap().marketplaces.clear();
|
||||||
|
drop(guard);
|
||||||
|
let snap = refresh.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
snap.fetch_error.as_deref().unwrap_or("").contains("no longer configured"),
|
||||||
|
"{snap:?}"
|
||||||
|
);
|
||||||
|
assert!(!git::cache_path(data.path(), "m1").exists(), "cache recreated");
|
||||||
|
assert_eq!(mgr.snapshot("m1"), None, "snapshot stored");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn removing_a_cache_waits_for_the_marketplaces_lock() {
|
||||||
|
let Some(fx) = GitFixture::new() else { return };
|
||||||
|
fx.with_all_kinds();
|
||||||
|
let data = tempfile::tempdir().unwrap();
|
||||||
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
|
let settings = settings_with(&fx.url());
|
||||||
|
refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
|
let cache = git::cache_path(data.path(), "m1");
|
||||||
|
assert!(cache.exists());
|
||||||
|
|
||||||
|
let lock = mgr.repo_lock("m1");
|
||||||
|
let guard = lock.lock().await;
|
||||||
|
let blocked = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
remove_marketplace_cache(&mgr, "m1"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(blocked.is_err(), "removal must wait for an in-flight fetch");
|
||||||
|
assert!(cache.exists());
|
||||||
|
drop(guard);
|
||||||
|
|
||||||
|
remove_marketplace_cache(&mgr, "m1").await;
|
||||||
|
assert!(!cache.exists());
|
||||||
|
assert_eq!(mgr.snapshot("m1"), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn concurrent_refreshes_all_succeed() {
|
async fn concurrent_refreshes_all_succeed() {
|
||||||
let Some(fx) = GitFixture::new() else { return };
|
let Some(fx) = GitFixture::new() else { return };
|
||||||
@@ -575,10 +703,11 @@ mod tests {
|
|||||||
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
let settings = settings_with(&fx.url());
|
let settings = settings_with(&fx.url());
|
||||||
|
|
||||||
|
let current = || settings.clone();
|
||||||
let (a, b, c) = tokio::join!(
|
let (a, b, c) = tokio::join!(
|
||||||
refresh_marketplace(&mgr, &settings, "m1"),
|
refresh_marketplace(&mgr, ¤t, "m1"),
|
||||||
refresh_marketplace(&mgr, &settings, "m1"),
|
refresh_marketplace(&mgr, ¤t, "m1"),
|
||||||
refresh_marketplace(&mgr, &settings, "m1"),
|
refresh_marketplace(&mgr, ¤t, "m1"),
|
||||||
);
|
);
|
||||||
for snap in [a, b, c] {
|
for snap in [a, b, c] {
|
||||||
assert_eq!(snap.fetch_error, None);
|
assert_eq!(snap.fetch_error, None);
|
||||||
@@ -594,7 +723,7 @@ mod tests {
|
|||||||
let settings = settings_with(&fx.url());
|
let settings = settings_with(&fx.url());
|
||||||
{
|
{
|
||||||
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
refresh_marketplace(&mgr, &settings, "m1").await;
|
refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
}
|
}
|
||||||
drop(fx);
|
drop(fx);
|
||||||
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
let mgr = MarketplaceManager::new(data.path().to_path_buf());
|
||||||
@@ -622,7 +751,7 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let mut project = crate::models::Project::new("p".into(), vec![]);
|
let mut project = crate::models::Project::new("p".into(), vec![]);
|
||||||
project.marketplace_installs = vec![install(ItemKind::Skill, "example-skill", &c1)];
|
project.marketplace_installs = vec![install(ItemKind::Skill, "example-skill", &c1)];
|
||||||
refresh_marketplace(&mgr, &settings, "m1").await;
|
refresh_marketplace(&mgr, &|| settings.clone(), "m1").await;
|
||||||
|
|
||||||
let updates = compute_updates(&mgr, &settings, &[project]);
|
let updates = compute_updates(&mgr, &settings, &[project]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user