Skip to content

Conversation

@mikoldin123
Copy link
Collaborator

Description

  • Possible fix for race condition in sending message request accept and actual message.

@mikoldin123 mikoldin123 self-assigned this Sep 23, 2025
@mikoldin123 mikoldin123 added bug Something isn't working Jira This ticket is being tracked in Jira labels Sep 23, 2025
Copy link
Collaborator

@mpretty-cyro mpretty-cyro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So based on the comment about setting things up to support an array of Message objects there are a few things that would need to be updated:

  • The MessageSendJob.Details should be updated to take/store messages: [Message] and variants: [Message.Variant]
    • Note: Since these are persisted we will need to keep the existing message/variant values and add @deprecated tags to them - the idea would be that we would remove them in a subsequent release (the MessageSendJob doesn't stay in the database for too long so the backwards compatibility can probably be removed in the next release)
    • The decoding should be updated to load the single message/variant values into the messages/variants array so that the backwards compatibility is localised into MessageSendJob.Details
  • The /sequence and /batch APIs have a limit of 20 requests, so we would need to handle that case (ie. if we try to send more than 20 messages at once then they should be split into multiple jobs - they would run into the race condition this is trying to solve, but with those numbers it's unlikely to actually be a problem)
  • The messageType value should be updated to just be a string of ", " separated message types (shouldn't need updatedMessageType)
  • The attachmentState logic will need to check the state of attachments for each message and defer if any of them have pending uploads

return nil
}

return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this return nil as the code shouldn't be reachable due to the returns from the switch statement

sentTimestampMs: UInt64(timestampMs)
)

viewModel.dependencies[singleton: .storage]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are no longer returning a publisher, you should update both of these database queries so be synchronous (viewModel.dependencies[singleton: .storage].write { [dependencies = viewModel.dependencies] db in)

This would mostly be to avoid weird situations where something else in the code validates one of these changes has occurred before sending messages (I don't think there is a case like that at the moment, but one could be added in the future)

else { return nil }

return viewModel.dependencies[singleton: .storage]
viewModel.dependencies[singleton: .storage]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably make the same change for the GroupUpdateInviteResponseMessage for consistency (I don't believe it has the same bug, but sending it as part of a /sequence is slightly more efficient and you already have the logic for it so it's not much more work) - will just need to update the function to return a generic Message instead of the MessageRequestResponse

timestampMs: Int64
) -> AnyPublisher<Void, Never> {
timestampMs: Int64,
shouldSequenceMessageRequestResponse: Bool = false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking maybe it would be better to always return the MessageRequestResponse/GroupUpdateInviteResponseMessage and have the caller decide how to send it - ie. the sendMessage function would do the /sequence call, and in the acceptMessageRequest() function we would just call:

viewModel.dependencies[singleton: .storage].writeAsync { [dependencies = viewModel.dependencies] db in
            try MessageSender.send(
                db,
                message: message,
                interactionId: nil,
                threadId: self.viewModel.threadData.threadId,
                threadVariant: self.viewModel.threadData.threadVariant,
                using: dependencies
            )
        }

This would allow us to simplify the logic in approveMessageRequestIfNeeded because it would just always return a Message if one needed to be sent

interactionId: Int64?,
to destination: Message.Destination,
isSyncMessage: Bool = false,
messageResponse: MessageRequestResponse? = nil,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking - maybe it would be cleaner to make this function take an array of [(message: Message, interactionId: Int64?)] instead of an explicit messageResponse parameter (would make it more reusable for sending multiple messages at once)?

threadId: String,
threadVariant: SessionThread.Variant,
isSyncMessage: Bool = false,
messageResponse: MessageRequestResponse? = nil,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the below comment this function would probably need to take additionalMessages: [(message: Message, interactionId: Int64?)]

Not ideal but would support the more generic "multi-sending" approach (you could also add a function which takes an array in Interaction but would still need the additionalMessages variant due to the MessageRequestResponse case 😞)

Comment on lines +196 to +206
let userSessionId: SessionId = dependencies[cache: .general].sessionId

// Convert and prepare the data for sending
let swarmPublicKey: String = {
switch details.destination {
case .contact(let publicKey): return publicKey
case .syncMessage: return userSessionId.hexString
case .closedGroup(let groupPublicKey): return groupPublicKey
case .openGroup, .openGroupInbox: preconditionFailure()
}
}()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicating logic from the MessageSender, would be better to consolidate this to avoid the duplication (we have something similar in Authentication) you could add the following to Message.Destination and then just use try details.destination.swarmPublicKey(using: dependencies) both here and in MessageSender:

func swarmPublicKey(using dependencies Dependencies) throws -> String {
    let userSessionId: SessionId = dependencies[cache: .general].sessionId

    switch self {
        case .contact(let publicKey): return publicKey
        case .syncMessage: return userSessionId.hexString
        case .closedGroup(let groupPublicKey): return groupPublicKey
        case .openGroup, .openGroupInbox: throw MessageSenderError.invalidDestination
    }
}

Note: You will need to add a new MessageSenderError.invalidDestination error

@mikoldin123 mikoldin123 marked this pull request as draft September 24, 2025 07:34
@mikoldin123 mikoldin123 changed the title Sequenced message request acceptance and message sending [WIP] Sequenced message request acceptance and message sending Sep 24, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working Jira This ticket is being tracked in Jira

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants