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 andwrite()
for non-files.unix: Write to given UNIX socket. Similar to file, but supports sending file descriptors.
buffer: Write to 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-dcrypt/encrypt: Write encrypted data.
wrapper: Can be used to implement other ostreams where data can be coming from any form of activity.
lib-compression/*
: Write zlib/bzlib/lz4/zstd compressed data.
There are also various other less generic ostreams.
A typical life cycle for an ostream can look like:
o_stream_create()
o_stream_cork()
o_stream_nsend*()
one or more timeso_stream_uncork()
If necessary, check errors with
o_stream_flush()
o_stream_cork()
o_stream_nsend*()
one or more timeso_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 while
running a flush callback (set via o_stream_set_flush_callback()
).
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.