summaryrefslogtreecommitdiff
path: root/src/ipcpd/unicast
diff options
context:
space:
mode:
Diffstat (limited to 'src/ipcpd/unicast')
-rw-r--r--src/ipcpd/unicast/CMakeLists.txt3
-rw-r--r--src/ipcpd/unicast/ca.c203
-rw-r--r--src/ipcpd/unicast/ca.h43
-rw-r--r--src/ipcpd/unicast/ca/mb-ecn.c1107
-rw-r--r--src/ipcpd/unicast/ca/mb-ecn.h27
-rw-r--r--src/ipcpd/unicast/ca/nop.c41
-rw-r--r--src/ipcpd/unicast/ca/nop.h20
-rw-r--r--src/ipcpd/unicast/ca/ops.h30
-rw-r--r--src/ipcpd/unicast/ca/tests/CMakeLists.txt78
-rw-r--r--src/ipcpd/unicast/ca/tests/ca_test.c392
-rw-r--r--src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c1294
-rw-r--r--src/ipcpd/unicast/ca/tests/mb_ecn_test.c3156
-rw-r--r--src/ipcpd/unicast/cap.c99
-rw-r--r--src/ipcpd/unicast/cap.h40
-rw-r--r--src/ipcpd/unicast/dir/dht.c72
-rw-r--r--src/ipcpd/unicast/dir/tests/dht_test.c63
-rw-r--r--src/ipcpd/unicast/dt.c354
-rw-r--r--src/ipcpd/unicast/dt.h3
-rw-r--r--src/ipcpd/unicast/fa.c488
-rw-r--r--src/ipcpd/unicast/fa.h6
-rw-r--r--src/ipcpd/unicast/main.c18
-rw-r--r--src/ipcpd/unicast/pff/alternate.c2
-rw-r--r--src/ipcpd/unicast/pff/multipath.c22
-rw-r--r--src/ipcpd/unicast/pff/multipath.h4
-rw-r--r--src/ipcpd/unicast/pff/pft.c48
-rw-r--r--src/ipcpd/unicast/pff/pft.h7
-rw-r--r--src/ipcpd/unicast/pff/simple.c22
-rw-r--r--src/ipcpd/unicast/pff/simple.h4
-rw-r--r--src/ipcpd/unicast/pff/tests/pft_test.c302
-rw-r--r--src/ipcpd/unicast/psched.c222
-rw-r--r--src/ipcpd/unicast/psched.h6
-rw-r--r--src/ipcpd/unicast/routing/graph.c8
-rw-r--r--src/ipcpd/unicast/routing/link-state.c8
-rw-r--r--src/ipcpd/unicast/tests/CMakeLists.txt34
-rw-r--r--src/ipcpd/unicast/tests/cap_test.c177
35 files changed, 7790 insertions, 613 deletions
diff --git a/src/ipcpd/unicast/CMakeLists.txt b/src/ipcpd/unicast/CMakeLists.txt
index d3388112..2373f877 100644
--- a/src/ipcpd/unicast/CMakeLists.txt
+++ b/src/ipcpd/unicast/CMakeLists.txt
@@ -6,6 +6,7 @@ protobuf_generate_c(DHT_PROTO_SRCS DHT_PROTO_HDRS
set(UNICAST_SOURCES
addr-auth.c
ca.c
+ cap.c
connmgr.c
dir.c
dt.c
@@ -43,7 +44,9 @@ ouroboros_target_debug_definitions(${IPCP_UNICAST_TARGET})
install(TARGETS ${IPCP_UNICAST_TARGET} RUNTIME DESTINATION ${CMAKE_INSTALL_SBINDIR})
if(BUILD_TESTS)
+ add_subdirectory(ca/tests)
add_subdirectory(dir/tests)
add_subdirectory(pff/tests)
add_subdirectory(routing/tests)
+ add_subdirectory(tests)
endif()
diff --git a/src/ipcpd/unicast/ca.c b/src/ipcpd/unicast/ca.c
index a1751672..290c817e 100644
--- a/src/ipcpd/unicast/ca.c
+++ b/src/ipcpd/unicast/ca.c
@@ -22,17 +22,48 @@
#define OUROBOROS_PREFIX "ca"
+#include "config.h"
+
+#include <ouroboros/list.h>
#include <ouroboros/logs.h>
#include "ca.h"
#include "ca/pol.h"
+#include <pthread.h>
+#include <stdlib.h>
+
+/*
+ * A ca_ctx holds congestion state for a (peer address, qos cube) PATH,
+ * not for a flow. In the default build the façade interns one ctx per
+ * (addr, qc) and shares it across every flow on that path; the policy
+ * runs on the shared ctx and cannot tell one flow from many. Per-flow
+ * ctx (IPCP_CA_PER_FLOW) is a testing build only: it skips interning so
+ * every flow gets its own ctx.
+ */
+
+struct ca_ctx {
+ uint64_t addr;
+ qoscube_t qc;
+ size_t refs;
+ void * pol; /* policy ctx (ops->ctx_create result) */
+ struct list_head next;
+};
+
struct {
- struct ca_ops * ops;
+ struct ca_ops * ops;
+#ifndef IPCP_CA_PER_FLOW
+ struct list_head buckets[CA_BUCKETS];
+ pthread_mutex_t mtx;
+#endif
} ca;
-int ca_init(enum pol_cong_avoid pol)
+int ca_init(enum pol_cong_avoid pol,
+ uint32_t rtt_ms)
{
+#ifndef IPCP_CA_PER_FLOW
+ size_t i;
+#endif
switch(pol) {
case CA_NONE:
log_dbg("Disabling congestion control.");
@@ -41,68 +72,196 @@ int ca_init(enum pol_cong_avoid pol)
case CA_MB_ECN:
log_dbg("Using multi-bit ECN.");
ca.ops = &mb_ecn_ca_ops;
+ mb_ecn_init(rtt_ms);
break;
default:
return -1;
}
+#ifndef IPCP_CA_PER_FLOW
+ for (i = 0; i < CA_BUCKETS; i++)
+ list_head_init(&ca.buckets[i]);
+
+ if (pthread_mutex_init(&ca.mtx, NULL) != 0)
+ return -1;
+#endif
return 0;
}
void ca_fini(void)
{
+#ifndef IPCP_CA_PER_FLOW
+ size_t i;
+
+ /* Data path is stopped; drain any ctx a flow left interned. */
+ for (i = 0; i < CA_BUCKETS; i++) {
+ struct list_head * p;
+ struct list_head * h;
+
+ list_for_each_safe(p, h, &ca.buckets[i]) {
+ struct ca_ctx * ctx;
+ ctx = list_entry(p, struct ca_ctx, next);
+ list_del(&ctx->next);
+ ca.ops->ctx_destroy(ctx->pol);
+ free(ctx);
+ }
+ }
+
+ pthread_mutex_destroy(&ca.mtx);
+#endif
ca.ops = NULL;
}
-void * ca_ctx_create(void)
+#ifndef IPCP_CA_PER_FLOW
+static size_t ca_bucket(uint64_t addr,
+ qoscube_t qc)
{
- return ca.ops->ctx_create();
+ return (addr ^ (addr >> 32) ^ (uint64_t) qc) & (CA_BUCKETS - 1);
+}
+#endif
+
+void * ca_ctx_get(uint64_t addr,
+ qoscube_t qc)
+{
+ struct ca_ctx * ctx;
+#ifndef IPCP_CA_PER_FLOW
+ struct list_head * p;
+ size_t b = ca_bucket(addr, qc);
+
+ pthread_mutex_lock(&ca.mtx);
+
+ list_for_each(p, &ca.buckets[b]) {
+ ctx = list_entry(p, struct ca_ctx, next);
+ if (ctx->addr == addr && ctx->qc == qc) {
+ ctx->refs++;
+ pthread_mutex_unlock(&ca.mtx);
+ return ctx;
+ }
+ }
+#endif
+ ctx = malloc(sizeof(*ctx));
+ if (ctx == NULL)
+ goto fail_ctx;
+
+ ctx->pol = ca.ops->ctx_create();
+ if (ctx->pol == NULL)
+ goto fail_pol;
+
+ ctx->addr = addr;
+ ctx->qc = qc;
+ ctx->refs = 1;
+
+#ifndef IPCP_CA_PER_FLOW
+ list_add(&ctx->next, &ca.buckets[b]);
+
+ pthread_mutex_unlock(&ca.mtx);
+#endif
+ return ctx;
+ fail_pol:
+ free(ctx);
+ fail_ctx:
+#ifndef IPCP_CA_PER_FLOW
+ pthread_mutex_unlock(&ca.mtx);
+#endif
+ return NULL;
}
-void ca_ctx_destroy(void * ctx)
+void ca_ctx_put(void * _ctx)
{
- return ca.ops->ctx_destroy(ctx);
+ struct ca_ctx * ctx = _ctx;
+
+#ifndef IPCP_CA_PER_FLOW
+ pthread_mutex_lock(&ca.mtx);
+
+ if (--ctx->refs > 0) {
+ pthread_mutex_unlock(&ca.mtx);
+ return;
+ }
+
+ list_del(&ctx->next);
+
+ pthread_mutex_unlock(&ca.mtx);
+#endif
+ ca.ops->ctx_destroy(ctx->pol);
+
+ free(ctx);
}
-ca_wnd_t ca_ctx_update_snd(void * ctx,
- size_t len)
+time_t ca_ctx_update_snd(void * _ctx,
+ size_t len,
+ uint8_t lecn,
+ uint64_t * ftag)
{
- return ca.ops->ctx_update_snd(ctx, len);
+ struct ca_ctx * ctx = _ctx;
+
+ return ca.ops->ctx_update_snd(ctx->pol, len, lecn, ctx->refs, ftag);
}
-bool ca_ctx_update_rcv(void * ctx,
+bool ca_ctx_update_rcv(void * _ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece)
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap)
{
- return ca.ops->ctx_update_rcv(ctx, len, ecn, ece);
+ struct ca_ctx * ctx = _ctx;
+
+ return ca.ops->ctx_update_rcv(ctx->pol, len, ecn, cap, ece, fcap);
}
-void ca_ctx_update_ece(void * ctx,
- uint16_t ece)
+void ca_ctx_update_ece(void * _ctx,
+ uint16_t ece,
+ uint8_t cap)
{
- return ca.ops->ctx_update_ece(ctx, ece);
+ struct ca_ctx * ctx = _ctx;
+
+ return ca.ops->ctx_update_ece(ctx->pol, ece, cap);
}
-void ca_wnd_wait(ca_wnd_t wnd)
+bool ca_ctx_hb_due(void * _ctx,
+ uint64_t now)
{
- return ca.ops->wnd_wait(wnd);
+ struct ca_ctx * ctx = _ctx;
+
+ if (ca.ops->ctx_hb_due == NULL)
+ return false;
+
+ return ca.ops->ctx_hb_due(ctx->pol, now);
+}
+
+void ca_ctx_rtt(void * _ctx,
+ uint64_t now,
+ uint64_t rtt)
+{
+ struct ca_ctx * ctx = _ctx;
+
+ if (ca.ops->ctx_rtt == NULL)
+ return;
+
+ ca.ops->ctx_rtt(ctx->pol, now, rtt);
}
-int ca_calc_ecn(int fd,
+int ca_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len)
+ size_t mean)
{
- return ca.ops->calc_ecn(fd, ecn, qc, len);
+ return ca.ops->calc_ecn(queued, ecn, qc, mean);
}
-ssize_t ca_print_stats(void * ctx,
+bool ca_marks_ecn(void)
+{
+ return ca.ops->marks_ecn;
+}
+
+ssize_t ca_print_stats(void * _ctx,
char * buf,
size_t len)
{
+ struct ca_ctx * ctx = _ctx;
+
if (ca.ops->print_stats == NULL)
return 0;
- return ca.ops->print_stats(ctx, buf, len);
+ return ca.ops->print_stats(ctx->pol, buf, len);
}
diff --git a/src/ipcpd/unicast/ca.h b/src/ipcpd/unicast/ca.h
index 47ea15a0..188fb08a 100644
--- a/src/ipcpd/unicast/ca.h
+++ b/src/ipcpd/unicast/ca.h
@@ -29,37 +29,54 @@
#include <stdbool.h>
#include <sys/types.h>
-typedef union {
- time_t wait;
-} ca_wnd_t;
+/* Buffer a policy's ca_print_stats output must fit in. */
+#define CA_STATS_STRLEN 2048
-int ca_init(enum pol_cong_avoid ca);
+int ca_init(enum pol_cong_avoid ca,
+ uint32_t rtt_ms);
void ca_fini(void);
/* OPS */
-void * ca_ctx_create(void);
+void * ca_ctx_get(uint64_t addr,
+ qoscube_t qc);
-void ca_ctx_destroy(void * ctx);
+void ca_ctx_put(void * ctx);
-ca_wnd_t ca_ctx_update_snd(void * ctx,
- size_t len);
+time_t ca_ctx_update_snd(void * ctx,
+ size_t len,
+ uint8_t lecn,
+ uint64_t * ftag);
bool ca_ctx_update_rcv(void * ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece);
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap);
void ca_ctx_update_ece(void * ctx,
- uint16_t ece);
+ uint16_t ece,
+ uint8_t cap);
+
+bool ca_ctx_hb_due(void * ctx,
+ uint64_t now);
-void ca_wnd_wait(ca_wnd_t wnd);
+void ca_ctx_rtt(void * ctx,
+ uint64_t now,
+ uint64_t rtt);
-int ca_calc_ecn(int fd,
+/*
+ * Marks congestion from the egress queue. Both queued and mean are
+ * in bytes, so their ratio is the queue depth in packets.
+ */
+int ca_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len);
+ size_t mean);
+
+bool ca_marks_ecn(void);
ssize_t ca_print_stats(void * ctx,
char * buf,
diff --git a/src/ipcpd/unicast/ca/mb-ecn.c b/src/ipcpd/unicast/ca/mb-ecn.c
index b310c4fc..59f1cae5 100644
--- a/src/ipcpd/unicast/ca/mb-ecn.c
+++ b/src/ipcpd/unicast/ca/mb-ecn.c
@@ -28,9 +28,10 @@
#include "config.h"
-#include <ouroboros/ipcp-dev.h>
#include <ouroboros/time.h>
+#include <ouroboros/utils.h>
+#include "cap.h"
#include "mb-ecn.h"
#include <inttypes.h>
@@ -38,47 +39,284 @@
#include <string.h>
#include <stdio.h>
-/* congestion avoidance constants */
-#define CA_SHFT 5 /* Average over 32 pkts */
-#define CA_WND (1 << CA_SHFT) /* 32 pkts receiver wnd */
-#define CA_UPD (1 << (CA_SHFT - 2)) /* Update snd every 8 pkt */
-#define CA_SLOT 24 /* Initial slot = 16 ms */
-#define CA_INC 1UL << 16 /* ~4MiB/s^2 additive inc */
-#define CA_IWL 1UL << 16 /* Initial limit ~4MiB/s */
-#define CA_MINPS 8 /* Mimimum pkts / slot */
-#define CA_MAXPS 64 /* Maximum pkts / slot */
-#define ECN_Q_SHFT 4
-#define ts_to_ns(ts) ((size_t) ts.tv_sec * BILLION + ts.tv_nsec)
+/*
+ * Multi-bit ECN congestion avoidance: a rate-based controller. The
+ * sender paces a token bucket at a rate steered by graded ECN
+ * feedback, so the backoff is proportional to the congestion. A
+ * backlogged flow ramps in slow start to find the path capacity,
+ * then settles into AIMD around its fair share. There is no sliding
+ * window and no per-flow timer; the control runs on sends.
+ *
+ * Rate law, per control step of dt seconds (r bytes/s, m the mark
+ * in ece units, m_ref = CA_ECE_REF, ai the additive slope):
+ *
+ * slow start dr = r * dt / ss_tc
+ * increase dr = (ai + r / T_probe) * dt
+ * decrease dr = -r * (min(m, CA_ECE_MAX) / m_ref) * dt + L,
+ * cut capped at r/2
+ * lead L = -dm * r / (m_ref * CA_MD_KD_DIV)
+ *
+ * dm is the mark's step since the last decrease, clamped to
+ * +-m_ref. On a rise L joins the cut before the r/2 cap; on a
+ * fall it returns after that cap, bounded on its own to
+ * +-r / CA_MD_KD_DIV, so a full cut is never handed back in one
+ * step.
+ *
+ * Every step scales by elapsed wall-clock time, not by packet
+ * count, so the per-second dynamics are RTT-independent.
+ *
+ * Pacer: a virtual clock vt advances at r; a packet's start tag is
+ * max(tag, vt) and it waits (tag - vt) / r.
+ *
+ * Receiver: ece is the time integral of ecn over a pricing window,
+ * ece = integral(ecn dt) / T. The window is a per-layer constant so
+ * every flow prices one bottleneck alike; it stretches only for a
+ * flow too slow to fill it with samples.
+ *
+ * Marking (mb_ecn_calc_ecn): ecn is the quarter-log2 of the queue
+ * measured in mark units U (U = CA_MARK_KNEE * mean), so the mark is
+ * a log-scale queue depth. Equilibrium is where increase balances
+ * decrease:
+ *
+ * ecn* = (m_ref / 32) * (ai * n / C + 1 / T_probe) = n + 2
+ *
+ * for n backlogged flows, i.e. a standing queue of 2^((n+2)/4) * U.
+ * This is the zero-delay fixpoint; feedback delay raises the real
+ * standing queue above it.
+ */
+
+/* ECE fixed point */
+#define CA_SHFT 5 /* ece fixed point: 32 * ecn */
+
+/* Receiver averaging window */
+#define CA_TW (1ULL << 26) /* pricing window ~67 ms */
+#define CA_TW_MIN (4ULL * MILLION) /* pricing window floor 4 ms */
+#define CA_TW_RTT_MUL 2 /* T_w = 2 * layer RTT */
+#define CA_TW_ABSMAX (1ULL << 32) /* window ceiling ~4.3 s */
+/* Quiet horizon, in windows (1 << shift): gap restart and the TTLs. */
+#define CA_TW_GAP_SHFT 2
+#define CA_RX_WBYTES 16000ULL /* 16 pkts x 1000 B a window */
+#define CA_RX_WCLOSE (2 * CA_RX_WBYTES) /* byte-triggered early close */
+#define CA_TW_SM_SHFT 2 /* window EWMA weight 1/4 */
+
+/* Congestion marking */
+#define CA_MARK_KNEE 1 /* mark onset (packets) */
+
+/* Rate machine */
+#define CA_RATE_MIN (1ULL << 13) /* 8 KiB/s rate floor */
+#define CA_RATE_INIT (1ULL << 16) /* slow start seed 64 KiB/s */
+/* Rate cap; also keeps rate * dt and rate * rise below 2^64. */
+#define CA_RATE_MAX (1ULL << 37)
+#define CA_INV_SHFT 32 /* reciprocal-rate fixp */
+#define CA_AI_RATE (1ULL << 17) /* 128 KiB/s^2 additive inc */
+#define CA_PROBE_TC (8ULL * BILLION) /* proportional probe TC 8s */
+#define CA_ECE_REF (16 << CA_SHFT) /* full congestion: ecn 16 */
+/* Decrease saturation, and the level below which the hold clears. */
+#define CA_ECE_MAX (2 * CA_ECE_REF) /* ecn 32 */
+#define CA_MD_KD_DIV 16 /* lead gain 1/16 */
+
+/* Control cadence */
+#define CA_DT_CTRL (BILLION / 1000) /* min rate-update spacing */
+#define CA_DT_CAP (BILLION / 20) /* idle-resume Δt clamp 50ms */
+#define CA_IDLE_PKTS 4 /* idle: gap over 4 packets */
+/* Feedback staleness floor; ctx->ece_ttl rides above it by rate. */
+#define CA_ECE_TTL (1ULL << 28) /* ~268 ms */
+
+/* Slow start */
+#define CA_SS_RTT_MUL 2 /* ss_tc = 2 * layer RTT */
+#define CA_SS_TC_MIN (BILLION / 1000) /* ramp floor 1 ms */
+#define CA_SS_TC_MAX (4ULL * BILLION) /* ramp ceiling 4 s */
+#define CA_RTT_SHFT 2 /* ss_tc EWMA weight 1/4 */
+#define CA_SS_TC_GRW 1 /* ramp climb cap 2x a sample */
+#define CA_SS_RTT_DEF 200 /* default layer RTT (ms) */
+
+/* Heartbeat */
+#define CA_HB_MIN (40 * MILLION) /* heartbeat interval floor */
+#define CA_HB_LOSS 4 /* stale horizons -> restart */
+
+/* Path capacity */
+#define CA_CAP_SHFT 5 /* floor = capacity / 32 */
+#define CA_CAP_SM_SHFT 1 /* capacity EWMA weight 1/2 */
+/* Outlives ece_ttl 16x: onset-fresh fcap re-seeds each episode. */
+#define CA_CAP_TTL_SHFT 4
+#define CA_RMIN_MAX (1ULL << 32) /* derived floor ceiling */
+
+/* Sender utilisation */
+#define CA_SND_WIN (1ULL << 26) /* sender util window ~67 ms */
+#define CA_USE_NUM 3 /* backlogged: offered >= */
+#define CA_USE_DEN 4 /* 3/4 * window-start rate */
+#define CA_SND_DEC_SHFT 4 /* offered max-filter 1/16 */
+#define CA_SND_DEC_CAP 16 /* bound gapped-close decay */
+#define CA_SND_BYT_MAX (1ULL << 33) /* offered-byte saturation */
+#define CA_PAC_DEN 4 /* backlogged: 1/4 deferred */
+
+/*
+ * Retuning invariants (pinned by the unit tests):
+ * - (1 << CA_TW_GAP_SHFT) * CA_TW > S * BILLION / CA_RATE_MIN, or
+ * a floor-rate flow's onset restart-loops (S ~ one MTU; both ns).
+ * - CA_RX_WBYTES * BILLION / CA_RATE_MIN < CA_TW_ABSMAX: the
+ * floor-rate window must clear the ceiling.
+ * - CA_TW < CA_RX_WBYTES * BILLION / CA_RATE_MIN: at the rate
+ * floor the sample budget, not the horizon, sizes the window.
+ * - CA_TW << CA_TW_GAP_SHFT <= CA_ECE_TTL: the estimator must
+ * not call a gap fresh that the sender still counts as live.
+ * - CA_ECE_TTL > S * BILLION / CA_RATE_MIN: the idle cap clears a
+ * floor-rate flow's inter-send gap, so pacing never reads as idle.
+ * - CA_DT_CAP < CA_ECE_TTL: the idle clamp needs the TTL above it,
+ * or every slow flow reads idle on every send.
+ * - CA_RATE_MAX * CA_DT_CAP, the folded lead * inv_rate at
+ * CA_RATE_MIN, and owed * BILLION (owed clamped in mb_ecn_snd) all
+ * keep the pacer arithmetic below 2^64.
+ * - CA_RATE_MIN <= CA_RATE_INIT and CA_RMIN_MAX < CA_RATE_MAX.
+ * - cap_enc(16 * mean) - cap_enc(mean) == CA_ECE_REF >> CA_SHFT: a
+ * queue of 16 packets is what reads as full congestion.
+ * - CA_MD_KD_DIV sets the lead gain. The term acts both ways (cut on
+ * a rise, give back on a fall), which cancels the DC bias a
+ * one-sided term would rectify into a standing rate difference
+ * between flows pricing one queue; that is what lets the gain run
+ * at 1/16 instead of the deadzone below 1/8.
+ * - T_w = clamp(CA_TW_RTT_MUL * RTT, CA_TW_MIN, CA_TW) scales only
+ * the receiver pricing window; CA_ECE_TTL, CA_SND_WIN, CA_DT_CAP
+ * and CA_DT_CTRL are absolute and must not be derived from it.
+ * - The gap-restart horizon is floored at CA_ECE_TTL, so a
+ * floor-rate flow's inter-packet gap never reads as an onset.
+ * - The ai_hold release threshold equals the decrease saturation
+ * clamp: a standing mark that is a legal equilibrium must be able
+ * to clear the hold.
+ *
+ * Structural invariants (not exercised by the unit tests):
+ * - CA_MARK_KNEE <= 4: the full decrease range must fit the ring
+ * (SSM_RBUFF_SIZE, not visible from this file).
+ * - ecn* = 2 + n holds for n <= 29 (the decrease clamp) and only
+ * with live capacity feedback.
+ */
struct mb_ecn_ctx {
- uint16_t rx_ece; /* Level of congestion (upstream) */
- size_t rx_ctr; /* Receiver side packet counter */
-
- uint16_t tx_ece; /* Level of congestion (downstream) */
- size_t tx_ctr; /* Sender side packet counter */
- size_t tx_wbc; /* Window byte count */
- size_t tx_wpc; /* Window packet count */
- size_t tx_wbl; /* Window byte limit */
- bool tx_cav; /* Congestion avoidance */
- size_t tx_mul; /* Slot size multiplier */
- size_t tx_inc; /* Additive increase */
- size_t tx_slot;
+ uint16_t rx_ece; /* smoothed congestion echo (32 * ecn) */
+ uint64_t rx_acc; /* window integral of ecn * dt */
+ uint64_t rx_byt; /* bytes arrived in current window */
+ uint64_t rx_ts; /* last packet arrival (ns) */
+ uint64_t rx_win; /* window start (ns) */
+ uint64_t rx_tw; /* adaptive averaging window (ns) */
+ uint8_t rx_cap; /* window bottleneck capacity code */
+
+ uint16_t tx_ece; /* congestion reported from downstream */
+ uint16_t tx_ecp; /* previous tx_ece (rise detection) */
+ uint8_t tx_loc; /* local first-hop ecn mark (fallback) */
+ bool tx_cav; /* past slow start */
+ bool ai_hold; /* freeze AI after loss until clear */
+ uint64_t rate; /* paced send rate (bytes/s) */
+ uint64_t rate_min; /* capacity-derived rate floor (B/s) */
+ uint64_t ai_rate; /* additive-increase slope (B/s^2) */
+ uint64_t ece_ttl; /* how long feedback stays valid (ns) */
+ uint64_t ss_tc; /* slow-start time constant (ns) */
+ uint64_t dec_acc; /* sub-ms decrease time carried (ns) */
+ uint64_t inv_rate; /* fixed-point 1/rate for pacing */
+ uint64_t vt; /* virtual service clock (bytes) */
+ uint64_t lead; /* pacer lead of last send (bytes) */
+ uint64_t last_ts; /* last clock advance (ns) */
+ uint64_t last_ctrl; /* last rate update (ns) */
+ uint64_t last_fb; /* last congestion feedback (ns) */
+ uint64_t last_sig; /* last liveness signal, incl. hb (ns) */
+ uint64_t n_fb; /* feedback updates received */
+ uint64_t n_rtt; /* heartbeat RTT samples folded */
+ uint64_t last_hb; /* last heartbeat emitted (ns) */
+ uint64_t last_res; /* last resume from idle (ns) */
+ uint64_t last_loc; /* last local mark seen (ns) */
+ uint64_t last_cap; /* last capacity applied (ns) */
+
+ uint64_t snd_byt; /* bytes offered this window (capped) */
+ size_t snd_flows; /* flows sharing the ctx, >= 1 */
+ uint64_t snd_pac; /* bytes the pacer held back this win */
+ uint64_t snd_win; /* utilisation window start (ns) */
+ uint64_t snd_r0; /* rate at window start */
+ uint64_t snd_rate; /* max-filter of offered rate (B/s) */
+ bool backlogged; /* offered load keeps the pacer busy */
+ bool src_limited; /* rate held at offered-load ceiling */
+ bool started; /* a real send has occurred */
+
+ /* Diagnostics only, read by mb_ecn_print_stats. */
+ uint8_t tx_cap; /* path capacity code fed back to us */
+ uint64_t n_ctrl; /* control steps taken */
+ uint64_t t_ctrl; /* wall time covered by steps (ns) */
+ uint64_t t_bank; /* increase time banked in steps (ns) */
+ uint64_t n_ttl; /* feedback aged out (TTL) */
+ uint64_t n_cap; /* capacity updates applied */
+ uint64_t n_loss; /* signal-loss cuts (collapse) */
+ uint64_t ss_peak; /* peak rate in slow start (bytes/s) */
};
+/* Layer slow-start time constant (ns), from the declared RTT. */
+static uint64_t mb_ecn_ss_tc = (uint64_t) CA_SS_RTT_MUL *
+ CA_SS_RTT_DEF * MILLION;
+
+/* Layer pricing window (ns), from the declared RTT. */
+static uint64_t mb_ecn_tw = CA_TW;
+
struct ca_ops mb_ecn_ca_ops = {
.ctx_create = mb_ecn_ctx_create,
.ctx_destroy = mb_ecn_ctx_destroy,
.ctx_update_snd = mb_ecn_ctx_update_snd,
.ctx_update_rcv = mb_ecn_ctx_update_rcv,
.ctx_update_ece = mb_ecn_ctx_update_ece,
- .wnd_wait = mb_ecn_wnd_wait,
+ .ctx_hb_due = mb_ecn_ctx_hb_due,
+ .ctx_rtt = mb_ecn_ctx_rtt,
.calc_ecn = mb_ecn_calc_ecn,
+ .marks_ecn = true,
.print_stats = mb_ecn_print_stats
};
+static uint64_t mb_ecn_rate_inv(uint64_t rate)
+{
+ return ((uint64_t) BILLION << CA_INV_SHFT) / rate;
+}
+
+/*
+ * Feedback arrives once per receiver window, and the window tracks
+ * the flow's byte rate. Mirror it: age the signal out only past the
+ * quiet horizon at the current rate, floored for fast flows.
+ */
+static uint64_t mb_ecn_ece_ttl(uint64_t rate)
+{
+ uint64_t ttl;
+
+ ttl = (1 << CA_TW_GAP_SHFT) * CA_RX_WBYTES * BILLION / rate;
+
+ return ttl > (uint64_t) CA_ECE_TTL ? ttl : (uint64_t) CA_ECE_TTL;
+}
+
+/* Derive the layer slow-start slope from the declared RTT (ms). */
+void mb_ecn_init(uint32_t rtt_ms)
+{
+ uint64_t tc;
+ uint64_t rtt;
+ uint64_t tw;
+
+ if (rtt_ms == 0) /* unspecified: safe default */
+ rtt_ms = CA_SS_RTT_DEF;
+
+ tc = (uint64_t) CA_SS_RTT_MUL * rtt_ms * MILLION;
+ if (tc < (uint64_t) CA_SS_TC_MIN)
+ tc = CA_SS_TC_MIN;
+
+ mb_ecn_ss_tc = tc;
+
+ rtt = (uint64_t) rtt_ms * MILLION;
+
+ tw = (uint64_t) CA_TW_RTT_MUL * rtt;
+ if (tw < CA_TW_MIN)
+ tw = CA_TW_MIN;
+
+ if (tw > CA_TW)
+ tw = CA_TW;
+
+ mb_ecn_tw = tw;
+}
+
void * mb_ecn_ctx_create(void)
{
struct timespec now;
+ uint64_t t;
struct mb_ecn_ctx * ctx;
ctx = malloc(sizeof(*ctx));
@@ -89,10 +327,29 @@ void * mb_ecn_ctx_create(void)
memset(ctx, 0, sizeof(*ctx));
- ctx->tx_mul = CA_SLOT;
- ctx->tx_wbl = CA_IWL;
- ctx->tx_inc = CA_INC;
- ctx->tx_slot = ts_to_ns(now) >> ctx->tx_mul;
+ t = TS_TO_UINT64(now);
+
+ ctx->rate = CA_RATE_INIT;
+ ctx->rate_min = CA_RATE_MIN;
+ ctx->ai_rate = CA_AI_RATE;
+ ctx->ss_tc = mb_ecn_ss_tc;
+ ctx->ece_ttl = mb_ecn_ece_ttl(CA_RATE_INIT);
+ ctx->inv_rate = mb_ecn_rate_inv(CA_RATE_INIT);
+ ctx->rx_ts = t;
+ ctx->rx_win = t;
+ ctx->rx_tw = mb_ecn_tw;
+ ctx->last_ts = t;
+ ctx->last_ctrl = t;
+ ctx->last_fb = t;
+ ctx->last_sig = t;
+ ctx->last_loc = t;
+ ctx->last_cap = t;
+
+ /* snd_win/last_ts re-seeded lazily on the first real send. */
+ ctx->snd_r0 = CA_RATE_INIT;
+ ctx->snd_rate = CA_RATE_INIT;
+ ctx->snd_flows = 1;
+ ctx->backlogged = true;
return (void *) ctx;
}
@@ -102,158 +359,670 @@ void mb_ecn_ctx_destroy(void * ctx)
free(ctx);
}
-#define _slot_after(new, old) ((int64_t) (old - new) < 0)
+/* Local first-hop mark exits slow start and covers dead feedback. */
+static void mb_ecn_loc(struct mb_ecn_ctx * ctx,
+ uint8_t lecn,
+ uint64_t t)
+{
+ if (lecn == 0)
+ return;
+
+ ctx->tx_loc = lecn;
+ ctx->tx_cav = true;
+ ctx->last_loc = t;
+}
+
+/* Slow start: ramp only while backlogged. */
+static void mb_ecn_slow_start(struct mb_ecn_ctx * ctx,
+ uint64_t dta)
+{
+ if (ctx->backlogged)
+ ctx->rate += ctx->rate * dta / ctx->ss_tc;
+}
+
+/* Additive increase plus a rate-independent proportional probe. */
+static void mb_ecn_increase(struct mb_ecn_ctx * ctx,
+ uint64_t dta)
+{
+ if (!ctx->backlogged)
+ return;
+
+ /* After a loss, hold until a clean signal drains the queue. */
+ if (ctx->ai_hold)
+ return;
+
+ ctx->rate += ctx->ai_rate * dta / BILLION;
+ ctx->rate += ctx->rate * dta / CA_PROBE_TC;
+}
+
+/*
+ * Multiplicative decrease: cut proportional to mark x elapsed time,
+ * plus a lead term on the mark's step, clamped and acting both ways.
+ */
+static void mb_ecn_decrease(struct mb_ecn_ctx * ctx,
+ uint64_t dtc)
+{
+ uint64_t dtm;
+ uint64_t mark;
+ uint64_t step;
+ uint64_t lead;
+ uint64_t cut;
+ uint16_t m;
+ bool up;
+
+ m = ctx->tx_ece > 0 ? ctx->tx_ece
+ : (uint16_t) (ctx->tx_loc << CA_SHFT);
+ if (m == 0) {
+ ctx->dec_acc = 0; /* unmarked time is not banked */
+ ctx->tx_ecp = 0;
+ return;
+ }
+
+ mark = MIN(m, CA_ECE_MAX);
+
+ /* Lead on the mark step; the clamp bounds it to rate/KD. */
+ up = m > ctx->tx_ecp;
+ step = up ? m - ctx->tx_ecp : ctx->tx_ecp - m;
+ step = MIN(step, CA_ECE_REF);
+ lead = ctx->rate * step / (CA_ECE_REF * CA_MD_KD_DIV);
+
+ cut = up ? lead : 0;
+
+ /*
+ * Bank the remainder: at a 1 ms control cadence, truncating
+ * to whole milliseconds would drop up to half of every cut.
+ */
+ ctx->dec_acc += dtc;
+ dtm = ctx->dec_acc / MILLION;
+ ctx->dec_acc -= dtm * MILLION;
+ if (mark * dtm >= CA_ECE_REF * 500)
+ cut += ctx->rate / 2;
+ else
+ cut += ctx->rate * mark * dtm / (CA_ECE_REF * 1000);
+
+ if (cut > ctx->rate / 2)
+ cut = ctx->rate / 2;
+
+ ctx->rate -= cut;
+
+ if (!up)
+ ctx->rate += lead;
+
+ ctx->tx_ecp = m;
+}
+
+/* Offered-load ceiling backstop while source-limited. */
+static void mb_ecn_ceiling(struct mb_ecn_ctx * ctx)
+{
+ uint64_t hi;
+
+ if (ctx->backlogged) {
+ ctx->src_limited = false;
+ return;
+ }
+
+ /* Land on the backlog level; a ceiling above it never clears. */
+ hi = ctx->snd_rate > CA_RATE_MAX / CA_USE_DEN * CA_USE_NUM
+ ? (uint64_t) CA_RATE_MAX
+ : ctx->snd_rate * CA_USE_DEN / CA_USE_NUM;
+ if (hi < CA_RATE_MIN)
+ hi = CA_RATE_MIN;
+
+ ctx->src_limited = ctx->rate > hi;
+ if (ctx->src_limited)
+ ctx->rate = hi;
+}
+
+static void mb_ecn_ctrl(struct mb_ecn_ctx * ctx,
+ uint64_t dtc)
+{
+ uint64_t dta;
+ uint64_t lo;
+
+ /* AI and slow start bank at most CA_DT_CAP of idle time. */
+ dta = MIN(dtc, (uint64_t) CA_DT_CAP);
+
+ ctx->n_ctrl++;
+ ctx->t_ctrl += dtc;
+ ctx->t_bank += dta;
+
+ if (ctx->tx_cav) {
+ mb_ecn_increase(ctx, dta);
+ mb_ecn_decrease(ctx, dtc);
+ } else {
+ mb_ecn_slow_start(ctx, dta);
+ }
+
+ mb_ecn_ceiling(ctx);
+
+ /* Capacity floor only while backlogged; else the absolute floor. */
+ lo = ctx->backlogged ? ctx->rate_min : (uint64_t) CA_RATE_MIN;
+ if (ctx->rate < lo)
+ ctx->rate = lo;
+
+ if (ctx->rate > CA_RATE_MAX)
+ ctx->rate = CA_RATE_MAX;
+
+ ctx->inv_rate = mb_ecn_rate_inv(ctx->rate);
+ ctx->ece_ttl = mb_ecn_ece_ttl(ctx->rate);
+
+ if (!ctx->tx_cav && ctx->rate > ctx->ss_peak)
+ ctx->ss_peak = ctx->rate;
+}
+
+/* Fold offered into the max filter: rise at once, decay 1/16 per window. */
+static void mb_ecn_offered(struct mb_ecn_ctx * ctx,
+ uint64_t offered,
+ uint64_t elapsed)
+{
+ uint64_t n;
+
+ if (offered >= ctx->snd_rate) {
+ ctx->snd_rate = offered;
+ return;
+ }
+
+ n = MIN(elapsed / CA_SND_WIN, CA_SND_DEC_CAP);
+ while (n-- > 0 && ctx->snd_rate > offered)
+ ctx->snd_rate -= (ctx->snd_rate - offered) >> CA_SND_DEC_SHFT;
+}
+
+/* Open a fresh utilisation window at t. */
+static void mb_ecn_win_open(struct mb_ecn_ctx * ctx,
+ uint64_t t)
+{
+ ctx->snd_win = t;
+ ctx->snd_byt = 0;
+ ctx->snd_pac = 0;
+ ctx->snd_r0 = ctx->rate;
+}
+
+/*
+ * Note the flow count; a window spanning two populations measures
+ * neither, so a change opens a fresh one.
+ */
+static void mb_ecn_flows(struct mb_ecn_ctx * ctx,
+ size_t flows,
+ uint64_t t)
+{
+ size_t n = flows > 0 ? flows : 1;
+
+ if (n == ctx->snd_flows)
+ return;
+
+ ctx->snd_flows = n;
+
+ mb_ecn_win_open(ctx, t);
+}
+
+/*
+ * Close the utilisation window: set backlogged from the level test,
+ * fold offered into the max filter, then reset the window.
+ */
+static void mb_ecn_win(struct mb_ecn_ctx * ctx,
+ uint64_t t)
+{
+ uint64_t elapsed = t - ctx->snd_win;
+ uint64_t offered;
+ bool was = ctx->backlogged;
+
+ /*
+ * snd_byt is the whole ctx's offered bytes but rate is what one
+ * flow may send, so share it out before either is compared.
+ */
+ offered = ctx->snd_byt * BILLION / elapsed / ctx->snd_flows;
+
+ /*
+ * Offered load is counted past the pacer, so it cannot tell a
+ * quiet source from one the pacer is holding back, and idle
+ * flows on the context drag it down. A window the pacer had to
+ * defer is rate-limited whatever the bytes say.
+ */
+ ctx->backlogged = offered * CA_USE_DEN >= ctx->snd_r0 * CA_USE_NUM
+ || ctx->snd_pac * CA_PAC_DEN >= ctx->snd_byt;
+
+ if (!was && ctx->backlogged) /* resume: fresh liveness baseline */
+ ctx->last_res = t;
+
+ mb_ecn_offered(ctx, offered, elapsed);
+
+ if (ctx->backlogged)
+ ctx->src_limited = false;
+
+ mb_ecn_win_open(ctx, t);
+}
+
+/* Age out congestion, local-mark and capacity signals once stale. */
+/* Heartbeat interval: ~1 RTT, floored so fast links don't over-probe. */
+static uint64_t mb_ecn_t_hb(const struct mb_ecn_ctx * ctx)
+{
+ uint64_t t = ctx->ss_tc >> 1;
+
+ return t > (uint64_t) CA_HB_MIN ? t : CA_HB_MIN;
+}
+
+/* Feedback collapsed while backlogged: halve like an RTO, stay in AIMD. */
+static void mb_ecn_loss(struct mb_ecn_ctx * ctx,
+ uint64_t t)
+{
+ ctx->rate -= ctx->rate / 2;
+ if (ctx->rate < (uint64_t) CA_RATE_MIN)
+ ctx->rate = CA_RATE_MIN;
+
+ ctx->inv_rate = mb_ecn_rate_inv(ctx->rate);
+ ctx->ece_ttl = mb_ecn_ece_ttl(ctx->rate);
+ ctx->last_sig = t;
+ ctx->ai_hold = true;
+ ctx->n_loss++;
+}
+
+static void mb_ecn_age(struct mb_ecn_ctx * ctx,
+ uint64_t t)
+{
+ uint64_t ttl = ctx->ece_ttl;
+ uint64_t ref = ctx->last_sig > ctx->last_res
+ ? ctx->last_sig : ctx->last_res;
+ uint64_t gap = t - ref;
+
+ /*
+ * Sustained silence while backlogged is feedback collapse: cut
+ * the rate in half and stay in AIMD, so a recovering flow climbs
+ * back additively instead of re-ramping. Repeated silence decays
+ * it geometrically toward the floor.
+ */
+ if (ctx->backlogged && ctx->n_fb + ctx->n_rtt > 0
+ && gap > (uint64_t) CA_HB_LOSS * ttl) {
+ mb_ecn_loss(ctx, t);
+ return;
+ }
+
+ if (t - ctx->last_fb > ctx->ece_ttl) {
+ if (ctx->tx_ece > 0)
+ ctx->n_ttl++;
+ ctx->tx_ece = 0;
+ }
+
+ if (t - ctx->last_loc > ctx->ece_ttl)
+ ctx->tx_loc = 0;
+
+ /* Stale capacity: fall back to the compile-time defaults. */
+ if (t - ctx->last_cap > ctx->ece_ttl << CA_CAP_TTL_SHFT) {
+ ctx->rate_min = CA_RATE_MIN;
+ ctx->ai_rate = CA_AI_RATE;
+ ctx->tx_cap = 0;
+ }
+}
+
+/* Advance the virtual clock; a gap past CA_DT_CAP credits a burst. */
+static void mb_ecn_advance(struct mb_ecn_ctx * ctx,
+ uint64_t dt,
+ size_t len,
+ uint64_t ftag)
+{
+ uint64_t burst;
+ uint64_t owed;
+
+ if (dt <= (uint64_t) CA_DT_CAP) {
+ ctx->vt += ctx->rate * dt / BILLION;
+ return;
+ }
+
+ burst = ctx->rate * CA_DT_CAP / BILLION;
+ if (burst < (uint64_t) len)
+ burst = len;
-ca_wnd_t mb_ecn_ctx_update_snd(void * _ctx,
- size_t len)
+ owed = ftag > ctx->vt ? ftag - ctx->vt + burst : burst;
+
+ /* Clamp so owed * BILLION cannot wrap (2^33 B backlog). */
+ if (owed > (1ULL << 33))
+ owed = 1ULL << 33;
+
+ if (dt >= owed * BILLION / ctx->rate)
+ ctx->vt += owed;
+ else
+ ctx->vt += ctx->rate * dt / BILLION;
+}
+
+static time_t mb_ecn_snd(struct mb_ecn_ctx * ctx,
+ size_t len,
+ uint64_t t,
+ uint64_t * ftag)
+{
+ uint64_t dt;
+ uint64_t dtc;
+ uint64_t idle;
+ uint64_t s;
+
+ /* Lazy warm-up seed: packet #1 is never an idle resume. */
+ if (!ctx->started) {
+ ctx->started = true;
+ ctx->last_ts = t;
+ ctx->last_res = t;
+ ctx->snd_win = t;
+ ctx->snd_r0 = ctx->rate;
+ }
+
+ dt = t - ctx->last_ts;
+ ctx->last_ts = t;
+
+ /*
+ * Idle gap clears backlog before aging: no false loss on resume.
+ * Measured against the pacer's own spacing, so a flow paced
+ * slower than CA_DT_CAP per packet does not read as idle on
+ * every send, and bounded by the staleness horizon.
+ */
+ idle = CA_IDLE_PKTS * len * BILLION / ctx->rate;
+ idle = MAX(idle, (uint64_t) CA_DT_CAP);
+ idle = MIN(idle, (uint64_t) CA_ECE_TTL);
+ if (dt > idle)
+ ctx->backlogged = false;
+
+ mb_ecn_age(ctx, t);
+
+ /* Offered-load estimator: accumulate, gate growth, size ceiling. */
+ ctx->snd_byt += len;
+ if (ctx->snd_byt > (uint64_t) CA_SND_BYT_MAX)
+ ctx->snd_byt = CA_SND_BYT_MAX;
+
+ if (t - ctx->snd_win >= (uint64_t) CA_SND_WIN)
+ mb_ecn_win(ctx, t);
+
+ /* Rate update before the vt advance: burst uses the clamped rate. */
+ dtc = t - ctx->last_ctrl;
+ if (dtc >= (uint64_t) CA_DT_CTRL) {
+ ctx->last_ctrl = t;
+ mb_ecn_ctrl(ctx, dtc);
+ }
+
+ mb_ecn_advance(ctx, dt, len, *ftag);
+
+ /* SFQ start tag: behind the clock starts now, ahead waits. */
+ s = *ftag > ctx->vt ? *ftag : ctx->vt;
+ *ftag = s + len;
+
+ if (s > ctx->vt)
+ ctx->snd_pac += len;
+
+ ctx->lead = s - ctx->vt;
+
+ /* Reciprocal pacing; folded so any lead * rate stays in range. */
+ if (s > ctx->vt)
+ return (time_t) ((ctx->lead * (ctx->inv_rate >> 16))
+ >> (CA_INV_SHFT - 16));
+
+ return 0;
+}
+
+time_t mb_ecn_ctx_update_snd(void * _ctx,
+ size_t len,
+ uint8_t lecn,
+ size_t flows,
+ uint64_t * ftag)
{
struct timespec now;
- size_t slot;
- ca_wnd_t wnd;
+ uint64_t t;
struct mb_ecn_ctx * ctx = _ctx;
clock_gettime(PTHREAD_COND_CLOCK, &now);
- slot = ts_to_ns(now) >> ctx->tx_mul;
+ t = TS_TO_UINT64(now);
- ctx->tx_ctr++;
- ctx->tx_wpc++;
- ctx->tx_wbc += len;
+ mb_ecn_flows(ctx, flows, t);
- if (ctx->tx_ctr > CA_WND)
- ctx->tx_ece = 0;
+ mb_ecn_loc(ctx, lecn, t);
- if (_slot_after(slot, ctx->tx_slot)) {
- bool carry = false; /* may carry over if window increases */
+ return mb_ecn_snd(ctx, len, t, ftag);
+}
- ctx->tx_slot = slot;
+/* Estimator idle, or a quiet gap past the horizon: restart fresh. */
+static bool mb_ecn_rcv_fresh(const struct mb_ecn_ctx * ctx,
+ uint64_t dt)
+{
+ uint64_t gap;
- if (!ctx->tx_cav) { /* Slow start */
- if (ctx->tx_wbc > ctx->tx_wbl)
- ctx->tx_wbl <<= 1;
- } else {
- if (ctx->tx_ece) /* Mult. Decrease */
- ctx->tx_wbl -= (ctx->tx_wbl * ctx->tx_ece)
- >> (CA_SHFT + 8);
- else /* Add. Increase */
- ctx->tx_wbl = ctx->tx_wbc + ctx->tx_inc;
- }
+ if (ctx->rx_ece == 0 && ctx->rx_acc == 0)
+ return true;
- /* Window scaling */
- if (ctx->tx_wpc < CA_MINPS) {
- size_t fact = 0; /* factor to scale the window up */
- size_t pkts = ctx->tx_wpc;
- while (pkts < CA_MINPS) {
- pkts <<= 1;
- fact++;
- }
- ctx->tx_mul += fact;
- ctx->tx_slot >>= fact;
- if ((ctx->tx_slot & ((1 << fact) - 1)) == 0) {
- carry = true;
- ctx->tx_slot += 1;
- }
- ctx->tx_wbl <<= fact;
- ctx->tx_inc <<= fact;
- } else if (ctx->tx_wpc > CA_MAXPS) {
- size_t fact = 0; /* factor to scale the window down */
- size_t pkts = ctx->tx_wpc;
- while (pkts > CA_MAXPS) {
- pkts >>= 1;
- fact++;
- }
- ctx->tx_mul -= fact;
- ctx->tx_slot <<= fact;
- ctx->tx_wbl >>= fact;
- ctx->tx_inc >>= fact;
- } else {
- ctx->tx_slot = slot;
- }
+ gap = ctx->rx_tw << CA_TW_GAP_SHFT;
- if (!carry) {
- ctx->tx_wbc = 0;
- ctx->tx_wpc = 0;
- }
- }
+ return dt > MAX(gap, (uint64_t) CA_ECE_TTL);
+}
- if (ctx->tx_wbc > ctx->tx_wbl)
- wnd.wait = ((ctx->tx_slot + 1) << ctx->tx_mul) - ts_to_ns(now);
+/*
+ * Size the next averaging window to ~16 packets at this rate, floored
+ * at the price horizon: a flow fast enough to fill the horizon
+ * integrates over CA_TW, a slower one stretches for its samples.
+ */
+static void mb_ecn_resize(struct mb_ecn_ctx * ctx,
+ uint64_t win)
+{
+ uint64_t tw = CA_RX_WBYTES * win / ctx->rx_byt;
+
+ if (tw > ctx->rx_tw)
+ ctx->rx_tw += (tw - ctx->rx_tw) >> CA_TW_SM_SHFT;
else
- wnd.wait = 0;
+ ctx->rx_tw -= (ctx->rx_tw - tw) >> CA_TW_SM_SHFT;
- return wnd;
+ if (ctx->rx_tw < mb_ecn_tw)
+ ctx->rx_tw = mb_ecn_tw;
+
+ if (ctx->rx_tw > CA_TW_ABSMAX)
+ ctx->rx_tw = CA_TW_ABSMAX;
}
-void mb_ecn_wnd_wait(ca_wnd_t wnd)
+static bool mb_ecn_rcv(struct mb_ecn_ctx * ctx,
+ size_t len,
+ uint8_t ecn,
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap,
+ uint64_t t)
{
- if (wnd.wait > 0) {
- struct timespec s = TIMESPEC_INIT_S(0);
- if (wnd.wait > BILLION) /* Don't care throttling < 1s */
- s.tv_sec = 1;
- else
- s.tv_nsec = wnd.wait;
+ uint64_t dt;
+ uint64_t win;
+
+ dt = t - ctx->rx_ts;
+ ctx->rx_ts = t;
- nanosleep(&s, NULL);
+ if (ctx->rx_ece == 0 && ctx->rx_acc == 0 && ecn == 0)
+ return false;
+
+ /* Onset, or ~4 windows of silence: emit fresh, undiluted. */
+ if (mb_ecn_rcv_fresh(ctx, dt)) {
+ ctx->rx_win = t;
+ ctx->rx_acc = 0;
+ ctx->rx_byt = len;
+ ctx->rx_cap = cap; /* fresh, seeds the new window */
+ ctx->rx_ece = (uint16_t) (ecn << CA_SHFT);
+ *ece = ctx->rx_ece;
+ *fcap = ctx->rx_cap;
+ return true;
+ }
+
+ /* Dwell clamp: one packet weighs at most one window of mark. */
+ ctx->rx_acc += ecn * MIN(dt, ctx->rx_tw);
+ ctx->rx_byt += len;
+
+ ctx->rx_cap = cap_min(ctx->rx_cap, cap);
+ win = t - ctx->rx_win;
+ if (win < ctx->rx_tw) {
+ /* Early close once 2x target bytes arrive (speed-up). */
+ if (ctx->rx_byt < CA_RX_WCLOSE || win < mb_ecn_tw) {
+ *ece = ctx->rx_ece;
+ return false;
+ }
}
+
+ /* Time-integral mean over the actual window elapsed (never rx_tw). */
+ ctx->rx_ece = (uint16_t) ((ctx->rx_acc << CA_SHFT) / win);
+
+ if (ctx->rx_byt > 0)
+ mb_ecn_resize(ctx, win);
+
+ *fcap = ctx->rx_cap;
+
+ ctx->rx_win = t;
+ ctx->rx_acc = 0;
+ ctx->rx_byt = 0;
+ ctx->rx_cap = 0; /* the next window starts unknown */
+
+ *ece = ctx->rx_ece;
+
+ return true;
}
bool mb_ecn_ctx_update_rcv(void * _ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece)
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap)
+{
+ struct timespec now;
+ struct mb_ecn_ctx * ctx = _ctx;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ return mb_ecn_rcv(ctx, len, ecn, cap, ece, fcap, TS_TO_UINT64(now));
+}
+
+static void mb_ecn_ece(struct mb_ecn_ctx * ctx,
+ uint16_t ece,
+ uint8_t cap,
+ uint64_t t)
{
- struct mb_ecn_ctx* ctx = _ctx;
- bool update;
+ uint64_t tgt;
- (void) len;
+ ctx->tx_ece = ece;
+ ctx->tx_cav = true; /* closed-loop feedback: leave slow start */
- if ((ctx->rx_ece | ecn) == 0)
- return false;
+ /* An unsaturated signal means the queue drained: resume. */
+ if (ece < (uint16_t) CA_ECE_MAX)
+ ctx->ai_hold = false;
- if (ecn == 0) { /* End of congestion */
- ctx->rx_ece >>= 2;
- update = ctx->rx_ece == 0;
- } else {
- if (ctx->rx_ece == 0) { /* Start of congestion */
- ctx->rx_ece = ecn;
- ctx->rx_ctr = 0;
- update = true;
- } else { /* Congestion update */
- ctx->rx_ece -= ctx->rx_ece >> CA_SHFT;
- ctx->rx_ece += ecn;
- update = (ctx->rx_ctr++ & (CA_UPD - 1)) == true;
- }
+ ctx->last_fb = t;
+ ctx->last_sig = t;
+ ctx->n_fb++;
+
+ /* Scale the floor and AI slope to the path bottleneck. */
+ if (cap != 0) {
+ tgt = cap_dec(cap) >> CA_CAP_SHFT;
+ if (tgt < CA_RATE_MIN)
+ tgt = CA_RATE_MIN;
+
+ if (tgt > CA_RMIN_MAX)
+ tgt = CA_RMIN_MAX;
+
+ if (tgt > ctx->rate_min)
+ ctx->rate_min += (tgt - ctx->rate_min)
+ >> CA_CAP_SM_SHFT;
+ else
+ ctx->rate_min -= (ctx->rate_min - tgt)
+ >> CA_CAP_SM_SHFT;
+
+ ctx->ai_rate = 2 * ctx->rate_min;
+ ctx->tx_cap = cap;
+ ctx->last_cap = t;
+ ctx->n_cap++;
}
- *ece = ctx->rx_ece;
+ /* Control from the feedback path: a starved sender recovers. */
+ if (t - ctx->last_ctrl < (uint64_t) CA_DT_CTRL)
+ return;
- return update;
-}
+ mb_ecn_ctrl(ctx, t - ctx->last_ctrl);
+ ctx->last_ctrl = t;
+}
void mb_ecn_ctx_update_ece(void * _ctx,
- uint16_t ece)
+ uint16_t ece,
+ uint8_t cap)
{
- struct mb_ecn_ctx* ctx = _ctx;
+ struct timespec now;
+ struct mb_ecn_ctx * ctx = _ctx;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ mb_ecn_ece(ctx, ece, cap, TS_TO_UINT64(now));
+}
+
+/* Due when the path stayed quiet for a heartbeat interval; arms the gap. */
+bool mb_ecn_ctx_hb_due(void * _ctx,
+ uint64_t now)
+{
+ struct mb_ecn_ctx * ctx = _ctx;
+ uint64_t t_hb = mb_ecn_t_hb(ctx);
+ uint64_t last;
- ctx->tx_ece = ece;
- ctx->tx_ctr = 0;
- ctx->tx_cav = true;
+ last = ctx->last_sig > ctx->last_hb ? ctx->last_sig : ctx->last_hb;
+ if (now - last < t_hb)
+ return false;
+
+ ctx->last_hb = now;
+
+ return true;
}
-int mb_ecn_calc_ecn(int fd,
+/* Fold a heartbeat RTT sample into the ramp clock; also counts as life. */
+void mb_ecn_ctx_rtt(void * _ctx,
+ uint64_t now,
+ uint64_t rtt)
+{
+ struct mb_ecn_ctx * ctx = _ctx;
+ uint64_t tgt;
+
+ tgt = (uint64_t) CA_SS_RTT_MUL * rtt;
+ if (tgt < (uint64_t) CA_SS_TC_MIN) /* track the true RTT both */
+ tgt = CA_SS_TC_MIN; /* ways: overshoot ~e^{1/2} */
+
+ if (tgt > (uint64_t) CA_SS_TC_MAX) /* at the real RTT, not the */
+ tgt = CA_SS_TC_MAX; /* declared worst case */
+
+ /*
+ * A control packet stuck behind a stalled reader returns an RTT
+ * worth seconds on a path worth milliseconds. Cap how far one
+ * sample carries the ramp, so a stall costs a step and a rise
+ * that holds still arrives within a few samples.
+ */
+ if (tgt > ctx->ss_tc << CA_SS_TC_GRW)
+ tgt = ctx->ss_tc << CA_SS_TC_GRW;
+
+ ctx->ss_tc += (tgt >> CA_RTT_SHFT) - (ctx->ss_tc >> CA_RTT_SHFT);
+
+ ctx->last_sig = now; /* liveness only: never ages the ece signal */
+ ctx->n_rtt++;
+}
+
+int mb_ecn_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len)
+ size_t mean)
{
- size_t q;
+ uint64_t u;
+ int q;
+ uint8_t mark;
- (void) len;
(void) qc;
- q = ipcp_flow_queued(fd);
+ if (queued == 0 || mean == 0)
+ return 0;
+
+ u = (uint64_t) CA_MARK_KNEE * mean;
+
+ /*
+ * Difference of two quarter-log2 codes is a log-scale ratio:
+ * the same queue in units of U marks the same on any link.
+ */
+ q = (int) cap_enc(queued) - (int) cap_enc(u);
+ if (q <= 0)
+ return 0;
- *ecn |= (uint8_t) (q >> ECN_Q_SHFT);
+ /* Saturate: a deeper queue must not wrap to a low mark. */
+ mark = q > 255 ? (uint8_t) 255 : (uint8_t) q;
+
+ if (mark > *ecn)
+ *ecn = mark;
return 0;
}
@@ -262,35 +1031,71 @@ ssize_t mb_ecn_print_stats(void * _ctx,
char * buf,
size_t len)
{
- struct mb_ecn_ctx* ctx = _ctx;
- char * regime;
+ struct mb_ecn_ctx * ctx = _ctx;
+ char * regime;
+ uint64_t rate;
+ uint64_t peak;
+ int code;
+ uint16_t m;
- if (len < 1024)
+ if (len < CA_STATS_STRLEN)
return 0;
- if (!ctx->tx_cav)
+ /* No signal seen: the rate is unconstrained drift, not a target. */
+ rate = ctx->tx_cav ? ctx->rate : 0;
+ peak = ctx->tx_cav ? ctx->ss_peak : 0;
+
+ /* Match the controller: MD fires on m, incl. the local fallback. */
+ m = ctx->tx_ece > 0 ? ctx->tx_ece
+ : (uint16_t) (ctx->tx_loc << CA_SHFT);
+
+ if (!ctx->tx_cav) {
regime = "Slow start";
- else if (ctx->tx_ece)
- regime = "Multiplicative dec";
- else
+ code = 0;
+ } else if (ctx->ai_hold) {
+ regime = "Loss recovery";
+ code = 4;
+ } else if (ctx->src_limited) {
+ regime = "Source limited";
+ code = 3;
+ } else if (m > 0) {
+ regime = "Proportional dec";
+ code = 2;
+ } else {
regime = "Additive inc";
+ code = 1;
+ }
sprintf(buf,
"Congestion avoidance algorithm: %20s\n"
"Upstream congestion level: %20u\n"
- "Upstream packet counter: %20zu\n"
"Downstream congestion level: %20u\n"
- "Downstream packet counter: %20zu\n"
- "Congestion window size (ns): %20" PRIu64 "\n"
- "Packets in this window: %20zu\n"
- "Bytes in this window: %20zu\n"
- "Max bytes in this window: %20zu\n"
- "Current congestion regime: %20s\n",
+ "Paced rate (bytes/s): %20" PRIu64 "\n"
+ "Pacer lead (bytes): %20" PRIu64 "\n"
+ "Congestion regime (code): %20d\n"
+ "Current congestion regime: %20s\n"
+ "Control steps (count): %20" PRIu64 "\n"
+ "Control time elapsed (ns): %20" PRIu64 "\n"
+ "Control time banked (ns): %20" PRIu64 "\n"
+ "Feedback updates (count): %20" PRIu64 "\n"
+ "Feedback timeouts (count): %20" PRIu64 "\n"
+ "Path capacity (bytes/s): %20" PRIu64 "\n"
+ "Capacity rate floor (bytes/s): %20" PRIu64 "\n"
+ "Capacity updates (count): %20" PRIu64 "\n"
+ "Slow start peak rate (bytes/s): %20" PRIu64 "\n"
+ "Signal-loss cuts (count): %20" PRIu64 "\n"
+ "Heartbeat RTT samples (count): %20" PRIu64 "\n"
+ "Ramp time constant (ns): %20" PRIu64 "\n",
"Multi-bit ECN",
- ctx->tx_ece, ctx->tx_ctr,
- ctx->rx_ece, ctx->rx_ctr, (uint64_t) (1ULL << ctx->tx_mul),
- ctx->tx_wpc, ctx->tx_wbc, ctx->tx_wbl,
- regime);
+ ctx->tx_ece,
+ ctx->rx_ece,
+ rate, ctx->lead, code,
+ regime,
+ ctx->n_ctrl, ctx->t_ctrl, ctx->t_bank,
+ ctx->n_fb, ctx->n_ttl,
+ cap_dec(ctx->tx_cap), ctx->rate_min, ctx->n_cap,
+ peak,
+ ctx->n_loss, ctx->n_rtt, ctx->ss_tc);
return strlen(buf);
}
diff --git a/src/ipcpd/unicast/ca/mb-ecn.h b/src/ipcpd/unicast/ca/mb-ecn.h
index 1be27764..08bb542d 100644
--- a/src/ipcpd/unicast/ca/mb-ecn.h
+++ b/src/ipcpd/unicast/ca/mb-ecn.h
@@ -25,27 +25,40 @@
#include "ops.h"
+void mb_ecn_init(uint32_t rtt_ms);
+
void * mb_ecn_ctx_create(void);
void mb_ecn_ctx_destroy(void * ctx);
-ca_wnd_t mb_ecn_ctx_update_snd(void * ctx,
- size_t len);
+time_t mb_ecn_ctx_update_snd(void * ctx,
+ size_t len,
+ uint8_t lecn,
+ size_t flows,
+ uint64_t * ftag);
bool mb_ecn_ctx_update_rcv(void * ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece);
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap);
void mb_ecn_ctx_update_ece(void * ctx,
- uint16_t ece);
+ uint16_t ece,
+ uint8_t cap);
+
+bool mb_ecn_ctx_hb_due(void * ctx,
+ uint64_t now);
-void mb_ecn_wnd_wait(ca_wnd_t wnd);
+void mb_ecn_ctx_rtt(void * ctx,
+ uint64_t now,
+ uint64_t rtt);
-int mb_ecn_calc_ecn(int fd,
+int mb_ecn_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len);
+ size_t mean);
ssize_t mb_ecn_print_stats(void * ctx,
char * buf,
diff --git a/src/ipcpd/unicast/ca/nop.c b/src/ipcpd/unicast/ca/nop.c
index e5cacf66..7a2f72db 100644
--- a/src/ipcpd/unicast/ca/nop.c
+++ b/src/ipcpd/unicast/ca/nop.c
@@ -30,8 +30,8 @@ struct ca_ops nop_ca_ops = {
.ctx_update_snd = nop_ctx_update_snd,
.ctx_update_rcv = nop_ctx_update_rcv,
.ctx_update_ece = nop_ctx_update_ece,
- .wnd_wait = nop_wnd_wait,
.calc_ecn = nop_calc_ecn,
+ .marks_ecn = false,
.print_stats = NULL
};
@@ -45,52 +45,55 @@ void nop_ctx_destroy(void * ctx)
(void) ctx;
}
-ca_wnd_t nop_ctx_update_snd(void * ctx,
- size_t len)
+time_t nop_ctx_update_snd(void * ctx,
+ size_t len,
+ uint8_t lecn,
+ size_t flows,
+ uint64_t * ftag)
{
- ca_wnd_t wnd;
-
(void) ctx;
(void) len;
+ (void) lecn;
+ (void) flows;
+ (void) ftag;
- memset(&wnd, 0, sizeof(wnd));
-
- return wnd;
-}
-
-void nop_wnd_wait(ca_wnd_t wnd)
-{
- (void) wnd;
+ return 0;
}
bool nop_ctx_update_rcv(void * ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece)
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap)
{
(void) ctx;
(void) len;
(void) ecn;
+ (void) cap;
(void) ece;
+ (void) fcap;
return false;
}
void nop_ctx_update_ece(void * ctx,
- uint16_t ece)
+ uint16_t ece,
+ uint8_t cap)
{
(void) ctx;
(void) ece;
+ (void) cap;
}
-int nop_calc_ecn(int fd,
+int nop_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len)
+ size_t mean)
{
- (void) fd;
- (void) len;
+ (void) queued;
+ (void) mean;
(void) ecn;
(void) qc;
diff --git a/src/ipcpd/unicast/ca/nop.h b/src/ipcpd/unicast/ca/nop.h
index 8b892e61..386a5310 100644
--- a/src/ipcpd/unicast/ca/nop.h
+++ b/src/ipcpd/unicast/ca/nop.h
@@ -29,23 +29,27 @@ void * nop_ctx_create(void);
void nop_ctx_destroy(void * ctx);
-ca_wnd_t nop_ctx_update_snd(void * ctx,
- size_t len);
+time_t nop_ctx_update_snd(void * ctx,
+ size_t len,
+ uint8_t lecn,
+ size_t flows,
+ uint64_t * ftag);
bool nop_ctx_update_rcv(void * ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece);
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap);
void nop_ctx_update_ece(void * ctx,
- uint16_t ece);
-
-void nop_wnd_wait(ca_wnd_t wnd);
+ uint16_t ece,
+ uint8_t cap);
-int nop_calc_ecn(int fd,
+int nop_calc_ecn(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len);
+ size_t mean);
extern struct ca_ops nop_ca_ops;
diff --git a/src/ipcpd/unicast/ca/ops.h b/src/ipcpd/unicast/ca/ops.h
index 6d2ddf1d..835fe0c5 100644
--- a/src/ipcpd/unicast/ca/ops.h
+++ b/src/ipcpd/unicast/ca/ops.h
@@ -30,23 +30,39 @@ struct ca_ops {
void (* ctx_destroy)(void * ctx);
- ca_wnd_t (* ctx_update_snd)(void * ctx,
- size_t len);
+ time_t (* ctx_update_snd)(void * ctx,
+ size_t len,
+ uint8_t lecn,
+ size_t flows,
+ uint64_t * ftag);
bool (* ctx_update_rcv)(void * ctx,
size_t len,
uint8_t ecn,
- uint16_t * ece);
+ uint8_t cap,
+ uint16_t * ece,
+ uint8_t * fcap);
void (* ctx_update_ece)(void * ctx,
- uint16_t ece);
+ uint16_t ece,
+ uint8_t cap);
+
+ /* Optional, can be NULL: heartbeat pacing + RTT feedback. */
+ bool (* ctx_hb_due)(void * ctx,
+ uint64_t now);
- void (* wnd_wait)(ca_wnd_t wnd);
+ void (* ctx_rtt)(void * ctx,
+ uint64_t now,
+ uint64_t rtt);
- int (* calc_ecn)(int fd,
+ /* queued and mean are bytes; their ratio is packets. */
+ int (* calc_ecn)(size_t queued,
uint8_t * ecn,
qoscube_t qc,
- size_t len);
+ size_t mean);
+
+ /* True if calc_ecn inspects the queue; gates the lookup. */
+ bool marks_ecn;
/* Optional, can be NULL */
ssize_t (* print_stats)(void * ctx,
diff --git a/src/ipcpd/unicast/ca/tests/CMakeLists.txt b/src/ipcpd/unicast/ca/tests/CMakeLists.txt
new file mode 100644
index 00000000..20e2349d
--- /dev/null
+++ b/src/ipcpd/unicast/ca/tests/CMakeLists.txt
@@ -0,0 +1,78 @@
+get_filename_component(CURRENT_SOURCE_PARENT_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(CURRENT_BINARY_PARENT_DIR
+ ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+
+get_filename_component(UNICAST_SOURCE_DIR ${CURRENT_SOURCE_PARENT_DIR} DIRECTORY)
+get_filename_component(UNICAST_BINARY_DIR ${CURRENT_BINARY_PARENT_DIR} DIRECTORY)
+
+get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(PARENT_DIR ${PARENT_PATH} NAME)
+
+compute_test_prefix()
+
+create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
+ # Add new tests here
+ mb_ecn_test.c
+ ca_test.c
+ )
+
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests}
+ ${UNICAST_SOURCE_DIR}/ca.c
+ ${UNICAST_SOURCE_DIR}/cap.c
+ ${CURRENT_SOURCE_PARENT_DIR}/nop.c
+ )
+
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CURRENT_SOURCE_PARENT_DIR}
+ ${CURRENT_BINARY_PARENT_DIR}
+ ${UNICAST_SOURCE_DIR}
+ ${UNICAST_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+ ${CMAKE_SOURCE_DIR}/src/ipcpd
+ ${CMAKE_BINARY_DIR}/src/ipcpd
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test PRIVATE ouroboros-common)
+
+add_dependencies(build_tests ${PARENT_DIR}_test)
+
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
+
+# The lab includes mb-ecn.c for its statics, so it needs its own binary
+create_test_sourcelist(${PARENT_DIR}_lab_tests test_lab_suite.c
+ mb_ecn_lab_test.c
+ )
+
+add_executable(${PARENT_DIR}_lab_test ${${PARENT_DIR}_lab_tests}
+ ${UNICAST_SOURCE_DIR}/cap.c
+ )
+
+target_include_directories(${PARENT_DIR}_lab_test PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CURRENT_SOURCE_PARENT_DIR}
+ ${CURRENT_BINARY_PARENT_DIR}
+ ${UNICAST_SOURCE_DIR}
+ ${UNICAST_BINARY_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+ ${CMAKE_SOURCE_DIR}/src/ipcpd
+ ${CMAKE_BINARY_DIR}/src/ipcpd
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_lab_test)
+target_link_libraries(${PARENT_DIR}_lab_test PRIVATE ouroboros-common)
+
+if(MB_ECN_LAB_FULL)
+ target_compile_definitions(${PARENT_DIR}_lab_test PRIVATE MB_ECN_LAB_FULL)
+endif()
+
+add_dependencies(build_tests ${PARENT_DIR}_lab_test)
+
+ouroboros_register_tests(TARGET ${PARENT_DIR}_lab_test
+ TESTS ${${PARENT_DIR}_lab_tests})
diff --git a/src/ipcpd/unicast/ca/tests/ca_test.c b/src/ipcpd/unicast/ca/tests/ca_test.c
new file mode 100644
index 00000000..1b86eab8
--- /dev/null
+++ b/src/ipcpd/unicast/ca/tests/ca_test.c
@@ -0,0 +1,392 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests for the congestion-avoidance interface
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#include "config.h"
+
+#include "ca.h"
+
+#include <test/test.h>
+
+#define ADDR_A 0x1111ULL
+#define ADDR_B 0x2222ULL
+
+static const struct {
+ enum pol_cong_avoid pol;
+ const char * name;
+} ca_pols[] = {
+ { CA_NONE, "none" },
+ { CA_MB_ECN, "mb-ecn" }
+};
+
+#define CA_POLS (sizeof(ca_pols) / sizeof(ca_pols[0]))
+
+static int test_ca_init_fini(enum pol_cong_avoid pol,
+ const char * name)
+{
+ TEST_START("(%s)", name);
+
+ if (ca_init(pol, 100) < 0) {
+ printf("Failed to init ca for %s.\n", name);
+ goto fail;
+ }
+
+ ca_fini();
+
+ TEST_SUCCESS("(%s)", name);
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL("(%s)", name);
+ return TEST_RC_FAIL;
+}
+
+static int test_ca_init_fini_all(void)
+{
+ int ret = 0;
+ size_t i;
+
+ for (i = 0; i < CA_POLS; i++)
+ ret |= test_ca_init_fini(ca_pols[i].pol, ca_pols[i].name);
+
+ return ret;
+}
+
+static int test_ca_init_invalid(void)
+{
+ TEST_START();
+
+ if (ca_init(CA_INVALID, 100) == 0) {
+ printf("Init accepted an invalid policy.\n");
+ ca_fini();
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_ca_ctx_share(enum pol_cong_avoid pol,
+ const char * name)
+{
+ void * c1;
+ void * c2;
+
+ TEST_START("(%s)", name);
+
+ if (ca_init(pol, 100) < 0) {
+ printf("Failed to init ca for %s.\n", name);
+ goto fail;
+ }
+
+ c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE);
+ if (c1 == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_init;
+ }
+
+ c2 = ca_ctx_get(ADDR_A, QOS_CUBE_BE);
+ if (c2 == NULL) {
+ printf("Failed to get second ctx.\n");
+ goto fail_c1;
+ }
+
+#ifdef IPCP_CA_PER_FLOW
+ if (c1 == c2) {
+ printf("Per-flow build shared a ctx across flows.\n");
+ goto fail_c2;
+ }
+#else
+ if (c1 != c2) {
+ printf("Aggregate build did not share ctx per (addr, qc).\n");
+ goto fail_c2;
+ }
+#endif
+ ca_ctx_put(c2);
+ ca_ctx_put(c1);
+
+ ca_fini();
+
+ TEST_SUCCESS("(%s)", name);
+
+ return TEST_RC_SUCCESS;
+ fail_c2:
+ ca_ctx_put(c2);
+ fail_c1:
+ ca_ctx_put(c1);
+ fail_init:
+ ca_fini();
+ fail:
+ TEST_FAIL("(%s)", name);
+ return TEST_RC_FAIL;
+}
+
+static int test_ca_ctx_share_all(void)
+{
+ int ret = 0;
+ size_t i;
+
+ for (i = 0; i < CA_POLS; i++)
+ ret |= test_ca_ctx_share(ca_pols[i].pol, ca_pols[i].name);
+
+ return ret;
+}
+
+static int test_ca_ctx_distinct(void)
+{
+ void * a_be;
+ void * b_be;
+ void * a_video;
+
+ TEST_START();
+
+ if (ca_init(CA_NONE, 100) < 0) {
+ printf("Failed to init ca.\n");
+ goto fail;
+ }
+
+ a_be = ca_ctx_get(ADDR_A, QOS_CUBE_BE);
+ if (a_be == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_init;
+ }
+
+ b_be = ca_ctx_get(ADDR_B, QOS_CUBE_BE);
+ if (b_be == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_a_be;
+ }
+
+ a_video = ca_ctx_get(ADDR_A, QOS_CUBE_VIDEO);
+ if (a_video == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_b_be;
+ }
+
+ if (a_be == b_be) {
+ printf("Distinct addresses shared a ctx.\n");
+ goto fail_a_video;
+ }
+
+ if (a_be == a_video) {
+ printf("Distinct qos cubes shared a ctx.\n");
+ goto fail_a_video;
+ }
+
+ ca_ctx_put(a_video);
+ ca_ctx_put(b_be);
+ ca_ctx_put(a_be);
+
+ ca_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_a_video:
+ ca_ctx_put(a_video);
+ fail_b_be:
+ ca_ctx_put(b_be);
+ fail_a_be:
+ ca_ctx_put(a_be);
+ fail_init:
+ ca_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Refcount survival is an aggregate-only property. */
+#ifndef IPCP_CA_PER_FLOW
+static int test_ca_ctx_refcount(void)
+{
+ void * c1;
+ void * c3;
+
+ TEST_START();
+
+ if (ca_init(CA_NONE, 100) < 0) {
+ printf("Failed to init ca.\n");
+ goto fail;
+ }
+
+ c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 1 */
+ if (c1 == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_init;
+ }
+
+ if (ca_ctx_get(ADDR_A, QOS_CUBE_BE) == NULL) { /* refs = 2 */
+ printf("Failed to get second ref.\n");
+ goto fail_c1;
+ }
+
+ ca_ctx_put(c1); /* refs = 1 */
+
+ c3 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 2 */
+ if (c3 == NULL) {
+ printf("Failed to get third ref.\n");
+ goto fail_c1;
+ }
+
+ if (c3 != c1) {
+ printf("Refcounted ctx freed while still referenced.\n");
+ goto fail_c3;
+ }
+
+ ca_ctx_put(c3);
+ ca_ctx_put(c1);
+
+ ca_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_c3:
+ ca_ctx_put(c3);
+ fail_c1:
+ ca_ctx_put(c1);
+ fail_init:
+ ca_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The last put frees the interned ctx; a fresh get recreates it. */
+static int test_ca_ctx_recreate(void)
+{
+ void * c1;
+ void * c2;
+ void * c3;
+
+ TEST_START();
+
+ if (ca_init(CA_NONE, 100) < 0) {
+ printf("Failed to init ca.\n");
+ goto fail;
+ }
+
+ c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 1 */
+ if (c1 == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_init;
+ }
+
+ ca_ctx_put(c1); /* refs = 0: freed and de-interned */
+
+ c2 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* fresh entry */
+ if (c2 == NULL) {
+ printf("Get after release did not recreate.\n");
+ goto fail_init;
+ }
+
+ c3 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 2: shares */
+ if (c3 == NULL) {
+ printf("Failed to share recreated ctx.\n");
+ goto fail_c2;
+ }
+
+ if (c3 != c2) {
+ printf("Recreated ctx did not intern.\n");
+ goto fail_c3;
+ }
+
+ ca_ctx_put(c3);
+ ca_ctx_put(c2);
+
+ ca_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_c3:
+ ca_ctx_put(c3);
+ fail_c2:
+ ca_ctx_put(c2);
+ fail_init:
+ ca_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* ca_fini drains a ctx a flow left interned, with no leak. */
+static int test_ca_fini_drains(void)
+{
+ void * c1;
+ void * c2;
+
+ TEST_START();
+
+ if (ca_init(CA_NONE, 100) < 0) {
+ printf("Failed to init ca.\n");
+ goto fail;
+ }
+
+ c1 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 1 */
+ if (c1 == NULL) {
+ printf("Failed to get ctx.\n");
+ goto fail_init;
+ }
+
+ c2 = ca_ctx_get(ADDR_A, QOS_CUBE_BE); /* refs = 2 */
+ if (c2 == NULL) {
+ printf("Failed to get second ref.\n");
+ goto fail_init;
+ }
+
+ /* Leave both refs live: ca_fini must drain and free the ctx. */
+ ca_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_init:
+ ca_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+#endif /* !IPCP_CA_PER_FLOW */
+
+int ca_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_ca_init_fini_all();
+ ret |= test_ca_init_invalid();
+ ret |= test_ca_ctx_share_all();
+ ret |= test_ca_ctx_distinct();
+#ifndef IPCP_CA_PER_FLOW
+ ret |= test_ca_ctx_refcount();
+ ret |= test_ca_ctx_recreate();
+ ret |= test_ca_fini_drains();
+#endif
+ return ret;
+}
diff --git a/src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c b/src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c
new file mode 100644
index 00000000..dac5e8ac
--- /dev/null
+++ b/src/ipcpd/unicast/ca/tests/mb_ecn_lab_test.c
@@ -0,0 +1,1294 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Shared-bottleneck lab for multi-bit ECN congestion avoidance
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#include "mb-ecn.c"
+#include <test/test.h>
+
+#define MS (MILLION) /* one millisecond in ns */
+#define LEN 1000 /* default packet size (bytes) */
+
+/* Create a context with the clock zeroed for deterministic time steps. */
+static struct mb_ecn_ctx * mk_ctx(void)
+{
+ struct mb_ecn_ctx * ctx;
+
+ ctx = mb_ecn_ctx_create();
+ if (ctx == NULL)
+ return NULL;
+
+ ctx->rx_ts = 0;
+ ctx->rx_win = 0;
+ ctx->last_ts = 0;
+ ctx->last_ctrl = 0;
+ ctx->last_fb = 0;
+ ctx->last_sig = 0;
+ ctx->last_loc = 0;
+ ctx->last_cap = 0;
+
+ ctx->snd_byt = 0;
+ ctx->snd_win = 0;
+ ctx->snd_r0 = CA_RATE_INIT;
+ ctx->snd_rate = CA_RATE_INIT;
+ ctx->backlogged = true;
+ ctx->src_limited = false;
+ ctx->started = false;
+ ctx->ss_tc = 20 * MS; /* fixed slope for deterministic SS */
+
+ return ctx;
+}
+
+/*
+ * ------------------------------------------------------------------
+ * Lab: packet-level shared-bottleneck simulator.
+ *
+ * Exact-time FIFO link of capacity cap: a packet departs at
+ * max(enqueue, previous departure) + len / cap. Packets are marked
+ * at enqueue from the instantaneous byte queue by mb_ecn_calc_ecn,
+ * the same function the forwarding path calls. Delivered packets
+ * drive a per-flow receiver estimator (mb_ecn_rcv); every window
+ * close is fed back to the sender as ece after a one-way lag,
+ * including the ece 0 release (fa.c). The sender sees its own
+ * previous packet's mark as the local fallback (fa.c l_ecn) and
+ * heartbeat pongs keep liveness. Greedy flows send whenever the
+ * pacer allows; CBR flows follow an absolute schedule. A tick every
+ * LAB_SAMPLE drains the link between sends, so feedback queued by a
+ * departure is due on time even while every flow sits idle. With
+ * cfg.shared every flow runs on one ctx, as a production build does,
+ * and the flow count follows t0, t1 and the churn period.
+ *
+ * The fixpoint tests assert; the sweep always returns success: an
+ * instrument, not a regression test.
+ * ------------------------------------------------------------------
+ */
+
+#define LAB_MAXF 8 /* most flows on one link */
+#define LAB_FIFO 16384 /* bottleneck ring, packets */
+#define LAB_FBQ 64 /* pending feedback ring */
+#define LAB_NONE UINT64_MAX /* no pending event */
+#define LAB_SAMPLE (5 * MS) /* service tick */
+
+struct lab_pkt {
+ uint64_t dep; /* departure time (ns) */
+ uint8_t ecn;
+ uint8_t f; /* flow index */
+};
+
+struct lab_fb {
+ uint64_t t;
+ uint16_t ece;
+ uint8_t fcap;
+};
+
+struct lab_flow {
+ struct mb_ecn_ctx * snd;
+ struct mb_ecn_ctx * rcv;
+ uint64_t t_snd; /* next send attempt (ns) */
+ uint64_t last; /* ctx clock high-water (ns) */
+ uint64_t ftag;
+ uint64_t ia; /* app interval, 0 = greedy */
+ uint64_t app; /* next app slot (ns) */
+ uint64_t lag; /* feedback one-way lag (ns) */
+ uint8_t lecn; /* own previous packet's mark */
+ struct lab_fb fbq[LAB_FBQ];
+ size_t fb_h;
+ size_t fb_n;
+ uint64_t hb_t; /* pong due, LAB_NONE = none */
+ uint64_t hb_rtt;
+ /* metrics, accumulated past warmup */
+ uint64_t m_t; /* last accounting time */
+ uint64_t dlv; /* delivered bytes */
+ uint64_t dlv2; /* delivered in score window */
+ uint64_t r_int; /* integral of rate dt */
+ uint64_t r_lo;
+ uint64_t r_hi;
+ uint64_t lead_B; /* lead-term cut volume */
+ uint64_t prop_B; /* proportional cut volume */
+ uint64_t cuts; /* >45% single-event cuts */
+ uint64_t hold_ns; /* time with ai_hold set */
+ uint64_t lim_ns; /* time src_limited (latch) */
+ uint64_t idl_ns; /* time the backlog test off */
+};
+
+struct lab_link {
+ uint64_t cap; /* bytes/s */
+ uint8_t cc; /* stamped capacity code */
+ uint64_t t_srv; /* line busy until (ns) */
+ uint64_t q; /* queued bytes */
+ uint64_t qmax; /* blocking threshold (bytes) */
+ struct lab_pkt pk[LAB_FIFO];
+ size_t h;
+ size_t n;
+ /* metrics */
+ uint64_t q_t; /* last q-change time */
+ uint64_t q_int; /* integral of q dt */
+ uint64_t mk_int; /* integral of ece(q) dt */
+ uint64_t e_from; /* empty-dwell start */
+ uint64_t e_ns; /* empty time past warmup */
+ size_t e_eps; /* empty episodes past warmup */
+ uint64_t dlv; /* delivered bytes */
+};
+
+struct lab_cfg {
+ const char * name;
+ uint64_t cap; /* bytes/s */
+ uint64_t dur; /* run length (ns) */
+ uint64_t wu; /* warmup excluded (ns) */
+ size_t len; /* packet size (bytes) */
+ uint64_t qmax; /* bytes */
+ size_t n; /* flows, up to LAB_MAXF */
+ uint64_t ia[LAB_MAXF]; /* app interval, 0 = greedy */
+ uint64_t lag[LAB_MAXF]; /* one-way feedback lag (ns) */
+ bool no_loc; /* disable local-mark path */
+ uint64_t st_d; /* service stall length (ns) */
+ uint64_t st_p; /* stall period, 0 = never */
+ unsigned st_f; /* stalled-flow mask, 0 = all */
+ uint64_t t0[LAB_MAXF]; /* flow start offsets (ns) */
+ uint64_t t1[LAB_MAXF]; /* flow stop, 0 = runs to end */
+ uint64_t r0[LAB_MAXF]; /* seed rate, 0 = slow start */
+ uint64_t sc_lo; /* score window (ns), as the */
+ uint64_t sc_hi; /* integration test scores */
+ bool shared; /* one ctx for every flow */
+ uint64_t ch_p; /* churn period, 0 = never */
+ unsigned ch_f; /* churning flow mask */
+};
+
+static struct lab_link lab_lnk;
+static struct lab_flow lab_fl[LAB_MAXF];
+static uint64_t lab_sc_lo;
+static uint64_t lab_sc_hi;
+static size_t lab_len;
+
+/*
+ * Does flow i hold the ctx at t? A churning flow holds it for the
+ * first half of every ch_p and is gone for the second.
+ */
+static bool lab_up(const struct lab_cfg * c,
+ size_t i,
+ uint64_t t)
+{
+ if (t < c->t0[i])
+ return false;
+
+ if (c->t1[i] > 0 && t >= c->t1[i])
+ return false;
+
+ if (c->ch_p == 0 || ((c->ch_f >> i) & 1) == 0)
+ return true;
+
+ return t % c->ch_p < c->ch_p / 2;
+}
+
+/* Flows holding the ctx at t, the count ca_ctx_get refcounts. */
+static size_t lab_live(const struct lab_cfg * c,
+ uint64_t t)
+{
+ size_t n = 0;
+ size_t i;
+
+ for (i = 0; i < c->n; i++)
+ if (lab_up(c, i, t))
+ n++;
+
+ return n > 0 ? n : 1;
+}
+
+/* The bottleneck marks with the production function, nothing else. */
+static uint8_t lab_mark(uint64_t q)
+{
+ uint8_t e = 0;
+
+ if (q == 0)
+ return 0;
+
+ mb_ecn_calc_ecn(q, &e, QOS_CUBE_BE, lab_len);
+
+ return e;
+}
+
+/* Track the queue integral, the mark integral and empty dwells. */
+static void lab_q_acct(struct lab_link * l,
+ uint64_t now,
+ uint64_t wu)
+{
+ uint64_t dt = now - l->q_t;
+
+ if (l->q_t >= wu && dt > 0) {
+ l->q_int += l->q * dt;
+ l->mk_int += (uint64_t) lab_mark(l->q) * 32 * dt;
+ }
+
+ if (l->q == 0) {
+ if (l->e_from == LAB_NONE)
+ l->e_from = l->q_t;
+ } else if (l->e_from != LAB_NONE) {
+ if (now >= wu) {
+ uint64_t f = l->e_from > wu ? l->e_from : wu;
+ l->e_ns += l->q_t > f ? l->q_t - f : 0;
+ l->e_eps++;
+ }
+ l->e_from = LAB_NONE;
+ }
+
+ l->q_t = now;
+}
+
+/* Deliver everything due; receiver estimator feeds the fb ring. */
+static void lab_service(struct lab_link * l,
+ uint64_t now,
+ uint64_t wu)
+{
+ uint16_t ece;
+ uint8_t fcap;
+
+ while (l->n > 0 && l->pk[l->h].dep <= now) {
+ struct lab_pkt * p = &l->pk[l->h];
+ struct lab_flow * f = &lab_fl[p->f];
+
+ lab_q_acct(l, p->dep, wu);
+ l->q -= lab_len;
+
+ if (p->dep >= wu) {
+ l->dlv += lab_len;
+ f->dlv += lab_len;
+ }
+
+ if (p->dep >= lab_sc_lo && p->dep < lab_sc_hi)
+ f->dlv2 += lab_len;
+
+ if (mb_ecn_rcv(f->rcv, lab_len, p->ecn, l->cc, &ece, &fcap,
+ p->dep) &&
+ f->fb_n < LAB_FBQ) {
+ size_t i = (f->fb_h + f->fb_n++) % LAB_FBQ;
+ f->fbq[i].t = p->dep + f->lag;
+ f->fbq[i].ece = ece;
+ f->fbq[i].fcap = fcap;
+ }
+
+ l->h = (l->h + 1) % LAB_FIFO;
+ l->n--;
+ }
+}
+
+/* Integrate rate, regime dwell and extrema between a flow's events. */
+static void lab_f_acct(struct lab_flow * f,
+ uint64_t now,
+ uint64_t wu)
+{
+ struct mb_ecn_ctx * c = f->snd;
+ uint64_t dt;
+ uint16_t m;
+
+ if (now < f->m_t)
+ now = f->m_t;
+
+ dt = now - f->m_t;
+ if (f->m_t >= wu && dt > 0) {
+ f->r_int += c->rate * dt;
+
+ if (c->ai_hold)
+ f->hold_ns += dt;
+
+ if (c->src_limited)
+ f->lim_ns += dt;
+
+ if (!c->backlogged)
+ f->idl_ns += dt;
+
+ m = c->tx_ece > 0 ? c->tx_ece
+ : (uint16_t) (c->tx_loc << CA_SHFT);
+
+ if (m > CA_ECE_MAX)
+ m = CA_ECE_MAX;
+ f->prop_B += c->rate / CA_ECE_REF * m * dt / BILLION;
+
+ if (c->rate < f->r_lo)
+ f->r_lo = c->rate;
+
+ if (c->rate > f->r_hi)
+ f->r_hi = c->rate;
+ }
+
+ f->m_t = now;
+}
+
+/* One send attempt; returns false when blocked on a full buffer. */
+static bool lab_send(struct lab_link * l,
+ struct lab_flow * f,
+ size_t fi,
+ size_t nf,
+ uint64_t wu,
+ bool no_loc)
+{
+ uint64_t t = f->t_snd;
+ uint64_t r0;
+ uint64_t dep;
+ uint8_t ecn;
+ time_t w;
+
+ lab_service(l, t, wu);
+
+ if (l->q + lab_len > l->qmax) { /* blocking write */
+ f->t_snd = l->pk[l->h].dep;
+ return false;
+ }
+
+ lab_f_acct(f, t, wu);
+
+ ecn = lab_mark(l->q);
+
+ r0 = f->snd->rate;
+
+ mb_ecn_flows(f->snd, nf, t);
+
+ if (!no_loc)
+ mb_ecn_loc(f->snd, f->lecn, t);
+
+ w = mb_ecn_snd(f->snd, lab_len, t, &f->ftag);
+
+ if (f->snd->rate * 100 < r0 * 55)
+ f->cuts++;
+
+ f->lecn = ecn;
+ f->last = t;
+
+ lab_q_acct(l, t, wu);
+
+ dep = (t > l->t_srv ? t : l->t_srv) + lab_len * BILLION / l->cap;
+ l->t_srv = dep;
+ l->pk[(l->h + l->n) % LAB_FIFO].dep = dep;
+ l->pk[(l->h + l->n) % LAB_FIFO].ecn = ecn;
+ l->pk[(l->h + l->n) % LAB_FIFO].f = (uint8_t) fi;
+ l->n++;
+ l->q += lab_len;
+
+ if (mb_ecn_ctx_hb_due(f->snd, t) && f->hb_t == LAB_NONE) {
+ f->hb_rtt = l->q * BILLION / l->cap + 2 * f->lag;
+ f->hb_t = t + f->hb_rtt;
+ }
+
+ if (f->ia == 0) {
+ f->t_snd = t + (w > 0 ? (uint64_t) w : 1);
+ } else {
+ f->app += f->ia;
+ f->t_snd = f->app > t + (uint64_t) w ? f->app
+ : t + (uint64_t) w;
+ }
+
+ return true;
+}
+
+/* Apply one queued feedback to the sender, with lead accounting. */
+static void lab_fb_apply(struct lab_flow * f,
+ uint64_t wu)
+{
+ struct lab_fb * fb = &f->fbq[f->fb_h];
+ uint64_t t = fb->t > f->last ? fb->t : f->last;
+ uint64_t r0 = f->snd->rate;
+ uint16_t step;
+ bool up;
+
+ lab_f_acct(f, t, wu);
+
+ if (t >= wu) {
+ up = fb->ece > f->snd->tx_ecp;
+ step = up ? fb->ece - f->snd->tx_ecp
+ : f->snd->tx_ecp - fb->ece;
+
+ if (step > CA_ECE_REF)
+ step = CA_ECE_REF;
+
+ if (up)
+ f->lead_B += r0 * step
+ / (CA_ECE_REF * CA_MD_KD_DIV);
+ }
+
+ mb_ecn_ece(f->snd, fb->ece, fb->fcap, t);
+
+ if (f->snd->rate * 100 < r0 * 55)
+ f->cuts++;
+
+ f->last = t;
+ f->fb_h = (f->fb_h + 1) % LAB_FBQ;
+ f->fb_n--;
+}
+
+static void lab_run(const struct lab_cfg * c)
+{
+ struct lab_link * l = &lab_lnk;
+ uint64_t smp = 0;
+ uint64_t st_t;
+ uint64_t span;
+ double secs;
+ size_t i;
+
+ memset(l, 0, sizeof(*l));
+
+ l->cap = c->cap;
+ l->cc = cap_enc(c->cap);
+ l->qmax = c->qmax;
+ l->e_from = LAB_NONE;
+
+ memset(lab_fl, 0, sizeof(lab_fl));
+
+ for (i = 0; i < c->n; i++) {
+ struct lab_flow * f = &lab_fl[i];
+
+ /* Production interns one ctx per (peer, qos cube). */
+ if (c->shared && i > 0) {
+ f->snd = lab_fl[0].snd;
+ f->rcv = lab_fl[0].rcv;
+ } else {
+ f->snd = mk_ctx();
+ f->rcv = mk_ctx();
+ }
+
+ if (f->snd == NULL || f->rcv == NULL) {
+ printf("lab: no memory.\n");
+ goto fail_ctx;
+ }
+
+ f->ia = c->ia[i];
+ f->lag = c->lag[i];
+ f->hb_t = LAB_NONE;
+ f->r_lo = UINT64_MAX;
+ f->t_snd = c->t0[i];
+ f->app = c->t0[i];
+ f->m_t = c->t0[i];
+ /* Layer-declared RTT seed; pongs then track truth. */
+ f->snd->ss_tc = 2 * CA_SS_RTT_DEF * MILLION;
+
+ /* Seeded: start in AIMD, so the sweep probes the
+ attractor and not the ramp. */
+ if (c->r0[i] == 0)
+ continue;
+
+ f->snd->rate = c->r0[i];
+ f->snd->inv_rate = mb_ecn_rate_inv(c->r0[i]);
+ f->snd->snd_r0 = c->r0[i];
+ f->snd->snd_rate = c->r0[i];
+ f->snd->tx_cav = true;
+ }
+
+ lab_len = c->len;
+ lab_sc_lo = c->sc_lo;
+ lab_sc_hi = c->sc_hi;
+
+ st_t = c->st_p > 0 ? c->st_p : LAB_NONE;
+
+ while (true) {
+ uint64_t nxt = LAB_NONE;
+ int ev = -1; /* flow * 4 + kind */
+
+ /*
+ * Sender-side service stall: the scheduler feeding
+ * the transmit queue pauses for st_d, the queue
+ * drains clean, and the resume bursts the backlog
+ * through the marker (dsched untrack/starve model).
+ * Jitter the period so it cannot phase-lock.
+ */
+ if (st_t != LAB_NONE && smp >= st_t) {
+ uint64_t end = st_t + c->st_d;
+ unsigned msk = c->st_f == 0 ? 3 : c->st_f;
+
+ for (i = 0; i < c->n; i++)
+ if (((msk >> i) & 1) && lab_fl[i].t_snd < end)
+ lab_fl[i].t_snd = end;
+ st_t += c->st_p + (st_t / c->st_p % 3) * c->st_p / 5;
+ }
+
+ for (i = 0; i < c->n; i++) {
+ struct lab_flow * f = &lab_fl[i];
+
+ if (c->t1[i] > 0 && f->t_snd >= c->t1[i]) {
+ f->t_snd = LAB_NONE;
+ } else if (c->ch_p > 0 && !lab_up(c, i, f->t_snd)) {
+ /* Gone: the app resumes next period. */
+ f->t_snd = (f->t_snd / c->ch_p + 1) * c->ch_p;
+ f->app = f->t_snd;
+ }
+
+ if (f->t_snd < nxt) {
+ nxt = f->t_snd;
+ ev = (int) i * 4;
+ }
+ if (f->fb_n > 0 && f->fbq[f->fb_h].t < nxt) {
+ nxt = f->fbq[f->fb_h].t;
+ ev = (int) i * 4 + 1;
+ }
+ if (f->hb_t < nxt) {
+ nxt = f->hb_t;
+ ev = (int) i * 4 + 2;
+ }
+ }
+
+ if (smp < nxt) {
+ nxt = smp;
+ ev = -2;
+ }
+
+ if (nxt >= c->dur)
+ break;
+
+ if (ev == -2) {
+ lab_service(l, smp, c->wu);
+ smp += LAB_SAMPLE;
+ continue;
+ }
+
+ i = (size_t) (ev / 4);
+ switch (ev % 4) {
+ case 0:
+ (void) lab_send(l, &lab_fl[i], i,
+ c->shared ? lab_live(c, nxt) : 1,
+ c->wu, c->no_loc);
+ break;
+ case 1:
+ lab_fb_apply(&lab_fl[i], c->wu);
+ break;
+ default:
+ lab_f_acct(&lab_fl[i], lab_fl[i].hb_t, c->wu);
+ if (lab_fl[i].hb_t > lab_fl[i].last)
+ lab_fl[i].last = lab_fl[i].hb_t;
+ mb_ecn_ctx_rtt(lab_fl[i].snd, lab_fl[i].last,
+ lab_fl[i].hb_rtt);
+ lab_fl[i].hb_t = LAB_NONE;
+ break;
+ }
+ }
+
+ lab_service(l, c->dur, c->wu);
+ lab_q_acct(l, c->dur, c->wu);
+
+ span = c->dur - c->wu;
+ secs = (double) span / BILLION;
+
+ printf("%-14s C %5.2f MB/s n %zu | util %5.1f%% "
+ "q %6.1f pkt mk %5.1f e%% %4.1f eps %3zu\n",
+ c->name, (double) c->cap / MILLION, c->n,
+ 100.0 * (double) l->dlv / ((double) c->cap * secs),
+ (double) l->q_int / ((double) span * c->len),
+ (double) l->mk_int / (double) span,
+ 100.0 * (double) l->e_ns / (double) span,
+ l->e_eps);
+
+ for (i = 0; i < c->n; i++) {
+ struct lab_flow * f = &lab_fl[i];
+ struct mb_ecn_ctx * s = f->snd;
+
+ lab_f_acct(f, c->dur, c->wu);
+
+ if (c->sc_hi > c->sc_lo)
+ printf(" f%zu score %.3f Mb/s in [%llu,%llu)s\n",
+ i, 8.0 * (double) f->dlv2 /
+ ((double) (c->sc_hi - c->sc_lo) / BILLION
+ * MILLION),
+ (unsigned long long) (c->sc_lo / BILLION),
+ (unsigned long long) (c->sc_hi / BILLION));
+ printf(" f%zu %s dlv %5.3f Mb/s rate mean %8.0f "
+ "lo %8" PRIu64 " hi %8" PRIu64 "\n"
+ " lead %8" PRIu64 " prop %8" PRIu64
+ " cuts %4" PRIu64 " hold %4.1f%% lim %4.1f%%"
+ " idl %4.1f%% loss %" PRIu64 "\n",
+ i, f->ia == 0 ? "gdy" : "cbr",
+ 8.0 * (double) f->dlv / ((double) secs * MILLION),
+ (double) f->r_int / (double) span,
+ f->r_lo == UINT64_MAX ? 0 : f->r_lo, f->r_hi,
+ f->lead_B, f->prop_B, f->cuts,
+ 100.0 * (double) f->hold_ns / (double) span,
+ 100.0 * (double) f->lim_ns / (double) span,
+ 100.0 * (double) f->idl_ns / (double) span,
+ s->n_loss);
+
+ if (c->shared && i > 0)
+ continue;
+
+ mb_ecn_ctx_destroy(f->snd);
+ mb_ecn_ctx_destroy(f->rcv);
+ }
+
+ return;
+ fail_ctx:
+ for (i = 0; i < c->n; i++) {
+ if (c->shared && i > 0)
+ break;
+
+ mb_ecn_ctx_destroy(lab_fl[i].snd);
+ mb_ecn_ctx_destroy(lab_fl[i].rcv);
+ }
+}
+
+/*
+ * Faithful test_cbr_protection / test_single_flow_slow_link protocol:
+ * 3-node chain, bottleneck one hop past the sender (no local mark),
+ * ~2 ms feedback path, CBR from t = 0, greedy joining at 300 ms,
+ * scored over the integration test's own window (slow start and
+ * convergence included, as the real assertion sees them).
+ */
+static void lab_cfg_std(struct lab_cfg * c,
+ const char * name,
+ uint64_t cap,
+ size_t n)
+{
+ size_t i;
+
+ memset(c, 0, sizeof(*c));
+
+ c->name = name;
+ c->cap = cap;
+ c->n = n;
+ c->len = LEN;
+ /* 1024 = SSM_RBUFF_SIZE (cmake/config/lib/ssm.cmake). */
+ c->qmax = 1024 * c->len;
+ c->no_loc = true;
+
+ for (i = 0; i < n; i++)
+ c->lag[i] = 2 * MS;
+
+ if (n == 2) { /* cbr_protection */
+ c->ia[1] = c->len * BILLION / 375000;
+ c->t0[0] = 300 * MS;
+ c->dur = 35ULL * BILLION;
+ c->wu = 30ULL * BILLION;
+ c->sc_lo = 5ULL * BILLION;
+ c->sc_hi = 30ULL * BILLION;
+ } else { /* single_flow_slow_link */
+ c->dur = 95ULL * BILLION;
+ c->wu = 55ULL * BILLION;
+ c->sc_lo = 20ULL * BILLION;
+ c->sc_hi = 50ULL * BILLION;
+ }
+}
+
+/* Spread the seeds may end on and still count as one attractor. */
+#define LAB_FIX_TOL 0.10
+
+/*
+ * Two flows on one bottleneck reach the same split whatever they start
+ * from: the difference mode contracts, so the seed cannot survive in
+ * the answer. A second attractor shows as seeds that disagree, a bias
+ * as agreement away from 1.
+ */
+static int test_mb_ecn_lab_fixpoint(uint64_t cap,
+ uint64_t lag)
+{
+ static struct lab_cfg c;
+ static const unsigned num[] = { 1, 1, 4 };
+ static const unsigned den[] = { 4, 1, 1 };
+ uint64_t kb = cap * 8 / 1000;
+ uint64_t ms = lag / MS;
+ double r[3];
+ double lo;
+ double hi;
+ size_t i;
+
+ TEST_START("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms);
+
+ for (i = 0; i < 3; i++) {
+ lab_cfg_std(&c, "fixpoint", cap, 2);
+
+ /* 10 Gb/s carries 9000 B frames; below 1 Gb/s, 1000 B. */
+ c.len = cap >= 125000000 ? 9000 : LEN;
+ c.qmax = 1024 * c.len;
+
+ c.ia[1] = 0; /* both greedy */
+ c.t0[0] = 0;
+ c.lag[0] = lag;
+ c.lag[1] = lag;
+ c.dur = 60ULL * BILLION;
+ c.wu = 30ULL * BILLION;
+ c.sc_lo = 30ULL * BILLION;
+ c.sc_hi = 60ULL * BILLION;
+ c.r0[0] = cap * num[i] / (num[i] + den[i]);
+ c.r0[1] = cap * den[i] / (num[i] + den[i]);
+
+ lab_run(&c);
+
+ if (lab_fl[1].dlv2 == 0) {
+ printf("seed %u:%u starved a flow.\n", num[i], den[i]);
+ goto fail;
+ }
+
+ r[i] = (double) lab_fl[0].dlv2 / (double) lab_fl[1].dlv2;
+ }
+
+ lo = hi = r[0];
+
+ for (i = 1; i < 3; i++) {
+ if (r[i] < lo)
+ lo = r[i];
+
+ if (r[i] > hi)
+ hi = r[i];
+ }
+
+ if (hi > lo * (1.0 + LAB_FIX_TOL)) {
+ printf("seeds disagree: %.3f %.3f %.3f.\n", r[0], r[1], r[2]);
+ goto fail;
+ }
+
+ if (lo < 1.0 - LAB_FIX_TOL || hi > 1.0 + LAB_FIX_TOL) {
+ printf("split %.3f..%.3f is not fair.\n", lo, hi);
+ goto fail;
+ }
+
+ TEST_SUCCESS("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms);
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms);
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_lab_fixpoint_all(void)
+{
+#ifdef MB_ECN_LAB_FULL
+ static const uint64_t cap[] = { 1250000000, 12500000,
+ 1250000, 62500 };
+#else
+ static const uint64_t cap[] = { 1250000, 62500 };
+#endif
+ static const uint64_t lag[] = { 2 * MS, 42 * MS };
+ int ret = 0;
+ size_t i;
+ size_t j;
+
+ for (i = 0; i < sizeof(cap) / sizeof(cap[0]); i++)
+ for (j = 0; j < sizeof(lag) / sizeof(lag[0]); j++)
+ ret |= test_mb_ecn_lab_fixpoint(cap[i], lag[j]);
+
+ return ret;
+}
+
+/* Seed weights: even, graded, and graded reversed. */
+static const unsigned lab_n8_w[3][LAB_MAXF] = {
+ { 1, 1, 1, 1, 1, 1, 1, 1 },
+ { 1, 2, 3, 4, 5, 6, 7, 8 },
+ { 8, 7, 6, 5, 4, 3, 2, 1 }
+};
+
+/* Spread across eight flows that still counts as one even split. */
+#define LAB_N8_TOL 0.10
+
+/*
+ * Below this, packet-size quantisation dominates the spread, so the
+ * split is not scored here; starvation (lo == 0) and the utilisation
+ * gate still apply, so the exemption is narrow.
+ */
+#define LAB_N8_FAIR 125000 /* bytes/s, 1 Mb/s */
+
+/* Aggregate the link has to carry for a run to say anything at all. */
+#define LAB_N8_UTIL 2 /* divisor: half of capacity */
+
+/*
+ * Eight flows on one bottleneck. The additive increase is per flow, so
+ * both the aggregate probe and the contraction rate scale with the flow
+ * count, and this is where that scaling shows.
+ */
+static int test_mb_ecn_lab_fixpoint_n8(uint64_t cap,
+ uint64_t lag)
+{
+ static struct lab_cfg c;
+ uint64_t kb = cap * 8 / 1000;
+ uint64_t ms = lag / MS;
+ double worst = 1.0;
+ uint64_t want;
+ uint64_t tot;
+ uint64_t sum;
+ uint64_t lo;
+ uint64_t hi;
+ size_t i;
+ size_t j;
+
+ TEST_START("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms);
+
+ for (i = 0; i < 3; i++) {
+ lab_cfg_std(&c, "fixpoint-n8", cap, LAB_MAXF);
+
+ /* 10 Gb/s carries 9000 B frames; below 1 Gb/s, 1000 B. */
+ c.len = cap >= 125000000 ? 9000 : LEN;
+ c.qmax = 1024 * c.len;
+
+ sum = 0;
+
+ for (j = 0; j < LAB_MAXF; j++)
+ sum += lab_n8_w[i][j];
+
+ for (j = 0; j < LAB_MAXF; j++) {
+ c.ia[j] = 0; /* all greedy */
+ c.t0[j] = 0;
+ c.lag[j] = lag;
+ c.r0[j] = cap * lab_n8_w[i][j] / sum;
+ }
+
+ c.dur = 400ULL * BILLION;
+ c.wu = 200ULL * BILLION;
+ c.sc_lo = 200ULL * BILLION;
+ c.sc_hi = 400ULL * BILLION;
+
+ lab_run(&c);
+
+ tot = 0;
+ lo = lab_fl[0].dlv2;
+ hi = lab_fl[0].dlv2;
+
+ for (j = 0; j < LAB_MAXF; j++) {
+ tot += lab_fl[j].dlv2;
+ if (lab_fl[j].dlv2 < lo)
+ lo = lab_fl[j].dlv2;
+
+ if (lab_fl[j].dlv2 > hi)
+ hi = lab_fl[j].dlv2;
+ }
+
+ if (lo == 0) {
+ printf("seed %zu wedged a flow.\n", i);
+ goto fail;
+ }
+
+ want = cap * ((c.sc_hi - c.sc_lo) / BILLION);
+ if (tot < want / LAB_N8_UTIL) {
+ printf("seed %zu carried %" PRIu64 " of %" PRIu64
+ " bytes.\n", i, tot, want);
+ goto fail;
+ }
+
+ if ((double) hi / (double) lo > worst)
+ worst = (double) hi / (double) lo;
+ }
+
+ if (cap >= LAB_N8_FAIR && worst > 1.0 + LAB_N8_TOL) {
+ printf("widest split %.3f across eight flows.\n", worst);
+ goto fail;
+ }
+
+ TEST_SUCCESS("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms);
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL("(%" PRIu64 " kb/s, lag %" PRIu64 " ms)", kb, ms);
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_lab_fixpoint_n8_all(void)
+{
+#ifdef MB_ECN_LAB_FULL
+ static const uint64_t cap[] = { 1250000000, 12500000,
+ 1250000, 62500 };
+#else
+ static const uint64_t cap[] = { 1250000, 62500 };
+#endif
+ static const uint64_t lag[] = { 2 * MS, 42 * MS };
+ int ret = 0;
+ size_t i;
+ size_t j;
+
+ for (i = 0; i < sizeof(cap) / sizeof(cap[0]); i++)
+ for (j = 0; j < sizeof(lag) / sizeof(lag[0]); j++)
+ ret |= test_mb_ecn_lab_fixpoint_n8(cap[i], lag[j]);
+
+ return ret;
+}
+
+/* Jain's fairness index over the score bytes of flows [lo, hi). */
+static double lab_jain(size_t lo,
+ size_t hi)
+{
+ double s = 0.0;
+ double s2 = 0.0;
+ double x;
+ size_t i;
+
+ for (i = lo; i < hi; i++) {
+ x = (double) lab_fl[i].dlv2;
+ s += x;
+ s2 += x * x;
+ }
+
+ if (s2 == 0.0)
+ return 0.0;
+
+ return s * s / ((double) (hi - lo) * s2);
+}
+
+/* Greedy flows on one ctx, all from t = 0, short feedback path. */
+static void lab_cfg_shared(struct lab_cfg * c,
+ const char * name,
+ uint64_t cap,
+ size_t n)
+{
+ size_t i;
+
+ lab_cfg_std(c, name, cap, n);
+
+ c->shared = true;
+
+ for (i = 0; i < n; i++) {
+ c->ia[i] = 0;
+ c->t0[i] = 0;
+ c->lag[i] = 2 * MS;
+ }
+}
+
+/*
+ * ------------------------------------------------------------------
+ * Shared context: n flows, one struct mb_ecn_ctx, one ftag each.
+ *
+ * This is what a production build runs: ca_ctx_get interns one ctx
+ * per (peer, qos cube), so rate, vt and the mark are shared and the
+ * start tag is all a flow owns. The pacer then admits n * rate, so
+ * the attractor for rate is C / n, and the offered load the ctx
+ * measures is n flows' bytes against one flow's rate.
+ * ------------------------------------------------------------------
+ */
+
+/*
+ * Spread the shared attractor may sit in and still count as a
+ * per-flow share. The flows share one virtual clock, so the pacer
+ * fires them in one burst per tick; where that burst is a large part
+ * of the bandwidth-delay product the quarter-log2 mark prices it as
+ * a queue and the loop settles into a deep sawtooth, down to ~0.55
+ * of C / n around 10 Mb/s at 1 kB packets. The band carries that and
+ * is still an order of magnitude under the path rate C.
+ */
+#define LAB_SHR_LO 0.45
+#define LAB_SHR_HI 1.10
+
+/* Peak of the same sawtooth, over the settled window. */
+#define LAB_SHR_PK 1.35
+
+/* One even split across the flows sharing the ctx. */
+#define LAB_SHR_JN 0.98
+
+/*
+ * Greedy flows on one ctx, optionally with half of them joining and
+ * leaving again mid-run. Scores the epoch after the last change.
+ */
+static int test_mb_ecn_lab_shared(uint64_t cap,
+ size_t n,
+ bool churn)
+{
+ static struct lab_cfg c;
+ uint64_t kb = cap * 8 / 1000;
+ uint64_t tc = 20ULL * BILLION;
+ uint64_t fair;
+ uint64_t mean;
+ double jain;
+ size_t nl;
+ size_t i;
+
+ TEST_START("(%" PRIu64 " kb/s, %zu flows%s)", kb, n,
+ churn ? ", churn" : "");
+
+ lab_cfg_shared(&c, churn ? "shr-churn" : "shr-gdy", cap, n);
+
+ nl = churn ? n / 2 : n;
+ /* Half the flows join at tc and are gone again at 2 * tc. */
+ for (i = nl; i < n; i++) {
+ c.t0[i] = tc;
+ c.t1[i] = 2 * tc;
+ }
+
+ fair = cap / nl;
+
+ /*
+ * Warmup ends at the last change, so r_hi is the peak of the
+ * epoch that has to settle back to the new share.
+ */
+ c.dur = 2 * tc + 60ULL * BILLION;
+ c.wu = churn ? 2 * tc : 40ULL * BILLION;
+ c.sc_lo = c.wu + 20ULL * BILLION;
+ c.sc_hi = c.dur;
+
+ lab_run(&c);
+
+ mean = lab_fl[0].r_int / (c.dur - c.wu);
+ jain = lab_jain(0, nl);
+
+ if (mean < (uint64_t) (LAB_SHR_LO * (double) fair)
+ || mean > (uint64_t) (LAB_SHR_HI * (double) fair)) {
+ printf("rate %" PRIu64 " is not a %" PRIu64 " share.\n",
+ mean, fair);
+ goto fail;
+ }
+
+ if (lab_fl[0].r_hi > (uint64_t) (LAB_SHR_PK * (double) fair)) {
+ printf("rate peaked at %.2f of the share.\n",
+ (double) lab_fl[0].r_hi / (double) fair);
+ goto fail;
+ }
+
+ if (jain < LAB_SHR_JN) {
+ printf("fairness %.4f across %zu flows.\n", jain, nl);
+ goto fail;
+ }
+
+ TEST_SUCCESS("(%" PRIu64 " kb/s, %zu flows%s)", kb, n,
+ churn ? ", churn" : "");
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL("(%" PRIu64 " kb/s, %zu flows%s)", kb, n,
+ churn ? ", churn" : "");
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Offered load per flow, as a divisor of the fair share. Low enough
+ * that the aggregate never fills the link, so nothing marks and the
+ * offered-load path is the only thing bounding the rate.
+ */
+#define LAB_SRC_DIV 4
+
+/*
+ * mb_ecn_ceiling admits twice the load ONE flow offers. The slack
+ * covers the additive increase banked between two window closes and
+ * the truncation in sharing the offered bytes out. Reading the whole
+ * ctx's load as one flow's puts the bound n times higher, so a wide
+ * slack still separates the two.
+ */
+#define LAB_SRC_CEIL 2.5
+
+/* Churn half-period; below CA_SND_WIN no window would ever close. */
+#define LAB_SRC_CHP (100 * MS)
+
+/*
+ * Source-limited flows on one ctx. Each offers a fixed rate well
+ * under its share, so mb_ecn_ceiling and the backlog level are all
+ * that bound the rate, and both read the offered load. With churn,
+ * the flows above n / 4 come and go every LAB_SRC_CHP, so a window
+ * that does not restart on the count change never measures one
+ * population.
+ */
+static int test_mb_ecn_lab_shared_load(uint64_t cap,
+ size_t n,
+ bool churn)
+{
+ static struct lab_cfg c;
+ uint64_t kb = cap * 8 / 1000;
+ uint64_t off = cap / (LAB_SRC_DIV * n);
+ uint64_t hi;
+ double jain;
+ size_t ns;
+ size_t i;
+
+ TEST_START("(%" PRIu64 " kb/s, %zu flows%s)", kb, n,
+ churn ? ", churn" : "");
+
+ lab_cfg_shared(&c, churn ? "src-churn" : "src-cbr", cap, n);
+
+ for (i = 0; i < n; i++)
+ c.ia[i] = c.len * BILLION / off;
+
+ if (churn) {
+ c.ch_p = 2 * LAB_SRC_CHP;
+ c.ch_f = ~0u << (n / 4);
+ }
+
+ c.dur = 120ULL * BILLION;
+ c.wu = 60ULL * BILLION;
+ c.sc_lo = 60ULL * BILLION;
+ c.sc_hi = 120ULL * BILLION;
+
+ lab_run(&c);
+
+ hi = lab_fl[0].r_hi;
+
+ /* Score the flows that shared the ctx over the same epochs. */
+ ns = churn ? n / 4 : 0;
+ jain = lab_jain(ns, n);
+
+ if (hi > (uint64_t) (LAB_SRC_CEIL * (double) off)) {
+ printf("rate peaked at %.2f of the %" PRIu64 " offered, "
+ "%.2f of the %" PRIu64 " path.\n",
+ (double) hi / (double) off, off,
+ (double) hi / (double) cap, cap);
+ goto fail;
+ }
+
+ if (jain < LAB_SHR_JN) {
+ printf("fairness %.4f across %zu flows.\n", jain, n - ns);
+ goto fail;
+ }
+
+ TEST_SUCCESS("(%" PRIu64 " kb/s, %zu flows%s)", kb, n,
+ churn ? ", churn" : "");
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL("(%" PRIu64 " kb/s, %zu flows%s)", kb, n,
+ churn ? ", churn" : "");
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_lab(void)
+{
+ static const uint64_t gc_cap[] = { 625000, 1250000, 12500000 };
+ static const char * gc_nm[] = { "gc-5M", "gc-10M", "gc-100M" };
+ static const uint64_t sf_cap[] = { 62500, 125000, 1250000 };
+ static const char * sf_nm[] = { "sf-500k", "sf-1M", "sf-10M" };
+ static const uint64_t g2_cap[] = {
+ 1250000, 1250000, 1250000, 62500, 62500, 62500
+ };
+ static const uint64_t g2_lag[] = {
+ 2 * MS, 20 * MS, 42 * MS, 2 * MS, 20 * MS, 42 * MS
+ };
+ static const char * g2_nm[] = {
+ "g2-10M-2", "g2-10M-20", "g2-10M-42",
+ "g2-500k-2", "g2-500k-20", "g2-500k-42"
+ };
+ static const uint64_t ul_lag[] = { 20 * MS, 42 * MS };
+ static const char * ul_nm[] = { "g2-ul20", "g2-ul42" };
+ static struct lab_cfg c;
+ size_t i;
+
+ TEST_START();
+
+ /*
+ * cbr_protection over capacity: a 3 Mb/s CBR flow shares the
+ * link with a greedy flow joining at 300 ms, so the share the
+ * CBR has to hold runs 60%, 30% and 3% of the link.
+ */
+ for (i = 0; i < 3; i++) {
+ lab_cfg_std(&c, gc_nm[i], gc_cap[i], 2);
+ lab_run(&c);
+ }
+
+ /*
+ * single_flow_slow_link over capacity: one greedy flow alone.
+ * The marking quantum is fixed in bytes, so capacity alone
+ * decides how much queueing delay one ecn step prices.
+ */
+ for (i = 0; i < 3; i++) {
+ lab_cfg_std(&c, sf_nm[i], sf_cap[i], 1);
+ lab_run(&c);
+ }
+
+ /*
+ * Two greedy flows over capacity and equal feedback lag: the
+ * split they settle on and how a long loop degrades it.
+ */
+ for (i = 0; i < 6; i++) {
+ lab_cfg_std(&c, g2_nm[i], g2_cap[i], 2);
+
+ c.ia[1] = 0;
+ c.lag[0] = g2_lag[i];
+ c.lag[1] = g2_lag[i];
+ c.dur = 65ULL * BILLION;
+ c.wu = 35ULL * BILLION;
+
+ lab_run(&c);
+ }
+
+ /* Unequal lag: flow 0 keeps 2 ms, flow 1 reacts slower. */
+ for (i = 0; i < 2; i++) {
+ lab_cfg_std(&c, ul_nm[i], 1250000, 2);
+
+ c.ia[1] = 0;
+ c.lag[1] = ul_lag[i];
+ c.dur = 65ULL * BILLION;
+ c.wu = 35ULL * BILLION;
+
+ lab_run(&c);
+ }
+
+ /*
+ * Service stalls: the scheduler feeding the transmit queue
+ * pauses, the queue drains clean and the resume bursts the
+ * backlog through the marker.
+ */
+ lab_cfg_std(&c, "st-gc", 1250000, 2);
+
+ c.st_d = 60 * MS;
+ c.st_p = 400 * MS;
+
+ lab_run(&c);
+
+ lab_cfg_std(&c, "st-sf", 125000, 1);
+
+ c.st_d = 200 * MS;
+ c.st_p = BILLION;
+
+ lab_run(&c);
+
+ /* Per-flow starvation: only the sparse CBR flow stalls. */
+ lab_cfg_std(&c, "st-pf", 1250000, 2);
+
+ c.st_d = 100 * MS;
+ c.st_p = 300 * MS;
+ c.st_f = 2;
+
+ lab_run(&c);
+
+ /*
+ * Greedy joins 10 s in, once the CBR flow has settled: a step
+ * into contention rather than a shared ramp.
+ */
+ lab_cfg_std(&c, "gc-late", 1250000, 2);
+
+ c.t0[0] = 10ULL * BILLION;
+ c.dur = 45ULL * BILLION;
+ c.wu = 40ULL * BILLION;
+ c.sc_lo = 15ULL * BILLION;
+ c.sc_hi = 40ULL * BILLION;
+
+ lab_run(&c);
+
+ /*
+ * Second greedy flow joins 5 s in: the incumbent has to give
+ * back half to a newcomer that is still in slow start.
+ */
+ lab_cfg_std(&c, "g2-stag", 1250000, 2);
+
+ c.ia[1] = 0;
+ c.t0[0] = 0;
+ c.t0[1] = 5ULL * BILLION;
+ c.dur = 65ULL * BILLION;
+ c.wu = 35ULL * BILLION;
+
+ lab_run(&c);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+}
+
+int mb_ecn_lab_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_mb_ecn_lab_shared(1250000, 5, false);
+ ret |= test_mb_ecn_lab_shared(1250000, 8, false);
+ ret |= test_mb_ecn_lab_shared(1250000, 8, true);
+ ret |= test_mb_ecn_lab_shared_load(1250000, 5, false);
+ ret |= test_mb_ecn_lab_shared_load(1250000, 8, false);
+ ret |= test_mb_ecn_lab_shared_load(1250000, 5, true);
+ ret |= test_mb_ecn_lab_shared_load(1250000, 8, true);
+ ret |= test_mb_ecn_lab_fixpoint_all();
+ ret |= test_mb_ecn_lab_fixpoint_n8_all();
+ ret |= test_mb_ecn_lab();
+
+ return ret;
+}
diff --git a/src/ipcpd/unicast/ca/tests/mb_ecn_test.c b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c
new file mode 100644
index 00000000..7186d3af
--- /dev/null
+++ b/src/ipcpd/unicast/ca/tests/mb_ecn_test.c
@@ -0,0 +1,3156 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests for multi-bit ECN congestion avoidance
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#include "mb-ecn.c"
+
+#include <test/test.h>
+
+#define MS (MILLION) /* one millisecond in ns */
+#define LEN 1000 /* default packet size (bytes) */
+
+/* Create a context with the clock zeroed for deterministic time steps. */
+static struct mb_ecn_ctx * mk_ctx(void)
+{
+ struct mb_ecn_ctx * ctx;
+
+ ctx = mb_ecn_ctx_create();
+ if (ctx == NULL)
+ return NULL;
+
+ ctx->rx_ts = 0;
+ ctx->rx_win = 0;
+ ctx->last_ts = 0;
+ ctx->last_ctrl = 0;
+ ctx->last_fb = 0;
+ ctx->last_sig = 0;
+ ctx->last_loc = 0;
+ ctx->last_cap = 0;
+
+ ctx->snd_byt = 0;
+ ctx->snd_win = 0;
+ ctx->snd_r0 = CA_RATE_INIT;
+ ctx->snd_rate = CA_RATE_INIT;
+ ctx->backlogged = true;
+ ctx->src_limited = false;
+ ctx->started = false;
+ ctx->ss_tc = 20 * MS; /* fixed slope for deterministic SS */
+
+ return ctx;
+}
+
+/*
+ * Drive ctx as a fully backlogged flow: offer a packet every paced
+ * wait, so the offered load tracks the paced rate. Returns end time.
+ */
+static uint64_t drive_backlogged(struct mb_ecn_ctx * ctx,
+ uint64_t * ftag,
+ uint64_t t,
+ uint64_t dur,
+ size_t len)
+{
+ uint64_t end = t + dur;
+ time_t w;
+
+ while (t < end) {
+ w = mb_ecn_snd(ctx, len, t, ftag);
+ t += w > 0 ? (uint64_t) w : 1;
+ }
+
+ return t;
+}
+
+static int test_mb_ecn_ctx_create_destroy(void)
+{
+ struct mb_ecn_ctx * ctx;
+
+ TEST_START();
+
+ ctx = mb_ecn_ctx_create();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ if (ctx->rate != CA_RATE_INIT) {
+ printf("Bad initial rate %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ if (ctx->rate_min != CA_RATE_MIN) {
+ printf("Bad initial floor %" PRIu64 ".\n", ctx->rate_min);
+ goto fail_ctx;
+ }
+
+ if (ctx->vt != 0) {
+ printf("Bad initial virtual clock %" PRIu64 ".\n", ctx->vt);
+ goto fail_ctx;
+ }
+
+ if (ctx->tx_cav) {
+ printf("Context did not start in slow start.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The pricing window derives from the declared RTT. */
+static int test_mb_ecn_init_window(void)
+{
+ TEST_START();
+
+ /* A fabric RTT lands on the floor, not below it. */
+ mb_ecn_init(1);
+
+ if (mb_ecn_tw != CA_TW_MIN) {
+ printf("fabric window %" PRIu64 ".\n", mb_ecn_tw);
+ goto fail;
+ }
+
+ /* A WAN RTT caps the window. */
+ mb_ecn_init(200);
+
+ if (mb_ecn_tw != CA_TW) {
+ printf("wan window %" PRIu64 ".\n", mb_ecn_tw);
+ goto fail;
+ }
+
+ /* An unspecified RTT takes the default and caps the window. */
+ mb_ecn_init(0);
+
+ if (mb_ecn_tw != CA_TW) {
+ printf("default window %" PRIu64 ".\n", mb_ecn_tw);
+ goto fail;
+ }
+
+ mb_ecn_init(CA_SS_RTT_DEF);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ mb_ecn_init(CA_SS_RTT_DEF);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Queue depth (packets) that reads as full congestion. */
+#define FULL_PKTS (CA_MARK_KNEE << ((CA_ECE_REF >> CA_SHFT) / 4))
+
+static int test_mb_ecn_calc_ecn(void)
+{
+ uint8_t ecn;
+
+ TEST_START();
+
+ /* One packet in the queue is the floor: it marks nothing. */
+ ecn = 0;
+
+ mb_ecn_calc_ecn(1400, &ecn, QOS_CUBE_BE, 1400);
+
+ if (ecn != 0) {
+ printf("Single packet marked %u.\n", ecn);
+ goto fail;
+ }
+
+ /* An unknown mean packet size cannot mark. */
+ ecn = 0;
+
+ mb_ecn_calc_ecn(1400, &ecn, QOS_CUBE_BE, 0);
+
+ if (ecn != 0) {
+ printf("Unknown mean marked %u.\n", ecn);
+ goto fail;
+ }
+
+ /* Each doubling of the queue adds 4. */
+ ecn = 0;
+
+ mb_ecn_calc_ecn(2 * 1400, &ecn, QOS_CUBE_BE, 1400);
+
+ if (ecn != 4) {
+ printf("Expected ecn 4 at 2 packets, got %u.\n", ecn);
+ goto fail;
+ }
+
+ /* FULL_PKTS packets is full congestion. */
+ ecn = 0;
+
+ mb_ecn_calc_ecn(FULL_PKTS * 1400, &ecn, QOS_CUBE_BE, 1400);
+
+ if (ecn != (CA_ECE_REF >> CA_SHFT)) {
+ printf("Expected ecn %u at full, got %u.\n",
+ CA_ECE_REF >> CA_SHFT, ecn);
+ goto fail;
+ }
+
+ /* The same packet count marks the same at any packet size. */
+ ecn = 0;
+
+ mb_ecn_calc_ecn(FULL_PKTS * 200, &ecn, QOS_CUBE_BE, 200);
+
+ if (ecn != (CA_ECE_REF >> CA_SHFT)) {
+ printf("Size dependence: exp %u, got %u.\n",
+ CA_ECE_REF >> CA_SHFT, ecn);
+ goto fail;
+ }
+
+ /* MAX keeps the larger value; a smaller mark cannot lower it. */
+ ecn = 0x80;
+
+ mb_ecn_calc_ecn(2 * 1400, &ecn, QOS_CUBE_BE, 1400);
+
+ if (ecn != 0x80) {
+ printf("Expected ecn 0x80, got 0x%x.\n", ecn);
+ goto fail;
+ }
+
+ ecn = 3;
+
+ mb_ecn_calc_ecn(4 * 1400, &ecn, QOS_CUBE_BE, 1400);
+
+ if (ecn != 8) {
+ printf("Expected ecn 8, got %u.\n", ecn);
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The first mark after idle emits the raw value with zero latency. */
+static int test_mb_ecn_rcv_onset_immediate(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ if (!mb_ecn_rcv(ctx, LEN, 4, 0, &ece, &fcap, MS)) {
+ printf("Onset did not update.\n");
+ goto fail_ctx;
+ }
+
+ if (ece != 4 << CA_SHFT) {
+ printf("Onset ece: exp %u, got %u.\n", 4 << CA_SHFT, ece);
+ goto fail_ctx;
+ }
+
+ if (mb_ecn_rcv(ctx, LEN, 4, 0, &ece, &fcap, 2 * MS)) {
+ printf("Mid-window packet updated.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Two flows on the same wall-clock mark timeline, 15x apart in byte
+ * rate: the same congestion estimate, but the faster flow's window is
+ * shorter, so it feeds back more often (cadence tracks byte rate).
+ */
+static int test_mb_ecn_rcv_rate_independent(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint16_t ea;
+ uint16_t eb;
+ uint8_t fcap;
+ size_t ua;
+ size_t ub;
+ size_t i;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ ea = 0;
+ eb = 0;
+ ua = 0;
+ ub = 0;
+
+ /* 300 ms of sustained mark 8; a at 1 kpps, b at ~66 pps. */
+ for (i = 1; i <= 300; i++) {
+ ua += mb_ecn_rcv(a, LEN, 8, 0, &ea, &fcap, i * MS) ? 1 : 0;
+ if (i % 15 != 0)
+ continue;
+
+ ub += mb_ecn_rcv(b, LEN, 8, 0, &eb, &fcap, i * MS) ? 1 : 0;
+ }
+
+ if (ea > eb + 32 || eb > ea + 32) {
+ printf("estimates diverge: %u vs %u.\n", ea, eb);
+ goto fail_ctx;
+ }
+
+ if (ua < ub + 2) {
+ printf("cadence not rate-scaled: %zu vs %zu.\n", ua, ub);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Two flows on one bottleneck, equal byte rate but 7.5x apart in
+ * packet size: the same congestion estimate and the same feedback
+ * cadence. Framing does not skew the control signal (fair share).
+ */
+static int test_mb_ecn_rcv_size_fair(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint16_t ea;
+ uint16_t eb;
+ uint8_t fcap;
+ size_t ua;
+ size_t ub;
+ uint64_t ta;
+ uint64_t tb;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ ea = 0;
+ eb = 0;
+ ua = 0;
+ ub = 0;
+ ta = 0;
+ tb = 0;
+
+ /* 1 MB/s each: a at 200 B / 200 us, b at 1500 B / 1.5 ms. */
+ while (ta < 500 * MS) {
+ ta += 200 * 1000;
+ ua += mb_ecn_rcv(a, 200, 8, 0, &ea, &fcap, ta) ? 1 : 0;
+ }
+
+ while (tb < 500 * MS) {
+ tb += 1500 * 1000;
+ ub += mb_ecn_rcv(b, 1500, 8, 0, &eb, &fcap, tb) ? 1 : 0;
+ }
+
+ if (ea != 8 << CA_SHFT || eb != 8 << CA_SHFT) {
+ printf("size-skewed estimate: %u vs %u.\n", ea, eb);
+ goto fail_ctx;
+ }
+
+ if (ua > ub + 3 || ub > ua + 3) {
+ printf("cadence skewed by size: %zu vs %zu.\n", ua, ub);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Release emits exactly one 0 and leaves the estimator fully idle. */
+static int test_mb_ecn_rcv_release_exact_zero(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ size_t ends;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ for (i = 1; i <= 140; i++)
+ mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, i * MS);
+
+ ends = 0;
+ for (i = 141; i <= 350; i++) {
+ if (!mb_ecn_rcv(ctx, LEN, 0, 0, &ece, &fcap, i * MS))
+ continue;
+
+ if (ece == 0)
+ ends++;
+ }
+
+ if (ends != 1) {
+ printf("end of congestion fired %zu times.\n", ends);
+ goto fail_ctx;
+ }
+
+ if (ctx->rx_ece != 0 || ctx->rx_acc != 0) {
+ printf("estimator not idle: ece %u acc %" PRIu64 ".\n",
+ ctx->rx_ece, ctx->rx_acc);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A gap past the window restarts fresh: no stale, diluted estimate. */
+static int test_mb_ecn_rcv_gap_restart(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint64_t t;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ mb_ecn_rcv(ctx, LEN, 6, 0, &ece, &fcap, MS);
+ mb_ecn_rcv(ctx, LEN, 6, 0, &ece, &fcap, 2 * MS);
+
+ t = 2 * MS + 10 * CA_TW;
+ if (!mb_ecn_rcv(ctx, LEN, 5, 0, &ece, &fcap, t)) {
+ printf("gap restart did not update.\n");
+ goto fail_ctx;
+ }
+
+ if (ece != 5 << CA_SHFT) {
+ printf("gap restart: exp %u, got %u.\n", 5 << CA_SHFT, ece);
+ goto fail_ctx;
+ }
+
+ t += 10 * CA_TW;
+ if (!mb_ecn_rcv(ctx, LEN, 0, 0, &ece, &fcap, t) || ece != 0) {
+ printf("gap with clean packet did not end: %u.\n", ece);
+ goto fail_ctx;
+ }
+
+ if (mb_ecn_rcv(ctx, LEN, 0, 0, &ece, &fcap, t + MS)) {
+ printf("idle packet updated.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * At a floored layer RTT, rx_tw sits at CA_TW_MIN, so 4 * rx_tw is
+ * well under CA_ECE_TTL. A gap in that band must still close the
+ * window as a diluted average, not restart fresh: a fresh restart
+ * always emits the raw undiluted mark (ecn << CA_SHFT), so an ece
+ * that low pins the CA_ECE_TTL floor in mb_ecn_rcv_fresh.
+ */
+static int test_mb_ecn_rcv_gap_floor(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint64_t t;
+
+ TEST_START();
+
+ mb_ecn_init(2);
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ if (ctx->rx_tw != CA_TW_MIN) {
+ printf("window not floored: %" PRIu64 ".\n", ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ /* Onset, then a second packet inside the window: mark banked. */
+ mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, MS);
+ mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, 2 * MS);
+
+ /* 20 ms gap: past 4 * rx_tw (16 ms), well under CA_ECE_TTL. */
+
+ t = 2 * MS + 20 * MS;
+ if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t)) {
+ printf("window did not close.\n");
+ goto fail_ctx;
+ }
+
+ /* A fresh restart would emit the raw mark 8 << CA_SHFT, undiluted. */
+ if (ece >= (8 << CA_SHFT)) {
+ printf("gap read as a fresh onset: ece %u.\n", ece);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+ mb_ecn_init(CA_SS_RTT_DEF);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ mb_ecn_init(CA_SS_RTT_DEF);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Max marks at max gaps: exact ceiling, no overflow past the edge. */
+static int test_mb_ecn_rcv_accum_bounds(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint64_t t;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, MS);
+
+ /* Two packets at dt just under CA_TW straddle the boundary. */
+ t = MS + CA_TW - 1;
+ if (mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, t)) {
+ printf("update before the window closed.\n");
+ goto fail_ctx;
+ }
+
+ t += CA_TW - 1;
+ if (!mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, t)) {
+ printf("no update at the window boundary.\n");
+ goto fail_ctx;
+ }
+
+ if (ece != 15 << CA_SHFT) {
+ printf("ceiling: exp %u, got %u.\n", 15 << CA_SHFT, ece);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * The window floors at CA_TW, tracks the rate below the knee,
+ * and only a pathological fold hits the CA_TW_ABSMAX ceiling.
+ */
+static int test_mb_ecn_rcv_window_clip_bounds(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint64_t ia;
+ uint64_t t;
+ size_t closes;
+
+ TEST_START();
+
+ /* 1 GbE is above the high knee: the window floors at CA_TW. */
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ia = 8000ULL * BILLION / 1000000000ULL;
+ t = 0;
+ closes = 0;
+ while (closes < 40) {
+ t += ia;
+ if (mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t))
+ closes++;
+ }
+
+ if (ctx->rx_tw != CA_TW) {
+ printf("high-rate window: exp %" PRIu64 ", got %" PRIu64
+ ".\n", (uint64_t) CA_TW, ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ /* 1 Mbps: below the knee, ~16 pkts = 16 * 8 ms = 131 ms. */
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ia = 8000ULL * BILLION / 1000000ULL;
+ t = 0;
+ closes = 0;
+ while (closes < 40) {
+ t += ia;
+ if (mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t))
+ closes++;
+ }
+
+ if (ctx->rx_tw < 120 * MS || ctx->rx_tw > 140 * MS) {
+ printf("low-rate window: exp ~131 ms, got %" PRIu64 ".\n",
+ ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ /* A near-empty window folds a huge target: ceiling holds. */
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ mb_ecn_rcv(ctx, 10, 8, 0, &ece, &fcap, MS);
+ mb_ecn_rcv(ctx, 10, 8, 0, &ece, &fcap, MS + CA_TW);
+
+ if (ctx->rx_tw != CA_TW_ABSMAX) {
+ printf("window ceiling breached: %" PRIu64 ".\n",
+ ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A CA-limited slow flow grows its window to hold ~16 packets. */
+static int test_mb_ecn_rcv_slow_window(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* 1400 B every 171 ms (~8 KB/s), sustained mark 8. */
+ for (i = 1; i <= 100; i++)
+ mb_ecn_rcv(ctx, 1400, 8, 0, &ece, &fcap, i * 171 * MS);
+
+ /* Target window 16 * 1000 B at 8187 B/s ~= 2.0 s. */
+ if (ctx->rx_tw < 1400 * MS || ctx->rx_tw > 2800 * MS) {
+ printf("slow window: exp ~2 s, got %" PRIu64 ".\n",
+ ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ /* Steady mark 8 emits exactly 256 once the window settles. */
+ if (ece != 8 << CA_SHFT) {
+ printf("slow-flow estimate: exp %u, got %u.\n",
+ 8 << CA_SHFT, ece);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A physically maximal window must fold without overflow or wrap. */
+static int test_mb_ecn_rcv_no_overflow_highrate(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ bool ok;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Open a window, then inject a maximal byte count and span. */
+ mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, 0);
+ ctx->rx_byt = CA_RATE_MAX / 8;
+ ctx->rx_ts = 2 * CA_TW - 2;
+
+ ok = mb_ecn_rcv(ctx, LEN, 15, 0, &ece, &fcap, 2 * CA_TW - 1);
+
+ if (!ok) {
+ printf("max-window close did not fire.\n");
+ goto fail_ctx;
+ }
+
+ if (ece > 8160) {
+ printf("estimate %u wrapped.\n", ece);
+ goto fail_ctx;
+ }
+
+ /* A wrapped numerator drives rx_tw to MAX; it must descend. */
+ if (ctx->rx_tw > CA_TW || ctx->rx_tw < CA_TW) {
+ printf("window %" PRIu64 " did not descend.\n", ctx->rx_tw);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * The sender holds a mark across the full inter-feedback gap (TTL >
+ * 2 * CA_TW); a repeated mark adds only the proportional cut.
+ */
+static int test_mb_ecn_ece_ttl_covers_cadence(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t prev;
+ uint64_t t;
+ uint64_t ftag = 0;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = (uint64_t) 100 << 20;
+ ctx->tx_cav = true;
+
+ mb_ecn_ece(ctx, 100, 0, MS);
+ mb_ecn_snd(ctx, LEN, MS, &ftag);
+
+ /* Sends between feedbacks spaced 2 * CA_TW + 5 ms apart. */
+ t = MS;
+ for (i = 0; i < 4; i++) {
+ t += (2 * CA_TW + 5 * MS) / 4;
+ mb_ecn_snd(ctx, LEN, t, &ftag);
+ if (ctx->tx_ece == 0) {
+ printf("mark cleared inside the feedback gap.\n");
+ goto fail_ctx;
+ }
+ }
+
+ /* Same mark again: rise 0, so only the proportional cut. */
+ prev = ctx->rate;
+ t += MS;
+ mb_ecn_ece(ctx, 100, 0, t);
+ mb_ecn_snd(ctx, LEN, t, &ftag);
+
+ if (prev - ctx->rate > prev / 100) {
+ printf("phantom lead cut: %" PRIu64 " -> %" PRIu64 ".\n",
+ prev, ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_slow_start(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t prev;
+ uint64_t t;
+ uint64_t ftag = 0;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ prev = ctx->rate;
+ t = 0;
+
+ /* No feedback: the flow stays in slow start and grows each step. */
+ for (i = 0; i < 16; i++) {
+ t += MS;
+ mb_ecn_snd(ctx, LEN, t, &ftag);
+ if (ctx->rate <= prev) {
+ printf("rate did not grow: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ prev = ctx->rate;
+ }
+
+ /* Exponential ramp doubles in ~ln2 * CA_SS_TC ~= 14 ms. */
+ if (ctx->rate < 2 * CA_RATE_INIT) {
+ printf("slow start too slow: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_dt_scaling_invariant(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint64_t inc_a;
+ uint64_t inc_b;
+ uint64_t t;
+ uint64_t fta = 0;
+ uint64_t ftb = 0;
+ size_t i;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ /* Leave slow start; seed a realistic rate (truncation-free). */
+ mb_ecn_ece(a, 0, 0, 0);
+ mb_ecn_ece(b, 0, 0, 0);
+ a->rate = (uint64_t) 10 << 20;
+ b->rate = (uint64_t) 10 << 20;
+
+ /* a: one 30 ms step. */
+ mb_ecn_snd(a, LEN, 30 * MS, &fta);
+
+ /* b: thirty 1 ms steps over the same 30 ms. */
+ t = 0;
+ for (i = 0; i < 30; i++) {
+ t += MS;
+ mb_ecn_snd(b, LEN, t, &ftb);
+ }
+
+ inc_a = a->rate - ((uint64_t) 10 << 20);
+ inc_b = b->rate - ((uint64_t) 10 << 20);
+
+ /* Equal within 1 %; the small gap is per-step integer truncation. */
+ if (inc_a == 0 || inc_b == 0) {
+ printf("no additive increase: %" PRIu64 " %" PRIu64 ".\n",
+ inc_a, inc_b);
+ goto fail_ctx;
+ }
+
+ if (inc_a > inc_b + inc_a / 100 || inc_b > inc_a + inc_a / 100) {
+ printf("cadence-dependent AI: %" PRIu64 " vs %" PRIu64 ".\n",
+ inc_a, inc_b);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_multiplicative_decrease(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t prev;
+ uint64_t t;
+ uint64_t ftag = 0;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = (uint64_t) 100 << 20;
+ prev = ctx->rate;
+ t = 0;
+
+ for (i = 0; i < 10; i++) {
+ t += MS;
+ mb_ecn_ece(ctx, CA_ECE_REF, 0, t);
+ mb_ecn_snd(ctx, LEN, t, &ftag);
+ if (ctx->rate >= prev) {
+ printf("rate did not shrink: %" PRIu64 ".\n",
+ ctx->rate);
+ goto fail_ctx;
+ }
+
+ prev = ctx->rate;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The hold releases on any unsaturated mark, including above REF. */
+static int test_mb_ecn_ai_hold_release(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t r0 = (uint64_t) 100 << 20;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* A saturated mark keeps the hold: the queue has not drained. */
+ ctx->ai_hold = true;
+
+ mb_ecn_ece(ctx, CA_ECE_MAX, 0, MS);
+
+ if (!ctx->ai_hold) {
+ printf("saturated feedback released the hold.\n");
+ goto fail_ctx;
+ }
+
+ /* A standing mark of 20 flows is unsaturated: release. */
+ ctx->ai_hold = true;
+
+ mb_ecn_ece(ctx, 22 << CA_SHFT, 0, 2 * MS);
+
+ if (ctx->ai_hold) {
+ printf("unsaturated feedback held the increase.\n");
+ goto fail_ctx;
+ }
+
+ /* The decrease still scales with the mark saturated at CA_ECE_MAX. */
+ ctx->rate = r0;
+ ctx->tx_cav = true;
+ ctx->tx_ece = CA_ECE_MAX;
+ ctx->tx_ecp = CA_ECE_MAX;
+ ctx->dec_acc = 0;
+
+ mb_ecn_decrease(ctx, MILLION);
+
+ if (ctx->rate > r0 - r0 / 700) {
+ printf("clamped decrease too weak: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_rate_floor(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /*
+ * A starved 600 ms at full mark takes the rate/2 branch, far
+ * below the floor; stays inside the initial ~976 ms mark TTL.
+ */
+ ctx->rate = CA_RATE_MIN + 1000;
+ mb_ecn_ece(ctx, CA_ECE_REF, 0, 0);
+ mb_ecn_snd(ctx, LEN, 600 * MS, &ftag);
+
+ if (ctx->rate != CA_RATE_MIN) {
+ printf("rate floor breached: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_mb_ecn_fixed_point(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t exp;
+ uint64_t t;
+ uint64_t ftag = 0;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ exp = CA_AI_RATE * CA_ECE_REF /
+ (128 - CA_ECE_REF * BILLION / CA_PROBE_TC);
+ t = 0;
+
+ /* ~20 s: the probe raises the loop time constant to CA_PROBE_TC. */
+ for (i = 0; i < 20000; i++) {
+ t += MS;
+ mb_ecn_ece(ctx, 128, 0, t);
+ mb_ecn_snd(ctx, LEN, t, &ftag);
+ }
+
+ if (ctx->rate < exp - exp / 4 || ctx->rate > exp + exp / 4) {
+ printf("no fixed point: exp ~%" PRIu64 ", got %" PRIu64 ".\n",
+ exp, ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The lead is two-sided: it cuts on a rise and gives back on a fall. */
+static int test_mb_ecn_lead_symmetric(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t r0 = (uint64_t) 100 << 20;
+ uint64_t net;
+ uint64_t drop;
+ uint64_t gain;
+ uint64_t kd2;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->tx_cav = true;
+
+ /* Rise of one reference: cut rate / CA_MD_KD_DIV, no more. */
+ ctx->rate = r0;
+ ctx->tx_ece = CA_ECE_REF;
+ ctx->tx_ecp = 0;
+ ctx->dec_acc = 0;
+
+ mb_ecn_decrease(ctx, 0);
+
+ drop = r0 - ctx->rate;
+ if (drop != r0 / CA_MD_KD_DIV) {
+ printf("rise cut %" PRIu64 ", want %" PRIu64 ".\n",
+ drop, r0 / CA_MD_KD_DIV);
+ goto fail_ctx;
+ }
+
+ /* Fall of one reference: give the same fraction back. */
+ ctx->rate = r0;
+ ctx->tx_ece = 1;
+ ctx->tx_ecp = CA_ECE_REF + 1;
+ ctx->dec_acc = 0;
+
+ mb_ecn_decrease(ctx, 0);
+
+ gain = ctx->rate - r0;
+ if (gain != r0 / CA_MD_KD_DIV) {
+ printf("fall boost %" PRIu64 ", want %" PRIu64 ".\n",
+ gain, r0 / CA_MD_KD_DIV);
+ goto fail_ctx;
+ }
+
+ /* A collapse from deep saturation is clamped to the same. */
+ ctx->rate = r0;
+ ctx->tx_ece = 1;
+ ctx->tx_ecp = 255 << CA_SHFT;
+ ctx->dec_acc = 0;
+
+ mb_ecn_decrease(ctx, 0);
+
+ gain = ctx->rate - r0;
+ if (gain != r0 / CA_MD_KD_DIV) {
+ printf("unclamped fall boost %" PRIu64 ".\n", gain);
+ goto fail_ctx;
+ }
+
+ /* A cycle that stays marked nets out: no standing bias. */
+ ctx->rate = r0;
+ ctx->tx_ecp = 4 << CA_SHFT;
+ ctx->tx_ece = 12 << CA_SHFT;
+ ctx->dec_acc = 0;
+
+ mb_ecn_decrease(ctx, 0);
+
+ ctx->tx_ece = 4 << CA_SHFT;
+ ctx->dec_acc = 0;
+
+ mb_ecn_decrease(ctx, 0);
+
+ net = ctx->rate > r0 ? ctx->rate - r0 : r0 - ctx->rate;
+ /*
+ * The two lead steps compound to (1 - x)(1 + x), x = 1 /
+ * (2 * CA_MD_KD_DIV), so net ~= r0 / (4 * CA_MD_KD_DIV^2).
+ * Band it a factor of 2 either side so a materially weaker
+ * gain (e.g. KD off by a factor of 4) fails the floor.
+ */
+ kd2 = (uint64_t) CA_MD_KD_DIV * CA_MD_KD_DIV;
+ if (net > r0 / (2 * kd2)) {
+ printf("cycle bias %" PRIu64 " of %" PRIu64 ".\n", net, r0);
+ goto fail_ctx;
+ }
+
+ if (net < r0 / (8 * kd2)) {
+ printf("lead gain weaker than expected: net %" PRIu64
+ " of %" PRIu64 ".\n", net, r0);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A local first-hop mark exits slow start with no feedback needed. */
+static int test_mb_ecn_slow_start_local_brake(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t prev;
+ uint64_t ftag = 0;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ for (i = 1; i <= 50; i++)
+ mb_ecn_snd(ctx, LEN, i * MS, &ftag);
+
+ if (ctx->tx_cav) {
+ printf("Left slow start without any signal.\n");
+ goto fail_ctx;
+ }
+
+ prev = ctx->rate;
+ mb_ecn_loc(ctx, 1, 50 * MS);
+ if (!ctx->tx_cav) {
+ printf("Local mark did not exit slow start.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_snd(ctx, LEN, 51 * MS, &ftag);
+ if (ctx->rate > prev + prev / 20) {
+ printf("SS ramp survived the brake: %" PRIu64 ".\n",
+ ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A clean path still ramps to line rate in well under a second. */
+static int test_mb_ecn_slow_start_clean_ramp(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Backlogged, no marks: slow start sprints in a couple windows. */
+ drive_backlogged(ctx, &ftag, MS, 2 * CA_SND_WIN, LEN);
+
+ if (ctx->rate < (1ULL << 24)) {
+ printf("backlogged slow start too slow: %" PRIu64 ".\n",
+ ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A sender starved of send-path control steps recovers through the
+ * feedback path: honest elapsed time, at most a 50% cut per step.
+ */
+static int test_mb_ecn_starved_decrease_escape(void)
+{
+ struct mb_ecn_ctx * ctx;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = (uint64_t) 100 << 20;
+ ctx->tx_cav = true;
+
+ /* Feedback arrives once per second; no sends at all. */
+ for (i = 1; i <= 6; i++)
+ mb_ecn_ece(ctx, 480, 0, i * BILLION);
+
+ if (ctx->rate > (5ULL << 19)) {
+ printf("still wedged at %" PRIu64 " B/s.\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ if (ctx->rate < CA_RATE_MIN) {
+ printf("rate floor breached: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* With feedback fully dead, the local mark alone recovers the rate. */
+static int test_mb_ecn_starved_local_fallback(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t t;
+ uint64_t ftag = 0;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = (uint64_t) 100 << 20;
+
+ for (i = 1; i <= 7; i++) {
+ t = i * BILLION;
+ mb_ecn_loc(ctx, 15, t);
+ mb_ecn_snd(ctx, LEN, t, &ftag);
+ }
+
+ if (!ctx->tx_cav) {
+ printf("Local mark did not exit slow start.\n");
+ goto fail_ctx;
+ }
+
+ if (ctx->rate > (5ULL << 19)) {
+ printf("still wedged at %" PRIu64 " B/s.\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * MD Δt-invariance: the same elapsed time under the same mark cuts
+ * the same, in one big step or five small ones.
+ */
+static int test_mb_ecn_decrease_dt_invariant(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint64_t cut_a;
+ uint64_t cut_b;
+ uint64_t r0;
+ uint64_t fta = 0;
+ uint64_t ftb = 0;
+ size_t i;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ r0 = (uint64_t) 100 << 20;
+ a->rate = r0;
+ b->rate = r0;
+
+ mb_ecn_ece(a, 256, 0, 0);
+ mb_ecn_ece(b, 256, 0, 0);
+
+ /* a: one 50 ms step; b: five 10 ms steps (both within DT_CAP). */
+ mb_ecn_snd(a, LEN, 50 * MS, &fta);
+
+ for (i = 1; i <= 5; i++)
+ mb_ecn_snd(b, LEN, i * 10 * MS, &ftb);
+
+ cut_a = r0 - a->rate;
+ cut_b = r0 - b->rate;
+
+ if (cut_a == 0 || cut_b == 0) {
+ printf("no cut: %" PRIu64 " %" PRIu64 ".\n", cut_a, cut_b);
+ goto fail_ctx;
+ }
+
+ /* Within 10%: residual is Euler compounding of MD and the probe. */
+ if (cut_a > cut_b + cut_a / 10 || cut_b > cut_a + cut_a / 10) {
+ printf("cadence-dependent MD: %" PRIu64 " vs %" PRIu64
+ ".\n", cut_a, cut_b);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Sub-ms control steps must not lose decrease time to truncation. */
+static int test_mb_ecn_decrease_subms_carry(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint64_t cut_a;
+ uint64_t cut_b;
+ uint64_t r0;
+ uint64_t fta = 0;
+ uint64_t ftb = 0;
+ size_t i;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ r0 = (uint64_t) 100 << 20;
+ a->rate = r0;
+ b->rate = r0;
+
+ mb_ecn_ece(a, 256, 0, 0);
+ mb_ecn_ece(b, 256, 0, 0);
+
+ /* Same 30 ms of marked time; b's steps have a 0.5 ms tail. */
+ for (i = 1; i <= 10; i++)
+ mb_ecn_snd(a, LEN, i * 3 * MS, &fta);
+
+ for (i = 1; i <= 20; i++)
+ mb_ecn_snd(b, LEN, i * 3 * MS / 2, &ftb);
+
+ cut_a = r0 - a->rate;
+ cut_b = r0 - b->rate;
+
+ if (cut_a == 0 || cut_b == 0) {
+ printf("no cut: %" PRIu64 " %" PRIu64 ".\n", cut_a, cut_b);
+ goto fail_ctx;
+ }
+
+ /* Within 10%: the sub-ms remainder must carry, not vanish. */
+ if (cut_a > cut_b + cut_a / 10 || cut_b > cut_a + cut_a / 10) {
+ printf("sub-ms decrease lost: %" PRIu64 " vs %" PRIu64
+ ".\n", cut_a, cut_b);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Resuming after a long idle gap: bounded AI, no cut from stale marks. */
+static int test_mb_ecn_idle_resume_bounded(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t prev;
+ uint64_t bump;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = (uint64_t) 10 << 20;
+
+ mb_ecn_loc(ctx, 15, MS);
+ mb_ecn_ece(ctx, 480, 0, MS);
+
+ prev = ctx->rate;
+
+ /* 600 s later: both signals stale; one capped AI + probe step. */
+ mb_ecn_snd(ctx, LEN, 600 * BILLION, &ftag);
+
+ if (ctx->rate < prev) {
+ printf("stale mark cut the rate: %" PRIu64 ".\n",
+ ctx->rate);
+ goto fail_ctx;
+ }
+
+ bump = CA_AI_RATE * CA_DT_CAP / BILLION;
+ bump += (prev + bump) * CA_DT_CAP / CA_PROBE_TC;
+
+ if (ctx->rate > prev + bump + 2) {
+ printf("idle resume cap breached: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The congestion signal ages out on wall-clock time, not packet count. */
+static int test_mb_ecn_ece_staleness(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* A fast flow's control step caches the floor TTL. */
+ ctx->rate = (uint64_t) 1 << 20;
+ ctx->tx_cav = true;
+ mb_ecn_snd(ctx, LEN, MS, &ftag);
+
+ if (ctx->ece_ttl != CA_ECE_TTL) {
+ printf("Fast-flow TTL: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ (uint64_t) CA_ECE_TTL, ctx->ece_ttl);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ece(ctx, CA_ECE_REF, 0, 2 * MS);
+
+ /* Just inside the TTL: the signal is still held. */
+ mb_ecn_snd(ctx, LEN, 2 * MS + ctx->ece_ttl, &ftag);
+ if (ctx->tx_ece == 0) {
+ printf("signal aged out too early.\n");
+ goto fail_ctx;
+ }
+
+ /* Past the TTL without feedback: the signal is cleared. */
+ mb_ecn_snd(ctx, LEN, 2 * MS + ctx->ece_ttl + 1, &ftag);
+ if (ctx->tx_ece != 0) {
+ printf("stale signal not cleared: %u.\n", ctx->tx_ece);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The staleness horizon stretches with a slow flow's window. */
+static int test_mb_ecn_ece_ttl_tracks_rate(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t want;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = 8192;
+ ctx->rate_min = 8192;
+ ctx->ai_rate = 0;
+ ctx->tx_cav = true;
+ mb_ecn_snd(ctx, 1400, MS, &ftag);
+
+ want = (1 << CA_TW_GAP_SHFT) * CA_RX_WBYTES * BILLION / ctx->rate;
+ if (ctx->ece_ttl != want) {
+ printf("Slow-flow TTL: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ want, ctx->ece_ttl);
+ goto fail_ctx;
+ }
+
+ if (ctx->ece_ttl < 7 * (uint64_t) BILLION) {
+ printf("TTL did not stretch: %" PRIu64 ".\n",
+ ctx->ece_ttl);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * First packet of a flow starts at the clock; a same-instant second
+ * packet leads by its length and is paced by lead / rate.
+ */
+static int test_mb_ecn_sfq_pace(void)
+{
+ struct mb_ecn_ctx * ctx;
+ time_t wait;
+ uint64_t ftag = 0;
+ time_t want;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = 1U << 20;
+ ctx->inv_rate = mb_ecn_rate_inv(ctx->rate);
+
+ /* First send: start tag equals the clock, so no wait. */
+ wait = mb_ecn_snd(ctx, 1500, 0, &ftag);
+ if (wait != 0) {
+ printf("first packet waited %ld, expected 0.\n", (long) wait);
+ goto fail_ctx;
+ }
+
+ /* Same instant (dt = 0): the flow now leads by 1500 B. */
+ wait = mb_ecn_snd(ctx, 1500, 0, &ftag);
+ want = (time_t) ((uint64_t) 1500 * BILLION / ctx->rate);
+
+ if (wait != want) {
+ printf("paced wait %ld, expected %ld.\n",
+ (long) wait, (long) want);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * The proportional probe grows the rate by the same fraction per unit
+ * time regardless of the absolute rate: two clean flows 100x apart in
+ * rate grow by the same ratio. Deleting the probe leaves only the tiny
+ * additive increase, failing the growth floor.
+ */
+static int test_mb_ecn_probe_scale_invariant(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint64_t ra0;
+ uint64_t rb0;
+ double ga;
+ double gb;
+ uint64_t t;
+ size_t i;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ /* Clean path (mark 0), out of slow start, backlogged, 100x apart. */
+ mb_ecn_ece(a, 0, 0, 0);
+ mb_ecn_ece(b, 0, 0, 0);
+ a->rate = (uint64_t) 10 << 20;
+ b->rate = (uint64_t) 1000 << 20;
+ a->backlogged = true;
+ b->backlogged = true;
+ ra0 = a->rate;
+ rb0 = b->rate;
+
+ /* Drive control via the feedback path so backlogged stays set. */
+ t = 0;
+ for (i = 0; i < 500; i++) {
+ t += MS;
+ mb_ecn_ece(a, 0, 0, t);
+ mb_ecn_ece(b, 0, 0, t);
+ }
+
+ ga = (double) a->rate / ra0;
+ gb = (double) b->rate / rb0;
+
+ if (ga < gb - gb / 50 || gb < ga - ga / 50) {
+ printf("probe not scale-invariant: %.4f vs %.4f.\n", ga, gb);
+ goto fail_ctx;
+ }
+
+ /* And it must actually grow: the probe is present, not deleted. */
+ if (ga < 1.05) {
+ printf("probe did not grow the rate: %.4f.\n", ga);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * The proportional probe e-folds the rate over CA_PROBE_TC: a clean flow
+ * grows by ~e in 8 s. Pinned to a literal e-band so a mistuned
+ * CA_PROBE_TC (e.g. 4 s gives e^2) fails.
+ */
+static int test_mb_ecn_probe_time_constant(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t r0;
+ double ratio;
+ uint64_t t;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Clean path, out of slow start, backlogged, below the ceiling. */
+ mb_ecn_ece(ctx, 0, 0, 0);
+
+ ctx->rate = (uint64_t) 1 << 30;
+ ctx->backlogged = true;
+ r0 = ctx->rate;
+
+ /* 8000 x 1 ms of clean growth, driven via the feedback path. */
+ t = 0;
+ for (i = 0; i < 8000; i++) {
+ t += MS;
+ mb_ecn_ece(ctx, 0, 0, t);
+ }
+
+ /* Undamped probe: 8 s at TC 8 s is one full e-fold, ~2.72x. */
+ ratio = (double) ctx->rate / r0;
+ if (ratio < 2.6 || ratio > 2.85) {
+ printf("probe TC off: exp ~2.72, got %.3fx over 8 s.\n", ratio);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The fed-back capacity is the MIN of the nonzero caps in the window. */
+static int test_mb_ecn_rcv_cap_window_min(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint64_t t;
+ bool upd;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Onset packet carries no capacity: feed back unknown. */
+ if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, MS)) {
+ printf("Onset did not update.\n");
+ goto fail_ctx;
+ }
+
+ if (fcap != 0) {
+ printf("Onset fed back cap: exp 0, got %u.\n", fcap);
+ goto fail_ctx;
+ }
+
+ mb_ecn_rcv(ctx, LEN, 8, 40, &ece, &fcap, 2 * MS);
+ mb_ecn_rcv(ctx, LEN, 8, 36, &ece, &fcap, 3 * MS);
+
+ t = 3 * MS + CA_TW;
+ if (!mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t)) {
+ printf("Window did not close.\n");
+ goto fail_ctx;
+ }
+
+ if (fcap != 36) {
+ printf("Window min cap: exp 36, got %u.\n", fcap);
+ goto fail_ctx;
+ }
+
+ /* The next window starts unknown; follow the adapted rx_tw. */
+ upd = false;
+ for (i = 0; i < 128 && !upd; i++) {
+ t += CA_TW;
+ upd = mb_ecn_rcv(ctx, LEN, 8, 0, &ece, &fcap, t);
+ }
+
+ if (!upd) {
+ printf("Second window did not close.\n");
+ goto fail_ctx;
+ }
+
+ if (fcap != 0) {
+ printf("Stale cap %u leaked into the next window.\n", fcap);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Onset and gap restarts emit the triggering packet's cap, fresh. */
+static int test_mb_ecn_rcv_cap_onset_fresh(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint16_t ece;
+ uint8_t fcap;
+ uint64_t t;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ if (!mb_ecn_rcv(ctx, LEN, 4, 77, &ece, &fcap, MS)) {
+ printf("Onset did not update.\n");
+ goto fail_ctx;
+ }
+
+ if (fcap != 77) {
+ printf("Onset cap: exp 77, got %u.\n", fcap);
+ goto fail_ctx;
+ }
+
+ mb_ecn_rcv(ctx, LEN, 4, 50, &ece, &fcap, 2 * MS);
+
+ /* A gap restart must not fold in the stale window min. */
+ t = 2 * MS + 5 * CA_TW;
+ if (!mb_ecn_rcv(ctx, LEN, 4, 90, &ece, &fcap, t)) {
+ printf("Gap restart did not update.\n");
+ goto fail_ctx;
+ }
+
+ if (fcap != 90) {
+ printf("Gap restart cap: exp 90, got %u.\n", fcap);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Fed-back capacity derives the floor and slope: EWMA toward C/32. */
+static int test_mb_ecn_ece_cap_derives_rates(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t tgt;
+ uint64_t want;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Code 120 = 2^30 B/s; target floor = 2^25 B/s. */
+ tgt = cap_dec(120) >> CA_CAP_SHFT;
+
+ mb_ecn_ece(ctx, 100, 120, MS);
+
+ want = CA_RATE_MIN + ((tgt - CA_RATE_MIN) >> CA_CAP_SM_SHFT);
+ if (ctx->rate_min != want) {
+ printf("Floor: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ want, ctx->rate_min);
+ goto fail_ctx;
+ }
+
+ if (ctx->ai_rate != 2 * ctx->rate_min) {
+ printf("AI slope did not track the floor.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ece(ctx, 100, 120, 2 * MS);
+
+ want += (tgt - want) >> CA_CAP_SM_SHFT;
+ if (ctx->rate_min != want) {
+ printf("Floor EWMA: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ want, ctx->rate_min);
+ goto fail_ctx;
+ }
+
+ if (ctx->n_cap != 2) {
+ printf("Capacity updates: exp 2, got %" PRIu64 ".\n",
+ ctx->n_cap);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Feedback without a capacity leaves the derived rates untouched. */
+static int test_mb_ecn_ece_cap_zero_keeps_rates(void)
+{
+ struct mb_ecn_ctx * ctx;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ mb_ecn_ece(ctx, 100, 0, MS);
+
+ if (ctx->rate_min != CA_RATE_MIN || ctx->ai_rate != CA_AI_RATE) {
+ printf("Unknown cap moved the derived rates.\n");
+ goto fail_ctx;
+ }
+
+ if (ctx->n_cap != 0) {
+ printf("Unknown cap counted as an update.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The derived floor clamps to [CA_RATE_MIN, CA_RMIN_MAX]. */
+static int test_mb_ecn_ece_cap_clamps(void)
+{
+ struct mb_ecn_ctx * ctx;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* A path slower than the default floor cannot lower it. */
+ mb_ecn_ece(ctx, 100, 1, MS);
+
+ if (ctx->rate_min != CA_RATE_MIN) {
+ printf("Slow path lowered the floor: %" PRIu64 ".\n",
+ ctx->rate_min);
+ goto fail_ctx;
+ }
+
+ /* A absurdly fast path saturates at the ceiling. */
+ for (i = 1; i <= 40; i++)
+ mb_ecn_ece(ctx, 100, 255, (1 + i) * MS);
+
+ if (ctx->rate_min > CA_RMIN_MAX) {
+ printf("Floor above the ceiling: %" PRIu64 ".\n",
+ ctx->rate_min);
+ goto fail_ctx;
+ }
+
+ if (ctx->rate_min < CA_RMIN_MAX - 4) {
+ printf("Floor did not reach the ceiling: %" PRIu64 ".\n",
+ ctx->rate_min);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Stale capacity reverts the derived rates to the defaults. */
+static int test_mb_ecn_cap_ttl_reverts(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ mb_ecn_ece(ctx, 100, 120, MS);
+
+ if (ctx->rate_min == CA_RATE_MIN) {
+ printf("Capacity did not derive a floor.\n");
+ goto fail_ctx;
+ }
+
+ /* Just inside the TTL: the derived rates hold. */
+ mb_ecn_snd(ctx, LEN, MS + (ctx->ece_ttl << CA_CAP_TTL_SHFT), &ftag);
+
+ if (ctx->rate_min == CA_RATE_MIN) {
+ printf("Derived rates reverted too early.\n");
+ goto fail_ctx;
+ }
+
+ /* Past the TTL: back to the defaults. */
+ mb_ecn_snd(ctx, LEN, MS + (ctx->ece_ttl << CA_CAP_TTL_SHFT) + 1,
+ &ftag);
+
+ if (ctx->rate_min != CA_RATE_MIN || ctx->ai_rate != CA_AI_RATE) {
+ printf("Stale capacity kept the derived rates.\n");
+ goto fail_ctx;
+ }
+
+ if (ctx->tx_cap != 0) {
+ printf("Stale capacity code not cleared.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The control law uses the per-ctx AI slope. */
+static int test_mb_ecn_ctrl_per_ctx_ai(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t want;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* Leave slow start; raise the slope as capacity would. */
+ mb_ecn_ece(ctx, 0, 0, 0);
+
+ ctx->rate = (uint64_t) 10 << 20;
+ ctx->ai_rate = 16 * CA_AI_RATE;
+
+ want = ctx->rate + ctx->ai_rate * (30 * MS) / BILLION;
+ want += want * (30 * MS) / CA_PROBE_TC;
+
+ mb_ecn_snd(ctx, LEN, 30 * MS, &ftag);
+
+ if (ctx->rate != want) {
+ printf("AI not per-ctx: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ want, ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The rate clamp honours the per-ctx derived floor. */
+static int test_mb_ecn_ctrl_per_ctx_floor(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate_min = (uint64_t) 1 << 20;
+ ctx->rate = ((uint64_t) 1 << 20) + 1000;
+
+ mb_ecn_ece(ctx, CA_ECE_REF, 0, 0);
+ mb_ecn_snd(ctx, LEN, 30 * MS, &ftag);
+
+ if (ctx->rate != ctx->rate_min) {
+ printf("Floor not per-ctx: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A paced flow slower than one packet per CA_DT_CAP must not decay:
+ * the gap credit law grants its true elapsed service, so the lead
+ * stays pinned at ~one packet instead of growing without bound.
+ */
+static int test_mb_ecn_snd_slow_rate_paced(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+ uint64_t t = 0;
+ time_t wait;
+ size_t i;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* 8 KB/s, 1400 B packets: inter-send gap ~171 ms > CA_DT_CAP. */
+ ctx->rate = 8192;
+ ctx->rate_min = 8192;
+ ctx->ai_rate = 0;
+ ctx->inv_rate = mb_ecn_rate_inv(8192);
+ ctx->tx_cav = true;
+
+ for (i = 0; i < 50; i++) {
+ wait = mb_ecn_snd(ctx, 1400, t, &ftag);
+ t += wait > 0 ? (uint64_t) wait : 1;
+ }
+
+ if (ctx->lead > 2 * 1400) {
+ printf("Pacer starves a slow flow: lead %" PRIu64 ".\n",
+ ctx->lead);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A long idle makes the aggregate source-limited, so the offered-load
+ * ceiling bounds the resume rate (hence the burst) well below the
+ * pre-idle rate.
+ */
+static int test_mb_ecn_snd_idle_burst_bound(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = (uint64_t) 1 << 20;
+ ctx->inv_rate = mb_ecn_rate_inv(ctx->rate);
+ ctx->tx_cav = true;
+
+ mb_ecn_snd(ctx, 1400, 0, &ftag); /* warm-up: sets started */
+
+ /* 600 s idle. */
+ mb_ecn_snd(ctx, 1400, 600 * BILLION, &ftag);
+
+ if (ctx->backlogged) {
+ printf("long idle did not clear backlogged.\n");
+ goto fail_ctx;
+ }
+
+ if (ctx->rate >= ((uint64_t) 1 << 20)) {
+ printf("idle resume rate not ceiling-bounded: %" PRIu64
+ ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A deep backlog at a low rate must not wrap the wait computation. */
+static int test_mb_ecn_snd_wait_no_overflow(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t want;
+ uint64_t ftag;
+ time_t wait;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = 8192;
+ ctx->inv_rate = mb_ecn_rate_inv(8192);
+
+ /* 128 flows x 1400 B of SFQ lead at the floor rate. */
+ ftag = 128 * 1400;
+
+ wait = mb_ecn_snd(ctx, 1400, 0, &ftag);
+ want = (uint64_t) 128 * 1400 * BILLION / 8192;
+
+ if ((uint64_t) wait < want - want / 100 ||
+ (uint64_t) wait > want + want / 100) {
+ printf("Wait wrapped: exp ~%" PRIu64 ", got %" PRIu64 ".\n",
+ want, (uint64_t) wait);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A fully paced-backlogged flow reads backlogged after a window. */
+static int test_mb_ecn_backlogged_paced(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->backlogged = false; /* prove a window close re-earns it */
+
+ drive_backlogged(ctx, &ftag, MS, 4 * CA_SND_WIN, LEN);
+
+ if (!ctx->backlogged) {
+ printf("paced-backlogged flow read source-limited.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A source-limited flow is capped to the backlog level above the
+ * offered estimate, and NOT re-floored to a high capacity rate_min.
+ */
+static int test_mb_ecn_source_limited_ceiling(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+ uint64_t t = 10 * MS;
+ uint64_t expect;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->tx_cav = true;
+ ctx->started = true;
+ ctx->backlogged = false;
+ ctx->rate = (uint64_t) 100 << 20;
+ ctx->inv_rate = mb_ecn_rate_inv(ctx->rate);
+ ctx->rate_min = (uint64_t) 50 << 20;
+ ctx->snd_rate = (uint64_t) 1 << 20;
+ ctx->snd_r0 = ctx->rate;
+ ctx->snd_win = t;
+ ctx->last_ts = t;
+ ctx->last_ctrl = t;
+
+ mb_ecn_snd(ctx, LEN, t + 2 * MS, &ftag);
+
+ expect = 1398101; /* (1 << 20) * 4 / 3, truncated */
+ if (ctx->rate != expect) {
+ printf("ceiling: exp %" PRIu64 ", got %" PRIu64 ".\n",
+ expect, ctx->rate);
+ goto fail_ctx;
+ }
+
+ if (!ctx->src_limited) {
+ printf("ceiling bound but src_limited not set.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ /* Non-power-of-two case: verify the exact level, not a step. */
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->tx_cav = true;
+ ctx->started = true;
+ ctx->backlogged = false;
+ ctx->rate = (uint64_t) 100 << 20;
+ ctx->inv_rate = mb_ecn_rate_inv(ctx->rate);
+ ctx->rate_min = (uint64_t) 50 << 20;
+ ctx->snd_rate = (uint64_t) 303 << 12;
+ ctx->snd_r0 = ctx->rate;
+ ctx->snd_win = t;
+ ctx->last_ts = t;
+ ctx->last_ctrl = t;
+
+ mb_ecn_snd(ctx, LEN, t + 2 * MS, &ftag);
+
+ expect = 1654784; /* (303 << 12) * 4 / 3, exact */
+ if (ctx->rate != expect) {
+ printf("exact ceiling: exp %" PRIu64 ", got %" PRIu64
+ ".\n", expect, ctx->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* One quiet window must not collapse the max-filter; it decays ~1/16. */
+static int test_mb_ecn_max_filter(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+ uint64_t hi = (uint64_t) 10 << 20;
+ uint64_t t = 10 * MS;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->started = true;
+ ctx->snd_rate = hi;
+ ctx->snd_r0 = hi;
+ ctx->snd_byt = 0;
+ ctx->snd_win = t;
+ ctx->last_ts = t;
+ ctx->last_ctrl = t;
+
+ /* Close one window with almost no bytes offered. */
+ mb_ecn_snd(ctx, LEN, t + CA_SND_WIN + 1, &ftag);
+
+ if (ctx->snd_rate >= hi || ctx->snd_rate < hi - hi / 8) {
+ printf("max-filter: exp ~15/16 of %" PRIu64 ", got %"
+ PRIu64 " after one quiet window.\n",
+ hi, ctx->snd_rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* A >CA_DT_CAP gap clears backlogged without touching the estimate. */
+static int test_mb_ecn_idle_clears_backlogged(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+ uint64_t snd_rate = (uint64_t) 5 << 20;
+ uint64_t t = 10 * MS;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->started = true;
+ ctx->backlogged = true;
+ ctx->rate = snd_rate;
+ ctx->snd_rate = snd_rate;
+ ctx->snd_win = t;
+ ctx->last_ts = t;
+ ctx->last_ctrl = t;
+
+ /* 55 ms gap: past CA_DT_CAP, under CA_SND_WIN (no window close). */
+ mb_ecn_snd(ctx, LEN, t + 55 * MS, &ftag);
+
+ if (ctx->backlogged) {
+ printf("idle gap did not clear backlogged.\n");
+ goto fail_ctx;
+ }
+
+ if (ctx->snd_rate != snd_rate) {
+ printf("idle step altered snd_rate %" PRIu64 ".\n",
+ ctx->snd_rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* The first send is never misread as idle, whatever the wall clock. */
+static int test_mb_ecn_first_send_warmup(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t ftag = 0;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ /* started == false; a large first timestamp must not look idle. */
+ mb_ecn_snd(ctx, LEN, 500 * MS, &ftag);
+
+ if (!ctx->started) {
+ printf("first send did not set the warm-up sentinel.\n");
+ goto fail_ctx;
+ }
+
+ if (!ctx->backlogged) {
+ printf("first send misclassified as idle.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Couple one or two backlogged flows through a shared bottleneck of
+ * capacity cap: each step marks the shared queue with the real
+ * calc_ecn, feeds each flow that mark delayed by its own lag (in
+ * steps), drives it backlogged for one step, then drains the queue.
+ * A faithful discrete run of the fluid model on the real control law,
+ * with the forwarder abstracted to a single shared price. b may be
+ * NULL for a single-flow run.
+ */
+#define TF_STEP (5 * MS) /* control step (ns) */
+#define TF_HIST 64 /* mark ring, bounds max lag */
+#define TF_QMAX (8192 * LEN) /* bottleneck buffer (bytes) */
+#define TF_MAXN 32 /* flows per shared-link run */
+
+static void shared_link_run(struct mb_ecn_ctx * a,
+ struct mb_ecn_ctx * b,
+ uint64_t cap,
+ size_t lag_a,
+ size_t lag_b,
+ size_t steps)
+{
+ uint16_t hist[TF_HIST];
+ uint64_t ta = 0;
+ uint64_t tb = 0;
+ uint64_t fta = 0;
+ uint64_t ftb = 0;
+ uint64_t q = 0;
+ uint64_t drain = cap * TF_STEP / BILLION;
+ uint64_t tgt;
+ uint64_t arr;
+ uint8_t cc = cap_enc(cap);
+ uint8_t ecn;
+ uint16_t ea;
+ uint16_t eb;
+ size_t k;
+
+ memset(hist, 0, sizeof(hist));
+
+ for (k = 0; k < steps; k++) {
+ tgt = (k + 1) * TF_STEP;
+ ecn = 0;
+
+ mb_ecn_calc_ecn(q, &ecn, QOS_CUBE_BE, LEN);
+ hist[k % TF_HIST] = (uint16_t) (ecn << CA_SHFT);
+
+ ea = k < lag_a ? 0 : hist[(k - lag_a) % TF_HIST];
+ if (ea > 0) /* no feedback until congestion */
+ mb_ecn_ece(a, ea, cc, ta);
+
+ /* Active flow: heartbeat/window liveness stays fresh. */
+ a->last_sig = ta;
+ if (tgt > ta)
+ ta = drive_backlogged(a, &fta, ta, tgt - ta, LEN);
+
+ arr = a->rate * TF_STEP / BILLION;
+
+ if (b != NULL) {
+ eb = k < lag_b ? 0 : hist[(k - lag_b) % TF_HIST];
+ if (eb > 0)
+ mb_ecn_ece(b, eb, cc, tb);
+
+ b->last_sig = tb;
+ if (tgt > tb)
+ tb = drive_backlogged(b, &ftb, tb,
+ tgt - tb, LEN);
+
+ arr += b->rate * TF_STEP / BILLION;
+ }
+
+ q += arr;
+ q = q > drain ? q - drain : 0;
+ if (q > (uint64_t) TF_QMAX)
+ q = TF_QMAX;
+ }
+}
+
+/*
+ * Two flows sharing one bottleneck, both fed every queue mark, must
+ * converge from a lopsided start toward an equal split (the fluid
+ * model's drho/dt -> 0) rather than latch winner-take-all. Isolates
+ * the rate law from the forwarder: neither flow is starved of marks.
+ */
+static int test_mb_ecn_two_flow_converge(void)
+{
+ struct mb_ecn_ctx * a;
+ struct mb_ecn_ctx * b;
+ uint64_t cap = 1ULL << 20;
+ uint64_t fair = (1ULL << 20) / 2;
+ uint64_t lo;
+ uint64_t hi;
+
+ TEST_START();
+
+ a = mk_ctx();
+ b = mk_ctx();
+ if (a == NULL || b == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ a->rate = cap; /* a hogs, b starts small */
+ b->rate = CA_RATE_INIT;
+
+ shared_link_run(a, b, cap, 0, 0, 6000);
+
+ lo = a->rate < b->rate ? a->rate : b->rate;
+ hi = a->rate > b->rate ? a->rate : b->rate;
+
+ if (lo < fair / 4) {
+ printf("flow starved: %" PRIu64 " / %" PRIu64 ".\n",
+ a->rate, b->rate);
+ goto fail_ctx;
+ }
+
+ if (hi > 3 * lo) {
+ printf("did not converge: %" PRIu64 " / %" PRIu64 ".\n",
+ a->rate, b->rate);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(a);
+ mb_ecn_ctx_destroy(b);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A remote bottleneck exits slow start only on fed-back congestion,
+ * so a longer feedback delay lets the exponential ramp overshoot
+ * further: the slow-start peak grows with RTT. Nothing caps the ramp
+ * at the path capacity, so the sender latches near the first-hop rate.
+ */
+static int test_mb_ecn_ramp_overshoot_grows_with_rtt(void)
+{
+ struct mb_ecn_ctx * lo_rtt;
+ struct mb_ecn_ctx * hi_rtt;
+ uint64_t cap = 1ULL << 20;
+
+ TEST_START();
+
+ lo_rtt = mk_ctx();
+ hi_rtt = mk_ctx();
+ if (lo_rtt == NULL || hi_rtt == NULL) {
+ printf("Failed to create contexts.\n");
+ goto fail_ctx;
+ }
+
+ /* Same bottleneck; feedback lags 1 vs 8 steps (~5 vs 40 ms). */
+ shared_link_run(lo_rtt, NULL, cap, 1, 0, 2000);
+ shared_link_run(hi_rtt, NULL, cap, 8, 0, 2000);
+
+ if (hi_rtt->ss_peak <= lo_rtt->ss_peak) {
+ printf("overshoot did not grow with RTT: %" PRIu64
+ " vs %" PRIu64 ".\n",
+ hi_rtt->ss_peak, lo_rtt->ss_peak);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(lo_rtt);
+ mb_ecn_ctx_destroy(hi_rtt);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(lo_rtt);
+ mb_ecn_ctx_destroy(hi_rtt);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Flows sharing a context offer bytes together but each may send at
+ * rate, so the window must be shared out before the backlog level is
+ * read. Four flows offering two thirds of a share each stay below it.
+ */
+static int test_mb_ecn_shared_ctx_offered_per_flow(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t rate = 1000000;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = rate;
+ ctx->snd_flows = 4;
+ ctx->snd_r0 = rate;
+ ctx->snd_win = 0;
+ ctx->snd_byt = 4 * rate * 2 / 3;
+
+ mb_ecn_win(ctx, BILLION);
+
+ if (ctx->backlogged) {
+ printf("aggregate load read as one flow's backlog.\n");
+ goto fail_ctx;
+ }
+
+ /* The close left a fresh window; a full share each clears it. */
+ ctx->snd_byt = 4 * rate;
+
+ mb_ecn_win(ctx, 2 * BILLION);
+
+ if (!ctx->backlogged) {
+ printf("per-flow share did not read as backlogged.\n");
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A join or a leave opens a fresh window, so none divides the bytes
+ * one population offered by the count of another. An unchanged count
+ * leaves the running window alone.
+ */
+static int test_mb_ecn_flow_count_restarts_window(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t rate = 1000000;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->rate = rate;
+ ctx->snd_flows = 2;
+ ctx->snd_win = MS;
+ ctx->snd_byt = 12345;
+ ctx->snd_r0 = 7;
+
+ mb_ecn_flows(ctx, 5, 8 * MS);
+
+ if (ctx->snd_flows != 5 || ctx->snd_byt != 0
+ || ctx->snd_win != 8 * MS || ctx->snd_r0 != rate) {
+ printf("count change left a stale window: flows=%zu "
+ "byt=%" PRIu64 " win=%" PRIu64 " r0=%" PRIu64
+ ".\n", ctx->snd_flows, ctx->snd_byt,
+ ctx->snd_win, ctx->snd_r0);
+ goto fail_ctx;
+ }
+
+ ctx->snd_byt = 999;
+
+ mb_ecn_flows(ctx, 5, 20 * MS);
+
+ if (ctx->snd_byt != 999 || ctx->snd_win != 8 * MS) {
+ printf("unchanged count restarted the window.\n");
+ goto fail_ctx;
+ }
+
+ /* An empty context still measures a single sender. */
+ mb_ecn_flows(ctx, 0, 30 * MS);
+
+ if (ctx->snd_flows != 1) {
+ printf("zero flows did not floor at one: %zu.\n",
+ ctx->snd_flows);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * The ceiling must land where a window of the delivered rate reads
+ * backlogged again: a context clamped above that level can never
+ * leave the clamp, and loses its capacity floor with it.
+ */
+static int test_mb_ecn_ceiling_clears_backlog(void)
+{
+ struct mb_ecn_ctx * ctx;
+ uint64_t x = 1 << 20;
+
+ TEST_START();
+
+ ctx = mk_ctx();
+ if (ctx == NULL) {
+ printf("Failed to create context.\n");
+ goto fail;
+ }
+
+ ctx->backlogged = false;
+ ctx->snd_rate = x;
+ ctx->rate = 100 * x;
+ ctx->rate_min = CA_RATE_MIN;
+
+ mb_ecn_ceiling(ctx);
+
+ if (ctx->rate >= 100 * x) {
+ printf("ceiling did not bind: %" PRIu64 ".\n", ctx->rate);
+ goto fail_ctx;
+ }
+
+ /* One window delivering x, with the pacer deferring nothing. */
+ ctx->snd_r0 = ctx->rate;
+ ctx->snd_flows = 1;
+ ctx->snd_win = 0;
+ ctx->snd_byt = x;
+ ctx->snd_pac = 0;
+
+ mb_ecn_win(ctx, BILLION);
+
+ if (!ctx->backlogged) {
+ printf("clamped at %" PRIu64 " cannot clear on %" PRIu64
+ ".\n", ctx->snd_r0, x);
+ goto fail_ctx;
+ }
+
+ mb_ecn_ctx_destroy(ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_ctx:
+ mb_ecn_ctx_destroy(ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int mb_ecn_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_mb_ecn_ctx_create_destroy();
+ ret |= test_mb_ecn_init_window();
+ ret |= test_mb_ecn_calc_ecn();
+ ret |= test_mb_ecn_rcv_onset_immediate();
+ ret |= test_mb_ecn_rcv_rate_independent();
+ ret |= test_mb_ecn_rcv_size_fair();
+ ret |= test_mb_ecn_rcv_release_exact_zero();
+ ret |= test_mb_ecn_rcv_gap_restart();
+ ret |= test_mb_ecn_rcv_gap_floor();
+ ret |= test_mb_ecn_rcv_accum_bounds();
+ ret |= test_mb_ecn_rcv_window_clip_bounds();
+ ret |= test_mb_ecn_rcv_slow_window();
+ ret |= test_mb_ecn_rcv_no_overflow_highrate();
+ ret |= test_mb_ecn_ece_ttl_covers_cadence();
+ ret |= test_mb_ecn_slow_start();
+ ret |= test_mb_ecn_dt_scaling_invariant();
+ ret |= test_mb_ecn_probe_scale_invariant();
+ ret |= test_mb_ecn_multiplicative_decrease();
+ ret |= test_mb_ecn_ai_hold_release();
+ ret |= test_mb_ecn_fixed_point();
+ ret |= test_mb_ecn_lead_symmetric();
+ ret |= test_mb_ecn_slow_start_local_brake();
+ ret |= test_mb_ecn_slow_start_clean_ramp();
+ ret |= test_mb_ecn_starved_decrease_escape();
+ ret |= test_mb_ecn_starved_local_fallback();
+ ret |= test_mb_ecn_decrease_dt_invariant();
+ ret |= test_mb_ecn_decrease_subms_carry();
+ ret |= test_mb_ecn_idle_resume_bounded();
+ ret |= test_mb_ecn_rate_floor();
+ ret |= test_mb_ecn_ece_staleness();
+ ret |= test_mb_ecn_ece_ttl_tracks_rate();
+ ret |= test_mb_ecn_sfq_pace();
+ ret |= test_mb_ecn_probe_time_constant();
+ ret |= test_mb_ecn_rcv_cap_window_min();
+ ret |= test_mb_ecn_rcv_cap_onset_fresh();
+ ret |= test_mb_ecn_ece_cap_derives_rates();
+ ret |= test_mb_ecn_ece_cap_zero_keeps_rates();
+ ret |= test_mb_ecn_ece_cap_clamps();
+ ret |= test_mb_ecn_cap_ttl_reverts();
+ ret |= test_mb_ecn_ctrl_per_ctx_ai();
+ ret |= test_mb_ecn_ctrl_per_ctx_floor();
+ ret |= test_mb_ecn_snd_slow_rate_paced();
+ ret |= test_mb_ecn_snd_idle_burst_bound();
+ ret |= test_mb_ecn_snd_wait_no_overflow();
+ ret |= test_mb_ecn_backlogged_paced();
+ ret |= test_mb_ecn_source_limited_ceiling();
+ ret |= test_mb_ecn_max_filter();
+ ret |= test_mb_ecn_idle_clears_backlogged();
+ ret |= test_mb_ecn_first_send_warmup();
+ ret |= test_mb_ecn_two_flow_converge();
+ ret |= test_mb_ecn_ramp_overshoot_grows_with_rtt();
+ ret |= test_mb_ecn_shared_ctx_offered_per_flow();
+ ret |= test_mb_ecn_flow_count_restarts_window();
+ ret |= test_mb_ecn_ceiling_clears_backlog();
+
+ return ret;
+}
diff --git a/src/ipcpd/unicast/cap.c b/src/ipcpd/unicast/cap.c
new file mode 100644
index 00000000..67b7967c
--- /dev/null
+++ b/src/ipcpd/unicast/cap.c
@@ -0,0 +1,99 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Link capacity codes
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+/*
+ * Rate <-> 8-bit code (cap_enc / cap_dec): the high 6 bits hold a
+ * band e = floor(log2 rate), the low 2 a quarter k splitting the
+ * band at 256 * 2^(k/4) = {256, 304, 362, 431}; code = 4 * e + k.
+ * Capacity is only ever needed to order-of-magnitude accuracy.
+ */
+
+#include "cap.h"
+
+uint8_t cap_enc(uint64_t rate)
+{
+ static const uint16_t thr[3] = {304, 362, 431};
+ uint64_t r = rate; /* copy halved to find band */
+ unsigned e = 0; /* band: floor log2 rate */
+ unsigned k = 0; /* quarter within band 0..3 */
+ unsigned c; /* code = 4 * band + quarter */
+ uint16_t top; /* rate scaled to [256, 512) */
+
+ if (rate == 0)
+ return 0;
+
+ while (r > 1) {
+ r >>= 1;
+ e++;
+ }
+
+ if (e >= 8)
+ top = (uint16_t) (rate >> (e - 8));
+ else
+ top = (uint16_t) (rate << (8 - e));
+
+ while (k < 3 && top >= thr[k])
+ k++;
+
+ c = 4 * e + k;
+ if (c == 0)
+ c = 1; /* 0 means unknown */
+
+ return (uint8_t) c;
+}
+
+uint64_t cap_dec(uint8_t c)
+{
+ static const uint16_t m[4] = {256, 304, 362, 431};
+ unsigned e = c >> 2; /* band = c >> 2 */
+ unsigned k = c & 3; /* quarter = c & 3 */
+
+ if (c == 0)
+ return 0;
+
+ if (e >= 8)
+ return (uint64_t) m[k] << (e - 8);
+
+ return ((uint64_t) m[k] << e) >> 8;
+}
+
+uint8_t cap_min(uint8_t a,
+ uint8_t b)
+{
+ if (a == 0)
+ return b;
+
+ if (b == 0)
+ return a;
+
+ return a < b ? a : b;
+}
+
+void cap_stamp(uint8_t * pci,
+ uint8_t own)
+{
+ if (own == 0)
+ return;
+
+ if (*pci == 0 || own < *pci)
+ *pci = own;
+}
diff --git a/src/ipcpd/unicast/cap.h b/src/ipcpd/unicast/cap.h
new file mode 100644
index 00000000..ca6b6355
--- /dev/null
+++ b/src/ipcpd/unicast/cap.h
@@ -0,0 +1,40 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Link capacity codes
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#ifndef OUROBOROS_IPCPD_UNICAST_CAP_H
+#define OUROBOROS_IPCPD_UNICAST_CAP_H
+
+#include <stdint.h>
+
+/* Quarter-log2 capacity code: ~2^(c / 4) bytes/s, 0 = unknown. */
+uint8_t cap_enc(uint64_t rate);
+
+uint64_t cap_dec(uint8_t c);
+
+uint8_t cap_min(uint8_t a,
+ uint8_t b);
+
+/* MIN-combine the own link code into the PCI byte. */
+void cap_stamp(uint8_t * pci,
+ uint8_t own);
+
+#endif /* OUROBOROS_IPCPD_UNICAST_CAP_H */
diff --git a/src/ipcpd/unicast/dir/dht.c b/src/ipcpd/unicast/dir/dht.c
index 8eeea800..9d60ce30 100644
--- a/src/ipcpd/unicast/dir/dht.c
+++ b/src/ipcpd/unicast/dir/dht.c
@@ -1597,6 +1597,7 @@ static ssize_t dht_kv_get_contacts(const uint8_t * key,
fail_contact:
while (i-- > 0)
dht_contact_msg__free_unpacked((*msgs)[i], NULL);
+
free(*msgs);
*msgs = NULL;
fail_msgs:
@@ -1763,6 +1764,7 @@ static int split_bucket(struct bucket * b)
fail_child:
while (i-- > 0)
bucket_destroy(b->children[i]);
+
return -1;
}
@@ -2236,7 +2238,7 @@ static int dht_send_msg(dht_msg_t * msg,
dht_msg__pack(msg, ssm_pk_buff_head(spb));
- if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, spb) < 0) {
+ if (dt_write_packet(addr, QOS_CUBE_BE, dht.eid, spb, NULL) < 0) {
log_warn("%s write failed", DHT_CODE(msg));
goto fail_send;
}
@@ -2849,6 +2851,7 @@ static dht_msg_t * do_dht_kv_find_node_req(const dht_find_req_msg_t * req)
fail_msg:
while (len-- > 0)
dht_contact_msg__free_unpacked(contacts[len], NULL);
+
free(contacts);
fail_contacts:
return NULL;
@@ -2951,8 +2954,9 @@ static dht_msg_t * do_dht_kv_find_value_req(const dht_find_req_msg_t * req)
fail_msg:
freebufs(vals, n_vals);
fail_vals:
- while (n_contacts-- > 0)
+ while (contacts != NULL && n_contacts-- > 0)
dht_contact_msg__free_unpacked(contacts[n_contacts], NULL);
+
free(contacts);
fail_contacts:
return NULL;
@@ -3312,6 +3316,42 @@ static int emergency_peer(struct list_head * pl)
return -ENOMEM;
}
+static bool __dht_kv_bucket_has_addr(struct bucket * b,
+ uint64_t addr)
+{
+ struct list_head * p;
+ size_t i;
+
+ assert(b != NULL);
+
+ if (*b->children != NULL)
+ for (i = 0; i < (1L << DHT_BETA); ++i)
+ if (__dht_kv_bucket_has_addr(b->children[i], addr))
+ return true;
+
+ llist_for_each(p, &b->contacts) {
+ struct contact * c;
+ c = list_entry(p, struct contact, next);
+ if (c->addr == addr)
+ return true;
+ }
+
+ return false;
+}
+
+static bool dht_kv_knows_peer(void)
+{
+ bool found;
+
+ pthread_rwlock_rdlock(&dht.db.lock);
+
+ found = __dht_kv_bucket_has_addr(dht.db.contacts.root, dht.peer);
+
+ pthread_rwlock_unlock(&dht.db.lock);
+
+ return found;
+}
+
static int dht_kv_seed_bootstrap_peer(void)
{
struct list_head pl;
@@ -3323,6 +3363,9 @@ static int dht_kv_seed_bootstrap_peer(void)
return 0;
}
+ if (dht_kv_knows_peer())
+ return 0;
+
if (emergency_peer(&pl) < 0) {
log_err("Could not create emergency peer.");
goto fail_peer;
@@ -3338,7 +3381,8 @@ static int dht_kv_seed_bootstrap_peer(void)
peer_list_destroy(&pl);
- return 0;
+ /* Sent, but not bootstrapped until the peer is in the DHT. */
+ return -EAGAIN;
fail_query:
peer_list_destroy(&pl);
fail_peer:
@@ -3427,6 +3471,8 @@ static void value_list_destroy(struct list_head * vl)
#define MUST_REPLICATE(v, now) ((now)->tv_sec > (v)->t_repl + dht.t_repl)
#define MUST_REPUBLISH(v, now) /* Close to expiry deadline */ \
(((v)->t_exp - (now)->tv_sec) < (DHT_N_REPUB * dht.t_repl))
+/* A local value must be (re)stored if near expiry or never stored. */
+#define MUST_STORE_LVAL(v, now) (MUST_REPUBLISH(v, now) || (v)->t_repl == 0)
static void dht_entry_get_repl_lists(const struct dht_entry * e,
struct list_head * repl,
struct list_head * rebl,
@@ -3448,7 +3494,7 @@ static void dht_entry_get_repl_lists(const struct dht_entry * e,
llist_for_each(p, &e->lvals) {
struct val_entry * v = list_entry(p, struct val_entry, next);
- if (MUST_REPLICATE(v, now) && MUST_REPUBLISH(v, now)) {
+ if (MUST_REPLICATE(v, now) && MUST_STORE_LVAL(v, now)) {
/* Add expire time here, to allow creating val_entry */
n = val_entry_create(v->val, now->tv_sec + dht.t_exp);
if (n == NULL)
@@ -3466,7 +3512,7 @@ static int dht_kv_next_values(uint8_t * key,
struct timespec now;
struct list_head * p;
struct list_head * h;
- struct dht_entry * e = NULL;
+ struct dht_entry * e;
assert(key != NULL);
assert(repl != NULL);
@@ -3479,20 +3525,19 @@ static int dht_kv_next_values(uint8_t * key,
pthread_rwlock_rdlock(&dht.db.lock);
- if (llist_is_empty(&dht.db.kv.ll))
- goto no_entries;
-
llist_for_each_safe(p, h, &dht.db.kv.ll) {
e = list_entry(p, struct dht_entry, next);
- if (IS_CLOSER(e->key, key))
+ if (!IS_CLOSER(key, e->key))
continue; /* Already processed */
- }
- if (e != NULL) {
memcpy(key, e->key, dht.id.len);
+
dht_entry_get_repl_lists(e, repl, rebl, &now);
+
+ if (!list_is_empty(repl) || !list_is_empty(rebl))
+ break;
}
- no_entries:
+
pthread_rwlock_unlock(&dht.db.lock);
return list_is_empty(repl) && list_is_empty(rebl) ? -ENOENT : 0;
@@ -3738,6 +3783,9 @@ static void * work(void * o)
log_dbg("DHT worker starting %ld seconds interval.", intv * n);
+ /* Flush names registered before we had peers to store them. */
+ dht_kv_replicate();
+
while (true) {
int i = 0;
while (tasks[i] != NULL) {
diff --git a/src/ipcpd/unicast/dir/tests/dht_test.c b/src/ipcpd/unicast/dir/tests/dht_test.c
index 1f7026b3..ee6861a0 100644
--- a/src/ipcpd/unicast/dir/tests/dht_test.c
+++ b/src/ipcpd/unicast/dir/tests/dht_test.c
@@ -796,6 +796,68 @@ static int test_dht_kv_get_values(void)
return TEST_RC_FAIL;
}
+static int test_dht_kv_next_values(void)
+{
+ struct list_head repl;
+ struct list_head rebl;
+ uint8_t * key;
+ size_t n;
+ size_t i;
+
+ TEST_START();
+
+ list_head_init(&repl);
+ list_head_init(&rebl);
+
+ if (dht_init(&test_dht_config) < 0) {
+ printf("Failed to create dht.\n");
+ goto fail_init;
+ }
+
+ if (fill_store_with_random_values(NULL, sizeof(uint64_t), 3) < 0) {
+ printf("Failed to fill store with random values.\n");
+ goto fail_fill;
+ }
+
+ key = dht_dup_key(dht.id.data);
+ if (key == NULL) {
+ printf("Failed to duplicate DHT ID.\n");
+ goto fail_fill;
+ }
+
+ n = 0;
+
+ for (i = 0; i < 5; ++i) {
+ if (dht_kv_next_values(key, &repl, &rebl) < 0)
+ break;
+
+ ++n;
+ value_list_destroy(&repl);
+ value_list_destroy(&rebl);
+ }
+
+ if (n != 3) {
+ printf("Failed to visit each entry once (%zu != 3).\n", n);
+ goto fail_next;
+ }
+
+ free(key);
+
+ dht_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_next:
+ free(key);
+ fail_fill:
+ dht_fini();
+ fail_init:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
static int test_dht_kv_find_node_req_msg(void)
{
dht_msg_t * msg;
@@ -1894,6 +1956,7 @@ int dht_test(int argc,
rc |= test_dht_kv_contact_list();
rc |= test_dht_kv_update_bucket();
rc |= test_dht_kv_get_values();
+ rc |= test_dht_kv_next_values();
rc |= test_dht_kv_find_node_req_msg();
rc |= test_dht_kv_find_node_rsp_msg();
rc |= test_dht_kv_find_node_rsp_msg_contacts();
diff --git a/src/ipcpd/unicast/dt.c b/src/ipcpd/unicast/dt.c
index 252477f4..84e62f05 100644
--- a/src/ipcpd/unicast/dt.c
+++ b/src/ipcpd/unicast/dt.c
@@ -31,10 +31,12 @@
#define DT "dt"
#define OUROBOROS_PREFIX DT
+#include <ouroboros/atomics.h>
#include <ouroboros/bitmap.h>
#include <ouroboros/errno.h>
#include <ouroboros/logs.h>
#include <ouroboros/dev.h>
+#include <ouroboros/ipcp-dev.h>
#include <ouroboros/notifier.h>
#include <ouroboros/rib.h>
#ifdef IPCP_FLOW_STATS
@@ -45,6 +47,7 @@
#include "common/comp.h"
#include "common/connmgr.h"
#include "ca.h"
+#include "cap.h"
#include "ipcp.h"
#include "dt.h"
#include "pff.h"
@@ -77,12 +80,14 @@ struct comp_info {
#define TTL_LEN 1
#define QOS_LEN 1
#define ECN_LEN 1
+#define CAP_LEN 1
struct dt_pci {
uint64_t dst_addr;
qoscube_t qc;
uint8_t ttl;
uint8_t ecn;
+ uint8_t cap;
uint64_t eid;
};
@@ -95,6 +100,7 @@ struct {
size_t qc_o;
size_t ttl_o;
size_t ecn_o;
+ size_t cap_o;
size_t eid_o;
/* Initial TTL value */
@@ -114,6 +120,7 @@ static void dt_pci_ser(uint8_t * head,
memcpy(head + dt_pci_info.qc_o, &dt_pci->qc, QOS_LEN);
memcpy(head + dt_pci_info.ttl_o, &ttl, TTL_LEN);
memcpy(head + dt_pci_info.ecn_o, &dt_pci->ecn, ECN_LEN);
+ memcpy(head + dt_pci_info.cap_o, &dt_pci->cap, CAP_LEN);
memcpy(head + dt_pci_info.eid_o, &dt_pci->eid, dt_pci_info.eid_size);
}
@@ -132,6 +139,7 @@ static void dt_pci_des(uint8_t * head,
memcpy(&dt_pci->qc, head + dt_pci_info.qc_o, QOS_LEN);
memcpy(&dt_pci->ttl, head + dt_pci_info.ttl_o, TTL_LEN);
memcpy(&dt_pci->ecn, head + dt_pci_info.ecn_o, ECN_LEN);
+ memcpy(&dt_pci->cap, head + dt_pci_info.cap_o, CAP_LEN);
memcpy(&dt_pci->eid, head + dt_pci_info.eid_o, dt_pci_info.eid_size);
}
@@ -139,7 +147,7 @@ static void dt_pci_shrink(struct ssm_pk_buff * spb)
{
assert(spb);
- ssm_pk_buff_head_release(spb, dt_pci_info.head_size);
+ ssm_pk_buff_pop(spb, dt_pci_info.head_size);
}
struct {
@@ -150,6 +158,7 @@ struct {
struct pff * pff[QOS_CUBE_MAX];
struct routing_i * routing[QOS_CUBE_MAX];
#ifdef IPCP_FLOW_STATS
+ /* Flow stats use lock-free atomics; stamp is the validity flag. */
struct {
time_t stamp;
uint64_t addr;
@@ -167,23 +176,29 @@ struct {
size_t w_drp_bytes[QOS_CUBE_MAX];
size_t f_nhp_pkt[QOS_CUBE_MAX];
size_t f_nhp_bytes[QOS_CUBE_MAX];
- pthread_mutex_t lock;
- } stat[PROG_MAX_FLOWS];
+ } stat[PROC_MAX_FLOWS];
size_t n_flows;
#endif
struct bmp * res_fds;
- struct comp_info comps[PROG_RES_FDS];
+ struct comp_info comps[PROC_RES_FDS];
pthread_rwlock_t lock;
pthread_t listener;
} dt;
+#ifdef IPCP_FLOW_STATS
+#define dt_stat_inc(idx, name, qc, len) \
+ do { \
+ FETCH_ADD_RELAXED(&dt.stat[idx].name ## _pkt[qc], 1); \
+ FETCH_ADD_RELAXED(&dt.stat[idx].name ## _bytes[qc], (len)); \
+ } while (0)
+#define dt_stat_load(idx, field, qc) LOAD_RELAXED(&dt.stat[idx].field[qc])
+
static int dt_rib_read(const char * path,
char * buf,
size_t len)
{
-#ifdef IPCP_FLOW_STATS
int fd;
int i;
char str[QOS_BLOCK_LEN + 1];
@@ -192,6 +207,8 @@ static int dt_rib_read(const char * path,
char tmstr[RIB_TM_STRLEN];
size_t rxqlen = 0;
size_t txqlen = 0;
+ time_t stamp;
+ uint64_t addr;
struct tm * tm;
/* NOTE: we may need stronger checks. */
@@ -205,22 +222,21 @@ static int dt_rib_read(const char * path,
buf[0] = '\0';
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- if (dt.stat[fd].stamp == 0) {
- pthread_mutex_unlock(&dt.stat[fd].lock);
+ stamp = LOAD_ACQUIRE(&dt.stat[fd].stamp);
+ if (stamp == 0)
return 0;
- }
- if (dt.stat[fd].addr == dt.addr)
+ addr = LOAD_RELAXED(&dt.stat[fd].addr);
+
+ if (addr == dt.addr)
sprintf(addrstr, "%s", dt.comps[fd].name);
else
- sprintf(addrstr, ADDR_FMT32, ADDR_VAL32(&dt.stat[fd].addr));
+ sprintf(addrstr, ADDR_FMT32, ADDR_VAL32(&addr));
- tm = gmtime(&dt.stat[fd].stamp);
+ tm = gmtime(&stamp);
strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
- if (fd >= PROG_RES_FDS) {
+ if (fd >= PROC_RES_FDS) {
fccntl(fd, FLOWGRXQLEN, &rxqlen);
fccntl(fd, FLOWGTXQLEN, &txqlen);
}
@@ -249,38 +265,29 @@ static int dt_rib_read(const char * path,
" failed nhop (packets): %20zu\n"
" failed nhop (bytes): %20zu\n",
i,
- dt.stat[fd].snd_pkt[i],
- dt.stat[fd].snd_bytes[i],
- dt.stat[fd].rcv_pkt[i],
- dt.stat[fd].rcv_bytes[i],
- dt.stat[fd].lcl_w_pkt[i],
- dt.stat[fd].lcl_w_bytes[i],
- dt.stat[fd].lcl_r_pkt[i],
- dt.stat[fd].lcl_r_bytes[i],
- dt.stat[fd].r_drp_pkt[i],
- dt.stat[fd].r_drp_bytes[i],
- dt.stat[fd].w_drp_pkt[i],
- dt.stat[fd].w_drp_bytes[i],
- dt.stat[fd].f_nhp_pkt[i],
- dt.stat[fd].f_nhp_bytes[i]
+ dt_stat_load(fd, snd_pkt, i),
+ dt_stat_load(fd, snd_bytes, i),
+ dt_stat_load(fd, rcv_pkt, i),
+ dt_stat_load(fd, rcv_bytes, i),
+ dt_stat_load(fd, lcl_w_pkt, i),
+ dt_stat_load(fd, lcl_w_bytes, i),
+ dt_stat_load(fd, lcl_r_pkt, i),
+ dt_stat_load(fd, lcl_r_bytes, i),
+ dt_stat_load(fd, r_drp_pkt, i),
+ dt_stat_load(fd, r_drp_bytes, i),
+ dt_stat_load(fd, w_drp_pkt, i),
+ dt_stat_load(fd, w_drp_bytes, i),
+ dt_stat_load(fd, f_nhp_pkt, i),
+ dt_stat_load(fd, f_nhp_bytes, i)
);
strcat(buf, str);
}
- pthread_mutex_unlock(&dt.stat[fd].lock);
-
return RIB_FILE_STRLEN;
-#else
- (void) path;
- (void) buf;
- (void) len;
- return 0;
-#endif
}
static int dt_rib_readdir(char *** buf)
{
-#ifdef IPCP_FLOW_STATS
char entry[RIB_PATH_LEN + 1];
size_t i;
int idx = 0;
@@ -296,15 +303,9 @@ static int dt_rib_readdir(char *** buf)
if (*buf == NULL)
goto fail_entries;
- for (i = 0; i < PROG_MAX_FLOWS; ++i) {
- pthread_mutex_lock(&dt.stat[i].lock);
-
- if (dt.stat[i].stamp == 0) {
- pthread_mutex_unlock(&dt.stat[i].lock);
- break;
- }
-
- pthread_mutex_unlock(&dt.stat[i].lock);
+ for (i = 0; i < PROC_MAX_FLOWS && idx < (int) dt.n_flows; ++i) {
+ if (LOAD_RELAXED(&dt.stat[i].stamp) == 0)
+ continue; /* n-1 flows start at PROC_RES_FDS */
sprintf(entry, "%zu", i);
@@ -323,43 +324,35 @@ static int dt_rib_readdir(char *** buf)
fail_entry:
while (idx-- > 0)
free((*buf)[idx]);
+
free(*buf);
fail_entries:
pthread_rwlock_unlock(&dt.lock);
return -ENOMEM;
-#else
- (void) buf;
- return 0;
-#endif
}
static int dt_rib_getattr(const char * path,
struct rib_attr * attr)
{
-#ifdef IPCP_FLOW_STATS
int fd;
char * entry;
+ time_t stamp;
entry = strstr(path, RIB_SEPARATOR) + 1;
assert(entry);
fd = atoi(entry);
- pthread_mutex_lock(&dt.stat[fd].lock);
+ stamp = LOAD_ACQUIRE(&dt.stat[fd].stamp);
- if (dt.stat[fd].stamp != -1) {
+ if (stamp != -1) {
attr->size = RIB_FILE_STRLEN;
- attr->mtime = dt.stat[fd].stamp;
+ attr->mtime = stamp;
} else {
attr->size = 0;
attr->mtime = 0;
}
- pthread_mutex_unlock(&dt.stat[fd].lock);
-#else
- (void) path;
- (void) attr;
-#endif
return 0;
}
@@ -369,29 +362,49 @@ static struct rib_ops r_ops = {
.getattr = dt_rib_getattr
};
-#ifdef IPCP_FLOW_STATS
static void stat_used(int fd,
uint64_t addr)
{
struct timespec now;
+ int i;
clock_gettime(CLOCK_REALTIME_COARSE, &now);
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- memset(&dt.stat[fd], 0, sizeof(dt.stat[fd]));
+ pthread_rwlock_wrlock(&dt.lock);
- dt.stat[fd].stamp = (addr != INVALID_ADDR) ? now.tv_sec : 0;
- dt.stat[fd].addr = addr;
+ STORE_RELEASE(&dt.stat[fd].stamp, 0);
- pthread_mutex_unlock(&dt.stat[fd].lock);
+ /* Don't memset: incremented without locks in fast path. */
+ for (i = 0; i < QOS_CUBE_MAX; ++i) {
+ STORE_RELAXED(&dt.stat[fd].snd_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].snd_bytes[i], 0);
+ STORE_RELAXED(&dt.stat[fd].rcv_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].rcv_bytes[i], 0);
+ STORE_RELAXED(&dt.stat[fd].lcl_r_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].lcl_r_bytes[i], 0);
+ STORE_RELAXED(&dt.stat[fd].lcl_w_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].lcl_w_bytes[i], 0);
+ STORE_RELAXED(&dt.stat[fd].r_drp_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].r_drp_bytes[i], 0);
+ STORE_RELAXED(&dt.stat[fd].w_drp_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].w_drp_bytes[i], 0);
+ STORE_RELAXED(&dt.stat[fd].f_nhp_pkt[i], 0);
+ STORE_RELAXED(&dt.stat[fd].f_nhp_bytes[i], 0);
+ }
- pthread_rwlock_wrlock(&dt.lock);
+ STORE_RELAXED(&dt.stat[fd].addr, addr);
- (addr != INVALID_ADDR) ? ++dt.n_flows : --dt.n_flows;
+ if (addr != INVALID_ADDR) {
+ STORE_RELEASE(&dt.stat[fd].stamp, now.tv_sec);
+ ++dt.n_flows;
+ } else {
+ --dt.n_flows;
+ }
pthread_rwlock_unlock(&dt.lock);
}
+#else
+#define dt_stat_inc(idx, name, qc, len) ((void) 0)
#endif
static void handle_event(void * self,
@@ -411,6 +424,8 @@ static void handle_event(void * self,
#ifdef IPCP_FLOW_STATS
stat_used(fd, c->conn_info.addr);
#endif
+ if (ipcp_flow_cap_arm(fd) < 0)
+ log_warn("Failed to arm capacity estimator.");
psched_add(dt.psched, fd);
log_dbg("Added fd %d to packet scheduler.", fd);
break;
@@ -427,28 +442,27 @@ static void handle_event(void * self,
}
}
-static void packet_handler(int fd,
- qoscube_t qc,
- struct ssm_pk_buff * spb)
+static time_t packet_handler(int fd,
+ qoscube_t qc,
+ struct ssm_pk_buff * spb)
{
struct dt_pci dt_pci;
int ret;
int ofd;
uint8_t * head;
size_t len;
+ size_t qlen;
+ size_t mlen;
+ uint8_t lcap;
+ bool marks;
len = ssm_pk_buff_len(spb);
#ifndef IPCP_FLOW_STATS
- (void) fd;
-#else
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- ++dt.stat[fd].rcv_pkt[qc];
- dt.stat[fd].rcv_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[fd].lock);
+ (void) fd;
#endif
+ dt_stat_inc(fd, rcv, qc, len);
+
memset(&dt_pci, 0, sizeof(dt_pci));
head = ssm_pk_buff_head(spb);
@@ -458,15 +472,8 @@ static void packet_handler(int fd,
if (dt_pci.ttl == 0) {
log_dbg("TTL was zero.");
ipcp_spb_release(spb);
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- ++dt.stat[fd].r_drp_pkt[qc];
- dt.stat[fd].r_drp_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[fd].lock);
-#endif
- return;
+ dt_stat_inc(fd, r_drp, qc, len);
+ return 0;
}
/* FIXME: Use qoscube from PCI instead of incoming flow. */
@@ -475,18 +482,19 @@ static void packet_handler(int fd,
log_dbg("No next hop for %" PRIu64 ".",
dt_pci.dst_addr);
ipcp_spb_release(spb);
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
+ dt_stat_inc(fd, f_nhp, qc, len);
+ return 0;
+ }
- ++dt.stat[fd].f_nhp_pkt[qc];
- dt.stat[fd].f_nhp_bytes[qc] += len;
+ marks = ca_marks_ecn();
+ qlen = marks ? ipcp_flow_queued(ofd) : 0;
+ mlen = marks ? ipcp_flow_mean_len(ofd) : 0;
+ lcap = marks ? cap_enc(ipcp_flow_cap(ofd)) : 0;
- pthread_mutex_unlock(&dt.stat[fd].lock);
-#endif
- return;
- }
+ (void) ca_calc_ecn(qlen, head + dt_pci_info.ecn_o, qc, mlen);
- (void) ca_calc_ecn(ofd, head + dt_pci_info.ecn_o, qc, len);
+ if (marks)
+ cap_stamp(head + dt_pci_info.cap_o, lcap);
ret = ipcp_flow_write(ofd, spb);
if (ret < 0) {
@@ -494,55 +502,37 @@ static void packet_handler(int fd,
if (ret == -EFLOWDOWN)
notifier_event(NOTIFY_DT_FLOW_DOWN, &ofd);
ipcp_spb_release(spb);
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[ofd].lock);
-
- ++dt.stat[ofd].w_drp_pkt[qc];
- dt.stat[ofd].w_drp_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[ofd].lock);
-#endif
- return;
+ dt_stat_inc(ofd, w_drp, qc, len);
+ return 0;
}
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[ofd].lock);
- ++dt.stat[ofd].snd_pkt[qc];
- dt.stat[ofd].snd_bytes[qc] += len;
+ dt_stat_inc(ofd, snd, qc, len);
- pthread_mutex_unlock(&dt.stat[ofd].lock);
-#endif
+ if (marks)
+ ipcp_flow_cap_update(ofd, qlen, len);
} else {
dt_pci_shrink(spb);
- if (dt_pci.eid >= PROG_RES_FDS) {
+ if (dt_pci.eid >= PROC_RES_FDS) {
uint8_t ecn = *(head + dt_pci_info.ecn_o);
- fa_np1_rcv(dt_pci.eid, ecn, spb);
- return;
+ uint8_t cap = *(head + dt_pci_info.cap_o);
+ fa_np1_rcv(dt_pci.eid, ecn, cap, spb);
+ return 0;
}
if (dt.comps[dt_pci.eid].post_packet == NULL) {
log_err("No registered component on eid %" PRIu64 ".",
dt_pci.eid);
ipcp_spb_release(spb);
- return;
+ return 0;
}
-#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
+ dt_stat_inc(fd, lcl_r, qc, len);
+ dt_stat_inc(dt_pci.eid, snd, qc, len);
- ++dt.stat[fd].lcl_r_pkt[qc];
- dt.stat[fd].lcl_r_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[fd].lock);
- pthread_mutex_lock(&dt.stat[dt_pci.eid].lock);
-
- ++dt.stat[dt_pci.eid].snd_pkt[qc];
- dt.stat[dt_pci.eid].snd_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[dt_pci.eid].lock);
-#endif
dt.comps[dt_pci.eid].post_packet(dt.comps[dt_pci.eid].comp,
spb);
}
+
+ return 0;
}
static void * dt_conn_handle(void * o)
@@ -569,7 +559,9 @@ int dt_init(struct dt_config cfg)
{
int i;
int j;
+#ifdef IPCP_FLOW_STATS
char dtstr[RIB_NAME_STRLEN + 1];
+#endif
enum pol_pff pp;
struct conn_info info;
@@ -599,10 +591,11 @@ int dt_init(struct dt_config cfg)
dt_pci_info.qc_o = dt_pci_info.addr_size;
dt_pci_info.ttl_o = dt_pci_info.qc_o + QOS_LEN;
dt_pci_info.ecn_o = dt_pci_info.ttl_o + TTL_LEN;
- dt_pci_info.eid_o = dt_pci_info.ecn_o + ECN_LEN;
+ dt_pci_info.cap_o = dt_pci_info.ecn_o + ECN_LEN;
+ dt_pci_info.eid_o = dt_pci_info.cap_o + CAP_LEN;
dt_pci_info.head_size = dt_pci_info.eid_o + dt_pci_info.eid_size;
- if (connmgr_comp_init(COMPID_DT, &info)) {
+ if (connmgr_comp_init(COMPID_DT, &info) != 0) {
log_err("Failed to register with connmgr.");
goto fail_connmgr_comp_init;
}
@@ -636,37 +629,27 @@ int dt_init(struct dt_config cfg)
goto fail_rwlock_init;
}
- dt.res_fds = bmp_create(PROG_RES_FDS, 0);
+ dt.res_fds = bmp_create(PROC_RES_FDS, 0);
if (dt.res_fds == NULL)
goto fail_res_fds;
#ifdef IPCP_FLOW_STATS
memset(dt.stat, 0, sizeof(dt.stat));
- for (i = 0; i < PROG_MAX_FLOWS; ++i)
- if (pthread_mutex_init(&dt.stat[i].lock, NULL)) {
- log_err("Failed to init mutex for flow %d.", i);
- for (j = 0; j < i; ++j)
- pthread_mutex_destroy(&dt.stat[j].lock);
- goto fail_stat_lock;
- }
-
dt.n_flows = 0;
-#endif
+
sprintf(dtstr, "%s." ADDR_FMT32, DT, ADDR_VAL32(&dt.addr));
if (rib_reg(dtstr, &r_ops)) {
log_err("Failed to register RIB.");
goto fail_rib_reg;
}
+#endif
return 0;
- fail_rib_reg:
#ifdef IPCP_FLOW_STATS
- for (i = 0; i < PROG_MAX_FLOWS; ++i)
- pthread_mutex_destroy(&dt.stat[i].lock);
- fail_stat_lock:
-#endif
+ fail_rib_reg:
bmp_destroy(dt.res_fds);
+#endif
fail_res_fds:
pthread_rwlock_destroy(&dt.lock);
fail_rwlock_init:
@@ -685,14 +668,14 @@ int dt_init(struct dt_config cfg)
void dt_fini(void)
{
+#ifdef IPCP_FLOW_STATS
char dtstr[RIB_NAME_STRLEN + 1];
+#endif
int i;
+#ifdef IPCP_FLOW_STATS
sprintf(dtstr, "%s.%" PRIu64, DT, dt.addr);
rib_unreg(dtstr);
-#ifdef IPCP_FLOW_STATS
- for (i = 0; i < PROG_MAX_FLOWS; ++i)
- pthread_mutex_destroy(&dt.stat[i].lock);
#endif
bmp_destroy(dt.res_fds);
@@ -791,7 +774,7 @@ int dt_reg_comp(void * comp,
void dt_unreg_comp(int eid)
{
- assert(eid >= 0 && eid < PROG_RES_FDS);
+ assert(eid >= 0 && eid < PROC_RES_FDS);
pthread_rwlock_wrlock(&dt.lock);
@@ -809,13 +792,18 @@ void dt_unreg_comp(int eid)
int dt_write_packet(uint64_t dst_addr,
qoscube_t qc,
uint64_t eid,
- struct ssm_pk_buff * spb)
+ struct ssm_pk_buff * spb,
+ uint8_t * ecn)
{
struct dt_pci dt_pci;
int fd;
int ret;
uint8_t * head;
size_t len;
+ size_t qlen;
+ size_t mlen;
+ uint8_t lcap;
+ bool marks;
assert(spb);
assert(dst_addr != dt.addr);
@@ -823,33 +811,21 @@ int dt_write_packet(uint64_t dst_addr,
#ifdef IPCP_FLOW_STATS
len = ssm_pk_buff_len(spb);
- if (eid < PROG_RES_FDS) {
- pthread_mutex_lock(&dt.stat[eid].lock);
-
- ++dt.stat[eid].lcl_r_pkt[qc];
- dt.stat[eid].lcl_r_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[eid].lock);
- }
+ if (eid < PROC_RES_FDS)
+ dt_stat_inc(eid, lcl_r, qc, len);
#endif
fd = pff_nhop(dt.pff[qc], dst_addr);
if (fd < 0) {
log_dbg("Could not get nhop for " ADDR_FMT32 ".",
ADDR_VAL32(&dst_addr));
#ifdef IPCP_FLOW_STATS
- if (eid < PROG_RES_FDS) {
- pthread_mutex_lock(&dt.stat[eid].lock);
-
- ++dt.stat[eid].lcl_r_pkt[qc];
- dt.stat[eid].lcl_r_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[eid].lock);
- }
+ if (eid < PROC_RES_FDS)
+ dt_stat_inc(eid, lcl_r, qc, len);
#endif
return -EPERM;
}
- head = ssm_pk_buff_head_alloc(spb, dt_pci_info.head_size);
+ head = ssm_pk_buff_push(spb, dt_pci_info.head_size);
if (head == NULL) {
log_dbg("Failed to allocate DT header.");
goto fail_write;
@@ -861,44 +837,46 @@ int dt_write_packet(uint64_t dst_addr,
dt_pci.qc = qc;
dt_pci.eid = eid;
dt_pci.ecn = 0;
+ dt_pci.cap = 0;
+
+ marks = ca_marks_ecn();
+ qlen = marks ? ipcp_flow_queued(fd) : 0;
+ mlen = marks ? ipcp_flow_mean_len(fd) : 0;
+ lcap = marks ? cap_enc(ipcp_flow_cap(fd)) : 0;
+
+ (void) ca_calc_ecn(qlen, &dt_pci.ecn, qc, mlen);
+
+ dt_pci.cap = lcap;
- (void) ca_calc_ecn(fd, &dt_pci.ecn, qc, len);
+ if (ecn != NULL)
+ *ecn = dt_pci.ecn;
dt_pci_ser(head, &dt_pci);
ret = ipcp_flow_write(fd, spb);
if (ret < 0) {
- log_dbg("Failed to write packet to fd %d.", fd);
+ log_dbg("Failed to write packet to fd %d: %d.", fd, ret);
if (ret == -EFLOWDOWN)
notifier_event(NOTIFY_DT_FLOW_DOWN, &fd);
goto fail_write;
}
#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
+ if (dt_pci.eid < PROC_RES_FDS)
+ dt_stat_inc(fd, lcl_w, qc, len);
- if (dt_pci.eid < PROG_RES_FDS) {
- ++dt.stat[fd].lcl_w_pkt[qc];
- dt.stat[fd].lcl_w_bytes[qc] += len;
- }
- ++dt.stat[fd].snd_pkt[qc];
- dt.stat[fd].snd_bytes[qc] += len;
-
- pthread_mutex_unlock(&dt.stat[fd].lock);
+ dt_stat_inc(fd, snd, qc, len);
#endif
+ if (marks)
+ ipcp_flow_cap_update(fd, qlen, len);
+
return 0;
fail_write:
#ifdef IPCP_FLOW_STATS
- pthread_mutex_lock(&dt.stat[fd].lock);
-
- if (eid < PROG_RES_FDS) {
- ++dt.stat[fd].lcl_w_pkt[qc];
- dt.stat[fd].lcl_w_bytes[qc] += len;
- }
- ++dt.stat[fd].w_drp_pkt[qc];
- dt.stat[fd].w_drp_bytes[qc] += len;
+ if (eid < PROC_RES_FDS)
+ dt_stat_inc(fd, lcl_w, qc, len);
- pthread_mutex_unlock(&dt.stat[fd].lock);
+ dt_stat_inc(fd, w_drp, qc, len);
#endif
return -1;
}
diff --git a/src/ipcpd/unicast/dt.h b/src/ipcpd/unicast/dt.h
index a484377d..a055efea 100644
--- a/src/ipcpd/unicast/dt.h
+++ b/src/ipcpd/unicast/dt.h
@@ -48,6 +48,7 @@ void dt_unreg_comp(int eid);
int dt_write_packet(uint64_t dst_addr,
qoscube_t qc,
uint64_t eid,
- struct ssm_pk_buff * spb);
+ struct ssm_pk_buff * spb,
+ uint8_t * ecn);
#endif /* OUROBOROS_IPCPD_UNICAST_DT_H */
diff --git a/src/ipcpd/unicast/fa.c b/src/ipcpd/unicast/fa.c
index ddf78e22..1c939fab 100644
--- a/src/ipcpd/unicast/fa.c
+++ b/src/ipcpd/unicast/fa.c
@@ -31,15 +31,19 @@
#define FA "flow-allocator"
#define OUROBOROS_PREFIX FA
+#include <ouroboros/atomics.h>
+#include <ouroboros/dev.h>
#include <ouroboros/endian.h>
-#include <ouroboros/logs.h>
-#include <ouroboros/fqueue.h>
#include <ouroboros/errno.h>
-#include <ouroboros/dev.h>
+#include <ouroboros/fqueue.h>
#include <ouroboros/ipcp-dev.h>
-#include <ouroboros/rib.h>
-#include <ouroboros/random.h>
+#include <ouroboros/logs.h>
+#include <ouroboros/np1_flow.h>
#include <ouroboros/pthread.h>
+#include <ouroboros/qoscube.h>
+#include <ouroboros/random.h>
+#include <ouroboros/rib.h>
+#include <ouroboros/time.h>
#include "addr-auth.h"
#include "dir.h"
@@ -58,12 +62,17 @@
#define CLOCK_REALTIME_COARSE CLOCK_REALTIME
#endif
-#define TIMEOUT 10 * MILLION /* nanoseconds */
+#define TIMEOUT 10 * MILLION /* nanoseconds */
+#define MSGBUFSZ 32768
+
+#define FLOW_REQ 0
+#define FLOW_REPLY 1
+#define FLOW_UPDATE 2
+#define FLOW_IRM_UPDATE 3
+#define FLOW_HB 4
+#define FLOW_ACK 5
-#define FLOW_REQ 0
-#define FLOW_REPLY 1
-#define FLOW_UPDATE 2
-#define MSGBUFSZ 2048
+#define HB_ID_LEN 16 /* 128-bit unguessable heartbeat nonce */
#define STAT_FILE_LEN 0
@@ -79,9 +88,11 @@ struct fa_msg {
uint32_t max_gap;
uint32_t timeout;
uint16_t ece;
+ uint8_t cap;
uint8_t code;
uint8_t availability;
- uint8_t in_order;
+ uint8_t service;
+ uint8_t hb_id[HB_ID_LEN]; /* heartbeat / ack nonce */
} __attribute__((packed));
struct cmd {
@@ -89,6 +100,22 @@ struct cmd {
struct ssm_pk_buff * spb;
};
+#define HB_TBL_TTL (4ULL * BILLION) /* drop unanswered heartbeats */
+#define HB_TBL_MAX 1024 /* cap outstanding heartbeats */
+#define HB_BUCKETS 256 /* nonce hash buckets (pow2) */
+
+/* RIB flow entry: the CA stats string plus the flow header. */
+#define FA_RIB_STRLEN (CA_STATS_STRLEN + 512)
+
+/* Outstanding heartbeat: send time kept locally, keyed by the nonce. */
+struct hb_ent {
+ uint8_t id[HB_ID_LEN];
+ uint64_t s_eid; /* originating flow (fd-reuse guard) */
+ uint64_t t_snd; /* send timestamp (ns) */
+ struct list_head hnext; /* nonce hash bucket chain */
+ struct list_head qnext; /* expiry FIFO, oldest at head */
+};
+
struct fa_flow {
#ifdef IPCP_FLOW_STATS
time_t stamp; /* Flow creation */
@@ -103,15 +130,17 @@ struct fa_flow {
size_t u_snd; /* Flow updates sent */
size_t u_rcv; /* Flow updates received */
#endif
- uint64_t s_eid; /* Local endpoint id */
- uint64_t r_eid; /* Remote endpoint id */
+ uint64_t s_eid; /* Local PoA id */
+ uint64_t r_eid; /* Remote PoA id */
uint64_t r_addr; /* Remote address */
void * ctx; /* Congestion avoidance context */
+ uint64_t fair; /* SFQ virtual finish tag (bytes) */
+ uint8_t l_ecn; /* Local first-hop mark (relaxed) */
};
struct {
pthread_rwlock_t flows_lock;
- struct fa_flow flows[PROG_MAX_FLOWS];
+ struct fa_flow flows[PROC_MAX_FLOWS];
#ifdef IPCP_FLOW_STATS
size_t n_flows;
#endif
@@ -122,21 +151,26 @@ struct {
pthread_mutex_t mtx;
pthread_t worker;
+ struct list_head hb_bkt[HB_BUCKETS]; /* nonce hash buckets */
+ struct list_head hb_q; /* expiry FIFO, oldest at head */
+ size_t n_hbs;
+ pthread_mutex_t hb_mtx;
+
struct psched * psched;
} fa;
+#ifdef IPCP_FLOW_STATS
static int fa_rib_read(const char * path,
char * buf,
size_t len)
{
-#ifdef IPCP_FLOW_STATS
struct fa_flow * flow;
int fd;
char r_addrstr[21];
char s_eidstr[21];
char r_eidstr[21];
char tmstr[RIB_TM_STRLEN];
- char castr[1024];
+ char castr[CA_STATS_STRLEN];
char * entry;
struct tm * tm;
@@ -145,10 +179,10 @@ static int fa_rib_read(const char * path,
fd = atoi(entry);
- if (fd < 0 || fd >= PROG_MAX_FLOWS)
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -1;
- if (len < 1536)
+ if (len < FA_RIB_STRLEN)
return 0;
flow = &fa.flows[fd];
@@ -169,13 +203,13 @@ static int fa_rib_read(const char * path,
tm = gmtime(&flow->stamp);
strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
- ca_print_stats(flow->ctx, castr, 1024);
+ ca_print_stats(flow->ctx, castr, CA_STATS_STRLEN);
sprintf(buf,
"Flow established at: %20s\n"
"Remote address: %20s\n"
- "Local endpoint ID: %20s\n"
- "Remote endpoint ID: %20s\n"
+ "Local PoA ID: %20s\n"
+ "Remote PoA ID: %20s\n"
"Sent (packets): %20zu\n"
"Sent (bytes): %20zu\n"
"Send failed (packets): %20zu\n"
@@ -199,17 +233,10 @@ static int fa_rib_read(const char * path,
pthread_rwlock_unlock(&fa.flows_lock);
return strlen(buf);
-#else
- (void) path;
- (void) buf;
- (void) len;
- return 0;
-#endif
}
static int fa_rib_readdir(char *** buf)
{
-#ifdef IPCP_FLOW_STATS
char entry[RIB_PATH_LEN + 1];
size_t i;
int idx = 0;
@@ -225,7 +252,7 @@ static int fa_rib_readdir(char *** buf)
if (*buf == NULL)
goto fail_entries;
- for (i = 0; i < PROG_MAX_FLOWS; ++i) {
+ for (i = 0; i < PROC_MAX_FLOWS; ++i) {
struct fa_flow * flow;
flow = &fa.flows[i];
@@ -250,20 +277,16 @@ static int fa_rib_readdir(char *** buf)
fail_entry:
while (idx-- > 0)
free((*buf)[idx]);
+
free(*buf);
fail_entries:
pthread_rwlock_unlock(&fa.flows_lock);
return -ENOMEM;
-#else
- (void) buf;
- return 0;
-#endif
}
static int fa_rib_getattr(const char * path,
struct rib_attr * attr)
{
-#ifdef IPCP_FLOW_STATS
int fd;
char * entry;
struct fa_flow * flow;
@@ -278,7 +301,7 @@ static int fa_rib_getattr(const char * path,
pthread_rwlock_rdlock(&fa.flows_lock);
if (flow->stamp != 0) {
- attr->size = 1536;
+ attr->size = FA_RIB_STRLEN;
attr->mtime = flow->stamp;
} else {
attr->size = 0;
@@ -286,10 +309,7 @@ static int fa_rib_getattr(const char * path,
}
pthread_rwlock_unlock(&fa.flows_lock);
-#else
- (void) path;
- (void) attr;
-#endif
+
return 0;
}
@@ -298,6 +318,7 @@ static struct rib_ops r_ops = {
.readdir = fa_rib_readdir,
.getattr = fa_rib_getattr
};
+#endif /* IPCP_FLOW_STATS */
static int eid_to_fd(uint64_t eid)
{
@@ -306,7 +327,7 @@ static int eid_to_fd(uint64_t eid)
fd = eid & 0xFFFFFFFF;
- if (fd < 0 || fd >= PROG_MAX_FLOWS)
+ if (fd < 0 || fd >= PROC_MAX_FLOWS)
return -1;
flow = &fa.flows[fd];
@@ -329,18 +350,140 @@ static uint64_t gen_eid(int fd)
return ((uint64_t) rnd << 32) + fd;
}
-static void packet_handler(int fd,
- qoscube_t qc,
- struct ssm_pk_buff * spb)
+/* The nonce is uniformly random, so its low word is a fine hash. */
+static size_t fa_hb_hash(const uint8_t * id)
+{
+ uint32_t h;
+
+ memcpy(&h, id, sizeof(h));
+
+ return h & (HB_BUCKETS - 1);
+}
+
+/* Record an outstanding heartbeat; expire stale entries as we go. */
+static void fa_hb_record(const uint8_t * id,
+ uint64_t s_eid,
+ uint64_t t_snd)
+{
+ struct hb_ent * ent;
+ struct list_head * p;
+ struct list_head * h;
+
+ ent = malloc(sizeof(*ent));
+ if (ent == NULL)
+ return;
+
+ memcpy(ent->id, id, HB_ID_LEN);
+ ent->s_eid = s_eid;
+ ent->t_snd = t_snd;
+
+ pthread_mutex_lock(&fa.hb_mtx);
+
+ /* The FIFO is time-ordered; stop at the first fresh entry. */
+ list_for_each_safe(p, h, &fa.hb_q) {
+ struct hb_ent * e = list_entry(p, struct hb_ent, qnext);
+ if (t_snd - e->t_snd <= HB_TBL_TTL)
+ break;
+ list_del(&e->hnext);
+ list_del(&e->qnext);
+ free(e);
+ fa.n_hbs--;
+ }
+
+ if (fa.n_hbs >= HB_TBL_MAX) {
+ pthread_mutex_unlock(&fa.hb_mtx);
+ free(ent);
+ return;
+ }
+
+ list_add(&ent->hnext, &fa.hb_bkt[fa_hb_hash(id)]);
+ list_add_tail(&ent->qnext, &fa.hb_q);
+ fa.n_hbs++;
+
+ pthread_mutex_unlock(&fa.hb_mtx);
+}
+
+/* Consume a heartbeat nonce, returning the flow and send time it maps to. */
+static int fa_hb_match(const uint8_t * id,
+ uint64_t * s_eid,
+ uint64_t * t_snd)
+{
+ struct list_head * bkt;
+ struct list_head * p;
+ struct list_head * h;
+
+ pthread_mutex_lock(&fa.hb_mtx);
+
+ bkt = &fa.hb_bkt[fa_hb_hash(id)];
+ list_for_each_safe(p, h, bkt) {
+ struct hb_ent * e = list_entry(p, struct hb_ent, hnext);
+ if (memcmp(e->id, id, HB_ID_LEN) == 0) {
+ *s_eid = e->s_eid;
+ *t_snd = e->t_snd;
+ list_del(&e->hnext);
+ list_del(&e->qnext);
+ free(e);
+ fa.n_hbs--;
+ pthread_mutex_unlock(&fa.hb_mtx);
+ return 0;
+ }
+ }
+
+ pthread_mutex_unlock(&fa.hb_mtx);
+
+ return -1;
+}
+
+/* Send a bare control message (heartbeat or ack) carrying only a nonce. */
+static int fa_send_ctrl(uint64_t r_addr,
+ uint8_t code,
+ const uint8_t * id)
+{
+ struct fa_msg * msg;
+ struct ssm_pk_buff * spb;
+ qoscube_t qc = QOS_CUBE_BE;
+
+ if (ipcp_spb_reserve(&spb, sizeof(*msg)))
+ return -1;
+
+ msg = (struct fa_msg *) ssm_pk_buff_head(spb);
+ memset(msg, 0, sizeof(*msg));
+
+ msg->code = code;
+ msg->s_addr = hton64(addr_auth_address());
+ memcpy(msg->hb_id, id, HB_ID_LEN);
+
+ if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) {
+ ipcp_spb_release(spb);
+ return -1;
+ }
+
+ return 0;
+}
+
+static time_t packet_handler(int fd,
+ qoscube_t qc,
+ struct ssm_pk_buff * spb)
{
struct fa_flow * flow;
+ struct timespec tv;
+ uint64_t now;
uint64_t r_addr;
uint64_t r_eid;
- ca_wnd_t wnd;
+ uint64_t s_eid;
+ bool hb;
+ uint8_t nonce[HB_ID_LEN];
+ time_t wait;
size_t len;
+ uint8_t ecn;
flow = &fa.flows[fd];
+ ecn = 0;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &tv);
+ now = TS_TO_UINT64(tv);
+
pthread_rwlock_wrlock(&fa.flows_lock);
len = ssm_pk_buff_len(spb);
@@ -349,16 +492,23 @@ static void packet_handler(int fd,
++flow->p_snd;
flow->b_snd += len;
#endif
- wnd = ca_ctx_update_snd(flow->ctx, len);
+ wait = ca_ctx_update_snd(flow->ctx, len,
+ LOAD_RELAXED(&flow->l_ecn), &flow->fair);
+ hb = ca_ctx_hb_due(flow->ctx, now);
r_addr = flow->r_addr;
r_eid = flow->r_eid;
+ s_eid = flow->s_eid;
pthread_rwlock_unlock(&fa.flows_lock);
- ca_wnd_wait(wnd);
+ if (hb && random_buffer(nonce, HB_ID_LEN) == 0) {
+ fa_hb_record(nonce, s_eid, now);
+ fa_send_ctrl(r_addr, FLOW_HB, nonce);
+ }
- if (dt_write_packet(r_addr, qc, r_eid, spb)) {
+ if (dt_write_packet(r_addr, qc, r_eid, spb, &ecn)) {
+ STORE_RELAXED(&flow->l_ecn, ecn);
ipcp_spb_release(spb);
log_dbg("Failed to forward packet.");
#ifdef IPCP_FLOW_STATS
@@ -367,8 +517,12 @@ static void packet_handler(int fd,
flow->b_snd_f += len;
pthread_rwlock_unlock(&fa.flows_lock);
#endif
- return;
+ return wait;
}
+
+ STORE_RELAXED(&flow->l_ecn, ecn);
+
+ return wait;
}
static int fa_flow_init(struct fa_flow * flow)
@@ -382,9 +536,7 @@ static int fa_flow_init(struct fa_flow * flow)
flow->s_eid = -1;
flow->r_addr = INVALID_ADDR;
- flow->ctx = ca_ctx_create();
- if (flow->ctx == NULL)
- return -1;
+ /* ctx is acquired once (r_addr, qc) are known (ca_ctx_get). */
#ifdef IPCP_FLOW_STATS
clock_gettime(CLOCK_REALTIME_COARSE, &now);
@@ -398,7 +550,8 @@ static int fa_flow_init(struct fa_flow * flow)
static void fa_flow_fini(struct fa_flow * flow)
{
- ca_ctx_destroy(flow->ctx);
+ if (flow->ctx != NULL)
+ ca_ctx_put(flow->ctx);
memset(flow, 0, sizeof(*flow));
@@ -496,11 +649,15 @@ static int fa_handle_flow_req(struct fa_msg * msg,
qs.availability = msg->availability;
qs.loss = ntoh32(msg->loss);
qs.ber = ntoh32(msg->ber);
- qs.in_order = msg->in_order;
+ qs.service = msg->service;
qs.max_gap = ntoh32(msg->max_gap);
qs.timeout = ntoh32(msg->timeout);
- fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UNICAST_MPL, &data);
+ /* Ack the seed nonce now: a clean RTT, before any accept delay. */
+ fa_send_ctrl(ntoh64(msg->s_addr), FLOW_ACK, msg->hb_id);
+
+ fd = ipcp_wait_flow_req_arr(dst, qs, IPCP_UNICAST_MPL,
+ IPCP_UNICAST_MTU, &data);
if (fd < 0)
return fd;
@@ -514,6 +671,13 @@ static int fa_handle_flow_req(struct fa_msg * msg,
flow->r_eid = ntoh64(msg->s_eid);
flow->r_addr = ntoh64(msg->s_addr);
+ flow->ctx = ca_ctx_get(flow->r_addr, qos_spec_to_cube(qs));
+ if (flow->ctx == NULL) {
+ fa_flow_fini(flow);
+ pthread_rwlock_unlock(&fa.flows_lock);
+ return -ENOMEM;
+ }
+
pthread_rwlock_unlock(&fa.flows_lock);
return fd;
@@ -528,7 +692,8 @@ static int fa_handle_flow_reply(struct fa_msg * msg,
time_t mpl = IPCP_UNICAST_MPL;
int response;
- assert(len >= sizeof(*msg));
+ if (len < sizeof(*msg))
+ return -EINVAL;
data.data = (uint8_t *) msg + sizeof(*msg);
data.len = len - sizeof(*msg);
@@ -558,7 +723,8 @@ static int fa_handle_flow_reply(struct fa_msg * msg,
pthread_rwlock_unlock(&fa.flows_lock);
- if (ipcp_flow_alloc_reply(fd, response, mpl, &data) < 0) {
+ if (ipcp_flow_alloc_reply(fd, response, mpl,
+ IPCP_UNICAST_MTU, &data) < 0) {
log_err("Failed to reply for flow allocation on fd %d.", fd);
return -EIRMD;
}
@@ -572,8 +738,8 @@ static int fa_handle_flow_update(struct fa_msg * msg,
struct fa_flow * flow;
int fd;
- (void) len;
- assert(len >= sizeof(*msg));
+ if (len < sizeof(*msg))
+ return -EINVAL;
pthread_rwlock_wrlock(&fa.flows_lock);
@@ -589,13 +755,93 @@ static int fa_handle_flow_update(struct fa_msg * msg,
#ifdef IPCP_FLOW_STATS
flow->u_rcv++;
#endif
- ca_ctx_update_ece(flow->ctx, ntoh16(msg->ece));
+ ca_ctx_update_ece(flow->ctx, ntoh16(msg->ece), msg->cap);
pthread_rwlock_unlock(&fa.flows_lock);
return 0;
}
+/* Heartbeat: reflect the nonce straight back to the sender's address. */
+static int fa_handle_flow_hb(struct fa_msg * msg,
+ size_t len)
+{
+ if (len < sizeof(*msg))
+ return -EINVAL;
+
+ return fa_send_ctrl(ntoh64(msg->s_addr), FLOW_ACK, msg->hb_id);
+}
+
+/* Ack: the reflected nonce yields an RTT sample for its path. */
+static int fa_handle_flow_ack(struct fa_msg * msg,
+ size_t len)
+{
+ struct timespec tv;
+ struct fa_flow * flow;
+ uint64_t now;
+ uint64_t t_snd;
+ uint64_t s_eid;
+ int fd;
+
+ if (len < sizeof(*msg))
+ return -EINVAL;
+
+ if (fa_hb_match(msg->hb_id, &s_eid, &t_snd) < 0)
+ return 0; /* unknown or stale nonce */
+
+ clock_gettime(PTHREAD_COND_CLOCK, &tv);
+ now = TS_TO_UINT64(tv);
+
+ pthread_rwlock_wrlock(&fa.flows_lock);
+
+ fd = eid_to_fd(s_eid);
+ if (fd >= 0 && now > t_snd) {
+ flow = &fa.flows[fd];
+ ca_ctx_rtt(flow->ctx, now, now - t_snd);
+ }
+
+ pthread_rwlock_unlock(&fa.flows_lock);
+
+ return 0;
+}
+
+static int fa_handle_flow_irm_update(struct fa_msg * msg,
+ size_t len)
+{
+ buffer_t data;
+ int fd;
+ int flow_id;
+
+ if (len < sizeof(*msg))
+ return -EINVAL;
+
+ data.data = (uint8_t *) msg + sizeof(*msg);
+ data.len = len - sizeof(*msg);
+
+ pthread_rwlock_rdlock(&fa.flows_lock);
+
+ fd = eid_to_fd(ntoh64(msg->r_eid));
+
+ pthread_rwlock_unlock(&fa.flows_lock);
+
+ if (fd < 0) {
+ log_err("Flow update for unknown EID %" PRIu64 ".",
+ ntoh64(msg->r_eid));
+ return -ENOTALLOC;
+ }
+
+ flow_id = np1_flow_id(fd);
+ if (flow_id < 0)
+ return -ENOTALLOC;
+
+ if (ipcp_flow_update_arr(flow_id, &data) < 0) {
+ log_err("Failed to relay flow update on fd %d.", fd);
+ return -EIRMD;
+ }
+
+ return 0;
+}
+
static void * fa_handle_packet(void * o)
{
(void) o;
@@ -624,6 +870,18 @@ static void * fa_handle_packet(void * o)
if (fa_handle_flow_update(msg, len) < 0)
log_err("Error handling flow update.");
break;
+ case FLOW_IRM_UPDATE:
+ if (fa_handle_flow_irm_update(msg, len) < 0)
+ log_err("Error handling flow update.");
+ break;
+ case FLOW_HB:
+ if (fa_handle_flow_hb(msg, len) < 0)
+ log_err("Error handling heartbeat.");
+ break;
+ case FLOW_ACK:
+ if (fa_handle_flow_ack(msg, len) < 0)
+ log_err("Error handling heartbeat ack.");
+ break;
default:
log_warn("Recieved unknown flow allocation message.");
break;
@@ -633,45 +891,62 @@ static void * fa_handle_packet(void * o)
return (void *) 0;
}
-int fa_init(void)
+int fa_init(uint16_t max_rtt)
{
pthread_condattr_t cattr;
+ size_t i;
- if (pthread_rwlock_init(&fa.flows_lock, NULL))
+ ipcp_flow_set_max_rtt(max_rtt);
+
+ if (pthread_rwlock_init(&fa.flows_lock, NULL) != 0)
goto fail_rwlock;
- if (pthread_mutex_init(&fa.mtx, NULL))
+ if (pthread_mutex_init(&fa.mtx, NULL) != 0)
goto fail_mtx;
- if (pthread_condattr_init(&cattr))
+ if (pthread_mutex_init(&fa.hb_mtx, NULL) != 0)
+ goto fail_hb_mtx;
+
+ if (pthread_condattr_init(&cattr) != 0)
goto fail_cattr;
#ifndef __APPLE__
pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
#endif
- if (pthread_cond_init(&fa.cond, &cattr))
+ if (pthread_cond_init(&fa.cond, &cattr) != 0)
goto fail_cond;
- if (rib_reg(FA, &r_ops))
+#ifdef IPCP_FLOW_STATS
+ if (rib_reg(FA, &r_ops) != 0)
goto fail_rib_reg;
+#endif
fa.eid = dt_reg_comp(&fa, &fa_post_packet, FA);
if ((int) fa.eid < 0)
goto fail_dt_reg;
list_head_init(&fa.cmds);
+ for (i = 0; i < HB_BUCKETS; i++)
+ list_head_init(&fa.hb_bkt[i]);
+
+ list_head_init(&fa.hb_q);
+ fa.n_hbs = 0;
pthread_condattr_destroy(&cattr);
return 0;
fail_dt_reg:
+#ifdef IPCP_FLOW_STATS
rib_unreg(FA);
fail_rib_reg:
+#endif
pthread_cond_destroy(&fa.cond);
fail_cond:
pthread_condattr_destroy(&cattr);
fail_cattr:
+ pthread_mutex_destroy(&fa.hb_mtx);
+ fail_hb_mtx:
pthread_mutex_destroy(&fa.mtx);
fail_mtx:
pthread_rwlock_destroy(&fa.flows_lock);
@@ -681,9 +956,20 @@ int fa_init(void)
void fa_fini(void)
{
+ struct list_head * p;
+ struct list_head * h;
+
+#ifdef IPCP_FLOW_STATS
rib_unreg(FA);
+#endif
+ list_for_each_safe(p, h, &fa.hb_q) {
+ struct hb_ent * e = list_entry(p, struct hb_ent, qnext);
+ list_del(&e->qnext);
+ free(e);
+ }
pthread_cond_destroy(&fa.cond);;
+ pthread_mutex_destroy(&fa.hb_mtx);
pthread_mutex_destroy(&fa.mtx);
pthread_rwlock_destroy(&fa.flows_lock);
}
@@ -766,6 +1052,8 @@ int fa_alloc(int fd,
qoscube_t qc = QOS_CUBE_BE;
size_t len;
uint64_t eid;
+ struct timespec tv;
+ uint8_t nonce[HB_ID_LEN];
addr = dir_query(dst);
if (addr == 0)
@@ -789,15 +1077,22 @@ int fa_alloc(int fd,
msg->availability = qs.availability;
msg->loss = hton32(qs.loss);
msg->ber = hton32(qs.ber);
- msg->in_order = qs.in_order;
+ msg->service = qs.service;
msg->max_gap = hton32(qs.max_gap);
msg->timeout = hton32(qs.timeout);
+ /* Seed an early RTT sample: peer acks this nonce on arrival. */
+ if (random_buffer(nonce, HB_ID_LEN) == 0) {
+ memcpy(msg->hb_id, nonce, HB_ID_LEN);
+ clock_gettime(PTHREAD_COND_CLOCK, &tv);
+ fa_hb_record(nonce, eid, TS_TO_UINT64(tv));
+ }
+
memcpy(msg + 1, dst, ipcp_dir_hash_len());
if (data->len > 0)
memcpy(ssm_pk_buff_head(spb) + len, data->data, data->len);
- if (dt_write_packet(addr, qc, fa.eid, spb)) {
+ if (dt_write_packet(addr, qc, fa.eid, spb, NULL)) {
log_err("Failed to send flow allocation request packet.");
ipcp_spb_release(spb);
return -1;
@@ -811,6 +1106,13 @@ int fa_alloc(int fd,
flow->r_addr = addr;
flow->s_eid = eid;
+ flow->ctx = ca_ctx_get(addr, qos_spec_to_cube(qs));
+ if (flow->ctx == NULL) {
+ fa_flow_fini(flow);
+ pthread_rwlock_unlock(&fa.flows_lock);
+ return -1;
+ }
+
pthread_rwlock_unlock(&fa.flows_lock);
return 0;
@@ -853,7 +1155,7 @@ int fa_alloc_resp(int fd,
pthread_rwlock_unlock(&fa.flows_lock);
- if (dt_write_packet(flow->r_addr, qc, fa.eid, spb)) {
+ if (dt_write_packet(flow->r_addr, qc, fa.eid, spb, NULL)) {
log_err("Failed to send flow allocation response packet.");
goto fail_packet;
}
@@ -878,6 +1180,44 @@ int fa_alloc_resp(int fd,
return -1;
}
+int fa_irm_update(int fd,
+ const buffer_t * data)
+{
+ struct fa_msg * msg;
+ struct ssm_pk_buff * spb;
+ struct fa_flow * flow;
+ qoscube_t qc = QOS_CUBE_BE;
+ uint64_t r_addr;
+
+ flow = &fa.flows[fd];
+
+ if (ipcp_spb_reserve(&spb, sizeof(*msg) + data->len))
+ return -1;
+
+ msg = (struct fa_msg *) ssm_pk_buff_head(spb);
+ memset(msg, 0, sizeof(*msg));
+
+ msg->code = FLOW_IRM_UPDATE;
+ if (data->len > 0)
+ memcpy(msg + 1, data->data, data->len);
+
+ pthread_rwlock_rdlock(&fa.flows_lock);
+
+ msg->r_eid = hton64(flow->r_eid);
+ msg->s_eid = hton64(flow->s_eid);
+ r_addr = flow->r_addr;
+
+ pthread_rwlock_unlock(&fa.flows_lock);
+
+ if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) {
+ log_err("Failed to send flow update packet.");
+ ipcp_spb_release(spb);
+ return -1;
+ }
+
+ return 0;
+}
+
int fa_dealloc(int fd)
{
if (ipcp_flow_fini(fd) < 0)
@@ -897,7 +1237,8 @@ int fa_dealloc(int fd)
}
static int fa_update_remote(int fd,
- uint16_t ece)
+ uint16_t ece,
+ uint8_t cap)
{
struct fa_msg * msg;
struct ssm_pk_buff * spb;
@@ -921,6 +1262,7 @@ static int fa_update_remote(int fd,
msg->code = FLOW_UPDATE;
msg->r_eid = hton64(flow->r_eid);
msg->ece = hton16(ece);
+ msg->cap = cap;
r_addr = flow->r_addr;
#ifdef IPCP_FLOW_STATS
@@ -929,7 +1271,7 @@ static int fa_update_remote(int fd,
pthread_rwlock_unlock(&fa.flows_lock);
- if (dt_write_packet(r_addr, qc, fa.eid, spb)) {
+ if (dt_write_packet(r_addr, qc, fa.eid, spb, NULL)) {
log_err("Failed to send flow update packet.");
ipcp_spb_release(spb);
return -1;
@@ -940,11 +1282,13 @@ static int fa_update_remote(int fd,
void fa_np1_rcv(uint64_t eid,
uint8_t ecn,
+ uint8_t cap,
struct ssm_pk_buff * spb)
{
struct fa_flow * flow;
bool update;
uint16_t ece;
+ uint8_t fcap;
int fd;
size_t len;
@@ -966,7 +1310,7 @@ void fa_np1_rcv(uint64_t eid,
++flow->p_rcv;
flow->b_rcv += len;
#endif
- update = ca_ctx_update_rcv(flow->ctx, len, ecn, &ece);
+ update = ca_ctx_update_rcv(flow->ctx, len, ecn, cap, &ece, &fcap);
pthread_rwlock_unlock(&fa.flows_lock);
@@ -982,5 +1326,5 @@ void fa_np1_rcv(uint64_t eid,
}
if (update)
- fa_update_remote(eid, ece);
+ fa_update_remote(eid, ece, fcap);
}
diff --git a/src/ipcpd/unicast/fa.h b/src/ipcpd/unicast/fa.h
index 0c19dc25..504d67d3 100644
--- a/src/ipcpd/unicast/fa.h
+++ b/src/ipcpd/unicast/fa.h
@@ -26,7 +26,7 @@
#include <ouroboros/qos.h>
#include <ouroboros/utils.h>
-int fa_init(void);
+int fa_init(uint16_t max_rtt);
void fa_fini(void);
@@ -45,8 +45,12 @@ int fa_alloc_resp(int fd,
int fa_dealloc(int fd);
+int fa_irm_update(int fd,
+ const buffer_t * data);
+
void fa_np1_rcv(uint64_t eid,
uint8_t ecn,
+ uint8_t cap,
struct ssm_pk_buff * spb);
#endif /* OUROBOROS_IPCPD_UNICAST_FA_H */
diff --git a/src/ipcpd/unicast/main.c b/src/ipcpd/unicast/main.c
index 583a04ff..320ce165 100644
--- a/src/ipcpd/unicast/main.c
+++ b/src/ipcpd/unicast/main.c
@@ -35,6 +35,7 @@
#include <ouroboros/ipcp-dev.h>
#include <ouroboros/logs.h>
#include <ouroboros/notifier.h>
+#include <ouroboros/qos.h>
#include <ouroboros/random.h>
#include <ouroboros/rib.h>
#include <ouroboros/time.h>
@@ -67,7 +68,7 @@ static int initialize_components(struct ipcp_config * conf)
log_info("IPCP got address %" PRIu64 ".", addr_auth_address());
- if (ca_init(conf->unicast.cong_avoid)) {
+ if (ca_init(conf->unicast.cong_avoid, conf->unicast.dt.max_rtt)) {
log_err("Failed to initialize congestion avoidance.");
goto fail_ca;
}
@@ -84,7 +85,7 @@ static int initialize_components(struct ipcp_config * conf)
goto fail_dir;
}
- if (fa_init()) {
+ if (fa_init(conf->unicast.dt.max_rtt)) {
log_err("Failed to initialize flow allocator component.");
goto fail_fa;
}
@@ -175,12 +176,14 @@ static void stop_components(void)
ipcp_set_state(IPCP_BOOT);
}
-static int unicast_ipcp_enroll(const char * dst,
- struct layer_info * info)
+static int unicast_ipcp_enroll(const char * dst,
+ const struct poa_addr * addr,
+ struct layer_info * info)
{
struct ipcp_config * conf;
struct conn conn;
uint8_t id[ENROLL_ID_LEN];
+ qosspec_t qs = qos_msg;
if (random_buffer(id, ENROLL_ID_LEN) < 0) {
log_err("Failed to generate enrollment ID.");
@@ -189,7 +192,7 @@ static int unicast_ipcp_enroll(const char * dst,
log_info_id(id, "Requesting enrollment.");
- if (connmgr_alloc(COMPID_ENROLL, dst, NULL, &conn) < 0) {
+ if (connmgr_alloc(COMPID_ENROLL, dst, &qs, addr, &conn) < 0) {
log_err_id(id, "Failed to get connection.");
goto fail_id;
}
@@ -273,7 +276,8 @@ static struct ipcp_ops unicast_ops = {
.ipcp_flow_alloc = fa_alloc,
.ipcp_flow_join = NULL,
.ipcp_flow_alloc_resp = fa_alloc_resp,
- .ipcp_flow_dealloc = fa_dealloc
+ .ipcp_flow_dealloc = fa_dealloc,
+ .ipcp_flow_update = fa_irm_update
};
int main(int argc,
@@ -307,8 +311,8 @@ int main(int argc,
ipcp_sigwait();
if (ipcp_get_state() == IPCP_SHUTDOWN) {
- stop_components();
ipcp_stop();
+ stop_components();
finalize_components();
} else {
ipcp_stop();
diff --git a/src/ipcpd/unicast/pff/alternate.c b/src/ipcpd/unicast/pff/alternate.c
index be1c35c0..1c508c1b 100644
--- a/src/ipcpd/unicast/pff/alternate.c
+++ b/src/ipcpd/unicast/pff/alternate.c
@@ -211,7 +211,7 @@ struct pff_i * alternate_pff_create(void)
if (pthread_rwlock_init(&tmp->lock, NULL))
goto fail_lock;
- tmp->pft = pft_create(PFT_SIZE, false);
+ tmp->pft = pft_create(PFT_SIZE);
if (tmp->pft == NULL)
goto fail_pft;
diff --git a/src/ipcpd/unicast/pff/multipath.c b/src/ipcpd/unicast/pff/multipath.c
index c636e789..c2c21078 100644
--- a/src/ipcpd/unicast/pff/multipath.c
+++ b/src/ipcpd/unicast/pff/multipath.c
@@ -49,7 +49,7 @@ struct pff_ops multipath_pff_ops = {
.del = multipath_pff_del,
.flush = multipath_pff_flush,
.nhop = multipath_pff_nhop,
- .flow_state_change = NULL
+ .flow_state_change = multipath_pff_flow_state_change
};
struct pff_i * multipath_pff_create(void)
@@ -63,7 +63,7 @@ struct pff_i * multipath_pff_create(void)
if (pthread_rwlock_init(&tmp->lock, NULL))
goto fail_rwlock;
- tmp->pft = pft_create(PFT_SIZE, false);
+ tmp->pft = pft_create(PFT_SIZE);
if (tmp->pft == NULL)
goto fail_pft;
@@ -170,6 +170,24 @@ void multipath_pff_flush(struct pff_i * pff_i)
pft_flush(pff_i->pft);
}
+int multipath_pff_flow_state_change(struct pff_i * pff_i,
+ int fd,
+ bool up)
+{
+ assert(pff_i);
+
+ if (up)
+ return 0;
+
+ pthread_rwlock_wrlock(&pff_i->lock);
+
+ pft_del_fd(pff_i->pft, fd);
+
+ pthread_rwlock_unlock(&pff_i->lock);
+
+ return 0;
+}
+
int multipath_pff_nhop(struct pff_i * pff_i,
uint64_t addr)
{
diff --git a/src/ipcpd/unicast/pff/multipath.h b/src/ipcpd/unicast/pff/multipath.h
index 5329f7fc..123030b6 100644
--- a/src/ipcpd/unicast/pff/multipath.h
+++ b/src/ipcpd/unicast/pff/multipath.h
@@ -53,6 +53,10 @@ void multipath_pff_flush(struct pff_i * pff_i);
int multipath_pff_nhop(struct pff_i * pff_i,
uint64_t addr);
+int multipath_pff_flow_state_change(struct pff_i * pff_i,
+ int fd,
+ bool up);
+
extern struct pff_ops multipath_pff_ops;
#endif /* OUROBOROS_IPCPD_UNICAST_MULTIPATH_PFF_H */
diff --git a/src/ipcpd/unicast/pff/pft.c b/src/ipcpd/unicast/pff/pft.c
index a0d70799..2a295a40 100644
--- a/src/ipcpd/unicast/pff/pft.c
+++ b/src/ipcpd/unicast/pff/pft.c
@@ -43,12 +43,10 @@ struct pft_entry {
struct pft {
struct list_head * buckets;
- bool hash_key;
uint64_t buckets_size;
};
-struct pft * pft_create(uint64_t buckets,
- bool hash_key)
+struct pft * pft_create(uint64_t buckets)
{
struct pft * tmp;
unsigned int i;
@@ -69,7 +67,6 @@ struct pft * pft_create(uint64_t buckets,
if (tmp == NULL)
return NULL;
- tmp->hash_key = hash_key;
tmp->buckets_size = buckets;
tmp->buckets = malloc(buckets * sizeof(*tmp->buckets));
@@ -94,18 +91,36 @@ void pft_destroy(struct pft * pft)
free(pft);
}
-void pft_flush(struct pft * pft)
+void pft_del_fd(struct pft * pft,
+ int fd)
{
unsigned int i;
struct list_head * p;
struct list_head * h;
struct pft_entry * entry;
+ size_t j;
+ size_t n;
assert(pft);
for (i = 0; i < pft->buckets_size; i++) {
list_for_each_safe(p, h, &(pft->buckets[i])) {
entry = list_entry(p, struct pft_entry, next);
+
+ n = 0;
+ for (j = 0; j < entry->len; j++) {
+ if (entry->fds[j] != fd)
+ entry->fds[n++] = entry->fds[j];
+ }
+
+ if (n == entry->len)
+ continue;
+
+ if (n > 0) {
+ entry->len = n;
+ continue;
+ }
+
list_del(&entry->next);
free(entry->fds);
free(entry);
@@ -113,22 +128,29 @@ void pft_flush(struct pft * pft)
}
}
-static uint64_t hash(uint64_t key)
+void pft_flush(struct pft * pft)
{
- uint64_t res[2];
+ unsigned int i;
+ struct list_head * p;
+ struct list_head * h;
+ struct pft_entry * entry;
- mem_hash(HASH_MD5, res, (uint8_t *) &key, sizeof(key));
+ assert(pft);
- return res[0];
+ for (i = 0; i < pft->buckets_size; i++) {
+ list_for_each_safe(p, h, &(pft->buckets[i])) {
+ entry = list_entry(p, struct pft_entry, next);
+ list_del(&entry->next);
+ free(entry->fds);
+ free(entry);
+ }
+ }
}
static uint64_t calc_key(struct pft * pft,
uint64_t dst)
{
- if (pft->hash_key)
- dst = hash(dst);
-
- return (dst & (pft->buckets_size - 1));
+ return hash_mix64(dst) & (pft->buckets_size - 1);
}
int pft_insert(struct pft * pft,
diff --git a/src/ipcpd/unicast/pff/pft.h b/src/ipcpd/unicast/pff/pft.h
index 3bb9cff7..3517e0ef 100644
--- a/src/ipcpd/unicast/pff/pft.h
+++ b/src/ipcpd/unicast/pff/pft.h
@@ -24,19 +24,20 @@
#define OUROBOROS_PFT_H
#include <stdint.h>
-#include <stdbool.h>
#include <stdlib.h>
struct pft;
/* Buckets is rounded up to the nearest power of 2 */
-struct pft * pft_create(uint64_t buckets,
- bool hash_key);
+struct pft * pft_create(uint64_t buckets);
void pft_destroy(struct pft * table);
void pft_flush(struct pft * table);
+void pft_del_fd(struct pft * table,
+ int fd);
+
/* Passes ownership of the block of memory */
int pft_insert(struct pft * pft,
uint64_t dst,
diff --git a/src/ipcpd/unicast/pff/simple.c b/src/ipcpd/unicast/pff/simple.c
index be542bdb..4347dcba 100644
--- a/src/ipcpd/unicast/pff/simple.c
+++ b/src/ipcpd/unicast/pff/simple.c
@@ -47,7 +47,7 @@ struct pff_ops simple_pff_ops = {
.del = simple_pff_del,
.flush = simple_pff_flush,
.nhop = simple_pff_nhop,
- .flow_state_change = NULL
+ .flow_state_change = simple_pff_flow_state_change
};
struct pff_i * simple_pff_create(void)
@@ -63,7 +63,7 @@ struct pff_i * simple_pff_create(void)
return NULL;
}
- tmp->pft = pft_create(PFT_SIZE, false);
+ tmp->pft = pft_create(PFT_SIZE);
if (tmp->pft == NULL) {
pthread_rwlock_destroy(&tmp->lock);
free(tmp);
@@ -170,6 +170,24 @@ void simple_pff_flush(struct pff_i * pff_i)
pft_flush(pff_i->pft);
}
+int simple_pff_flow_state_change(struct pff_i * pff_i,
+ int fd,
+ bool up)
+{
+ assert(pff_i);
+
+ if (up)
+ return 0;
+
+ pthread_rwlock_wrlock(&pff_i->lock);
+
+ pft_del_fd(pff_i->pft, fd);
+
+ pthread_rwlock_unlock(&pff_i->lock);
+
+ return 0;
+}
+
int simple_pff_nhop(struct pff_i * pff_i,
uint64_t addr)
{
diff --git a/src/ipcpd/unicast/pff/simple.h b/src/ipcpd/unicast/pff/simple.h
index 1046e4c4..b72aba21 100644
--- a/src/ipcpd/unicast/pff/simple.h
+++ b/src/ipcpd/unicast/pff/simple.h
@@ -52,6 +52,10 @@ void simple_pff_flush(struct pff_i * pff_i);
int simple_pff_nhop(struct pff_i * pff_i,
uint64_t addr);
+int simple_pff_flow_state_change(struct pff_i * pff_i,
+ int fd,
+ bool up);
+
extern struct pff_ops simple_pff_ops;
#endif /* OUROBOROS_IPCPD_UNICAST_SIMPLE_PFF_H */
diff --git a/src/ipcpd/unicast/pff/tests/pft_test.c b/src/ipcpd/unicast/pff/tests/pft_test.c
index 4962c241..0b4a165b 100644
--- a/src/ipcpd/unicast/pff/tests/pft_test.c
+++ b/src/ipcpd/unicast/pff/tests/pft_test.c
@@ -22,105 +22,321 @@
#include "pft.c"
+#include <test/test.h>
+
#include <stdio.h>
#define TBL_SIZE 256
#define INT_TEST 4
-int pft_test(int argc,
- char ** argv)
+/* Next hops used by the del_fd tests. */
+#define FD_GONE 7
+#define FD_KEEP 8
+#define FD_OTHER 9
+
+static int pft_add(struct pft * pft,
+ uint64_t dst,
+ const int * fds,
+ size_t len)
+{
+ int * blk;
+ size_t i;
+
+ blk = malloc(sizeof(*blk) * len);
+ if (blk == NULL)
+ return -1;
+
+ for (i = 0; i < len; i++)
+ blk[i] = fds[i];
+
+ if (pft_insert(pft, dst, blk, len)) {
+ free(blk);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int test_pft_create_destroy(void)
{
struct pft * pft;
- int i;
- int * j;
- size_t len;
- (void) argc;
- (void) argv;
+ TEST_START();
- pft = pft_create(TBL_SIZE, true);
+ pft = pft_create(TBL_SIZE);
if (pft == NULL) {
printf("Failed to create.\n");
- return -1;
+ goto fail;
}
pft_destroy(pft);
- pft = pft_create(TBL_SIZE, false);
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_pft_insert_lookup(void)
+{
+ struct pft * pft;
+ int * j;
+ size_t len;
+ int i;
+
+ TEST_START();
+
+ pft = pft_create(TBL_SIZE);
if (pft == NULL) {
printf("Failed to create.\n");
- return -1;
+ goto fail;
}
for (i = 0; i < TBL_SIZE + INT_TEST + 2; i++) {
- j = malloc(sizeof(*j));
- if (j == NULL) {
- printf("Failed to malloc.\n");
- pft_destroy(pft);
- return -1;
- }
- *j = i;
-
- if (pft_insert(pft, i, j, 1)) {
+ if (pft_add(pft, i, &i, 1)) {
printf("Failed to insert.\n");
- pft_destroy(pft);
- free(j);
- return -1;
+ goto fail_pft;
}
}
if (pft_lookup(pft, INT_TEST, &j, &len)) {
printf("Failed to lookup.\n");
- pft_destroy(pft);
- return -1;
+ goto fail_pft;
}
if (*j != INT_TEST) {
printf("Lookup returned wrong value (%d != %d).\n",
INT_TEST, *j);
- pft_destroy(pft);
- return -1;
+ goto fail_pft;
}
if (pft_lookup(pft, TBL_SIZE + INT_TEST, &j, &len)) {
- printf("Failed to lookup.\n");
- pft_destroy(pft);
- return -1;
+ printf("Failed to lookup on a shared bucket.\n");
+ goto fail_pft;
}
if (*j != TBL_SIZE + INT_TEST) {
printf("Lookup returned wrong value (%d != %d).\n",
- INT_TEST, *j);
- pft_destroy(pft);
- return -1;
+ TBL_SIZE + INT_TEST, *j);
+ goto fail_pft;
+ }
+
+ pft_destroy(pft);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_pft:
+ pft_destroy(pft);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_pft_delete(void)
+{
+ struct pft * pft;
+ int * j;
+ size_t len;
+ int i;
+
+ TEST_START();
+
+ pft = pft_create(TBL_SIZE);
+ if (pft == NULL) {
+ printf("Failed to create.\n");
+ goto fail;
+ }
+
+ for (i = 0; i < TBL_SIZE + INT_TEST + 2; i++) {
+ if (pft_add(pft, i, &i, 1)) {
+ printf("Failed to insert.\n");
+ goto fail_pft;
+ }
}
if (pft_delete(pft, INT_TEST)) {
printf("Failed to delete.\n");
- pft_destroy(pft);
- return -1;
+ goto fail_pft;
}
if (pft_lookup(pft, INT_TEST, &j, &len) == 0) {
printf("Failed to delete properly.\n");
- pft_destroy(pft);
- return -1;
+ goto fail_pft;
}
if (pft_lookup(pft, TBL_SIZE + INT_TEST, &j, &len)) {
printf("Failed to lookup after deletion.\n");
- pft_destroy(pft);
- return -1;
+ goto fail_pft;
}
if (*j != TBL_SIZE + INT_TEST) {
printf("Lookup returned wrong value (%d != %d).\n",
- INT_TEST, *j);
- pft_destroy(pft);
- return -1;
+ TBL_SIZE + INT_TEST, *j);
+ goto fail_pft;
}
pft_destroy(pft);
- return 0;
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_pft:
+ pft_destroy(pft);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_pft_del_fd_sole(void)
+{
+ struct pft * pft;
+ int * j;
+ size_t len;
+ int fds[] = {FD_GONE};
+
+ TEST_START();
+
+ pft = pft_create(TBL_SIZE);
+ if (pft == NULL) {
+ printf("Failed to create.\n");
+ goto fail;
+ }
+
+ if (pft_add(pft, INT_TEST, fds, 1)) {
+ printf("Failed to insert.\n");
+ goto fail_pft;
+ }
+
+ pft_del_fd(pft, FD_GONE);
+
+ if (pft_lookup(pft, INT_TEST, &j, &len) == 0) {
+ printf("Route without a next hop survived.\n");
+ goto fail_pft;
+ }
+
+ pft_destroy(pft);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_pft:
+ pft_destroy(pft);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_pft_del_fd_shared(void)
+{
+ struct pft * pft;
+ int * j;
+ size_t len;
+ int fds[] = {FD_KEEP, FD_GONE, FD_OTHER};
+
+ TEST_START();
+
+ pft = pft_create(TBL_SIZE);
+ if (pft == NULL) {
+ printf("Failed to create.\n");
+ goto fail;
+ }
+
+ if (pft_add(pft, INT_TEST, fds, 3)) {
+ printf("Failed to insert.\n");
+ goto fail_pft;
+ }
+
+ pft_del_fd(pft, FD_GONE);
+
+ if (pft_lookup(pft, INT_TEST, &j, &len)) {
+ printf("Route with next hops left was dropped.\n");
+ goto fail_pft;
+ }
+
+ if (len != 2) {
+ printf("Expected 2 next hops, got %zu.\n", len);
+ goto fail_pft;
+ }
+
+ if (j[0] != FD_KEEP || j[1] != FD_OTHER) {
+ printf("Next hops not preserved in order (%d, %d).\n",
+ j[0], j[1]);
+ goto fail_pft;
+ }
+
+ pft_destroy(pft);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_pft:
+ pft_destroy(pft);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_pft_del_fd_untouched(void)
+{
+ struct pft * pft;
+ int * j;
+ size_t len;
+ int fds[] = {FD_KEEP};
+
+ TEST_START();
+
+ pft = pft_create(TBL_SIZE);
+ if (pft == NULL) {
+ printf("Failed to create.\n");
+ goto fail;
+ }
+
+ if (pft_add(pft, INT_TEST, fds, 1)) {
+ printf("Failed to insert.\n");
+ goto fail_pft;
+ }
+
+ pft_del_fd(pft, FD_GONE);
+
+ if (pft_lookup(pft, INT_TEST, &j, &len)) {
+ printf("Unrelated route was dropped.\n");
+ goto fail_pft;
+ }
+
+ if (len != 1 || *j != FD_KEEP) {
+ printf("Unrelated route was modified.\n");
+ goto fail_pft;
+ }
+
+ pft_destroy(pft);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail_pft:
+ pft_destroy(pft);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int pft_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_pft_create_destroy();
+ ret |= test_pft_insert_lookup();
+ ret |= test_pft_delete();
+ ret |= test_pft_del_fd_sole();
+ ret |= test_pft_del_fd_shared();
+ ret |= test_pft_del_fd_untouched();
+
+ return ret;
}
diff --git a/src/ipcpd/unicast/psched.c b/src/ipcpd/unicast/psched.c
index 21e23617..dce85120 100644
--- a/src/ipcpd/unicast/psched.c
+++ b/src/ipcpd/unicast/psched.c
@@ -30,6 +30,7 @@
#include <ouroboros/errno.h>
#include <ouroboros/notifier.h>
+#include <ouroboros/time.h>
#include "common/connmgr.h"
#include "ipcp.h"
@@ -50,7 +51,7 @@ static int qos_prio [] = {
#endif
struct psched {
- fset_t * set[QOS_CUBE_MAX];
+ fset_t * set[QOS_CUBE_MAX * IPCP_SCHED_THR_MUL];
next_packet_fn_t callback;
read_fn_t read;
pthread_t readers[QOS_CUBE_MAX * IPCP_SCHED_THR_MUL];
@@ -59,23 +60,162 @@ struct psched {
struct sched_info {
struct psched * sch;
qoscube_t qc;
+ size_t idx;
};
+/* Map an FD to one reader's set: one FD, one thread (no shared FDs). */
+static size_t fd_set_idx(int fd, qoscube_t qc)
+{
+ return qc + ((size_t) fd % IPCP_SCHED_THR_MUL) * QOS_CUBE_MAX;
+}
+
static void cleanup_reader(void * o)
{
fqueue_destroy((fqueue_t *) o);
}
+/*
+ * Per-reader deadline scheduler: a paced flow is served, then deferred
+ * to its next-send deadline instead of blocking the thread, so it never
+ * stalls its thread-mates.
+ */
+struct dsched {
+ uint64_t deadline[PROC_MAX_FLOWS]; /* absolute ns, per tracked fd */
+ int active[PROC_MAX_FLOWS]; /* compact list of tracked fds */
+ int posn[PROC_MAX_FLOWS]; /* fd -> active index, -1 = none */
+ size_t n;
+};
+
+static void cleanup_dsched(void * o)
+{
+ free(o);
+}
+
+static void dsched_track(struct dsched * d,
+ int fd,
+ uint64_t deadline)
+{
+ if (d->posn[fd] >= 0)
+ return;
+
+ d->deadline[fd] = deadline;
+ d->posn[fd] = (int) d->n;
+ d->active[d->n++] = fd;
+}
+
+static void dsched_untrack(struct dsched * d,
+ int fd)
+{
+ int i = d->posn[fd];
+
+ if (i < 0)
+ return;
+
+ d->active[i] = d->active[--d->n];
+ d->posn[d->active[i]] = i;
+ d->posn[fd] = -1;
+}
+
+/* Fold a deadline into the earliest pending one (0 = none yet). */
+static uint64_t dmin_fold(uint64_t dmin,
+ uint64_t deadline)
+{
+ if (dmin == 0 || deadline < dmin)
+ return deadline;
+
+ return dmin;
+}
+
+static uint64_t dsched_serve(struct dsched * d,
+ struct psched * sched,
+ qoscube_t qc,
+ uint64_t now)
+{
+ struct ssm_pk_buff * spb;
+ uint64_t dmin = 0;
+ size_t i;
+ int fd;
+ int ret;
+ time_t wait;
+ bool served;
+
+ /* Round-robin one packet per flow so none monopolises egress. */
+ do {
+ served = false;
+
+ for (i = 0; i < d->n; ) {
+ fd = d->active[i];
+
+ if (d->deadline[fd] > now) {
+ dmin = dmin_fold(dmin, d->deadline[fd]);
+ ++i;
+ continue;
+ }
+
+ ret = sched->read(fd, &spb);
+ if (ret == -EAGAIN) { /* empty now, keep it */
+ ++i;
+ continue;
+ }
+
+ if (ret < 0) {
+ dsched_untrack(d, fd);
+ continue;
+ }
+
+ wait = sched->callback(fd, qc, spb);
+ served = true;
+
+ if (wait > 0) {
+ d->deadline[fd] = now + (uint64_t) wait;
+ dmin = dmin_fold(dmin, d->deadline[fd]);
+ }
+
+ ++i;
+ }
+ } while (served);
+
+ return dmin;
+}
+
+static void dsched_events(struct dsched * d,
+ fqueue_t * fq,
+ uint64_t now)
+{
+ int fd;
+
+ while ((fd = fqueue_next(fq)) >= 0) {
+ switch (fqueue_type(fq)) {
+ case FLOW_DEALLOC:
+ dsched_untrack(d, fd);
+ notifier_event(NOTIFY_DT_FLOW_DEALLOC, &fd);
+ break;
+ case FLOW_DOWN:
+ notifier_event(NOTIFY_DT_FLOW_DOWN, &fd);
+ break;
+ case FLOW_UP:
+ notifier_event(NOTIFY_DT_FLOW_UP, &fd);
+ break;
+ case FLOW_PKT:
+ dsched_track(d, fd, now);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
static void * packet_reader(void * o)
{
- struct psched * sched;
- struct ssm_pk_buff * spb;
- int fd;
- fqueue_t * fq;
- qoscube_t qc;
+ struct psched * sched;
+ struct dsched * d;
+ fqueue_t * fq;
+ qoscube_t qc;
+ size_t idx;
sched = ((struct sched_info *) o)->sch;
qc = ((struct sched_info *) o)->qc;
+ idx = ((struct sched_info *) o)->idx;
ipcp_lock_to_core();
@@ -85,37 +225,51 @@ static void * packet_reader(void * o)
if (fq == NULL)
return (void *) -1;
+ d = malloc(sizeof(*d));
+ if (d == NULL) {
+ fqueue_destroy(fq);
+ return (void *) -1;
+ }
+
+ memset(d, 0, sizeof(*d));
+ memset(d->posn, 0xFF, sizeof(d->posn)); /* -1: nothing tracked yet */
+
+ pthread_cleanup_push(cleanup_dsched, d);
pthread_cleanup_push(cleanup_reader, fq);
while (true) {
- int ret = fevent(sched->set[qc], fq, NULL);
+ struct timespec now_ts;
+ struct timespec to;
+ struct timespec * timeo;
+ uint64_t now;
+ uint64_t dmin;
+ uint64_t delta;
+ int ret;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now_ts);
+
+ now = TS_TO_UINT64(now_ts);
+
+ dmin = dsched_serve(d, sched, qc, now);
+
+ if (dmin == 0) {
+ timeo = NULL;
+ } else {
+ delta = dmin > now ? dmin - now : 1;
+ to.tv_sec = (time_t) (delta / BILLION);
+ to.tv_nsec = (long) (delta % BILLION);
+ timeo = &to;
+ }
+
+ ret = fevent(sched->set[idx], fq, timeo);
if (ret < 0)
continue;
- while ((fd = fqueue_next(fq)) >= 0) {
- switch (fqueue_type(fq)) {
- case FLOW_DEALLOC:
- notifier_event(NOTIFY_DT_FLOW_DEALLOC, &fd);
- break;
- case FLOW_DOWN:
- notifier_event(NOTIFY_DT_FLOW_DOWN, &fd);
- break;
- case FLOW_UP:
- notifier_event(NOTIFY_DT_FLOW_UP, &fd);
- break;
- case FLOW_PKT:
- if (sched->read(fd, &spb) < 0)
- continue;
-
- sched->callback(fd, qc, spb);
- break;
- default:
- break;
- }
- }
+ dsched_events(d, fq, now);
}
pthread_cleanup_pop(true);
+ pthread_cleanup_pop(true);
return (void *) 0;
}
@@ -137,7 +291,7 @@ struct psched * psched_create(next_packet_fn_t callback,
psched->callback = callback;
psched->read = read;
- for (i = 0; i < QOS_CUBE_MAX; ++i) {
+ for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) {
psched->set[i] = fset_create();
if (psched->set[i] == NULL) {
for (j = 0; j < i; ++j)
@@ -155,6 +309,7 @@ struct psched * psched_create(next_packet_fn_t callback,
}
infos[i]->sch = psched;
infos[i]->qc = i % QOS_CUBE_MAX;
+ infos[i]->idx = i;
}
for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i) {
@@ -196,11 +351,12 @@ struct psched * psched_create(next_packet_fn_t callback,
fail_sched:
for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j)
pthread_cancel(psched->readers[j]);
+
for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j)
pthread_join(psched->readers[j], NULL);
#endif
fail_infos:
- for (j = 0; j < QOS_CUBE_MAX; ++j)
+ for (j = 0; j < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++j)
fset_destroy(psched->set[j]);
fail_flow_set:
free(psched);
@@ -219,7 +375,7 @@ void psched_destroy(struct psched * psched)
pthread_join(psched->readers[i], NULL);
}
- for (i = 0; i < QOS_CUBE_MAX; ++i)
+ for (i = 0; i < QOS_CUBE_MAX * IPCP_SCHED_THR_MUL; ++i)
fset_destroy(psched->set[i]);
free(psched);
@@ -233,7 +389,7 @@ void psched_add(struct psched * psched,
assert(psched);
ipcp_flow_get_qoscube(fd, &qc);
- fset_add(psched->set[qc], fd);
+ fset_add(psched->set[fd_set_idx(fd, qc)], fd);
}
void psched_del(struct psched * psched,
@@ -244,5 +400,5 @@ void psched_del(struct psched * psched,
assert(psched);
ipcp_flow_get_qoscube(fd, &qc);
- fset_del(psched->set[qc], fd);
+ fset_del(psched->set[fd_set_idx(fd, qc)], fd);
}
diff --git a/src/ipcpd/unicast/psched.h b/src/ipcpd/unicast/psched.h
index d83bb793..8c2914b3 100644
--- a/src/ipcpd/unicast/psched.h
+++ b/src/ipcpd/unicast/psched.h
@@ -26,9 +26,9 @@
#include <ouroboros/ipcp-dev.h>
#include <ouroboros/fqueue.h>
-typedef void (* next_packet_fn_t)(int fd,
- qoscube_t qc,
- struct ssm_pk_buff * spb);
+typedef time_t (* next_packet_fn_t)(int fd,
+ qoscube_t qc,
+ struct ssm_pk_buff * spb);
typedef int (* read_fn_t)(int fd,
struct ssm_pk_buff ** spb);
diff --git a/src/ipcpd/unicast/routing/graph.c b/src/ipcpd/unicast/routing/graph.c
index 0226c762..c168eb7d 100644
--- a/src/ipcpd/unicast/routing/graph.c
+++ b/src/ipcpd/unicast/routing/graph.c
@@ -603,9 +603,9 @@ static int graph_routing_table_lfa(struct graph * graph,
struct list_head * table,
int ** dist)
{
- int * n_dist[PROG_MAX_FLOWS];
- uint64_t addrs[PROG_MAX_FLOWS];
- int n_index[PROG_MAX_FLOWS];
+ int * n_dist[PROC_MAX_FLOWS];
+ uint64_t addrs[PROC_MAX_FLOWS];
+ int n_index[PROC_MAX_FLOWS];
struct list_head * p;
struct list_head * q;
struct vertex * v;
@@ -618,7 +618,7 @@ static int graph_routing_table_lfa(struct graph * graph,
if (graph_routing_table_simple(graph, s_addr, table, dist))
goto fail_table;
- for (j = 0; j < PROG_MAX_FLOWS; j++) {
+ for (j = 0; j < PROC_MAX_FLOWS; j++) {
n_dist[j] = NULL;
n_index[j] = -1;
addrs[j] = -1;
diff --git a/src/ipcpd/unicast/routing/link-state.c b/src/ipcpd/unicast/routing/link-state.c
index 051dd98d..4fba2f05 100644
--- a/src/ipcpd/unicast/routing/link-state.c
+++ b/src/ipcpd/unicast/routing/link-state.c
@@ -415,7 +415,7 @@ static void calculate_pff(struct routing_i * instance)
struct list_head table;
struct list_head * p;
struct list_head * q;
- int fds[PROG_MAX_FLOWS];
+ int fds[PROC_MAX_FLOWS];
assert(instance);
@@ -878,9 +878,15 @@ static void handle_event(void * self,
break;
case NOTIFY_DT_CONN_UP:
flow_event(c->flow_info.fd, true);
+
+ if (lsdb_add_link(ls.addr, c->conn_info.addr, 0, &qs) < 0)
+ log_dbg("Failed to re-add adjacency to lsdb.");
break;
case NOTIFY_DT_CONN_DOWN:
flow_event(c->flow_info.fd, false);
+
+ if (lsdb_del_link(ls.addr, c->conn_info.addr) < 0)
+ log_dbg("Local link was not in lsdb.");
break;
case NOTIFY_MGMT_CONN_ADD:
fccntl(c->flow_info.fd, FLOWGFLAGS, &flags);
diff --git a/src/ipcpd/unicast/tests/CMakeLists.txt b/src/ipcpd/unicast/tests/CMakeLists.txt
new file mode 100644
index 00000000..2e35ed66
--- /dev/null
+++ b/src/ipcpd/unicast/tests/CMakeLists.txt
@@ -0,0 +1,34 @@
+get_filename_component(CURRENT_SOURCE_PARENT_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(CURRENT_BINARY_PARENT_DIR
+ ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
+
+get_filename_component(PARENT_PATH ${CMAKE_CURRENT_SOURCE_DIR} DIRECTORY)
+get_filename_component(PARENT_DIR ${PARENT_PATH} NAME)
+
+compute_test_prefix()
+
+create_test_sourcelist(${PARENT_DIR}_tests test_suite.c
+ # Add new tests here
+ cap_test.c
+ )
+
+add_executable(${PARENT_DIR}_test ${${PARENT_DIR}_tests})
+
+target_include_directories(${PARENT_DIR}_test PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CURRENT_SOURCE_PARENT_DIR}
+ ${CURRENT_BINARY_PARENT_DIR}
+ ${CMAKE_SOURCE_DIR}/include
+ ${CMAKE_BINARY_DIR}/include
+ ${CMAKE_SOURCE_DIR}/src/ipcpd
+ ${CMAKE_BINARY_DIR}/src/ipcpd
+)
+
+disable_test_logging_for_target(${PARENT_DIR}_test)
+target_link_libraries(${PARENT_DIR}_test PRIVATE ouroboros-common)
+
+add_dependencies(build_tests ${PARENT_DIR}_test)
+
+ouroboros_register_tests(TARGET ${PARENT_DIR}_test TESTS ${${PARENT_DIR}_tests})
diff --git a/src/ipcpd/unicast/tests/cap_test.c b/src/ipcpd/unicast/tests/cap_test.c
new file mode 100644
index 00000000..e3c3f8b3
--- /dev/null
+++ b/src/ipcpd/unicast/tests/cap_test.c
@@ -0,0 +1,177 @@
+/*
+ * Ouroboros - Copyright (C) 2016 - 2026
+ *
+ * Unit tests for link capacity codes
+ *
+ * Dimitri Staessens <dimitri@ouroboros.rocks>
+ * Sander Vrijders <sander@ouroboros.rocks>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., http://www.fsf.org/about/contact/.
+ */
+
+#include "cap.c"
+
+#include <test/test.h>
+
+/* Exact roundtrip holds for codes >= 32 (rates >= 256 B/s). */
+static int test_cap_codec_roundtrip(void)
+{
+ unsigned c;
+
+ TEST_START();
+
+ for (c = 32; c <= 255; c++) {
+ if (cap_enc(cap_dec((uint8_t) c)) != c) {
+ printf("Code %u does not roundtrip.\n", c);
+ goto fail;
+ }
+
+ if (cap_dec((uint8_t) c) <= cap_dec((uint8_t) (c - 1))) {
+ printf("Decode not monotone at %u.\n", c);
+ goto fail;
+ }
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_codec_bounds(void)
+{
+ TEST_START();
+
+ if (cap_enc(0) != 0 || cap_dec(0) != 0) {
+ printf("Zero is not unknown.\n");
+ goto fail;
+ }
+
+ if (cap_enc(1) != 1) {
+ printf("Rate 1 encoded as %u.\n", cap_enc(1));
+ goto fail;
+ }
+
+ if (cap_enc(UINT64_MAX) != 255) {
+ printf("Max rate encoded as %u.\n", cap_enc(UINT64_MAX));
+ goto fail;
+ }
+
+ if (cap_dec(255) <= cap_dec(254)) {
+ printf("Top code does not decode.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_min(void)
+{
+ TEST_START();
+
+ if (cap_min(0, 42) != 42 || cap_min(42, 0) != 42) {
+ printf("Unknown not skipped in min.\n");
+ goto fail;
+ }
+
+ if (cap_min(0, 0) != 0) {
+ printf("Two unknowns not unknown.\n");
+ goto fail;
+ }
+
+ if (cap_min(97, 42) != 42 || cap_min(42, 97) != 42) {
+ printf("Min not taken.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+static int test_cap_stamp(void)
+{
+ uint8_t pci;
+
+ TEST_START();
+
+ pci = 42;
+
+ cap_stamp(&pci, 0);
+
+ if (pci != 42) {
+ printf("Unknown own code overwrote the byte.\n");
+ goto fail;
+ }
+
+ pci = 0;
+
+ cap_stamp(&pci, 97);
+
+ if (pci != 97) {
+ printf("Own code not written into unknown.\n");
+ goto fail;
+ }
+
+ pci = 97;
+
+ cap_stamp(&pci, 42);
+
+ if (pci != 42) {
+ printf("Lower own code did not lower the byte.\n");
+ goto fail;
+ }
+
+ pci = 42;
+
+ cap_stamp(&pci, 97);
+
+ if (pci != 42) {
+ printf("Higher own code raised the byte.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+int cap_test(int argc,
+ char ** argv)
+{
+ int ret = 0;
+
+ (void) argc;
+ (void) argv;
+
+ ret |= test_cap_codec_roundtrip();
+ ret |= test_cap_codec_bounds();
+ ret |= test_cap_min();
+ ret |= test_cap_stamp();
+
+ return ret;
+}