return NULL;
}
+ /* The selected proxy upstream is handed off to new_common but
+ * never tracked through the request lifecycle here; retire the
+ * inflight counter at connect-success time so P2C scoring stays
+ * accurate. Wiring per-request success/failure is left for a
+ * follow-up. */
+ rspamd_upstream_release(up);
+
return rspamd_http_connection_new_common(ctx, fd, body_handler,
error_handler, finish_handler, opts,
RSPAMD_HTTP_CLIENT,
RSPAMD_UPSTREAM_UNLOCK(upstream);
}
+void rspamd_upstream_release(struct upstream *up)
+{
+ if (up == NULL) {
+ return;
+ }
+
+ RSPAMD_UPSTREAM_LOCK(up);
+ /* Pair with the increment in rspamd_upstream_get_common /
+ * rspamd_upstream_get_token_bucket without disturbing error or
+ * latency state. */
+ if (up->inflight > 0) {
+ up->inflight--;
+ }
+ RSPAMD_UPSTREAM_UNLOCK(up);
+}
+
void rspamd_upstream_set_weight(struct upstream *up, unsigned int weight)
{
RSPAMD_UPSTREAM_LOCK(up);
*/
void rspamd_upstream_ok(struct upstream *up);
+/**
+ * Retire an upstream selection without affecting error counters or latency.
+ * Use this when neither success nor failure semantics apply: message-copy
+ * failures after a successful selection, fire-and-forget address lookups,
+ * or hand-off paths where success/failure is signalled by a different
+ * layer. Decrements the inflight counter so P2C load comparisons stay
+ * accurate; otherwise abandoned selections would skew selection forever.
+ */
+void rspamd_upstream_release(struct upstream *up);
+
/**
* Set weight for an upstream
* @param up
return 2;
}
addr = rspamd_upstream_addr_next(selected);
+ /* Fire-and-forget ping: the session below tracks the address
+ * directly, not the upstream, so retire the inflight counter
+ * immediately rather than leaking it. */
+ rspamd_upstream_release(selected);
}
if (addr != NULL) {
if (err) {
g_error_free(err);
}
+ /* Selection happened but no request will be sent: retire the
+ * inflight counter so P2C scoring isn't skewed by abandoned
+ * picks. */
+ rspamd_upstream_release(bk_conn->up);
continue;
}
g_error_free(err);
}
+ /* Selection succeeded but no request will be sent: retire the
+ * inflight counter so P2C scoring isn't skewed by abandoned
+ * picks. */
+ rspamd_upstream_release(session->master_conn->up);
goto err; /* No fallback here */
}
}
}
+ TEST_CASE("release retires inflight without affecting selection bias")
+ {
+ /*
+ * release() must decrement inflight just like ok()/fail() do, so
+ * abandoned selections (e.g. message-copy failures, fire-and-forget
+ * lookups) don't permanently skew the P2C comparator. We verify by
+ * leaking via release on one upstream and checking that selection
+ * stays balanced — unlike the "loaded upstream" test where leaking
+ * with no retirement skews selection away from it.
+ */
+ p2c_test_ctx t(3);
+ std::map<std::string, int> hits;
+
+ /* Burn many get/release pairs on whatever P2C picks first. If
+ * release didn't retire inflight, that upstream would build up
+ * a load score and stop being picked. */
+ for (int i = 0; i < 100; i++) {
+ auto *up = rspamd_upstream_get(t.ups, RSPAMD_UPSTREAM_P2C, nullptr, 0);
+ REQUIRE(up != nullptr);
+ rspamd_upstream_release(up);
+ }
+
+ for (int i = 0; i < 1500; i++) {
+ auto *up = rspamd_upstream_get(t.ups, RSPAMD_UPSTREAM_P2C, nullptr, 0);
+ REQUIRE(up != nullptr);
+ hits[rspamd_upstream_name(up)]++;
+ rspamd_upstream_ok(up);
+ }
+
+ CHECK(hits.size() == 3);
+ for (const auto &[name, count]: hits) {
+ /* Each ~500; ±40% tolerance to absorb P2C noise. */
+ CHECK(count >= 300);
+ }
+ }
+
+ TEST_CASE("release on null is a no-op")
+ {
+ rspamd_upstream_release(nullptr);
+ }
+
TEST_CASE("get/fail rounds keep inflight bounded")
{
/*