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

Add docs best effort responses #3865

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open

Conversation

oggy-dfin
Copy link
Member

DO NOT MERGE YET - this should be merged only once best-effort responses go live on mainnet.

This PR contains the documentation for the new best-effort response feature. In the process, I noticed that we were lacking a good introductory page on inter-canister calls, addressing topics such as:

  • how do I find which endpoints to call?

  • what's the role of Candid in inter-canister calls?

  • why do I care about the desugaring of async/await

  • attaching cycles to calls

  • Follows the developer docs style guide.

  • Follows the best practices and guidelines.

  • New documentation pages include document tags.

  • New documentation pages include SEO keywords.

  • New documents are in the .mdx file format to support the previous two components.

  • New documents are registered in /sidebars.js, otherwise, it will not appear in the
    side navigation bar.

  • Make sure that the .github/CODEOWNERS file is
    filled with new documents that you added. This way we can ensure that future Pull Requests are reviewed by the right people.

We were missing quite a lot of relevant topics: how to discover
canister endpoints, guidance on how to perform calls, attaching cycles, no
explanation of why the async/await desugaring was important etc. I also now
changed the tag to beginner, as the Rust/Motoko language-specific documents are
also rated as "beginner".

The previous discussion was not really motivated (why should the user care about
desugaring?) and quite language dependent, so I removed most of it - the
language dependent stuff should go into the language specific docs.

Now added all this and also added the difference between best-effort and
guaranteed response calls.
@oggy-dfin oggy-dfin requested review from a team as code owners December 6, 2024 16:42
@github-actions github-actions bot added the documentation Changes to Developer Docs label Dec 6, 2024
Copy link

github-actions bot commented Dec 6, 2024

🤖 Here's your preview: https://tai3p-vaaaa-aaaam-abema-cai.icp0.io

Copy link
Contributor

@alin-at-dfinity alin-at-dfinity left a comment

Choose a reason for hiding this comment

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

Only looked at message-execution-properties.mdx for now and added some nitpicks.

However, I can't help thinking that the topic could be explained more clearly. E.g. start with calls/tasks vs. message executions as we already do, but before we list any properties; just a couple of pictures with simplified sequence diagrams. That way, the difference between call/task and message execution would be immediately clear. It would also immediately make it clear why two requests Req1 and Req2 delivered in that order may produce two responses Rep2 and Rep1, in reverse order (even before the protocol may decide to deliver them in an arbitrary order).

Very likely not for this PR, though.

Copy link
Contributor

@derlerd-dfinity derlerd-dfinity left a comment

Choose a reason for hiding this comment

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

I left feedback on everything but message-execution-properties.md as Alin already made a pass over this one. I'll make a pass once you've addressed Alin's comments.


## Overview of language runtime for asynchronous code
Copy link
Contributor

Choose a reason for hiding this comment

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

Re "true" response from the callee: Strictly speaking, it is also not guaranteed in the current messaging model that the response will come from the callee. The response could also be generated by the system if the request couldn't be delivered to the callee.

The property the current messaging model provides is that the response is globally unique, i.e., if the system produces a reject one can conclude that the callee never saw the request.

Copy link
Member Author

Choose a reason for hiding this comment

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

Re "true" response from the callee: Strictly speaking, it is also not guaranteed in the current messaging model that the response will come from the callee. The response could also be generated by the system if the request couldn't be delivered to the callee.

True, I dropped the "from the callee" part.

The property the current messaging model provides is that the response is globally unique, i.e., if the system produces a reject one can conclude that the callee never saw the request.

Well technically a CANISTER_ERROR is also produced by the system but it means that the callee has seen the request (whether any mutation was made on the callee side is anyone's guess, though)

Copy link
Contributor

Choose a reason for hiding this comment

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

True, I dropped the "from the callee" part.

FWIW, I still see

in this mode the "true" response from the callee is not guaranteed to be delivered

Maybe you just didn't push your change yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have pushed the change, I see below:

but in this mode the "true" response is not guaranteed to be delivered, and can be masked by a system generated error response instead.

Not sure what's going on 🤷

:::

:::info
Note that if the code does not `await` the response, the code after the callback is executed in the same message, until the next inter-canister call is triggered using `await`.
Note that if the code does not `await` the response, the code after the callback is executed in the same message execution, until the next inter-canister call is triggered using `await`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess I just find it awkward that we talk about a call or a callback without an await. If there's no await, nothing is called, you've merely created a future even though it's not obvious from the syntax.

IOW, leaving it at "the code after the await is executed as a separate message execution (callback)" (which the paragraph above already says) is IMHO cleaner than adding "but if there's no await, there's no callback". I would rather change this paragraph to say "note that it is actually the await, not the thing that looks like a function call that triggers the canister call and marks the separation point between message executions". Or something to that effect.


## Overview of language runtime for asynchronous code
Copy link
Contributor

Choose a reason for hiding this comment

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

True, I dropped the "from the callee" part.

FWIW, I still see

in this mode the "true" response from the callee is not guaranteed to be delivered

Maybe you just didn't push your change yet.

Copy link
Contributor

@andrew-lee-work andrew-lee-work left a comment

Choose a reason for hiding this comment

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

LGTM, left some comments.


```bash
dfx canister call subscriber setup_subscribe '(principal "<INSERT_PUBLISHER_PRINCIPAL_HERE>", "Apples")'
$ dfx canister call caller stubborn_set '(42)'

Choose a reason for hiding this comment

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

Out of curiosity. Can you make a bounded_wait or unbounded_wait call specifically in dfx?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not exactly sure what you mean, since dfx calls get their results by polling the ingress


Best-effort calls address both of these problems. However, they can require more effort on the part of the caller to handle the additional error case. Here are some guidelines how to choose between the two types of calls:

* Always prefer best-effort response calls for calls that don't change the state of the callee, i.e., reads.

Choose a reason for hiding this comment

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

reads -> queries? Or is there a difference I'm not aware of?

Copy link
Member Author

Choose a reason for hiding this comment

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

You can always mark a read as a query (though you don't have to)

@derlerd-dfinity
Copy link
Contributor

One thing I just noticed: It seems we updated icrc1_ledger_usage but not the page right next to it: ICP ledger usage.

In this context we should also provide some pointers back and forth (to and from the tutorial page) in terms of how to interpret the related "unclean" errors as the ledger also uses panics as a tool for controlledly rolling back.


let no_unknowns = true;
loop {
match Call::new(ledger, "icrc1_transfer")
Copy link
Member

Choose a reason for hiding this comment

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

Should this be Call::unbounded_wait instead of Call::new? If not, what's the difference between the two?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes it should be! Sorry for the confusion (Call:new is what the CDK alpha version at hand was actually using, so I was going back and forth when checking whether the call compiles, and this slipped through)


A common problem in both distributed and decentralized systems is keeping separate services (or canisters) synchronized with one another. While there are many potential solutions to this problem, a popular one is the **publisher/subscriber** pattern, or "PubSub". PubSub is an especially valuable pattern on ICP as its primary drawback, message delivery failures, does not apply.
We assume that you have the Internet Computer [developer tools](/docs/current/developer-docs/getting-started/install) installed. To follow along, clone the [Git repository](https://github.com/oggy-dfin/icc_rust_docs) of this tutorial. You can find the completed code in the `caller` package. Check out the Git repository of the tutorial. Then, in the tutorial directory, start a local IC instance in the background and install the counter canister.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
We assume that you have the Internet Computer [developer tools](/docs/current/developer-docs/getting-started/install) installed. To follow along, clone the [Git repository](https://github.com/oggy-dfin/icc_rust_docs) of this tutorial. You can find the completed code in the `caller` package. Check out the Git repository of the tutorial. Then, in the tutorial directory, start a local IC instance in the background and install the counter canister.
To follow along, clone the [Git repository](https://github.com/oggy-dfin/icc_rust_docs) of this tutorial. You can find the completed code in the `caller` package.
Check out the Git repository of the tutorial. Then, in the tutorial directory, start a local IC instance in the background and install the counter canister:

Copy link
Member Author

Choose a reason for hiding this comment

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

I noticed that all the Rust docs have a pointer to the dev tools installation; are we getting rid of this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes.

Before getting started, assure you have set up your developer environment according to the instructions in the [developer environment guide](./dev-env.mdx).
## Basic example: getting and setting the counter value

For the first example, we'll build a `call_get_and_set` method on the caller canister, which just invokes the `get_and_set` method on the counter project and forwards the result. This method sets the counter to the new value, and returns the previous counter value. The finished code for the project is already available in `src/caller`. You can clear the contents of `src/caller/src/lib.rs`, and paste the following code. The explanation is in the comments.
Copy link
Collaborator

@jessiemongeon1 jessiemongeon1 Feb 18, 2025

Choose a reason for hiding this comment

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

Suggested change
For the first example, we'll build a `call_get_and_set` method on the caller canister, which just invokes the `get_and_set` method on the counter project and forwards the result. This method sets the counter to the new value, and returns the previous counter value. The finished code for the project is already available in `src/caller`. You can clear the contents of `src/caller/src/lib.rs`, and paste the following code. The explanation is in the comments.
Our first example is a `call_get_and_set` method on the caller canister, which simply invokes the `get_and_set` method on the counter project and forwards the result. This method sets the counter to the new value and returns the previous counter value.
The finished code for the project is already available in `src/caller`. You can clear the contents of `src/caller/src/lib.rs`, and paste the following code. The explanation is within the comments.

Copy link
Member Author

Choose a reason for hiding this comment

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

Reworded the suggestion slightly

Copy link
Collaborator

Choose a reason for hiding this comment

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

We do not use "we" or "our" in the Dev Docs. This is a stylistic choice from leadership.

Copy link
Member Author

Choose a reason for hiding this comment

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

I understand the general intent, but I don't see why we'd need to enforce this blindly. Clearly here it's "our" as "yours, the learner's, and mine, the teacher's", and not as "DFINITY's and definitely not yours". So I'd push back because I don't see the point of why we'd hurt readability, though it's certainly not a hill I'll die on.

Copy link
Collaborator

Choose a reason for hiding this comment

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

From my understanding, this word choice was enforced due to legal concerns, so I'd like to adhere to it.


For the first example, we'll build a `call_get_and_set` method on the caller canister, which just invokes the `get_and_set` method on the counter project and forwards the result. This method sets the counter to the new value, and returns the previous counter value. The finished code for the project is already available in `src/caller`. You can clear the contents of `src/caller/src/lib.rs`, and paste the following code. The explanation is in the comments.

```rust
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this example reference a GItHub link rather than inserting the code manually?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but it turns out that GH links are a pain to work with while writing the docs, both for the author and the reviewers. It's especially bad for the reviewers, because they can't see the actual code in the PR, and they can't comment on it unless the author has another PR on whatever repo the GH source is, plus they have to context switch. They're also a pain for the author, because to update an example you need to make a push to another branch somewhere, view the commit in GH, select the appropriate line numbers, and paste back.

I'm aware that I pushed for testable code samples myself :) I still think this is more important than author/reviewer convenience because documentation rots so quickly otherwise, and thanks for taking the time to make the GH plugin work! But given the current experience, my ideal solution would be to have the samples in the SAME repository as the portal, and ideally be able to just reference them using relative paths instead of having to deal with commits. Basically what this plugin claims to provide.

In the absence of the ideal solution, my current idea is to stick all the samples directly in the PR during the review process, and then replace them with links to a GH repo before committing.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can reference GH files directly in the repo if they are pulled in as submodules, such as how we reference the files from the @dfinity/examples repo. In a perfect world, every example we have lives in that repo, but that is probably not realistic (or at least not currently).

As long as the plan is to replace the code with links before the PR is merged, that is fine :) Thank you!

Copy link
Member Author

Choose a reason for hiding this comment

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

If I referenced this through a submodule, the reviewers still wouldn't get to see the diff of the changes in the example, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

No, but they would have a corresponding Example repo PR to reference that showed just those changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Changes to Developer Docs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants