cmcfarlen commented on PR #13537:
URL: https://github.com/apache/trafficserver/pull/13537#issuecomment-5260522750

   The design is right and the sharing boundary is drawn in the correct place. 
Since "what may be shared" is the whole question here, I verified that side 
rather than just reading it.
   
   **The regression premise checks out, and it is in the shipped 10.2.0.** 
#12685 ("Remove PCRE references, complete migration to PCRE2") is on both 
master and 10.2.x, and `Regex::compile()` unconditionally runs 
`pcre2_jit_compile(code, PCRE2_JIT_COMPLETE)` (`Regex.cc:446`). So every 
instance really did pay a full JIT compile per rule, and reload cost really did 
scale with mapping count rather than unique rule files.
   
   **Sharing the compiled rules is safe, and the match-context change was not 
optional.** Previously `RemapRegex` held `RegexMatchContext const 
*_match_context` pointing at one `RemapInstance`'s context. Once a `RemapRegex` 
is shared by every instance that loaded the same file, that member becomes 
flatly wrong — whichever instance compiled it last would own the pointer 
everyone uses. Passing the context as an argument is the necessary fix, not a 
stylistic one, and the class comment spelling out "nothing per-instance or 
per-transaction may be stored here" is the kind of note that will actually 
prevent the next regression.
   
   I also confirmed the concurrent read is safe rather than assuming it. 
`RegexMatchContext` owns only a bare `pcre2_match_context` for the match limit 
— no JIT stack. JIT stacks live exclusively in `RegexContext`, which is 
`thread_local RegexContext ctx` (`Regex.cc:89`), so each ET_NET thread has its 
own. That matters because a PCRE2 JIT stack must not be used by two threads at 
once; if `RegexMatchContext` had carried one, the pre-existing per-instance 
sharing would already have been a problem and this patch would have widened it. 
It doesn't, so a `pcre2_match_context` read concurrently is fine, and the 
compiled `pcre2_code` is documented as safe for concurrent matching.
   
   **The weak ownership does what the comment claims.** The cache stores 
`std::weak_ptr<RuleSet const>` and never extends a lifetime, `entry.lock()` 
cannot hand back an expired generation so the hit path needs no separate 
liveness check, and pruning is genuinely just bookkeeping. Compiling under the 
lock is the right trade: it serializes duplicate work instead of racing several 
instances into compiling the same source.
   
   **One thing I would change.** `rule_hits` is sized only when profiling is on:
   
   ```cpp
   if (ri->profile) {
     ri->rule_hits.resize(ri->rule_set->rules().size());
   }
   ```
   
   and indexed only when profiling is on:
   
   ```cpp
   if (ri->profile) {
     ink_atomic_increment(&(ri->rule_hits[rule_ix]), 1);
   ```
   
   That is correct today — `profile` is set during argument parsing and never 
changes afterward, so the two guards cannot disagree. But the invariant is 
implicit and split across `TSRemapNewInstance` and `TSRemapDoRemap`, and the 
failure mode if it is ever broken is not a null check away: 
`ink_atomic_increment` on `&rule_hits[ix]` of an empty vector is an 
out-of-bounds atomic write on the heap, from every ET_NET thread, on every 
matching request. Given the class comment already warns the next person to add 
per-instance state "indexed in lockstep with `RuleSet::rules()`", it seems 
worth making that unconditional — resizing always costs a handful of ints per 
instance, and it removes the coupling entirely. Failing that, an assert on the 
size next to the increment would at least make a debug build say so.
   
   Smaller notes, none blocking:
   
   - The cache mutex is a single global one, so a compile of file A blocks a 
compile of file B. Remap loading is single-threaded, so this is uncontended in 
practice and costs nothing — but that also means the "concurrent instances of 
the same source must serialize here" comment describes a case that may not 
currently arise. Harmless either way; just do not count on it for parallelism 
if remap loading is ever parallelized.
   - The key is the resolved filename (absolute, or prefixed with 
`TSConfigDirGet()`), but not canonicalized: a symlink, a `..`, or a `./` prefix 
yields a distinct key. Because the content is compared too, the only 
consequence is a missed share and one redundant compile, never a wrong rule 
set. Fine as-is; worth knowing it is path-string equality rather than same-file 
identity.
   - The `stat()` / read pair handles the TOCTOU correctly by comparing 
`gcount()` against the stat size, so a file rewritten between the two is an 
error rather than a truncated rule set. Good.
   
   **On the test.** Asserting via `grep -c` on the `regex_remap` debug output 
is brittle in the usual ways — it depends on debug tags staying enabled and on 
the exact `Dbg` wording — and the file is honest about the first of those. 
Given there is no metric or RPC exposing cache hits, I do not have a better 
suggestion, and checking both a shared and a deliberately isolated rule file 
plus a post-reload generation is the right set of cases. Just be aware that 
rewording either debug line silently turns these assertions into no-ops that 
still pass the redirect checks.
   
   **Backport.** This is already tracked for 10.2.x, and it should pick 
cleanly: the new test uses `Test.AddConfigReload(..., 
expect_tasks=["remap.config"])` and the await-file helper, both of which are on 
10.2.x (the former via #13502). Since the regression shipped in rc0, landing it 
there seems right.
   
   Nothing blocking from me.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to