Skip to content

Commit 4cd8d3e

Browse files
authored
Merge pull request DeusData#810 from DeusData/distill/536-cross-repo
fix(cross-repo): normalize URLs, match templates, run both directions
2 parents 90f59c5 + c5aef83 commit 4cd8d3e

2 files changed

Lines changed: 451 additions & 48 deletions

File tree

‎src/pipeline/pass_cross_repo.c‎

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ enum {
3535
CR_INIT_CAP = 32,
3636
CR_COL_3 = 3,
3737
CR_COL_4 = 4,
38+
CR_SCHEME_SKIP = 3, /* strlen("://") */
39+
CR_ROUTE_PREFIX_LEN = 9, /* strlen("__route__") */
40+
CR_ANY_LEN = 3, /* strlen("ANY") */
3841
};
3942

4043
#define CR_MS_PER_SEC 1000.0
@@ -115,7 +118,10 @@ static void delete_cross_edges(cbm_store_t *store, const char *project) {
115118
cbm_store_delete_edges_by_type(store, project, "CROSS_TRPC_CALLS");
116119
}
117120

118-
/* Insert a CROSS_* edge into a store. */
121+
/* Insert a CROSS_* edge into a store. Idempotent by construction: the edges
122+
* table is UNIQUE(source_id, target_id, type) and cbm_store_insert_edge
123+
* upserts on conflict, so a pair reached from both match directions or on a
124+
* repeat run never duplicates a row. (#523) */
119125
static void insert_cross_edge(cbm_store_t *store, const char *project, int64_t from_id,
120126
int64_t to_id, const char *edge_type, const char *props) {
121127
cbm_edge_t edge = {
@@ -128,6 +134,24 @@ static void insert_cross_edge(cbm_store_t *store, const char *project, int64_t f
128134
cbm_store_insert_edge(store, &edge);
129135
}
130136

137+
/* Strip "scheme://host[:port]" from a stored HTTP_CALLS url, returning the
138+
* path. url_path property values are stored raw from the call's first string
139+
* argument, so they can be full URLs ("scheme://host:port/v2/x") — and
140+
* cbm_route_canon_path only canonicalizes placeholder syntax, never strips
141+
* authorities. Returns "/" for a URL with no path after the host (a request
142+
* against the bare base URL targets the root route). (#523) */
143+
static const char *cr_url_path(const char *url) {
144+
if (!url) {
145+
return url;
146+
}
147+
const char *scheme_end = strstr(url, "://");
148+
if (!scheme_end) {
149+
return url; /* already a bare path */
150+
}
151+
const char *path_start = strchr(scheme_end + CR_SCHEME_SKIP, '/');
152+
return path_start ? path_start : "/";
153+
}
154+
131155
/* Look up a node's name and file_path by id. */
132156
static void lookup_node_info(struct sqlite3 *db, int64_t node_id, char *name_out, size_t name_sz,
133157
char *file_out, size_t file_sz) {
@@ -208,6 +232,114 @@ static int64_t find_route_handler(cbm_store_t *target_store, const char *route_q
208232
return handler_id;
209233
}
210234

235+
/* Segment-wise match of a concrete path against a route template path, where a
236+
* "{...}" segment in the template matches any single non-empty concrete
237+
* segment. Both inputs are bare paths (no method prefix, no authority).
238+
* Leading/trailing slashes are insignificant. Returns true on a full match. */
239+
static bool cr_path_matches_template(const char *concrete, const char *templ) {
240+
const char *c = concrete;
241+
const char *t = templ;
242+
while (*c && *t) {
243+
if (*c == '/') {
244+
c++;
245+
}
246+
if (*t == '/') {
247+
t++;
248+
}
249+
const char *cseg = c;
250+
while (*c && *c != '/') {
251+
c++;
252+
}
253+
const char *tseg = t;
254+
while (*t && *t != '/') {
255+
t++;
256+
}
257+
size_t clen = (size_t)(c - cseg);
258+
size_t tlen = (size_t)(t - tseg);
259+
bool t_is_param = (tlen >= PAIR_LEN && tseg[0] == '{' && tseg[tlen - 1] == '}');
260+
if (!t_is_param) {
261+
if (clen != tlen || strncmp(cseg, tseg, clen) != 0) {
262+
return false;
263+
}
264+
} else if (clen == 0) {
265+
return false; /* a parameter never matches an empty segment */
266+
}
267+
}
268+
while (*c == '/') {
269+
c++;
270+
}
271+
while (*t == '/') {
272+
t++;
273+
}
274+
return *c == '\0' && *t == '\0';
275+
}
276+
277+
/* Fallback for when the exact route-QN lookup misses: a concrete client path
278+
* ("/v2/orders/123") never exact-matches a templated route QN
279+
* ("__route__GET__/v2/orders/{}"). Enumerate the target store's Route nodes
280+
* and segment-match the concrete path against each template. On a match, copy
281+
* the route QN into out_qn and return the handler id; returns 0 on no match.
282+
*
283+
* COST: this scans every Route node of the target project once per unmatched
284+
* HTTP_CALLS edge — O(calls × routes) per project pair. Acceptable while both
285+
* factors stay small (calls are capped at CR_MAX_EDGES and it only runs for
286+
* edges the exact lookup missed); revisit with a prepared template index if
287+
* cross-repo matching ever shows up in profiles. (#523) */
288+
static int64_t find_route_handler_fuzzy(cbm_store_t *target_store, const char *concrete_path,
289+
const char *method, char *out_qn, size_t out_qn_sz,
290+
char *handler_name, size_t name_sz, char *handler_file,
291+
size_t file_sz) {
292+
struct sqlite3 *db = cbm_store_get_db(target_store);
293+
if (!db || !concrete_path || !concrete_path[0]) {
294+
return 0;
295+
}
296+
sqlite3_stmt *s = NULL;
297+
if (sqlite3_prepare_v2(db, "SELECT qualified_name FROM nodes WHERE label = 'Route'",
298+
CBM_NOT_FOUND, &s, NULL) != SQLITE_OK) {
299+
return 0;
300+
}
301+
int64_t found = 0;
302+
while (sqlite3_step(s) == SQLITE_ROW) {
303+
const char *qn = (const char *)sqlite3_column_text(s, 0);
304+
if (!qn || strncmp(qn, "__route__", CR_ROUTE_PREFIX_LEN) != 0) {
305+
continue;
306+
}
307+
/* Split "__route__<METHOD>__<path>" */
308+
const char *rest = qn + CR_ROUTE_PREFIX_LEN;
309+
const char *sep = strstr(rest, "__");
310+
if (!sep) {
311+
continue;
312+
}
313+
size_t mlen = (size_t)(sep - rest);
314+
const char *rpath = sep + PAIR_LEN;
315+
/* Method gate: the route's method must equal the caller's, or be ANY.
316+
* A missing caller method matches any route method. */
317+
if (method && method[0]) {
318+
bool same_method = (strncmp(rest, method, mlen) == 0 && method[mlen] == '\0');
319+
bool route_any = (mlen == CR_ANY_LEN && strncmp(rest, "ANY", CR_ANY_LEN) == 0);
320+
if (!same_method && !route_any) {
321+
continue;
322+
}
323+
}
324+
if (!cr_path_matches_template(concrete_path, rpath)) {
325+
continue;
326+
}
327+
/* A concrete path can match more than one stored template (e.g. a raw
328+
* "{id}" variant and its canonical "{}" form). Only accept a Route that
329+
* actually has a HANDLES edge — the handler is attached to the
330+
* canonical node. Keep scanning otherwise. */
331+
int64_t hid =
332+
find_route_handler(target_store, qn, handler_name, name_sz, handler_file, file_sz);
333+
if (hid != 0) {
334+
snprintf(out_qn, out_qn_sz, "%s", qn);
335+
found = hid;
336+
break;
337+
}
338+
}
339+
sqlite3_finalize(s);
340+
return found;
341+
}
342+
211343
/* Emit CROSS_* edge for a route match: forward into source, reverse into target. */
212344
static void emit_cross_route_bidirectional(cbm_store_t *src_store, const char *src_project,
213345
struct sqlite3 *src_db, int64_t caller_id,
@@ -284,12 +416,12 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
284416
continue;
285417
}
286418

287-
/* Build the expected Route QN in the target project (param-canonicalized
288-
* so client url_path matches the server handler regardless of framework
289-
* placeholder syntax). */
419+
/* Build the expected Route QN in the target project (authority-stripped
420+
* and param-canonicalized so client url_path matches the server handler
421+
* regardless of base URL and framework placeholder syntax). */
290422
char route_qn[CR_QN_BUF];
291423
char cpath[CBM_SZ_256];
292-
const char *curl = cbm_route_canon_path(url_path, cpath, sizeof(cpath));
424+
const char *curl = cbm_route_canon_path(cr_url_path(url_path), cpath, sizeof(cpath));
293425
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method[0] ? method : "ANY", curl);
294426

295427
char handler_name[CBM_SZ_256] = {0};
@@ -303,6 +435,14 @@ static int match_http_routes(cbm_store_t *src_store, const char *src_project,
303435
handler_id = find_route_handler(tgt_store, route_qn, handler_name, sizeof(handler_name),
304436
handler_file, sizeof(handler_file));
305437
}
438+
if (handler_id == 0) {
439+
/* Exact QN lookup missed. A concrete client path ("/v2/orders/123")
440+
* never exact-matches a templated route ("/v2/orders/{}"), so fall
441+
* back to segment-wise template matching. (#523) */
442+
handler_id = find_route_handler_fuzzy(
443+
tgt_store, curl, method[0] ? method : NULL, route_qn, sizeof(route_qn),
444+
handler_name, sizeof(handler_name), handler_file, sizeof(handler_file));
445+
}
306446
if (handler_id == 0) {
307447
continue;
308448
}
@@ -655,6 +795,15 @@ cbm_cross_repo_result_t cbm_cross_repo_match(const char *project, const char **t
655795
}
656796

657797
result.http_edges += match_http_routes(src_store, project, tgt_store, tgt);
798+
/* Reverse direction: when this pass runs from the provider side, the
799+
* consumer's HTTP_CALLS live in tgt, not src — the forward pass above
800+
* finds nothing because the provider has no outbound calls. This also
801+
* re-creates the provider-side reverse edges that delete_cross_edges
802+
* just wiped, which a provider-side run previously destroyed for good.
803+
* A caller edge lives in exactly one DB, so the two directions scan
804+
* disjoint edge sets and never double-count a pair; the store's
805+
* (source, target, type) upsert keeps re-recorded rows unique. (#523) */
806+
result.http_edges += match_http_routes(tgt_store, tgt, src_store, project);
658807
result.async_edges += match_async_routes(src_store, project, tgt_store, tgt);
659808
result.channel_edges += match_channels(src_store, project, tgt_store, tgt);
660809
result.grpc_edges += match_typed_routes(src_store, project, tgt_store, tgt, "GRPC_CALLS",

0 commit comments

Comments
 (0)