From 384e14246dc7383be854cd60fcf3a70db1d215aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:12:15 +0000 Subject: [PATCH 1/7] Initial plan From a2bac985ec9da22abf2baae4d89d6745c0397ffc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:15:21 +0000 Subject: [PATCH 2/7] Enhance profile setup with cached/creating indicators and parallelization - Rename AccountMaker to ProfileMaker and all related variables - Add is_cached detection in get_relay_profile method - Update progress messages to show cached vs creating status - Parallelize receiver profile setup using threads - Keep deltachat-rpc API calls unchanged (get_all_accounts, add_account) Co-authored-by: hpk42 <73579+hpk42@users.noreply.github.com> --- cmping.py | 123 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 45 deletions(-) diff --git a/cmping.py b/cmping.py index 852284f..e82e6a1 100644 --- a/cmping.py +++ b/cmping.py @@ -116,7 +116,7 @@ def main(): raise SystemExit(0 if pinger.received == expected_total else 1) -class AccountMaker: +class ProfileMaker: def __init__(self, dc): self.dc = dc self.online = [] @@ -127,36 +127,38 @@ def wait_all_online(self): ac = remaining.pop() ac.wait_for_event(EventType.IMAP_INBOX_IDLE) - def _add_online(self, account): - account.start_io() - self.online.append(account) + def _add_online(self, profile): + profile.start_io() + self.online.append(profile) - def get_relay_account(self, domain): - # Try to find an existing account for this domain/IP - for account in self.dc.get_all_accounts(): - addr = account.get_config("configured_addr") + def get_relay_profile(self, domain): + # Try to find an existing profile for this domain/IP + is_cached = False + for profile in self.dc.get_all_accounts(): + addr = profile.get_config("configured_addr") if addr is not None: # Extract the domain/IP from the configured address addr_domain = addr.split("@")[1] if "@" in addr else None if addr_domain == domain: - if account not in self.online: + if profile not in self.online: + is_cached = True break else: - account = self.dc.add_account() + profile = self.dc.add_account() qr_url = create_qr_url(domain) try: - account.set_config_from_qr(qr_url) + profile.set_config_from_qr(qr_url) except Exception as e: - print(f"✗ Failed to configure account on {domain}: {e}") + print(f"✗ Failed to configure profile on {domain}: {e}") raise try: - self._add_online(account) + self._add_online(profile) except Exception as e: - print(f"✗ Failed to bring account online for {domain}: {e}") + print(f"✗ Failed to bring profile online for {domain}: {e}") raise - return account + return profile, is_cached def perform_ping(args): @@ -164,66 +166,97 @@ def perform_ping(args): print(f"# using accounts_dir at: {accounts_dir}") with Rpc(accounts_dir=accounts_dir) as rpc: dc = DeltaChat(rpc) - maker = AccountMaker(dc) + maker = ProfileMaker(dc) - # Calculate total accounts needed - total_accounts = 1 + args.numrecipients - accounts_created = 0 + # Calculate total profiles needed + total_profiles = 1 + args.numrecipients + profiles_setup = 0 + profiles_cached = 0 + profiles_created = 0 - # Create sender account with progress + # Create sender profile with progress print( - f"# Setting up accounts: {accounts_created}/{total_accounts}", + f"# Setting up profiles: {profiles_setup}/{total_profiles} (cached: {profiles_cached}, creating: {profiles_created})", end="", flush=True, ) try: - sender = maker.get_relay_account(args.relay1) - accounts_created += 1 + sender, is_cached = maker.get_relay_profile(args.relay1) + profiles_setup += 1 + if is_cached: + profiles_cached += 1 + else: + profiles_created += 1 print( - f"\r# Setting up accounts: {accounts_created}/{total_accounts}", + f"\r# Setting up profiles: {profiles_setup}/{total_profiles} (cached: {profiles_cached}, creating: {profiles_created})", end="", flush=True, ) except Exception as e: - print(f"\r✗ Failed to setup sender account on {args.relay1}: {e}") + print(f"\r✗ Failed to setup sender profile on {args.relay1}: {e}") sys.exit(1) - # Create receiver accounts with progress + # Create receiver profiles with progress - parallelize fresh profile creation receivers = [] - for i in range(args.numrecipients): + receiver_errors = [] + receiver_lock = threading.Lock() + + def setup_receiver_profile(i): + """Setup a single receiver profile""" try: - receiver = maker.get_relay_account(args.relay2) - receivers.append(receiver) - accounts_created += 1 - print( - f"\r# Setting up accounts: {accounts_created}/{total_accounts}", - end="", - flush=True, - ) + receiver, is_cached = maker.get_relay_profile(args.relay2) + with receiver_lock: + receivers.append(receiver) + nonlocal profiles_setup, profiles_cached, profiles_created + profiles_setup += 1 + if is_cached: + profiles_cached += 1 + else: + profiles_created += 1 + print( + f"\r# Setting up profiles: {profiles_setup}/{total_profiles} (cached: {profiles_cached}, creating: {profiles_created})", + end="", + flush=True, + ) except Exception as e: - print( - f"\r✗ Failed to setup receiver account {i+1} on {args.relay2}: {e}" - ) - sys.exit(1) + with receiver_lock: + receiver_errors.append((i, e)) + + # Create threads for parallel profile setup + threads = [] + for i in range(args.numrecipients): + t = threading.Thread(target=setup_receiver_profile, args=(i,)) + t.start() + threads.append(t) + + # Wait for all threads to complete + for t in threads: + t.join() + + # Check for errors + if receiver_errors: + for i, e in receiver_errors: + print(f"\r✗ Failed to setup receiver profile {i+1} on {args.relay2}: {e}") + sys.exit(1) - # Account setup complete + # Profile setup complete print( - f"\r# Setting up accounts: {accounts_created}/{total_accounts} - Complete!" + f"\r# Setting up profiles: {profiles_setup}/{total_profiles} (cached: {profiles_cached}, creating: {profiles_created}) - Complete!" ) - # Wait for all accounts to be online with timeout feedback - print("# Waiting for all accounts to be online...", end="", flush=True) + # Wait for all profiles to be online with timeout feedback + print("# Waiting for all profiles to be online...", end="", flush=True) try: maker.wait_all_online() print(" Done!") except Exception as e: - print(f"\n✗ Timeout or error waiting for accounts to be online: {e}") + print(f"\n✗ Timeout or error waiting for profiles to be online: {e}") sys.exit(1) # Create a group chat from sender and add all receivers group = sender.create_group("cmping") for receiver in receivers: - # Create a contact for the receiver account and add to group + # Create a contact for the receiver profile and add to group contact = sender.create_contact(receiver) group.add_contact(contact) From e9b623b91e38675105c8d712252f5c10de46db06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:18:07 +0000 Subject: [PATCH 3/7] Fix threading race conditions and improve parallelization - Add lock to ProfileMaker to prevent race conditions during profile lookup/creation - Move start_io() call outside lock to allow true parallelization - Fix nonlocal declaration placement in setup_receiver_profile - Add comments explaining thread-safety approach Co-authored-by: hpk42 <73579+hpk42@users.noreply.github.com> --- cmping.py | 50 +++++++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/cmping.py b/cmping.py index e82e6a1..3c8baa5 100644 --- a/cmping.py +++ b/cmping.py @@ -120,6 +120,7 @@ class ProfileMaker: def __init__(self, dc): self.dc = dc self.online = [] + self.lock = threading.Lock() def wait_all_online(self): remaining = list(self.online) @@ -129,29 +130,34 @@ def wait_all_online(self): def _add_online(self, profile): profile.start_io() - self.online.append(profile) + with self.lock: + self.online.append(profile) def get_relay_profile(self, domain): - # Try to find an existing profile for this domain/IP - is_cached = False - for profile in self.dc.get_all_accounts(): - addr = profile.get_config("configured_addr") - if addr is not None: - # Extract the domain/IP from the configured address - addr_domain = addr.split("@")[1] if "@" in addr else None - if addr_domain == domain: - if profile not in self.online: - is_cached = True - break - else: - profile = self.dc.add_account() - qr_url = create_qr_url(domain) - try: - profile.set_config_from_qr(qr_url) - except Exception as e: - print(f"✗ Failed to configure profile on {domain}: {e}") - raise + # Thread-safe profile lookup and creation + with self.lock: + # Try to find an existing profile for this domain/IP + is_cached = False + for profile in self.dc.get_all_accounts(): + addr = profile.get_config("configured_addr") + if addr is not None: + # Extract the domain/IP from the configured address + addr_domain = addr.split("@")[1] if "@" in addr else None + if addr_domain == domain: + if profile not in self.online: + is_cached = True + break + else: + profile = self.dc.add_account() + qr_url = create_qr_url(domain) + try: + profile.set_config_from_qr(qr_url) + except Exception as e: + print(f"✗ Failed to configure profile on {domain}: {e}") + raise + # Bring profile online outside the lock to allow parallelization + # start_io() is the time-consuming operation we want to parallelize try: self._add_online(profile) except Exception as e: @@ -203,11 +209,13 @@ def perform_ping(args): def setup_receiver_profile(i): """Setup a single receiver profile""" + nonlocal profiles_setup, profiles_cached, profiles_created try: + # Each thread creates its own profile (cached or fresh) + # The ProfileMaker ensures each call returns a different profile receiver, is_cached = maker.get_relay_profile(args.relay2) with receiver_lock: receivers.append(receiver) - nonlocal profiles_setup, profiles_cached, profiles_created profiles_setup += 1 if is_cached: profiles_cached += 1 From 406f20d67a39a5bfddb3c0ea44e1ffe946dbddac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:19:21 +0000 Subject: [PATCH 4/7] Add clarifying comments for thread-safety and caching logic - Clarify that cached profiles are those found in existing accounts - Explain that multiple profiles per domain are supported (for receivers) - Document why start_io() is called outside the lock (parallelization) - Improve comments in setup_receiver_profile to explain behavior Co-authored-by: hpk42 <73579+hpk42@users.noreply.github.com> --- cmping.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cmping.py b/cmping.py index 3c8baa5..651aa62 100644 --- a/cmping.py +++ b/cmping.py @@ -129,14 +129,18 @@ def wait_all_online(self): ac.wait_for_event(EventType.IMAP_INBOX_IDLE) def _add_online(self, profile): + # Call start_io() outside the lock to allow parallel execution + # This is the time-consuming operation that benefits from parallelization profile.start_io() + # Only lock when modifying the shared state with self.lock: self.online.append(profile) def get_relay_profile(self, domain): # Thread-safe profile lookup and creation with self.lock: - # Try to find an existing profile for this domain/IP + # Try to find an existing (cached) profile for this domain/IP + # that is not already online (to allow multiple profiles per domain) is_cached = False for profile in self.dc.get_all_accounts(): addr = profile.get_config("configured_addr") @@ -145,9 +149,12 @@ def get_relay_profile(self, domain): addr_domain = addr.split("@")[1] if "@" in addr else None if addr_domain == domain: if profile not in self.online: + # Found a cached profile that we can reuse is_cached = True break + # Profile already online, continue looking for another one else: + # No cached profile found, create a fresh one profile = self.dc.add_account() qr_url = create_qr_url(domain) try: @@ -211,8 +218,9 @@ def setup_receiver_profile(i): """Setup a single receiver profile""" nonlocal profiles_setup, profiles_cached, profiles_created try: - # Each thread creates its own profile (cached or fresh) - # The ProfileMaker ensures each call returns a different profile + # Each thread gets a profile for the domain (either cached or fresh) + # If multiple cached profiles exist, different threads may get different ones + # If no cached profiles exist, new ones are created receiver, is_cached = maker.get_relay_profile(args.relay2) with receiver_lock: receivers.append(receiver) From 6f789d0f422e88cdb78c29c8dba553fda18d3745 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:48:13 +0000 Subject: [PATCH 5/7] Add progress bar for profiles coming online and show error events - Display precise count: "Waiting for N profile(s) to come online: X/N" - Show ERROR events during profile setup with detailed messages - Show WARNING events during profile setup - Update progress counter in real-time as profiles come online - Remove redundant "Waiting for all profiles" generic message Co-authored-by: hpk42 <73579+hpk42@users.noreply.github.com> --- cmping.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/cmping.py b/cmping.py index 651aa62..235daec 100644 --- a/cmping.py +++ b/cmping.py @@ -123,10 +123,26 @@ def __init__(self, dc): self.lock = threading.Lock() def wait_all_online(self): + total = len(self.online) + online_count = 0 + print(f"# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + remaining = list(self.online) while remaining: - ac = remaining.pop() - ac.wait_for_event(EventType.IMAP_INBOX_IDLE) + profile = remaining.pop() + while True: + event = profile.wait_for_event() + if event.kind == EventType.IMAP_INBOX_IDLE: + online_count += 1 + print(f"\r# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + break + elif event.kind == EventType.ERROR: + print(f"\n✗ ERROR during profile setup: {event.msg}") + elif event.kind == EventType.WARNING: + print(f"\n⚠ WARNING during profile setup: {event.msg}") + print(f"# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + + print() # Final newline def _add_online(self, profile): # Call start_io() outside the lock to allow parallel execution @@ -260,11 +276,9 @@ def setup_receiver_profile(i): f"\r# Setting up profiles: {profiles_setup}/{total_profiles} (cached: {profiles_cached}, creating: {profiles_created}) - Complete!" ) - # Wait for all profiles to be online with timeout feedback - print("# Waiting for all profiles to be online...", end="", flush=True) + # Wait for all profiles to come online with progress try: maker.wait_all_online() - print(" Done!") except Exception as e: print(f"\n✗ Timeout or error waiting for profiles to be online: {e}") sys.exit(1) From 1cca82713cfefff57d31b9fc915bd3fad2e49429 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:50:30 +0000 Subject: [PATCH 6/7] Remove unnecessary lock from _add_online, improve comments - Remove lock from _add_online: Python list.append() is atomic - Keep lock in get_relay_profile: prevents race condition where multiple threads select same cached profile - Add detailed comments explaining why lock is needed in get_relay_profile - Clarify that deltachat-rpc-server handles all API calls thread-safely - Extract print_progress helper to reduce code duplication Co-authored-by: hpk42 <73579+hpk42@users.noreply.github.com> --- cmping.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cmping.py b/cmping.py index 235daec..e169760 100644 --- a/cmping.py +++ b/cmping.py @@ -125,35 +125,44 @@ def __init__(self, dc): def wait_all_online(self): total = len(self.online) online_count = 0 - print(f"# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + + def print_progress(): + print(f"\r# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + + print_progress() remaining = list(self.online) while remaining: profile = remaining.pop() + # Wait for events until profile comes online + # Timeout is handled by caller via try/except while True: event = profile.wait_for_event() if event.kind == EventType.IMAP_INBOX_IDLE: online_count += 1 - print(f"\r# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + print_progress() break elif event.kind == EventType.ERROR: + # Log error but continue - profile setup may still succeed print(f"\n✗ ERROR during profile setup: {event.msg}") + print_progress() elif event.kind == EventType.WARNING: print(f"\n⚠ WARNING during profile setup: {event.msg}") - print(f"# Waiting for {total} profile(s) to come online: {online_count}/{total}", end="", flush=True) + print_progress() print() # Final newline def _add_online(self, profile): - # Call start_io() outside the lock to allow parallel execution + # Call start_io() to bring profile online # This is the time-consuming operation that benefits from parallelization profile.start_io() - # Only lock when modifying the shared state - with self.lock: - self.online.append(profile) + # Python list.append() is atomic, no lock needed + self.online.append(profile) def get_relay_profile(self, domain): - # Thread-safe profile lookup and creation + # Lock needed to prevent multiple threads from selecting the same cached profile + # The check-then-act pattern (check if profile in online, then use it) requires atomicity + # All RPC calls (get_all_accounts, add_account, set_config_from_qr) are thread-safe on server side with self.lock: # Try to find an existing (cached) profile for this domain/IP # that is not already online (to allow multiple profiles per domain) From fcd14876a0d9d4edbfbbea45d46dafb5797e0d45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 14:51:17 +0000 Subject: [PATCH 7/7] Add clarifying comments about thread safety and timeouts - Clarify that len() and list copy are atomic operations in Python - Document that timeout handling is done at caller level - Explain that list copy avoids concurrent modification issues Co-authored-by: hpk42 <73579+hpk42@users.noreply.github.com> --- cmping.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmping.py b/cmping.py index e169760..69848a7 100644 --- a/cmping.py +++ b/cmping.py @@ -123,6 +123,7 @@ def __init__(self, dc): self.lock = threading.Lock() def wait_all_online(self): + # Read list length once at start (atomic operation in Python) total = len(self.online) online_count = 0 @@ -131,11 +132,12 @@ def print_progress(): print_progress() + # Make a copy to avoid issues with concurrent modifications remaining = list(self.online) while remaining: profile = remaining.pop() # Wait for events until profile comes online - # Timeout is handled by caller via try/except + # Timeout is handled by caller via try/except around wait_all_online() while True: event = profile.wait_for_event() if event.kind == EventType.IMAP_INBOX_IDLE: