]> git.ipfire.org Git - thirdparty/haproxy.git/log
thirdparty/haproxy.git
2 weeks agoIMPORT: ebtree: only use __builtin_prefetch() when supported
Willy Tarreau [Sat, 5 Jul 2025 19:57:33 +0000 (21:57 +0200)] 
IMPORT: ebtree: only use __builtin_prefetch() when supported

It looks like __builtin_prefetch() appeared in gcc-3.1 as there's no
mention of it in 3.0's doc. Let's replace it with eb_prefetch() which
maps to __builtin_prefetch() on supported compilers and falls back to
the usual do{}while(0) on other ones. It was tested to properly build
with tcc as well as gcc-2.95.

This is ebtree commit 7ee6ede56a57a046cb552ed31302b93ff1a21b1a.

2 weeks agoIMPORT: eb32/64: optimize insert for modern CPUs
Willy Tarreau [Thu, 12 Jun 2025 22:13:06 +0000 (00:13 +0200)] 
IMPORT: eb32/64: optimize insert for modern CPUs

Similar to previous patches, let's improve the insert() descent loop to
avoid discovering mandatory data too late. The change here is even
simpler than previous ones, a prefetch was installed and troot is
calculated before last instruction in a speculative way. This was enough
to gain +50% insertion rate on random data.

This is ebtree commit e893f8cc4d44b10f406b9d1d78bd4a9bd9183ccf.

2 weeks agoIMPORT: ebmb: optimize the lookup for modern CPUs
Willy Tarreau [Sun, 8 Jun 2025 09:50:59 +0000 (11:50 +0200)] 
IMPORT: ebmb: optimize the lookup for modern CPUs

This is the same principles as for the latest improvements made on
integer trees. Applying the same recipes made the ebmb_lookup()
function jump from 10.07 to 12.25 million lookups per second on a
10k random values tree (+21.6%).

It's likely that the ebmb_lookup_longest() code could also benefit
from this, though this was neither explored nor tested.

This is ebtree commit a159731fd6b91648a2fef3b953feeb830438c924.

2 weeks agoIMPORT: eb32/eb64: place an unlikely() on the leaf test
Willy Tarreau [Sun, 8 Jun 2025 17:51:49 +0000 (19:51 +0200)] 
IMPORT: eb32/eb64: place an unlikely() on the leaf test

In the loop we can help the compiler build slightly more efficient code
by placing an unlikely() around the leaf test. This shows a consistent
0.5% performance gain both on eb32 and eb64.

This is ebtree commit 6c9cdbda496837bac1e0738c14e42faa0d1b92c4.

2 weeks agoIMPORT: eb32: drop the now useless node_bit variable
Willy Tarreau [Sun, 8 Jun 2025 17:47:02 +0000 (19:47 +0200)] 
IMPORT: eb32: drop the now useless node_bit variable

This one was previously used to preload from the node and keep a copy
in a register on i386 machines with few registers. With the new more
optimal code it's totally useless, so let's get rid of it. By the way
the 64 bit code didn't use that at all already.

This is ebtree commit 1e219a74cfa09e785baf3637b6d55993d88b47ef.

2 weeks agoIMPORT: eb32/eb64: use a more parallelizable check for lack of common bits
Willy Tarreau [Sat, 7 Jun 2025 11:12:40 +0000 (13:12 +0200)] 
IMPORT: eb32/eb64: use a more parallelizable check for lack of common bits

Instead of shifting the XOR value right and comparing it to 1, which
roughly requires 2 sequential instructions, better test if the XOR has
any bit above the current bit, which means any bit set among those
strictly higher, or in other words that XOR & (-bit << 1) is non-zero.
This is one less instruction in the fast path and gives another nice
performance gain on random keys (in million lookups/s):

    eb32   1k:  33.17 -> 37.30   +12.5%
          10k:  15.74 -> 17.08   +8.51%
         100k:   8.00 ->  9.00   +12.5%
    eb64   1k:  34.40 -> 38.10   +10.8%
          10k:  16.17 -> 17.10   +5.75%
         100k:   8.38 ->  8.87   +5.85%

This is ebtree commit c942a2771758eed4f4584fe23cf2914573817a6b.

2 weeks agoIMPORT: eb32/eb64: reorder the lookup loop for modern CPUs
Willy Tarreau [Sat, 7 Jun 2025 12:36:16 +0000 (14:36 +0200)] 
IMPORT: eb32/eb64: reorder the lookup loop for modern CPUs

The current code calculates the next troot based on a calculation.
This was efficient when the algorithm was developed many years ago
on K6 and K7 CPUs running at low frequencies with few registers and
limited branch prediction units but nowadays with ultra-deep pipelines
and high latency memory that's no longer efficient, because the CPU
needs to have completed multiple operations before knowing which
address to start fetching from. It's sad because we only have two
branches each time but the CPU cannot know it. In addition, the
calculation is performed late in the loop, which does not help the
address generation unit to start prefetching next data.

Instead we should help the CPU by preloading data early from the node
and calculing troot as soon as possible. The CPU will be able to
postpone that processing until the dependencies are available and it
really needs to dereference it. In addition we must absolutely avoid
serializing instructions such as "(a >> b) & 1" because there's no
way for the compiler to parallelize that code nor for the CPU to pre-
process some early data.

What this patch does is relatively simple:

  - we try to prefetch the next two branches as soon as the
    node is known, which will help dereference the selected node in
    the next iteration; it was shown that it only works with the next
    changes though, otherwise it can reduce the performance instead.
    In practice the prefetching will start a bit later once the node
    is really in the cache, but since there's no dependency between
    these instructions and any other one, we let the CPU optimize as
    it wants.

  - we preload all important data from the node (next two branches,
    key and node.bit) very early even if not immediately needed.
    This is cheap, it doesn't cause any pipeline stall and speeds
    up later operations.

  - we pre-calculate 1<<bit that we assign into a register, so as
    to avoid serializing instructions when deciding which branch to
    take.

  - we assign the troot based on a ternary operation (or if/else) so
    that the CPU knows upfront the two possible next addresses without
    waiting for the end of a calculation and can prefetch their contents
    every time the branch prediction unit guesses right.

Just doing this provides significant gains at various tree sizes on
random keys (in million lookups per second):

  eb32   1k:  29.07 -> 33.17  +14.1%
        10k:  14.27 -> 15.74  +10.3%
       100k:   6.64 ->  8.00  +20.5%
  eb64   1k:  27.51 -> 34.40  +25.0%
        10k:  13.54 -> 16.17  +19.4%
       100k:   7.53 ->  8.38  +11.3%

The performance is now much closer to the sequential keys. This was
done for all variants ({32,64}{,i,le,ge}).

Another point, the equality test in the loop improves the performance
when looking up random keys (since we don't need to reach the leaf),
but is counter-productive for sequential keys, which can gain ~17%
without that test. However sequential keys are normally not used with
exact lookups, but rather with lookup_ge() that spans a time frame,
and which does not have that test for this precise reason, so in the
end both use cases are served optimally.

It's interesting to note that everything here is solely based on data
dependencies, and that trying to perform *less* operations upfront
always ends up with lower performance (typically the original one).

This is ebtree commit 05a0613e97f51b6665ad5ae2801199ad55991534.

2 weeks agoIMPORT: ebtree: delete unusable ebpttree.c
Willy Tarreau [Wed, 11 Jun 2025 20:00:18 +0000 (22:00 +0200)] 
IMPORT: ebtree: delete unusable ebpttree.c

Since commit 21fd162 ("[MEDIUM] make ebpttree rely solely on eb32/eb64
trees") it was no longer used and no longer builds. The commit message
mentions that the file is no longer needed, probably that a rebase failed
and left the file there.

This is ebtree commit fcfaf8df90e322992f6ba3212c8ad439d3640cb7.

2 weeks agoDOC: internals: document the shm-stats-file format/mapping
Aurelien DARRAGON [Tue, 16 Sep 2025 16:44:02 +0000 (18:44 +0200)] 
DOC: internals: document the shm-stats-file format/mapping

Add some documentation about shm stats file structure to help writing
tools that can parse the file to use the shared stats counters.

This file was written for shm stats file version 1.0 specifically,
it may need to be updated when the shm stats file structure changes
in the future.

2 weeks agoMINOR: counters: document that tg shared counters are tied to shm-stats-file mapping
Aurelien DARRAGON [Tue, 16 Sep 2025 16:30:51 +0000 (18:30 +0200)] 
MINOR: counters: document that tg shared counters are tied to shm-stats-file mapping

Let's explicitly mention that fe_counters_shared_tg and
be_counters_shared_tg structs are embedded in shm_stats_file_object
struct so any change in those structs will result in shm stats file
incompatibility between processes, thus extra precaution must be
taken when making changes to them.

Note that the provisionning made in shm_stats_file_object struct could
be used to add members to {fe,be}_counters_shared_tg without changing
shm_stats_file_object struct size if needed in order to preserve
shm stats file version.

2 weeks agoCLEANUP: log: remove deadcode in px_parse_log_steps()
Aurelien DARRAGON [Tue, 16 Sep 2025 06:17:03 +0000 (08:17 +0200)] 
CLEANUP: log: remove deadcode in px_parse_log_steps()

When logsteps proxy storage was migrated from eb nodes to bitmasks in
6a92b14 ("MEDIUM: log/proxy: store log-steps selection using a bitmask,
not an eb tree"), some unused eb node related code was left over in
px_parse_log_steps()

Not only this code is unused, it also resulted in wasted memory since
an eb node was allocated for nothing.

This should fix GH #3121

2 weeks agoBUG/MEDIUM: pattern: fix possible infinite loops on deletion (try 2)
Willy Tarreau [Tue, 16 Sep 2025 09:49:01 +0000 (11:49 +0200)] 
BUG/MEDIUM: pattern: fix possible infinite loops on deletion (try 2)

Commit e36b3b60b3 ("MEDIUM: migrate the patterns reference to cebs_tree")
changed the construction of the loops used to look up matching nodes, and
since we don't need two elements anymore, the "continue" statement now
loops on the same element when deleting. Let's fix this to make sure it
passes through the next one.

While this bug is 3.3 only, it turns out that 3.2 is also affected by
the incorrect loop construct in pat_ref_set_from_node(), where it's
possible to run an infinite loop since commit 010c34b8c7 ("MEDIUM:
pattern: consider gen_id in pat_ref_set_from_node()") due to the
"continue" statement being placed before the ebmb_next_dup() call.

As such the relevant part of this fix (pat_ref_set_from_elt) will
need to be backported to 3.2.

2 weeks agoRevert "BUG/MEDIUM: pattern: fix possible infinite loops on deletion"
Willy Tarreau [Tue, 16 Sep 2025 14:27:21 +0000 (16:27 +0200)] 
Revert "BUG/MEDIUM: pattern: fix possible infinite loops on deletion"

This reverts commit 359a829ccb8693e0b29808acc0fa7975735c0353.
The fix is neither sufficient nor correct (it triggers ASAN). Better
redo it cleanly rather than accumulate invalid fixes.

2 weeks agoCI: scripts: mkdir BUILDSSL_TMPDIR
William Lallemand [Tue, 16 Sep 2025 13:32:08 +0000 (15:32 +0200)] 
CI: scripts: mkdir BUILDSSL_TMPDIR

Creates the BUILDSSL_TMPDIR at the beginning of the script instead of
having to create it in each download functions

2 weeks agoCI: github: add an OpenSSL + ECH job
William Lallemand [Tue, 16 Sep 2025 10:01:23 +0000 (12:01 +0200)] 
CI: github: add an OpenSSL + ECH job

The upcoming ECH feature need a patched OpenSSL with the "feature/ech"
branch.

This daily job launches an openssl build, as well as haproxy build with
reg-tests.

2 weeks agoCI: scripts: add support for git in openssl builds
William Lallemand [Tue, 16 Sep 2025 09:50:34 +0000 (11:50 +0200)] 
CI: scripts: add support for git in openssl builds

Add support for git releases downloaded from github in openssl builds:

- GIT_TYPE variable allow you to chose between "branch" or "commit"
- OPENSSL_VERSION variable supports a "git-" prefix
- "git-${commit_id}" is stored in .openssl_version instead of the branch
  name for version comparison.

2 weeks agoBUG/MEDIUM: pattern: fix possible infinite loops on deletion
Willy Tarreau [Tue, 16 Sep 2025 09:49:01 +0000 (11:49 +0200)] 
BUG/MEDIUM: pattern: fix possible infinite loops on deletion

Commit e36b3b60b3 ("MEDIUM: migrate the patterns reference to cebs_tree")
changed the construction of the loops used to look up matching nodes, and
since we don't need two elements anymore, the "continue" statement now
loops on the same element when deleting. Let's fix this to make sure it
passes through the next one.

No backport is needed, this is only 3.3.

2 weeks agoCLEANUP: vars: use the item API for the variables trees
Willy Tarreau [Tue, 16 Sep 2025 08:47:52 +0000 (10:47 +0200)] 
CLEANUP: vars: use the item API for the variables trees

The variables trees use the immediate cebtree API, better use the
item one which is more expressive and safer. The "node" field was
renamed to "name_node" to avoid any ambiguity.

2 weeks agoCLEANUP: tools: use the item API for the file names tree
Willy Tarreau [Tue, 16 Sep 2025 08:41:19 +0000 (10:41 +0200)] 
CLEANUP: tools: use the item API for the file names tree

The file names tree uses the immediate cebtree API, better use the
item one which is more expressive and safer.

2 weeks agoMEDIUM: connection: reintegrate conn_hash_node into connection
Willy Tarreau [Fri, 12 Sep 2025 16:12:18 +0000 (18:12 +0200)] 
MEDIUM: connection: reintegrate conn_hash_node into connection

Previously the conn_hash_node was placed outside the connection due
to the big size of the eb64_node that could have negatively impacted
frontend connections. But having it outside also means that one
extra allocation is needed for each backend connection, and that one
memory indirection is needed for each lookup.

With the compact trees, the tree node is smaller (16 bytes vs 40) so
the overhead is much lower. By integrating it into the connection,
We're also eliminating one pointer from the connection to the hash
node and one pointer from the hash node to the connection (in addition
to the extra object bookkeeping). This results in saving at least 24
bytes per total backend connection, and only inflates connections by
16 bytes (from 240 to 256), which is a reasonable compromise.

Tests on a 64-core EPYC show a 2.4% increase in the request rate
(from 2.08 to 2.13 Mrps).

2 weeks agoMEDIUM: connection: move idle connection trees to ceb64
Willy Tarreau [Fri, 12 Sep 2025 13:28:33 +0000 (15:28 +0200)] 
MEDIUM: connection: move idle connection trees to ceb64

Idle connection trees currently require a 56-byte conn_hash_node per
connection, which can be reduced to 32 bytes by moving to ceb64. While
ceb64 is theoretically slower, in practice here we're essentially
dealing with trees that almost always contain a single key and many
duplicates. In this case, ceb64 insert and lookup functions become
faster than eb64 ones because all duplicates are a list accessed in
O(1) while it's a subtree for eb64. In tests it is impossible to tell
the difference between the two, so it's worth reducing the memory
usage.

This commit brings the following memory savings to conn_hash_node
(one per backend connection), and to srv_per_thread (one per thread
and per server):

     struct       before  after  delta
  conn_hash_nodea   56     32     -24
  srv_per_thread    96     72     -24

The delicate part is conn_delete_from_tree(), because we need to
know the tree root the connection is attached to. But thanks to
recent cleanups, it's now clear enough (i.e. idle/safe/avail vs
session are easy to distinguish).

2 weeks agoMINOR: connection: pass the thread number to conn_delete_from_tree()
Willy Tarreau [Fri, 12 Sep 2025 14:47:12 +0000 (16:47 +0200)] 
MINOR: connection: pass the thread number to conn_delete_from_tree()

We'll soon need to choose the server's root based on the connection's
flags, and for this we'll need the thread it's attached to, which is
not always the current one. This patch simply passes the thread number
from all callers. They know it because they just set the idle_conns
lock on it prior to calling the function.

2 weeks agoCLEANUP: backend: use a single variable for removed in srv_cleanup_idle_conns()
Willy Tarreau [Fri, 12 Sep 2025 14:28:58 +0000 (16:28 +0200)] 
CLEANUP: backend: use a single variable for removed in srv_cleanup_idle_conns()

Probably due to older code, there's a boolean variable used to set
another one which is then checked. Also the first check is made under
the lock, which is unnecessary. Let's simplify this and use a single
variable. This only makes the code clearer, it doesn't change the output
code.

2 weeks agoMINOR: server: pass the server and thread to srv_migrate_conns_to_remove()
Willy Tarreau [Fri, 12 Sep 2025 14:24:16 +0000 (16:24 +0200)] 
MINOR: server: pass the server and thread to srv_migrate_conns_to_remove()

We'll need to have access to the srv_per_thread element soon from this
function, and there's no particular reason for passing it list pointers
so let's pass the server and the thread so that it is autonomous. It
also makes the calling code simpler.

2 weeks agoCLEANUP: server: use eb64_entry() not ebmb_entry() to convert an eb64
Willy Tarreau [Fri, 12 Sep 2025 13:49:48 +0000 (15:49 +0200)] 
CLEANUP: server: use eb64_entry() not ebmb_entry() to convert an eb64

There were a few leftovers from an earlier version of the conn_hash_node
that was using ebmb nodes. A few calls to ebmb_first() and ebmb_entry()
were still present while acting on an eb64 tree. These are harmless as
one is just eb_first() and the other container_of(), but it's confusing
so let's clean them up.

2 weeks agoCLEANUP: backend: factor the connection lookup loop
Willy Tarreau [Fri, 12 Sep 2025 13:30:30 +0000 (15:30 +0200)] 
CLEANUP: backend: factor the connection lookup loop

The connection lookup loop is made of two identical blocks, one looking
in the idle or safe lists and the other one looking into the safe list
only. The second one is skipped if a connection was found or if the request
looks for a safe one (since already done). Also the two are slightly
different due to leftovers from earlier versions in that the second one
checks for safe connections and not the first one, and the second one
sets is_safe which is not used later.

Let's just rationalize all this by placing them in a loop which checks
first from the idle conns and second from the safe ones, or skips the
first step if the request wants a safe connection. This reduces the
code and shortens the time spent under the lock.

2 weeks agoCLEANUP: proxy: slightly reorganize fields to plug some holes
Willy Tarreau [Sun, 24 Aug 2025 10:38:18 +0000 (12:38 +0200)] 
CLEANUP: proxy: slightly reorganize fields to plug some holes

The proxy struct has several small holes that deserved being plugged by
moving a few fields around. Now we're down to 3056 from 3072 previously,
and the remaining holes are small.

At the moment, compared to before this series, we're seeing these
sizes:

    type\size   7d554ca62   current  delta
    listener       752        704     -48  (-6.4%)
    server        4032       3840    -192  (-4.8%)
    proxy         3184       3056    -128  (-4%)
    stktable      3392       3328     -64  (-1.9%)

Configs with many servers have shrunk by about 4% in RAM and configs
with many proxies by about 3%.

2 weeks agoCLEANUP: server: slightly reorder fields in the struct to plug holes
Willy Tarreau [Sun, 24 Aug 2025 10:25:51 +0000 (12:25 +0200)] 
CLEANUP: server: slightly reorder fields in the struct to plug holes

The struct server still has a lot of holes and padding that make it
quite big. By moving a few fields aronud between areas which do not
interact (e.g. boot vs aligned areas), it's quite easy to plug some
of them and/or to arrange larger ones which could be reused later with
a bit more effort. Here we've reduced holes by 40 bytes, allowing the
struct to shrink by one more cache line (64 bytes). The new size is
3840 bytes.

2 weeks agoMEDIUM: server: index server ID using compact trees
Willy Tarreau [Sun, 24 Aug 2025 09:21:02 +0000 (11:21 +0200)] 
MEDIUM: server: index server ID using compact trees

The server ID is currently stored as a 32-bit int using an eb32 tree.
It's used essentially to find holes in order to automatically assign IDs,
and to detect duplicates. Let's change this to use compact trees instead
in order to save 24 bytes in struct server for this node, plus 8 bytes in
struct proxy. The server struct is still 3904 bytes large (due to
alignment) and the proxy struct is 3072.

2 weeks agoMEDIUM: listener: index listener ID using compact trees
Willy Tarreau [Sun, 24 Aug 2025 09:12:49 +0000 (11:12 +0200)] 
MEDIUM: listener: index listener ID using compact trees

The listener ID is currently stored as a 32-bit int using an eb32 tree.
It's used essentially to find holes in order to automatically assign IDs,
and to detect duplicates. Let's change this to use compact trees instead
in order to save 24 bytes in struct listener for this node, plus 8 bytes
in struct proxy. The struct listener is now 704 bytes large, and the
struct proxy 3080.

2 weeks agoMEDIUM: proxy: index proxy ID using compact trees
Willy Tarreau [Sat, 23 Aug 2025 17:57:29 +0000 (19:57 +0200)] 
MEDIUM: proxy: index proxy ID using compact trees

The proxy ID is currently stored as a 32-bit int using an eb32 tree.
It's used essentially to find holes in order to automatically assign IDs,
and to detect duplicates. Let's change this to use compact trees instead
in order to save 24 bytes in struct proxy for this node, plus 8 bytes in
the root (which is static so not much relevant here). Now the proxy is
3088 bytes large.

2 weeks agoMINOR: proxy: add proxy_index_id() to index a proxy by its ID
Willy Tarreau [Sat, 23 Aug 2025 17:45:03 +0000 (19:45 +0200)] 
MINOR: proxy: add proxy_index_id() to index a proxy by its ID

This avoids needlessly exposing the tree's root and the mechanics outside
of the low-level code.

2 weeks agoMINOR: listener: add listener_index_id() to index a listener by its ID
Willy Tarreau [Sat, 23 Aug 2025 17:37:26 +0000 (19:37 +0200)] 
MINOR: listener: add listener_index_id() to index a listener by its ID

This avoids needlessly exposing the tree's root and the mechanics outside
of the low-level code.

2 weeks agoMINOR: server: add server_index_id() to index a server by its ID
Willy Tarreau [Sat, 23 Aug 2025 17:33:52 +0000 (19:33 +0200)] 
MINOR: server: add server_index_id() to index a server by its ID

This avoids needlessly exposing the tree's root and the mechanics outside
of the low-level code.

2 weeks agoCLEANUP: server: use server_find_by_id() when looking for already used IDs
Willy Tarreau [Sat, 23 Aug 2025 17:26:54 +0000 (19:26 +0200)] 
CLEANUP: server: use server_find_by_id() when looking for already used IDs

In srv_parse_id(), there's no point doing all the low-level work with
the tree functions to check for the existence of an ID, we already have
server_find_by_id() which does exactly this, so let's use it.

2 weeks agoMINOR: server: add server_get_next_id() to find next free server ID
Willy Tarreau [Sat, 23 Aug 2025 17:26:08 +0000 (19:26 +0200)] 
MINOR: server: add server_get_next_id() to find next free server ID

This was previously achieved via the generic get_next_id() but we'll soon
get rid of generic ID trees so let's have a dedicated server_get_next_id().
As a bonus it reduces the exposure of the tree's root outside of the functions.

2 weeks agoMINOR: listener: add listener_get_next_id() to find next free listener ID
Willy Tarreau [Sat, 23 Aug 2025 17:25:03 +0000 (19:25 +0200)] 
MINOR: listener: add listener_get_next_id() to find next free listener ID

This was previously achieved via the generic get_next_id() but we'll soon
get rid of generic ID trees so let's have a dedicated listener_get_next_id().
As a bonus it reduces the exposure of the tree's root outside of the functions.

2 weeks agoMINOR: proxy: add proxy_get_next_id() to find next free proxy ID
Willy Tarreau [Sat, 23 Aug 2025 17:24:21 +0000 (19:24 +0200)] 
MINOR: proxy: add proxy_get_next_id() to find next free proxy ID

This was previously achieved via the generic get_next_id() but we'll soon
get rid of generic ID trees so let's have a dedicated proxy_get_next_id().

2 weeks agoMEDIUM: stktable: index table names using compact trees
Willy Tarreau [Tue, 15 Jul 2025 11:50:03 +0000 (13:50 +0200)] 
MEDIUM: stktable: index table names using compact trees

Here we're saving 64 bytes per stick-table, from 3392 to 3328, and the
change was really straightforward so there's no reason not to do it.

2 weeks agoMEDIUM: proxy: switch conf.name to cebis_tree
Willy Tarreau [Tue, 15 Jul 2025 09:47:54 +0000 (11:47 +0200)] 
MEDIUM: proxy: switch conf.name to cebis_tree

This is used to index the proxy's name and it contains a copy of the
pointer to the proxy's name in <id>. Changing that for a ceb_node placed
just before <id> saves 32 bytes to the struct proxy, which is now 3112
bytes large.

Here we need to continue to support duplicates since they're still
allowed between type-incompatible proxies.

Interestingly, the use of cebis_next_dup() instead of cebis_next() in
proxy_find_by_name() allows us to get rid of an strcmp() that was
performed for each use_backend rule. A test with a large config
(100k backends) shows that we can get 3% extra performance on a
config involving a static use_backend rule (3.09M to 3.18M rps),
and even 4.5% on a dynamic rule selecting a random backend (2.47M
to 2.59M).

2 weeks agoMEDIUM: server: switch the host_dn member to cebis_tree
Willy Tarreau [Mon, 7 Jul 2025 15:11:33 +0000 (17:11 +0200)] 
MEDIUM: server: switch the host_dn member to cebis_tree

This member is used to index the hostname_dn contents for DNS resolution.
Let's replace it with a cebis_tree to save another 32 bytes (24 for the
node + 8 by avoiding the duplication of the pointer). The struct server is
now at 3904 bytes.

2 weeks agoMEDIUM: server: switch conf.name to cebis_tree
Willy Tarreau [Mon, 7 Jul 2025 13:33:40 +0000 (15:33 +0200)] 
MEDIUM: server: switch conf.name to cebis_tree

This is used to index the server name and it contains a copy of the
pointer to the server's name in <id>. Changing that for a ceb_node placed
just before <id> saves 32 bytes to the struct server, which remains 3968
bytes large due to alignment. The proxy struct shrinks by 8 bytes to 3144.

It's worth noting that the current way duplicate names are handled remains
based on the previous mechanism where dups were permitted. Ideally we
should now reject them during insertion and use unique key trees instead.

2 weeks agoMEDIUM: server: switch addr_node to cebis_tree
Willy Tarreau [Mon, 7 Jul 2025 13:13:15 +0000 (15:13 +0200)] 
MEDIUM: server: switch addr_node to cebis_tree

This contains the text representation of the server's address, for use
with stick-tables with "srvkey addr". Switching them to a compact node
saves 24 more bytes from this structure. The key was moved to an external
pointer "addr_key" right after the node.

The server struct is now 3968 bytes (down from 4032) due to alignment, and
the proxy struct shrinks by 8 bytes to 3152.

2 weeks agoMEDIUM: guid: switch guid to more compact cebuis_tree
Willy Tarreau [Mon, 17 Feb 2025 08:39:04 +0000 (09:39 +0100)] 
MEDIUM: guid: switch guid to more compact cebuis_tree

The current guid struct size is 56 bytes. Once reduced using compact
trees, it goes down to 32 (almost half). We're not on a critical path
and size matters here, so better switch to this.

It's worth noting that the name part could also be stored in the
guid_node at the end to save 8 extra byte (no pointer needed anymore),
however the purpose of this struct is to be embedded into other ones,
which is not compatible with having a dynamic size.

Affected struct sizes in bytes:

           Before     After   Diff
  server    4032       4032     0*
  proxy     3184       3160    -24
  listener   752        728    -24

*: struct server is full of holes and padding (176 bytes) and is
64-byte aligned. Moving the guid_node elsewhere such as after sess_conn
reduces it to 3968, or one less cache line. There's no point in moving
anything now because forthcoming patches will arrange other parts.

2 weeks agoMEDIUM: migrate the patterns reference to cebs_tree
Willy Tarreau [Sun, 12 Jan 2025 18:38:28 +0000 (19:38 +0100)] 
MEDIUM: migrate the patterns reference to cebs_tree

cebs_tree are 24 bytes smaller than ebst_tree (16B vs 40B), and pattern
references are only used during map/acl updates, so their storage is
pure loss between updates (which most of the time never happen). By
switching their indexing to compact trees, we can save 16 to 24 bytes
per entry depending on alightment (here it's 24 per struct but 16
practical as malloc's alignment keeps 8 unused).

Tested on core i7-8650U running at 3.0 GHz, with a file containing
17.7M IP addresses (16.7M different):

   $ time  ./haproxy -c -f acl-ip.cfg

Save 280 MB RAM for 17.7M IP addresses, and slightly speeds up the
startup (5.8%, from 19.2s to 18.2s), a part of which possible being
attributed to having to write less memory. Note that this is on small
strings. On larger ones such as user-agents, ebtree doesn't reread
the whole key and might be more efficient.

Before:
  RAM (VSZ/RSS): 4443912 3912444

  real    0m19.211s
  user    0m18.138s
  sys     0m1.068s

  Overhead  Command         Shared Object      Symbol
    44.79%  haproxy  haproxy            [.] ebst_insert
    25.07%  haproxy  haproxy            [.] ebmb_insert_prefix
     3.44%  haproxy  libc-2.33.so       [.] __libc_calloc
     2.71%  haproxy  libc-2.33.so       [.] _int_malloc
     2.33%  haproxy  haproxy            [.] free_pattern_tree
     1.78%  haproxy  libc-2.33.so       [.] inet_pton4
     1.62%  haproxy  libc-2.33.so       [.] _IO_fgets
     1.58%  haproxy  libc-2.33.so       [.] _int_free
     1.56%  haproxy  haproxy            [.] pat_ref_push
     1.35%  haproxy  libc-2.33.so       [.] malloc_consolidate
     1.16%  haproxy  libc-2.33.so       [.] __strlen_avx2
     0.79%  haproxy  haproxy            [.] pat_idx_tree_ip
     0.76%  haproxy  haproxy            [.] pat_ref_read_from_file
     0.60%  haproxy  libc-2.33.so       [.] __strrchr_avx2
     0.55%  haproxy  libc-2.33.so       [.] unlink_chunk.constprop.0
     0.54%  haproxy  libc-2.33.so       [.] __memchr_avx2
     0.46%  haproxy  haproxy            [.] pat_ref_append

After:
  RAM (VSZ/RSS): 4166108 3634768

  real    0m18.114s
  user    0m17.113s
  sys     0m0.996s

  Overhead  Command  Shared Object       Symbol
    38.99%  haproxy  haproxy             [.] cebs_insert
    27.09%  haproxy  haproxy             [.] ebmb_insert_prefix
     3.63%  haproxy  libc-2.33.so        [.] __libc_calloc
     3.18%  haproxy  libc-2.33.so        [.] _int_malloc
     2.69%  haproxy  haproxy             [.] free_pattern_tree
     1.99%  haproxy  libc-2.33.so        [.] inet_pton4
     1.74%  haproxy  libc-2.33.so        [.] _IO_fgets
     1.73%  haproxy  libc-2.33.so        [.] _int_free
     1.57%  haproxy  haproxy             [.] pat_ref_push
     1.48%  haproxy  libc-2.33.so        [.] malloc_consolidate
     1.22%  haproxy  libc-2.33.so        [.] __strlen_avx2
     1.05%  haproxy  libc-2.33.so        [.] __strcmp_avx2
     0.80%  haproxy  haproxy             [.] pat_idx_tree_ip
     0.74%  haproxy  libc-2.33.so        [.] __memchr_avx2
     0.69%  haproxy  libc-2.33.so        [.] __strrchr_avx2
     0.69%  haproxy  libc-2.33.so        [.] _IO_getline_info
     0.62%  haproxy  haproxy             [.] pat_ref_read_from_file
     0.56%  haproxy  libc-2.33.so        [.] unlink_chunk.constprop.0
     0.56%  haproxy  libc-2.33.so        [.] cfree@GLIBC_2.2.5
     0.46%  haproxy  haproxy             [.] pat_ref_append

If the addresses are totally disordered (via "shuf" on the input file),
we see both implementations reach exactly 68.0s (slower due to much
higher cache miss ratio).

On large strings such as user agents (1 million here), it's now slightly
slower (+9%):

Before:
  real    0m2.475s
  user    0m2.316s
  sys     0m0.155s

After:
  real    0m2.696s
  user    0m2.544s
  sys     0m0.147s

But such patterns are much less common than short ones, and the memory
savings do still count.

Note that while it could be tempting to get rid of the list that chains
all these pat_ref_elt together and only enumerate them by walking along
the tree to save 16 extra bytes per entry, that's not possible due to
the problem that insertion ordering is critical (think overlapping regex
such as /index.* and /index.html). Currently it's not possible to proceed
differently because patterns are first pre-loaded into the pat_ref via
pat_ref_read_from_file_smp() and later indexed by pattern_read_from_file(),
which has to only redo the second part anyway for maps/acls declared
multiple times.

2 weeks agoIMPORT: cebtree: import version 0.5.0 to support duplicates
Willy Tarreau [Mon, 7 Jul 2025 08:58:21 +0000 (10:58 +0200)] 
IMPORT: cebtree: import version 0.5.0 to support duplicates

The support for duplicates is necessary for various use cases related
to config names, so let's upgrade to the latest version which brings
this support. This updates the cebtree code to commit 808ed67 (tag
0.5.0). A few tiny adaptations were needed:
  - replace a few ceb_node** with ceb_root** since pointers are now
    tagged ;
  - replace cebu*.h with ceb*.h since both are now merged in the same
    include file. This way we can drop the unused cebu*.h files from
    cebtree that are provided only for compatibility.
  - rename immediate storage functions to cebXX_imm_XXX() as per the API
    change in 0.5 that makes immediate explicit rather than implicit.
    This only affects vars and tools.c:copy_file_name().

The tests continue to work.

2 weeks agoBUILD: makefile: implement support for running a command in range
Willy Tarreau [Tue, 16 Sep 2025 07:21:34 +0000 (09:21 +0200)] 
BUILD: makefile: implement support for running a command in range

When running "make range", it would be convenient to support running
reg tests or anything else such as "size", "pahole" or even benchmarks.
Such commands are usually specific to the developer's environment, so
let's just pass a generic variable TEST_CMD that is executed as-is if
not empty.

This way it becomes possible to run "make range RANGE=... TEST_CMD=...".

2 weeks agoBUG/MINOR: resolvers: always normalize FQDN from response
Valentine Krasnobaeva [Wed, 10 Sep 2025 16:30:19 +0000 (18:30 +0200)] 
BUG/MINOR: resolvers: always normalize FQDN from response

RFC1034 states the following:

By convention, domain names can be stored with arbitrary case, but
domain name comparisons for all present domain functions are done in a
case-insensitive manner, assuming an ASCII character set, and a high
order zero bit. This means that you are free to create a node with
label "A" or a node with label "a", but not both as brothers; you could
refer to either using "a" or "A".

In practice, most DNS resolvers normalize domain labels (i.e., convert
them to lowercase) before performing searches or comparisons to ensure
this requirement is met.

While HAProxy normalizes the domain name in the request, it currently
does not do so for the response. Commit 75cc653 ("MEDIUM: resolvers:
replace bogus resolv_hostname_cmp() with memcmp()") intentionally
removed the `tolower()` conversion from `resolv_hostname_cmp()` for
safety and performance reasons.

This commit re-introduces the necessary normalization for FQDNs received
in the response. The change is made in `resolv_read_name()`, where labels
are processed as an unsigned char string, allowing `tolower()` to be
applied safely. Since a typical FQDN has only 3-4 labels, replacing
`memcpy()` with an explicit copy that also applies `tolower()` should
not introduce a significant performance degradation.

This patch addresses the rare edge case, as most resolvers perform this
normalization themselves.

This fixes the GitHub issue #3102. This fix may be backported in all stable
versions since 2.5 included 2.5.

2 weeks agoBUG/MINOR: ocsp: Crash when updating CA during ocsp updates
Remi Tricot-Le Breton [Mon, 15 Sep 2025 13:32:40 +0000 (15:32 +0200)] 
BUG/MINOR: ocsp: Crash when updating CA during ocsp updates

If an ocsp response is set to be updated automatically and some
certificate or CA updates are performed on the CLI, if the CLI update
happens while the OCSP response is being updated and is then detached
from the udapte tree, it might be wrongly inserted into the update tree
in 'ssl_sock_load_ocsp', and then reinserted when the update finishes.

The update tree then gets corrupted and we could end up crashing when
accessing other nodes in the ocsp response update tree.

This patch must be backported up to 2.8.
This patch fixes GitHub #3100.

3 weeks agoMEDIUM: log/proxy: store log-steps selection using a bitmask, not an eb tree
Aurelien DARRAGON [Fri, 29 Aug 2025 09:10:56 +0000 (11:10 +0200)] 
MEDIUM: log/proxy: store log-steps selection using a bitmask, not an eb tree

An eb tree was used to anticipate for infinite amount of custom log steps
configured at a proxy level. In turns out this makes no sense to configure
that much logging steps for a proxy, and the cost of the eb tree is non
negligible in terms of memory footprint, especially when used in a default
section.

Instead, let's use a simple bitmask, which allows up to 64 logging steps
configured at proxy level. If we lack space some day (and need more than
64 logging steps to be configured), we could simply modify
"struct log_steps" to spread the bitmask over multiple 64bits integers,
minor some adjustments where the mask is set and checked.

3 weeks agoBUG/MEDIUM: http_ana: fix potential NULL deref in http_process_req_common()
Aurelien DARRAGON [Mon, 15 Sep 2025 07:22:19 +0000 (09:22 +0200)] 
BUG/MEDIUM: http_ana: fix potential NULL deref in http_process_req_common()

As reported by @kenballus in GH #3118, a potential NULL-deref was
introduced in 3da1d63 ("BUG/MEDIUM: http_ana: handle yield for "stats
http-request" evaluation")

Indeed, px->uri_auth may be NULL when stats directive is not involved in
the current proxy section.

The bug went unnoticed because it didn't seem to cause any side-effect
so far and valgrind didn't catch it. However ASAN did, so let's fix it
before it causes harm.

It should be backported with 3da1d63.

3 weeks agoRevert "BUG/MINOR: ocsp: Crash when updating CA during ocsp updates"
Christopher Faulet [Mon, 15 Sep 2025 08:16:20 +0000 (10:16 +0200)] 
Revert "BUG/MINOR: ocsp: Crash when updating CA during ocsp updates"

This reverts commit 167ea8fc7b0cf9d1bf71ec03d7eac3141fbe0080.

The patch was backported by mistake.

3 weeks agoBUG/MINOR: ocsp: Crash when updating CA during ocsp updates
Remi Tricot-Le Breton [Mon, 8 Sep 2025 13:44:23 +0000 (15:44 +0200)] 
BUG/MINOR: ocsp: Crash when updating CA during ocsp updates

If an ocsp response is set to be updated automatically and some
certificate or CA updates are performed on the CLI, if the CLI update
happens while the OCSP response is being updated and is then detached
from the udapte tree, it might be wrongly inserted into the update tree
in 'ssl_sock_load_ocsp', and then reinserted when the update finishes.

The update tree then gets corrupted and we could end up crashing when
accessing other nodes in the ocsp response update tree.

This patch must be backported up to 2.8.
This patch fixes GitHub #3100.

3 weeks agoBUG/MEDIUM: resolvers: Wake resolver task up whne unlinking a stream requester
Christopher Faulet [Mon, 15 Sep 2025 05:57:28 +0000 (07:57 +0200)] 
BUG/MEDIUM: resolvers: Wake resolver task up whne unlinking a stream requester

Another regression introduced with the commit 3023e9819 ("BUG/MINOR:
resolvers: Restore round-robin selection on records in DNS answers"). Stream
requesters are unlinked from any theards. So we must not try to queue the
resolver's task here because it is not allowed to do so from another thread
than the task thread. Instead, we can simply wake the resolver's task up. It
is only performed when the last stream requester is unlink from the
resolution.

This patch should fix the issue #3119. It must be backported with the commit
above.

3 weeks agoBUG/MEDIUM: resolvers: Accept to create resolution without hostname
Christopher Faulet [Fri, 12 Sep 2025 09:52:04 +0000 (11:52 +0200)] 
BUG/MEDIUM: resolvers: Accept to create resolution without hostname

A regression was introduced by commit 6cf2401ed ("BUG/MEDIUM: resolvers:
Make resolution owns its hostname_dn value"). In fact, it is possible (an
allowed ?!) to create a resolution without hostname (hostname_dn ==
NULL). It only happens on startup for a server relying on a resolver but
defined with an IP address and not a hostname

Because of the patch above, an error is triggered during the configuration
parsing when this happens, while it should be accepted.

This patch must be backported with the commit above.

3 weeks agoBUG/MEDIUM: resolvers: Make resolution owns its hostname_dn value
Christopher Faulet [Fri, 12 Sep 2025 07:50:56 +0000 (09:50 +0200)] 
BUG/MEDIUM: resolvers: Make resolution owns its hostname_dn value

The commit 37abe56b1 ("BUG/MEDIUM: resolvers: Properly cache do-resolv
resolution") introduced a regression. A resolution does not own its
hostname_dn value, it is a pointer on the first request value. But since the
commit above, it is possible to have orphan resolution, with no
requester. So it is important to modify the resolutions to make it owns its
hostname_dn value by duplicating it when it is created.

This patch must be backported with the commit above.

3 weeks agoBUG/MEDIUM: resolvers: Test for empty tree when getting a record from DNS answer
Christopher Faulet [Thu, 11 Sep 2025 19:21:44 +0000 (21:21 +0200)] 
BUG/MEDIUM: resolvers: Test for empty tree when getting a record from DNS answer

In the previous fix 5d1d93fad ("BUG/MEDIUM: resolvers: Properly handle empty
tree when getting a record from the DNS answer"), I missed the fact the
answer tree can be empty.

So, to avoid crashes, when the answer tree is empty, we immediately exit
from resolv_get_ip_from_response() function with RSLV_UPD_NO_IP_FOUND. In
addition, when a record is removed from the tree, we take care to reset the
next node saved if necessary.

This patch must be backported with the commit above.

3 weeks agoDOC: proxy-protocol: Add TLS group and sig scheme TLVs
Collison, Steven [Thu, 11 Sep 2025 19:28:30 +0000 (19:28 +0000)] 
DOC: proxy-protocol: Add TLS group and sig scheme TLVs

This change adds the PP2_SUBTYPE_SSL_GROUP and PP2_SUBTYPE_SSL_SIG_SCHEME
code point reservations in proxy_protocol.txt. The motivation for adding
these two TLVs is for backend visibility into the negotiated TLS key
exchange group and handshake signature scheme.

Demand for visibility is expected to increase as endpoints migrate to use
new Post-Quantum resistant algorithms for key exchange and signatures.

3 weeks agoMINOR: activity/memory: count allocations performed under a lock
Willy Tarreau [Thu, 11 Sep 2025 13:36:13 +0000 (15:36 +0200)] 
MINOR: activity/memory: count allocations performed under a lock

By checking the current thread's locking status, it becomes possible
to know during a memory allocation whether it's performed under a lock
or not. Both pools and memprofile functions were instrumented to check
for this and to increment the memprofile bin's locked_calls counter.

This one, when not zero, is reported on "show profiling memory" with a
percentage of all allocations that such locked allocations represent.
This way it becomes possible to try to target certain code paths that
are particularly expensive. Example:

  $ socat - /tmp/sock1 <<< "show profiling memory"|grep lock
     20297301           0     2598054528              0|   0x62a820fa3991 sockaddr_alloc+0x61/0xa3 p_alloc(128) [pool=sockaddr] [locked=54962 (0.2 %)]
            0    20297301              0     2598054528|   0x62a820fa3a24 sockaddr_free+0x44/0x59 p_free(-128) [pool=sockaddr] [locked=34300 (0.1 %)]
      9908432           0     1268279296              0|   0x62a820eb8524 main+0x81974 p_alloc(128) [pool=task] [locked=9908432 (100.0 %)]
      9908432           0      554872192              0|   0x62a820eb85a6 main+0x819f6 p_alloc(56) [pool=tasklet] [locked=9908432 (100.0 %)]
       263001           0       63120240              0|   0x62a820fa3c97 conn_new+0x37/0x1b2 p_alloc(240) [pool=connection] [locked=20662 (7.8 %)]
        71643           0       47307584              0|   0x62a82105204d pool_get_from_os_noinc+0x12d/0x161 posix_memalign(660) [locked=5393 (7.5 %)]

3 weeks agoMINOR: activity: collect CPU time spent on memory allocations for each task
Willy Tarreau [Thu, 11 Sep 2025 12:52:00 +0000 (14:52 +0200)] 
MINOR: activity: collect CPU time spent on memory allocations for each task

When task profiling is enabled, the pool alloc/free code will measure the
time it takes to perform memory allocation after a cache miss or memory
freeing to the shared cache or OS. The time taken with the thread-local
cache is never measured as measuring that time is very expensive compared
to the pool access time. Here doing so costs around 2% performance at 2M
req/s, only when task profiling is enabled, so this remains reasonable.
The scheduler takes care of collecting that time and updating the
sched_activity entry corresponding to the current task when task profiling
is enabled.

The goal clearly is to track places that are wasting CPU time allocating
and releasing too often, or causing large evictions. This appears like
this in "show profiling tasks aggr":

  Tasks activity over 11.428 sec till 0.000 sec ago:
    function                      calls   cpu_tot   cpu_avg   lkw_avg   lkd_avg   mem_avg   lat_avg
    process_stream             44183891   16.47m    22.36us   491.0ns   1.154us   1.000ns   101.1us
    h1_io_cb                   57386064   4.011m    4.193us   20.00ns   16.00ns      -      29.47us
    sc_conn_io_cb              42088024   49.04s    1.165us      -         -         -      54.67us
    h1_timeout_task              438171   196.5ms   448.0ns      -         -         -      100.1us
    srv_cleanup_toremove_conns       65   1.468ms   22.58us   184.0ns   87.00ns      -      101.3us
    task_process_applet               3   508.0us   169.3us      -      107.0us   1.847us   29.67us
    srv_cleanup_idle_conns            6   225.3us   37.55us   15.74us   36.84us      -      49.47us
    accept_queue_process              2   45.62us   22.81us      -         -      4.949us   54.33us

3 weeks agoMINOR: activity: add a new mem_avg column to show profiling stats
Willy Tarreau [Thu, 11 Sep 2025 12:49:02 +0000 (14:49 +0200)] 
MINOR: activity: add a new mem_avg column to show profiling stats

This new column will be used for reporting the average time spent
allocating or freeing memory in a task when task profiling is enabled.
For now it is not updated.

3 weeks agoMINOR: activity: collect time spent with a lock held for each task
Willy Tarreau [Thu, 11 Sep 2025 09:26:40 +0000 (11:26 +0200)] 
MINOR: activity: collect time spent with a lock held for each task

When DEBUG_THREAD > 0 and task profiling enabled, we'll now measure the
time spent with at least one lock held for each task. The time is
collected by locking operations when locks are taken raising the level
to one, or released resetting the level. An accumulator is updated in
the thread_ctx struct that is collected by the scheduler when the task
returns, and updated in the sched_activity entry of the related task.

This allows to observe figures like this one:

  Tasks activity over 259.516 sec till 0.000 sec ago:
    function                      calls   cpu_tot   cpu_avg   lkw_avg   lkd_avg   lat_avg
    h1_io_cb                   15466589   2.574m    9.984us      -         -      33.45us <- sock_conn_iocb@src/sock.c:1099 tasklet_wakeup
    sc_conn_io_cb               8047994   8.325s    1.034us      -         -      870.1us <- sc_app_chk_rcv_conn@src/stconn.c:844 tasklet_wakeup
    process_stream              7734689   4.356m    33.79us   1.990us   1.641us   1.554ms <- sc_notify@src/stconn.c:1206 task_wakeup
    process_stream              7734292   46.74m    362.6us   278.3us   132.2us   972.0us <- stream_new@src/stream.c:585 task_wakeup
    sc_conn_io_cb               7733158   46.88s    6.061us      -         -      68.78us <- h1_wake_stream_for_recv@src/mux_h1.c:3633 tasklet_wakeup
    task_process_applet         6603593   4.484m    40.74us   16.69us   34.00us   96.47us <- sc_app_chk_snd_applet@src/stconn.c:1043 appctx_wakeup
    task_process_applet         4761796   3.420m    43.09us   18.79us   39.28us   138.2us <- __process_running_peer_sync@src/peers.c:3579 appctx_wakeup
    process_table_expire        4710662   4.880m    62.16us   9.648us   53.95us   158.6us <- run_tasks_from_lists@src/task.c:671 task_queue
    stktable_add_pend_updates   4171868   6.786s    1.626us      -      1.487us   47.94us <- stktable_add_pend_updates@src/stick_table.c:869 tasklet_wakeup
    h1_io_cb                    2871683   1.198s    417.0ns   70.00ns   69.00ns   1.005ms <- h1_takeover@src/mux_h1.c:5659 tasklet_wakeup
    process_peer_sync           2304957   5.368s    2.328us      -      1.156us   68.54us <- stktable_add_pend_updates@src/stick_table.c:873 task_wakeup
    process_peer_sync           1388141   3.174s    2.286us      -      1.130us   52.31us <- run_tasks_from_lists@src/task.c:671 task_queue
    stktable_add_pend_updates    463488   3.530s    7.615us   2.000ns   7.134us   771.2us <- stktable_touch_with_exp@src/stick_table.c:654 tasklet_wakeup

Here we see that almost the entirety of stktable_add_pend_updates() is
spent under a lock, that 1/3 of the execution time of process_stream()
was performed under a lock and that 2/3 of it was spent waiting for a
lock (this is related to the 10 track-sc present in this config), and
that the locking time in process_peer_sync() has now significantly
reduced. This is more visible with "show profiling tasks aggr":

  Tasks activity over 475.354 sec till 0.000 sec ago:
    function                      calls   cpu_tot   cpu_avg   lkw_avg   lkd_avg   lat_avg
    h1_io_cb                   25742539   3.699m    8.622us   11.00ns   10.00ns   188.0us
    sc_conn_io_cb              22565666   1.475m    3.920us      -         -      473.9us
    process_stream             21665212   1.195h    198.6us   140.6us   67.08us   1.266ms
    task_process_applet        16352495   11.31m    41.51us   17.98us   36.55us   112.3us
    process_peer_sync           7831923   17.15s    2.189us      -      1.107us   41.27us
    process_table_expire        6878569   6.866m    59.89us   9.359us   51.91us   151.8us
    stktable_add_pend_updates   6602502   14.77s    2.236us      -      2.060us   119.8us
    h1_timeout_task                 801   703.4us   878.0ns      -         -      185.7us
    srv_cleanup_toremove_conns      347   12.43ms   35.82us   240.0ns   70.00ns   1.924ms
    accept_queue_process            142   1.384ms   9.743us      -         -      340.6us
    srv_cleanup_idle_conns           74   475.0us   6.418us   896.0ns   5.667us   114.6us

3 weeks agoMINOR: activity: add a new lkd_avg column to show profiling stats
Willy Tarreau [Thu, 11 Sep 2025 09:26:01 +0000 (11:26 +0200)] 
MINOR: activity: add a new lkd_avg column to show profiling stats

This new column will be used for reporting the average time spent
in a task with at least one lock held. It will only have a non-zero
value when DEBUG_THREAD > 0. For now it is not updated.

3 weeks agoMINOR: thread: add a lock level information in the thread_ctx
Willy Tarreau [Thu, 11 Sep 2025 06:29:25 +0000 (08:29 +0200)] 
MINOR: thread: add a lock level information in the thread_ctx

The new lock_level field indicates the number of cumulated locks that
are held by the current thread. It's fed as soon as DEBUG_THREAD is at
least 1. In addition, thread_isolate() adds 128, so that it's even
possible to check for combinations of both. The value is also reported
in thread dumps (warnings and panics).

3 weeks agoMINOR: activity: collect time spent waiting on a lock for each task
Willy Tarreau [Thu, 11 Sep 2025 08:47:35 +0000 (10:47 +0200)] 
MINOR: activity: collect time spent waiting on a lock for each task

When DEBUG_THREAD > 0, and if task profiling is enabled, then each
locking attempt will measure the time it takes to obtain the lock, then
add that time to a thread_ctx accumulator that the scheduler will then
retrieve to update the current task's sched_activity entry. The value
will then appear avearaged over the number of calls in the lkw_avg column
of "show profiling tasks", such as below:

  Tasks activity over 48.298 sec till 0.000 sec ago:
    function                      calls   cpu_tot   cpu_avg   lkw_avg   lat_avg
    h1_io_cb                    3200170   26.81s    8.377us      -      32.73us <- sock_conn_iocb@src/sock.c:1099 tasklet_wakeup
    sc_conn_io_cb               1657841   1.645s    992.0ns      -      853.0us <- sc_app_chk_rcv_conn@src/stconn.c:844 tasklet_wakeup
    process_stream              1600450   49.16s    30.71us   1.936us   1.392ms <- sc_notify@src/stconn.c:1206 task_wakeup
    process_stream              1600321   7.770m    291.3us   209.1us   901.6us <- stream_new@src/stream.c:585 task_wakeup
    sc_conn_io_cb               1599928   7.975s    4.984us      -      65.77us <- h1_wake_stream_for_recv@src/mux_h1.c:3633 tasklet_wakeup
    task_process_applet          997609   46.37s    46.48us   16.80us   113.0us <- sc_app_chk_snd_applet@src/stconn.c:1043 appctx_wakeup
    process_table_expire         922074   48.79s    52.92us   7.275us   181.1us <- run_tasks_from_lists@src/task.c:670 task_queue
    stktable_add_pend_updates    705423   1.511s    2.142us      -      56.81us <- stktable_add_pend_updates@src/stick_table.c:869 tasklet_wakeup
    task_process_applet          683511   34.75s    50.84us   18.37us   153.3us <- __process_running_peer_sync@src/peers.c:3579 appctx_wakeup
    h1_io_cb                     535395   198.1ms   370.0ns   72.00ns   930.4us <- h1_takeover@src/mux_h1.c:5659 tasklet_wakeup

It now makes it pretty obvious which tasks (hence call chains) spend their
time waiting on a lock and for what share of their execution time.

3 weeks agoMINOR: activity: add a new lkw_avg column to show profiling stats
Willy Tarreau [Thu, 11 Sep 2025 08:42:02 +0000 (10:42 +0200)] 
MINOR: activity: add a new lkw_avg column to show profiling stats

This new column will be used for reporting the average time spent waiting
for a lock. It will only have a non-zero value when DEBUG_THREAD > 0. For
now it is not updated.

3 weeks agoMINOR: activity: don't report the lat_tot column for show profiling tasks
Willy Tarreau [Thu, 11 Sep 2025 08:33:50 +0000 (10:33 +0200)] 
MINOR: activity: don't report the lat_tot column for show profiling tasks

This column is pretty useless, as the total latency experienced by tasks
is meaningless, what matters is the average per call. Since we'll add more
columns and we need to keep all of this readable, let's get rid of this
column.

3 weeks agoBUG/MINOR: resolvers: Restore round-robin selection on records in DNS answers
Christopher Faulet [Thu, 11 Sep 2025 13:20:35 +0000 (15:20 +0200)] 
BUG/MINOR: resolvers: Restore round-robin selection on records in DNS answers

Since the commit dcb696cd3 ("MEDIUM: resolvers: hash the records before
inserting them into the tree"), When several records are found in a DNS
answer, the round robin selection over these records is no longer performed.

Indeed, before a list of records was used. To ensure each records was
selected one after the other, at each selection, the first record of the
list was moved at the end. When this list was replaced bu a tree, the same
mechanism was preserved. However, the record is indexed using its key, a
hash of the record. So its position never changes. When it is removed and
reinserted in the tree, its position remains the same. When we walk though
the tree, starting from the root, the records are always evaluated in the
same order. So, even if there are several records in a DNS answer, the same
IP address is always selected.

It is quite easy to trigger the issue with a do-resolv action.

To fix the issue, the node to perform the next selection is now saved. So
instead of restarting from the root each time, we can restart from the next
node of the previous call.

Thanks to Damien Claisse for the issue analysis and for the reproducer.

This patch should fix the issue #3116. It must be backported as far as 2.6.

3 weeks agoBUG/MEDIUM: resolvers: Properly cache do-resolv resolution
Christopher Faulet [Thu, 11 Sep 2025 08:34:46 +0000 (10:34 +0200)] 
BUG/MEDIUM: resolvers: Properly cache do-resolv resolution

As stated by the documentation, when a do-resolv resolution is performed,
the result should be cached for <hold.valid> milliseconds. However, the only
way to cache the result is to always have a requester. When the last
requester is unlink from the resolution, the resolution is released. So, for
a do-resolv resolution, it means it could only work by chance if the same
FQDN is requested enough to always have at least two streams waiting for the
resolution. And because in that case, the cached result is used, it means
the traffic must be quite high.

In fact, a good approach to fix the issue is to keep orphan resolutions to
be able cache the result and only release them after hold.valid milliseconds
after the last real resolution. The resolver's task already releases orphan
resolutions. So we only need to check the expiration date and take care to
not release the resolution when the last stream is unlink from it.

This patch should be backported to all stable versions. We can start to
backport it as far as 3.1 and then wait a bit.

3 weeks agoBUILD: ssl: functions defined but not used
William Lallemand [Thu, 11 Sep 2025 13:32:59 +0000 (15:32 +0200)] 
BUILD: ssl: functions defined but not used

Previous patch 50d191b ("MINOR: ssl: set functions as static when no
protypes in the .h") broke the WolfSSL function with unused functions.

This patch add __maybe_unused to ssl_sock_sctl_parse_cbk(),
ssl_sock_sctl_add_cbk() and ssl_sock_msgcbk()

3 weeks agoMINOR: ssl: set functions as static when no protypes in the .h
William Lallemand [Thu, 11 Sep 2025 13:23:59 +0000 (15:23 +0200)] 
MINOR: ssl: set functions as static when no protypes in the .h

Check with -Wmissing-prototypes what should be static.

src/ssl_sock.c:1572:5: error: no previous prototype for â€˜ssl_sock_sctl_add_cbk’ [-Werror=missing-prototypes]
 1572 | int ssl_sock_sctl_add_cbk(SSL *ssl, unsigned ext_type, const unsigned char **out, size_t *outlen, int *al, void *add_arg)
      |     ^~~~~~~~~~~~~~~~~~~~~
src/ssl_sock.c:1582:5: error: no previous prototype for â€˜ssl_sock_sctl_parse_cbk’ [-Werror=missing-prototypes]
 1582 | int ssl_sock_sctl_parse_cbk(SSL *s, unsigned int ext_type, const unsigned char *in, size_t inlen, int *al, void *parse_arg)
      |     ^~~~~~~~~~~~~~~~~~~~~~~
src/ssl_sock.c:1604:6: error: no previous prototype for â€˜ssl_sock_infocbk’ [-Werror=missing-prototypes]
 1604 | void ssl_sock_infocbk(const SSL *ssl, int where, int ret)
      |      ^~~~~~~~~~~~~~~~
src/ssl_sock.c:2107:6: error: no previous prototype for â€˜ssl_sock_msgcbk’ [-Werror=missing-prototypes]
 2107 | void ssl_sock_msgcbk(int write_p, int version, int content_type, const void *buf, size_t len, SSL *ssl, void *arg)
      |      ^~~~~~~~~~~~~~~
src/ssl_sock.c:3936:5: error: no previous prototype for â€˜sh_ssl_sess_new_cb’ [-Werror=missing-prototypes]
 3936 | int sh_ssl_sess_new_cb(SSL *ssl, SSL_SESSION *sess)
      |     ^~~~~~~~~~~~~~~~~~
src/ssl_sock.c:3990:14: error: no previous prototype for â€˜sh_ssl_sess_get_cb’ [-Werror=missing-prototypes]
 3990 | SSL_SESSION *sh_ssl_sess_get_cb(SSL *ssl, __OPENSSL_110_CONST__ unsigned char *key, int key_len, int *do_copy)
      |              ^~~~~~~~~~~~~~~~~~
src/ssl_sock.c:4043:6: error: no previous prototype for â€˜sh_ssl_sess_remove_cb’ [-Werror=missing-prototypes]
 4043 | void sh_ssl_sess_remove_cb(SSL_CTX *ctx, SSL_SESSION *sess)
      |      ^~~~~~~~~~~~~~~~~~~~~
src/ssl_sock.c:4075:6: error: no previous prototype for â€˜ssl_set_shctx’ [-Werror=missing-prototypes]
 4075 | void ssl_set_shctx(SSL_CTX *ctx)
      |      ^~~~~~~~~~~~~
src/ssl_sock.c:4103:6: error: no previous prototype for â€˜SSL_CTX_keylog’ [-Werror=missing-prototypes]
 4103 | void SSL_CTX_keylog(const SSL *ssl, const char *line)
      |      ^~~~~~~~~~~~~~
src/ssl_sock.c:5167:6: error: no previous prototype for â€˜ssl_sock_deinit’ [-Werror=missing-prototypes]
 5167 | void ssl_sock_deinit()
      |      ^~~~~~~~~~~~~~~
src/ssl_sock.c:6976:6: error: no previous prototype for â€˜ssl_sock_close’ [-Werror=missing-prototypes]
 6976 | void ssl_sock_close(struct connection *conn, void *xprt_ctx) {
      |      ^~~~~~~~~~~~~~
src/ssl_sock.c:7846:17: error: no previous prototype for â€˜ssl_action_wait_for_hs’ [-Werror=missing-prototypes]
 7846 | enum act_return ssl_action_wait_for_hs(struct act_rule *rule, struct proxy *px,
      |                 ^~~~~~~~~~~~~~~~~~~~~~

3 weeks agoMINOR: ocsp: put internal functions as static ones
William Lallemand [Thu, 11 Sep 2025 13:13:23 +0000 (15:13 +0200)] 
MINOR: ocsp: put internal functions as static ones

-Wmissing-prototypes let us check which functions can be made static and
is not used elsewhere.

rc/ssl_ocsp.c:1079:5: error: no previous prototype for â€˜ssl_ocsp_update_insert_after_error’ [-Werror=missing-prototypes]
 1079 | int ssl_ocsp_update_insert_after_error(struct certificate_ocsp *ocsp)
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/ssl_ocsp.c:1116:6: error: no previous prototype for â€˜ocsp_update_response_stline_cb’ [-Werror=missing-prototypes]
 1116 | void ocsp_update_response_stline_cb(struct httpclient *hc)
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/ssl_ocsp.c:1127:6: error: no previous prototype for â€˜ocsp_update_response_headers_cb’ [-Werror=missing-prototypes]
 1127 | void ocsp_update_response_headers_cb(struct httpclient *hc)
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/ssl_ocsp.c:1138:6: error: no previous prototype for â€˜ocsp_update_response_body_cb’ [-Werror=missing-prototypes]
 1138 | void ocsp_update_response_body_cb(struct httpclient *hc)
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/ssl_ocsp.c:1149:6: error: no previous prototype for â€˜ocsp_update_response_end_cb’ [-Werror=missing-prototypes]
 1149 | void ocsp_update_response_end_cb(struct httpclient *hc)
      |      ^~~~~~~~~~~~~~~~~~~~~~~~~~~
src/ssl_ocsp.c:2095:5: error: no previous prototype for â€˜ocsp_update_postparser_init’ [-Werror=missing-prototypes]
 2095 | int ocsp_update_postparser_init()
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~

3 weeks agoBUG/MINOR: ocsp: prototype inconsistency
William Lallemand [Thu, 11 Sep 2025 13:05:25 +0000 (15:05 +0200)] 
BUG/MINOR: ocsp: prototype inconsistency

Inconsistencies between the .h and the .c can't be catched because the
.h is not included in the .c.

ocsp_update_init() does not have the right prototype and lacks a const
attribute.

Must be backported in all previous stable versions.

3 weeks agoBUG/MINOR: ssl: Fix potential NULL deref in trace callback
Remi Tricot-Le Breton [Wed, 10 Sep 2025 08:31:08 +0000 (10:31 +0200)] 
BUG/MINOR: ssl: Fix potential NULL deref in trace callback

'conn' might be NULL in the trace callback so the calls to
conn_err_code_str must be covered by a proper check.

This issue was found by Coverity and raised in GitHub #3112.
The patch must be backported to 3.2.

3 weeks agoBUG/MINOR: ssl: Potential NULL deref in trace macro
Remi Tricot-Le Breton [Wed, 10 Sep 2025 08:13:22 +0000 (10:13 +0200)] 
BUG/MINOR: ssl: Potential NULL deref in trace macro

'ctx' might be NULL when we exit 'ssl_sock_handshake', it can't be
dereferenced without check in the trace macro.

This was found by Coverity andraised in GitHub #3113.
This patch should be backported up to 3.2.

3 weeks agoBUG/MEDIUM: jws: return size_t in JWS functions
William Lallemand [Thu, 11 Sep 2025 12:25:03 +0000 (14:25 +0200)] 
BUG/MEDIUM: jws: return size_t in JWS functions

JWS functions are supposed to return 0 upon error or when nothing was
produced. This was done in order to put easily the return value in
trash->data without having to check the return value.

However functions like a2base64url() or snprintf() could return a
negative value, which would be casted in a unsigned int if this happen.

This patch add checks on the JWS functions to ensure that no negative
value can be returned, and change the prototype from int to size_t.

This is also related to issue #3114.

Must be backported to 3.2.

3 weeks agoBUG/MINOR: acme: null pointer dereference upon allocation failure
William Lallemand [Thu, 11 Sep 2025 11:42:45 +0000 (13:42 +0200)] 
BUG/MINOR: acme: null pointer dereference upon allocation failure

Reported in issue #3115:

      11. var_compare_op: Comparing task to null implies that task might be null.
681                if (!task) {
682                        ret++;
683                        ha_alert("acme: couldn't start the scheduler!\n");
684                }
CID 1609721: (#1 of 1): Dereference after null check (FORWARD_NULL)
12. var_deref_op: Dereferencing null pointer task.
685                task->nice = 0;
686                task->process = acme_scheduler;
687
688                task_wakeup(task, TASK_WOKEN_INIT);
689        }
690

Task would be dereferenced upon allocation failure instead of falling
back to the end of the function after the error.

Should be backported in 3.2.

3 weeks agoDOC: quic: clarifies limited-quic support quic-interop
Amaury Denoyelle [Thu, 11 Sep 2025 08:06:26 +0000 (10:06 +0200)] 
DOC: quic: clarifies limited-quic support

This patch extends the documentation for "limited-quic" global keyword.
It mentions first that it relies on USE_QUIC_OPENSSL_COMPAT=1 build
option.

Compatibility with TLS libraries is now clearly exposed. In particular,
it highlights the fact that it is mostly targetted at OpenSSL version
prior to 3.5.2, and that it should be disabled if a recent OpenSSL
release is available. It also states that limited-quic does nothing if
USE_QUIC_OPENSSL_COMPAT is not set during compilation.

3 weeks agoMINOR: quic: display build warning for compat layer on recent OpenSSL
Amaury Denoyelle [Tue, 9 Sep 2025 15:19:13 +0000 (17:19 +0200)] 
MINOR: quic: display build warning for compat layer on recent OpenSSL

Build option USE_QUIC_OPENSSL_COMPAT=1 must be set to activate QUIC
support for OpenSSL prior to version 3.5.2. This compiles an internal
compatibility layer, which must be then activated at runtime with global
option limited-quic.

Starting from OpenSSL version 3.5.2, a proper QUIC TLS API is now
exposed. Thus, the compatibility layer is unneeded. However it can still
be compiled against newer OpenSSL releases and activated at runtime,
mostly for test purpose.

As this compatibility layer has some limitations, (no support for QUIC
0-RTT), it's important that users notice this situation and disable it
if possible. Thus, this patch adds a notice warning when
USE_QUIC_OPENSSL_COMPAT=1 is set when building against OpenSSL 3.5.2 and
above. This should be sufficient for users and packagers to understand
that this option is not necessary anymore.

Note that USE_QUIC_OPENSSL_COMPAT=1 is incompatible with others TLS
library which exposed a QUIC API based on original BoringSSL patches
set. A build error will prevent the compatibility layer to be built.
limited-quic option is thus silently ignored.

3 weeks agoMINOR: quic-be: make SSL/QUIC objects use their own indexes (ssl_qc_app_data_index)
Frederic Lecaille [Wed, 10 Sep 2025 16:11:50 +0000 (18:11 +0200)] 
MINOR: quic-be: make SSL/QUIC objects use their own indexes (ssl_qc_app_data_index)

This index is used to retrieve the quic_conn object from its SSL object, the same
way the connection is retrieved from its SSL object for SSL/TCP connections.

This patch implements two helper functions to avoid the ugly code with such blocks:

   #ifdef USE_QUIC
   else if (qc) { .. }
   #endif

Implement ssl_sock_get_listener() to return the listener from an SSL object.
Implement ssl_sock_get_conn() to return the connection from an SSL object
and optionally a pointer to the ssl_sock_ctx struct attached to the connections
or the quic_conns.

Use this functions where applicable:
   - ssl_tlsext_ticket_key_cb() calls ssl_sock_get_listener()
   - ssl_sock_infocbk() calls ssl_sock_get_conn()
   - ssl_sock_msgcbk() calls ssl_sock_get_ssl_conn()
   - ssl_sess_new_srv_cb() calls ssl_sock_get_conn()
   - ssl_sock_srv_verifycbk() calls ssl_sock_get_conn()

Also modify qc_ssl_sess_init() to initialize the ssl_qc_app_data_index index for
the QUIC backends.

3 weeks agoMINOR: quic: get rid of ->target quic_conn struct member
Frederic Lecaille [Tue, 9 Sep 2025 09:49:33 +0000 (11:49 +0200)] 
MINOR: quic: get rid of ->target quic_conn struct member

The ->li (struct listener *) member of quic_conn struct was replaced by a
->target (struct obj_type *) member by this commit:

    MINOR: quic-be: get rid of ->li quic_conn member

to abstract the connection type (front or back) when implementing QUIC for the
backends. In these cases, ->target was a pointer to the ojb_type of a server
struct. This could not work with the dynamic servers contrary to the listeners
which are not dynamic.

This patch almost reverts the one mentioned above. ->target pointer to obj_type member
is replaced by ->li pointer to listener struct member. As the listener are not
dynamic, this is easy to do this. All one has to do is to replace the
objt_listener(qc->target) statement by qc->li where applicable.

For the backend connection, when needed, this is always qc->conn->target which is
used only when qc->conn is initialized. The only "problematic" case is for
quic_dgram_parse() which takes a pointer to an obj_type as third argument.
But this obj_type is only used to call quic_rx_pkt_parse(). Inside this function
it is used to access the proxy counters of the connection thanks to qc_counters().
So, this obj_type argument may be null for now on with this patch. This is the
reason why qc_counters() is modified to take this into consideration.

3 weeks agoBUG/MAJOR: stream: Force channel analysis on successful synchronous send
Christopher Faulet [Thu, 11 Sep 2025 07:38:41 +0000 (09:38 +0200)] 
BUG/MAJOR: stream: Force channel analysis on successful synchronous send

This patchs reverts commit a498e527b ("BUG/MAJOR: stream: Remove READ/WRITE
events on channels after analysers eval") because of a regression. It was an
attempt to properly detect synchronous sends, even when the stream was woken
up on a write event. However, the fix was wrong because it could mask
shutdowns performed during process_stream() and block the stream.

Indeed, when a shutdown is performed, because an error occurred for
instance, a write event is reported. The commit above could mask this event
while the shutdown prevent any synchronous sends. In such case, the stream
could remain blocked infinitly because an I/O event was missed.

So to properly fix the original issue (#3070), the write event must not be
masked before a synchronous send. Instead, we now force the channel analysis
by setting explicitly CF_WAKE_ONCE flags on the corresponding channel if a
write event is reported after the synchronous send. CF_WRITE_EVENT flag is
remove explicitly just before, so it is quite easy to detect.

This patch must be backport to all stable version in same time of the commit
above.

3 weeks agoMEDIUM: peers: move process_peer_sync() to a single thread
Willy Tarreau [Wed, 10 Sep 2025 16:52:56 +0000 (18:52 +0200)] 
MEDIUM: peers: move process_peer_sync() to a single thread

The remaining half of the task_queue() and task_wakeup() contention
is caused by this function when peers are in use, because just like
process_table_expire(), it's created using task_new_anywhere() and
is woken up for local updates. Let's turn it to single thread by
rotating the assigned threads during initialization so that a table
only runs on one thread at a time.

Here we go backwards to assign the threads, so that on small setups
they don't end up on the same CPUs as the ones used by the stick-tables.
This way this will make an even better use of large machines. The
performance remains the same as with previous patch, even slightly
better (1-3% on avg).

At this point there's almost no multi-threaded task activity anymore
(only srv_cleanup_idle_server once in a while). This should improve
the situation described by Felipe in issues #3084 and #3101.

This should be backported to 3.2 after some extended checks.

3 weeks agoMEDIUM: stick-table: move process_table_expire() to a single thread
Willy Tarreau [Wed, 10 Sep 2025 16:47:50 +0000 (18:47 +0200)] 
MEDIUM: stick-table: move process_table_expire() to a single thread

A big deal of the task_queue() contention is caused by this function
because it's created using task_new_anywhere() and is subject to
heavy updates. Let's turn it to single thread by rotating the assigned
threads during initialization so that a table only runs on one thread
at a time.

However there's a trick: the function used to call task_queue() to
requeue the task if it had advanced its timer (may only happen when
learning an entry from a peer). We can't do that anymore since we can't
queue another thread's task. Thus instead of the task needs to be
scheduled earlier than previously planned, we simply perform a wakeup.
It will likely do nothing and will self-adjust its next wakeup timer.

Doing so halves the number of multi-thread task wakeups. In addition
the request rate at saturation increased by 12% with 16 peers and 40
tables on a 16 8-thread processes. This should improve the situation
described by Felipe in issues #3084 and #3101.

This should be backported to 3.2 after some extended checks.

3 weeks agoBUG/MINOR: stick-table: make sure never to miss a process_table_expire update
Willy Tarreau [Wed, 10 Sep 2025 16:45:01 +0000 (18:45 +0200)] 
BUG/MINOR: stick-table: make sure never to miss a process_table_expire update

In stktable_requeue_exp(), there's a tiny race at the beginning during
which we check the task's expiration date to decide whether or not to
wake process_table_expire() up. During this race, the task might just
have finished running on its owner thread and we can miss a task_queue()
opportunity, which probably explains why during testing it seldom happens
that a few entries are left at the end.

Let's perform a CAS to confirm the value is still the same before
leaving. This way we're certain that our value has been seen at least
once.

This should be backported to 3.2.

3 weeks agoMEDIUM: resolvers: make the process_resolvers() task single-threaded
Willy Tarreau [Wed, 10 Sep 2025 14:51:14 +0000 (16:51 +0200)] 
MEDIUM: resolvers: make the process_resolvers() task single-threaded

This task is sometimes caught triggering the watchdog while waiting for
the infamous resolvers lock, or the scheduler's wait queue lock in
task_queue(). Both are caused by its multi-threaded capability. The
task may indeed start on a thread that's different from the one that
is currently receiving a response and that holds the resolvers lock,
and when being queued back, it requires to lock the wait queue. Both
problems disappear when sticking it to a single thread. But for configs
running multiple resolvers sections, it would be suboptimal to run them
all on the same thread. In order to avoid this, we implement a counter
in the resolvers_finalize_config() section that rotates the thread for
each resolvers section.

This was sufficient to further improve the performance here, making the
CPU usage drop to about 7% (from 11 previously or 38 initially) and not
showing any resolvers lock contention anymore in perf top output.

The change was kept fairly minimal to permit a backport once enough
testing is conducted on it. It could address a significant part of
the trouble reported by Felipe in GH issue #3101.

3 weeks agoMEDIUM: dns: bind the nameserver sockets to the initiating thread
Willy Tarreau [Wed, 10 Sep 2025 14:37:40 +0000 (16:37 +0200)] 
MEDIUM: dns: bind the nameserver sockets to the initiating thread

There's still a big architectural limitation in the dns/resolvers code
regarding threads: resolvers run as a task that is scheduled to run
anywhere, and each NS dgram socket is bound to any thread of the same
thread group as the initiating thread. This becomes a big problem when
dealing with multiple nameservers because responses arrive on any thread,
start by locking the resolvers section, and other threads dealing with
responses are just stuck waiting for the lock to disappear. This means
that most of the time is exclusively spent causing contention. The
process_resolvers() function also also suffers from this contention
but apparently less often.

It turns out that the nameserver sockets are created during emission
of the first packet, triggered from the resolvers task. The present
patch exploits this to stick all sockets to the calling thread instead
of any thread. This way there is no longer any contention between
multiple nameservers of a same resolvers section. Tests with a section
having 10 name servers showed that the CPU usage dropped from 38 to
about 10%, or almost by a factor of 4.

Note that TCP resolvers do not offer this possibility because the
tasks that manage the applets are created earlier to run anywhere
during config parsing. This might possibly be refined later, e.g.
by changing the task's affinity when it first runs.

The change was kept fairly minimal to permit a backport once enough
testing is conducted on it. It could address a significant part of
the trouble reported by Felipe in GH issue #3101.

3 weeks agoBUG/MEDIUM: ssl: Fix a crash if we failed to create the mux
Olivier Houchard [Wed, 10 Sep 2025 10:03:01 +0000 (12:03 +0200)] 
BUG/MEDIUM: ssl: Fix a crash if we failed to create the mux

In ssl_sock_io_cb(), if we failed to create the mux, we may have
destroyed the connection, so only attempt to access it to get the ALPN
if conn_create_mux() was successful.
This fixes crashes that may happen when using ssl.

3 weeks agoBUG/MEDIUM: ssl: Fix a crash when using QUIC
Olivier Houchard [Wed, 10 Sep 2025 09:40:32 +0000 (11:40 +0200)] 
BUG/MEDIUM: ssl: Fix a crash when using QUIC

Commit 5ab9954faa9c815425fa39171ad33e75f4f7d56f introduced a new flag in
ssl_sock_ctx, to know that an ALPN was negociated, however, the way to
get the ssl_sock_ctx was wrong for QUIC. If we're using QUIC, get it
from the quic_conn.
This should fix crashes when attempting to use QUIC.

3 weeks agoDEBUG: stick-tables: export stktable_add_pend_updates() for better reporting
Willy Tarreau [Wed, 10 Sep 2025 09:32:51 +0000 (11:32 +0200)] 
DEBUG: stick-tables: export stktable_add_pend_updates() for better reporting

This function is a tasklet handler used to send peers updates, and it can
happen quite a bit in "show tasks" and "show profiling tasks", so let's
export it so that we don't face a cryptic symbol name:

  $ socat - /tmp/haproxy-n10.stat <<< "show tasks"
  Running tasks: 43 (8 threads)
    function                     places     %    lat_tot   lat_avg  calls_tot  calls_avg calls%
    process_table_expire             16   37.2   1.072m    4.021s      115831       7239   15.4
    task_process_applet              15   34.8   1.072m    4.287s      486299      32419   65.0
    stktable_add_pend_updates         8   18.6      -         -         89725      11215   12.0
    sc_conn_io_cb                     3    6.9      -         -          5007       1669    0.6
    process_peer_sync                 1    2.3   4.293s    4.293s       50765      50765    6.7

This should be backported to 3.2 as it participates to debugging the
table+peers processing overhead.

3 weeks agoBUG/MEDIUM: stick-tables: don't loop on non-expirable entries
Willy Tarreau [Wed, 10 Sep 2025 09:27:27 +0000 (11:27 +0200)] 
BUG/MEDIUM: stick-tables: don't loop on non-expirable entries

The stick-table expiration of ref-counted entries was insufficiently
addresse by commit 324f0a60ab ("BUG/MINOR: stick-tables: never leave
used entries without expiration"), because now entries are just requeued
where they were, so they're visited over and over for long sessions,
causing process_table_expire() to loop, eating CPU and causing lock
contention.

Here we take care of refreshing their timeer when they are met, so
that we don't meet them more than once per stick-table lifetime. It
should address at least a part of the recent degradation that Felipe
noticed in GH #3084.

Since the fix above was marked for backporting to 3.2, this one should
be backported there as well.

3 weeks agoMINOR: tools: don't emit "+0" for symbol names which exactly match known ones
Willy Tarreau [Wed, 10 Sep 2025 08:27:07 +0000 (10:27 +0200)] 
MINOR: tools: don't emit "+0" for symbol names which exactly match known ones

resolve_sym_name() knows a number of symbols, but when one exactly matches
(e.g. a task's handler), it systematically displays the offset behind it
("+0"). Let's only show the offset when non-zero. This can be backported
as this is helpful for debugging.

3 weeks agoMINOR: activity: indicate the number of calls on "show tasks"
Willy Tarreau [Wed, 10 Sep 2025 08:09:33 +0000 (10:09 +0200)] 
MINOR: activity: indicate the number of calls on "show tasks"

The "show tasks" command can be useful to inspect run queues for active
tasks, but currently it's difficult to distinguish an occasional running
task from a heavily active one. Let's collect the number of calls for
each of them, report them average on the number of instances of each task
as well as a percentage of the total used. This way it even becomes
possible to get a hint about how CPU usage is distributed.

3 weeks agoBUG/MINOR: activity: fix reporting of task latency
Willy Tarreau [Wed, 10 Sep 2025 08:36:27 +0000 (10:36 +0200)] 
BUG/MINOR: activity: fix reporting of task latency

In 2.4, "show tasks" was introduced by commit 7eff06e162 ("MINOR:
activity: add a new "show tasks" command to list currently active tasks")
to expose some info about running tasks. The latency is not correct
because it's a u32 subtracted from a u64. It ought to have been casted
to u32 for the operation, which is what this patch does.

This can be backported to 2.4.

3 weeks agoBUILD: ssl: address a recent build warning when QUIC is enabled
Willy Tarreau [Wed, 10 Sep 2025 08:43:08 +0000 (10:43 +0200)] 
BUILD: ssl: address a recent build warning when QUIC is enabled

Since commit 5ab9954faa ("MINOR: ssl: Add a flag to let it known we have
an ALPN negociated"), when building with QUIC we get this warning:

  src/ssl_sock.c: In function 'ssl_sock_advertise_alpn_protos':
  src/ssl_sock.c:2189:2: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]

Let's just move the instructions after the optional declaration. No
backport is needed.

3 weeks agoMEDIUM: server: Make use of the stored ALPN stored in the server
Olivier Houchard [Mon, 8 Sep 2025 21:35:17 +0000 (23:35 +0200)] 
MEDIUM: server: Make use of the stored ALPN stored in the server

Now that which ALPN gets negociated for a given server, use that to
decide if we can create the mux right away in connect_server(), and use
it in conn_install_mux_be().
That way, we may create the mux soon enough for early data to be sent,
before the handshake has been completed.
This commit depends on several previous commits, and it has not been
deemed important enough to backport.

3 weeks agoCLEANUP: backend: clarify the cases where we want to use early data
Willy Tarreau [Thu, 7 Aug 2025 15:32:24 +0000 (17:32 +0200)] 
CLEANUP: backend: clarify the cases where we want to use early data

The conditions to use early data on output are super tricky and
detected later, so that it's difficult to figure how this works. This
patch splits the condition in two parts, the one that can be performed
early that is based on config/client/etc. It is used to clear a variable
that allows early data to be used in case any condition is not satisfied.
It was purposely split into multiple independent and reviewable tests.

The second part remains where it was at the end, and is used to temporarily
clear the handshake flags to let the data layer use early data. This one
being tricky, a large comment explaining the principle was added.

The logic was not changed at all, only the code was made more readable.

3 weeks agoCLEANUP: backend: simplify the complex ifdef related to 0RTT in connect_server()
Willy Tarreau [Thu, 7 Aug 2025 15:06:45 +0000 (17:06 +0200)] 
CLEANUP: backend: simplify the complex ifdef related to 0RTT in connect_server()

Since 3.0 we have HAVE_SSL_0RTT precisely to avoid checking horribly
complicated and unmaintainable conditions to detect support for 0RTT.
Let's just drop the complex condition and use the macro instead.

3 weeks agoCLEANUP: backend: invert the condition to start the mux in connect_server()
Willy Tarreau [Thu, 7 Aug 2025 14:30:38 +0000 (16:30 +0200)] 
CLEANUP: backend: invert the condition to start the mux in connect_server()

Instead of trying to switch from delayed start to instant start based
on a single condition, let's do the opposite and preset the condition
to instant start and detect what could cause it to be delayed, thus
falling back to the slow mode. The condition remains exactly the
inverted one and better matches the comment about ALPN being the only
cause of such a delay.

3 weeks agoCLEANUP: backend: clarify the role of the init_mux variable in connect_server()
Willy Tarreau [Thu, 7 Aug 2025 14:07:37 +0000 (16:07 +0200)] 
CLEANUP: backend: clarify the role of the init_mux variable in connect_server()

The init_mux variable is currently used in a way that's not super easy
to grasp. It's set a bit too late and requires to know a lot of info at
once. Let's first rename it to "may_start_mux_now" to clarify its role,
as the purpose is not to *force* the mux to be initialized now but to
permit it to do it.