Skip to content

Commit 2d21778

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

File tree

8 files changed

+224
-230
lines changed

8 files changed

+224
-230
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 jakarta.annotation.Nonnull;
23+
import jakarta.enterprise.context.ApplicationScoped;
24+
import java.util.UUID;
25+
import java.util.concurrent.CompletableFuture;
26+
import java.util.concurrent.CompletionStage;
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+
@ApplicationScoped
37+
public class DefaultRequestIdGenerator implements RequestIdGenerator {
38+
39+
record RequestId(UUID uuid, long counter) {
40+
41+
RequestId() {
42+
this(UUID.randomUUID(), 1);
43+
}
44+
45+
@Override
46+
@Nonnull
47+
public String toString() {
48+
return String.format("%s_%019d", uuid(), counter());
49+
}
50+
51+
RequestId increment() {
52+
return counter == Long.MAX_VALUE ? new RequestId() : new RequestId(uuid, counter + 1);
53+
}
54+
}
55+
56+
final AtomicReference<RequestId> state = new AtomicReference<>(new RequestId());
57+
58+
@Override
59+
public CompletionStage<String> generateRequestId() {
60+
return CompletableFuture.completedFuture(nextRequestId().toString());
61+
}
62+
63+
RequestId nextRequestId() {
64+
return state.getAndUpdate(RequestId::increment);
65+
}
66+
}

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

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,71 @@
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 java.util.concurrent.CompletableFuture;
29+
import org.apache.iceberg.rest.responses.ErrorResponse;
2830
import org.apache.polaris.service.config.FilterPriorities;
2931
import org.apache.polaris.service.logging.LoggingConfiguration;
32+
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
33+
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
3036

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

3739
public static final String REQUEST_ID_KEY = "requestId";
3840

41+
private static final Logger LOGGER = LoggerFactory.getLogger(RequestIdFilter.class);
42+
3943
@Inject LoggingConfiguration loggingConfiguration;
4044
@Inject RequestIdGenerator requestIdGenerator;
4145

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

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

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,18 @@
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 java.util.concurrent.CompletionStage;
2623

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-
}
24+
/**
25+
* A generator for request IDs.
26+
*
27+
* @see RequestIdFilter
28+
*/
29+
public interface RequestIdGenerator {
5130

52-
@VisibleForTesting
53-
public void setCounter(long counter) {
54-
state.set(new State(state.get().uuid, counter));
55-
}
31+
/**
32+
* Generates a new request ID. IDs must be fast to generate and unique. If the generation involves
33+
* I/O (which is not recommended), it should be performed asynchronously.
34+
*/
35+
CompletionStage<String> generateRequestId();
5636
}

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)