fix: pre-release shared instances fixes (#6856)

* fix: dedupe notifications

* fix: notifs + members btn robustness

* fix: temp fix for external file wrongly showing updated

* fix: fmt
This commit is contained in:
Calum H.
2026-07-24 16:47:08 +00:00
committed by GitHub
parent cd56b98975
commit fbe70c6938
14 changed files with 179 additions and 34 deletions
@@ -25,6 +25,7 @@ pub async fn invite_shared_instance_users(
user_ids: Vec<String>,
) -> crate::Result<SharedInstanceUsers> {
let state = State::get().await?;
let _shared_instance_lock = state.lock_shared_instance(instance_id).await;
let (metadata, attachment) =
shared_instance_for_invites(instance_id, user_ids.len(), &state)
.await?;
@@ -64,6 +65,7 @@ pub async fn create_shared_instance_invite_link(
replace_invite_id: Option<String>,
) -> crate::Result<SharedInstanceInviteLink> {
let state = State::get().await?;
let _shared_instance_lock = state.lock_shared_instance(instance_id).await;
let (metadata, attachment) =
shared_instance_for_invites(instance_id, 0, &state).await?;
ensure_owner(&attachment)?;
@@ -129,6 +131,7 @@ pub async fn remove_shared_instance_users(
has_pending_recipients: bool,
) -> crate::Result<SharedInstanceUsers> {
let state = State::get().await?;
let _shared_instance_lock = state.lock_shared_instance(instance_id).await;
let Some(attachment) = shared_attachment(instance_id, &state).await? else {
return Ok(SharedInstanceUsers::empty());
};
+5 -1
View File
@@ -30,6 +30,7 @@ pub async fn authenticate_finish_flow(
.await?;
creds.upsert(&state.pool).await?;
state.friends_socket.disconnect().await?;
state
.friends_socket
.connect(&state.pool, &state.api_semaphore, &state.process_manager)
@@ -45,8 +46,8 @@ pub async fn logout() -> crate::Result<()> {
if let Some(current) = current {
ModrinthCredentials::remove(&current.user_id, &state.pool).await?;
state.friends_socket.disconnect().await?;
}
state.friends_socket.disconnect().await?;
Ok(())
}
@@ -57,6 +58,9 @@ pub async fn get_credentials() -> crate::Result<Option<ModrinthCredentials>> {
let current =
ModrinthCredentials::get_and_refresh(&state.pool, &state.api_semaphore)
.await?;
if current.is_none() {
state.friends_socket.disconnect().await?;
}
Ok(current)
}
+46 -5
View File
@@ -26,6 +26,7 @@ use serde_json::Value;
use std::net::SocketAddr;
use std::ops::Deref;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::net::tcp::OwnedReadHalf;
@@ -38,6 +39,8 @@ pub(super) type TunnelSockets = Arc<DashMap<Uuid, Arc<InternalTunnelSocket>>>;
pub struct FriendsSocket {
write: WriteSocket,
connection_lock: Mutex<()>,
connection_generation: Arc<AtomicU64>,
user_statuses: Arc<DashMap<UserId, UserStatus>>,
tunnel_sockets: TunnelSockets,
}
@@ -60,6 +63,8 @@ impl FriendsSocket {
pub fn new() -> Self {
Self {
write: Arc::new(RwLock::new(None)),
connection_lock: Mutex::new(()),
connection_generation: Arc::new(AtomicU64::new(0)),
user_statuses: Arc::new(DashMap::new()),
tunnel_sockets: Arc::new(DashMap::new()),
}
@@ -72,6 +77,11 @@ impl FriendsSocket {
semaphore: &FetchSemaphore,
process_manager: &ProcessManager,
) -> crate::Result<()> {
let _connection_guard = self.connection_lock.lock().await;
if self.is_connected().await {
return Ok(());
}
let credentials =
ModrinthCredentials::get_and_refresh(exec, semaphore).await?;
@@ -94,6 +104,10 @@ impl FriendsSocket {
Ok((socket, _)) => {
tracing::info!("Connected to friends socket");
let (write, read) = socket.split();
let connection_generation = self
.connection_generation
.fetch_add(1, Ordering::AcqRel)
+ 1;
{
let mut write_lock = self.write.write().await;
@@ -107,12 +121,21 @@ impl FriendsSocket {
}
let write_handle = self.write.clone();
let connection_generation_handle =
self.connection_generation.clone();
let statuses = self.user_statuses.clone();
let sockets = self.tunnel_sockets.clone();
tokio::spawn(async move {
let mut read_stream = read;
while let Some(msg_result) = read_stream.next().await {
if connection_generation_handle
.load(Ordering::Acquire)
!= connection_generation
{
break;
}
match msg_result {
Ok(msg) => {
let server_message = match msg {
@@ -222,8 +245,17 @@ impl FriendsSocket {
}
}
let mut w = write_handle.write().await;
*w = None;
if connection_generation_handle.load(Ordering::Acquire)
== connection_generation
{
let mut write = write_handle.write().await;
if connection_generation_handle
.load(Ordering::Acquire)
== connection_generation
{
*write = None;
}
}
});
}
Err(e) => {
@@ -302,11 +334,20 @@ impl FriendsSocket {
#[tracing::instrument(skip(self))]
pub async fn disconnect(&self) -> crate::Result<()> {
let _connection_guard = self.connection_lock.lock().await;
self.connection_generation.fetch_add(1, Ordering::AcqRel);
let mut write_lock = self.write.write().await;
if let Some(ref mut write_half) = *write_lock {
SinkExt::close(write_half).await?;
*write_lock = None;
let write_half = write_lock.take();
drop(write_lock);
self.user_statuses.clear();
self.tunnel_sockets.clear();
if let Some(mut write_half) = write_half {
SinkExt::close(&mut write_half).await?;
}
Ok(())
}
@@ -13,7 +13,7 @@ pub(crate) async fn remove_instance(
})?;
let _content_lock = state.lock_instance_content(instance_id).await;
delete_instance_row_and_content_lock(&instance.id, state).await?;
delete_instance_row_and_locks(&instance.id, state).await?;
let path = state.directories.instances_dir().join(&instance.path);
if path.exists() {
@@ -23,13 +23,13 @@ pub(crate) async fn remove_instance(
Ok(())
}
async fn delete_instance_row_and_content_lock(
async fn delete_instance_row_and_locks(
instance_id: &str,
state: &State,
) -> crate::Result<()> {
// Keep these together so deleted instances cannot leave stale entries in the per-instance lock map.
// Keep these together so deleted instances cannot leave stale entries in the per-instance lock maps.
instance_rows::delete_instance_by_id(instance_id, &state.pool).await?;
state.remove_instance_content_lock(instance_id);
state.remove_instance_locks(instance_id);
Ok(())
}
+18 -1
View File
@@ -74,6 +74,8 @@ pub struct State {
pub(crate) install_db_semaphore: Semaphore,
/// Serializes filesystem reconciliation and content mutations per instance.
instance_content_locks: DashMap<String, Arc<Mutex<()>>>,
/// Serializes shared instance attachment and recipient mutations per instance.
shared_instance_locks: DashMap<String, Arc<Mutex<()>>>,
/// Discord RPC
pub discord_rpc: DiscordGuard,
@@ -111,8 +113,22 @@ impl State {
lock.lock_owned().await
}
pub(crate) fn remove_instance_content_lock(&self, instance_id: &str) {
pub(crate) async fn lock_shared_instance(
&self,
instance_id: &str,
) -> OwnedMutexGuard<()> {
let lock = self
.shared_instance_locks
.entry(instance_id.to_string())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
lock.lock_owned().await
}
pub(crate) fn remove_instance_locks(&self, instance_id: &str) {
let _ = self.instance_content_locks.remove(instance_id);
let _ = self.shared_instance_locks.remove(instance_id);
}
pub async fn init(app_identifier: String) -> crate::Result<()> {
@@ -231,6 +247,7 @@ impl State {
install_job_semaphore: Semaphore::new(MAX_CONCURRENT_INSTALL_JOBS),
install_db_semaphore: Semaphore::new(1),
instance_content_locks: DashMap::new(),
shared_instance_locks: DashMap::new(),
discord_rpc,
process_manager,
friends_socket,
@@ -90,6 +90,7 @@
:requested-label="requestedButtonLabel"
:requested-tooltip="requestedTooltip(friend.username)"
:user-profile-link="userProfileLink"
:disabled="!canInvite"
@invite="inviteFriend"
@cancel="cancelInvite"
/>
@@ -348,6 +349,7 @@ const {
})
function inviteFriend(friend: InvitePlayersUser) {
if (!props.canInvite) return
emit('invite', {
user: friend,
source: 'friend',
@@ -355,6 +357,7 @@ function inviteFriend(friend: InvitePlayersUser) {
}
function cancelInvite(friend: InvitePlayersUser) {
if (!props.canInvite) return
emit('cancel', friend)
}
@@ -37,7 +37,7 @@
</button>
</ButtonStyled>
<ButtonStyled v-else-if="status === 'pending'" type="outlined">
<button @click="$emit('cancel', user)">
<button :disabled="disabled" @click="$emit('cancel', user)">
{{ cancelLabel }}
</button>
</ButtonStyled>
@@ -49,7 +49,7 @@
</ButtonStyled>
</span>
<ButtonStyled v-else color-fill="none">
<button @click="$emit('invite', user)">
<button :disabled="disabled" @click="$emit('invite', user)">
{{ inviteLabel }}
</button>
</ButtonStyled>
@@ -74,9 +74,11 @@ const props = withDefaults(
inviteLabel: string
requestedLabel: string
requestedTooltip: string
disabled?: boolean
userProfileLink?: (username: string) => InvitePlayersUserProfileLink
}>(),
{
disabled: false,
userProfileLink: undefined,
},
)