]> git.ipfire.org Git - thirdparty/samba.git/commitdiff
vfs_aio_ratelimit: add cluster-wide rate limiting via daemon aggregation
authorAvan Thakkar <athakkar@redhat.com>
Fri, 13 Feb 2026 10:51:37 +0000 (16:21 +0530)
committerGünther Deschner <gd@samba.org>
Wed, 22 Jul 2026 15:50:41 +0000 (15:50 +0000)
Add cluster coordination using a daemon-based architecture. Each node runs
a ratelimitd daemon that aggregates local smbd activity and broadcasts node
summaries via CTDB. VFS processes receive summaries and dynamically distribute
the global rate limit fairly among all active processes.

Network message complexity: O(M²) where M is the number of nodes.

Signed-off-by: Avan Thakkar <athakkar@redhat.com>
Reviewed-by: Anoop C S <anoopcs@samba.org>
Reviewed-by: Guenther Deschner <gd@samba.org>
librpc/idl/messaging.idl
source3/include/ratelimit_protocol.h [new file with mode: 0644]
source3/modules/vfs_aio_ratelimit.c
source3/smbd/server.c
source3/smbd/smbd_ratelimitd.c [new file with mode: 0644]
source3/smbd/smbd_ratelimitd.h [new file with mode: 0644]
source3/wscript_build

index 6445cd2f4fcd452cfb78744f0b495b9c0f249657..6c825a6092d32266a19642727bd61e53d24adb24 100644 (file)
@@ -163,6 +163,13 @@ interface messaging
                MSG_DSDB_PWD_LOG                = 0x0802,
                MSG_GROUP_LOG                   = 0x0803,
 
+               /*
+                * VFS aio_ratelimit cluster coordination messages
+                * Node-level aggregation via ratelimitd daemon
+                */
+               MSG_VFS_AIO_RATELIMIT_READ_NODE_SUMMARY = 0x0900,
+               MSG_VFS_AIO_RATELIMIT_WRITE_NODE_SUMMARY = 0x0901,
+
                /* dbwrap messages 4001-4999 (0x0FA0 - 0x1387) */
                /* MSG_DBWRAP_TDB2_CHANGES              = 4001, */
                /* MSG_DBWRAP_G_LOCK_RETRY              = 4002, */
diff --git a/source3/include/ratelimit_protocol.h b/source3/include/ratelimit_protocol.h
new file mode 100644 (file)
index 0000000..37753e9
--- /dev/null
@@ -0,0 +1,83 @@
+/* Rate limiting protocol definitions for smbd ↔ ratelimitd communication */
+
+#ifndef __RATELIMIT_PROTOCOL_H__
+#define __RATELIMIT_PROTOCOL_H__
+
+#include "lib/util/time.h"
+#include "messages.h"
+
+#define RATELIMITD_SOCKET_NAME "ratelimitd.sock"
+
+/* Protocol version for compatibility checking */
+#define RATELIMIT_PROTOCOL_VERSION 2
+#define RATELIMIT_SHARE_NAME_LEN 256
+
+static inline uint64_t time_now_usec(void)
+{
+       struct timespec ts;
+
+       clock_gettime_mono(&ts);
+       return (uint64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
+}
+
+/* Operation types for rate limiting */
+enum ratelimit_operation {
+       RATELIMIT_OP_INVALID = 0,
+       RATELIMIT_OP_READ = 1,
+       RATELIMIT_OP_WRITE = 2,
+};
+
+/* Activity report from VFS process to local daemon via Unix socket */
+struct ratelimit_activity_report {
+       uint64_t timestamp_usec;
+       int64_t recent_iops;
+       uint32_t protocol_version;
+       uint32_t operation;
+       int32_t pid;
+       uint32_t inflight_ios;
+       char share_name[RATELIMIT_SHARE_NAME_LEN];
+} PACKED_STRUCT;
+
+/* Node summary broadcast from daemon to cluster */
+struct ratelimit_node_summary {
+       uint32_t vnn;
+       int32_t process_count;
+       uint64_t timestamp_usec;
+       char share_name[RATELIMIT_SHARE_NAME_LEN];
+} PACKED_STRUCT;
+
+static inline uint32_t ratelimit_msg_type_summary(uint32_t operation)
+{
+       switch (operation) {
+       case RATELIMIT_OP_READ:
+               return MSG_VFS_AIO_RATELIMIT_READ_NODE_SUMMARY;
+       case RATELIMIT_OP_WRITE:
+               return MSG_VFS_AIO_RATELIMIT_WRITE_NODE_SUMMARY;
+       default:
+               return 0;
+       }
+}
+
+static inline uint32_t ratelimit_op_from_string(const char *op_str)
+{
+       if (strcmp(op_str, "read") == 0) {
+               return RATELIMIT_OP_READ;
+       } else if (strcmp(op_str, "write") == 0) {
+               return RATELIMIT_OP_WRITE;
+       }
+       return RATELIMIT_OP_INVALID;
+}
+
+static inline const char *ratelimit_op_to_string(uint32_t operation)
+{
+       switch (operation) {
+       case RATELIMIT_OP_READ:
+               return "read";
+       case RATELIMIT_OP_WRITE:
+               return "write";
+       default:
+               return "invalid";
+       }
+}
+
+#endif /* __RATELIMIT_PROTOCOL_H__ */
index 3ac1aec34e12f438b7a45beb65c3328821a6d603..7c0a643c4362fa45333c11534ef72064d466df84 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Asynchronous I/O rate-limiting VFS module.
+ * Asynchronous I/O rate-limiting VFS module with cluster-wide coordination.
  *
  * Copyright (c) 2025 Shachar Sharon <ssharon@redhat.com>
  * Copyright (c) 2025 Avan Thakkar <athakkar@redhat.com>
@@ -19,7 +19,7 @@
  */
 
 /*
-  Token-base rate-limiter using Samba's VFS stack-able module. For each samba
+  Token-based rate-limiter using Samba's VFS stack-able module. For each samba
   share a user may define READ/WRITE thresholds in terms of IOPS or BYTES
   per-second. If one of those thresholds is exceeded along the asynchronous
   I/O path, a delay is injected before sending back a reply to the caller,
   persisted to a local TDB, allowing limits to be enforced consistently
   across client reconnects and smbd restarts.
 
+  CLUSTER-WIDE COORDINATION VIA DAEMON:
+  In cluster mode, each smbd process reports its activity to a local
+  ratelimitd daemon via Unix socket. The daemon aggregates activity from
+  all smbd processes on the node and broadcasts node-level summaries to
+  other nodes via Samba's messaging system. This reduces network message
+  volume from O(N²) to O(M²) where N=processes and M=nodes.
+
+  Processes receive node summaries and dynamically recalculate their local
+  rate limits to ensure the global limit is enforced cluster-wide. The
+  global limit is distributed equally among all active smbd processes
+  performing I/O (per-process distribution model).
+
   An example to smb.conf segment (zero value implies ignore-this-option):
 
   [share]
 #include "lib/util/time.h"
 #include "lib/util/tevent_unix.h"
 #include "lib/util/util_tdb.h"
+#include "lib/util/server_id.h"
 #include "tdb.h"
+#include "messages.h"
 #include "system/filesys.h"
+#include "lib/global_contexts.h"
+#include "ratelimit_protocol.h"
 
 #undef DBGC_CLASS
 #define DBGC_CLASS DBGC_VFS
 /* TDB schema version */
 #define RATELIMIT_TDB_VERSION 1
 
+/* Activity tracking intervals */
+#define ACTIVITY_BROADCAST_INTERVAL_US (1000000L)
+#define ACTIVITY_TIMEOUT_US (5000000L)
+
+/* Initial capacity for node tracking array (grows dynamically) */
+#define INITIAL_TRACKED_CAPACITY 16
+
 static unsigned int ref_count = 0;
 static TDB_CONTEXT *ratelimit_tdb;
 
@@ -95,8 +118,18 @@ struct ratelimit_tdb_record {
        uint8_t reserved[64 - (8 + 4 + 4)];
 } PACKED_STRUCT;
 
+/* Node-level tracking */
+struct node_count {
+       uint32_t vnn;
+       int32_t process_count;
+       uint64_t last_seen_us;
+       bool is_active;
+};
+
 /* Token-based rate-limiter control state using a token-bucket. */
 struct ratelimiter {
+       struct ratelimiter *prev, *next;
+
        const char *op;
        uint64_t last_usec;
        uint64_t last_save_usec;
@@ -104,8 +137,10 @@ struct ratelimiter {
        float bytes_tokens;
        int64_t iops_total;
        int64_t bytes_total;
-       int64_t iops_limit;
-       int64_t bw_limit;
+       int64_t global_iops_limit;
+       int64_t global_bw_limit;
+       int64_t local_iops_limit;
+       int64_t local_bw_limit;
        float iops_capacity;
        float bytes_capacity;
 
@@ -115,7 +150,31 @@ struct ratelimiter {
         * are reconfigured in the future (e.g. reload, per-client limits).
         */
        float burst_mult;
+
        int snum;
+       char share_name[RATELIMIT_SHARE_NAME_LEN];
+
+       /* Cluster coordination via daemon */
+       bool cluster_mode;
+       struct messaging_context *msg_ctx;
+       struct server_id my_server_id;
+       int daemon_sock;
+       uint64_t last_report_to_daemon_us;
+
+       /* Activity tracking */
+       uint32_t inflight_ios;
+       int64_t recent_iops;
+
+       struct node_count *node_counts;
+       int num_tracked_nodes;
+       int max_tracked_nodes;
+
+       int num_active_processes;
+
+       /* Statistics */
+       uint64_t total_reports_sent;
+       uint64_t total_summaries_received;
+       uint64_t total_limit_recalcs;
 };
 
 /* In-memory rate-limiting entry per connection */
@@ -124,12 +183,455 @@ struct vfs_aio_ratelimit_config {
        struct ratelimiter wr_ratelimiter;
 };
 
-static uint64_t time_now_usec(void)
+/*
+ * Process-global dispatch lists, one per op.
+ * Samba's messaging layer delivers a given msg_type to only one registered
+ * callback per process, so we register once per op per process and
+ * fan out to every ratelimiter on the matching list ourselves.
+ */
+static struct ratelimiter *read_ratelimiters_list = NULL;
+static struct ratelimiter *write_ratelimiters_list = NULL;
+
+static struct ratelimiter **ratelimiter_dispatch_list(const char *op)
+{
+       if (strcmp(op, "read") == 0) {
+               return &read_ratelimiters_list;
+       }
+       return &write_ratelimiters_list;
+}
+
+static bool ensure_node_count_capacity(struct ratelimiter *rl)
 {
-       struct timespec ts;
+       struct node_count *new_array;
+       int new_max;
+
+       if (rl->num_tracked_nodes < rl->max_tracked_nodes) {
+               return true;
+       }
+
+       new_max = rl->max_tracked_nodes * 2;
 
-       clock_gettime_mono(&ts);
-       return (uint64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
+       new_array = talloc_realloc_zero(talloc_parent(rl->node_counts),
+                                       rl->node_counts,
+                                       struct node_count,
+                                       new_max);
+       if (new_array == NULL) {
+               DBG_ERR("[%s snum:%d %s] Failed to grow node_counts: %d -> "
+                       "%d\n",
+                       MODULE_NAME,
+                       rl->snum,
+                       rl->op,
+                       rl->max_tracked_nodes,
+                       new_max);
+               return false;
+       }
+
+       rl->node_counts = new_array;
+       rl->max_tracked_nodes = new_max;
+
+       return true;
+}
+
+static void update_node_count(struct ratelimiter *rl,
+                             uint32_t vnn,
+                             int32_t process_count)
+{
+       struct node_count *nc = NULL;
+       uint64_t now = time_now_usec();
+       int i;
+
+       /* Find existing node entry */
+       for (i = 0; i < rl->num_tracked_nodes; i++) {
+               if (rl->node_counts[i].vnn == vnn) {
+                       nc = &rl->node_counts[i];
+                       break;
+               }
+       }
+
+       if (nc == NULL) {
+               if (!ensure_node_count_capacity(rl)) {
+                       return;
+               }
+
+               nc = &rl->node_counts[rl->num_tracked_nodes++];
+               nc->vnn = vnn;
+               nc->is_active = false;
+       }
+
+       nc->process_count = process_count;
+       nc->last_seen_us = now;
+       nc->is_active = (process_count > 0);
+
+       DBG_DEBUG("[%s snum:%d %s] Updated node vnn=%u: count=%d active=%d\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 vnn,
+                 process_count,
+                 nc->is_active);
+}
+
+static int count_active_processes(struct ratelimiter *rl)
+{
+       uint64_t now = time_now_usec();
+       int total_count = 0;
+       int timed_out = 0;
+       bool i_am_active;
+       int i;
+
+       i_am_active = (rl->inflight_ios > 0) || (rl->recent_iops > 0);
+
+       if (i_am_active) {
+               total_count = 1;
+               DBG_DEBUG("[%s snum:%d %s] I am ACTIVE "
+                         "(inflight=%u recent=%" PRId64 ")\n",
+                         MODULE_NAME,
+                         rl->snum,
+                         rl->op,
+                         rl->inflight_ios,
+                         rl->recent_iops);
+       } else {
+               DBG_DEBUG("[%s snum:%d %s] I am IDLE "
+                         "(inflight=%u recent=%" PRId64 ")\n",
+                         MODULE_NAME,
+                         rl->snum,
+                         rl->op,
+                         rl->inflight_ios,
+                         rl->recent_iops);
+       }
+
+       for (i = 0; i < rl->num_tracked_nodes; i++) {
+               struct node_count *nc = &rl->node_counts[i];
+               uint64_t age_us = now - nc->last_seen_us;
+
+               /* Check timeout */
+               if (age_us > ACTIVITY_TIMEOUT_US) {
+                       if (nc->is_active) {
+                               DBG_NOTICE("[%s snum:%d %s] Node vnn=%u TIMED "
+                                          "OUT "
+                                          "(age=%" PRIu64 " ms)\n",
+                                          MODULE_NAME,
+                                          rl->snum,
+                                          rl->op,
+                                          nc->vnn,
+                                          age_us / 1000);
+                               nc->is_active = false;
+                               timed_out++;
+                       }
+                       continue;
+               }
+
+               if (!nc->is_active || nc->process_count <= 0) {
+                       continue;
+               }
+
+               if (nc->vnn == rl->my_server_id.vnn) {
+                       total_count += (nc->process_count - 1);
+               } else {
+                       total_count += nc->process_count;
+               }
+       }
+
+       DBG_DEBUG("[%s snum:%d %s] Total=%d active processes "
+                 "(timed_out=%d)\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 MAX(total_count, 1),
+                 timed_out);
+
+       return MAX(total_count, 1);
+}
+
+/* Recalculate local limits based on active processes */
+static void recalculate_local_limits(struct ratelimiter *rl)
+{
+       int new_active = count_active_processes(rl);
+       int64_t old_local_iops;
+       int64_t old_local_bw;
+       float old_iops_cap;
+       float old_bytes_cap;
+       float old_iops_tokens;
+       float old_bytes_tokens;
+
+       if (new_active == rl->num_active_processes) {
+               return;
+       }
+
+       old_local_iops = rl->local_iops_limit;
+       old_local_bw = rl->local_bw_limit;
+       old_iops_cap = rl->iops_capacity;
+       old_bytes_cap = rl->bytes_capacity;
+
+       DBG_DEBUG("[%s snum:%d %s] *** RECALCULATING LIMITS *** "
+                 "Active processes changed: %d -> %d\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 rl->num_active_processes,
+                 new_active);
+
+       rl->num_active_processes = new_active;
+       rl->total_limit_recalcs++;
+
+       rl->local_iops_limit = (rl->global_iops_limit > 0)
+                                      ? MAX(rl->global_iops_limit /
+                                                    new_active,
+                                            1)
+                                      : 0;
+       rl->local_bw_limit = (rl->global_bw_limit > 0)
+                                    ? MAX(rl->global_bw_limit / new_active, 1)
+                                    : 0;
+
+       rl->iops_capacity = (float)rl->local_iops_limit * rl->burst_mult;
+       rl->bytes_capacity = (float)rl->local_bw_limit * rl->burst_mult;
+
+       old_iops_tokens = rl->iops_tokens;
+       old_bytes_tokens = rl->bytes_tokens;
+
+       rl->iops_tokens = MIN(rl->iops_tokens, rl->iops_capacity);
+       rl->bytes_tokens = MIN(rl->bytes_tokens, rl->bytes_capacity);
+
+       DBG_DEBUG("[%s snum:%d %s] *** LIMITS UPDATED ***\n"
+                 "  IOPS:  local_limit %" PRId64 " -> %" PRId64
+                 " (global=%" PRId64 ")\n"
+                 "         capacity %.2f -> %.2f, tokens %.2f -> %.2f\n"
+                 "  BW:    local_limit %" PRId64 " -> %" PRId64
+                 " (global=%" PRId64 ")\n"
+                 "         capacity %.2f -> %.2f, tokens %.2f -> %.2f\n"
+                 "  Active processes: %d, Total recalcs: %" PRIu64 "\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 old_local_iops,
+                 rl->local_iops_limit,
+                 rl->global_iops_limit,
+                 old_iops_cap,
+                 rl->iops_capacity,
+                 old_iops_tokens,
+                 rl->iops_tokens,
+                 old_local_bw,
+                 rl->local_bw_limit,
+                 rl->global_bw_limit,
+                 old_bytes_cap,
+                 rl->bytes_capacity,
+                 old_bytes_tokens,
+                 rl->bytes_tokens,
+                 rl->num_active_processes,
+                 rl->total_limit_recalcs);
+}
+
+static int connect_to_ratelimitd(void)
+{
+       int sock;
+       struct sockaddr_un addr;
+       char *socket_path = NULL;
+       int ret;
+
+       socket_path = state_path(talloc_tos(), RATELIMITD_SOCKET_NAME);
+       if (socket_path == NULL) {
+               DBG_ERR("[%s] Failed to allocate socket path\n", MODULE_NAME);
+               return -1;
+       }
+
+       sock = socket(AF_UNIX, SOCK_DGRAM, 0);
+       if (sock < 0) {
+               DBG_ERR("[%s] socket() failed: %s\n",
+                       MODULE_NAME,
+                       strerror(errno));
+               TALLOC_FREE(socket_path);
+               return -1;
+       }
+
+       memset(&addr, 0, sizeof(addr));
+       addr.sun_family = AF_UNIX;
+       strlcpy(addr.sun_path, socket_path, sizeof(addr.sun_path));
+
+       ret = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
+       if (ret < 0) {
+               DBG_WARNING(
+                       "[%s] connect() to ratelimitd failed: %s (path=%s)\n",
+                       MODULE_NAME,
+                       strerror(errno),
+                       socket_path);
+               close(sock);
+               TALLOC_FREE(socket_path);
+               return -1;
+       }
+
+       DBG_DEBUG("[%s] Connected to ratelimitd: socket=%s\n",
+                 MODULE_NAME,
+                 socket_path);
+
+       TALLOC_FREE(socket_path);
+       return sock;
+}
+
+static void report_to_daemon(struct ratelimiter *rl)
+{
+       struct ratelimit_activity_report report = {0};
+       ssize_t ret;
+
+       if (rl->daemon_sock < 0) {
+               DBG_DEBUG("[%s snum:%d %s] report_to_daemon: daemon_sock "
+                         "invalid, aborting\n",
+                         MODULE_NAME,
+                         rl->snum,
+                         rl->op);
+               return;
+       }
+
+       /* Build report */
+       report.protocol_version = RATELIMIT_PROTOCOL_VERSION;
+       report.pid = (int32_t)getpid();
+       strlcpy(report.share_name, rl->share_name, sizeof(report.share_name));
+       report.operation = ratelimit_op_from_string(rl->op);
+       report.recent_iops = rl->recent_iops;
+       report.inflight_ios = rl->inflight_ios;
+       report.timestamp_usec = time_now_usec();
+
+       /* Send to daemon */
+       ret = send(rl->daemon_sock, &report, sizeof(report), MSG_DONTWAIT);
+
+       if (ret != sizeof(report)) {
+               DBG_DEBUG("[%s snum:%d %s] report_to_daemon: send failed "
+                         "ret=%zd expected=%zu errno=%d\n",
+                         MODULE_NAME,
+                         rl->snum,
+                         rl->op,
+                         ret,
+                         sizeof(report),
+                         errno);
+
+               if (errno != EAGAIN && errno != EWOULDBLOCK) {
+                       DBG_WARNING("[%s snum:%d %s] Failed to report to "
+                                   "daemon: %s\n",
+                                   MODULE_NAME,
+                                   rl->snum,
+                                   rl->op,
+                                   strerror(errno));
+
+                       DBG_DEBUG("[%s snum:%d %s] report_to_daemon: "
+                                 "attempting reconnect\n",
+                                 MODULE_NAME,
+                                 rl->snum,
+                                 rl->op);
+
+                       close(rl->daemon_sock);
+                       rl->daemon_sock = connect_to_ratelimitd();
+
+                       if (rl->daemon_sock < 0) {
+                               DBG_ERR("[%s snum:%d %s] Reconnect failed, "
+                                       "cluster coordination lost\n",
+                                       MODULE_NAME,
+                                       rl->snum,
+                                       rl->op);
+                       }
+               } else {
+                       DBG_DEBUG("[%s snum:%d %s] report_to_daemon: "
+                                 "EAGAIN/EWOULDBLOCK - "
+                                 "buffer full, dropping report\n",
+                                 MODULE_NAME,
+                                 rl->snum,
+                                 rl->op);
+               }
+               return;
+       }
+
+       DBG_DEBUG("[%s snum:%d %s] Reported to daemon: "
+                 "recent_iops=%" PRId64 " inflight=%u total_sent=%" PRIu64
+                 "\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 rl->recent_iops,
+                 rl->inflight_ios,
+                 rl->total_reports_sent + 1);
+
+       rl->total_reports_sent++;
+
+       /* Reset recent counter after report */
+       rl->recent_iops = 0;
+       rl->last_report_to_daemon_us = time_now_usec();
+}
+
+/*
+ * Apply one received node summary to a single ratelimiter that is known
+ * to match (share_name already checked by the caller).
+ */
+static void handle_node_summary_apply(
+       struct ratelimiter *rl,
+       const struct ratelimit_node_summary *summary)
+{
+       int old_active;
+
+       DBG_DEBUG("[%s snum:%d %s] Received node summary from vnn=%u: "
+                 "process_count=%d (summaries_received=%" PRIu64 ")\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 summary->vnn,
+                 summary->process_count,
+                 rl->total_summaries_received + 1);
+
+       rl->total_summaries_received++;
+
+       old_active = rl->num_active_processes;
+
+       update_node_count(rl, summary->vnn, summary->process_count);
+
+       recalculate_local_limits(rl);
+
+       DBG_DEBUG("[%s snum:%d %s] handle_node_summary_apply: "
+                 "after update num_active=%d (was %d)\n",
+                 MODULE_NAME,
+                 rl->snum,
+                 rl->op,
+                 rl->num_active_processes,
+                 old_active);
+}
+
+/*
+ * Registered once per op per process.
+ */
+static void handle_node_summary_dispatch(struct messaging_context *msg_ctx,
+                                        void *private_data,
+                                        uint32_t msg_type,
+                                        struct server_id server_id,
+                                        DATA_BLOB *data)
+{
+       struct ratelimiter *list;
+       struct ratelimiter *rl;
+       struct ratelimit_node_summary summary;
+
+       if (data->length != sizeof(struct ratelimit_node_summary)) {
+               DBG_ERR("[%s] Invalid node summary size %zu (expected %zu)\n",
+                       MODULE_NAME,
+                       data->length,
+                       sizeof(struct ratelimit_node_summary));
+               return;
+       }
+
+       memcpy(&summary, data->data, sizeof(summary));
+       summary.share_name[sizeof(summary.share_name) - 1] = '\0';
+
+       list = (msg_type == MSG_VFS_AIO_RATELIMIT_READ_NODE_SUMMARY)
+                      ? read_ratelimiters_list
+                      : write_ratelimiters_list;
+
+       for (rl = list; rl != NULL; rl = rl->next) {
+               if (strcmp(summary.share_name, rl->share_name) != 0) {
+                       DBG_DEBUG("[%s snum:%d %s] Ignoring summary for "
+                                 "share=%s from vnn=%u\n",
+                                 MODULE_NAME,
+                                 rl->snum,
+                                 rl->op,
+                                 summary.share_name,
+                                 summary.vnn);
+                       continue;
+               }
+               handle_node_summary_apply(rl, &summary);
+       }
 }
 
 static bool ratelimit_tdb_check_version(void)
@@ -342,26 +844,172 @@ static void ratelimit_load_tdb(struct ratelimiter *rl)
        TALLOC_FREE(key.dptr);
 }
 
-static void ratelimiter_init(struct ratelimiter *rl,
+static void ratelimiter_cleanup(struct ratelimiter *rl)
+{
+       if (rl == NULL) {
+               return;
+       }
+
+       if (rl->cluster_mode) {
+               struct ratelimiter **list = ratelimiter_dispatch_list(rl->op);
+
+               DLIST_REMOVE(*list, rl);
+
+               if (*list == NULL && rl->msg_ctx != NULL) {
+                       /*
+                        * Last share for this op in this process -
+                        * deregister the shared dispatcher.
+                        */
+                       messaging_deregister(rl->msg_ctx,
+                                            ratelimit_msg_type_summary(
+                                                    ratelimit_op_from_string(
+                                                            rl->op)),
+                                            NULL);
+               }
+       }
+
+       /* Close daemon connection */
+       if (rl->daemon_sock >= 0) {
+               close(rl->daemon_sock);
+               rl->daemon_sock = -1;
+       }
+
+       /* Free node tracking array */
+       TALLOC_FREE(rl->node_counts);
+       rl->num_tracked_nodes = 0;
+       rl->max_tracked_nodes = 0;
+}
+
+static void ratelimiter_init_local_only(struct ratelimiter *rl)
+{
+       rl->num_active_processes = 1;
+       rl->local_iops_limit = rl->global_iops_limit;
+       rl->local_bw_limit = rl->global_bw_limit;
+       rl->daemon_sock = -1;
+       rl->node_counts = NULL;
+       rl->num_tracked_nodes = 0;
+       rl->max_tracked_nodes = 0;
+
+       DBG_NOTICE("[%s snum:%d %s] Cluster mode DISABLED - using "
+                  "per-node limits "
+                  "samba_clustering=%s\n",
+                  MODULE_NAME,
+                  rl->snum,
+                  rl->op,
+                  rl->cluster_mode ? "enabled" : "disabled");
+}
+
+static void ratelimiter_init(TALLOC_CTX *mem_ctx,
+                            struct ratelimiter *rl,
                             int snum,
                             const char *op,
                             int64_t iops_limit,
                             int64_t bw_limit,
                             float burst_mult)
 {
+       const struct loadparm_substitution
+               *lp_sub = loadparm_s3_global_substitution();
+       const char *servicename = NULL;
+
        ZERO_STRUCTP(rl);
        rl->op = op;
        rl->snum = snum;
 
-       rl->iops_limit = iops_limit;
-       rl->bw_limit = bw_limit;
+       servicename = lp_servicename(talloc_tos(), lp_sub, snum);
+       if (servicename != NULL) {
+               strlcpy(rl->share_name, servicename, sizeof(rl->share_name));
+       }
+
+       /* Store both global and local limits */
+       rl->global_iops_limit = iops_limit;
+       rl->global_bw_limit = bw_limit;
        rl->burst_mult = burst_mult;
 
        rl->iops_total = 0;
        rl->bytes_total = 0;
 
-       rl->iops_capacity = (float)(iops_limit)*burst_mult;
-       rl->bytes_capacity = (float)(bw_limit)*burst_mult;
+       rl->cluster_mode = lp_clustering();
+
+       if (rl->cluster_mode) {
+               rl->msg_ctx = global_messaging_context();
+               rl->my_server_id = messaging_server_id(rl->msg_ctx);
+
+               /* Start with single-process assumption */
+               rl->num_active_processes = 1;
+               rl->local_iops_limit = iops_limit;
+               rl->local_bw_limit = bw_limit;
+
+               /* Connect to daemon */
+               rl->daemon_sock = connect_to_ratelimitd();
+               if (rl->daemon_sock < 0) {
+                       DBG_WARNING("[%s snum:%d %s] Failed to connect to "
+                                   "ratelimitd, "
+                                   "disabling cluster mode\n",
+                                   MODULE_NAME,
+                                   rl->snum,
+                                   rl->op);
+                       rl->msg_ctx = NULL;
+                       rl->cluster_mode = false;
+                       ratelimiter_init_local_only(rl);
+               } else {
+                       rl->max_tracked_nodes = INITIAL_TRACKED_CAPACITY;
+                       rl->num_tracked_nodes = 0;
+                       rl->node_counts = talloc_zero_array(
+                               mem_ctx,
+                               struct node_count,
+                               rl->max_tracked_nodes);
+                       if (rl->node_counts == NULL) {
+                               DBG_ERR("[%s snum:%d %s] Failed to allocate "
+                                       "node "
+                                       "tracking\n",
+                                       MODULE_NAME,
+                                       rl->snum,
+                                       rl->op);
+                               close(rl->daemon_sock);
+                               rl->msg_ctx = NULL;
+                               rl->cluster_mode = false;
+                               ratelimiter_init_local_only(rl);
+                       } else {
+                               struct ratelimiter **list =
+                                       ratelimiter_dispatch_list(rl->op);
+                               bool need_register = (*list == NULL);
+
+                               if (need_register) {
+                                       /*
+                                        * First share for this op in this
+                                        * process, so register the shared
+                                        * dispatcher once. Samba's messaging
+                                        * layer only delivers to one callback
+                                        * per msg_type per process.
+                                        */
+                                       messaging_register(
+                                               rl->msg_ctx,
+                                               NULL,
+                                               ratelimit_msg_type_summary(
+                                                       ratelimit_op_from_string(
+                                                               rl->op)),
+                                               handle_node_summary_dispatch);
+                               }
+
+                               DLIST_ADD(*list, rl);
+
+                               DBG_NOTICE(
+                                       "[%s snum:%d %s] Cluster mode enabled "
+                                       "via daemon (capacity=%d, "
+                                       "registered_dispatcher=%s)\n",
+                                       MODULE_NAME,
+                                       rl->snum,
+                                       rl->op,
+                                       rl->max_tracked_nodes,
+                                       need_register ? "yes" : "no");
+                       }
+               }
+       } else {
+               ratelimiter_init_local_only(rl);
+       }
+
+       rl->iops_capacity = (float)rl->local_iops_limit * burst_mult;
+       rl->bytes_capacity = (float)rl->local_bw_limit * burst_mult;
 
        rl->last_usec = 0;
        rl->last_save_usec = rl->last_usec;
@@ -371,20 +1019,21 @@ static void ratelimiter_init(struct ratelimiter *rl,
        /* Load from global TDB if available */
        ratelimit_load_tdb(rl);
 
-       DBG_DEBUG("[%s snum:%d %s] init ratelimiter:"
-                 " iops_limit=%" PRId64 " bw_limit=%" PRId64
-                 " burst_mult=%.2f\n",
+       DBG_DEBUG("[%s snum:%d %s] Initialized ratelimiter: "
+                 "global_iops_limit=%" PRId64 " global_bw_limit=%" PRId64
+                 " burst_mult=%.2f cluster_mode=%s\n",
                  MODULE_NAME,
                  rl->snum,
                  rl->op,
-                 rl->iops_limit,
-                 rl->bw_limit,
-                 rl->burst_mult);
+                 rl->global_iops_limit,
+                 rl->global_bw_limit,
+                 rl->burst_mult,
+                 rl->cluster_mode ? "yes" : "no");
 }
 
 static bool ratelimiter_enabled(const struct ratelimiter *rl)
 {
-       return (rl->iops_limit > 0) || (rl->bw_limit > 0);
+       return (rl->local_iops_limit > 0) || (rl->local_bw_limit > 0);
 }
 
 static float ratelimiter_calc_refill(uint64_t elapsed,
@@ -429,23 +1078,23 @@ static void ratelimiter_refill(struct ratelimiter *rl)
 
        elapsed = now - rl->last_usec;
 
-       if (rl->iops_limit > 0) {
+       if (rl->local_iops_limit > 0) {
                float refill;
 
                refill = ratelimiter_calc_refill(elapsed,
                                                 rl->iops_capacity,
-                                                rl->iops_limit);
+                                                rl->local_iops_limit);
 
                rl->iops_tokens = MIN(rl->iops_tokens + refill,
                                      rl->iops_capacity);
        }
 
-       if (rl->bw_limit > 0) {
+       if (rl->local_bw_limit > 0) {
                float refill;
 
                refill = ratelimiter_calc_refill(elapsed,
                                                 rl->bytes_capacity,
-                                                rl->bw_limit);
+                                                rl->local_bw_limit);
 
                rl->bytes_tokens = MIN(rl->bytes_tokens + refill,
                                       rl->bytes_capacity);
@@ -482,23 +1131,30 @@ static uint32_t ratelimiter_pre_io(struct ratelimiter *rl, int64_t nbytes)
        /* Refill tokens based on elapsed time */
        ratelimiter_refill(rl);
 
+       /* Track in-flight I/O for cluster coordination */
+       if (rl->cluster_mode) {
+               rl->inflight_ios++;
+       }
+
        /* Consume tokens for this operation */
-       if (rl->iops_limit > 0) {
+       if (rl->local_iops_limit > 0) {
                rl->iops_tokens -= 1.0f;
                if (rl->iops_tokens < 0.0f) {
                        iops_deficit = -rl->iops_tokens;
                }
        }
 
-       if (rl->bw_limit > 0) {
+       if (rl->local_bw_limit > 0) {
                rl->bytes_tokens -= (float)nbytes;
                if (rl->bytes_tokens < 0.0f) {
                        bw_deficit = -rl->bytes_tokens;
                }
        }
 
-       delay_usec = ratelimiter_deficit_to_delay(iops_deficit, rl->iops_limit);
-       bw_delay = ratelimiter_deficit_to_delay(bw_deficit, rl->bw_limit);
+       delay_usec = ratelimiter_deficit_to_delay(iops_deficit,
+                                                 rl->local_iops_limit);
+       bw_delay = ratelimiter_deficit_to_delay(bw_deficit,
+                                               rl->local_bw_limit);
 
        if (bw_delay > delay_usec) {
                delay_usec = bw_delay;
@@ -506,6 +1162,12 @@ static uint32_t ratelimiter_pre_io(struct ratelimiter *rl, int64_t nbytes)
 
        rl->iops_total += 1;
        rl->bytes_total += nbytes;
+
+       /* Track recent I/O for cluster coordination */
+       if (rl->cluster_mode) {
+               rl->recent_iops++;
+       }
+
        now = time_now_usec();
 
        if ((now - rl->last_save_usec) > SAVE_INTERVAL_USEC) {
@@ -513,14 +1175,24 @@ static uint32_t ratelimiter_pre_io(struct ratelimiter *rl, int64_t nbytes)
                rl->last_save_usec = now;
        }
 
+       /* Report to daemon for cluster coordination */
+       if (rl->cluster_mode && now - rl->last_report_to_daemon_us >
+                                       ACTIVITY_BROADCAST_INTERVAL_US)
+       {
+               report_to_daemon(rl);
+       }
+
        DBG_DEBUG("[%s snum:%d %s] delay_usec=%" PRIu32
-                 " iops_tokens=%.2f bytes_tokens=%.2f\n",
+                 " iops_tokens=%.2f bytes_tokens=%.2f "
+                 "(local limits: iops=%" PRId64 " bw=%" PRId64 ")\n",
                  MODULE_NAME,
                  rl->snum,
                  rl->op,
                  delay_usec,
                  rl->iops_tokens,
-                 rl->bytes_tokens);
+                 rl->bytes_tokens,
+                 rl->local_iops_limit,
+                 rl->local_bw_limit);
 
        return delay_usec;
 }
@@ -529,7 +1201,12 @@ static void ratelimiter_post_io(struct ratelimiter *rl,
                                int64_t nbytes_want,
                                int64_t nbytes_done)
 {
-       if (rl->bw_limit > 0 && nbytes_done < nbytes_want) {
+       /* Update in-flight counter for cluster coordination */
+       if (rl->cluster_mode && rl->inflight_ios > 0) {
+               rl->inflight_ios--;
+       }
+
+       if (rl->local_bw_limit > 0 && nbytes_done < nbytes_want) {
                int64_t unused = nbytes_want - MAX(nbytes_done, (int64_t)0);
 
                rl->bytes_tokens = MIN(rl->bytes_tokens + (float)unused,
@@ -581,7 +1258,7 @@ static uint64_t vfs_aio_ratelimit_lp_parm_bw(int snum,
        }
 
        if (!conv_str_size_error(str, &val)) {
-               DBG_ERR("[%s] invalid value for %s: '%s'\n",
+               DBG_ERR("[%s] Invalid value for %s: '%s'\n",
                        MODULE_NAME,
                        option,
                        str);
@@ -611,7 +1288,8 @@ static void vfs_aio_ratelimit_setup(struct vfs_aio_ratelimit_config *config,
                                                      BURST_MULT_DEF,
                                                      100) / 10.0f;
 
-       ratelimiter_init(&config->rd_ratelimiter,
+       ratelimiter_init(config,
+                        &config->rd_ratelimiter,
                         snum,
                         "read",
                         iops_limit,
@@ -632,7 +1310,8 @@ static void vfs_aio_ratelimit_setup(struct vfs_aio_ratelimit_config *config,
                                                      BURST_MULT_DEF,
                                                      100) / 10.0f;
 
-       ratelimiter_init(&config->wr_ratelimiter,
+       ratelimiter_init(config,
+                        &config->wr_ratelimiter,
                         snum,
                         "write",
                         iops_limit,
@@ -688,8 +1367,7 @@ static int vfs_aio_ratelimit_connect(struct vfs_handle_struct *handle,
 
        ret = vfs_aio_ratelimit_new_config(handle);
        if (ret < 0) {
-               DBG_ERR("[%s] failed to create new config: "
-                       "service=%s snum=%d\n",
+               DBG_ERR("[%s] Failed to create config: service=%s snum=%d\n",
                        MODULE_NAME,
                        service,
                        SNUM(handle->conn));
@@ -715,6 +1393,9 @@ static void vfs_aio_ratelimit_disconnect(struct vfs_handle_struct *handle)
        ratelimit_save_tdb(&config->rd_ratelimiter);
        ratelimit_save_tdb(&config->wr_ratelimiter);
 
+       ratelimiter_cleanup(&config->rd_ratelimiter);
+       ratelimiter_cleanup(&config->wr_ratelimiter);
+
        ref_count--;
 
        if (ref_count == 0 && ratelimit_tdb != NULL) {
index c34bed84aa06ad159fc2eaed504d9d284e81ec29..ce3586f01268fdc6d21943e3449daba0051bc1bd 100644 (file)
@@ -54,6 +54,9 @@
 #include "locking/leases_db.h"
 #include "smbd/notifyd/notifyd.h"
 #include "smbd/smbd_cleanupd.h"
+#ifdef WITH_RATELIMITD
+#include "smbd/smbd_ratelimitd.h"
+#endif
 #include "lib/util/sys_rw.h"
 #include "cleanupdb.h"
 #include "g_lock.h"
@@ -91,6 +94,9 @@ struct smbd_parent_context {
 
        struct server_id cleanupd;
        struct server_id notifyd;
+#ifdef WITH_RATELIMITD
+       struct server_id ratelimitd;
+#endif
 
        struct tevent_timer *cleanup_te;
 
@@ -878,6 +884,189 @@ static void cleanupd_started(struct tevent_req *req)
        }
 }
 
+/**************************************************************************
+ * ratelimitd - cluster-wide rate limit coordination daemon
+ **************************************************************************/
+#ifdef WITH_RATELIMITD
+static void ratelimitd_stopped(struct tevent_req *req)
+{
+       int ret = smbd_ratelimitd_recv(req);
+       if (ret != 0) {
+               DBG_ERR("ratelimitd stopped with error: %s\n", strerror(ret));
+       } else {
+               DBG_NOTICE("ratelimitd stopped cleanly\n");
+       }
+}
+
+static bool smbd_ratelimitd_init(struct messaging_context *msg,
+                                bool interactive,
+                                struct server_id *ppid)
+{
+       struct tevent_context *ev = messaging_tevent_context(msg);
+       struct tevent_req *req;
+       pid_t pid;
+       NTSTATUS status;
+       bool ok;
+
+       /*
+        * Start daemon unconditionally when clustering is enabled.
+        * VFS modules load dynamically, so we can't detect aio_ratelimit
+        * usage at startup. When idle, daemon only waits in event loop
+        * and broadcasts nothing.
+        */
+       if (!lp_clustering()) {
+               DBG_DEBUG("Clustering disabled, not starting ratelimitd\n");
+               return true;
+       }
+
+       if (interactive) {
+               req = smbd_ratelimitd_send(msg, ev, msg);
+               *ppid = messaging_server_id(msg);
+               return (req != NULL);
+       }
+
+       pid = fork();
+       if (pid == -1) {
+               DBG_ERR("ratelimitd fork failed: %s\n", strerror(errno));
+               return false;
+       }
+
+       if (pid != 0) {
+               DBG_DEBUG("Started ratelimitd pid=%d\n", (int)pid);
+
+               if (am_parent != NULL) {
+                       add_child_pid(am_parent, pid);
+               }
+
+               *ppid = pid_to_procid(pid);
+               return true;
+       }
+
+       status = smbd_reinit_after_fork(msg, ev, true);
+       if (!NT_STATUS_IS_OK(status)) {
+               DBG_ERR("reinit_after_fork failed: %s\n", nt_errstr(status));
+               exit(1);
+       }
+
+       process_set_title("smbd-ratelimitd", "ratelimitd");
+       set_remote_machine_name("ratelimitd", false);
+
+       reopen_logs();
+
+       req = smbd_ratelimitd_send(msg, ev, msg);
+       if (req == NULL) {
+               DBG_ERR("smbd_ratelimitd_send failed\n");
+               exit(1);
+       }
+
+       tevent_req_set_callback(req, ratelimitd_stopped, msg);
+
+       ok = tevent_req_poll(req, ev);
+       if (!ok) {
+               DBG_ERR("tevent_req_poll failed: %s\n", strerror(errno));
+       }
+       exit(0);
+}
+
+static void ratelimitd_init_trigger(struct tevent_req *subreq);
+
+struct ratelimitd_init_state {
+       struct tevent_context *ev;
+       struct messaging_context *msg;
+       struct server_id *ppid;
+       bool ok;
+};
+
+static struct tevent_req *ratelimitd_init_send(struct tevent_context *ev,
+                                              TALLOC_CTX *mem_ctx,
+                                              struct messaging_context *msg,
+                                              struct server_id *ppid)
+{
+       struct tevent_req *req = NULL;
+       struct tevent_req *subreq = NULL;
+       struct ratelimitd_init_state *state = NULL;
+
+       req = tevent_req_create(mem_ctx, &state, struct ratelimitd_init_state);
+       if (req == NULL) {
+               return NULL;
+       }
+
+       *state = (struct ratelimitd_init_state){
+               .msg = msg,
+               .ev = ev,
+               .ppid = ppid,
+       };
+
+       subreq = tevent_wakeup_send(state,
+                                   ev,
+                                   tevent_timeval_current_ofs(1, 0));
+       if (tevent_req_nomem(subreq, req)) {
+               return tevent_req_post(req, ev);
+       }
+
+       tevent_req_set_callback(subreq, ratelimitd_init_trigger, req);
+       return req;
+}
+
+static void ratelimitd_init_trigger(struct tevent_req *subreq)
+{
+       struct tevent_req *req = tevent_req_callback_data(subreq,
+                                                         struct tevent_req);
+       struct ratelimitd_init_state *state = tevent_req_data(
+               req, struct ratelimitd_init_state);
+       bool ok;
+
+       DBG_NOTICE("Triggering ratelimitd startup\n");
+
+       ok = tevent_wakeup_recv(subreq);
+       TALLOC_FREE(subreq);
+       if (!ok) {
+               tevent_req_error(req, ENOMEM);
+               return;
+       }
+
+       state->ok = smbd_ratelimitd_init(state->msg, false, state->ppid);
+       if (state->ok) {
+               DBG_NOTICE("ratelimitd restarted\n");
+               tevent_req_done(req);
+               return;
+       }
+
+       DBG_NOTICE("ratelimitd startup failed, rescheduling\n");
+
+       subreq = tevent_wakeup_send(state,
+                                   state->ev,
+                                   tevent_timeval_current_ofs(1, 0));
+       if (tevent_req_nomem(subreq, req)) {
+               DBG_ERR("scheduling ratelimitd restart failed, giving up\n");
+               return;
+       }
+
+       tevent_req_set_callback(subreq, ratelimitd_init_trigger, req);
+       return;
+}
+
+static bool ratelimitd_init_recv(struct tevent_req *req)
+{
+       struct ratelimitd_init_state *state = tevent_req_data(
+               req, struct ratelimitd_init_state);
+
+       return state->ok;
+}
+
+static void ratelimitd_started(struct tevent_req *req)
+{
+       bool ok;
+
+       ok = ratelimitd_init_recv(req);
+       TALLOC_FREE(req);
+       if (!ok) {
+               DBG_ERR("Failed to restart ratelimitd, giving up\n");
+               return;
+       }
+}
+#endif /* WITH_RATELIMITD */
+
 static void remove_child_pid(struct smbd_parent_context *parent,
                             pid_t pid,
                             bool unclean_shutdown,
@@ -946,6 +1135,28 @@ static void remove_child_pid(struct smbd_parent_context *parent,
                return;
        }
 
+#ifdef WITH_RATELIMITD
+       if (pid == procid_to_pid(&parent->ratelimitd)) {
+               struct tevent_req *req;
+               struct tevent_context *ev = messaging_tevent_context(
+                       parent->msg_ctx);
+
+               server_id_set_disconnected(&parent->ratelimitd);
+
+               DBG_WARNING("Restarting ratelimitd\n");
+               req = ratelimitd_init_send(ev,
+                                          parent,
+                                          parent->msg_ctx,
+                                          &parent->ratelimitd);
+               if (req == NULL) {
+                       DBG_ERR("Failed to restart ratelimitd\n");
+                       return;
+               }
+               tevent_req_set_callback(req, ratelimitd_started, parent);
+               return;
+       }
+#endif /* WITH_RATELIMITD */
+
        ok = cleanupdb_store_child(pid, unclean_shutdown);
        if (!ok) {
                DBG_ERR("cleanupdb_store_child failed\n");
@@ -2458,6 +2669,16 @@ quic_disabled:
                exit_daemon("Samba cannot init the cleanupd", EACCES);
        }
 
+#ifdef WITH_RATELIMITD
+       if (!smbd_ratelimitd_init(msg_ctx,
+                                 cmdline_daemon_cfg->interactive,
+                                 &parent->ratelimitd))
+       {
+               DBG_WARNING("ratelimitd init failed, "
+                           "cluster rate limiting will not be available\n");
+       }
+#endif /* WITH_RATELIMITD */
+
        if (!W_ERROR_IS_OK(registry_init_full()))
                exit_daemon("Samba cannot init registry", EACCES);
 
diff --git a/source3/smbd/smbd_ratelimitd.c b/source3/smbd/smbd_ratelimitd.c
new file mode 100644 (file)
index 0000000..04e149e
--- /dev/null
@@ -0,0 +1,578 @@
+/*
+ * Samba rate limiting coordination daemon
+ *
+ * Copyright (c) 2026 Avan Thakkar <athakkar@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "includes.h"
+#include "smbd_ratelimitd.h"
+#include "ratelimit_protocol.h"
+#include "lib/util/time.h"
+#include "lib/util/tevent_ntstatus.h"
+#include "lib/util/tevent_unix.h"
+#include "messages.h"
+#include "librpc/gen_ndr/messaging.h"
+#include "system/filesys.h"
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_VFS
+
+#define MODULE_NAME "ratelimitd"
+
+/* Activity timeout - 5 seconds */
+#define RATELIMITD_ACTIVITY_TIMEOUT_US (5000000L)
+
+/* Broadcast interval - 1 second */
+#define RATELIMITD_BROADCAST_INTERVAL_US (1000000L)
+
+/* Remove processes inactive for more than 1 hour */
+#define INACTIVE_CLEANUP_THRESHOLD_US (3600000000L)
+
+/* Per-process activity tracking */
+struct process_activity {
+       struct process_activity *prev, *next;
+       pid_t pid;
+       char share_name[RATELIMIT_SHARE_NAME_LEN];
+       char op[8];
+       int64_t recent_iops;
+       uint32_t inflight_ios;
+       uint64_t last_seen_usec;
+       bool is_active;
+};
+
+struct smbd_ratelimitd_state {
+       struct tevent_context *ev;
+       struct messaging_context *msg_ctx;
+       int unix_sock;
+       char *socket_path;
+       uint32_t my_vnn;
+
+       struct process_activity *processes;
+
+       struct tevent_timer *broadcast_timer;
+       uint64_t last_broadcast_usec;
+
+       uint64_t total_reports_received;
+       uint64_t total_broadcasts_sent;
+};
+
+static int smbd_ratelimitd_state_destructor(
+       struct smbd_ratelimitd_state *state)
+{
+       int ret;
+
+       if (state->unix_sock != -1) {
+               close(state->unix_sock);
+               state->unix_sock = -1;
+       }
+
+       if (state->socket_path != NULL) {
+               ret = unlink(state->socket_path);
+               if (ret == 0) {
+                       DBG_DEBUG("[%s] Removed socket: %s\n",
+                                 MODULE_NAME,
+                                 state->socket_path);
+               } else if (errno != ENOENT) {
+                       DBG_WARNING("[%s] Failed to remove socket %s: %s\n",
+                                   MODULE_NAME,
+                                   state->socket_path,
+                                   strerror(errno));
+               }
+       }
+
+       return 0;
+}
+
+static struct process_activity *find_process_activity(
+       struct smbd_ratelimitd_state *state,
+       pid_t pid,
+       const char *share_name,
+       const char *op)
+{
+       struct process_activity *proc;
+
+       for (proc = state->processes; proc != NULL; proc = proc->next) {
+               if (proc->pid == pid &&
+                   strcmp(proc->share_name, share_name) == 0 &&
+                   strcmp(proc->op, op) == 0)
+               {
+                       DBG_DEBUG("[%s] find_process_activity: FOUND\n",
+                                 MODULE_NAME);
+                       return proc;
+               }
+       }
+
+       DBG_DEBUG("[%s] find_process_activity: NOT FOUND\n", MODULE_NAME);
+       return NULL;
+}
+
+static void update_process_activity(
+       struct smbd_ratelimitd_state *state,
+       const struct ratelimit_activity_report *report)
+{
+       struct process_activity *proc;
+       const char *op_str;
+       bool is_new = false;
+
+       op_str = ratelimit_op_to_string(report->operation);
+
+       proc = find_process_activity(state,
+                                    (pid_t)report->pid,
+                                    report->share_name,
+                                    op_str);
+
+       if (proc == NULL) {
+               DBG_DEBUG("[%s] update_process_activity: process not found, "
+                         "creating new\n",
+                         MODULE_NAME);
+
+               proc = talloc_zero(state, struct process_activity);
+               if (proc == NULL) {
+                       DBG_ERR("[%s] update_process_activity: failed to "
+                               "allocate process entry\n",
+                               MODULE_NAME);
+                       return;
+               }
+
+               proc->pid = report->pid;
+               strlcpy(proc->share_name,
+                       report->share_name,
+                       sizeof(proc->share_name));
+               strlcpy(proc->op, op_str, sizeof(proc->op));
+               is_new = true;
+
+               DLIST_ADD(state->processes, proc);
+
+               DBG_DEBUG("[%s] NEW process tracked: pid=%d share=%s op=%s\n",
+                         MODULE_NAME,
+                         proc->pid,
+                         proc->share_name,
+                         proc->op);
+       } else {
+               DBG_DEBUG("[%s] update_process_activity: updating existing "
+                         "process\n",
+                         MODULE_NAME);
+       }
+
+       proc->recent_iops = report->recent_iops;
+       proc->inflight_ios = report->inflight_ios;
+       proc->last_seen_usec = report->timestamp_usec;
+       proc->is_active = (report->recent_iops > 0 ||
+                          report->inflight_ios > 0);
+
+       DBG_DEBUG("[%s] Updated: pid=%d share=%s op=%s iops=%" PRId64 " "
+                 "inflight=%u active=%d %s\n",
+                 MODULE_NAME,
+                 proc->pid,
+                 proc->share_name,
+                 proc->op,
+                 proc->recent_iops,
+                 proc->inflight_ios,
+                 proc->is_active,
+                 is_new ? "[NEW]" : "[EXISTING]");
+}
+
+static void handle_unix_socket_read(struct tevent_context *ev,
+                                   struct tevent_fd *fde,
+                                   uint16_t flags,
+                                   void *private_data)
+{
+       struct smbd_ratelimitd_state *state = private_data;
+       struct ratelimit_activity_report report;
+       struct sockaddr_un from;
+       socklen_t fromlen = sizeof(from);
+       ssize_t ret;
+
+       ret = recvfrom(state->unix_sock,
+                      &report,
+                      sizeof(report),
+                      0,
+                      (struct sockaddr *)&from,
+                      &fromlen);
+
+       if (ret < 0) {
+               DBG_ERR("[%s] recvfrom() failed: %s\n",
+                       MODULE_NAME,
+                       strerror(errno));
+               return;
+       }
+
+       if (ret != sizeof(report)) {
+               DBG_ERR("[%s] Short read: got %zd, expected %zu\n",
+                       MODULE_NAME,
+                       ret,
+                       sizeof(report));
+               return;
+       }
+
+       report.share_name[sizeof(report.share_name) - 1] = '\0';
+
+       DBG_DEBUG("[%s] handle_unix_socket_read: received report "
+                 "pid=%d share=%s op=%s recent_iops=%" PRId64 "\n",
+                 MODULE_NAME,
+                 report.pid,
+                 report.share_name,
+                 ratelimit_op_to_string(report.operation),
+                 report.recent_iops);
+
+       if (report.protocol_version != RATELIMIT_PROTOCOL_VERSION) {
+               DBG_ERR("[%s] Protocol mismatch: got %u, expected %u\n",
+                       MODULE_NAME,
+                       report.protocol_version,
+                       RATELIMIT_PROTOCOL_VERSION);
+               return;
+       }
+
+       update_process_activity(state, &report);
+       state->total_reports_received++;
+
+       DBG_DEBUG("[%s] handle_unix_socket_read: DONE total_reports=%" PRIu64
+                 "\n",
+                 MODULE_NAME,
+                 state->total_reports_received);
+}
+
+static void cleanup_inactive_processes(struct smbd_ratelimitd_state *state)
+{
+       uint64_t now = time_now_usec();
+       struct process_activity *proc, *next;
+       unsigned int removed = 0;
+
+       for (proc = state->processes; proc != NULL; proc = next) {
+               uint64_t age_us = now - proc->last_seen_usec;
+               next = proc->next;
+
+               if (!proc->is_active && age_us > INACTIVE_CLEANUP_THRESHOLD_US)
+               {
+                       DLIST_REMOVE(state->processes, proc);
+                       TALLOC_FREE(proc);
+                       removed++;
+               }
+       }
+
+       if (removed > 0) {
+               DBG_NOTICE("[%s] Cleaned up %d inactive processes\n",
+                          MODULE_NAME,
+                          removed);
+       }
+}
+
+static void broadcast_summary(struct smbd_ratelimitd_state *state,
+                             const char *op,
+                             const char *share_name,
+                             int32_t count,
+                             uint64_t timestamp_usec)
+{
+       struct ratelimit_node_summary summary = {0};
+       DATA_BLOB blob;
+
+       if (count <= 0) {
+               return;
+       }
+
+       summary.vnn = state->my_vnn;
+       summary.process_count = count;
+       summary.timestamp_usec = timestamp_usec;
+       strlcpy(summary.share_name, share_name, sizeof(summary.share_name));
+
+       blob = data_blob_const(&summary, sizeof(summary));
+
+       messaging_send_all(state->msg_ctx,
+                          ratelimit_msg_type_summary(
+                                  ratelimit_op_from_string(op)),
+                          blob.data,
+                          blob.length);
+
+       state->total_broadcasts_sent++;
+
+       DBG_DEBUG("[%s] Broadcast: op=%s share=%s count=%d\n",
+                 MODULE_NAME,
+                 op,
+                 share_name,
+                 count);
+}
+
+/*
+ * Broadcast one ratelimit_node_summary per active (snum, op) pair.
+ * Each VFS ratelimiter gets only the count of processes that are
+ * actually doing I/O on a particular share, so it divides the correct
+ * per-share limit.
+ */
+static void broadcast_per_share_summaries(struct smbd_ratelimitd_state *state,
+                                         uint64_t now)
+{
+       struct process_activity *proc, *inner;
+       int broadcasts = 0;
+       int timed_out = 0;
+
+       for (proc = state->processes; proc != NULL; proc = proc->next) {
+               uint64_t age_us = now - proc->last_seen_usec;
+               bool already_done = false;
+               int32_t count = 0;
+
+               if (age_us > RATELIMITD_ACTIVITY_TIMEOUT_US) {
+                       if (proc->is_active) {
+                               DBG_NOTICE("[%s] Process TIMED OUT "
+                                          "(pid=%d share=%s op=%s "
+                                          "age=%" PRIu64 " ms)\n",
+                                          MODULE_NAME,
+                                          proc->pid,
+                                          proc->share_name,
+                                          proc->op,
+                                          age_us / 1000);
+                               proc->is_active = false;
+                               timed_out++;
+                       }
+                       continue;
+               }
+
+               if (!proc->is_active) {
+                       continue;
+               }
+
+               /*
+                * Skip if an earlier active process with the same
+                * (share_name, op) already triggered a broadcast for this
+                * combination.
+                */
+               for (inner = state->processes; inner != proc;
+                    inner = inner->next)
+               {
+                       uint64_t inner_age = now - inner->last_seen_usec;
+                       if (strcmp(inner->share_name, proc->share_name) == 0 &&
+                           strcmp(inner->op, proc->op) == 0 &&
+                           inner->is_active &&
+                           inner_age <= RATELIMITD_ACTIVITY_TIMEOUT_US)
+                       {
+                               already_done = true;
+                               break;
+                       }
+               }
+               if (already_done) {
+                       continue;
+               }
+
+               /*
+                * Count all active non-timed-out procs for this
+                * (share_name, op)
+                */
+               for (inner = state->processes; inner != NULL;
+                    inner = inner->next)
+               {
+                       uint64_t inner_age = now - inner->last_seen_usec;
+                       if (strcmp(inner->share_name, proc->share_name) == 0 &&
+                           strcmp(inner->op, proc->op) == 0 &&
+                           inner->is_active &&
+                           inner_age <= RATELIMITD_ACTIVITY_TIMEOUT_US)
+                       {
+                               count++;
+                       }
+               }
+
+               broadcast_summary(
+                       state, proc->op, proc->share_name, count, now);
+               broadcasts++;
+       }
+
+       if (broadcasts == 0) {
+               DBG_DEBUG("[%s] No active processes, skipped broadcasts "
+                         "(timed_out=%d)\n",
+                         MODULE_NAME,
+                         timed_out);
+       } else {
+               DBG_DEBUG("[%s] Sent %d per-share broadcasts "
+                         "(timed_out=%d)\n",
+                         MODULE_NAME,
+                         broadcasts,
+                         timed_out);
+       }
+}
+
+static void broadcast_timer_handler(struct tevent_context *ev,
+                                   struct tevent_timer *te,
+                                   struct timeval current_time,
+                                   void *private_data)
+{
+       struct smbd_ratelimitd_state *state = private_data;
+       uint64_t now = time_now_usec();
+
+       broadcast_per_share_summaries(state, now);
+
+       state->last_broadcast_usec = now;
+
+       /* Cleanup old inactive entries every 100 timer ticks (~100 seconds) */
+       if ((state->total_broadcasts_sent > 0) &&
+           (state->total_broadcasts_sent % 100 == 0))
+       {
+               cleanup_inactive_processes(state);
+       }
+
+       DBG_DEBUG("[%s] broadcast_timer_handler: rescheduling timer\n",
+                 MODULE_NAME);
+
+       state->broadcast_timer = tevent_add_timer(
+               state->ev,
+               state,
+               timeval_current_ofs(RATELIMITD_BROADCAST_INTERVAL_US / 1000000,
+                                   RATELIMITD_BROADCAST_INTERVAL_US %
+                                           1000000),
+               broadcast_timer_handler,
+               state);
+       if (state->broadcast_timer == NULL) {
+               DBG_ERR("[%s] Failed to reschedule broadcast timer: "
+                       "cluster coordination suspended\n",
+                       MODULE_NAME);
+       }
+}
+
+struct tevent_req *smbd_ratelimitd_send(TALLOC_CTX *mem_ctx,
+                                       struct tevent_context *ev,
+                                       struct messaging_context *msg)
+{
+       struct tevent_req *req;
+       struct smbd_ratelimitd_state *state;
+       struct sockaddr_un addr;
+       struct tevent_fd *fde;
+       size_t len;
+       int ret;
+
+       req = tevent_req_create(mem_ctx, &state, struct smbd_ratelimitd_state);
+       if (req == NULL) {
+               return NULL;
+       }
+
+       state->ev = ev;
+       state->msg_ctx = msg;
+       state->my_vnn = get_my_vnn();
+       state->unix_sock = -1;
+       state->socket_path = NULL;
+
+       talloc_set_destructor(state, smbd_ratelimitd_state_destructor);
+
+       state->processes = NULL;
+
+       state->socket_path = state_path(state, RATELIMITD_SOCKET_NAME);
+       if (state->socket_path == NULL) {
+               DBG_ERR("[%s] Failed to allocate socket path\n", MODULE_NAME);
+               tevent_req_error(req, ENOMEM);
+               return tevent_req_post(req, ev);
+       }
+
+       DBG_NOTICE("[%s] Creating socket at: %s\n",
+                  MODULE_NAME,
+                  state->socket_path);
+
+       state->unix_sock = socket(AF_UNIX, SOCK_DGRAM, 0);
+       if (state->unix_sock < 0) {
+               DBG_ERR("[%s] socket() failed: %s\n",
+                       MODULE_NAME,
+                       strerror(errno));
+               tevent_req_error(req, errno);
+               return tevent_req_post(req, ev);
+       }
+
+       /* Remove stale socket from previous daemon instance */
+       ret = unlink(state->socket_path);
+       if (ret == 0) {
+               DBG_DEBUG("[%s] Removed stale socket: %s\n",
+                         MODULE_NAME,
+                         state->socket_path);
+       } else if (errno != ENOENT) {
+               DBG_DEBUG("[%s] unlink(%s) failed: %s (continuing anyway)\n",
+                         MODULE_NAME,
+                         state->socket_path,
+                         strerror(errno));
+       }
+
+       memset(&addr, 0, sizeof(addr));
+       addr.sun_family = AF_UNIX;
+
+       len = strlcpy(addr.sun_path,
+                     state->socket_path,
+                     sizeof(addr.sun_path));
+       if (len >= sizeof(addr.sun_path)) {
+               DBG_ERR("[%s] Socket path too long: %s\n",
+                       MODULE_NAME,
+                       state->socket_path);
+               close(state->unix_sock);
+               state->unix_sock = -1;
+               tevent_req_error(req, ENAMETOOLONG);
+               return tevent_req_post(req, ev);
+       }
+
+       ret = bind(state->unix_sock, (struct sockaddr *)&addr, sizeof(addr));
+       if (ret < 0) {
+               DBG_ERR("[%s] bind() failed on %s: %s\n",
+                       MODULE_NAME,
+                       state->socket_path,
+                       strerror(errno));
+               close(state->unix_sock);
+               state->unix_sock = -1;
+               tevent_req_error(req, errno);
+               return tevent_req_post(req, ev);
+       }
+
+       ret = chmod(state->socket_path, 0660);
+       if (ret < 0) {
+               DBG_WARNING("[%s] chmod() failed: %s\n",
+                           MODULE_NAME,
+                           strerror(errno));
+       }
+
+       fde = tevent_add_fd(state->ev,
+                           state,
+                           state->unix_sock,
+                           TEVENT_FD_READ,
+                           handle_unix_socket_read,
+                           state);
+       if (fde == NULL) {
+               DBG_ERR("[%s] tevent_add_fd failed\n", MODULE_NAME);
+               close(state->unix_sock);
+               state->unix_sock = -1;
+               tevent_req_error(req, ENOMEM);
+               return tevent_req_post(req, ev);
+       }
+
+       state->broadcast_timer = tevent_add_timer(
+               state->ev,
+               state,
+               timeval_current_ofs(RATELIMITD_BROADCAST_INTERVAL_US / 1000000,
+                                   RATELIMITD_BROADCAST_INTERVAL_US %
+                                           1000000),
+               broadcast_timer_handler,
+               state);
+       if (state->broadcast_timer == NULL) {
+               DBG_ERR("[%s] tevent_add_timer failed\n", MODULE_NAME);
+               close(state->unix_sock);
+               state->unix_sock = -1;
+               tevent_req_error(req, ENOMEM);
+               return tevent_req_post(req, ev);
+       }
+
+       DBG_NOTICE("[%s] Daemon initialized: vnn=%u socket=%s\n",
+                  MODULE_NAME,
+                  state->my_vnn,
+                  state->socket_path);
+
+       return req;
+}
+
+int smbd_ratelimitd_recv(struct tevent_req *req)
+{
+       return tevent_req_simple_recv_unix(req);
+}
diff --git a/source3/smbd/smbd_ratelimitd.h b/source3/smbd/smbd_ratelimitd.h
new file mode 100644 (file)
index 0000000..089cf56
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Samba rate limiting coordination daemon
+ *
+ * Copyright (c) 2026 Avan Thakkar <athakkar@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __SMBD_RATELIMITD_H__
+#define __SMBD_RATELIMITD_H__
+
+#include "replace.h"
+#include <tevent.h>
+#include "messages.h"
+
+struct tevent_req *smbd_ratelimitd_send(TALLOC_CTX *mem_ctx,
+                                       struct tevent_context *ev,
+                                       struct messaging_context *msg);
+
+/*
+ * Complete ratelimitd request
+ */
+int smbd_ratelimitd_recv(struct tevent_req *req);
+
+#endif /* __SMBD_RATELIMITD_H__ */
index e6bfd9605d6117cedb6bedc39d715750255df03c..b70afca3586dc85a5dd7b7eab993eb76d323f601 100644 (file)
@@ -1131,7 +1131,7 @@ bld.SAMBA3_SUBSYSTEM('fd_handle',
 ########################## BINARIES #################################
 
 bld.SAMBA3_BINARY('smbd/smbd',
-                 source='smbd/server.c smbd/smbd_cleanupd.c',
+                 source='smbd/server.c smbd/smbd_cleanupd.c smbd/smbd_ratelimitd.c',
                  deps='''
                       CMDLINE_S3
                       smbd_base