[eeps] Signals

Fred Hebert mononcqc@REDACTED
Sun Sep 28 15:13:30 CEST 2014


Nitpicks:

On 09/27, Marcin Sokolowski wrote:
> 2) gen_server:call is blocking which is perfectly fine, selective receive
> that is happening behind the scenes is not.
> 

It rarely happens anymore. An optimization has been added since R14A for
this (and in 17.3 for HiPE).

To make it work, a reference (make_ref()) has to be created in a
function and then sent in a message. In the same function, a selective
receive is then made. If no message can match unless it contains the
same reference, the compiler automatically makes sure the VM will skip
messages received before the creation of that reference.

Therefore, the selective receive ignores all messages that came before
the reference was generated, including to set up the monitor there, and
the call is efficient.

>  == Sending ==
> 
>  I propose following syntax for sending signals
> 

The 'signal' term is already used for EXIT messages, for example, which
can be trapped with process_flag(trap_exit, true). If you intend on
creating a new class of messages, 'signal' is already used.

>  1) Any message that is send to a process that is expecting that message
> doing selective receive should be send as signal. All gen_server:call
> responses should be signals so selective receive can be avoided
> 

This is no longer required. It depends, though, because you did mention
having a second TCP connection to handle these. This would change, but
not sure what the benefit would be at high volume; you could juts be
clogging up the signal line with all the responses.

>  2) Any operational type of messages that are sent to control behaviour of
> the process should be sent as signals. For processes handling large number
> of messages it is not possible to control process quickly enough - control
> message might arrive way to late.
> 

Ah, this is the fun one. Currently, Erlang processes and messages have a
property such that all messages and signals sent by process A to process
B will arrive in order.

This means that if I'm writing a monitoring server that receives
different types of messages:

- {lock, Pid, Resource}: Pid locks Resource
- {read, Pid, Resource}: Pid fetches the value of the resource
- {write, Pid, Resource, NewVal}: Pid modifies the resource
- {free, Pid, Resource}: Pid frees the lock it has on Resource
- {'EXIT', Pid, Reason}: Pid is dead, release all of its Resources

If I receive {'EXIT', Pid, _}, I know the process is dead for good and no
further requests will happen. I can drop the local state attached to
Pid, freeing all resources.

So if I try, in order, to do lock -> write -> free -> lock again ->
write again, and then die, I will see the sequence of events:

{lock, Pid, Resource} -> {write, Pid, Resource, NewVal} -> {free, Pid, Resource} ->
{lock, Pid, Resource} -> {write, Pid, Resource, NewVal} -> {'EXIT', Pid, Reason}

Which is fine and easy to reason about.

Under the new scheme, and assuming {'EXIT', Pid, _} comes back in as a
'signal', I need to care for the following possible ordering:

{lock, Pid, Resource} -> {'EXIT', Pid, Reason} -> {write, Pid, Resource, NewVal} ->
{free, Pid, Resource} -> {lock, Pid, Resource} -> {write, Pid, Resource, NewVal}

The one that hurts here is the last 'lock' and its followup 'write'.
Assuming I'm writing my code properly, I should get a new 'EXIT' message
somewhere after registration. So these 2 interleavings are possible:

{lock, Pid, Resource} -> {'EXIT', Pid, Reason} -> {write, Pid, Resource, NewVal} ->
{free, Pid, Resource} -> {lock, Pid, Resource} -> {'EXIT', Pid, Reason} -> {write, Pid, Resource, NewVal}

Which is fine, but there is also:

{lock, Pid, Resource} -> {'EXIT', Pid, Reason} -> {write, Pid, Resource, NewVal} ->
{free, Pid, Resource} -> {lock, Pid, Resource} -> {write, Pid, Resource, NewVal} -> {'EXIT', Pid, Reason}

And in fact, because they're two different channels with different busy
levels, those are all also few of the possibilities:

{lock, Pid, Resource} -> {write, Pid, Resource, NewVal} -> {'EXIT', Pid, Reason} ->
{free, Pid, Resource} -> {lock, Pid, Resource} -> {write, Pid, Resource, NewVal} -> {'EXIT', Pid, Reason}

{lock, Pid, Resource} -> {write, Pid, Resource, NewVal} -> {'EXIT', Pid, Reason} ->
{free, Pid, Resource} -> {lock, Pid, Resource} -> {'EXIT', Pid, Reason} -> {write, Pid, Resource, NewVal}

Because I'm using a concurrent system that may or may not be
distributed, there is no longer any guarantee that the order I get is
any good or representative of reality.

This makes systems a lot harder to reason about, and would likely break
existing systems. Raise any of your messages to the 'signal' level (even
if you leave exits out of it) and the same problem happens.

Mind you, this problem already partially exists today if you build a
system such that:

A <----- B <--,
  <----- C ---'

Where A can receive messages from C directly, and from C through B, but
these are rarely seen in the wild, and C and B risk being seen as two
different entities, or a bug in design that needs to be (and can be) rectified.

> 1) requiring processes to acquire "special permission" to send signals,
> without it signals would be just simple messages
> 

Wouldn't acquiring special permissions on the receiver make more sense?

> 3) limiting the type of messages that could be sent as signals (e.g. atoms
> only)
> 
This would mean impossibility to know who sent you a message

I've skipped over the implementation details because the semantic ones
seem more important to think about at this point, including one of the
two main rationale points no longer being a problem via compiler
optimizations.

Regards,
Fred.



More information about the eeps mailing list