Skip to content

Commit 6ed62ec

Browse files
committed
Priority Messages EEP
1 parent 6bc544a commit 6ed62ec

File tree

3 files changed

+394
-0
lines changed

3 files changed

+394
-0
lines changed

eeps/eep-0076-1.png

53.3 KB
Loading

eeps/eep-0076-2.png

70.4 KB
Loading

eeps/eep-0076.md

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
Author: Rickard Green <rickard(at)erlang(dot)org>
2+
Status: Draft
3+
Type: Standards Track
4+
Created: 07-Jan-2025
5+
Post-History: https://erlangforums.com/t/eep-76-priority-messages
6+
Erlang-Version: OTP-28.0
7+
****
8+
EEP 76: Priority Messages
9+
----
10+
11+
Abstract
12+
========
13+
14+
In some scenarios it is important to propagate certain information to a process
15+
quickly without the receiving process having to search the whole message queue
16+
which can become very inefficient if the message queue is long. This EEP
17+
introduces the concept of priority messages to the language which
18+
aim to solve this issue.
19+
20+
Motivation
21+
==========
22+
23+
Asynchronous signaling is *the Erlang way* of communicating between processes.
24+
The message signal is the most common type of signal. When a message signal is
25+
received, it is added to the end of the message queue of the receiving process.
26+
As a result of this, the messages in the message queue will be ordered in
27+
reception order. When the receiving process fetch a message from the message
28+
queue, using the `receive` expression, it begins searching at the start of the
29+
message queue. Searching for a matching message is an `O(N)` operation where
30+
`N` equals the amount of messages preceding the matching message.
31+
32+
![Message Reception][]
33+
34+
Figure 1.
35+
36+
This works great in most cases, but in certain scenarios it does not work at
37+
all. At least not without paying a huge performance penalty.
38+
39+
A Couple of Problematic Scenarios
40+
---------------------------------
41+
42+
### Long Message Queue Notification
43+
44+
As of Erlang/OTP 27.0 it is possible to set up a system monitor monitoring the
45+
message queue lengths of processes in the system. When a message queue length
46+
exceeds a certain limit, you might want to change strategy of handling incoming
47+
messages. In order to do that, you typically need to inform the process with a
48+
long message queue about this.
49+
50+
Sending it a message informing about the long message queue will not work,
51+
since this message will end up at the end of the long message queue. If the
52+
receiver handles messages one at a time in message queue order, it will take a
53+
long time until the receiver fetch this message. The situation will at this
54+
point very likely have become even worse.
55+
56+
If the receiver instead periodically tries to search for such messages using
57+
a selective receive, it will periodically have to do a lot of work. This
58+
especially when the message queue is long. Polling the message queue length
59+
using `process_info/2` will in this case be a better workaround. That is,
60+
communicating this information between processes using asynchronous signaling
61+
does not work in this scenario, or at least work very poorly.
62+
63+
### Prioritized Termination
64+
65+
Another scenario that have similar issues. A worker process that handles large
66+
jobs is supervised in a supervision tree. It is easy to envision that such a
67+
worker could get a large amount of requests in its message queue. If the
68+
supervisor dies or wants the worker to terminate, the worker will receive an
69+
exit signal from its supervisor. If the worker trap exits, the corresponding
70+
`'EXIT'` message will end up at the end of the message queue.
71+
72+
If one wants to be able to terminate the worker prior to having to handle all
73+
other requests in the message queue, one either has to stop using trap exit or
74+
periodically do selective receives searching for such `'EXIT'` messages. Not
75+
trapping exits might not be an option and doing periodical selective receives
76+
will be very expensive if the message queue is long. The [pull request 8371][]
77+
aimed to solve this scenario.
78+
79+
A workaround in this scenario could be to poll the supervisor using the
80+
`is_process_alive/1` BIF in combination with polling of an ETS table where the
81+
supervisor can order it to terminate. That is, also in this scenario using
82+
asynchronous signaling in order to communicate this information between
83+
processes does not work, or at least work very poorly.
84+
85+
Polling Workarounds
86+
-------------------
87+
88+
In order to be able to solve scenarios like these without the risk of having to
89+
do a lot of work in the receiving process, one have to resort to passing the
90+
information other ways and let the receiver poll for that information. For
91+
example, write something into an ETS table and let the receiver poll that ETS
92+
table for information. This will prevent potentially very large costs of
93+
having to repeatedly do selective receives, but the polling will not be for
94+
free either.
95+
96+
In order to be able to handle scenarios like the ones above using asynchronous
97+
signaling, which is *the Erlang way* to communicate between processes, the
98+
following mechanism for sending and receiving priority messages between
99+
processes is proposed.
100+
101+
Rationale
102+
=========
103+
104+
By letting certain messages get priority status and upon reception of such
105+
messages insert them before ordinary messages in the message queue we can
106+
handle scenarios like the above with very little overhead. Besides getting a
107+
solution that most likely will have less overhead than any workaround for
108+
communicating information like this, we also get a solution where asynchronous
109+
signaling between processes still can be used.
110+
111+
The proposed handling of priority messages in the message queue:
112+
113+
![Priority Message Reception][]
114+
115+
Figure 2.
116+
117+
There will be no way for the Erlang code to distinguishing a priority message
118+
from an ordinary message when fetching a message from the message queue. Such
119+
knowledge needs to be part of the message protocol that the process should
120+
adhere to.
121+
122+
The total message queue length in figure 2 equals `P+M`. The lengths `P` and
123+
`M` will not be visible. The only visible length is the total message queue
124+
length.
125+
126+
A `receive` expression will select the first message, from the start, in the
127+
message queue that matches, just as before.
128+
129+
How to Insert Priority Messages in the Message Queue?
130+
-----------------------------------------------------
131+
132+
By letting priority messages overtake ordinary messages that already exist in
133+
the message queue we get priority messages ordered in reception order among
134+
priority messages followed by ordinary messages ordered in reception order
135+
among ordinary messages. Instead of just overtaking ordinary messages, one
136+
could choose to let a priority message overtake all messages in the message
137+
queue regardless of whether they are priority messages or not, but then
138+
multiple priority messages would accumulate in reverse order. Having these two
139+
sets of messages ordered internally by reception order at least to me feels the
140+
most useful. Just as in the case of ordinary messages we will probably want to
141+
handle priority messages in reception order.
142+
143+
Note that the reception order of signals is not changed. If a process sends an
144+
ordinary message and then a priority message to a another process, the ordinary
145+
message will be received first and then the priority message will be received.
146+
The only difference is that when the priority message is received, it will be
147+
inserted earlier in the message queue than the ordinary message. That is,
148+
[the signal ordering guarantee][] of the language will still be respected. This
149+
just modifies how the message queue is managed.
150+
151+
How to Determine What Should be a Priority Message?
152+
---------------------------------------------------
153+
154+
By introducing priority messages, the messages in the queue will not
155+
necessarily be in the order the corresponding signals were received. There will
156+
be a lot of code that assumes that the order of messages in the message queue
157+
is in reception order, so it is reasonable that one should need to opt-in in
158+
order to be able to receive priority messages.
159+
160+
This EEP propose that selected priority marked messages, selected exit
161+
messages, and selected monitor messages should be treated as priority messages.
162+
Perhaps one would want other types of messages to be treated as priority
163+
messages as well, but the set of allowed priority messages can easily be
164+
extended in the future if that should be the case. The following list
165+
describes how the different types of messages will be enabled as priority
166+
messages:
167+
168+
* *Priority Marked Messages* - A message is marked as a priority message by the
169+
sender by passing the option `priority` in the option list that is passed as
170+
third argument to the `erlang:send/3` BIF. The receiver opts-in for reception
171+
of priority marked messages from a specific sender by calling the
172+
`process_flag/2` BIF like this:
173+
`process_flag({priority_marked_message, SenderPid}, true)`.
174+
* *Exit Messages* - The receiver opts-in for reception of priority exit
175+
messages from a specific process or port by calling the `process_flag/2` BIF
176+
like this:
177+
`process_flag({priority_exit_message, SenderPidOrPort}, true)`.
178+
* *Monitor Messages* - The receiver opts-in for reception of priority monitor
179+
messages due to a specific monitor being triggered by calling the
180+
`process_flag/2` BIF like this:
181+
`process_flag({priority_monitor_message, MonitorRef}, true)`.
182+
The receiver can also opt-in for reception of priority monitor messages by
183+
passing the option `priority` in the option list that is passed as third
184+
argument to the `monitor/3` BIF when creating the monitor.
185+
186+
The receiver process can at any time disable reception of certain priority
187+
messages by passing `false` as second argument to any of the above listed
188+
`process_flag/2` BIF calls.
189+
190+
The reason for not having options for accepting all priority marked messages,
191+
all exit messages, or all monitor messages as priority messages is the risk of
192+
introducing bugs when code in other modules are called from the process
193+
accepting priority messages. For example, if a process enables all monitor
194+
messages as priority messages and then makes a call into a module that makes
195+
a `gen_server` call, a `'DOWN'` message due to the call could be selected even
196+
though a reply message due to the call had been delivered before the `'DOWN'`
197+
message. In this case, the call would fail even though it actually succeeded.
198+
The reply message would then also be left as garbage in the message queue
199+
without any code picking it up.
200+
201+
When a potential priority message is received, the receiver will check if it
202+
has enabled priority message reception for this message. If it has been
203+
enabled, the priority message will overtake all ordinary messages in the
204+
message queue and will be inserted after the last accepted priority message in
205+
the queue. If it has not been enabled, the message will be treated as any
206+
ordinary message and will be added to the end of the message queue. See figure
207+
2.
208+
209+
The Selective Receive Optimization
210+
----------------------------------
211+
212+
Current Erlang runtime system has a selective receive optimization that can
213+
prevent the need to search large parts of the message queue for a matching
214+
message. It is triggered when a reference is created and then matched against
215+
in all clauses of a `receive` expression. Messages present in the message queue
216+
when the reference is created do not have to be inspected, since they cannot
217+
contain the reference.
218+
219+
When the optimization is triggered a marker is inserted into the message queue
220+
and only messages after the marker are searched. This optimization can make a
221+
huge impact on performance if the process has a long message queue. This
222+
optimization is frequently used in OTP code such as, for example, in a
223+
`gen_server` call.
224+
225+
The insertion of a priority message in the message queue clashes with the
226+
receive optimization since a reference now can appear earlier in the message
227+
queue than where the receive marker was inserted. One solution to this problem
228+
could be to disable the selective receive optimization on processes that
229+
enables priority messages. The user of priority messages would in that case
230+
have to be very careful not to call into modules that might rely on the
231+
selective receive optimization. This would more or less make it impossible to
232+
safely call modules that you don't have full control over yourself, since it
233+
in the future might be modified in a way so that it relies on the selective
234+
receive optimization taking effect. Therefor I find it unacceptable to disable
235+
the selective receive optimization. The priority message implementation needs
236+
to be able to preserve the selective receive optimization.
237+
238+
Distributed Erlang
239+
------------------
240+
241+
Handling of priority messages should be completely distribution transparent.
242+
You should be able to send and receive priority messages between nodes the
243+
same way as done locally.
244+
245+
Alternative Solutions Considered
246+
--------------------------------
247+
248+
A separate priority message queue per process exposed to the Erlang program
249+
could be an alternative solution. You would need a way similar to this
250+
proposal to choose which messages should be accepted as priority messages.
251+
There would also need to be some new syntax in order to multiplex matching of
252+
messages from the different message queues. This would be a larger change of
253+
the language without providing any extra benefits as I see it.
254+
255+
There have been suggestions for multiple priority levels similar to the process
256+
priority levels. This could be viewed as an extension to this proposal. The
257+
implementation could relatively easily be extended with multiple priority
258+
levels even though it would complicate the implementation. A `low` priority
259+
level similar to the process priority level `low` which is mixed with the
260+
`normal` process priority level would be very strange to introduce, though.
261+
This since there would not be any easy way of understanding which message will
262+
be fetched from the message queue at a specific message queue state. I think
263+
multiple priority levels should be left for the future if a good enough use
264+
case is presented.
265+
266+
Backwards Compatibility
267+
=======================
268+
269+
Since the receiver process needs to opt-in in order to get any special handling
270+
of priority messages, this will be completely backwards compatible.
271+
272+
Summary
273+
=======
274+
275+
The proposed solution for priority messages enables users to solve problems
276+
using asynchronous signaling, which is *the Erlang way* of communicating,
277+
where they previously had to resort to workarounds using polling of some sort.
278+
It is likely to reduce the performance impact in most, if not all, scenarios
279+
where one otherwise needs to resort to polling of some sort. Since you need to
280+
opt-in to this new behavior it is completely backwards compatible. The changes
281+
to the language are very small, just "a light touch". On the conceptual level,
282+
it is very easy to understand how the priority messaging works assuming that
283+
you understand how asynchronous signaling in the language work.
284+
285+
Reference Implementation
286+
========================
287+
288+
The reference implementation can be found in [pull request 9269][] of the
289+
[Erlang/OTP repository][].
290+
291+
Care has been taken to have as small impact as possible on processes not
292+
utilizing priority messages. Processes not enabling reception of priority
293+
messages will not use any more memory at all due to the priority messages
294+
implementation.
295+
296+
A few Notes on the Implementation
297+
---------------------------------
298+
299+
### The Message Queue
300+
301+
The message queue may contain messages as well as receive markers utilized by
302+
the selective receive optimization. Receive markers are currently also used
303+
for adjustments that needs to be done to the message queue during certain
304+
operations. That is, the current code traversing the message queue needs to be
305+
prepared to encounter receive markers of different types.
306+
307+
When the user enables reception of priority messages, a block containing three
308+
receive markers and an area for auxiliary data is allocated. The receive
309+
markers are of new types distinguishable from the already existing receive
310+
markers. The auxiliary data, among other things, contains a red/black search
311+
tree containing information about what type of messages the process accepts as
312+
priority messages. All memory allocated for handling of priority messages is
313+
referred to from this memory block.
314+
315+
The first and the second receive markers are inserted at the start of the
316+
message queue. The first marker marks the start of priority messages and the
317+
second marks the end of priority messages. The first marker also serves as an
318+
entrance for finding all information about the priority message handling. When
319+
a priority message is accepted it will be inserted just before the end marker.
320+
The third marker is inserted in the message queue when we need to remember a
321+
place in the message queue. This is needed when a priority message is accepted
322+
while we currently are traversing the message queue.
323+
324+
#### Receive Optimization
325+
326+
If we have active receive markers for the selective receive optimization in the
327+
message queue and a priority message is accepted, we scan the message for
328+
references. If a reference corresponding to a receive marker is found, we mark
329+
in the receive marker that the reference has been seen in the part of the
330+
message queue containing priority messages. When we enter a `receive`
331+
expression where a receive marker is used and it has been marked in the
332+
receive marker that the reference has been seen in a priority message, we
333+
search the priority messages prior to continuing with the messages after the
334+
receive marker.
335+
336+
A further optimization that could be done to the receive optimization is to
337+
insert yet another receive marker before the first priority message containing
338+
the reference, but I see that as a premature optimization. A process is not
339+
expected to accumulate a large amount of priority messages. If so, the process
340+
has used priority messages in a way not intended.
341+
342+
### Priority Messages in Transit
343+
344+
There exists a number of different types of signals. For each type of signal an
345+
action is taken when the signal is received. Ordinary messages are special
346+
since they are very common and the only action taken upon reception of an
347+
ordinary message is to add it to the end of the message queue. Due to this,
348+
the signal queue for incoming signals is arranged as a skip list where each
349+
non ordinary message signal points to the next non ordinary message signal.
350+
This way we can move a whole batch of ordinary messages into the message queue
351+
at once.
352+
353+
Priority marked message signals need to be sent as non ordinary message
354+
signals, since they need to have another action taken than the default. There
355+
are other signals that are received as non ordinary message signals, but then
356+
transformed into ordinary messages depending on the state of the receiving
357+
process. An example of such a signal is a message sent using an alias. Upon
358+
reception of such a message the receiver checks if the alias is still active.
359+
If it is, then adds it to the end of the message queue; otherwise, drops the
360+
message. Since a message sent using an alias is very similar to a priority
361+
marked message, the implementation for alias messages has been generalized to
362+
handle *alternate action messages*. Both a priority marked message and a
363+
message sent using an alias are just messages with an alternate action to take
364+
upon reception than the default, so both of them will use the alternate action
365+
message implementation.
366+
367+
[Message Reception]: eep-0076-1.png
368+
"Message Reception"
369+
370+
[Priority Message Reception]: eep-0076-2.png
371+
"Priority Message Reception"
372+
373+
[the signal ordering guarantee]: https://www.erlang.org/doc/system/ref_man_processes.html#delivery-of-signals
374+
375+
[pull request 8371]: https://github.com/erlang/otp/pull/8371
376+
377+
[pull request 9269]: https://github.com/erlang/otp/pull/9269
378+
379+
[Erlang/OTP repository]: https://github.com/erlang/otp
380+
381+
Copyright
382+
=========
383+
384+
This document is placed in the public domain or under the CC0-1.0-Universal
385+
license, whichever is more permissive.
386+
387+
[EmacsVar]: <> "Local Variables:"
388+
[EmacsVar]: <> "mode: indented-text"
389+
[EmacsVar]: <> "indent-tabs-mode: nil"
390+
[EmacsVar]: <> "sentence-end-double-space: t"
391+
[EmacsVar]: <> "fill-column: 70"
392+
[EmacsVar]: <> "coding: utf-8"
393+
[EmacsVar]: <> "End:"
394+
[VimVar]: <> " vim: set fileencoding=utf-8 expandtab shiftwidth=4 softtabstop=4: "

0 commit comments

Comments
 (0)