Skip to content

Conversation

@adnanhemani
Copy link
Contributor

This PR makes two changes:

  1. Generate a Request ID for all incoming requests. It will prioritize using a Request ID specified by the incoming request - but if one is not specified, then a new one is generated. The Request ID is therefore always available in the ContainerRequestContext downstream.
  2. Creates a new ContainerResponseFilter where the Request ID is added as a header to all responses.

I'm having some trouble with the naming of the classes, please do help suggest better names!

@github-project-automation github-project-automation bot moved this to PRs In Progress in Basic Kanban Board Sep 18, 2025
@adnanhemani adnanhemani marked this pull request as ready for review September 18, 2025 08:00
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
MDC.put(REQUEST_ID_KEY, requestId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm admittedly nit-picking a bit 😅 but I'd argue that the MDC.put statement should stay in LoggingMDCFilter since it's about logging.

What should be moved here is just the rc.setProperty call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can agree to that :) Will adjust in this revision.


@Provider
public class RequestIdResponseFilter implements ContainerResponseFilter {
private static final String REQUEST_ID_HEADER = "X-Request-Id";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The X- prefix is deprecated, FYI. See:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers

But more importantly, why not use the header name used for incoming requests, Polaris-Request-Id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL "X-" is deprecated! Thanks!

We can - I thought this was more of a standardized name prior. Let me change this.

return Map.of(
"polaris.log.request-id-header-name",
REQUEST_ID_HEADER,
"polaris.bootstrap.credentials",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look right. There is no Quarkus configuration named polaris.bootstrap.credentials.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's odd - I found it from a different test and the README as well.

I've removed it and the test still works 🤔 I can investigate this later as this is not the point of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README is correct, but the other test is wrong indeed.

adutra
adutra previously approved these changes Sep 22, 2025
@github-project-automation github-project-automation bot moved this from PRs In Progress to Ready to merge in Basic Kanban Board Sep 22, 2025
public void filter(ContainerRequestContext rc) {
var requestId = rc.getHeaderString(loggingConfiguration.requestIdHeaderName());
if (requestId == null) {
requestId = UUID.randomUUID().toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is exhausting the randomness pool a concern? @snazy : WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Also: UUID.randomUUID() is in theory a blocking call (happens only when the entropy source is empty though), and therefore should be avoided in filters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the request ID does not have to be a UUID (I'm a bit out-of-date on this) it may be worth using a per-node (or per-thread) UUID (allocated one per restart) plus a simple counter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions:

  1. Use org.apache.polaris.ids.impl.SnowflakeIdGeneratorImpl#idToTimeUuid
  2. Use a simple AtomicLong counter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also UUID v7 may be worth considering (optional, for follow-up): https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7

Copy link
Contributor Author

@adnanhemani adnanhemani Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the RequestIdGenerator, which is a very, very simple ID generator - since we don't require the level of complexity that has been made in the SnowflakeIdGenerator. It's just a quick implementation of @dimas-b's suggestion above:

If the request ID does not have to be a UUID (I'm a bit out-of-date on this) it may be worth using a per-node (or per-thread) UUID (allocated one per restart) plus a simple counter.

I'm very hesitant to introduce a complete dependency on the NoSql Persistence for the Service module and so creating the RequestIdGenerator is my minor effort to avoid doing that. I'm sure there may be a better reason for introducing this dependency in the future, but I don't want to sidetrack the simple goal (and simple requirements) of this PR with whether we should introduce this dependency or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure that UUID.randomUUID() is blocking call and in what chance will it exhaust the system entropy?

I've never got a satisfying answer for that question 😄

Most commenters stress the fact that dev/urandom never blocks.

This is certainly true, but doesn't address the fact that the entropy pool might get exhausted at some point, in which case your UUIDs will have very poor randomness.

Furthermore, the UUID.randomUUID() call is explicitly considered blocking by BlockHound:

reactor/BlockHound#157

I think that this is due to the fact that new SecureRandom() does some magic to select the random numbers provider, and even if the default provider doesn't block (it plugs into /dev/urandom), you still can configure your JVM with a different provider that could block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here’s a post that shows how to work around BlockHound: https://stackoverflow.com/a/75687886/933856.

That said, we probably shouldn’t make decisions based on a single anecdote or assumptions about JVM configurations. What if a user runs Polaris on their own JVM and it breaks? That scenario is very likely. And what if a future JVM introduces a breaking change?

Do we need to worry about that now? Probably not. It feels like a premature optimization at this stage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quarkus doesn't use BlockHound – I cited BlockHound as an example. But Quarkus does have a mechanism to detect blocking calls in non-blocking contexts, we already had the problem with RealmContextFilter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I posted that link to demonstrate that people disagree with BlockHound on whether it is blocking call. Does Quarkus consider also UUID.randomUUID() as a blocking-call? If not, we got another reason to optimize it later.


@Test
public void testRequestIdHeaderNotSpecifiedAndCounterExhausted() {
requestIdGenerator.setCounter(Long.MAX_VALUE / 2 + 1);
Copy link
Contributor

@adutra adutra Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is actually testing the internals of RequestIdGenerator. I'd prefer to move this to a new RequestIdGeneratorTest class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the next revision!

assertThat(currentRequestIdBase).isNotEqualTo(uuidBase);
}

private boolean isValidDefaultUUID(String str) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I'm not sure we need to verify that request IDs have any particular structure. For all intents and purposes, the ID is an opaque string and it doesn't matter how it's generated, as long as it is unique.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it - I'm still testing the format in RequestIdGenerator (just to make sure that what we expect to happen is still happening), but removed it from here.

@Priority(FilterPriorities.REQUEST_ID_FILTER)
@Provider
public class RequestIdFilter implements ContainerRequestFilter {
@Inject RequestIdGenerator requestIdGenerator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this line down to line 40


@ApplicationScoped
public class RequestIdGenerator {
private static String BASE_DEFAULT_UUID = UUID.randomUUID().toString();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static String BASE_DEFAULT_UUID = UUID.randomUUID().toString();
private static String baseDefaultUuid = UUID.randomUUID().toString();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, thanks!


public String generateRequestId() {
String requestId = BASE_DEFAULT_UUID + "_" + COUNTER.incrementAndGet();
if (COUNTER.get() >= COUNTER_SOFT_MAX) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many issues here:

  1. COUNTER.get() will not necessarily return what COUNTER.incrementAndGet() returned in the previous line.
  2. Many threads may enter this if block in parallel, and will all try to assign the BASE_DEFAULT_UUID field.
  3. BASE_DEFAULT_UUID is not volatile nor atomic, so the value write on line 38 may not be immediately visible to other threads.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you are right and I'm embarrassed that I didn't catch these. I've fixed this in the next revision using an AtomicBoolean as a way to track whether a reset is in progress. Please let me know if I've missed anything else.

Copy link
Contributor Author

@adnanhemani adnanhemani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responded to comments from @adutra . Please let me know if there is anything else!


@Test
public void testRequestIdHeaderNotSpecifiedAndCounterExhausted() {
requestIdGenerator.setCounter(Long.MAX_VALUE / 2 + 1);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the next revision!

assertThat(currentRequestIdBase).isNotEqualTo(uuidBase);
}

private boolean isValidDefaultUUID(String str) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it - I'm still testing the format in RequestIdGenerator (just to make sure that what we expect to happen is still happening), but removed it from here.


@ApplicationScoped
public class RequestIdGenerator {
private static String BASE_DEFAULT_UUID = UUID.randomUUID().toString();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL, thanks!


public String generateRequestId() {
String requestId = BASE_DEFAULT_UUID + "_" + COUNTER.incrementAndGet();
if (COUNTER.get() >= COUNTER_SOFT_MAX) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, you are right and I'm embarrassed that I didn't catch these. I've fixed this in the next revision using an AtomicBoolean as a way to track whether a reset is in progress. Please let me know if I've missed anything else.

import jakarta.ws.rs.Priorities;

public final class FilterPriorities {
public static final int REQUEST_ID_FILTER = Priorities.AUTHENTICATION - 101;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: -> REALM_CONTEXT_FILTER - 1 to make it more readable?

flyrain
flyrain previously approved these changes Sep 26, 2025
Copy link
Contributor

@flyrain flyrain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

flyrain
flyrain previously approved these changes Sep 26, 2025
Copy link
Contributor

@flyrain flyrain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 Thanks @adnanhemani for the change!

}

State increment() {
return counter >= COUNTER_SOFT_MAX ? new State() : new State(uuid, counter + 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why COUNTER_SOFT_MAX ? There is no risk of overflowing here.


@Test
void testCounterIncrementsSequentially() {
// requestIdGenerator.setCounter(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: dangling comment.

}

@Test
void testSetCounterChangesNextGeneratedId() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this test has any value, setCounter is not meant to be called outside of tests, and besides, I think we can remove that method.

}

@VisibleForTesting
public void setCounter(long counter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this method, RequestIdGenerator.state is already package-private.

}

@Test
void testGenerateRequestId_ReturnsUniqueIds() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test could be made multi-threaded.

@adutra
Copy link
Contributor

adutra commented Sep 30, 2025

In order to move things forward, and since @flyrain already approved, I'm going to merge this PR now even if my latest comments are still open. I will address them myself in a follow-up PR.

@adutra adutra merged commit 2f0c7a4 into apache:main Sep 30, 2025
14 checks passed
@github-project-automation github-project-automation bot moved this from Ready to merge to Done in Basic Kanban Board Sep 30, 2025
adutra added a commit to adutra/polaris that referenced this pull request Sep 30, 2025
Summary of changes:

1. Extracted an interface from `RequestIdGenerator`.
2. The `generateRequestId` method now returns a `CompletionStage<String>` in case custom implementations need to perform I/O or other blocking calls during request ID generation.
3. Also addressed comments in apache#2602.
adutra added a commit to adutra/polaris that referenced this pull request Sep 30, 2025
Summary of changes:

1. Extracted an interface from `RequestIdGenerator`.
2. The `generateRequestId` method now returns a `Uni<String>` in case custom implementations need to perform I/O or other blocking calls during request ID generation.
3. Also addressed comments in apache#2602.
adutra added a commit to adutra/polaris that referenced this pull request Sep 30, 2025
Summary of changes:

1. Extracted an interface from `RequestIdGenerator`.
2. The `generateRequestId` method now returns a `Uni<String>` in case custom implementations need to perform I/O or other blocking calls during request ID generation.
3. Also addressed comments in apache#2602.
adutra added a commit that referenced this pull request Oct 3, 2025
Summary of changes:

1. Extracted an interface from `RequestIdGenerator`.
2. The `generateRequestId` method now returns a `Uni<String>` in case custom implementations need to perform I/O or other blocking calls during request ID generation.
3. Also addressed comments in #2602.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants