Commit Graph
2810 Commits
Author SHA1 Message Date
Wez Furlong 4ed60d30a1 docs: update for release 2025-01-29 17:05:37 -07:00
Wez Furlong 833f82a818 egress_source: fix potentially stale rr state when config changes
Previously, we would compare only the names of the pools when
updating the round robin source selection state.

That meant that if you change the composition of a pool by changing
either the number of the definition of its constituent sources,
that change may not take effect until the corresponding queues
had aged out.

This commit resolves this by comparing both the name and the
composition of the pool when deciding to rebuild the source
selection state.
2025-01-29 12:38:38 -07:00
Wez Furlong 8042f7b07a config: invalid pooled lua context when epoch changes
I noticed while testing the throttle serialization commit
that precedes this one that my directly-in-the-lua-file config
changes weren't being picked up when the config epoch changed.

This commit revises the pooling logic to also check the epoch
in addition to the age of the pooled entries.
2025-01-29 11:24:36 -07:00
Wez Furlong 3da38d986e throttle: serialize in string notation
Prior to exposing max_burst in the throttle string parser, we
couldn't guarantee to represent any arbitrary throttle as a string,
so we serialized as the underlying struct fields.

That's a bit unwieldy to read, so let's switch to the string
representation.

This commit makes the string repr infallible (previously it could fail
if you had specified max_burst), then tackles the fanout from that.

Apparently none of our snapshot tests include any of these throttles,
so I manually spot checked it with `kcli inspect-sched-q` to observe
the `max_message_rate`.
2025-01-29 10:57:18 -07:00
Wez Furlong 04a1777727 docs: changelog for #329 2025-01-29 10:28:54 -07:00
ncai a2640800b8 add test for rfc3464 with arrival-date 2025-01-29 10:27:41 -07:00
ncai 1a43079daf convert arrival-date header for oob 2025-01-29 10:27:41 -07:00
Wez Furlong ea8f2be678 docs: add weird period example to throttle docs 2025-01-29 09:45:04 -07:00
Wez Furlong 87e0939f43 throttle: expose max_burst throttle spec
This was always present, it just wasn't something we provided a syntax
to specify.

max_burst allows you to control how quickly your throttle budget will
be consumed over the throttle time period.

The default max_burst is equal to the limit you specify, allowing
the full budget to be instantaneously consumed.

refs: https://github.com/KumoCorp/kumomta/issues/326
2025-01-29 09:22:57 -07:00
Wez Furlong c4d53b07db queue: fix immediately-due requeue race condition
We've been trying to run down an issue where a subset of messages
can get stuck in the scheduled queue. This has only occured in
a single deployment as far as we know; that deployment makes
use of the `requeue_message` event handler.

The identifying characteristics of the bad state are:

* `kcli inspect-sched-q` returns information about messages where
  the due time is less than the `now` field in the inspect result.
* The scheduled queue depth for that queue is flat

I've been scratching my head over this, but I *think* the issue
is a race condition during insertion.  The potential race was:

* [thread a] insert message to SINGLETON_WHEEL that is due immediately
* [wheel maintainer thread] pop due message from SINGLETON_WHEEL
* [wheel maintainer thread] verify that message is tracked by
  queue-local hash <fail>
* [thread a] add message to queue-local hash, bump scheduled queue
  counters

Those immediately due messages would sit there until a rebind forces
them to be reconsidered, or until kumod was restarted (with the same
overall effect).

The race condition could only trigger when:

* The message(s) being inserted are immediately due
* It is some multiple of 3 seconds since the singleton wheel was
  started; that is how often it awakes to process the scheduled
  queue timer wheel.

This commit re-orders things such that the queue-local hash is updated
prior to inserting into SINGLETON_WHEEL. The ordering is enforced
by the containing mutex.
2025-01-28 13:38:49 -07:00
Wez Furlong 8d9b10aad1 rfc5321 client: add Read/Write error variants
We had a few too many "bland" NotConnected errors showing up in
transient failure logs; while it may have been technically
correct, it was lacking in context.

This commit replaces NotConnected in the read/write error paths
with Read/Write error variants that include the error message,
partial read (for Read errors) and the associated command/stage.

For pipelining errors, we can now dup this same state into each
of the command slots when considering the pipelining error as
well; previously we couldn't clone the error type because of
the (unused!) ssl error variant.
2025-01-28 13:16:44 -07:00
Wez Furlong 9f8602bd1d maildir: handle TOCTOU-ish race when creating dir structure
The CI flakes I've been running down are due to this:

```json
{"type":"TransientFailure","queue":"maildir","response":{"code":400,"enhanced_code":null,
"content":"failed to write to maildir: creating dirs for maildir /tmp/.tmpBi1p8H/maildir in queue maildir: File exists (os error 17)"}
```

which sounds like a race condition when multiple threads are trying
to create that maildir directory structure from scratch at the same
time.

This commit adds some error handling to catch and deal with this
appropriately.
2025-01-28 12:34:21 -07:00
Wez Furlong 00241b85cc ci: reduce flakeyness of disconnect/reconnect strategy tests
Make sure that we wait for the sink deliveries to complete before
making the overall summary assertion.
2025-01-28 11:19:54 -07:00
Wez Furlong 3af505f450 kcli: tracer: allow unbounded buffering of trace data
It's more acceptable for the client to go nuts with its own memory usage
than it is for the server side to waste memory on trace data, so let's
allow it to buffer indefinitely while it is busy formatting the output.
2025-01-28 11:07:59 -07:00
Wez Furlong dc1541c030 kcli: tracer: remove redundant bytes read|written output
When we output just a single line of data, there is no need
to also emit another line with its length. This was a side effect
of adding terse mode to the client side.

Now that we're doing the terse processing server side, the
non-Abbreviated cases are never actually terse-processing eligible
on the client (assuming that the client and server version match),
so having the byte length summary presented there is especially
noticeable.

We can simply elide it by trimming off trailing whitespace.
2025-01-28 10:59:09 -07:00
Wez Furlong 5f76d85d23 kcli: switch tracers to async websocket client
This should make things play a little more nicely in the client,
but it's not a significant difference in throughput or performance.
2025-01-28 10:49:23 -07:00
Wez Furlong e3b3bfefb3 docs: changelog for #328 2025-01-28 10:35:58 -07:00
Wez Furlong d224feb62f tracing: enable server-side processing of terse flag
Cutting down on the data being transmitted can reduce the number
of lag events.

Increase the channel buffer size as well while we're in here.
2025-01-28 10:35:19 -07:00
Wez Furlong e3c3f5fbe0 tracing: emit diagnostic if the tracer has lagged
Previously we'd simply drop the connection. With this behavior we now
emit an error to the journal (once per tracer session) and emit a
diagnost to the client (each time) whenever some number of events
were dropped by the internal broadcast channel.
2025-01-28 10:35:19 -07:00
Wez Furlong 33bc16555b smtp_dispatcher: add message_id to tracer metadata 2025-01-28 10:35:18 -07:00
Wez Furlong dccb0cc03a smtp_dispatcher: respect reconnect_strategy for 503 errors 2025-01-28 10:35:18 -07:00
Nathalie Caiandncai 1dc4081c1d change kafka payload type to vector of bytes (#328)
* change kafka payload type

Co-authored-by: ncai <ncai@chapsvision.com>
2025-01-28 10:35:04 -07:00
Wez Furlong a17971e4c3 ci: increase some timeouts
This reduces flakiness when there are multiple jobs running concurrently
in CI
2025-01-27 09:51:36 -07:00
Wez Furlong 768181ac2b smtp_dispatcher: avoid categorizing reconnects as "no more hosts"
This adds a way for the lower level dispatcher impl to communicate
back up to the higher level that the session is done and can be
closed out without treating it as a connection failure.

For SMTP the criteria for this is that, if we ever got as far
as MAIL FROM, then we consider that the connection was established
OK and that means that if we subsequently reach a point where we need to
connect again, then it must be due to an earlier policy issue that was
already recorded as a transient failure.

In that circumstance, if we have not more hosts to connect to, we
will indicate that we terminated "OK".

This works because the upper level checks for this state before
asking the session to connect again.

If there are more addresses that could be tried, we will report a
connection failure for the next message picked up by the session;
this commit only changes the behavior for the case where there
are no more addresses left in the current connection plan.
2025-01-27 09:01:37 -07:00
Wez Furlong 399c8505ae avoid potential cache thrashing issue with remember_broken_tls
A customer reported a really long log record like:

```
KumoMTA internal: failed to connect to any candidate hosts: TLS
handshake failed: tls handshake eof, will re-connect in the clear
because opportunistic_tls_reconnect_on_failed_handshake=true, TLS
handshake failed: tls handshake eof ...
```

the site in question had just a single address in its connection plan.

I don't see how we could loop like this unless the cache that is
used to indicate that TLS is broken is being thrashed and the value
that we write to it in response to this event triggering is
never actually sticking in the cache.

This commit introduces a per-session variable to flag that TLS
is broken for this particular site, so that we don't have to rely
on the cached value when we're about to loop and retry.
2025-01-24 19:34:11 -07:00
Wez Furlong f9d8ceee9d inspect-sched-q: add now time to the response
This is useful when sanity checking the due times vs. the current time
2025-01-24 15:28:37 -07:00
Wez Furlong 41b6e79c4e tidy up chrono timedelta math
I'm investigating an issue where a block of messages appear to be stuck
in a scheduled state.  In looking at the math here, I'm wondering if it
is possible for some overflow to happen when converting from chrono
to std time types, so adjust the logic to be less prone to such a thing,
if it is occurring.
2025-01-24 15:11:03 -07:00
Wez Furlong e474516b01 add reconnect_strategy option
This controls what we do in an smtp session when we experience a
disconnect during message sending; do we give up on the session,
continue with the connection plan, or try to connect to the same
host again and continue sending any additional messages there?
2025-01-24 13:43:49 -07:00
Wez Furlong 54e809dc93 smtp_dispatcher: fix incorrect reuse of closed connection
In the smtp client we internally map IO and timeout errors into
a NotConnected state and break its internal socket, prevent it
from trying to talk to a broken session.

In the dispatcher layer that sits above it, when we consider
whether we need to open a connection, we look only to see if
we have a connection open or not.

If that connection is in a broken state we would keep trying to
use it, because we didn't consider that it might have such
an internal broken state.

This commit fixes up that logic so that we realize that it is
broken, and allow us to move to the next host in the connection
plan.
2025-01-24 13:43:49 -07:00
Wez Furlong b29cea10f4 rfc5321 client: fix rset regression 2025-01-24 13:43:49 -07:00
Wez Furlong 01535da45f rfc5321 client: break socket in more IO error cases
For the read code paths: if we experience an IO error or timeout,
ensure that we break the socket so that we don't try to reuse it
in the future, like we do for (most of) the write code paths.

For the write code paths, there were a couple of missing spots
where we didn't guarantee to break the socket, so ensure that
we do by centralizing all writes through a newly factored function
for that purpose.

Ensure that we log a diagnostic to the tracer for IO and timeout errors.

Generate a Closed event for the tracer whenever we break the socket.

Remove ClientError::Io, as we want to standardize on NotConnected
for the IO error case.  This highlighted that flushing the socket
could also generate an IO error, so add the same sort of diagnostic
trace, socket break and error code mapping there for that as well.
2025-01-24 13:43:48 -07:00
Mike Hillyer c606ac8244 Update changelog title for consistency. 2025-01-24 11:50:14 -05:00
Wez Furlong 6841841283 first run at an inspect-sched-q command
This can return a sample (or all) of the messages as well as
some additional queue metadata for a named scheduled queue.

refs: #231
2025-01-23 17:10:41 -07:00
Wez Furlong 01c52eef03 docs: update for 2025.01.23-7273d2bc release 2025-01-23 13:49:35 -07:00
Wez Furlong 7273d2bcd6 cargo update 2025-01-23 11:56:31 -07:00
Wez Furlong af6de45322 kumo.make_message: accepts bytes and not just utf8
Sometimes you have non-utf8 data that you want to load in
2025-01-23 10:46:14 -07:00
Wez Furlong bb66d06169 docs: add opportunistic_tls_reconnect_on_failed_handshake to changelog 2025-01-23 07:32:08 -07:00
Wez Furlong c215ed9c2f rfc5321: add enable_rset and enable_pipelining options
These control whether we'll use RSET and/or PIPELINING when
sending via SMTP.
2025-01-23 07:20:00 -07:00
Wez Furlong 636f998a25 rfc5321 client: avoid RSET for first message on a connection
This is to avoid always running into a postfix load shedding
strategy where it categorizes RSET as a junk command and will
reject the transaction.

https://www.postfix.org/STRESS_README.html
2025-01-22 18:59:55 -07:00
Wez Furlong 3dfa728bd4 rfc5321 client: use single buffer for pipeline send
Previously, when using pipelining, we'd still send the individual
commands one by one, resulting in potentially multiple packets over the
wire, then we'd read the command responses back.

This was really only taking advantage of half of what pipelining offers,
by avoiding the read stalls between each of the pipelined commands.

What this commit does is to pre-format those writes into a single buffer
so that we have a single logical send which stands a good chance of
being transmitted as a single packet on the wire.  (TLS may complicate
this in practice).

This should make a marginal improvement in latency, but it may also help
to address some flakiness in the integration tests that we part of
df8bb0f3ae, where we sometimes see the
disconnect without reading the 421 response back.  My theory for those
is that, due to noisy neighbors on the CI system, we stand a good chance
of hitting a write error when writing out the later commands in the
pipeline, after the one that triggered a 421 in the peer, if that has
had time to propagate the shutdown back to us already.
2025-01-22 16:26:42 -07:00
Wez Furlong 5a81ed0501 smtp_server: flush/shutdown TLS gracefully when concurrent connections exceeded
The other place we need to handle, similarly to d6459ab2e4
2025-01-22 15:51:36 -07:00
Wez Furlong df8bb0f3ae rfc5321 client: fixup pipelining error handling
98a5479ce9 changed the logic around
pipelining error handling in a subtle way: it switched from sequentially
considering both the transport and protocol errors for each of the
pipelined commands to eager consideration of transport errors, before
considering any transport errors.

In a situation where the remote host issues a 421 response to the MAIL
FROM that is part of our pipeline of (RSET, MAIL FROM, RCPT TO, DATA),
rather than reporting a 421 response to MAIl FROM, we'd end up bubbling
up the NotConnected error from RCPT TO, leading to the miscategorization
and so on that was improved in caadc0e7e8

This commit fixes up this situation by:

* Ensuring that we pad out the number of results from pipeline_commands
  to match the number of commands, synthesizing NotConnected errors
  for the remaining slots if we experience a transport error.

* Check for any transport error and adjust the logic to essentially
  prefer handling a protocol error from an earlier stage, rather
  than taking the transport error.  This allows the transport-success
  (eg: peer didn't disconnect) case to continue to defer acting upon
  any protocol errors until we have the full set of protocol responses
  back, so that we can handle the obscure but important DATA case that
  was the original intent behind the changes in
  98a5479ce9
2025-01-22 15:12:25 -07:00
Wez Furlong d6459ab2e4 smtp_server: gracefully flush socket when rejecting with 421
Previously, we'd just drop the socket without a care about any
pending data that might be buffered up inside it.

The next commit depends on seeing the 421 from the integration test,
but because of the drop, we'd just see a rustls error about the peer
disconnecting without sending close-notify.

This commit adds a graceful shutdown to the 421 rejection case.
2025-01-22 15:10:13 -07:00
Wez Furlong caadc0e7e8 ready_queue: log TransientFailure if deliver_message errors out
The logic in this part of the ready_queue is to loop while we have
messages, and work through the connection plan.

There are a few cases in the lower level smtp_dispatcher that don't
directly manipulate the disposition of a message if something goes
wrong. The logic at that layer will bubble up an error with the
expectation that the ready_queue looping logic will try again
on another connection.

What actually happened was that the error propagated up and the
message would get "silently" re-queued (with the recent Delay
logging, we'd get a DispatcherDrop record for the context).

This isn't desirable because the visibility into what happened
is low, and also because the action being taken isn't necessarily
in alignment with what smtp_dispatcher thinks is happening.

What this commit does is to catch the error case (instead of
letting it propagate) and log a transient failure.

This handles the visibility side of this issue.
We'll tackle the mismatched expectations in a follow-on commit.
2025-01-22 14:27:39 -07:00
Wez Furlong b8752b23b1 docs: mention LMTP support
refs: #267
2025-01-20 08:32:48 -07:00
Wez Furlong cf6f3540e7 Add new Delay log record type
The purpose of this record is to log additional context about why
a message might end up in the scheduled queue when it hasn't
logged a TransientFailure.

There are a few situations around handling throttles and limits
where we might put a message back into the scheduled queue, without also
logging a TransientFailure record. It's possible that we should
reconsider some of those, but for the moment, there is an observability
hole that needs to be filled.

What this commit does is introduce an `InsertContext` which can hold one
or more `InsertReason`s about why a message is being inserted into the
scheduled queue.

There are 3 primary reasons for insertion:

* Received - the message was just received/injected
* Enumerated - the message was discovered in spool enumeration
* DueTimeWasReached - the message is now due for delivery and is being
  popped off the scheduled queue

The additional reasons can be added to the context to provide more
color about what happened.

When a message is added to the scheduled queue, the accumulation
in the InsertContext is examined, and if the context doesn't
indicate that the message was Enumerated and it wasn't also
already logged as a TransientFailure, a `Delay` record is
logged.

The `Delay` record includes in its `response.content` the ordered set of
InsertReasons as well as the delay duration and due time.

Logging Delay records might place undesirable pressure on the
logging storage, so you may wish to disable it via:

```lua
kumo.configure_local_logs {
  per_record = {
    Delay = {
      -- Suppress Delay records
      enable = false
    }
  }
}
```

or similar.
2025-01-17 13:21:47 -07:00
Wez Furlong 80b7e6520e resolve-shaping-domain: allow specifying the source name 2025-01-17 10:07:02 -07:00
Wez Furlong 9770d333b5 egress_source: allow empty egress pools
We said that this worked, and while it does in the code,
we wouldn't allow you to deserialize an empty list of sources.
2025-01-16 11:46:05 -07:00
Wez Furlong 12895496be tsa: fix automation rules with multiple SetConfig actions
Only the first would take effect because the rule_hash we
computed included all of the actions, so each individual
action would appear to be a duplicate of the first.

This commit ensures that we vary the hash per-action
to avoid this, and augments the integration test
to explicitly verify the result.
2025-01-15 14:41:50 -07:00
Wez Furlong 9cbef3b202 fix deserialize error: unknown field relay_from_authz issue
The changes in e4b743cb8f to improve
memoize performance could cause the error message above because
the __newindex method wasn't actually writing the new value to
the unshared table, it was returning the existing value!

Most of this commit is really plumbing to support calling through the
cache layers in the same way that the listener would use, but in the
context of the unit tests in the lua file.

A nice side effect of this plumbing is that it is now possible
to iterate (via pairs) and index fields of the main config
objects that are returned by the most common `kumo.make_xxx`
functions.
2025-01-13 16:51:38 -07:00