Skip to content
This repository has been archived by the owner on Aug 15, 2022. It is now read-only.

Implement TAP-based request-response flow on top of RPCs #399

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

NoTuxNoBux
Copy link

Summary

This introduces requests with responses, which are just RPCs that can send back a value. Requests are also just layered on top of RPCs and don't introduce any new special treatment, they are just some convenience sugar on top of RPCs where you want to send back a response.

Sending a request will return a Task already known from TAP [1], so you can easily await the response to your request from the receiver. It works similar to HTTP requests.

Context

I needed a way to send back information from one party to another after sending an RPC, and the only way to do that seemed to be two RPCs: one to send the request, and an RPC on the sender that the receiver can in turn invoke to send the response back. This approach resulted in rather entangled code where I would have two handler classes, one for each RPC, that needed to do bookkeeping about what was sent and what response went with what request.

This moves that bookkeeping into Forge, so senders can just await the response and don't have to worry about anything else.

Examples

networkObject.RegisterRequest<SayHelloRequest,SayHelloResponse>("SayHello", async (context) => {
    await DoSomethingElseAsync();
    
    // You can do more async stuff here, and even return a Task yourself. The response won't be sent back until
    // the returned task resolves and will use the value produced by the returned task.

    return new SayHelloResponse("You said: '" + context.Data.Message + "', hello back to you!");
});
var response = await networkObject.SendRequest<SayHelloResponse>(someOtherClient, "SayHello", new SayHelloRequest("Good day!"));

var theOtherPartyReplied = response.Message;

// theOtherPartyReplied = "You said: 'Good Day!', hello back to you!"

Limitations

  • There is no UI to generate requests (yet), like there is for RPCs. This means you need to register them yourself for the moment. It also means that no SendSayHelloRequest is automatically generated in the scenario above and you cannot (yet) avoid specifying the return type in SendRequest (nor enforce the request data type).
  • Requests receive a context that can only have exactly one parameter, the data for the request. If you want to send multiple, serializable, properties, you must put them in an ISerializable class or struct and pass that. Likewise, you can only return one response.
    • I specifically modelled ISerializable after Forge Alloy's new approach, so porting is easier.
  • Not all Receivers work for requests: you either send a request to a single party, or to multiple specific parties (and you will get an array of responses when all of them have come back); you cannot send a request to "all" parties without specifying them explicitly, currently, as the sender must know for how many responses to wait before it resolves the promise, and must in turn know how many parties are going to handle the request.

NOTE: This doesn't necessarily need to be merged, but I wanted to upstream it anyway, to contribute back to the Forge community. Similar support for TAP may also be interesting to port over to Alloy as it's just one message going out and another coming back, so this could act as a reference implementation.

[1] https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap

This introduces requests with responses, which are just RPCs that can
send back a value. Requests are also just layered on top of RPCs and
don't introduce any new special treatment, they are just some
convenience sugar on top of RPCs where you want to send back a response.

Sending a request will return a Task and follow TAP [1], so you can
easily await the response to your request from the receiver.

[1] https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
var requestData = rpcArgs.GetNext<byte[]>();

BMSByte buffer = new BMSByte();
buffer.Clone(requestData);
Copy link
Author

Choose a reason for hiding this comment

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

This may not be optimal as it copies data unnecessarily: I haven't checked if there is a way to create a BMSByte from an existing byte[] without copying.

var responseData = rpcArgs.GetNext<byte[]>();

BMSByte buffer = new BMSByte();
buffer.Clone(responseData);
Copy link
Author

Choose a reason for hiding this comment

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

Same as above.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant