Skip to content

Commit d7310be

Browse files
Abel Milashclaude
andcommitted
Expand concurrency_benchmark.py docstring with per-test descriptions
Each test now has a brief explanation of what it runs, what property it validates, and what a failure would indicate. Also clarifies that speedup measures async-sequential vs async-concurrent, not async vs sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 51e653f commit d7310be

1 file changed

Lines changed: 53 additions & 15 deletions

File tree

examples/aio/advanced/concurrency_benchmark.py

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,65 @@
44
"""
55
Async concurrency benchmark and validation for the Dataverse Python SDK.
66
7-
Validates seven properties of the async client end-to-end:
8-
9-
1. Non-blocking reads — GET calls do not freeze the event loop.
10-
2. Read throughput — N reads via asyncio.gather() beat sequential.
11-
3. Write concurrency — N parallel creates beat sequential; POST path.
12-
4. Pagination non-blocking— async generator yields between pages (canary
13-
keeps ticking between page fetches).
14-
5. Mixed fan-out — different operation types run simultaneously.
15-
6. Error resilience — one failing call in gather() does not kill
16-
the others (return_exceptions=True pattern).
17-
7. Real-world fan-out — metadata for multiple tables in parallel.
7+
Measures async-sequential vs async-concurrent performance (not async vs sync).
8+
Speedup = time for N sequential awaits / time for N concurrent gather() calls.
9+
10+
Tests
11+
-----
12+
1. Non-blocking reads (canary)
13+
A background task ticks every 10 ms while each GET call runs. Measures the
14+
max gap between ticks. A blocking call (e.g. requests instead of aiohttp,
15+
or time.sleep instead of asyncio.sleep) would starve the canary and produce
16+
a gap equal to the full round-trip. Covers: records.list, tables.list,
17+
tables.get, query.sql, query.fetchxml, query.builder.
18+
19+
2. Read throughput (sequential vs concurrent)
20+
Runs N reads sequentially then N reads with asyncio.gather(). Confirms the
21+
HTTP GET path actually parallelizes. An internal lock or misplaced await
22+
would collapse the speedup to ~1x. Covers: records.list, query.sql,
23+
tables.get.
24+
25+
3. Write concurrency (POST path)
26+
Same as Test 2 but for records.create() (POST). The POST path uses a
27+
different timeout branch (120 s vs 10 s for GET) and different server
28+
behavior. A separate test ensures writes are also truly concurrent, not
29+
just reads. Creates N records in parallel then cleans them up.
30+
31+
4. Pagination non-blocking (async generator canary)
32+
Runs list_pages(), fetchxml().execute_pages(), and builder().execute_pages()
33+
while the canary ticks. Verifies the async generator yields control back to
34+
the event loop between page fetches. A generator that does not await
35+
properly between pages would block other tasks during multi-page queries.
36+
37+
5. Mixed fan-out (cross-operation concurrency)
38+
Fires 6 different operation types simultaneously in one gather(): records.list,
39+
tables.get (x2), query.sql, query.fetchxml, query.builder. A shared internal
40+
resource (metadata cache lock, single connection) could accidentally serialize
41+
different operation types even if same-type parallelism works fine. This test
42+
catches cross-operation serialization.
43+
44+
6. Error resilience
45+
Fires 5 calls — 3 good, 2 intentionally bad — using gather(return_exceptions=True).
46+
Verifies the 3 good calls complete and return results despite the 2 failures.
47+
Without return_exceptions=True, one exception cancels all in-flight coroutines.
48+
Validates the correct usage pattern and confirms the SDK does not suppress
49+
exceptions in a way that would break this pattern.
50+
51+
7. Real-world metadata fan-out
52+
Fetches schema info for 6 tables sequentially then in parallel. The most
53+
common real-world async use case: an application needs metadata for several
54+
tables at startup. Demonstrates the pattern works end-to-end with real results.
1855
1956
How to interpret results
2057
------------------------
58+
- Speedup: async-sequential vs async-concurrent, not async vs sync.
59+
Expect 3-15x on WAN. Low speedup (<2x) suggests server throttling
60+
or accidental serialization in the SDK.
2161
- Max tick gap (canary tests): Windows timer resolution is ~15 ms, so gaps
2262
up to ~30 ms are normal. Gaps > 200 ms indicate a blocking call.
23-
- Speedup: depends on network latency. Expect 3-15x on WAN. Very low
24-
speedup (<2x) suggests server throttling or accidental serialization.
2563
26-
Tip: run with PYTHONASYNCIODEBUG=1 for the asyncio debug mode which logs
27-
a warning whenever a coroutine holds the event loop for more than 100 ms.
64+
Tip: run with PYTHONASYNCIODEBUG=1 to log a warning whenever a coroutine
65+
holds the event loop for more than 100 ms.
2866
2967
Requirements:
3068
pip install PowerPlatform-Dataverse-Client[async] azure-identity

0 commit comments

Comments
 (0)