core: re-use link owner members when connecting to a group again

Connecting to a prepared group creates the link owner members from the link
data inside withFastStore, before the relay loop, and throws when every relay
connection fails. That error is an agent BROKER error, which the clients treat
as retryable: they offer Retry and re-send the same command, which inserted the
same owner members a second time -

  UNIQUE constraint failed: group_members.group_id, group_members.member_id

so joining a channel over a flaky network and retrying failed on a raw SQLite
error. Owner member creation was the only step in the command that was not
already idempotent: connectToRelay re-uses the relay member record via
getCreateRelayForMember, and a join that failed leaves the connection at
ConnPrepared for joinPreparedConn' to resume, because joinContact advances the
status only after the agent call returns.
This commit is contained in:
Narasimha-sc
2026-08-20 14:03:27 +00:00
parent 58aee54ad6
commit be8d51b642
2 changed files with 11 additions and 2 deletions
+1 -1
View File
@@ -2272,7 +2272,7 @@ processChatCommand cxt nm = \case
Just GroupOwnerContact {contactId, memberId}
| memberId == MemberId ownerId -> Just contactId
_ -> Nothing
void $ createLinkOwnerMember db cxt user gInfo' ctId_ (MemberId ownerId) ownerKey
void $ getCreateLinkOwnerMember db cxt user gInfo' ctId_ (MemberId ownerId) ownerKey
pure gInfo'
rs <- withGroupLock "connectPreparedGroup" groupId $
mapConcurrently (connectToRelay user gInfo') relays
+10 -1
View File
@@ -193,7 +193,7 @@ module Simplex.Chat.Store.Groups
getXGrpLinkMemReceived,
setXGrpLinkMemReceived,
createNewUnknownGroupMember,
createLinkOwnerMember,
getCreateLinkOwnerMember,
updatePreparedChannelMember,
updateUnknownMemberAnnounced,
updateRosterMemberAnnounced,
@@ -3505,6 +3505,15 @@ createNewUnknownGroupMember db cxt user@User {userId, userContactId} GroupInfo {
where
VersionRange minV maxV = vr cxt
-- Re-use owner member record on retry: owners are created before the relay loop,
-- which is what fails when the network does (see getCreateRelayForMember)
getCreateLinkOwnerMember :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe ContactId -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember
getCreateLinkOwnerMember db cxt user gInfo contactId_ memberId ownerKey =
liftIO (runExceptT $ getGroupMemberByMemberId db cxt user gInfo memberId) >>= \case
Right m -> pure m
Left (SEGroupMemberNotFoundByMemberId _) -> createLinkOwnerMember db cxt user gInfo contactId_ memberId ownerKey
Left e -> throwError e
createLinkOwnerMember :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe ContactId -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember
createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupId} contactId_ memberId ownerKey = do
currentTs <- liftIO getCurrentTime