-
-
Notifications
You must be signed in to change notification settings - Fork 44
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
Remove useless identity contracts after leaving group #1885
Remove useless identity contracts after leaving group #1885
Conversation
Passing run #1992 ↗︎
Details:
Review all test suite changes for PR #1885 ↗︎ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a good first step, although I'm not sure about the approach taken.
The main issue is that this is reading and acting upon information found in a different contracts, i.e., group A is reading information about group B. I know this is sometimes unavoidable, but in general it should not be done because it can introduce all sorts of rare race conditions (this is why I refactored many of the complex actions into simpler actions with side effects). I think a good model to use is that the (Chelonia) state for a certain client should always be the same, no matter the order events are processed or how they end up being processed. I'm not convinced that this is the case here, because there could be several groups syncing in the background and this could remove a member's contract from another group.
I've thought about this issue (I mean removing identity contracts) in the past, and I came to the conclusion that the 'best' approach would probably involve some kind of tagged reference counting. So, for example, each contract has a set of subscription 'reasons' (which could be the name of the contract that requested it) and once that set is empty, the contract is removed.
The second challenge with implementing this (in general, this isn't about the approach used here) is that we rely on the identity contract state being present to display (past) information. For example, if you have a group with A, B and C, and then B leaves, you still want (probably) to use B's profile information in the chatroom. By removing the profile entirely, this will no longer be the case.
Solving this second issue is trickier, but I suppose we could use some kind of cache layer for removed profile pictures.
A third issue (and maybe distinct from this issue, which is about removing contracts) is fetching unnecessary information. Let's go back to the previous example of a group with A, B and C. Let's say now that B leaves the group at time t
, and the group contract was at height GH
and B's contract at height BH
. Now, if C were to, for example, log in from a new device, they'd sync the contracts for A, B, C and G in their entirety, even though contract B should only be synced up to BH
. I'm mentioning this issue because of trying to always build the same state, but I realise it's difficult to tackle.
@corrideat, I totally agree that every process in a certain contract should not rely on another contract's state as much as possible. But we need them sometimes. So if it's needed, we should wait until those event queues are empty. What do you think, @corrideat? |
I think that that could work, but I also see potential for deadlocks if there are several groups doing this. What do you think about the other issues mentioned? (For example, that the profile will no longer be available, which affects chats) |
We don't remove the identity contracts which is used for any other purposes, such as the identity contracts which is used for direct messages, and the identity contracts of another group members. In my updates of this PR, However, the removing identity contracts should not be done until the necessary (all the contracts invocation queue?) invocation queues are empty. @corrideat, what do you recommend for this? Do you think setInterval should be used for waiting the those queued invocations to be fully executed? |
Right, but I'm talking about this situation:
|
@corrideat, please review this PR, and focus on |
I feel that we're talking past each other. It's good to handle this, but this is not the situation I was mentioning. In the situation that I mentioned |
For the situation you mentioned, |
@Silver-IT Yes, you're right. I take it back, the scenario that I described isn't an issue, and because you're not checking whether the profile is active, other related scenarios that I thought of aren't an issue either (this was, create So, overall, now I think that this could work, if you can wait on the queues to emptied in a safe manner. |
frontend/controller/actions/group.js
Outdated
'gi.actions/group/removeUselessIdentityContracts': function ({ contractID, possiblyUselessContractIDs }) { | ||
// NOTE: should remove the identity contracts which we don't need to sync anymore | ||
// for users who don't have any common groups, and any common DMs | ||
const interval = setInterval(() => { |
There was a problem hiding this comment.
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 using setInterval
in general, and in this case it seems like it might work but is unsafe. If you must do it this way, firstly I'd use setTimeout instead, which ensures that the callback isn't executed more than once at the same time. I'd also combine it with a /wait
call to start it off, since you're expecting that those contracts to be removed, so most of the time there won't be any more events after the call to /wait
completes.
A safer approach to doing this, without any setTimeout
/ setInterval
would be to create a new sync barrier API. This barrier API would be something of a semaphore that'd also keep track in its internal state what's being waited, to attempt to break cycles in some way.
…fter-leaving-group
frontend/controller/actions/group.js
Outdated
// for users who don't have any common groups, and any common DMs | ||
const interval = setInterval(() => { | ||
const pending = Object.entries(sbp('okTurtles.eventQueue/queuedInvocations')) | ||
.filter(([q]) => typeof q === 'string') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly does this line do and why is it here?
I think this would work without it... and possibly be safer too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The pending
variable would be filled with pending invocations. Since we should access the state of other contracts when we are leaving the group, we should make it clear that those contracts are all up to date. So just wait until this pending
should be an empty list.
The reason why we could not use await sbp('chelonia/contract/wait', contractIDs)
is that this has a potential deadlock. I need another solution that doesn't use setInterval
(I knew I shouldn't use it, but could not find another solution), and I will find the better solution with the help of @corrideat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I wasn't very clear, I meant why is this checking for typeof q === 'strings'
? I believe they should all be strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @Silver-IT!
This does seem like it would work.
While the ideal way to solve this would be to use reference counting (#1249), we are not anywhere close to having that implemented yet, and this solution should work well until then I think.
And actually, even with reference counting there is still the issue that @corrideat brought up, of display names and profile pictures being missing from chatrooms for identities that have left the group. This PR actually doesn't have that problem, because it only removes identity contracts when we are the ones that left the group.
I left a single comment/request.
…fter-leaving-group
Instead of using const foo = () => {
sbp('chelonia/contract/wait', contractIDs).then(() => {
// You need to check the user because if the session changed this is no longer relevant
// and you could be removing contracts that you shouldn't
if (currentUserHasChanged) return; // alternatively, you could use an event listener
if (contractIDsInQueue) { // the check that you're doing with
// const pending = Object.entries(sbp('okTurtles.eventQueue/queuedInvocations'))...
// IMPORTANT: The check needs to be updated to check the
// same queues that `chelonia/contract/wait` is using
// or else this could cause an infinite loop.
// (Even if you use the `setInterval` approach, you should
// still only check the relevant queues, because there could
// always be unrelated things running in the background)
foo()
return
}
// The rest of the logic follows here
}).catch(/* ... */);
} I think this would be safe (or safer) while avoiding unnecessary checking. It also checks for the user changing, which is needed because you don't want to remove contracts in that case. Maybe you can also give up after a certain number of attempts. ETA: IMHO not needed, but you can also wrap the |
@corrideat, I don't think we can simply remove Let's imagine that there is a pending invocation which is to publish a new event (as well as that we are running the above codes). This would always check if the invocation queue is empty at the beginning of publish. If the queue is not empty, it will add another invocation in the end and wait. So there will be inifinite adding invocations to the queue and waiting for the queues to be empty on both sides. As a result (Also it was a starting point), the problem is that we should have a way to wait the invocation queue to be empty without adding new invocation to the queue. That was the reason why I used cc: @taoeffect |
Greg and I discussed this last week, and that's the reason to make sure that the check for the queue state uses the exact same queues as Let's go over that would happen:
|
Yeah, Correct. So we have no way to give up using If you and also Greg are fine with using |
@Silver-IT I am fairly convinced by @corrideat's updated version of his code that it will not result in an infinite loop. I don't think |
Actually, I already tried that way. But it results in a infinite loop. That's why I added a comment above.
The one of the solutions is this. We need to use |
@Silver-IT Please try again, using the latest code that @corrideat used. Note: he just added this code about an hour ago. (Did you use that latest version?) |
I have already tried that way this morning. It results in a infinite loop. |
@Silver-IT Are you able to push your changes? I can't see how an infinite loop would happen, but it'd be easier to see the issue with a working example. |
frontend/controller/actions/group.js
Outdated
} | ||
|
||
const pending = Object.entries(sbp('okTurtles.eventQueue/queuedInvocations')) | ||
.filter(([q]) => typeof q === 'string') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is where the main issue lies. As explained in the comments, you must filter out the irrelevant keys. It's not enough to check whether there are any queued events, but you must check whether there are events only for the keys you're interested in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, here you'd have:
const waitFor = [...chatRoomIDs, ...groupIDs]
sbp('chelonia/contract/wait', waitFor).then(() => {
// ...
const pending = Object.entries(sbp('okTurtles.eventQueue/queuedInvocations'))
.filter(x => waitFor.includes(x))
if (pending.length) {
return removeIdentityContracts()
}
// ...
})
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment below about the outer scope mostly referred to this waitFor
here. I.e., you could refresh the list as the list of groups or chatrooms could've changed between the time wait
was called and pending
is checked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great @Silver-IT! One forgotten debug logging statement that needs to be removed and after that I think it's GTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I added this logging to test this:
const identityContractsToRemove = possiblyUselessContractIDs.filter(cID => !identityContractsMapToKeepSyncing[cID])
console.error('going to remove', identityContractsToRemove.length, 'contracts:', identityContractsToRemove)
And then I did these steps:
- [Tab1] u1 creates group and copies invite link
- [Tab2] u2 joins using invite link
- [Tab3] u3 joins using invite link
- [Tab1] u1 leaves group
This resulted in the following error appearing in the console:
Note also that the error that I added that I expected to appear, does not appear in the console!
The console won't be logged. Because in your situation, there is no chatroomIDs and groupIDs to consider. I will check the console error you pointed, and get you updated. |
And about the error you got while testing, is that what we could ignore. I think that's related to the updated workflow. It happens in cc: @corrideat |
I replied on Slack about this: https://okturtles.slack.com/archives/C0EH7P20Y/p1710933952734909 (You can reply there). But I also want to share another error that I got with @corrideat about what happened when I rejoined Is this one of the errors that's supposed to happen? If so, should it be a warning? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved after @Silver-IT's reply on Slack. I missed the behavior on line 913
Summary of Changes