Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SES-1966] Attachment batch download and tidy-up #1507

Merged
merged 15 commits into from
Jul 1, 2024

Conversation

simophin
Copy link

Description

This PR reworks some aspect of the attachment download:

  1. A new AttachmentDownloadHelper is created to batch the download request.
  2. The batch download requests will be checked preliminarily, before the request is transformed into a AttachmentDownloadJob

There's one more fix on the ThreadUtils where the ExecutorService seems to misbehave. Changed over to use the Kotlin Coroutine's IO dispatcher.

Copy link
Collaborator

@AL-Session AL-Session left a comment

Choose a reason for hiding this comment

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

Great work - some optional comments for you to address as you see fit. Approved!

@simophin
Copy link
Author

simophin commented Jun 3, 2024

@bemusementpark Wanna have a look again? I've extracted the said Flow operator to Flow.timedBuffer.

fanchao added 5 commits June 21, 2024 11:07
…ownload-task

# Conflicts:
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
#	app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
Copy link

@bemusementpark bemusementpark left a comment

Choose a reason for hiding this comment

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

a few ideas, mostly the naming, maybe it's more of a JobScheduler, or a Debouncer, than a Helper, which suggests more benign impedance matching/courtesy functions, rather than actual business logic.

val maxItems = 5

// When
val result = flow.timedBuffer(timeoutMillis, maxItems).toCollection(mutableListOf())

Choose a reason for hiding this comment

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

Suggested change
val result = flow.timedBuffer(timeoutMillis, maxItems).toCollection(mutableListOf())
val result = flow.timedBuffer(timeoutMillis, maxItems).toList()

Can we use built-ins? result doesn't need to be a MutableList. Don't unnecessarily expose.

Copy link
Author

Choose a reason for hiding this comment

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

Yep! ChatGPT really needs to up its game!

val pendingAttachmentIDs = storage
.getAllPendingJobs(AttachmentDownloadJob.KEY, AttachmentUploadJob.KEY)
.values
.mapNotNullTo(hashSetOf()) {

Choose a reason for hiding this comment

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

It'd be more readable to just mapNotNull, and toSet, if you want performance, then convert to sequence above

Copy link
Author

Choose a reason for hiding this comment

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

Done. Let's go for readability

// Before handling out attachment to the download task, we need to
// check the requisite for that attachment. This check is very likely to be
// performed again in the download task, but adding stuff into job system
// is expensive so we need to avoid spawning new task whenever we can.

Choose a reason for hiding this comment

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

This is a detailed low-level comment, but there's no comment on this class or entire init block, so the intention of this big chain is kind of unclear. Consider adding higher level comments and/or breaking this out into a well-named function .

init {
    initAttachmentProcessing() // <- insert better name here
}

Copy link
Author

Choose a reason for hiding this comment

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

Tided up

downloadRequests
.receiveAsFlow()
.timedBuffer(BUFFER_TIMEOUT_MILLS, BUFFER_MAX_ITEMS)
.map { attachments ->

Choose a reason for hiding this comment

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

Maybe some fun deduplicate(attachment) could replace this block with .map(::deduplicate) and make this chain a lot more manageable/readable.

Copy link
Author

Choose a reason for hiding this comment

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

Done

}

val threadRecipient = storage.getRecipientForThread(message.threadId)
if (threadRecipient == null) {

Choose a reason for hiding this comment

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

do we have to retrieve a thread AND a Contact before we can return a boolean ||?

Is it not valid if we have a thread, but no contact?

Copy link
Author

Choose a reason for hiding this comment

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

I guess not, have updated to reflect this

Copy link
Author

Choose a reason for hiding this comment

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

The logic was inserted here trying to replicate the same in AttachmentDownloadJob.execute. I guess it's better to extract the logic from that function and use it here.


if (buffer.size < maxItems) {
// If the buffer is not full, wait until the time limit is reached
delay((System.currentTimeMillis() + timeoutMillis - bufferBeganAt).coerceAtLeast(0L))

Choose a reason for hiding this comment

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

Suggested change
delay((System.currentTimeMillis() + timeoutMillis - bufferBeganAt).coerceAtLeast(0L))
delay(System.currentTimeMillis() + timeoutMillis - bufferBeganAt))

Don't need it to be non-negative, <= 0 returns immediately.

Copy link
Author

Choose a reason for hiding this comment

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

A negative number doesn't make sense for delay. If I am the implementator of delay, I would throw an exception. Were you looking at the implementation of the delay itself? I'd prefer not to rely on the leniency of such implementation details.

Copy link

@bemusementpark bemusementpark Jun 28, 2024

Choose a reason for hiding this comment

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

I did look at the source, haha, yeah, but it's also documented:

delay
suspend fun delay(timeMillis: Long)(source)
Delays coroutine for at least the given time without blocking a thread and resumes it after a specified time. If the given timeMillis is non-positive, this function returns immediately.

Copy link
Author

@simophin simophin left a comment

Choose a reason for hiding this comment

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

@bemusementpark I have addressed some of your concerns and also reverted some changes to make my intent clearer.

if (message.isOutgoing) return true

val sender = message.individualRecipient.address.serialize()
val contact = storage.getContactWithSessionID(sender)
Copy link
Author

Choose a reason for hiding this comment

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

I think this is just introducing unnecessary mental load. It takes no time for the original code to read but it'd probably take me more than 1 sec to read with this style. Also I feel really awkward to have inter-inline function returns in Kotlin, it seems like after-thought after introducing these inline functions that could change the main syntax flow.


if (buffer.size < maxItems) {
// If the buffer is not full, wait until the time limit is reached
delay((System.currentTimeMillis() + timeoutMillis - bufferBeganAt).coerceAtLeast(0L))
Copy link
Author

Choose a reason for hiding this comment

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

A negative number doesn't make sense for delay. If I am the implementator of delay, I would throw an exception. Were you looking at the implementation of the delay itself? I'd prefer not to rely on the leniency of such implementation details.

val maxItems = 5

// When
val result = flow.timedBuffer(timeoutMillis, maxItems).toCollection(mutableListOf())
Copy link
Author

Choose a reason for hiding this comment

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

Yep! ChatGPT really needs to up its game!

}

val threadRecipient = storage.getRecipientForThread(message.threadId)
if (threadRecipient == null) {
Copy link
Author

Choose a reason for hiding this comment

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

I guess not, have updated to reflect this

// Before handling out attachment to the download task, we need to
// check the requisite for that attachment. This check is very likely to be
// performed again in the download task, but adding stuff into job system
// is expensive so we need to avoid spawning new task whenever we can.
Copy link
Author

Choose a reason for hiding this comment

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

Tided up

}

val threadRecipient = storage.getRecipientForThread(message.threadId)
if (threadRecipient == null) {
Copy link
Author

Choose a reason for hiding this comment

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

The logic was inserted here trying to replicate the same in AttachmentDownloadJob.execute. I guess it's better to extract the logic from that function and use it here.

downloadRequests
.receiveAsFlow()
.timedBuffer(BUFFER_TIMEOUT_MILLS, BUFFER_MAX_ITEMS)
.map { attachments ->
Copy link
Author

Choose a reason for hiding this comment

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

Done

val pendingAttachmentIDs = storage
.getAllPendingJobs(AttachmentDownloadJob.KEY, AttachmentUploadJob.KEY)
.values
.mapNotNullTo(hashSetOf()) {
Copy link
Author

Choose a reason for hiding this comment

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

Done. Let's go for readability

Copy link

@bemusementpark bemusementpark left a comment

Choose a reason for hiding this comment

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

I'm liking the KDoc and structure here! maybe a tiny bit more Boolean simplification could help, otherwise LGTM!

* requests will go through different level of checking before they are queued for download.
*
* To use this handler, call [onAttachmentDownloadRequest] with the attachment that needs to be
* downloaded. The call to [onAttachmentDownloadRequest] is cheap and can be called multiple times.

Choose a reason for hiding this comment

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

love it

jobQueue.add(
AttachmentDownloadJob(
attachmentID = attachment.attachmentId.rowId,
databaseMessageID = attachment.mmsId

Choose a reason for hiding this comment

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

this is cool, now this is the first interesting part you see, which is clearly adding jobs, so kinda self-documenting IMHO.

* time [timeoutMillis] limit.
*/
fun <T> Flow<T>.timedBuffer(timeoutMillis: Long, maxItems: Int): Flow<List<T>> {
return channelFlow {

Choose a reason for hiding this comment

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

= could save some brackets and indentation.

if (threadRecipient == null || sender == null || (contact == null && !selfSend)) {
return false
}
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) {

Choose a reason for hiding this comment

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

Would it simpler, like this?

Suggested change
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) {
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && !selfSend) {

or even:

Suggested change
if (!threadRecipient.isGroupRecipient && contact?.isTrusted != true && storage.getUserPublicKey() != sender) {
return threadRecipient.isGroupRecipient || contact?.isTrusted == true || selfSend

actually, that is a big mental unlock for me understanding this easily without squinting.

if ( its a group or its a trusted contact, or its from us ) then its good 👌

Copy link
Author

Choose a reason for hiding this comment

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

tbh I have no idea what that check actually check. This whole check is existing code and I figure I'd reuse the same

}

val contact = sender?.let { storage.getContactWithSessionID(it) }
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) {

Choose a reason for hiding this comment

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

  1. If they're in a group are they definitely a contact? this part requires that we have a contact for them, even if they're in a group. Is that necessary? (I don't know the full semantic of a Contact)

  2. I think the last of this is partially built into the next if statement.

Suggested change
if (threadRecipient == null || sender == null || (contact == null && !selfSend)) {
if (threadRecipient == null || sender == null)) {

but then you can probably inline the threadRecipient and sender :

val threadRecipient = storage.getRecipientForThread(threadID) ?: return false

etc.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's one of those painful "what do you mean by contact" problem that we have on Android.
From doing some quick tests we were able to see a photo in a group from someone who isn't an actual contact. But this db call probably means something different by "contact", especially with the isTrusted property in there.

if (message.isOutgoing) return true

val sender = message.individualRecipient.address.serialize()
val contact = storage.getContactWithSessionID(sender)

Choose a reason for hiding this comment

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

I'm not a fan of the ?: run { ... } syntax myself, but once the logs are gone and it's a 1-liner it's nicer ?: return false

I thought these would maintain the same functionality just reduce the repetitive references to each val.

@ThomasSession ThomasSession merged commit 0da949c into oxen-io:dev Jul 1, 2024
1 check was pending
ThomasSession added a commit that referenced this pull request Jul 9, 2024
* fix: Authenticate all Open Group API calls

* Use unblinded authentication when we have `capabilities` data for the open group server we are sending the request to but don't have the `blind` capability
* Use blinded authentication when we haven't gotten any `capabilities` for an open group server, or if we have `capabilities` and the server has the `blind` capability

* Hide send button when message contains only whitespace

* Fix bug displaying user ID when quoting own message

* Fix notification update for incoming unsend request

* Improve check if author is own user when quoting messages

* Fixed video call auto rotate, when auto rotate is disabled

* refactor: simplify comparison

* Stop playing message if deleted

* Accidental change

* Accidental change

* Comments

* Feedback

* Comments

* Import

* Fix delete message for everyone doesn't stop the audio playing

* Correct the usage of flowOn

* Import

* Optimise XML

* Remove unused file

* Remove view pools

* Remove the use of executor in ThreadUtils

* Using trim and empty to capture semantic concept of nothing being in there

* Remove config checks (PR 1294)

Refactor: remove checks for whether new config is enabled throughout config factory generation.
First commit from PR 1294.

* [SES-2162] - Remove wrapping of config message (#1517)

* Remove wrapping of config message

* Addresses feedback

* Merged in ThreadUtils fix

* JDK installation

* Revert JDK change

---------

Co-authored-by: fanchao <[email protected]>

* Update libsession

* [SES-337] Add rounded corners to thumbnail in QuoteView (#1285)

* Add rounded corners to thumbnail in QuoteView

* Simplify ThumbnailView

* Cleanup ThumbnailView

* Removed include custom attributes

The custom attributes are not passed to the view.
I added the radius programatically instead.

* Clipping whole thumbnail view instead of just the image requests

---------

Co-authored-by: AL-Session <[email protected]>
Co-authored-by: ThomasSession <[email protected]>

* Highlight @you mentions (#985)

* Highlight @you mentions

* fix: resolve merge conflicts

* Setting the proper design rules for mentions

* New RoundedBackgroundSpan, applied to "you"  mentions

The rounded background highlighter can take padding, so there is no need to add those extra spaces at the start and end.

* Better mention highlight logic

Some mention highlight should only format the text and not apply any styling.
Also making sure we cater for all cases properly

* Updated the text color logic based on design rules

* Fine tuning the color rules

* Removing usage of Resources.getSystem()

Only making the db call if there actually is a mention

* Moving color definition outside the loop to avoid repetitions

---------

Co-authored-by: charles <[email protected]>
Co-authored-by: 0x330a <[email protected]>
Co-authored-by: ThomasSession <[email protected]>

* [SES-2018] Refactor mention  (#1510)

* Refactor mention

* Fixes robolectric test problem

* Fixes tests

* Naming and comments

* Naming

* Dispatcher

---------

Co-authored-by: fanchao <[email protected]>

* [SES-1966] Attachment batch download and tidy-up (#1507)

* Attachment batch download

* Addressed feedback and test issues

* Feedback fixes

* timedWindow for flow

* Feedback

* Dispatchers

* Remove `flowOn`

* New implementation of timedBuffer

* Organise import

* Feedback

* Fix test

* Tidied up logic around `eligibleForDownload`

* Updated comment

---------

Co-authored-by: fanchao <[email protected]>

* Fix issue with span being the full length (#1528)

* Proper display of unresolved names in mentions (#1530)

* Fix issue with span being the full length

* Making sure a mention with a username without a resolved name still displayed with the appropriate style with the truncated is

* Testnet build (#1532)

Co-authored-by: fanchao <[email protected]>

* Allow "public.loki.foundation" to be accessed by http (#1534)

Co-authored-by: fanchao <[email protected]>

* Bumping the version code and name

* Reverting temporary change

---------

Co-authored-by: charles <[email protected]>
Co-authored-by: andrew <[email protected]>
Co-authored-by: aaronkerckhoff <[email protected]>
Co-authored-by: Rugved Darwhekar <[email protected]>
Co-authored-by: 0x330a <[email protected]>
Co-authored-by: fanchao <[email protected]>
Co-authored-by: Fanchao Liu <[email protected]>
Co-authored-by: AL-Session <[email protected]>
Co-authored-by: ceokot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants