Commit 6de8868d (reconnect: Fix broken inactivity probe if there is no
other reason to wake up.) enables poll_loop to run immediately on the
next millisecond when the probe interval has expired with no receive
attempted. Its reasoning notes that "in a correctly written application
we should not fall into this case more than once in a row".
ovsdb-server falls into it forever on a session whose peer stops
reading. It does not call jsonrpc_session_recv() while a session has a
send backlog, so reconnect_receive_attempted() is never called for it,
the probe expiration can never be passed, and the FSM asks for the
'now + 1' wake-up on every pass. The session is neither probed nor
disconnected, and every wake-up walks every connection, so the cost
grows with the session count: 10.5% of a core with the stalled session
alone, 21.9% alongside 50 healthy sessions, 42.3% alongside 300. This
has been observed in production, where ovn-octavia-provider clients
raised an exception but did not die, keeping their backlog frozen and
degrading the ovsdb-server service.
Let a jsonrpc_session user declare that it does not receive while there
is a backlog, and have ovsdb-server do so for its sessions. For such a
session, jsonrpc_session_run() records a receive attempt whenever a
backlog remains after trying to send it: that send attempt is the
session's look at the connection. The receive gate still prevents a
disconnection while ovsdb-server itself is busy, and stops producing the
'now + 1' wake-ups. A backlog that moves still counts as activity, so a
peer that keeps taking data is kept; one whose queue does not move for a
probe interval is probed and, with the probe queued behind the backlog,
disconnected one interval later.
Testing. A client reads an 8 MB monitor reply at a fixed rate and never
answers the probe, against the 5 s inactivity_probe default. Passes/s
is the mean (and peak) of voluntary_ctxt_switches from /proc/PID/status,
which poll_block() advances once per pass, sampled once a second over
72 s past the first probe interval.
unpatched patched
rate passes/s session passes/s session
tcp
0 kB/s 830.1 (933) kept 1.6 (7) dropped t+12 s
4 kB/s 667.1 (918) kept 0.3 (7) dropped t+27 s
20 kB/s 32.8 (832) kept 0.6 (7) kept
50 kB/s 0.6 (6) kept 0.6 (7) kept
ssl
0 kB/s 810.3 (906) kept 1.5 (7) dropped t+12 s
50 kB/s 796.7 (878) kept 1.4 (6) dropped t+12 s
225 kB/s 344.7 (883) read, t+37 s 1.5 (6) dropped t+12 s
500 kB/s 0.7 (7) read, t+22 s 1.5 (6) read, t+19 s *
kept: still connected when the 84 s run ends. read: read the
whole reply, then dropped for not answering the probe.
* 1 run in 3 dropped at the first timeout, t+10 s.
A real octavia-driver-agent (ovn-octavia-provider 2.0.0) over ssl,
stopped with SIGSTOP in the middle of a 25.6M-cell northbound dump: the
unpatched server runs at 823 passes/s and never drops the session; the
patched one runs at 1.9 passes/s and drops it at t+10.2 s.
Fixes: 6de8868d19ea ("reconnect: Fix broken inactivity probe if there is no
other reason to wake up.")
Suggested-by: Ilya Maximets <[email protected]>
Signed-off-by: Aeliton G. Silva <[email protected]>
Assisted-by: Claude Opus 5.5, Claude Code
---
Notes:
v3:
- Rework following review: instead of teaching the reconnect FSM
about queued bytes, a jsonrpc_session user declares that it does
not receive while there is a backlog
(jsonrpc_session_set_backlog_blocks_recv()), and such a session
records a receive attempt whenever a backlog remains after trying
to send it. reconnect.c and the Python library are unchanged.
- Squash v2 2/2 into this patch: the same rule now falls out of the
existing probe and timeout.
- Tests: the two ovsdb-server checks use ovsdb-client/block instead
of a new Python client, and also check that the stalled session is
disconnected.
lib/jsonrpc.c | 28 +++++++++++
lib/jsonrpc.h | 2 +
ovsdb/jsonrpc-server.c | 4 ++
tests/ovsdb-server.at | 110 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 144 insertions(+)
diff --git a/lib/jsonrpc.c b/lib/jsonrpc.c
index f01a2e56f..f80a9a947 100644
--- a/lib/jsonrpc.c
+++ b/lib/jsonrpc.c
@@ -833,6 +833,10 @@ struct jsonrpc_session {
/* Limits for jsonrpc. */
size_t max_n_msgs;
size_t max_backlog_bytes;
+
+ /* The user does not call jsonrpc_session_recv() while there is a send
+ * backlog. See jsonrpc_session_set_backlog_blocks_recv(). */
+ bool backlog_blocks_recv;
};
static void
@@ -887,6 +891,7 @@ jsonrpc_session_open_multiple(const struct svec *remotes,
bool retry)
s->seqno = 0;
s->dscp = 0;
s->last_error = 0;
+ s->backlog_blocks_recv = false;
jsonrpc_session_set_backlog_threshold(s, 0, 0);
@@ -934,6 +939,7 @@ jsonrpc_session_open_unreliably(struct jsonrpc *jsonrpc,
uint8_t dscp)
s->stream = NULL;
s->pstream = NULL;
s->seqno = 1;
+ s->backlog_blocks_recv = false;
jsonrpc_session_set_backlog_threshold(s, 0, 0);
return s;
@@ -1065,6 +1071,17 @@ jsonrpc_session_run(struct jsonrpc_session *s)
reconnect_activity(s->reconnect, time_msec());
}
+ if (s->backlog_blocks_recv && jsonrpc_get_backlog(s->rpc)) {
+ /* Our user will not try to receive until the backlog is sent, so
+ * the receive attempt the inactivity probe waits for would never
+ * happen, and the FSM would ask to be woken up every millisecond
+ * to make it. Trying to send the backlog is this session's look
+ * at the connection, so count it as one: a peer that takes
+ * nothing for a probe interval is probed and then disconnected,
+ * the same as a peer that sends nothing. */
+ reconnect_receive_attempted(s->reconnect, time_msec());
+ }
+
error = jsonrpc_get_status(s->rpc);
if (error) {
reconnect_disconnected(s->reconnect, time_msec(), error);
@@ -1355,6 +1372,17 @@ jsonrpc_session_set_options(struct jsonrpc_session *s,
jsonrpc_session_set_dscp(s, options->dscp);
}
+/* Declares whether a send backlog on 's' blocks its user from calling
+ * jsonrpc_session_recv(). A user that does not receive while 's' has a
+ * backlog should set 'backlog_blocks_recv' to true, so that a peer which stops
+ * taking data is still subject to the inactivity probe. */
+void
+jsonrpc_session_set_backlog_blocks_recv(struct jsonrpc_session *s,
+ bool backlog_blocks_recv)
+{
+ s->backlog_blocks_recv = backlog_blocks_recv;
+}
+
/* Sets thresholds for send backlog. If send backlog contains more than
* 'max_n_msgs' messages or is larger than 'max_backlog_bytes' bytes,
* connection will be closed (then reconnected, if that feature is enabled). */
diff --git a/lib/jsonrpc.h b/lib/jsonrpc.h
index 1baffcd80..f38ef8ca9 100644
--- a/lib/jsonrpc.h
+++ b/lib/jsonrpc.h
@@ -153,6 +153,8 @@ void jsonrpc_session_set_probe_interval(struct
jsonrpc_session *,
int probe_interval);
void jsonrpc_session_set_dscp(struct jsonrpc_session *,
uint8_t dscp);
+void jsonrpc_session_set_backlog_blocks_recv(struct jsonrpc_session *,
+ bool backlog_blocks_recv);
void jsonrpc_session_set_backlog_threshold(struct jsonrpc_session *,
size_t max_n_msgs,
size_t max_backlog_bytes);
diff --git a/ovsdb/jsonrpc-server.c b/ovsdb/jsonrpc-server.c
index 92e82228f..599d27c7f 100644
--- a/ovsdb/jsonrpc-server.c
+++ b/ovsdb/jsonrpc-server.c
@@ -617,6 +617,10 @@ ovsdb_jsonrpc_session_create(struct ovsdb_jsonrpc_remote
*remote,
s->js_seqno = jsonrpc_session_get_seqno(js);
s->read_only = read_only;
+ /* ovsdb_jsonrpc_session_run() does not receive while there is a
+ * backlog. */
+ jsonrpc_session_set_backlog_blocks_recv(js, true);
+
remote->server->n_sessions++;
return s;
diff --git a/tests/ovsdb-server.at b/tests/ovsdb-server.at
index 7042b1fd3..a4b1a840b 100644
--- a/tests/ovsdb-server.at
+++ b/tests/ovsdb-server.at
@@ -3161,3 +3161,113 @@ TEST_CONFIG_FILE([complex config], [
}
}
], [0])
+
+AT_BANNER([OVSDB -- ovsdb-server peers that stop reading])
+
+dnl One field of ovsdb-server's memory/show, which omits a zero field.
+m4_define([MEMORY_SHOW_FIELD],
+ [ovs-appctl -t ovsdb-server memory/show dnl
+ | tr ' ' '\n' | sed -n 's/^$1:\(.*\)/\1/p'])
+
+dnl The cumulative poll_create_node count: one per fd the poll loop
+dnl registers, per pass, so its rate tracks the pass rate.
+m4_define([POLL_CREATE_NODE_TOTAL],
+ [ovs-appctl -t ovsdb-server coverage/show dnl
+ | awk '/^poll_create_node/ { for (i = 1; i <= NF; i++) dnl
+ if ($i == "total:") { print $(i + 1); exit } }'])
+
+# OVSDB_SERVER_CHECK_STALLED_PEER(PROTOCOL)
+#
+# Monitors a database over a PROTOCOL ("tcp" or "ssl") remote from an
+# ovsdb-client that then stops reading (ovsdb-client/block), and makes
+# ovsdb-server queue more for it than the sockets hold, so a backlog stands
+# in ovsdb-server's own queue. ovsdb-server does not receive on a session
+# with a backlog, so the inactivity probe, which waits for a receive
+# attempt, used to make the poll loop wake up every millisecond to make
+# one, forever, and the session was neither probed nor disconnected.
+#
+# Checks that the poll loop stays near its idle rate past the probe
+# interval, and that the stalled session is disconnected by the ordinary
+# inactivity probe. Over ssl the payload sits in the SSL layer and only
+# a few bytes stand in ovsdb-server's queue, which is the shape seen in
+# production.
+m4_define([OVSDB_SERVER_CHECK_STALLED_PEER],
+ [AT_SETUP([ovsdb-server drops a $1 peer that stops reading])
+ AT_KEYWORDS([ovsdb server positive $1 backlog probe])
+ m4_if([$1], [ssl],
+ [AT_SKIP_IF([test "$HAVE_OPENSSL" = no])
+ PKIDIR=$abs_top_builddir/tests
+ server_ssl="--private-key=$PKIDIR/testpki-privkey2.pem"
+ server_ssl="$server_ssl --certificate=$PKIDIR/testpki-cert2.pem"
+ server_ssl="$server_ssl --ca-cert=$PKIDIR/testpki-cacert.pem"
+ client_ssl="--private-key=$PKIDIR/testpki-privkey.pem"
+ client_ssl="$client_ssl --certificate=$PKIDIR/testpki-cert.pem"
+ client_ssl="$client_ssl --ca-cert=$PKIDIR/testpki-cacert.pem"],
+ [server_ssl= client_ssl=])
+ ordinal_schema > schema
+ AT_CHECK([ovsdb-tool create db schema], [0], [ignore], [ignore])
+ on_exit 'kill `cat *.pid`'
+ AT_CHECK([ovsdb-server --log-file --detach --no-chdir --pidfile dnl
+ $server_ssl --remote=p$1:0:127.0.0.1 db], [0], [ignore], [ignore])
+ PARSE_LISTENING_PORT([ovsdb-server.log], [PORT])
+ target=$1:127.0.0.1:$PORT
+
+ idle_before=`POLL_CREATE_NODE_TOTAL`
+ sleep 2
+ idle_after=`POLL_CREATE_NODE_TOTAL`
+ : ${idle_before:=0} ${idle_after:=0}
+ idle_rate=`expr $idle_after - $idle_before`
+ idle_rate=`expr $idle_rate / 2`
+
+ dnl The monitor detaches once it has the (empty) initial contents, and
+ dnl from here on reads nothing at all.
+ AT_CHECK([ovsdb-client --detach --no-chdir --pidfile $client_ssl dnl
+ monitor $target ordinals ordinals > monitor.out], [0], [ignore],
+ [ignore])
+ AT_CHECK([ovs-appctl -t ovsdb-client ovsdb-client/block], [0], [ignore])
+
+ dnl Insert 60 kB rows until updates for the blocked monitor stand in
+ dnl ovsdb-server's queue. Stop there: the probe interval runs from the
+ dnl last time that queue moved.
+ name=`printf '%060000d' 0`
+ for i in `seq 1 500`; do
+ ovsdb-client $client_ssl transact $target dnl
+ "@<:@\"ordinals\", {\"op\": \"insert\", \"table\": \"ordinals\", dnl
+ \"row\": {\"number\": $i, \"name\": \"$name\"}}@:>@" > /dev/null
+ backlog=`MEMORY_SHOW_FIELD([backlog])`
+ test -n "$backlog" && break
+ done
+
+ dnl Hard preconditions, not assumptions: without a standing backlog the
+ dnl checks below would pass on a server that still spins, and with any
+ dnl other session the disconnect below might not be the stalled one.
+ OVS_WAIT_UNTIL([test "`MEMORY_SHOW_FIELD([backlog])`" -gt 0])
+ OVS_WAIT_UNTIL([test "`MEMORY_SHOW_FIELD([sessions])`" = 1])
+
+ dnl Past the 5 s default probe interval of a command-line remote.
+ sleep 6
+ stalled_before=`POLL_CREATE_NODE_TOTAL`
+ sleep 2
+ stalled_after=`POLL_CREATE_NODE_TOTAL`
+ : ${stalled_before:=0} ${stalled_after:=0}
+ stalled_rate=`expr $stalled_after - $stalled_before`
+ stalled_rate=`expr $stalled_rate / 2`
+
+ dnl Before the fix this measured thousands per second against an idle
+ dnl rate of about 10. Allow ten times the idle rate.
+ limit=`expr $idle_rate + 10`
+ limit=`expr $limit \* 10`
+ echo "poll_create_node/s: idle $idle_rate, stalled $stalled_rate, dnl
+limit $limit"
+ AT_CHECK([test $stalled_rate -lt $limit])
+
+ dnl Nothing reaches the peer for a probe interval, so it is probed, and
+ dnl the probe cannot get past the backlog either, so it is disconnected
+ dnl one interval later.
+ OVS_WAIT_UNTIL([grep -q 'no response to inactivity probe' ovsdb-server.log])
+ OVS_WAIT_UNTIL([test -z "`MEMORY_SHOW_FIELD([sessions])`"])
+ OVSDB_SERVER_SHUTDOWN(["/no response to inactivity probe/d"])
+ AT_CLEANUP])
+
+OVSDB_SERVER_CHECK_STALLED_PEER([tcp])
+OVSDB_SERVER_CHECK_STALLED_PEER([ssl])
--
2.43.0
--
_'Esta mensagem é direcionada apenas para os endereços constantes no
cabeçalho inicial. Se você não está listado nos endereços constantes no
cabeçalho, pedimos-lhe que desconsidere completamente o conteúdo dessa
mensagem e cuja cópia, encaminhamento e/ou execução das ações citadas estão
imediatamente anuladas e proibidas'._
* **'Apesar do Magazine Luiza tomar
todas as precauções razoáveis para assegurar que nenhum vírus esteja
presente nesse e-mail, a empresa não poderá aceitar a responsabilidade por
quaisquer perdas ou danos causados por esse e-mail ou por seus anexos'.*
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev