Skip to content

Commit c7fac87

Browse files
author
dalqen-agent
committed
dalqen snapshot: implement attempt-1
1 parent c721b3d commit c7fac87

4 files changed

Lines changed: 217 additions & 0 deletions

File tree

‎context-kg/_meta/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ sources: 0
1111
## Technical
1212

1313
- [[pole-python-monorepo]] — 轻量 Monorepo 与兼容发行边界 | architecture, python, monorepo, packaging
14+
- [[pole-instrument]] — 自动增强产品边界、生命周期与 HTTPX v1 设计 | architecture, python, instrumentation
1415

1516
## Tasks
1617

‎context-kg/technical/adr/pole-python-monorepo.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,4 @@ pre-fork 生命周期和诊断接口。在该决策完成前,核心客户端
5757
## 相关页面
5858

5959
- [[todo]]
60+
- [[pole-instrument]]
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
---
2+
title: pole-instrument Technical Design
3+
tags: [architecture, python, instrumentation]
4+
links: [pole-python-monorepo, todo]
5+
updated: 2026-08-19
6+
sources: 0
7+
---
8+
9+
# pole-instrument Technical Design
10+
11+
## Status and product boundary
12+
13+
Proposed for implementation. Automatic instrumentation ships as the separate PyPI
14+
distribution `pole-instrument-python`, imported as `pole_instrument`, and depends on the
15+
public API of `pole-client-python`. The compatibility client remains unchanged: its wheel
16+
must not contain `sitecustomize.py`, `usercustomize.py`, `.pth` files, framework imports,
17+
or automatic patches.
18+
19+
The console entry point is `pole-instrument`. The instrumentation core has no framework
20+
dependency; adapter dependencies are isolated in extras. Version 1 begins with one
21+
reference adapter, `httpx`, supporting HTTPX 0.27–0.28 sync and async clients. Server
22+
instrumentation, streaming bodies, gRPC, Dubbo, Thrift, and frameworks not listed here
23+
are unsupported in version 1.
24+
25+
## Configuration
26+
27+
Instrumentation is disabled by default. Configuration precedence is programmatic
28+
arguments, then command-line options, then environment variables, then defaults.
29+
`POLE_INSTRUMENT_ENABLED` is a boolean and defaults to false.
30+
`POLE_INSTRUMENT_STRICT` is a boolean and defaults to false. The enabled adapter list
31+
defaults to all installed supported adapters and may be set with
32+
`POLE_INSTRUMENT_ADAPTERS=httpx`. Boolean values are exactly `true`, `false`, `1`, or `0`
33+
after ASCII case folding; adapter names are ASCII lowercase tokens.
34+
35+
Invalid values and unknown adapters produce an `invalid_configuration` diagnostic.
36+
The programmatic API raises `ConfigurationError`; the launcher exits with status 2.
37+
No framework patch or Sidecar session is created after invalid configuration. Strict
38+
mode affects request-time failures only and does not make invalid configuration valid.
39+
40+
## Activation lifecycle
41+
42+
`pole_instrument.activate(config=None)` is explicit and idempotent: repeated calls with
43+
equivalent configuration return the existing `Instrumentation` handle; conflicting
44+
configuration raises `AlreadyActiveError`. Activation after framework import is
45+
supported by patching the documented public HTTPX client send methods. Activation
46+
before import registers a bounded import callback and installs the same patch once.
47+
48+
The handle's `shutdown()` is idempotent. It closes the owned `SidecarSession`, removes
49+
the import callback, and performs unpatch only when the currently installed method is
50+
the wrapper owned by that handle. Existing user wrappers and callbacks are preserved.
51+
The console launcher activates immediately before invoking the target module or command
52+
and always shuts down in `finally`. No session, channel, or worker thread is created at
53+
module import time.
54+
55+
## Outbound HTTPX sequence
56+
57+
The wrapper reads the original request URL before mutation. A DNS host supplies service;
58+
the optional `x-pole-target-namespace` request extension supplies namespace and defaults
59+
to `default`. IP literals, missing hosts, and values rejected by `TargetService` mean
60+
that no target can be derived. The adapter must not guess a target or listener port.
61+
62+
For a valid target, the wrapper obtains one snapshot, reads
63+
`ListenerSnapshot.generation`, calls
64+
`SidecarSession.endpoint(ListenerProtocol.HTTP)`, constructs public
65+
`TargetService(namespace, service)`, and calls `TargetService.to_metadata()` with the
66+
request headers. That public call incorporates `current_traffic_context()` and replaces
67+
forged reserved metadata. The request authority and Host header continue to identify the
68+
original service while the network connection is made to the Sidecar listener. Calls
69+
made by the instrumentation package set a context-local recursion guard.
70+
71+
The adapter maintains a transport pool per snapshot generation. Before acquiring a
72+
connection it compares the current `ListenerSnapshot.generation`; a change closes and
73+
discards the old pool atomically. `SidecarUnavailableError` also invalidates the pool
74+
before fallback. A request that has observed invalidation may not acquire from the old
75+
generation. In-flight requests may complete on the generation they already acquired.
76+
77+
## Failure policy
78+
79+
Default request behavior is fail open: missing socket, initialization timeout, invalid
80+
snapshot, stream loss, unavailable endpoint, target derivation failure, metadata
81+
validation failure, or adapter exception invokes the original HTTPX transport against
82+
the original URL without Pole target headers. Fallback has a bounded deadline and never
83+
waits for Sidecar reconnection. In strict mode the same conditions raise a documented
84+
`InstrumentationUnavailableError` before application I/O. Registration rejection is
85+
not applicable to the version 1 client-only adapter. Diagnostics identify the category
86+
and adapter but never include URL userinfo, target values, headers, baggage, or bodies.
87+
88+
## Patch coexistence and concurrency
89+
90+
Wrappers retain and invoke the method present at activation, so user middleware remains
91+
in the chain exactly once. Ownership markers prevent duplicate and recursive patches.
92+
OpenTelemetry may be installed before or after Pole; Pole changes routing headers only,
93+
does not create spans, and preserves its wrapper so both instrumentations execute once.
94+
Unsupported HTTPX versions remain unpatched and report `unsupported_version`.
95+
96+
`SidecarSession` and sync pools are protected by process-local locks. Async pools belong
97+
to the event loop that created them and are never shared across loops. Activation calls
98+
`os.register_at_fork`: the child clears inherited session, lock, pool, and activation
99+
state without joining parent threads, then lazily creates a new session on its first
100+
instrumented request. Preload servers must activate in a post-fork worker hook when one
101+
is available. Shutdown closes sync resources and schedules async cleanup on the owning
102+
loop; if that event loop is already closed, resources are abandoned with one diagnostic
103+
rather than blocking process exit.
104+
105+
## Diagnostics, security, and compatibility
106+
107+
`pole_instrument.activation_status()` returns an immutable snapshot containing enabled,
108+
strict, installed adapter names, patch errors, Sidecar availability, and generation. It
109+
contains no request data. Structured diagnostics use stable event names, apply a rate
110+
limit of one event per category per minute with a suppressed count, and never record
111+
credentials, target metadata, baggage, arbitrary headers, URL query/userinfo, or request bodies.
112+
Bootstrap connects only to the configured local Unix Domain Socket accepted by
113+
`SidecarSession`; remote bootstrap transports are not added.
114+
115+
Both distributions support Python 3.9–3.13. `pole-client-python` retains its current
116+
exports, dependencies, contracts, and artifact contents. The new distribution depends
117+
on a compatible client range and exposes HTTPX only through `[httpx]`; importing core
118+
with no framework or OpenTelemetry installed succeeds. Release gates inspect both
119+
artifacts and install client-only, instrument-core, and adapter-extra environments.
120+
121+
## Verification plan
122+
123+
- Document-contract tests require every settled decision and this traceability table.
124+
- Activation tests cover disabled import, configuration precedence/errors, idempotency,
125+
before/after-import patching, coexistence, recursion, and owned unpatch.
126+
- A real HTTPX request plus real UDS Sidecar fixture covers endpoint routing, canonical
127+
target/TrafficContext metadata, fail open, strict mode, stream loss, and generation
128+
pool replacement for sync and async clients.
129+
- Lifecycle tests cover threads, two event loops, forked workers, repeated shutdown,
130+
redacted/rate-limited diagnostics, and secret sentinels.
131+
- Compatibility gates run the existing Thin SDK suite unchanged, compile all sources,
132+
build both distributions, inspect artifacts, and smoke-test Python 3.9–3.13.
133+
134+
## Traceability
135+
136+
| Acceptance | Design section | Planned test |
137+
| --- | --- | --- |
138+
| AC1, AC13, AC14 | Product boundary; compatibility | artifact and isolated-import tests |
139+
| AC2, AC3 | Configuration; activation lifecycle | activation/config table tests |
140+
| AC4, AC5, AC6 | Outbound sequence; failure policy | real HTTPX plus UDS tests |
141+
| AC7 | Product boundary | explicit v1 server non-goal assertion |
142+
| AC8 | Product boundary | HTTPX support-matrix tests |
143+
| AC9 | Patch coexistence | user/OpenTelemetry ordering tests |
144+
| AC10 | Concurrency | fork, thread, and event-loop tests |
145+
| AC11, AC12 | Diagnostics and security | status, rate limit, secret-sentinel tests |
146+
| AC15 | Traceability | document-contract test |
147+
148+
## Open decisions
149+
150+
There are no open decisions required for the version 1 HTTPX client milestone. Adding
151+
server adapters or another protocol requires a new design revision, support matrix, and
152+
public-seam E2E evidence before implementation.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from pathlib import Path
2+
import unittest
3+
4+
5+
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
6+
DESIGN_PATH = REPOSITORY_ROOT / "context-kg" / "technical" / "pole-instrument.md"
7+
8+
9+
class PoleInstrumentTechnicalDesignTest(unittest.TestCase):
10+
@classmethod
11+
def design(cls) -> str:
12+
return DESIGN_PATH.read_text(encoding="utf-8")
13+
14+
def test_defines_product_configuration_and_activation_lifecycle(self):
15+
design = self.design()
16+
for decision in (
17+
"pole-instrument-python",
18+
"pole_instrument",
19+
"pole-instrument",
20+
"POLE_INSTRUMENT_ENABLED",
21+
"POLE_INSTRUMENT_STRICT",
22+
"disabled by default",
23+
"idempotent",
24+
"unpatch",
25+
"after framework import",
26+
):
27+
with self.subTest(decision=decision):
28+
self.assertIn(decision, design)
29+
30+
def test_defines_outbound_routing_invalidation_and_failure_policy(self):
31+
design = self.design()
32+
for decision in (
33+
"SidecarSession.endpoint(ListenerProtocol.HTTP)",
34+
"TargetService.to_metadata()",
35+
"current_traffic_context()",
36+
"ListenerSnapshot.generation",
37+
"SidecarUnavailableError",
38+
"fail open",
39+
"strict mode",
40+
"must not guess",
41+
):
42+
with self.subTest(decision=decision):
43+
self.assertIn(decision, design)
44+
45+
def test_defines_coexistence_process_diagnostics_security_and_traceability(self):
46+
design = self.design()
47+
for decision in (
48+
"OpenTelemetry",
49+
"os.register_at_fork",
50+
"event loop",
51+
"activation_status()",
52+
"rate limit",
53+
"request bodies",
54+
"Python 3.9",
55+
"Traceability",
56+
"AC15",
57+
):
58+
with self.subTest(decision=decision):
59+
self.assertIn(decision, design)
60+
61+
62+
if __name__ == "__main__":
63+
unittest.main()

0 commit comments

Comments
 (0)