Chris and Mark, On Wed, Sep 25, 2024 at 7:47 AM Christopher Schultz < ch...@christopherschultz.net> wrote:
> Igal and Mark, > > On 9/24/24 13:55, Igal Sapir wrote: > > Mark, > > > > > >> On Mon, Sep 23, 2024 at 12:25 AM Mark Thomas <ma...@apache.org> wrote: > >> > >>> On 23/09/2024 04:28, Igal Sapir wrote: > >>>> Hello, > >>>> > >>>> The current implementation of getRequestId() is optimized for speed > and > >>>> generates IDs that are unique to a running instance of Tomcat. > >>>> > >>>> But most server configurations nowadays require uniqueness across the > >>> whole > >>>> system, and currently we do not offer that as: > >>>> > >>>> 1. Request IDs are only unique to a running Tomcat instance > >>>> > >>>> 2. Request IDs are reset to 0 each time Tomcat is restarted > >>>> > >>>> 3. Request IDs are sometimes generated by another system like a load > >>>> balancer or reverse proxy, and passed around via the HTTP header > >>>> "X-Request-Id" > >>>> > >>>> I want to propose a patch that would: > >>>> > >>>> 1. Check for HTTP header "X-Request-Id" and if valid (e.g. does not > >>> attempt > >>>> SQL or XSS injection etc.) returns it > >>> > >>> > >>> That is behaviour we'd typically place in a Valve or Filter. Possibly > an > >>> extension to the RemoteIp[Valve|Filter] ? > >>> > >>> Rather than us validate it, I'd make processing it optional and the > >>> admins responsibility to ensure it is trusted if they opt to process > it. > >>> > >> > > Yes, that makes sense. I can add that part to the RemoteIp[Valve|Filter] > > as long as we can add a Setter for requestId that can be called from the > > filter/valve. > > I could have sworn this feature already existed. Rainer added support > for httpd-generated unique request identifiers to mod_jk a while back > and maybe I just assumed that he also added the code in Tomcat to accept > them and wire them into getRequestId. > > >>>> 2. Generates a URL-safe Base64-encoded UUID (22 CaSe sensitive > >>> characters) > >>> > >>> How expensive is that process compared to the existing mechanism? > >>> > >> > > I will run some benchmarks to find out. While it would almost certainly > be > > more expensive than AtomicLong.incrementAndGet(), I would think that the > > value that it adds can be acceptable up to some arbitrary threshold of > > per-request overhead, e.g. 1ms is too much but 50us might be acceptable. > > > > We could also make it lazy-init so that it is only processed when > > getRequestId() is called the first time per Request (though I would want > it > > in the Logs so every request would trigger it in that case), and possibly > > and opt-in and pluggable implementation according to the next point > below. > > We could also generate a "proper" UUID on startup, discard a portion of > it, and then increment a counter on each request. OR maybe not even > bother discarding anything. Just a huge integer value that we begin > randomly and then increment from there. > > I'd like to see the performance numbers, of course. > I now have the performance numbers to show. I tested 4 implementations on my laptop, which has a CPU from late 2015 with 4 cores and 2 threads each, i.e. 8 CPU threads: Intel(R) Xeon(R) CPU E3-1505M v5 @ 2.80GHz. The implementations are: 1. The current implementation, i.e. AtomicLong as Hex string 2. UUIDv4 via java.util.UUID.randomUUID(), e.g. "3f31da8f-d5ec-4409-ba26-516793cb676d" 3. UUIDv4 Short, i.e. Base64 URL Encode of option 2 above, e.g. "PNY3MaFjSROh4Sxytzg76A" 4. UUIDv7 via f4b6a3/uuid-creator [2], e.g. "01922cab-468f-77a9-b4e8-2c242d53f47d", which offers chronological order but requires a dependency (only 56KB though) and adds some risk as it is less tested than the JDK implementations As expected, options 2, 3, and 4 are 15x - 20x compared to option 1: Benchmark Mode Cnt Score Error Units 1. BenchmarkIdGenerator.benchAtomicLong avgt 5 21.698 ± 0.227 ns/op 2. BenchmarkIdGenerator.benchmarkUuidV4 avgt 5 398.252 ± 13.169 ns/op 3. BenchmarkIdGenerator.benchmarkUuidV4Short avgt 5 417.014 ± 14.663 ns/op 4. BenchmarkIdGenerator.benchmarkUuidV7 avgt 5 279.479 ± 3.570 ns/op But as can be seen above (all times are in nanoseconds), so even option 3, which is the "slowest", takes less than half a microsecond. > > I like the idea of lazy work, here. Since this is a new feature in the > spec, probably 0% of applications are using it right now. Why bother > wasting CPU cycles generating request identifiers for applications that > aren't even using them? > > >>>> The value will be set to the requestId private variable to ensure > >>>> consistent return value for multiple calls on the same Request. > >>>> > >>>> I have the code ready, but wanted to discuss the matter here first. > >>> > >>> The Servlet spec requires only that the ID is unique for the lifetime > of > >>> the container. > >>> > >>> How will this interact with ServletRequest.getRequestId() and the > >>> associated methods? > >>> > >> > > My idea is that ServletRequest.getRequestId() would still delegate calls > to > > Coyote's getRequestId(), so the new implementation would be used for it. > > > > > >> > >>> Should we make the request ID generator a pluggable component? If so, > of > >>> what? > >>> > >> > > That would be great as it would allow us to keep the current behavior as > > default, for example, make this enhancement opt-in, and allow for future > > implementations e.g. UUIDv7 which allows for natural order sorting. > > I like plugability, and then we can offer at least two implementations: > the naive one we have now which guarantees uniqueness within a single > JVM and another one which guarantees universal uniqueness[1] but likely > has a bit of a performance penalty. > Agreed. I still like the pluggability idea which would allow the user to choose the best implementation for them. > Which component is a great question. On one hand I'm thinking that Coyote > > is part of the Connectors, but on the other hand we might want to allow > > admins to configure different behavior for different Hosts or Contexts? > > > > I personally feel that running multiple Hosts or Contexts in a single > > Tomcat deployment is something that was more valuable in the past when > > compute resources were much more expensive than they are today. Nowadays > > it is easy to deploy Tomcat in a container like Docker and map the hosts > > and ports as needed, and I rarely find myself needing to have different > > configurations for different Hosts or Contexts. > > Speaking of hosts... since an engine probably already has a jvmRoute > defined on it, maybe we could use that to provide cross-cluster > uniqueness. You could have jvmRoute + random seed + counter = request id > and that should be unique across your cluster. > To take both of your ideas another step in that direction, a default "fast" implementation which can also be globally unique can be made of a Long value coming from an AtomicLong, prefixed by a the time when the class was loaded, e.g.: static String idPrefix = Long.toHexString( System.nanoTime() ); String requestId = idPrefix + "-" + Long.toHexString( atomicLong.nextLong() ); This would provide a fast result, whereas another implementation like option 3 above could provide a fixed width result, e.g. when you want to store it in a database, at the expense of some CPU cycles. We can make this fast one kind-of "fixed width" by initializing the AtomicLong to some high value rather than 0. Igal [2] https://github.com/f4b6a3/uuid-creator/wiki/1.7.-UUIDv7 > > -chris > > [1] Of course, UUIDs are not actually guaranteed to be unique. Just > really really REALLY unlikely to experience colissions. >