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