sf-veritas 0.10.3__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.

Potentially problematic release.


This version of sf-veritas might be problematic. Click here for more details.

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