|
4 | 4 | """ |
5 | 5 | Async concurrency benchmark and validation for the Dataverse Python SDK. |
6 | 6 |
|
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. |
18 | 55 |
|
19 | 56 | How to interpret results |
20 | 57 | ------------------------ |
| 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. |
21 | 61 | - Max tick gap (canary tests): Windows timer resolution is ~15 ms, so gaps |
22 | 62 | 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. |
25 | 63 |
|
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. |
28 | 66 |
|
29 | 67 | Requirements: |
30 | 68 | pip install PowerPlatform-Dataverse-Client[async] azure-identity |
|
0 commit comments