Skip to content

Commit 04aaa9a

Browse files
wu-shengclaude
andcommitted
fix: websockets 13.x uri attribute; test websockets 13.1; fork test over HTTP
- The sans-io ClientProtocol exposes the parsed URI as `wsuri` in websockets 13.x and `uri` in later releases; the new asyncio wrapper now falls back accordingly (it crashed stub creation on 13.x-16.x). websockets 13.1 added to the support matrix on all Python versions, so the new-API path is also exercised on Python 3.10 (websockets 17 requires >= 3.11). - sw_fork_support now reports over the HTTP protocol: forking with a live gRPC channel is subject to upstream at-fork races (grpc/grpc#43055) that can silently drop either side's segments (both directions were observed in CI). HTTP has no at-fork hazard, so the test deterministically validates the complete cross-fork trace again (parent entry/exit -> child entry with CrossProcess ref). The gRPC transport path remains covered by sw_gunicorn. Locally validated: websockets 10.4 (legacy), 13.1 and 17.0.1 (new asyncio path), and the fork case — full dataValidate pass each. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cebad09 commit 04aaa9a

5 files changed

Lines changed: 44 additions & 14 deletions

File tree

docs/en/setup/Plugins.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ or a limitation of SkyWalking auto-instrumentation (welcome to contribute!)
5151
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - NOT SUPPORTED YET; Python >=3.10 - ['1.26', '1.25']; | `sw_urllib3` |
5252
| [urllib3](https://urllib3.readthedocs.io/en/latest/) | Python >=3.12 - ['2.3', '2.0']; | `sw_urllib3_v2` |
5353
| [urllib_request](https://docs.python.org/3/library/urllib.request.html) | Python >=3.7 - ['*']; | `sw_urllib_request` |
54-
| [websockets](https://websockets.readthedocs.io) | Python >=3.11 - ['10.3', '10.4', '17.0.1']; Python >=3.7 - ['10.3', '10.4']; | `sw_websockets` |
54+
| [websockets](https://websockets.readthedocs.io) | Python >=3.11 - ['10.3', '10.4', '13.1', '17.0.1']; Python >=3.7 - ['10.3', '10.4', '13.1']; | `sw_websockets` |
5555
### Notes
5656
- The celery server running with "celery -A ..." should be run with the HTTP protocol
5757
as it uses multiprocessing by default which is not compatible with the gRPC protocol implementation

skywalking/plugins/sw_websockets.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
link_vector = ['https://websockets.readthedocs.io']
2222
support_matrix = {
2323
'websockets': {
24-
'>=3.11': ['10.3', '10.4', '17.0.1'],
25-
'>=3.7': ['10.3', '10.4'] # websockets >= 14 requires Python >= 3.11
24+
'>=3.11': ['10.3', '10.4', '13.1', '17.0.1'],
25+
'>=3.7': ['10.3', '10.4', '13.1'] # websockets >= 14 requires Python >= 3.11
2626
}
2727
}
2828
note = """Both the legacy (websockets.legacy, websockets <= 13) and the new asyncio
@@ -99,7 +99,8 @@ def _install_new_client(ClientConnection): # noqa
9999
_connection_handshake = ClientConnection.handshake
100100

101101
async def _sw_connection_handshake(self, *args, **kwargs):
102-
uri = self.protocol.uri
102+
# the sans-io ClientProtocol renamed the attribute wsuri -> uri over time
103+
uri = getattr(self.protocol, 'uri', None) or self.protocol.wsuri
103104
span = get_context().new_exit_span(op=uri.path or '/', peer=f'{uri.host}:{uri.port}',
104105
component=Component.Websockets)
105106
with span:

tests/plugin/web/sw_fork_support/docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,10 @@ services:
4444
SW_AGENT_NAME: provider
4545
SW_AGENT_LOGGING_LEVEL: INFO
4646
SW_AGENT_EXPERIMENTAL_FORK_SUPPORT: 'true'
47+
# HTTP reporter keeps this test deterministic: forking with a live gRPC channel
48+
# is subject to upstream at-fork races (grpc/grpc#43055) that can silently drop
49+
# either side's segments; the gRPC transport is covered by sw_gunicorn
50+
SW_AGENT_PROTOCOL: http
51+
SW_AGENT_COLLECTOR_BACKEND_SERVICES: collector:12800
4752
networks:
4853
beyond:

tests/plugin/web/sw_fork_support/expected.data.yml

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,38 @@
1515
# limitations under the License.
1616
#
1717

18-
# The forked child's own segment (entry span with a CrossProcess ref) is delivered
19-
# on a best-effort basis: reporting from a child forked while the parent's gRPC
20-
# channel is live can be broken by upstream at-fork races (grpc/grpc#43055), so
21-
# only the deterministic parent segment is asserted (segmentSize: ge 1).
2218
segmentItems:
2319
- serviceName: provider
24-
segmentSize: ge 1
20+
segmentSize: 2
2521
segments:
22+
- segmentId: not null
23+
spans:
24+
- operationName: /users
25+
parentSpanId: -1
26+
spanId: 0
27+
spanLayer: Http
28+
tags:
29+
- key: http.method
30+
value: GET
31+
- key: http.url
32+
value: http://127.0.0.1:9091/users
33+
- key: http.status_code
34+
value: '200'
35+
refs:
36+
- parentEndpoint: /users
37+
networkAddress: '127.0.0.1:9091'
38+
refType: CrossProcess
39+
parentSpanId: 1
40+
parentTraceSegmentId: not null
41+
parentServiceInstance: not null
42+
parentService: provider
43+
traceId: not null
44+
startTime: gt 0
45+
endTime: gt 0
46+
componentId: 7001
47+
spanType: Entry
48+
peer: not null
49+
skipAnalysis: false
2650
- segmentId: not null
2751
spans:
2852
- operationName: /users

tests/plugin/web/sw_fork_support/test_fork_support.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ def prepare():
3131
class TestPlugin(TestPluginBase):
3232
"""
3333
Explicit os.fork() with SW_AGENT_EXPERIMENTAL_FORK_SUPPORT: the parent keeps its
34-
agent and the forked child restarts one as a `-child(pid)` instance.
35-
The child's own segment (entry with a CrossProcess ref) is best-effort — forking
36-
with a live parent gRPC channel is subject to upstream at-fork races
37-
(grpc/grpc#43055) — so only the parent's segment is asserted here; the fully
38-
deterministic cross-process validation lives in the sw_gunicorn test.
34+
agent, the forked child restarts one as a `-child(pid)` instance, and the trace
35+
stays continuous across the fork (parent entry/exit -> child entry with ref).
36+
Runs over the HTTP reporter: forking with a live gRPC channel is subject to
37+
upstream at-fork races (grpc/grpc#43055) that can silently drop either side's
38+
segments; the gRPC transport path is covered deterministically by sw_gunicorn.
3939
"""
4040

4141
@pytest.mark.parametrize('version', ['grpcio>=1.83'])

0 commit comments

Comments
 (0)