Project

General

Profile

Actions

Feature #17365

closed

Can not respond (well) to a Ractor

Added by marcandre (Marc-Andre Lafortune) almost 4 years ago. Updated almost 4 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:101240]

Description

Summary: currently there is no good way to return a response to a Ractor message.

Sorry, this is long.
Points 1 to 3 look at possible current solutions and why they aren't great.
Point 4 discusses how Elixir/Erlang's builtin filtering allows responses.
Last point proposes one of the many APIs that would allow responses.

Details:

If I want to program a "server" using Ractor, there has to be some way to receive the data from it.
To simplify, say I want a global Config that can be used to set/retrieve some global config parameters.
To set a parameter, I can use server.send [:set, :key, 'value'].
But what about retrieving? There is no good way to achieve that with the current API.

  1. "pull" API

It is not safe, as two clients could send a :set before the server answers, and the clients could resolve their server.take in the reverse order.

Another issue is that Ractor.yield is blocking, so the unexpected death of the client could mean the server hangs, and subsequent requests/responses are desynchronized and thus wrong.

My impression is that the "pull" API is best only used for monitoring of Ractors, rescuing exceptions, etc., or otherwise reserved for Ractors that are not shared, is this correct?

  1. "push" API

It seems much more appropriate to design a server such that one sends the client ractor with the push API. E.g. the client calls server.send [:retrieve, :key, Ractor.current]; the server can use the last element cient_ractor to respond with client_ractor.send 'value' that is non-blocking.

The client can then call Ractor.receive, immediately or later, to get the answer.

This is perfect, except that the client can not use Ractor.receive for any other purpose. It can not act itself a server, or if it calls multiple servers then it must do so synchroneously. Otherwise it might receive a request for something other than the response it was waiting for.

  1. create Ractor + "push" + "pull"

The only way I can think of currently is to create a temporary private Ractor (both to be able to use the "pull" and the "push" API):

# on the client:
response = Ractor.new(server, *etc) { |server, *etc|
  server.send [:retrieve, :key, Ractor.current].freeze
  Ractor.yield(Ractor.receive, move: true)
}.take

# on the server
case Ractor.receive
in [:retrieve, key, client_ractor]
  client_ractor.send('response')
# ...
end

I fear this would be quite inefficient (one Ractor per request, extra move of data) and seems very verbose.

  1. Filtered receive

If I look at Elixir/Erlang, this is not an issue because the equivalent of Ractor.receive has builtin pattern matching.

The key is that unmatched messages are queued for later retrieval. This way there can be different Ractor.receive used in different ways in the same Ractor and they will not interact (assuming they use different patterns).

For a general server ("gen_server"), a unique tag is created for each request, that is sent with the request and with the response

The same pattern is possible to implement with Ruby but this can only work if as long as all the Ractor.receive use this implementation in a given Ractor, it has to be thread-safe, etc.

Issue is that it may not be possible to have the same protocol and access to the same receive method, in particular if some of the functionality is provided in a gem.

  1. In conclusion...

The API of Ractor is currently lacking a good way to handle responses.

It needs to allow filtering/subdivision of the inbox in some way.

One API could be to add a tag: nil parameter to Ractor#send and Ractor.receive that would use that value to match.

A server could decide to use the default nil tag for it's main API, and ask its clients to specify a tag for a response:

my_tag = :some_return_tag
server.send(:retrieve, :key, Ractor.current, my_tag)
Ractor.receive tag: my_tag

# on the server
case Ractor.receive
in [:retrieve, key, client_ractor, client_tag]
  client_ractor.send('response', tag: client_tag)
# ...
end

Tags would have to be Ractor-shareable objects and they could be compared by identity.

Note that messages sent with a non-nil tag (e.g. send 'value' tag: 42) would not be matched by Ractor.receive.
Maybe we should allow for a special tag: :* to match any tag?

There are other solutions possible; a request ID could be returned by Ractor#send, or there could be an API to create object for returns (like a "Self-addressed stamped envelope"), etc.

The basic filtering API I'm proposing has the advantage of being reasonable easy to implement efficiently and still allowing other patterns (for example handling messages by priority, assuming there can be a 0 timeout, see #17363), but I'll be happy as long as we can offer efficient and reliable builtin ways to respond to Ractor messages.


Related issues 1 (0 open1 closed)

Related to Ruby master - Feature #17378: Ractor#receive with filtering like other actor langaugeClosedActions
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0