You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@community.apache.org by rb...@apache.org on 2026/09/25 20:33:52 UTC
(comdev-metrics) branch rbowen-participants updated: Parallelize per-month participant API calls (max 4 concurrent)
This is an automated email from the ASF dual-hosted git repository.
rbowen pushed a commit to branch rbowen-participants
in repository https://gitbox.apache.org/repos/asf/comdev-metrics.git
The following commit(s) were added to refs/heads/rbowen-participants by this push:
new 118a547 Parallelize per-month participant API calls (max 4 concurrent)
118a547 is described below
commit 118a54707ffc10c577628df1755b7ab80e6283dc
Author: Rich Bowen <rb...@rcbowen.com>
AuthorDate: Fri Sep 25 16:33:34 2026 -0400
Parallelize per-month participant API calls (max 4 concurrent)
Run the per-month numparts requests for high-volume months
concurrently via a bounded thread pool instead of serially.
Capped at 4 in-flight requests to stay a polite consumer of the
shared Pony Mail API. Roughly halves cold-refresh time on
high-volume projects with no change to results.
- Bounded ThreadPoolExecutor (max 4 workers) in the hybrid
participant counter; small months still counted client-side
- Thread-safe per-month call counter (lock-guarded)
- No change to counts, accuracy, or cache format
---
src/asfmetrics/collectors/mailing_lists.py | 39 +++++++++++++++++++++++++-----
1 file changed, 33 insertions(+), 6 deletions(-)
diff --git a/src/asfmetrics/collectors/mailing_lists.py b/src/asfmetrics/collectors/mailing_lists.py
index 8653268..d881973 100644
--- a/src/asfmetrics/collectors/mailing_lists.py
+++ b/src/asfmetrics/collectors/mailing_lists.py
@@ -18,6 +18,8 @@ See: incubator-ponymail-foal/docs/API.md
from datetime import datetime
import time
+import threading
+from concurrent.futures import ThreadPoolExecutor
import re
import httpx
@@ -33,6 +35,12 @@ ACTIVITY_THRESHOLD = "12M" # Full year — trends need context beyond a single
_COLLECTOR_NAME = "mailing_lists"
+# Max concurrent per-month numparts requests to the Pony Mail API. Kept
+# deliberately low (4) to stay a polite consumer of a shared community API
+# and avoid rate-limiting/blacklisting. Do not raise without checking ASF
+# infra tolerance.
+_MAX_CONCURRENCY = 4
+
def _widen_timespan_one_month(timespan: str) -> str:
"""Return a Pony Mail timespan one month wider than ``timespan``.
@@ -141,12 +149,15 @@ def _participants_by_month(emails: list) -> dict[str, int]:
# Module-level counter so the CLI can report how many per-month API calls
# the hybrid strategy actually made on a run (timing/observability).
_MONTH_CALLS = 0
+# Guards _MONTH_CALLS since per-month calls now run on a thread pool.
+_MONTH_CALLS_LOCK = threading.Lock()
def reset_call_counter() -> None:
"""Reset the per-month API call counter (call once at run start)."""
global _MONTH_CALLS
- _MONTH_CALLS = 0
+ with _MONTH_CALLS_LOCK:
+ _MONTH_CALLS = 0
def get_call_counter() -> int:
@@ -175,7 +186,8 @@ def _fetch_month_numparts(
url = f"{base_url}stats.json"
payload = {"list": list_name, "domain": domain, "d": month_key}
try:
- _MONTH_CALLS += 1
+ with _MONTH_CALLS_LOCK:
+ _MONTH_CALLS += 1
resp = httpx.post(url, json=payload, timeout=30)
resp.raise_for_status()
data = resp.json()
@@ -213,16 +225,31 @@ def _participants_by_month_hybrid(
client_counts = _participants_by_month(emails)
result: dict[str, int] = {}
+ api_months: list[str] = []
for month_key, msg_count in active_months.items():
if only_months is not None and month_key not in only_months:
continue
if msg_count > EMAILS_CAP_THRESHOLD:
- # emails[] unreliable for this month → authoritative per-month call
- np = _fetch_month_numparts(list_name, domain, month_key, base_url)
- result[month_key] = np if np is not None else client_counts.get(month_key, 0)
+ # emails[] unreliable for this month → needs an authoritative
+ # per-month call (collected below and run concurrently).
+ api_months.append(month_key)
else:
- # Small month → emails[] is complete; client-side count is exact
+ # Small month → emails[] is complete; client-side count is exact.
result[month_key] = client_counts.get(month_key, 0)
+
+ # Run the per-month numparts calls concurrently, but bounded to
+ # _MAX_CONCURRENCY to stay polite to the shared Pony Mail API. Each
+ # call falls back to the client-side count if it fails.
+ if api_months:
+ def _one(mk: str) -> tuple[str, int]:
+ np = _fetch_month_numparts(list_name, domain, mk, base_url)
+ return mk, (np if np is not None else client_counts.get(mk, 0))
+
+ workers = min(_MAX_CONCURRENCY, len(api_months))
+ with ThreadPoolExecutor(max_workers=workers) as pool:
+ for mk, count in pool.map(_one, api_months):
+ result[mk] = count
+
return result