sf-veritas 0.10.3__cp311-cp311-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.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +5167 -0
- sf_veritas/app_config.py +49 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +10 -0
- sf_veritas/custom_excepthook.py +304 -0
- sf_veritas/custom_log_handler.py +129 -0
- sf_veritas/custom_output_wrapper.py +144 -0
- sf_veritas/custom_print.py +146 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +186 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/fast_frame_info.py +116 -0
- sf_veritas/fast_network_hop.py +293 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/funcspan_config_loader.py +556 -0
- sf_veritas/function_span_profiler.py +1174 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +497 -0
- sf_veritas/libsfnettee.so +0 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +76 -0
- sf_veritas/patches/network_libraries/aiohttp.py +281 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +419 -0
- sf_veritas/patches/network_libraries/httpcore.py +515 -0
- sf_veritas/patches/network_libraries/httplib2.py +204 -0
- sf_veritas/patches/network_libraries/httpx.py +515 -0
- sf_veritas/patches/network_libraries/niquests.py +211 -0
- sf_veritas/patches/network_libraries/pycurl.py +385 -0
- sf_veritas/patches/network_libraries/requests.py +633 -0
- sf_veritas/patches/network_libraries/tornado.py +341 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +468 -0
- sf_veritas/patches/network_libraries/utils.py +398 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +218 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
- sf_veritas/patches/web_frameworks/bottle.py +502 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +944 -0
- sf_veritas/patches/web_frameworks/eve.py +395 -0
- sf_veritas/patches/web_frameworks/falcon.py +926 -0
- sf_veritas/patches/web_frameworks/fastapi.py +724 -0
- sf_veritas/patches/web_frameworks/flask.py +520 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +551 -0
- sf_veritas/patches/web_frameworks/pyramid.py +428 -0
- sf_veritas/patches/web_frameworks/quart.py +824 -0
- sf_veritas/patches/web_frameworks/robyn.py +697 -0
- sf_veritas/patches/web_frameworks/sanic.py +857 -0
- sf_veritas/patches/web_frameworks/starlette.py +723 -0
- sf_veritas/patches/web_frameworks/strawberry.py +813 -0
- sf_veritas/patches/web_frameworks/tornado.py +481 -0
- sf_veritas/patches/web_frameworks/utils.py +91 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +409 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +970 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +132 -0
- sf_veritas/types.py +47 -0
- sf_veritas/unified_interceptor.py +1580 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.10.3.dist-info/METADATA +97 -0
- sf_veritas-0.10.3.dist-info/RECORD +132 -0
- sf_veritas-0.10.3.dist-info/WHEEL +5 -0
- sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
- sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
- sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
- sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
- sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
- sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
- sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
- sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
- sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
- sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
- sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
- sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
- sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
- sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
- sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
- sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
- sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
- sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
- sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
- sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
- sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
- sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
- sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
- 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
|
+
}
|