Skip to content

Commit 8f5f2b6

Browse files
committed
Extract interface for RequestIdGenerator
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.
1 parent 2f0c7a4 commit 8f5f2b6

File tree

9 files changed

+323
-229
lines changed

9 files changed

+323
-229
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.service.tracing;
21+
22+
import io.smallrye.mutiny.Uni;
23+
import jakarta.annotation.Nonnull;
24+
import jakarta.enterprise.context.ApplicationScoped;
25+
import jakarta.ws.rs.container.ContainerRequestContext;
26+
import java.util.UUID;
27+
import java.util.concurrent.atomic.AtomicReference;
28+
29+
/**
30+
* Default implementation of {@link RequestIdGenerator}, striking a balance between randomness and
31+
* performance.
32+
*
33+
* <p>The IDs generated by this generator are of the form: {@code UUID_COUNTER}. The UUID part is
34+
* randomly generated at startup, and the counter is incremented for each request.
35+
*
36+
* <p>In the unlikely event that the counter overflows, a new UUID is generated and the counter is
37+
* reset to 1.
38+
*/
39+
@ApplicationScoped
40+
public class DefaultRequestIdGenerator implements RequestIdGenerator {
41+
42+
record RequestId(UUID uuid, long counter) {
43+
44+
RequestId() {
45+
this(UUID.randomUUID(), 1);
46+
}
47+
48+
@Override
49+
@Nonnull
50+
public String toString() {
51+
return String.format("%s_%019d", uuid(), counter());
52+
}
53+
54+
RequestId increment() {
55+
return counter == Long.MAX_VALUE ? new RequestId() : new RequestId(uuid, counter + 1);
56+
}
57+
}
58+
59+
final AtomicReference<RequestId> state = new AtomicReference<>(new RequestId());
60+
61+
@Override
62+
public Uni<String> generateRequestId(ContainerRequestContext requestContext) {
63+
return Uni.createFrom().item(nextRequestId().toString());
64+
}
65+
66+
RequestId nextRequestId() {
67+
return state.getAndUpdate(RequestId::increment);
68+
}
69+
}

runtime/service/src/main/java/org/apache/polaris/service/tracing/RequestIdFilter.java

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,62 @@
1818
*/
1919
package org.apache.polaris.service.tracing;
2020

21-
import jakarta.annotation.Priority;
22-
import jakarta.enterprise.context.ApplicationScoped;
21+
import io.smallrye.common.vertx.ContextLocals;
22+
import io.smallrye.mutiny.Uni;
2323
import jakarta.inject.Inject;
2424
import jakarta.ws.rs.container.ContainerRequestContext;
25-
import jakarta.ws.rs.container.ContainerRequestFilter;
26-
import jakarta.ws.rs.container.PreMatching;
27-
import jakarta.ws.rs.ext.Provider;
25+
import jakarta.ws.rs.container.ContainerResponseContext;
26+
import jakarta.ws.rs.core.MediaType;
27+
import jakarta.ws.rs.core.Response;
28+
import org.apache.iceberg.rest.responses.ErrorResponse;
2829
import org.apache.polaris.service.config.FilterPriorities;
2930
import org.apache.polaris.service.logging.LoggingConfiguration;
31+
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
32+
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
3035

31-
@PreMatching
32-
@ApplicationScoped
33-
@Priority(FilterPriorities.REQUEST_ID_FILTER)
34-
@Provider
35-
public class RequestIdFilter implements ContainerRequestFilter {
36+
public class RequestIdFilter {
3637

3738
public static final String REQUEST_ID_KEY = "requestId";
3839

40+
private static final Logger LOGGER = LoggerFactory.getLogger(RequestIdFilter.class);
41+
3942
@Inject LoggingConfiguration loggingConfiguration;
4043
@Inject RequestIdGenerator requestIdGenerator;
4144

42-
@Override
43-
public void filter(ContainerRequestContext rc) {
45+
@ServerRequestFilter(preMatching = true, priority = FilterPriorities.REQUEST_ID_FILTER)
46+
public Uni<Response> assignRequestId(ContainerRequestContext rc) {
4447
var requestId = rc.getHeaderString(loggingConfiguration.requestIdHeaderName());
45-
if (requestId == null) {
46-
requestId = requestIdGenerator.generateRequestId();
48+
return (requestId != null
49+
? Uni.createFrom().item(requestId)
50+
: requestIdGenerator.generateRequestId(rc))
51+
.onItem()
52+
.invoke(id -> rc.setProperty(REQUEST_ID_KEY, id))
53+
.invoke(id -> ContextLocals.put(REQUEST_ID_KEY, id))
54+
.onItemOrFailure()
55+
.transform((id, error) -> error == null ? null : errorResponse(error));
56+
}
57+
58+
@ServerResponseFilter
59+
public void addResponseHeader(
60+
ContainerRequestContext request, ContainerResponseContext response) {
61+
String requestId = (String) request.getProperty(REQUEST_ID_KEY);
62+
if (requestId != null) { // can be null if request ID generation fails
63+
response.getHeaders().add(loggingConfiguration.requestIdHeaderName(), requestId);
4764
}
48-
rc.setProperty(REQUEST_ID_KEY, requestId);
65+
}
66+
67+
private static Response errorResponse(Throwable error) {
68+
LOGGER.error("Failed to generate request ID", error);
69+
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
70+
.type(MediaType.APPLICATION_JSON_TYPE)
71+
.entity(
72+
ErrorResponse.builder()
73+
.responseCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
74+
.withMessage("Request ID generation failed")
75+
.withType("RequestIdGenerationError")
76+
.build())
77+
.build();
4978
}
5079
}

runtime/service/src/main/java/org/apache/polaris/service/tracing/RequestIdGenerator.java

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,21 @@
1919

2020
package org.apache.polaris.service.tracing;
2121

22-
import com.google.common.annotations.VisibleForTesting;
23-
import jakarta.enterprise.context.ApplicationScoped;
24-
import java.util.UUID;
25-
import java.util.concurrent.atomic.AtomicReference;
22+
import io.smallrye.mutiny.Uni;
23+
import jakarta.ws.rs.container.ContainerRequestContext;
2624

27-
@ApplicationScoped
28-
public class RequestIdGenerator {
29-
static final Long COUNTER_SOFT_MAX = Long.MAX_VALUE / 2;
30-
31-
record State(String uuid, long counter) {
32-
33-
State() {
34-
this(UUID.randomUUID().toString(), 1);
35-
}
36-
37-
String requestId() {
38-
return String.format("%s_%019d", uuid, counter);
39-
}
40-
41-
State increment() {
42-
return counter >= COUNTER_SOFT_MAX ? new State() : new State(uuid, counter + 1);
43-
}
44-
}
45-
46-
final AtomicReference<State> state = new AtomicReference<>(new State());
47-
48-
public String generateRequestId() {
49-
return state.getAndUpdate(State::increment).requestId();
50-
}
51-
52-
@VisibleForTesting
53-
public void setCounter(long counter) {
54-
state.set(new State(state.get().uuid, counter));
55-
}
25+
/**
26+
* A generator for request IDs.
27+
*
28+
* @see RequestIdFilter
29+
*/
30+
public interface RequestIdGenerator {
31+
32+
/**
33+
* Generates a new request ID. IDs must be fast to generate and unique. If the generation involves
34+
* I/O (which is not recommended), it should be performed asynchronously.
35+
*
36+
* @param requestContext The JAX-RS request context
37+
*/
38+
Uni<String> generateRequestId(ContainerRequestContext requestContext);
5639
}

runtime/service/src/main/java/org/apache/polaris/service/tracing/RequestIdResponseFilter.java

Lines changed: 0 additions & 46 deletions
This file was deleted.

runtime/service/src/main/java/org/apache/polaris/service/tracing/TracingFilter.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ public void filter(ContainerRequestContext rc) {
4747
if (!sdkDisabled) {
4848
Span span = Span.current();
4949
String requestId = (String) rc.getProperty(RequestIdFilter.REQUEST_ID_KEY);
50-
if (requestId != null) {
51-
span.setAttribute(REQUEST_ID_ATTRIBUTE, requestId);
52-
}
50+
span.setAttribute(REQUEST_ID_ATTRIBUTE, requestId);
5351
RealmContext realmContext =
5452
(RealmContext) rc.getProperty(RealmContextFilter.REALM_CONTEXT_KEY);
5553
span.setAttribute(REALM_ID_ATTRIBUTE, realmContext.getRealmIdentifier());
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.polaris.service.tracing;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
24+
import java.util.Set;
25+
import java.util.UUID;
26+
import java.util.concurrent.ConcurrentSkipListSet;
27+
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.Executors;
29+
import org.apache.polaris.service.tracing.DefaultRequestIdGenerator.RequestId;
30+
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Test;
32+
33+
public class DefaultRequestIdGeneratorTest {
34+
35+
private DefaultRequestIdGenerator requestIdGenerator;
36+
37+
@BeforeEach
38+
void setUp() {
39+
requestIdGenerator = new DefaultRequestIdGenerator();
40+
}
41+
42+
@Test
43+
void testGeneratesUniqueIds() {
44+
Set<String> generatedIds = new ConcurrentSkipListSet<>();
45+
try (ExecutorService executor = Executors.newFixedThreadPool(10)) {
46+
for (int i = 0; i < 1000; i++) {
47+
executor.execute(() -> generatedIds.add(requestIdGenerator.nextRequestId().toString()));
48+
}
49+
}
50+
assertThat(generatedIds).hasSize(1000);
51+
}
52+
53+
@Test
54+
void testCounterIncrementsSequentially() {
55+
assertThat(requestIdGenerator.nextRequestId().counter()).isEqualTo(1L);
56+
assertThat(requestIdGenerator.nextRequestId().counter()).isEqualTo(2L);
57+
assertThat(requestIdGenerator.nextRequestId().counter()).isEqualTo(3L);
58+
}
59+
60+
@Test
61+
void testCounterRotationAtMax() {
62+
requestIdGenerator.state.set(new RequestId(UUID.randomUUID(), Long.MAX_VALUE));
63+
64+
var beforeRotation = requestIdGenerator.nextRequestId();
65+
var afterRotation = requestIdGenerator.nextRequestId();
66+
67+
// The UUID should be different after rotation
68+
assertThat(beforeRotation.uuid()).isNotEqualTo(afterRotation.uuid());
69+
70+
// The counter should be reset to 1 after rotation
71+
assertThat(beforeRotation.counter()).isEqualTo(Long.MAX_VALUE);
72+
assertThat(afterRotation.counter()).isEqualTo(1L);
73+
}
74+
75+
@Test
76+
void testRequestIdToString() {
77+
var uuid = UUID.randomUUID();
78+
assertThat(new RequestId(uuid, 1L).toString()).isEqualTo(uuid + "_0000000000000000001");
79+
assertThat(new RequestId(uuid, 12345L).toString()).isEqualTo(uuid + "_0000000000000012345");
80+
assertThat(new RequestId(uuid, Long.MAX_VALUE).toString())
81+
.isEqualTo(uuid + "_9223372036854775807");
82+
}
83+
}

0 commit comments

Comments
 (0)