sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,1454 @@
1
+ // sf_veritas/_sfnetworkhop.c
2
+ // Ultra-fast network hop capture with non-blocking producers, no drops,
3
+ // and an HTTP/2 multiplexed libcurl-multi sender.
4
+
5
+ #define PY_SSIZE_T_CLEAN
6
+ #include <Python.h>
7
+ #include <pthread.h>
8
+ #include <curl/curl.h>
9
+ #include <stdatomic.h>
10
+ #include <stdint.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <time.h>
14
+ #include <sys/time.h>
15
+ #include "sf_tls.h"
16
+ extern void sf_guard_enter(void);
17
+ extern void sf_guard_leave(void);
18
+ extern int sf_guard_active(void);
19
+
20
+ #ifndef SFN_RING_CAP
21
+ #define SFN_RING_CAP 65536 // power-of-two recommended
22
+ #endif
23
+
24
+ #ifndef SFN_ENDPOINT_CAP
25
+ #define SFN_ENDPOINT_CAP 2048
26
+ #endif
27
+
28
+ #ifndef SFN_MAX_INFLIGHT
29
+ #define SFN_MAX_INFLIGHT 128 // number of concurrent requests over H2
30
+ #endif
31
+
32
+ // ---------- Message (three types: fast work, work with bodies, ready body) ----------
33
+ typedef enum {
34
+ SFN_MSG_WORK, // raw work item (needs JSON building)
35
+ SFN_MSG_WORK_BODIES, // work item with request/response bodies
36
+ SFN_MSG_BODY // pre-built body (legacy path)
37
+ } sfn_msg_type;
38
+
39
+ typedef struct {
40
+ sfn_msg_type type;
41
+ union {
42
+ struct { // type == SFN_MSG_WORK
43
+ char *session_id; // malloced copy
44
+ size_t session_len;
45
+ int endpoint_id;
46
+ } work;
47
+ struct { // type == SFN_MSG_WORK_BODIES
48
+ char *session_id; // malloced copy
49
+ size_t session_len;
50
+ int endpoint_id;
51
+ char *route; // malloced JSON-escaped string or NULL (overrides registered route)
52
+ size_t route_len;
53
+ char *query_params; // malloced JSON-escaped string or NULL
54
+ size_t query_params_len;
55
+ char *req_headers; // malloced JSON-escaped string or NULL
56
+ size_t req_headers_len;
57
+ char *req_body; // malloced JSON-escaped string or NULL
58
+ size_t req_body_len;
59
+ char *resp_headers; // malloced JSON-escaped string or NULL
60
+ size_t resp_headers_len;
61
+ char *resp_body; // malloced JSON-escaped string or NULL
62
+ size_t resp_body_len;
63
+ } work_bodies;
64
+ struct { // type == SFN_MSG_BODY
65
+ char *body;
66
+ size_t len;
67
+ } body;
68
+ } data;
69
+ } sfn_msg_t;
70
+
71
+ // ---------- Ring buffer (bounded, cache-friendly) ----------
72
+ static sfn_msg_t *g_ring = NULL;
73
+ static size_t g_cap = 0;
74
+ static _Atomic size_t g_head = 0; // consumer index
75
+ static _Atomic size_t g_tail = 0; // producer index
76
+
77
+ // REMOVED: spinlock for MPMC push (replaced with lock-free CAS below)
78
+
79
+ // ---------- Overflow (unbounded, no-drop) ----------
80
+ typedef struct sfn_node_t {
81
+ sfn_msg_t msg;
82
+ struct sfn_node_t *next;
83
+ } sfn_node_t;
84
+
85
+ static _Atomic(sfn_node_t*) g_overflow_head = NULL;
86
+
87
+ // ---------- wake/sleep ----------
88
+ static pthread_mutex_t g_cv_mtx = PTHREAD_MUTEX_INITIALIZER;
89
+ static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
90
+ static _Atomic int g_running = 0;
91
+
92
+ // Thread pool for concurrent senders (configurable via SF_NETWORKHOP_SENDER_THREADS)
93
+ #define MAX_SENDER_THREADS 16
94
+ static pthread_t g_sender_threads[MAX_SENDER_THREADS];
95
+ static int g_num_sender_threads = 0;
96
+
97
+ // ---------- libcurl state ----------
98
+ __thread CURLM *g_multi = NULL; // per-thread multi interface (HTTP/2 multiplexing)
99
+ static CURL *g_share_template = NULL; // template to clone options from
100
+ static struct curl_slist *g_hdrs = NULL;
101
+ static _Atomic int g_inflight = 0;
102
+
103
+ // simple easy-handle pool (LIFO)
104
+ typedef struct pool_node_t { CURL *easy; struct pool_node_t *next; } pool_node_t;
105
+ static pool_node_t *g_easy_pool = NULL;
106
+ static pthread_mutex_t g_pool_mtx = PTHREAD_MUTEX_INITIALIZER;
107
+
108
+ // ---------- config ----------
109
+ static char *g_url = NULL;
110
+ static char *g_query_escaped = NULL;
111
+ static char *g_api_key = NULL;
112
+ static char *g_service_uuid = NULL;
113
+ static int g_http2 = 0;
114
+
115
+ // JSON prefix/suffix
116
+ static char *g_json_prefix = NULL;
117
+ static size_t g_json_prefix_len = 0;
118
+ static const char *JSON_SUFFIX = "}}";
119
+
120
+ // ---------- Endpoint registry ----------
121
+ typedef struct {
122
+ char *suffix; // pre-escaped invariant suffix (ends with ,\"timestampMs\":\")
123
+ size_t suffix_len;
124
+ int in_use;
125
+ } endpoint_entry;
126
+
127
+ static endpoint_entry g_endpoints[SFN_ENDPOINT_CAP];
128
+ static _Atomic int g_endpoint_count = 0;
129
+
130
+ // ---------- helpers ----------
131
+ static inline uint64_t now_ms(void) {
132
+ #if defined(CLOCK_REALTIME_COARSE)
133
+ struct timespec ts;
134
+ clock_gettime(CLOCK_REALTIME_COARSE, &ts);
135
+ return ((uint64_t)ts.tv_sec) * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000ULL);
136
+ #else
137
+ struct timeval tv;
138
+ gettimeofday(&tv, NULL);
139
+ return ((uint64_t)tv.tv_sec) * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL);
140
+ #endif
141
+ }
142
+
143
+ static char *str_dup(const char *s) {
144
+ size_t n = strlen(s);
145
+ char *p = (char*)malloc(n + 1);
146
+ if (!p) return NULL;
147
+ memcpy(p, s, n);
148
+ p[n] = 0;
149
+ return p;
150
+ }
151
+
152
+ static char *json_escape(const char *s) {
153
+ const unsigned char *in = (const unsigned char*)s;
154
+ size_t extra = 0;
155
+ for (const unsigned char *p = in; *p; ++p) {
156
+ switch (*p) {
157
+ case '\\': case '"': case '\n': case '\r': case '\t': case '\b': case '\f':
158
+ extra++; break;
159
+ default:
160
+ if (*p < 0x20) extra += 5; // \u00XX for other control chars
161
+ }
162
+ }
163
+ size_t inlen = strlen(s);
164
+ char *out = (char*)malloc(inlen + extra + 1);
165
+ if (!out) return NULL;
166
+
167
+ char *o = out;
168
+ for (const unsigned char *p = in; *p; ++p) {
169
+ switch (*p) {
170
+ case '\\': *o++='\\'; *o++='\\'; break;
171
+ case '"': *o++='\\'; *o++='"'; break;
172
+ case '\n': *o++='\\'; *o++='n'; break;
173
+ case '\r': *o++='\\'; *o++='r'; break;
174
+ case '\t': *o++='\\'; *o++='t'; break;
175
+ case '\b': *o++='\\'; *o++='b'; break;
176
+ case '\f': *o++='\\'; *o++='f'; break;
177
+ default:
178
+ if (*p < 0x20) {
179
+ // Unicode escape for rare control chars
180
+ static const char hex[] = "0123456789abcdef";
181
+ *o++='\\'; *o++='u'; *o++='0'; *o++='0';
182
+ *o++=hex[(*p)>>4]; *o++=hex[(*p)&0xF];
183
+ } else {
184
+ *o++ = (char)*p;
185
+ }
186
+ }
187
+ }
188
+ *o = 0;
189
+ return out;
190
+ }
191
+
192
+ static char *json_escape_query(const char *s) {
193
+ const unsigned char *in = (const unsigned char*)s;
194
+ size_t extra = 0;
195
+ for (const unsigned char *p = in; *p; ++p) {
196
+ switch (*p) {
197
+ case '\\': case '"': case '\n': case '\r': case '\t': extra++; break;
198
+ default: break;
199
+ }
200
+ }
201
+ size_t inlen = strlen(s);
202
+ char *out = (char*)malloc(inlen + extra + 1);
203
+ if (!out) return NULL;
204
+ char *o = out;
205
+ for (const unsigned char *p = in; *p; ++p) {
206
+ switch (*p) {
207
+ case '\\': *o++='\\'; *o++='\\'; break;
208
+ case '"': *o++='\\'; *o++='"'; break;
209
+ case '\n': *o++='\\'; *o++='n'; break;
210
+ case '\r': *o++='\\'; *o++='r'; break;
211
+ case '\t': *o++='\\'; *o++='t'; break;
212
+ default: *o++=(char)*p;
213
+ }
214
+ }
215
+ *o=0;
216
+ return out;
217
+ }
218
+
219
+ static int build_prefix(void) {
220
+ const char *p1 = "{\"query\":\"";
221
+ const char *p2 = "\",\"variables\":{";
222
+ const char *k1 = "\"apiKey\":\"";
223
+ const char *k2 = "\",\"serviceUuid\":\"";
224
+
225
+ size_t n = strlen(p1) + strlen(g_query_escaped) + strlen(p2)
226
+ + strlen(k1) + strlen(g_api_key)
227
+ + strlen(k2) + strlen(g_service_uuid) + 5;
228
+
229
+ char *prefix = (char*)malloc(n);
230
+ if (!prefix) return 0;
231
+
232
+ char *o = prefix;
233
+ o += sprintf(o, "%s%s%s", p1, g_query_escaped, p2);
234
+ o += sprintf(o, "%s%s", k1, g_api_key);
235
+ o += sprintf(o, "%s%s\"", k2, g_service_uuid);
236
+ *o = '\0';
237
+
238
+ g_json_prefix = prefix;
239
+ g_json_prefix_len = (size_t)(o - prefix);
240
+ return 1;
241
+ }
242
+
243
+ static size_t json_escape_inline(char *out, const char *in, size_t in_len) {
244
+ char *o = out;
245
+ for (size_t i = 0; i < in_len; ++i) {
246
+ unsigned char c = (unsigned char)in[i];
247
+ switch (c) {
248
+ case '\\': *o++='\\'; *o++='\\'; break;
249
+ case '"': *o++='\\'; *o++='"'; break;
250
+ default:
251
+ if (c < 0x20) {
252
+ static const char hex[] = "0123456789abcdef";
253
+ *o++='\\'; *o++='u'; *o++='0'; *o++='0';
254
+ *o++=hex[c>>4]; *o++=hex[c&0xF];
255
+ } else {
256
+ *o++ = (char)c;
257
+ }
258
+ }
259
+ }
260
+ return (size_t)(o - out);
261
+ }
262
+
263
+ static inline size_t max_escaped_size(size_t len) { return len * 6; }
264
+
265
+ // ---------- Endpoint registry ----------
266
+ static int make_endpoint_suffix(
267
+ const char *line_e, const char *column_e,
268
+ const char *name_e, const char *entrypoint_e, const char *route_e,
269
+ char **out, size_t *out_len
270
+ ) {
271
+ const char *k1 = ",\"line\":\"";
272
+ const char *k2 = "\",\"column\":\"";
273
+ const char *k3 = "\",\"name\":\"";
274
+ const char *k4 = "\",\"entrypoint\":\"";
275
+ const char *k5 = "\",\"route\":\"";
276
+ const char *k6 = "\",\"timestampMs\":\"";
277
+
278
+ size_t n = strlen(k1)+strlen(line_e)
279
+ + strlen(k2)+strlen(column_e)
280
+ + strlen(k3)+strlen(name_e)
281
+ + strlen(k4)+strlen(entrypoint_e)
282
+ + strlen(k5)+strlen(route_e)
283
+ + strlen(k6);
284
+
285
+ char *buf = (char*)malloc(n + 1);
286
+ if (!buf) return 0;
287
+ char *o = buf;
288
+ o += sprintf(o, "%s%s%s%s%s%s%s%s%s%s%s",
289
+ k1, line_e, k2, column_e, k3, name_e, k4, entrypoint_e, k5, route_e, k6);
290
+ *o = 0;
291
+ *out = buf;
292
+ *out_len = (size_t)(o - buf);
293
+ return 1;
294
+ }
295
+
296
+ static int register_endpoint_internal(
297
+ const char *line, const char *column, const char *name, const char *entrypoint, const char *route
298
+ ) {
299
+ int idx = atomic_fetch_add_explicit(&g_endpoint_count, 1, memory_order_acq_rel);
300
+ if (idx < 0 || idx >= SFN_ENDPOINT_CAP) return -1;
301
+
302
+ char *line_e = json_escape(line);
303
+ char *col_e = json_escape(column);
304
+ char *name_e = json_escape(name);
305
+ char *ep_e = json_escape(entrypoint);
306
+ char *route_e = json_escape(route ? route : "");
307
+ if (!line_e || !col_e || !name_e || !ep_e || !route_e) {
308
+ free(line_e); free(col_e); free(name_e); free(ep_e); free(route_e);
309
+ return -1;
310
+ }
311
+ char *suffix = NULL; size_t suffix_len = 0;
312
+ if (!make_endpoint_suffix(line_e, col_e, name_e, ep_e, route_e, &suffix, &suffix_len)) {
313
+ free(line_e); free(col_e); free(name_e); free(ep_e); free(route_e);
314
+ return -1;
315
+ }
316
+ free(line_e); free(col_e); free(name_e); free(ep_e); free(route_e);
317
+
318
+ g_endpoints[idx].suffix = suffix;
319
+ g_endpoints[idx].suffix_len = suffix_len;
320
+ g_endpoints[idx].in_use = 1;
321
+ return idx;
322
+ }
323
+
324
+ // ---------- message helpers ----------
325
+ static inline void msg_free(sfn_msg_t *msg) {
326
+ if (!msg) return;
327
+ if (msg->type == SFN_MSG_WORK) {
328
+ free(msg->data.work.session_id);
329
+ } else if (msg->type == SFN_MSG_WORK_BODIES) {
330
+ free(msg->data.work_bodies.session_id);
331
+ free(msg->data.work_bodies.route);
332
+ free(msg->data.work_bodies.query_params);
333
+ free(msg->data.work_bodies.req_headers);
334
+ free(msg->data.work_bodies.req_body);
335
+ free(msg->data.work_bodies.resp_headers);
336
+ free(msg->data.work_bodies.resp_body);
337
+ } else if (msg->type == SFN_MSG_BODY) {
338
+ free(msg->data.body.body);
339
+ }
340
+ }
341
+
342
+ // ---------- overflow ops ----------
343
+ static inline void overflow_push(sfn_msg_t msg) {
344
+ sfn_node_t *n = (sfn_node_t*)malloc(sizeof(sfn_node_t));
345
+ if (!n) { msg_free(&msg); return; }
346
+ n->msg = msg;
347
+ sfn_node_t *old = atomic_load_explicit(&g_overflow_head, memory_order_relaxed);
348
+ do { n->next = old; }
349
+ while (!atomic_compare_exchange_weak_explicit(
350
+ &g_overflow_head, &old, n, memory_order_release, memory_order_relaxed));
351
+
352
+ pthread_mutex_lock(&g_cv_mtx);
353
+ pthread_cond_signal(&g_cv);
354
+ pthread_mutex_unlock(&g_cv_mtx);
355
+ }
356
+
357
+ static inline sfn_node_t* overflow_pop_all(void) {
358
+ return atomic_exchange_explicit(&g_overflow_head, NULL, memory_order_acq_rel);
359
+ }
360
+
361
+ static inline void overflow_free_list(sfn_node_t* list) {
362
+ while (list) {
363
+ sfn_node_t* next = list->next;
364
+ msg_free(&list->msg);
365
+ free(list);
366
+ list = next;
367
+ }
368
+ }
369
+
370
+ // ---------- ring ops ----------
371
+ static inline size_t ring_count(void) {
372
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
373
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
374
+ return t - h;
375
+ }
376
+ static inline int ring_empty(void) { return ring_count() == 0; }
377
+
378
+ // PERFORMANCE: Lock-free CAS-based push (removes global contention point)
379
+ // Many producers can make forward progress concurrently without spinning
380
+ static int ring_try_push(sfn_msg_t msg) {
381
+ for (;;) {
382
+ size_t t = atomic_load_explicit(&g_tail, memory_order_relaxed);
383
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
384
+
385
+ if ((t - h) >= g_cap) {
386
+ return 0; // full
387
+ }
388
+
389
+ // Try to claim slot t via CAS
390
+ if (atomic_compare_exchange_weak_explicit(
391
+ &g_tail, &t, t + 1, memory_order_acq_rel, memory_order_relaxed)) {
392
+
393
+ int was_empty = (h == t);
394
+ size_t idx = t % g_cap;
395
+ g_ring[idx] = msg; // single writer for this idx (we own it after CAS succeeds)
396
+
397
+ if (was_empty) {
398
+ pthread_mutex_lock(&g_cv_mtx);
399
+ pthread_cond_signal(&g_cv);
400
+ pthread_mutex_unlock(&g_cv_mtx);
401
+ #if LIBCURL_VERSION_NUM >= 0x073E00 // 7.62.0+
402
+ // Also wake the multi loop if it's sleeping (immediate response)
403
+ if (g_multi) curl_multi_wakeup(g_multi);
404
+ #endif
405
+ }
406
+ return 1;
407
+ }
408
+ // else: lost race, retry
409
+ }
410
+ }
411
+
412
+ static int ring_pop(sfn_msg_t *out_msg) {
413
+ size_t h = atomic_load_explicit(&g_head, memory_order_relaxed);
414
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
415
+ if (h == t) return 0;
416
+ size_t idx = h % g_cap;
417
+ *out_msg = g_ring[idx];
418
+ // Clear for safety
419
+ g_ring[idx].type = SFN_MSG_BODY;
420
+ g_ring[idx].data.body.body = NULL;
421
+ g_ring[idx].data.body.len = 0;
422
+ atomic_store_explicit(&g_head, h + 1, memory_order_release);
423
+ return 1;
424
+ }
425
+
426
+ // ---------- curl callbacks ----------
427
+ static size_t _sink_write(char *ptr, size_t size, size_t nmemb, void *userdata) {
428
+ (void)ptr; (void)userdata; return size * nmemb;
429
+ }
430
+ static size_t _sink_header(char *ptr, size_t size, size_t nmemb, void *userdata) {
431
+ (void)ptr; (void)userdata; return size * nmemb;
432
+ }
433
+
434
+ // ---------- easy-handle pool ----------
435
+ static CURL* pool_acquire_easy(void) {
436
+ pthread_mutex_lock(&g_pool_mtx);
437
+ pool_node_t *node = g_easy_pool;
438
+ if (node) g_easy_pool = node->next;
439
+ pthread_mutex_unlock(&g_pool_mtx);
440
+ if (node) {
441
+ CURL *e = node->easy;
442
+ free(node);
443
+ // PERFORMANCE: NO curl_easy_reset() - keep invariants intact!
444
+ // This eliminates option reconfiguration churn (libcurl connection reuse becomes cheap)
445
+ // Only per-message options (POSTFIELDS/SIZE/PRIVATE) are set in multi_add_message()
446
+ return e;
447
+ }
448
+ // new handle
449
+ CURL *e = curl_easy_init();
450
+ if (!e) return NULL;
451
+ curl_easy_setopt(e, CURLOPT_URL, g_url);
452
+ curl_easy_setopt(e, CURLOPT_HTTPHEADER, g_hdrs);
453
+ curl_easy_setopt(e, CURLOPT_SSL_VERIFYPEER, 0L);
454
+ curl_easy_setopt(e, CURLOPT_SSL_VERIFYHOST, 0L);
455
+ curl_easy_setopt(e, CURLOPT_WRITEFUNCTION, _sink_write);
456
+ curl_easy_setopt(e, CURLOPT_HEADERFUNCTION, _sink_header);
457
+ #ifdef CURL_HTTP_VERSION_2TLS
458
+ if (g_http2) curl_easy_setopt(e, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
459
+ #endif
460
+ curl_easy_setopt(e, CURLOPT_TCP_KEEPALIVE, 1L);
461
+ curl_easy_setopt(e, CURLOPT_NOSIGNAL, 1L);
462
+ #ifdef TCP_NODELAY
463
+ curl_easy_setopt(e, CURLOPT_TCP_NODELAY, 1L);
464
+ #endif
465
+ return e;
466
+ }
467
+
468
+ static void pool_release_easy(CURL *e) {
469
+ if (!e) return;
470
+ pool_node_t *node = (pool_node_t*)malloc(sizeof(pool_node_t));
471
+ if (!node) { curl_easy_cleanup(e); return; }
472
+ node->easy = e;
473
+ pthread_mutex_lock(&g_pool_mtx);
474
+ node->next = g_easy_pool;
475
+ g_easy_pool = node;
476
+ pthread_mutex_unlock(&g_pool_mtx);
477
+ }
478
+
479
+ // ---------- JSON building (on background thread) ----------
480
+ static char* build_body_from_work(const char *session_id, size_t session_len, int endpoint_id, size_t *out_len) {
481
+ if (endpoint_id < 0 || endpoint_id >= SFN_ENDPOINT_CAP) return NULL;
482
+ if (!g_endpoints[endpoint_id].in_use) return NULL;
483
+
484
+ // NOW get the timestamp (on background thread, not request path!)
485
+ uint64_t tms = now_ms();
486
+ char ts_buf[32];
487
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
488
+
489
+ size_t sess_frag_max = 14 + max_escaped_size(session_len) + 1;
490
+ size_t total_max = g_json_prefix_len + sess_frag_max
491
+ + g_endpoints[endpoint_id].suffix_len
492
+ + (size_t)ts_len + 2;
493
+
494
+ char *body = (char*)malloc(total_max + 1);
495
+ if (!body) return NULL;
496
+
497
+ char *o = body;
498
+ memcpy(o, g_json_prefix, g_json_prefix_len); o += g_json_prefix_len;
499
+ memcpy(o, ",\"sessionId\":\"", 14); o += 14;
500
+ o += json_escape_inline(o, session_id, session_len); *o++='"';
501
+ memcpy(o, g_endpoints[endpoint_id].suffix, g_endpoints[endpoint_id].suffix_len);
502
+ o += g_endpoints[endpoint_id].suffix_len;
503
+ memcpy(o, ts_buf, (size_t)ts_len); o += ts_len;
504
+ *o++ = '"'; memcpy(o, JSON_SUFFIX, 2); o += 2; *o = 0;
505
+
506
+ *out_len = (size_t)(o - body);
507
+ return body;
508
+ }
509
+
510
+ // Build JSON body with request/response headers and bodies (on background thread)
511
+ static char* build_body_from_work_with_bodies(
512
+ const char *session_id, size_t session_len, int endpoint_id,
513
+ const char *route, size_t route_len,
514
+ const char *query_params, size_t query_params_len,
515
+ const char *req_headers, size_t req_headers_len,
516
+ const char *req_body, size_t req_body_len,
517
+ const char *resp_headers, size_t resp_headers_len,
518
+ const char *resp_body, size_t resp_body_len,
519
+ size_t *out_len
520
+ ) {
521
+ if (endpoint_id < 0 || endpoint_id >= SFN_ENDPOINT_CAP) return NULL;
522
+ if (!g_endpoints[endpoint_id].in_use) return NULL;
523
+
524
+ // Get timestamp (on background thread)
525
+ uint64_t tms = now_ms();
526
+ char ts_buf[32];
527
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
528
+
529
+ // Calculate max size (headers/bodies are already JSON-escaped)
530
+ size_t sess_frag_max = 14 + max_escaped_size(session_len) + 1;
531
+ size_t total_max = g_json_prefix_len + sess_frag_max
532
+ + g_endpoints[endpoint_id].suffix_len
533
+ + (size_t)ts_len;
534
+
535
+ // Add space for route/query override fields (already escaped)
536
+ if (route) total_max += 12 + route_len; // ",\"route\":\""
537
+ if (query_params) total_max += 18 + query_params_len; // ",\"queryParams\":\""
538
+
539
+ // Add space for request/response fields (already escaped, just need quotes and keys)
540
+ if (req_headers) total_max += 20 + req_headers_len; // ",\"requestHeaders\":\""
541
+ if (req_body) total_max += 17 + req_body_len; // ",\"requestBody\":\""
542
+ if (resp_headers) total_max += 21 + resp_headers_len; // ",\"responseHeaders\":\""
543
+ if (resp_body) total_max += 18 + resp_body_len; // ",\"responseBody\":\""
544
+ total_max += 10; // closing quotes and braces
545
+
546
+ char *body = (char*)malloc(total_max + 1);
547
+ if (!body) return NULL;
548
+
549
+ char *o = body;
550
+ memcpy(o, g_json_prefix, g_json_prefix_len); o += g_json_prefix_len;
551
+ memcpy(o, ",\"sessionId\":\"", 14); o += 14;
552
+ o += json_escape_inline(o, session_id, session_len); *o++='"';
553
+ memcpy(o, g_endpoints[endpoint_id].suffix, g_endpoints[endpoint_id].suffix_len);
554
+ o += g_endpoints[endpoint_id].suffix_len;
555
+ memcpy(o, ts_buf, (size_t)ts_len); o += ts_len;
556
+ *o++ = '"';
557
+
558
+ // Add route override if present (already JSON-escaped) - this overrides the registered route in suffix
559
+ if (route && route_len > 0) {
560
+ memcpy(o, ",\"route\":\"", 10); o += 10;
561
+ memcpy(o, route, route_len); o += route_len;
562
+ *o++ = '"';
563
+ }
564
+
565
+ // Add query params if present (already JSON-escaped)
566
+ if (query_params && query_params_len > 0) {
567
+ memcpy(o, ",\"queryParams\":\"", 16); o += 16;
568
+ memcpy(o, query_params, query_params_len); o += query_params_len;
569
+ *o++ = '"';
570
+ }
571
+
572
+ // Add request/response fields if present (already JSON-escaped)
573
+ if (req_headers && req_headers_len > 0) {
574
+ memcpy(o, ",\"requestHeaders\":\"", 19); o += 19;
575
+ memcpy(o, req_headers, req_headers_len); o += req_headers_len;
576
+ *o++ = '"';
577
+ }
578
+ if (req_body && req_body_len > 0) {
579
+ memcpy(o, ",\"requestBody\":\"", 16); o += 16;
580
+ memcpy(o, req_body, req_body_len); o += req_body_len;
581
+ *o++ = '"';
582
+ }
583
+ if (resp_headers && resp_headers_len > 0) {
584
+ memcpy(o, ",\"responseHeaders\":\"", 20); o += 20;
585
+ memcpy(o, resp_headers, resp_headers_len); o += resp_headers_len;
586
+ *o++ = '"';
587
+ }
588
+ if (resp_body && resp_body_len > 0) {
589
+ memcpy(o, ",\"responseBody\":\"", 17); o += 17;
590
+ memcpy(o, resp_body, resp_body_len); o += resp_body_len;
591
+ *o++ = '"';
592
+ }
593
+
594
+ memcpy(o, JSON_SUFFIX, 2); o += 2; *o = 0;
595
+
596
+ *out_len = (size_t)(o - body);
597
+ return body;
598
+ }
599
+
600
+ // ---------- multi helpers ----------
601
+ static void multi_add_message(char *body, size_t len) {
602
+ CURL *e = pool_acquire_easy();
603
+ if (!e) { free(body); return; }
604
+
605
+ curl_easy_setopt(e, CURLOPT_POSTFIELDS, body);
606
+ curl_easy_setopt(e, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)len);
607
+ // keep pointer to free later
608
+ curl_easy_setopt(e, CURLOPT_PRIVATE, body);
609
+
610
+ CURLMcode mc = curl_multi_add_handle(g_multi, e);
611
+ if (mc != CURLM_OK) {
612
+ pool_release_easy(e);
613
+ free(body);
614
+ return;
615
+ }
616
+ atomic_fetch_add_explicit(&g_inflight, 1, memory_order_acq_rel);
617
+
618
+ #if LIBCURL_VERSION_NUM >= 0x073E00 // 7.62.0+
619
+ // PERFORMANCE: Wake multi loop immediately (avoid 50ms sleep tail latency)
620
+ curl_multi_wakeup(g_multi);
621
+ #endif
622
+ }
623
+
624
+ static void process_msg(sfn_msg_t *msg) {
625
+ if (msg->type == SFN_MSG_WORK) {
626
+ // Build JSON from work item (expensive work happens HERE, not in request path)
627
+ size_t len = 0;
628
+ char *body = build_body_from_work(
629
+ msg->data.work.session_id,
630
+ msg->data.work.session_len,
631
+ msg->data.work.endpoint_id,
632
+ &len
633
+ );
634
+ if (body) {
635
+ multi_add_message(body, len);
636
+ }
637
+ free(msg->data.work.session_id); // done with session_id
638
+ } else if (msg->type == SFN_MSG_WORK_BODIES) {
639
+ // Build JSON with request/response bodies (expensive work on background thread)
640
+ size_t len = 0;
641
+ char *body = build_body_from_work_with_bodies(
642
+ msg->data.work_bodies.session_id,
643
+ msg->data.work_bodies.session_len,
644
+ msg->data.work_bodies.endpoint_id,
645
+ msg->data.work_bodies.route,
646
+ msg->data.work_bodies.route_len,
647
+ msg->data.work_bodies.query_params,
648
+ msg->data.work_bodies.query_params_len,
649
+ msg->data.work_bodies.req_headers,
650
+ msg->data.work_bodies.req_headers_len,
651
+ msg->data.work_bodies.req_body,
652
+ msg->data.work_bodies.req_body_len,
653
+ msg->data.work_bodies.resp_headers,
654
+ msg->data.work_bodies.resp_headers_len,
655
+ msg->data.work_bodies.resp_body,
656
+ msg->data.work_bodies.resp_body_len,
657
+ &len
658
+ );
659
+ if (body) {
660
+ multi_add_message(body, len);
661
+ }
662
+ // Free all work_bodies fields
663
+ free(msg->data.work_bodies.session_id);
664
+ free(msg->data.work_bodies.route);
665
+ free(msg->data.work_bodies.query_params);
666
+ free(msg->data.work_bodies.req_headers);
667
+ free(msg->data.work_bodies.req_body);
668
+ free(msg->data.work_bodies.resp_headers);
669
+ free(msg->data.work_bodies.resp_body);
670
+ } else if (msg->type == SFN_MSG_BODY) {
671
+ // Already built (legacy path)
672
+ multi_add_message(msg->data.body.body, msg->data.body.len);
673
+ }
674
+ }
675
+
676
+ static void multi_pump(void) {
677
+ int running = 0;
678
+ // Drive state machine
679
+ curl_multi_perform(g_multi, &running);
680
+
681
+ // Reap completions
682
+ int msgs = 0;
683
+ CURLMsg *msg;
684
+ while ((msg = curl_multi_info_read(g_multi, &msgs))) {
685
+ if (msg->msg == CURLMSG_DONE) {
686
+ CURL *e = msg->easy_handle;
687
+ char *body = NULL;
688
+ curl_easy_getinfo(e, CURLINFO_PRIVATE, &body);
689
+ curl_multi_remove_handle(g_multi, e);
690
+ pool_release_easy(e);
691
+ free(body);
692
+ atomic_fetch_sub_explicit(&g_inflight, 1, memory_order_acq_rel);
693
+ }
694
+ }
695
+ }
696
+
697
+ // ---------- Network capture suppression ----------
698
+ // Define the telemetry guard to prevent recursive capture
699
+ // Each C extension is compiled separately, so we need our own thread-local copy
700
+
701
+ // ---------- pthread cleanup handler for sender threads ----------
702
+ // ---------- sender thread (multi + H2 multiplex) ----------
703
+ static void *sender_main(void *arg) {
704
+ (void)arg;
705
+
706
+ // CRITICAL: Set telemetry guard for this thread to prevent _sfteepreload.c from capturing our traffic
707
+ // This sender thread only sends telemetry, so ALL its network traffic should be suppressed
708
+ sf_guard_enter();
709
+
710
+ // Initialize per-thread curl_multi handle
711
+ g_multi = curl_multi_init();
712
+ if (!g_multi) {
713
+ sf_guard_leave();
714
+ return NULL;
715
+ }
716
+ #ifdef CURLPIPE_MULTIPLEX
717
+ curl_multi_setopt(g_multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
718
+ #endif
719
+ curl_multi_setopt(g_multi, CURLMOPT_MAX_TOTAL_CONNECTIONS, 1L);
720
+ curl_multi_setopt(g_multi, CURLMOPT_MAX_HOST_CONNECTIONS, 1L);
721
+
722
+ while (atomic_load(&g_running)) {
723
+ // Fill up inflight up to SFN_MAX_INFLIGHT
724
+ while (atomic_load_explicit(&g_inflight, memory_order_acquire) < SFN_MAX_INFLIGHT) {
725
+ sfn_msg_t msg;
726
+ if (ring_pop(&msg)) {
727
+ process_msg(&msg);
728
+ continue;
729
+ }
730
+ // drain overflow into ring-ish order (reverse the LIFO)
731
+ sfn_node_t *list = overflow_pop_all();
732
+ if (list) {
733
+ // reverse to approx FIFO
734
+ sfn_node_t *rev = NULL;
735
+ while (list) { sfn_node_t *n = list->next; list->next = rev; rev = list; list = n; }
736
+ while (rev && atomic_load_explicit(&g_inflight, memory_order_acquire) < SFN_MAX_INFLIGHT) {
737
+ sfn_node_t *n = rev->next;
738
+ process_msg(&rev->msg);
739
+ free(rev);
740
+ rev = n;
741
+ }
742
+ // any leftover (shouldn't happen often) goes back to overflow
743
+ while (rev) { sfn_node_t *n = rev->next; overflow_push(rev->msg); free(rev); rev = n; }
744
+ continue;
745
+ }
746
+ break; // nothing to send
747
+ }
748
+
749
+ // Drive transfers & poll
750
+ multi_pump();
751
+
752
+ // Sleep a bit if idle; otherwise use multi_poll for efficient wake-up
753
+ if (atomic_load_explicit(&g_inflight, memory_order_acquire) == 0 &&
754
+ ring_empty() &&
755
+ atomic_load_explicit(&g_overflow_head, memory_order_acquire) == NULL) {
756
+ pthread_mutex_lock(&g_cv_mtx);
757
+ if (atomic_load_explicit(&g_inflight, memory_order_acquire) == 0 &&
758
+ ring_empty() &&
759
+ atomic_load_explicit(&g_overflow_head, memory_order_acquire) == NULL &&
760
+ atomic_load(&g_running)) {
761
+ pthread_cond_wait(&g_cv, &g_cv_mtx);
762
+ }
763
+ pthread_mutex_unlock(&g_cv_mtx);
764
+ } else {
765
+ // PERFORMANCE: Short poll wait (5ms) - curl_multi_wakeup handles immediate nudges
766
+ // This removes scheduler-scale latency without busy-spinning
767
+ int numfds = 0;
768
+ #if LIBCURL_VERSION_NUM >= 0x074200 // curl >= 7.66.0
769
+ curl_multi_poll(g_multi, NULL, 0, 5, &numfds); // 5ms guard (was 50ms)
770
+ #else
771
+ curl_multi_wait(g_multi, NULL, 0, 5, &numfds);
772
+ #endif
773
+ }
774
+ }
775
+ // final drain
776
+ while (atomic_load_explicit(&g_inflight, memory_order_acquire) > 0) {
777
+ multi_pump();
778
+ int nfds = 0;
779
+ #if LIBCURL_VERSION_NUM >= 0x074200 // curl >= 7.66.0
780
+ curl_multi_poll(g_multi, NULL, 0, 10, &nfds);
781
+ #else
782
+ curl_multi_wait(g_multi, NULL, 0, 10, &nfds);
783
+ #endif
784
+ }
785
+
786
+ if (g_multi) {
787
+ curl_multi_cleanup(g_multi);
788
+ g_multi = NULL;
789
+ }
790
+ sf_guard_leave();
791
+ return NULL;
792
+ }
793
+
794
+ // ---------- Python API ----------
795
+ static PyObject *py_init(PyObject *self, PyObject *args, PyObject *kw) {
796
+ const char *url, *query, *api_key, *service_uuid;
797
+ int http2 = 0;
798
+ static char *kwlist[] = {"url","query","api_key","service_uuid","http2", NULL};
799
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|i",
800
+ kwlist, &url, &query, &api_key, &service_uuid, &http2)) {
801
+ Py_RETURN_FALSE;
802
+ }
803
+ if (g_running) Py_RETURN_TRUE;
804
+
805
+ g_url = str_dup(url);
806
+ g_query_escaped = json_escape_query(query);
807
+ g_api_key = str_dup(api_key);
808
+ g_service_uuid = str_dup(service_uuid);
809
+ g_http2 = http2 ? 1 : 0;
810
+ if (!g_url || !g_query_escaped || !g_api_key || !g_service_uuid) {
811
+ Py_RETURN_FALSE;
812
+ }
813
+ if (!build_prefix()) { Py_RETURN_FALSE; }
814
+
815
+ g_cap = SFN_RING_CAP;
816
+ g_ring = (sfn_msg_t*)calloc(g_cap, sizeof(sfn_msg_t));
817
+ if (!g_ring) { Py_RETURN_FALSE; }
818
+
819
+ curl_global_init(CURL_GLOBAL_DEFAULT);
820
+
821
+ // common headers (REMOVED: X-Sf3-IsTelemetryMessage - no longer needed with guard flag)
822
+ g_hdrs = NULL;
823
+ g_hdrs = curl_slist_append(g_hdrs, "Content-Type: application/json");
824
+ g_hdrs = curl_slist_append(g_hdrs, "Expect:"); // PERFORMANCE: Disable 100-continue (avoids 200ms stalls)
825
+
826
+ // NOTE: g_multi is now initialized per-thread in sender_main()
827
+ // This allows multiple sender threads each with their own curl_multi instance
828
+
829
+ // a template easy to copy options from (kept as reference; not added to multi)
830
+ g_share_template = curl_easy_init();
831
+ if (!g_share_template) { Py_RETURN_FALSE; }
832
+ curl_easy_setopt(g_share_template, CURLOPT_URL, g_url);
833
+ curl_easy_setopt(g_share_template, CURLOPT_HTTPHEADER, g_hdrs);
834
+ curl_easy_setopt(g_share_template, CURLOPT_SSL_VERIFYPEER, 0L);
835
+ curl_easy_setopt(g_share_template, CURLOPT_SSL_VERIFYHOST, 0L);
836
+ curl_easy_setopt(g_share_template, CURLOPT_WRITEFUNCTION, _sink_write);
837
+ curl_easy_setopt(g_share_template, CURLOPT_HEADERFUNCTION, _sink_header);
838
+ #ifdef CURL_HTTP_VERSION_2TLS
839
+ if (g_http2) curl_easy_setopt(g_share_template, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
840
+ #endif
841
+ curl_easy_setopt(g_share_template, CURLOPT_TCP_KEEPALIVE, 1L);
842
+ curl_easy_setopt(g_share_template, CURLOPT_NOSIGNAL, 1L);
843
+ #ifdef TCP_NODELAY
844
+ curl_easy_setopt(g_share_template, CURLOPT_TCP_NODELAY, 1L);
845
+ #endif
846
+
847
+ // Parse SF_NETWORKHOP_SENDER_THREADS environment variable (default: 2, max: 16)
848
+ // NetworkHop expected to have decent volume, so default to 2 threads
849
+ const char *num_threads_env = getenv("SF_NETWORKHOP_SENDER_THREADS");
850
+ g_num_sender_threads = num_threads_env ? atoi(num_threads_env) : 2;
851
+ if (g_num_sender_threads < 1) g_num_sender_threads = 1;
852
+ if (g_num_sender_threads > MAX_SENDER_THREADS) g_num_sender_threads = MAX_SENDER_THREADS;
853
+
854
+ atomic_store(&g_running, 1);
855
+
856
+ // Start thread pool
857
+ for (int i = 0; i < g_num_sender_threads; i++) {
858
+ if (pthread_create(&g_sender_threads[i], NULL, sender_main, NULL) != 0) {
859
+ atomic_store(&g_running, 0);
860
+ // Join any threads that were already created
861
+ for (int j = 0; j < i; j++) {
862
+ pthread_join(g_sender_threads[j], NULL);
863
+ }
864
+ Py_RETURN_FALSE;
865
+ }
866
+ }
867
+
868
+ Py_RETURN_TRUE;
869
+ }
870
+
871
+ static PyObject *py_register_endpoint(PyObject *self, PyObject *args, PyObject *kw) {
872
+ const char *line, *column, *name, *entrypoint, *route = NULL;
873
+ static char *kwlist[] = {"line","column","name","entrypoint","route", NULL};
874
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssss|z", kwlist,
875
+ &line, &column, &name, &entrypoint, &route)) {
876
+ return PyLong_FromLong(-1);
877
+ }
878
+ if (!g_running || g_json_prefix == NULL) {
879
+ return PyLong_FromLong(-1);
880
+ }
881
+ int idx = register_endpoint_internal(line, column, name, entrypoint, route);
882
+ return PyLong_FromLong(idx);
883
+ }
884
+
885
+ // generic (kept); producer path is GIL-free
886
+ static PyObject *py_networkhop(PyObject *self, PyObject *args, PyObject *kw) {
887
+ const char *session_id, *line, *column, *name, *entrypoint;
888
+ Py_ssize_t session_len = 0, line_len = 0, column_len = 0, name_len = 0, entrypoint_len = 0;
889
+ static char *kwlist[] = {"session_id","line","column","name","entrypoint", NULL};
890
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#s#s#s#", kwlist,
891
+ &session_id, &session_len,
892
+ &line, &line_len,
893
+ &column, &column_len,
894
+ &name, &name_len,
895
+ &entrypoint, &entrypoint_len)) {
896
+ Py_RETURN_NONE;
897
+ }
898
+ if (!g_running || g_json_prefix == NULL) Py_RETURN_NONE;
899
+
900
+ char *body = NULL; size_t len = 0;
901
+
902
+ Py_BEGIN_ALLOW_THREADS
903
+ // Build the full body
904
+ {
905
+ uint64_t tms = now_ms();
906
+ char ts_buf[32];
907
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
908
+
909
+ size_t sess_frag_max = 14 + max_escaped_size((size_t)session_len) + 1;
910
+ size_t max_len = g_json_prefix_len + sess_frag_max + 200
911
+ + max_escaped_size((size_t)line_len)
912
+ + max_escaped_size((size_t)column_len)
913
+ + max_escaped_size((size_t)name_len)
914
+ + max_escaped_size((size_t)entrypoint_len)
915
+ + (size_t)ts_len + 2;
916
+
917
+ body = (char*)malloc(max_len + 1);
918
+ if (body) {
919
+ char *o = body;
920
+ memcpy(o, g_json_prefix, g_json_prefix_len); o += g_json_prefix_len;
921
+ memcpy(o, ",\"sessionId\":\"", 14); o += 14;
922
+ o += json_escape_inline(o, session_id, (size_t)session_len); *o++='"';
923
+
924
+ memcpy(o, ",\"line\":\"", 10); o += 10; o += json_escape_inline(o, line, (size_t)line_len);
925
+ memcpy(o, "\",\"column\":\"", 12); o += 12; o += json_escape_inline(o, column, (size_t)column_len);
926
+ memcpy(o, "\",\"name\":\"", 10); o += 10; o += json_escape_inline(o, name, (size_t)name_len);
927
+ memcpy(o, "\",\"entrypoint\":\"", 16); o += 16; o += json_escape_inline(o, entrypoint, (size_t)entrypoint_len);
928
+
929
+ memcpy(o, "\",\"timestampMs\":\"", 17); o += 17;
930
+ memcpy(o, ts_buf, (size_t)ts_len); o += ts_len;
931
+ *o++ = '"'; memcpy(o, JSON_SUFFIX, 2); o += 2; *o = 0;
932
+
933
+ len = (size_t)(o - body);
934
+
935
+ sfn_msg_t msg = { .type = SFN_MSG_BODY, .data.body = { .body = body, .len = len } };
936
+ if (!ring_try_push(msg)) overflow_push(msg);
937
+ }
938
+ }
939
+ Py_END_ALLOW_THREADS
940
+
941
+ Py_RETURN_NONE;
942
+ }
943
+
944
+ // ULTRA-FAST PATH: Zero-copy - just queue the pointer, background thread copies
945
+ // This achieves TRUE async: only atomic queue operation on hot path (~1µs)
946
+ static PyObject *py_networkhop_fast(PyObject *self, PyObject *args, PyObject *kw) {
947
+ const char *session_id; Py_ssize_t session_len = 0; int endpoint_id = -1;
948
+ static char *kwlist[] = {"session_id","endpoint_id", NULL};
949
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#i", kwlist,
950
+ &session_id, &session_len, &endpoint_id)) {
951
+ Py_RETURN_NONE;
952
+ }
953
+ if (!g_running || g_json_prefix == NULL) Py_RETURN_NONE;
954
+ if (endpoint_id < 0 || endpoint_id >= SFN_ENDPOINT_CAP) Py_RETURN_NONE;
955
+ if (!g_endpoints[endpoint_id].in_use) Py_RETURN_NONE;
956
+
957
+ // CRITICAL OPTIMIZATION: Do the malloc+memcpy in a SINGLE atomic operation
958
+ // to minimize time holding the GIL. This is faster than releasing GIL,
959
+ // doing malloc, then reacquiring.
960
+ size_t len = (size_t)session_len;
961
+ char *sid_copy = (char*)malloc(len + 1);
962
+ if (!sid_copy) Py_RETURN_NONE;
963
+
964
+ memcpy(sid_copy, session_id, len);
965
+ sid_copy[len] = 0;
966
+
967
+ sfn_msg_t msg = {
968
+ .type = SFN_MSG_WORK,
969
+ .data.work = {
970
+ .session_id = sid_copy,
971
+ .session_len = len,
972
+ .endpoint_id = endpoint_id
973
+ }
974
+ };
975
+
976
+ // Release GIL for queue operation (this is the only blocking part)
977
+ int pushed = 0;
978
+ Py_BEGIN_ALLOW_THREADS
979
+ pushed = ring_try_push(msg);
980
+ if (!pushed) overflow_push(msg);
981
+ Py_END_ALLOW_THREADS
982
+
983
+ Py_RETURN_NONE;
984
+ }
985
+
986
+ // Helper: concatenate dict of header key-value pairs or list of bytes chunks (with GIL held)
987
+ // Returns malloced JSON-escaped string
988
+ static char* extract_and_escape_data(PyObject *obj, size_t *out_len) {
989
+ if (!obj || obj == Py_None) {
990
+ *out_len = 0;
991
+ return NULL;
992
+ }
993
+
994
+ // Handle dict (headers) - format as JSON object string
995
+ if (PyDict_Check(obj)) {
996
+ // Build JSON object: {"key":"value","key2":"value2"}
997
+ PyObject *items = PyDict_Items(obj);
998
+ if (!items) { *out_len = 0; return NULL; }
999
+
1000
+ Py_ssize_t nitems = PyList_Size(items);
1001
+ if (nitems == 0) {
1002
+ Py_DECREF(items);
1003
+ *out_len = 0;
1004
+ return NULL;
1005
+ }
1006
+
1007
+ // Build JSON object (will be double-escaped: once here, once when inserted into variables)
1008
+ // Estimate size: {"key":"value","key2":"value2"}
1009
+ size_t total = 2; // {}
1010
+ for (Py_ssize_t i = 0; i < nitems; i++) {
1011
+ PyObject *pair = PyList_GetItem(items, i);
1012
+ PyObject *key = PyTuple_GetItem(pair, 0);
1013
+ PyObject *val = PyTuple_GetItem(pair, 1);
1014
+ const char *key_str = PyUnicode_AsUTF8(key);
1015
+ const char *val_str = PyUnicode_AsUTF8(val);
1016
+ if (key_str && val_str) {
1017
+ // "key":"value", (worst case: double length for escaping)
1018
+ total += strlen(key_str) * 2 + strlen(val_str) * 2 + 6; // quotes, colon, comma
1019
+ }
1020
+ }
1021
+
1022
+ char *result = (char*)malloc(total * 2 + 1); // *2 for extra safety
1023
+ if (!result) {
1024
+ Py_DECREF(items);
1025
+ *out_len = 0;
1026
+ return NULL;
1027
+ }
1028
+
1029
+ char *o = result;
1030
+ *o++ = '{';
1031
+ int first = 1;
1032
+ for (Py_ssize_t i = 0; i < nitems; i++) {
1033
+ PyObject *pair = PyList_GetItem(items, i);
1034
+ PyObject *key = PyTuple_GetItem(pair, 0);
1035
+ PyObject *val = PyTuple_GetItem(pair, 1);
1036
+
1037
+ // Convert key to string (keys are always strings in JSON)
1038
+ const char *key_str = NULL;
1039
+ PyObject *key_str_obj = NULL;
1040
+ if (PyUnicode_Check(key)) {
1041
+ key_str = PyUnicode_AsUTF8(key);
1042
+ } else {
1043
+ key_str_obj = PyObject_Str(key);
1044
+ if (key_str_obj) {
1045
+ key_str = PyUnicode_AsUTF8(key_str_obj);
1046
+ }
1047
+ }
1048
+
1049
+ if (!key_str) {
1050
+ Py_XDECREF(key_str_obj);
1051
+ if (PyErr_Occurred()) PyErr_Clear();
1052
+ continue;
1053
+ }
1054
+
1055
+ if (!first) *o++ = ',';
1056
+ first = 0;
1057
+
1058
+ // Escape key (always quoted)
1059
+ *o++ = '"';
1060
+ char *key_esc = json_escape(key_str);
1061
+ if (key_esc) {
1062
+ size_t klen = strlen(key_esc);
1063
+ memcpy(o, key_esc, klen);
1064
+ o += klen;
1065
+ free(key_esc);
1066
+ }
1067
+ *o++ = '"';
1068
+ *o++ = ':';
1069
+
1070
+ // Handle value based on type (preserve JSON types)
1071
+ if (val == Py_None) {
1072
+ // null
1073
+ memcpy(o, "null", 4);
1074
+ o += 4;
1075
+ } else if (PyBool_Check(val)) {
1076
+ // true or false
1077
+ if (val == Py_True) {
1078
+ memcpy(o, "true", 4);
1079
+ o += 4;
1080
+ } else {
1081
+ memcpy(o, "false", 5);
1082
+ o += 5;
1083
+ }
1084
+ } else if (PyLong_Check(val)) {
1085
+ // integer (no quotes)
1086
+ long num = PyLong_AsLong(val);
1087
+ if (num == -1 && PyErr_Occurred()) {
1088
+ PyErr_Clear();
1089
+ memcpy(o, "null", 4);
1090
+ o += 4;
1091
+ } else {
1092
+ char buf[32];
1093
+ int len = snprintf(buf, sizeof(buf), "%ld", num);
1094
+ memcpy(o, buf, len);
1095
+ o += len;
1096
+ }
1097
+ } else if (PyFloat_Check(val)) {
1098
+ // float (no quotes)
1099
+ double num = PyFloat_AsDouble(val);
1100
+ if (num == -1.0 && PyErr_Occurred()) {
1101
+ PyErr_Clear();
1102
+ memcpy(o, "null", 4);
1103
+ o += 4;
1104
+ } else {
1105
+ char buf[64];
1106
+ int len = snprintf(buf, sizeof(buf), "%.17g", num);
1107
+ memcpy(o, buf, len);
1108
+ o += len;
1109
+ }
1110
+ } else {
1111
+ // String or other type - convert to string and quote
1112
+ const char *val_str = NULL;
1113
+ PyObject *val_str_obj = NULL;
1114
+ if (PyUnicode_Check(val)) {
1115
+ val_str = PyUnicode_AsUTF8(val);
1116
+ } else {
1117
+ val_str_obj = PyObject_Str(val);
1118
+ if (val_str_obj) {
1119
+ val_str = PyUnicode_AsUTF8(val_str_obj);
1120
+ }
1121
+ }
1122
+
1123
+ if (val_str) {
1124
+ *o++ = '"';
1125
+ char *val_esc = json_escape(val_str);
1126
+ if (val_esc) {
1127
+ size_t vlen = strlen(val_esc);
1128
+ memcpy(o, val_esc, vlen);
1129
+ o += vlen;
1130
+ free(val_esc);
1131
+ }
1132
+ *o++ = '"';
1133
+ } else {
1134
+ // Fallback to null if conversion failed
1135
+ memcpy(o, "null", 4);
1136
+ o += 4;
1137
+ }
1138
+
1139
+ Py_XDECREF(val_str_obj);
1140
+ }
1141
+
1142
+ // Clean up temporary key string object
1143
+ Py_XDECREF(key_str_obj);
1144
+
1145
+ // Clear any exceptions that occurred during conversion
1146
+ if (PyErr_Occurred()) {
1147
+ PyErr_Clear();
1148
+ }
1149
+ }
1150
+ *o++ = '}';
1151
+ *o = '\0';
1152
+ Py_DECREF(items);
1153
+
1154
+ *out_len = (size_t)(o - result);
1155
+ return result;
1156
+ }
1157
+
1158
+ // Handle list of bytes (body chunks)
1159
+ if (PyList_Check(obj)) {
1160
+ Py_ssize_t nchunks = PyList_Size(obj);
1161
+ if (nchunks == 0) {
1162
+ *out_len = 0;
1163
+ return NULL;
1164
+ }
1165
+
1166
+ // First pass: calculate total size
1167
+ size_t total = 0;
1168
+ for (Py_ssize_t i = 0; i < nchunks; i++) {
1169
+ PyObject *chunk = PyList_GetItem(obj, i);
1170
+ if (PyBytes_Check(chunk)) {
1171
+ total += (size_t)PyBytes_Size(chunk);
1172
+ }
1173
+ }
1174
+
1175
+ if (total == 0) {
1176
+ *out_len = 0;
1177
+ return NULL;
1178
+ }
1179
+
1180
+ // Allocate raw buffer
1181
+ char *raw = (char*)malloc(total + 1);
1182
+ if (!raw) {
1183
+ *out_len = 0;
1184
+ return NULL;
1185
+ }
1186
+
1187
+ // Second pass: concatenate
1188
+ char *o = raw;
1189
+ for (Py_ssize_t i = 0; i < nchunks; i++) {
1190
+ PyObject *chunk = PyList_GetItem(obj, i);
1191
+ if (PyBytes_Check(chunk)) {
1192
+ char *data = PyBytes_AsString(chunk);
1193
+ if (!data) {
1194
+ PyErr_Clear(); // Clear exception and skip this chunk
1195
+ continue;
1196
+ }
1197
+ Py_ssize_t len = PyBytes_Size(chunk);
1198
+ memcpy(o, data, (size_t)len);
1199
+ o += len;
1200
+ }
1201
+ }
1202
+ *o = '\0';
1203
+
1204
+ // JSON-escape
1205
+ char *escaped = json_escape(raw);
1206
+ free(raw);
1207
+
1208
+ if (escaped) {
1209
+ *out_len = strlen(escaped);
1210
+ return escaped;
1211
+ }
1212
+ *out_len = 0;
1213
+ return NULL;
1214
+ }
1215
+
1216
+ // Handle bytes
1217
+ if (PyBytes_Check(obj)) {
1218
+ char *data = PyBytes_AsString(obj);
1219
+ if (!data) {
1220
+ PyErr_Clear(); // Clear exception
1221
+ *out_len = 0;
1222
+ return NULL;
1223
+ }
1224
+ Py_ssize_t len = PyBytes_Size(obj);
1225
+ char *escaped = json_escape(data);
1226
+ if (escaped) {
1227
+ *out_len = strlen(escaped);
1228
+ return escaped;
1229
+ }
1230
+ *out_len = 0;
1231
+ return NULL;
1232
+ }
1233
+
1234
+ // Handle string
1235
+ if (PyUnicode_Check(obj)) {
1236
+ const char *str = PyUnicode_AsUTF8(obj);
1237
+ if (str) {
1238
+ char *escaped = json_escape(str);
1239
+ if (escaped) {
1240
+ *out_len = strlen(escaped);
1241
+ return escaped;
1242
+ }
1243
+ } else {
1244
+ // Clear exception from PyUnicode_AsUTF8 (invalid UTF-8 string)
1245
+ PyErr_Clear();
1246
+ }
1247
+ *out_len = 0;
1248
+ return NULL;
1249
+ }
1250
+
1251
+ *out_len = 0;
1252
+ return NULL;
1253
+ }
1254
+
1255
+ // Ultra-fast body capture with GIL-released consolidation
1256
+ static PyObject *py_networkhop_with_bodies(PyObject *self, PyObject *args, PyObject *kw) {
1257
+ const char *session_id; Py_ssize_t session_len = 0;
1258
+ int endpoint_id = -1;
1259
+ const char *raw_path = NULL;
1260
+ const char *raw_query_string = NULL; Py_ssize_t raw_query_len = 0;
1261
+ PyObject *req_headers_obj = NULL, *req_body_obj = NULL;
1262
+ PyObject *resp_headers_obj = NULL, *resp_body_obj = NULL;
1263
+
1264
+ static char *kwlist[] = {"session_id", "endpoint_id", "raw_path", "raw_query_string",
1265
+ "request_headers", "request_body",
1266
+ "response_headers", "response_body", NULL};
1267
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#i|zy#OOOO", kwlist,
1268
+ &session_id, &session_len, &endpoint_id,
1269
+ &raw_path,
1270
+ &raw_query_string, &raw_query_len,
1271
+ &req_headers_obj, &req_body_obj,
1272
+ &resp_headers_obj, &resp_body_obj)) {
1273
+ return NULL; // PyArg_ParseTupleAndKeywords sets exception on failure
1274
+ }
1275
+
1276
+ if (!g_running || g_json_prefix == NULL) Py_RETURN_NONE;
1277
+ if (endpoint_id < 0 || endpoint_id >= SFN_ENDPOINT_CAP) Py_RETURN_NONE;
1278
+ if (!g_endpoints[endpoint_id].in_use) Py_RETURN_NONE;
1279
+
1280
+ // Copy session_id with GIL held (small, fast)
1281
+ size_t len = (size_t)session_len;
1282
+ char *sid_copy = (char*)malloc(len + 1);
1283
+ if (!sid_copy) Py_RETURN_NONE;
1284
+ memcpy(sid_copy, session_id, len);
1285
+ sid_copy[len] = 0;
1286
+
1287
+ // JSON-escape raw_path if provided (it's a string)
1288
+ char *route_escaped = NULL;
1289
+ size_t route_len = 0;
1290
+ if (raw_path) {
1291
+ route_escaped = json_escape(raw_path);
1292
+ if (route_escaped) route_len = strlen(route_escaped);
1293
+ }
1294
+
1295
+ // Decode and JSON-escape raw_query_string if provided (it's bytes)
1296
+ char *query_params_escaped = NULL;
1297
+ size_t query_params_len = 0;
1298
+ if (raw_query_string && raw_query_len > 0) {
1299
+ // Decode bytes to string (UTF-8)
1300
+ char *query_str = (char*)malloc((size_t)raw_query_len + 1);
1301
+ if (query_str) {
1302
+ memcpy(query_str, raw_query_string, (size_t)raw_query_len);
1303
+ query_str[raw_query_len] = 0;
1304
+
1305
+ // JSON-escape it
1306
+ query_params_escaped = json_escape(query_str);
1307
+ if (query_params_escaped) query_params_len = strlen(query_params_escaped);
1308
+
1309
+ free(query_str);
1310
+ }
1311
+ }
1312
+
1313
+ // Extract and JSON-escape request/response data (with GIL held)
1314
+ size_t req_headers_len = 0, req_body_len = 0, resp_headers_len = 0, resp_body_len = 0;
1315
+ char *req_headers = extract_and_escape_data(req_headers_obj, &req_headers_len);
1316
+ char *req_body = extract_and_escape_data(req_body_obj, &req_body_len);
1317
+ char *resp_headers = extract_and_escape_data(resp_headers_obj, &resp_headers_len);
1318
+ char *resp_body = extract_and_escape_data(resp_body_obj, &resp_body_len);
1319
+
1320
+ // Check if any Python exceptions occurred during extraction
1321
+ if (PyErr_Occurred()) {
1322
+ // Clean up allocated memory
1323
+ free(sid_copy);
1324
+ free(route_escaped);
1325
+ free(query_params_escaped);
1326
+ free(req_headers);
1327
+ free(req_body);
1328
+ free(resp_headers);
1329
+ free(resp_body);
1330
+ return NULL; // Propagate the exception
1331
+ }
1332
+
1333
+ // Build message (all data copied, GIL can be released for queue)
1334
+ sfn_msg_t msg = {
1335
+ .type = SFN_MSG_WORK_BODIES,
1336
+ .data.work_bodies = {
1337
+ .session_id = sid_copy,
1338
+ .session_len = len,
1339
+ .endpoint_id = endpoint_id,
1340
+ .route = route_escaped,
1341
+ .route_len = route_len,
1342
+ .query_params = query_params_escaped,
1343
+ .query_params_len = query_params_len,
1344
+ .req_headers = req_headers,
1345
+ .req_headers_len = req_headers_len,
1346
+ .req_body = req_body,
1347
+ .req_body_len = req_body_len,
1348
+ .resp_headers = resp_headers,
1349
+ .resp_headers_len = resp_headers_len,
1350
+ .resp_body = resp_body,
1351
+ .resp_body_len = resp_body_len
1352
+ }
1353
+ };
1354
+
1355
+ // Release GIL for queue operation
1356
+ int pushed = 0;
1357
+ Py_BEGIN_ALLOW_THREADS
1358
+ pushed = ring_try_push(msg);
1359
+ if (!pushed) overflow_push(msg);
1360
+ Py_END_ALLOW_THREADS
1361
+
1362
+ Py_RETURN_NONE;
1363
+ }
1364
+
1365
+ static PyObject *py_shutdown(PyObject *self, PyObject *args) {
1366
+ if (!g_running) Py_RETURN_NONE;
1367
+ atomic_store(&g_running, 0);
1368
+
1369
+ // Wake ALL threads with broadcast (not signal)
1370
+ pthread_mutex_lock(&g_cv_mtx);
1371
+ pthread_cond_broadcast(&g_cv);
1372
+ pthread_mutex_unlock(&g_cv_mtx);
1373
+
1374
+ // Join all sender threads in thread pool
1375
+ for (int i = 0; i < g_num_sender_threads; i++) {
1376
+ if (g_sender_threads[i]) {
1377
+ pthread_join(g_sender_threads[i], NULL);
1378
+ g_sender_threads[i] = 0;
1379
+ }
1380
+ }
1381
+ g_num_sender_threads = 0;
1382
+
1383
+ // NOTE: g_multi is now per-thread and cleaned by pthread_cleanup_push in sender_main()
1384
+ // No need to clean it here
1385
+
1386
+ // cleanup pool
1387
+ pthread_mutex_lock(&g_pool_mtx);
1388
+ while (g_easy_pool) {
1389
+ pool_node_t *n = g_easy_pool; g_easy_pool = n->next;
1390
+ curl_easy_cleanup(n->easy);
1391
+ free(n);
1392
+ }
1393
+ pthread_mutex_unlock(&g_pool_mtx);
1394
+
1395
+ if (g_share_template) { curl_easy_cleanup(g_share_template); g_share_template = NULL; }
1396
+ if (g_hdrs) { curl_slist_free_all(g_hdrs); g_hdrs = NULL; }
1397
+
1398
+ curl_global_cleanup();
1399
+
1400
+ free(g_url); g_url=NULL;
1401
+ free(g_query_escaped); g_query_escaped=NULL;
1402
+ free(g_json_prefix); g_json_prefix=NULL;
1403
+ free(g_api_key); g_api_key=NULL;
1404
+ free(g_service_uuid); g_service_uuid=NULL;
1405
+
1406
+ for (int i = 0; i < SFN_ENDPOINT_CAP; ++i) {
1407
+ if (g_endpoints[i].in_use) {
1408
+ free(g_endpoints[i].suffix);
1409
+ g_endpoints[i].suffix = NULL;
1410
+ g_endpoints[i].suffix_len = 0;
1411
+ g_endpoints[i].in_use = 0;
1412
+ }
1413
+ }
1414
+ atomic_store(&g_endpoint_count, 0);
1415
+
1416
+ if (g_ring) {
1417
+ sfn_msg_t msg;
1418
+ while (ring_pop(&msg)) {
1419
+ msg_free(&msg);
1420
+ }
1421
+ free(g_ring); g_ring=NULL;
1422
+ }
1423
+ sfn_node_t *list = overflow_pop_all();
1424
+ while (list) {
1425
+ sfn_node_t *next = list->next;
1426
+ msg_free(&list->msg);
1427
+ free(list);
1428
+ list = next;
1429
+ }
1430
+
1431
+ Py_RETURN_NONE;
1432
+ }
1433
+
1434
+ static PyMethodDef SFNetworkHopMethods[] = {
1435
+ {"init", (PyCFunction)py_init, METH_VARARGS | METH_KEYWORDS, "Init networkhop and start sender"},
1436
+ {"register_endpoint", (PyCFunction)py_register_endpoint, METH_VARARGS | METH_KEYWORDS, "Register endpoint invariants -> endpoint_id"},
1437
+ {"networkhop", (PyCFunction)py_networkhop, METH_VARARGS | METH_KEYWORDS, "Send network hop (generic)"},
1438
+ {"networkhop_fast", (PyCFunction)py_networkhop_fast, METH_VARARGS | METH_KEYWORDS, "Send network hop (fast, uses endpoint_id)"},
1439
+ {"networkhop_with_bodies",(PyCFunction)py_networkhop_with_bodies,METH_VARARGS | METH_KEYWORDS, "Send network hop with optional body chunks (GIL-released)"},
1440
+ {"shutdown", (PyCFunction)py_shutdown, METH_NOARGS, "Shutdown sender and free state"},
1441
+ {NULL, NULL, 0, NULL}
1442
+ };
1443
+
1444
+ static struct PyModuleDef sfnetworkhopmodule = {
1445
+ PyModuleDef_HEAD_INIT,
1446
+ "_sfnetworkhop",
1447
+ "sf_veritas ultra-fast network hop capture",
1448
+ -1,
1449
+ SFNetworkHopMethods
1450
+ };
1451
+
1452
+ PyMODINIT_FUNC PyInit__sfnetworkhop(void) {
1453
+ return PyModule_Create(&sfnetworkhopmodule);
1454
+ }