|
| 1 | +# Copyright (c) Microsoft Corporation. |
| 2 | +# Licensed under the MIT license. |
| 3 | + |
| 4 | +""" |
| 5 | +Demonstrates that ContextVar values are NOT inherited by ThreadPoolExecutor |
| 6 | +worker threads — confirming the correlation ID bug in _dispatch_chunks. |
| 7 | +
|
| 8 | +Run from repo root: |
| 9 | + .conda/python.exe examples/advanced/contextvar_thread_demo.py |
| 10 | +""" |
| 11 | + |
| 12 | +import threading |
| 13 | +from contextvars import ContextVar, copy_context |
| 14 | +from concurrent.futures import ThreadPoolExecutor |
| 15 | + |
| 16 | +# Mirrors the SDK's _CALL_SCOPE_CORRELATION_ID exactly |
| 17 | +CORRELATION_ID: ContextVar[str | None] = ContextVar("CORRELATION_ID", default=None) |
| 18 | + |
| 19 | +def read_correlation_id(label: str) -> str: |
| 20 | + """Read the ContextVar — simulates what _RequestContext.from_request() does.""" |
| 21 | + value = CORRELATION_ID.get() |
| 22 | + print(f" [{label}] thread={threading.current_thread().name:20s} " |
| 23 | + f"correlation_id = {value!r}") |
| 24 | + return value |
| 25 | + |
| 26 | + |
| 27 | +# --------------------------------------------------------------------------- |
| 28 | +# Part 1: WITHOUT fix — plain ThreadPoolExecutor.submit() |
| 29 | +# --------------------------------------------------------------------------- |
| 30 | +print("=" * 60) |
| 31 | +print("PART 1: Plain submit() — no context propagation (current SDK)") |
| 32 | +print("=" * 60) |
| 33 | + |
| 34 | +CORRELATION_ID.set("abc-123-shared-id") |
| 35 | +print(f"\nMain thread sets correlation_id = 'abc-123-shared-id'") |
| 36 | +print(f"Dispatching 3 chunks to worker threads...\n") |
| 37 | + |
| 38 | +with ThreadPoolExecutor(max_workers=3) as pool: |
| 39 | + futures = [pool.submit(read_correlation_id, f"chunk-{i}") for i in range(3)] |
| 40 | + results_before = [f.result() for f in futures] |
| 41 | + |
| 42 | +print(f"\nMain thread still sees: {CORRELATION_ID.get()!r}") |
| 43 | +print(f"Worker results: {results_before}") |
| 44 | +print(f"\n=> All workers got None — correlation ID is LOST in concurrent path.\n") |
| 45 | + |
| 46 | + |
| 47 | +# --------------------------------------------------------------------------- |
| 48 | +# Part 2: WITH fix — copy_context().run() |
| 49 | +# --------------------------------------------------------------------------- |
| 50 | +print("=" * 60) |
| 51 | +print("PART 2: copy_context() — correct propagation (proposed fix)") |
| 52 | +print("=" * 60) |
| 53 | + |
| 54 | +CORRELATION_ID.set("abc-123-shared-id") |
| 55 | +print(f"\nMain thread sets correlation_id = 'abc-123-shared-id'") |
| 56 | +print(f"Dispatching 3 chunks with ctx.run()...\n") |
| 57 | + |
| 58 | +ctx = copy_context() # snapshot the main thread's context |
| 59 | +with ThreadPoolExecutor(max_workers=3) as pool: |
| 60 | + futures = [pool.submit(ctx.run, read_correlation_id, f"chunk-{i}") for i in range(3)] |
| 61 | + results_after = [f.result() for f in futures] |
| 62 | + |
| 63 | +print(f"\nMain thread still sees: {CORRELATION_ID.get()!r}") |
| 64 | +print(f"Worker results: {results_after}") |
| 65 | +print(f"\n=> All workers got 'abc-123-shared-id' — correlation ID is preserved.\n") |
| 66 | + |
| 67 | + |
| 68 | +# --------------------------------------------------------------------------- |
| 69 | +# Part 3: Test the actual SDK _dispatch_chunks with the fix applied |
| 70 | +# --------------------------------------------------------------------------- |
| 71 | +print("=" * 60) |
| 72 | +print("PART 3: Real SDK _dispatch_chunks (with fix applied)") |
| 73 | +print("=" * 60) |
| 74 | + |
| 75 | +from PowerPlatform.Dataverse.data._odata import ( |
| 76 | + _dispatch_chunks, |
| 77 | + _CALL_SCOPE_CORRELATION_ID, |
| 78 | +) |
| 79 | + |
| 80 | +def simulate_chunk_request(chunk): |
| 81 | + """Reads the SDK's real ContextVar — same as _RequestContext.from_request().""" |
| 82 | + corr_id = _CALL_SCOPE_CORRELATION_ID.get() |
| 83 | + print(f" chunk={chunk} x-ms-correlation-id = {corr_id!r} " |
| 84 | + f"(thread={threading.current_thread().name})") |
| 85 | + return corr_id |
| 86 | + |
| 87 | +_CALL_SCOPE_CORRELATION_ID.set("real-sdk-call-uuid-xyz") |
| 88 | +print(f"\nSDK: _call_scope sets correlation_id = 'real-sdk-call-uuid-xyz'") |
| 89 | +print(f"SDK: _dispatch_chunks dispatches 3 chunks with max_workers=3\n") |
| 90 | + |
| 91 | +chunks = ["chunk-A", "chunk-B", "chunk-C"] |
| 92 | +results = _dispatch_chunks(simulate_chunk_request, chunks, max_workers=3) |
| 93 | + |
| 94 | +print(f"\nResults: {results}") |
| 95 | +if all(r == "real-sdk-call-uuid-xyz" for r in results): |
| 96 | + print("=> [PASS] All chunks received the correct correlation ID.") |
| 97 | +else: |
| 98 | + print("=> [FAIL] Some chunks got None — fix not working.") |
0 commit comments