This documentation is for Dovecot v2.x, see wiki1 for v1.x documentation.
Differences between revisions 5 and 6
Revision 5 as of 2018-06-12 18:23:47
Size: 4900
Editor: JeffSipek
Comment: add o_stream_close() to the typical life cycle description
Revision 6 as of 2021-09-24 15:10:19
Size: 68
Editor: TimoSirainen
Comment:
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
= Output Streams =

{{{lib/ostream.h}}} describes Dovecot's output streams. Output streams can be stacked on top of each others as many times as wanted.

Output streams actually writing data:
 * file: Write to given fd using {{{pwrite()}}} for files and {{{write()}}} for non-files.
 * unix: Write to given UNIX socket. Similar to file, but supports sending file descriptors.
 * buffer: Write to [[Design/Buffers|buffer]].

Output stream filters:
 * hash: Calculate hash of the ostream while it's being written.
 * escaped: Write output escaped via callback. Built-in support for HEX and JSON escaping.
 * multiplex: Multiplex-iostreams support multiple iostream channels inside a single parent istream.
 * null: All the output is discarded.
 * failure-at: Insert a failure at the specified offset. This can be useful for testing.
 * lib-mail/ostream-dot: Write SMTP-style DATA input where the output ends with an empty "." line.
 * lib-dcrypt/encrypt: Write encrypted data.
 * lib-compression/*: Write zlib/bzlib/lz4/lzma compressed data.

A typical life cycle for an ostream can look like:
 * create
 * `o_stream_cork()`
 * `o_stream_nsend*()` one or more times
 * `o_stream_uncork()`
 * If necessary, check errors with `o_stream_flush()`
 * `o_stream_cork()`
 * `o_stream_nsend*()` one or more times
 * `o_stream_uncork()`
 * finalize the ostream with `o_stream_finish()`
 * optionally close the ostream with `o_stream_close()`
 * unref or destroy

Once the ostream is finished, it can't be written to anymore. The `o_stream_finish()` call writes any potential trailer that the ostream may have (e.g. ostream-gz, ostream-encrypt, ostream-dot) while still allowing the caller to check if the trailer write failed. After `o_stream_finish()` is called, any further write will panic. The ostreams that require a trailer will panic if `o_stream_finish()` hasn't been called before the stream is destroyed, but other ostreams don't currently require this. Still, it's not always easy to know whether there might be ostreams that require the trailer, so if there's any doubt, it's preferred to call `o_stream_finish()` just before destroying the ostream.

Usually calling `o_stream_finish()` will also finish its parent ostream. This may or may not be wanted depending on the situation. For example ostream-dot must be finished to write the last "." line, but ostream-dot is always a sub-stream of something else that must not be finished yet. This is why ostream-dot by default has called `o_stream_set_finish_also_parent(FALSE)`, so finishing the ostream-dot won't finish the parent stream. Similarly `connection.c` API sets `o_stream_set_finish_via_child(FALSE)` so none of the socket connections created via it will be finished even though one of their sub-streams is finished. These functions may need to be called explicitly in other situations.

When doing a lot of writes, you can simplify the error handling by delaying the error checking. Use the `o_stream_nsend*()` functions and afterwards check the error with `o_stream_flush()` or `o_stream_finish()`. If you forgot to do this check before the ostream is destroyed, it will panic with: `output stream %s is missing error handling` regardless of whether there is an error or not. If you don't care about errors for the ostream (e.g. because it's a client socket and there's nothing you can do about the write errors), you can use `o_stream_set_no_error_handling()` to fully disable error checks. You can also use `o_stream_ignore_last_errors()` to ignore the errors so far, but not for future writes.

Writes are non-buffered by default. To add buffering, use {{{o_stream_cork()}}} to start buffering and {{{o_stream_uncork()}}} to stop/flush. When output buffer gets full, it's automatically flushed even while the stream is corked. The term "cork" is used because with TCP connections the call actually sets/removes TCP cork option. It's quite easy to forget to enable the corking with files, making the performance worse. The corking/uncorking is done automatically when flush callbacks are called. Using `o_stream_uncork()` will trigger an automatic `o_stream_flush()` but the error is ignored. This is why it acts similarly to `o_stream_nsend*()`, i.e. it requires another explicit `o_stream_flush()`, `o_stream_finish()` or error ignoring before the ostream is destroyed.

If output buffer's size isn't unlimited, the writes can also fail or be partial because there's no more space in the buffer and {{{write()}}} syscall is returning {{{EAGAIN}}}. This of course doesn't happen with blocking fds (e.g. files), but you need to handle this in some way with non-blocking network sockets. A common way in Dovecot to handle this is to just use unlimited buffer sizes and after each write check if the buffer size becomes too large, and when it does it stops writing until more space is available.
Moved to https://doc.dovecot.org/developer_manual/design/ostreams/

None: Design/OutputStreams (last edited 2021-09-24 15:10:19 by TimoSirainen)