Description: Retry TLS reads and writes that return GNUTLS_E_AGAIN gnutls_record_send() and gnutls_record_recv() may return GNUTLS_E_AGAIN or GNUTLS_E_INTERRUPTED. Both are non-fatal -- gnutls_error_is_fatal() returns 0 for them -- and the call is meant to be repeated. csync2 passes the negative return straight up: conn_read() turns it into a short read and csync_recv_file() calls csync_fatal("Read-error while receiving data."), while csync_send_file() sees rc != chunk and calls csync_fatal("Write-error while sending data."). . The whole transfer therefore dies on a condition GnuTLS reports as harmless. In practice this happens after roughly 7.4 GiB moved over a single connection, which makes a large first synchronisation abort and restart repeatedly until it eventually completes. Instrumenting both call sites on a three-node cluster shows the cause directly: . sender: TLSDIAG send rc=-28 (Resource temporarily unavailable, try again.) count=512 receiver: TLSDIAG recv rc=-28 (Resource temporarily unavailable, try again.) fatal=0 count=512 . Both ends must be fixed. Patching only the sending side was measured to change nothing at all: the receiving side then aborts on the same condition, at the same threshold, with the same number of retries needed. . Measured on three Debian nodes synchronising a 31.66 GB tree of 56525 mixed files to two peers, with all scheduled jobs stopped: . unpatched : 7 csync2 runs needed, aborting after 8.0, 15.4 and 22.9 GB patched : 1 csync2 run, no aborts . The GnuTLS manual requires that gnutls_record_send() be called again "with the exact same parameters" after GNUTLS_E_AGAIN or GNUTLS_E_INTERRUPTED, and that gnutls_record_recv() simply be called again. The loop below preserves this: the offset is only advanced after a successful send, so a retry repeats the identical (pointer, length) pair. csync2 uses a blocking socket -- O_NONBLOCK is never set on conn_fd_in/conn_fd_out -- so the retry cannot spin: the next call blocks in the underlying read()/write(). . The non-TLS branch of WRITE() already loops over partial writes and retries on EINTR; this makes the TLS branch behave the same way. Author: w4zu Origin: other Forwarded: https://github.com/LINBIT/csync2/issues/45 Last-Update: 2026-09-17 --- a/conn.c +++ b/conn.c @@ -471,7 +471,13 @@ { #ifdef HAVE_LIBGNUTLS if (csync_conn_usessl) - return gnutls_record_recv(conn_tls_session, buf, count); + { + ssize_t rc; + do { + rc = gnutls_record_recv(conn_tls_session, buf, count); + } while (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED); + return (int)rc; + } else #endif return read(conn_fd_in, buf, count); @@ -482,8 +488,21 @@ static int n, total; #ifdef HAVE_LIBGNUTLS - if (csync_conn_usessl) - return gnutls_record_send(conn_tls_session, buf, count); + if (csync_conn_usessl) { + size_t sent = 0; + while (sent < count) { + ssize_t rc = gnutls_record_send(conn_tls_session, + ((const char *)buf) + sent, count - sent); + if (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED) + continue; + if (rc < 0) + return sent ? (int)sent : (int)rc; + if (rc == 0) + break; + sent += (size_t)rc; + } + return (int)sent; + } else #endif {