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

refactor!: use more steps for invites #484

Merged
merged 28 commits into from
Mar 13, 2024
Merged

refactor!: use more steps for invites #484

merged 28 commits into from
Mar 13, 2024

Conversation

EvanHahn
Copy link
Contributor

@EvanHahn EvanHahn commented Feb 19, 2024

I recommend reviewing this with whitespace changes disabled.

See #447 for details.

I plan to add invite cancelations in an upcoming PR.

Closes #279.
Closes #363.
Closes #446.

src/member-api.js Outdated Show resolved Hide resolved
src/invite-api.js Show resolved Hide resolved
src/local-peers.js Show resolved Hide resolved
src/mapeo-manager.js Show resolved Hide resolved
src/member-api.js Show resolved Hide resolved
@EvanHahn EvanHahn marked this pull request as ready for review February 19, 2024 21:26
Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

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

I ran out of time today to complete the review - there is a lot of code here to process. Some initial comments here, which might be resolved in other parts of the code that I have not read yet.

proto/rpc.proto Outdated Show resolved Hide resolved
proto/rpc.proto Outdated Show resolved Hide resolved
src/invite-api.js Show resolved Hide resolved
src/invite-api.js Outdated Show resolved Hide resolved
src/invite-api.js Outdated Show resolved Hide resolved
src/invite-api.js Show resolved Hide resolved
src/invite-api.js Outdated Show resolved Hide resolved
src/invite-api.js Show resolved Hide resolved
src/invite-api.js Outdated Show resolved Hide resolved
src/invite-api.js Outdated Show resolved Hide resolved
Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

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

Didn't review the tests, but went through all the other code, and it all looks good other than a couple of small changes. Opinions about deferred promises can be ignored for now.

A thought looking at this, a utility function like this would be helpful:

async function onceSatisfied(emitter, eventName, check, { signal }) {
  return new Promise((res, rej) => {
    signal.once('abort', () => {
       emitter.off(eventName, onEvent)
    })
    emitter.on(eventName, function onEvent (...args) {
        const satisfied = checked.apply(null, args)
        if (!satisfied) return
        emitter.off(eventName, onEvent)
        res.apply(null, args)
    })
  })

We've got quite a few places where we want a await once(), but waiting for an event with specific arguments. A helper like that would make writing the code without the deferred promise a bit easier.

proto/rpc.proto Outdated Show resolved Hide resolved
src/invite-api.js Outdated Show resolved Hide resolved
src/lib/hashmap.js Show resolved Hide resolved
src/invite-api.js Show resolved Hide resolved
src/invite-api.js Show resolved Hide resolved
src/member-api.js Outdated Show resolved Hide resolved
src/member-api.js Show resolved Hide resolved
src/member-api.js Show resolved Hide resolved
src/member-api.js Show resolved Hide resolved
src/member-api.js Outdated Show resolved Hide resolved
Copy link
Member

@gmaclennan gmaclennan left a comment

Choose a reason for hiding this comment

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

Just reviewed the onceSatisfied changes and left some comments - I think these need fixed.
Do you know why CI is failing?

timingSafeEqual(projectDetailsPeerId, peerId) &&
timingSafeEqual(inviteId, details.inviteId),
{ signal: projectDetailsAbortController.signal }
).then((args) => args?.[1])

Copy link
Member

Choose a reason for hiding this comment

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

I think this is needed to avoid an uncaught error if #sendAcceptResponse below throws, since this could throw in a tick after that once this function context no longer exists.

Suggested change
projectDetailsPromise.catch(noop)

Copy link
Member

Choose a reason for hiding this comment

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

(we do want projectDetailsProject to be able to throw, hence this is on a separate line)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can easily change this, but onceSatisfied should never reject, even if aborted—it will resolve null. Therefore, I don't think this needs to be caught. Happy to change any of that, including my understanding.

Copy link
Member

Choose a reason for hiding this comment

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

ah, right, I hadn't fully parsed the implementation of onceSatisfied. I see it won't reject, but notice it can result in an uncaught error, if the check function throws, e.g. in this case it would throw if somehow projectDetailsPeerId or other params were a number or undefined (timingSafeEqual would throw a TypeError).

I think it's always best to assume that async functions throw, and make sure that they are caught, rather than relying on the assumption that they won't throw, because down the line it's easy to add some code that results in them throwing, and it's hard to retain the memory of "this function must never throw".

In a case like this I'm not a huge fan of returning null to indicate abort or an error from a helper like this, although I think that's just a personal preference. I prefer to throw, and if I want to use null as my check then I can do fnThatMightThrow().catch(() => null) or I can choose to handle the error a different way.

Copy link
Member

Choose a reason for hiding this comment

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

TL;DR, onceSatisfied needs to wrap check() in a try...catch, and either reject or return null if it throws. My preference is to throw, but fine with keeping res(null) if preferred. I think we should have the .catch(noop) anyway, so anyone refactoring / changing code doesn't have to remember which async functions can throw and which can't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

onceSatisfied now throws. I added .catch(noop) to its calls. See 759e5c5.

src/member-api.js Show resolved Hide resolved
try {
let handleAborted = noop
const timeoutId = setTimeout(() => {
handleAborted = () => {
Copy link
Member

Choose a reason for hiding this comment

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

I think you need to abort the abortSignal in this timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

yeah but if we never receive an invite response, await receiveInviteResponsePromise will never resolve, and the cleanup functions will never be called. Or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that sequence of events would be:

  1. We set up the receive invite abort controller.
    https://github.com/digidem/mapeo-core-next/blob/25d3c073cb9992ea66f0ecf381bcf5efb2819cea/src/member-api.js#L154
  2. We immediately add it to our list of cleanup functions.
    https://github.com/digidem/mapeo-core-next/blob/25d3c073cb9992ea66f0ecf381bcf5efb2819cea/src/member-api.js#L155-L157
  3. The timeout fires and handleAborted now throws.
    https://github.com/digidem/mapeo-core-next/blob/25d3c073cb9992ea66f0ecf381bcf5efb2819cea/src/member-api.js#L121-L125
  4. handleAborted is called, which throws an error.
    https://github.com/digidem/mapeo-core-next/blob/25d3c073cb9992ea66f0ecf381bcf5efb2819cea/src/member-api.js#L179
  5. The error is caught and cleaned up.
    https://github.com/digidem/mapeo-core-next/blob/25d3c073cb9992ea66f0ecf381bcf5efb2819cea/src/member-api.js#L211-L213

More broadly: even if this code is technically correct1, it's hard to follow. I spent a good amount of time trying to make this clearer but I know I missed at least one good idea: onceSatisfied. I'll try to think of further ways to simplify this, as it's hard to understand.

Footnotes

  1. which it may not be

Copy link
Member

Choose a reason for hiding this comment

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

The code that is going to
potentially take a long time and we want to timeout is awaiting the invite to be accepted on line 181. If that never resolves, then I don't see how the timeout can abort it, since the abort only happens if the code keeps executing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're totally right. Fixed in dbfb6a9.

@EvanHahn
Copy link
Contributor Author

Do you know why CI is failing?

Yes, silly reason: I canceled it. I did this because I knew I was gonna keep working, so I didn't want to waste GitHub's time.

@EvanHahn
Copy link
Contributor Author

Addressed the next round of comments!

@EvanHahn
Copy link
Contributor Author

Got offline approval to merge this, so I'm going for it.

@EvanHahn EvanHahn merged commit 488db27 into main Mar 13, 2024
4 checks passed
@EvanHahn EvanHahn deleted the update-invites branch March 13, 2024 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants