Skip to content

Commit 1ca592c

Browse files
Add optional total_count for config service api's (#288)
* Add generic api's for filtering, pagination in getAllConfigs call
1 parent 68982ae commit 1ca592c

9 files changed

Lines changed: 161 additions & 36 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.hypertrace.config.objectstore;
2+
3+
import java.util.List;
4+
5+
public interface ConfigsResponse<T> {
6+
List<T> getContextualConfigObjects();
7+
8+
long totalCount();
9+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.hypertrace.config.objectstore;
2+
3+
import java.util.List;
4+
5+
class ConfigsResponseImpl<T> implements ConfigsResponse<T> {
6+
private final List<T> contextualConfigObjects;
7+
private final long totalCount;
8+
9+
public ConfigsResponseImpl(List<T> contextualConfigObjects, long totalCount) {
10+
this.contextualConfigObjects = contextualConfigObjects;
11+
this.totalCount = totalCount;
12+
}
13+
14+
@Override
15+
public List<T> getContextualConfigObjects() {
16+
return contextualConfigObjects;
17+
}
18+
19+
@Override
20+
public long totalCount() {
21+
return totalCount;
22+
}
23+
}

config-object-store/src/main/java/org/hypertrace/config/objectstore/IdentifiedFilterPushedDownObjectStore.java

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.hypertrace.config.service.v1.ConfigServiceGrpc;
1010
import org.hypertrace.config.service.v1.Filter;
1111
import org.hypertrace.config.service.v1.GetAllConfigsRequest;
12+
import org.hypertrace.config.service.v1.GetAllConfigsResponse;
1213
import org.hypertrace.config.service.v1.Pagination;
1314
import org.hypertrace.config.service.v1.SortBy;
1415
import org.hypertrace.core.grpcutils.context.RequestContext;
@@ -39,9 +40,9 @@ protected IdentifiedFilterPushedDownObjectStore(
3940

4041
public List<ContextualConfigObject<T>> getMatchingObjects(
4142
RequestContext context, F filterInput, List<S> sortInput, @Nullable Pagination pagination) {
42-
Filter filter = buildFilter(filterInput);
43-
List<SortBy> sortByList = sortInput.stream().map(this::buildSort).collect(Collectors.toList());
44-
return getMatchingObjects(context, filter, sortByList, pagination);
43+
ConfigsResponse<ContextualConfigObject<T>> configsResponse =
44+
getMatchingObjects(context, filterInput, sortInput, pagination, false);
45+
return configsResponse.getContextualConfigObjects();
4546
}
4647

4748
public List<ContextualConfigObject<T>> getMatchingObjects(
@@ -71,28 +72,54 @@ public Optional<T> getMatchingData(RequestContext context, F filterInput, List<S
7172
return getMatchingObject(context, filterInput, sortInput).map(ConfigObject::getData);
7273
}
7374

74-
List<ContextualConfigObject<T>> getMatchingObjects(
75-
RequestContext context, Filter filter, List<SortBy> sortByList, Pagination pagination) {
76-
return context
77-
.call(
75+
public ConfigsResponse<ContextualConfigObject<T>> getMatchingObjectsWithTotalCount(
76+
RequestContext context, F filterInput, List<S> sortInput, @Nullable Pagination pagination) {
77+
return getMatchingObjects(context, filterInput, sortInput, pagination, true);
78+
}
79+
80+
public ConfigsResponse<T> getMatchingDataWithTotalCount(
81+
RequestContext context, F filterInput, List<S> sortInput, @Nullable Pagination pagination) {
82+
ConfigsResponse<ContextualConfigObject<T>> configsResponse =
83+
getMatchingObjects(context, filterInput, sortInput, pagination, true);
84+
return new ConfigsResponseImpl<>(
85+
configsResponse.getContextualConfigObjects().stream()
86+
.map(ConfigObject::getData)
87+
.collect(Collectors.toUnmodifiableList()),
88+
configsResponse.totalCount());
89+
}
90+
91+
ConfigsResponse<ContextualConfigObject<T>> getMatchingObjects(
92+
RequestContext context,
93+
F filterInput,
94+
List<S> sortInput,
95+
Pagination pagination,
96+
boolean totalIncluded) {
97+
Filter filter = buildFilter(filterInput);
98+
List<SortBy> sortByList = sortInput.stream().map(this::buildSort).collect(Collectors.toList());
99+
GetAllConfigsResponse getConfigsResponse =
100+
context.call(
78101
() ->
79102
this.configServiceBlockingStub
80103
.withDeadline(getDeadline())
81-
.getAllConfigs(buildGetAllConfigsRequest(filter, sortByList, pagination)))
82-
.getContextSpecificConfigsList()
83-
.stream()
84-
.map(
85-
contextSpecificConfig ->
86-
ContextualConfigObjectImpl.tryBuild(
87-
contextSpecificConfig, this::buildDataFromValue))
88-
.flatMap(Optional::stream)
89-
.collect(
90-
Collectors.collectingAndThen(
91-
Collectors.toUnmodifiableList(), this::orderFetchedObjects));
104+
.getAllConfigs(
105+
buildGetAllConfigsRequest(filter, sortByList, pagination, totalIncluded)));
106+
107+
List<ContextualConfigObject<T>> contextualConfigObjectsList =
108+
getConfigsResponse.getContextSpecificConfigsList().stream()
109+
.map(
110+
contextSpecificConfig ->
111+
ContextualConfigObjectImpl.tryBuild(
112+
contextSpecificConfig, this::buildDataFromValue))
113+
.flatMap(Optional::stream)
114+
.collect(
115+
Collectors.collectingAndThen(
116+
Collectors.toUnmodifiableList(), this::orderFetchedObjects));
117+
return new ConfigsResponseImpl<>(
118+
contextualConfigObjectsList, getConfigsResponse.getTotalCount());
92119
}
93120

94121
private GetAllConfigsRequest buildGetAllConfigsRequest(
95-
Filter filter, List<SortBy> sortByList, Pagination pagination) {
122+
Filter filter, List<SortBy> sortByList, Pagination pagination, boolean totalIncluded) {
96123
GetAllConfigsRequest.Builder getAllConfigsRequest =
97124
GetAllConfigsRequest.newBuilder()
98125
.setResourceName(this.resourceName)
@@ -102,6 +129,9 @@ private GetAllConfigsRequest buildGetAllConfigsRequest(
102129
if (pagination != null) {
103130
getAllConfigsRequest.setPagination(pagination);
104131
}
132+
if (totalIncluded) {
133+
getAllConfigsRequest.setIncludeTotal(true);
134+
}
105135
return getAllConfigsRequest.build();
106136
}
107137

config-service-api/src/main/proto/org/hypertrace/config/service/v1/config_service.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,17 @@ message GetAllConfigsRequest {
100100
// optional - pagination parameters to limit and offset the result set.
101101
// Useful for retrieving configs in pages when total count is large.
102102
Pagination pagination = 5;
103+
104+
// optional - include total count in the response
105+
bool include_total = 6;
103106
}
104107

105108
message GetAllConfigsResponse {
106109
// list of config values along with the associated contexts, sorted in the descending order of their creation time
107110
repeated ContextSpecificConfig context_specific_configs = 1;
111+
112+
// Total number of records matching the filter before pagination if include_total is true.
113+
optional int64 total_count = 2;
108114
}
109115

110116
message ContextSpecificConfig {

config-service-impl/src/main/java/org/hypertrace/config/service/ConfigServiceGrpcImpl.java

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.hypertrace.config.service.ConfigServiceUtils.merge;
66

77
import com.google.common.collect.ImmutableMap;
8+
import com.google.common.util.concurrent.ThreadFactoryBuilder;
89
import com.google.protobuf.Value;
910
import io.grpc.Status;
1011
import io.grpc.stub.StreamObserver;
@@ -13,6 +14,9 @@
1314
import java.util.Map.Entry;
1415
import java.util.Optional;
1516
import java.util.Set;
17+
import java.util.concurrent.CompletableFuture;
18+
import java.util.concurrent.Executor;
19+
import java.util.concurrent.Executors;
1620
import java.util.stream.Collectors;
1721
import lombok.extern.slf4j.Slf4j;
1822
import org.hypertrace.config.service.store.ConfigStore;
@@ -42,6 +46,10 @@ public class ConfigServiceGrpcImpl extends ConfigServiceGrpc.ConfigServiceImplBa
4246
private static String DEFAULT_USER_ID = "Unknown";
4347
private static String DEFAULT_USER_EMAIL = "Unknown";
4448
private final ConfigStore configStore;
49+
private static final Executor configExecutor =
50+
Executors.newFixedThreadPool(
51+
4,
52+
new ThreadFactoryBuilder().setNameFormat("config-executor-%d").setDaemon(true).build());
4553

4654
public ConfigServiceGrpcImpl(ConfigStore configStore) {
4755
this.configStore = configStore;
@@ -115,21 +123,51 @@ public void getConfig(
115123
public void getAllConfigs(
116124
GetAllConfigsRequest request, StreamObserver<GetAllConfigsResponse> responseObserver) {
117125
try {
118-
List<ContextSpecificConfig> contextSpecificConfigList =
126+
ConfigResource configResource =
127+
new ConfigResource(
128+
request.getResourceName(), request.getResourceNamespace(), getTenantId());
129+
130+
CompletableFuture<Long> totalCountFuture = null;
131+
132+
// Start count query first (in parallel)
133+
if (request.getIncludeTotal()) {
134+
totalCountFuture =
135+
CompletableFuture.supplyAsync(
136+
() -> {
137+
try {
138+
return configStore.getMatchingConfigsCount(configResource, request.getFilter());
139+
} catch (Exception e) {
140+
throw Status.INTERNAL
141+
.withCause(e)
142+
.withDescription("Failed to fetch total count")
143+
.asRuntimeException();
144+
}
145+
},
146+
configExecutor);
147+
}
148+
149+
// Then run getAllConfigs on the current thread
150+
List<ContextSpecificConfig> configList =
119151
configStore.getAllConfigs(
120-
new ConfigResource(
121-
request.getResourceName(), request.getResourceNamespace(), getTenantId()),
152+
configResource,
122153
request.getFilter(),
123154
request.getPagination(),
124155
request.getSortByList());
125-
responseObserver.onNext(
126-
GetAllConfigsResponse.newBuilder()
127-
.addAllContextSpecificConfigs(contextSpecificConfigList)
128-
.build());
156+
157+
// Build the response
158+
GetAllConfigsResponse.Builder responseBuilder =
159+
GetAllConfigsResponse.newBuilder().addAllContextSpecificConfigs(configList);
160+
161+
if (totalCountFuture != null) {
162+
long totalCount = totalCountFuture.join(); // Wait if not finished
163+
responseBuilder.setTotalCount(totalCount);
164+
}
165+
166+
responseObserver.onNext(responseBuilder.build());
129167
responseObserver.onCompleted();
130168
} catch (Exception e) {
131-
log.error("Get all configs failed for request:{}", request, e);
132-
responseObserver.onError(e);
169+
log.error("Get all configs failed for request: {}", request, e);
170+
responseObserver.onError(Status.fromThrowable(e).asRuntimeException());
133171
}
134172
}
135173

config-service-impl/src/main/java/org/hypertrace/config/service/store/ConfigStore.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ List<ContextSpecificConfig> getAllConfigs(
7070
ConfigResource configResource, Filter filter, Pagination pagination, List<SortBy> sortByList)
7171
throws IOException;
7272

73+
long getMatchingConfigsCount(ConfigResource configResource, Filter filter);
74+
7375
/**
7476
* Write each of the provided config value associated with the specified config resource to the
7577
* store.

config-service-impl/src/main/java/org/hypertrace/config/service/store/DocumentConfigStore.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.google.common.collect.Streams.zip;
44
import static org.hypertrace.config.service.store.ConfigDocument.CONTEXT_FIELD_NAME;
5+
import static org.hypertrace.config.service.store.ConfigDocument.CREATION_TIMESTAMP_FIELD_NAME;
56
import static org.hypertrace.config.service.store.ConfigDocument.RESOURCE_FIELD_NAME;
67
import static org.hypertrace.config.service.store.ConfigDocument.RESOURCE_NAMESPACE_FIELD_NAME;
78
import static org.hypertrace.config.service.store.ConfigDocument.TENANT_ID_FIELD_NAME;
@@ -15,7 +16,6 @@
1516
import java.time.Clock;
1617
import java.util.ArrayList;
1718
import java.util.Collections;
18-
import java.util.Comparator;
1919
import java.util.HashSet;
2020
import java.util.LinkedHashMap;
2121
import java.util.List;
@@ -230,12 +230,21 @@ public List<ContextSpecificConfig> getAllConfigs(
230230
processDocument(documentIterator.next(), seenContexts, configList);
231231
}
232232
}
233-
234-
configList.sort(
235-
Comparator.comparingLong(ContextSpecificConfig::getCreationTimestamp).reversed());
236233
return configList;
237234
}
238235

236+
@Override
237+
public long getMatchingConfigsCount(
238+
ConfigResource configResource, org.hypertrace.config.service.v1.Filter filter) {
239+
Query query =
240+
buildQuery(
241+
configResource,
242+
filter,
243+
org.hypertrace.config.service.v1.Pagination.getDefaultInstance(),
244+
Collections.emptyList());
245+
return collection.count(query);
246+
}
247+
239248
private Query buildQuery(
240249
ConfigResource configResource,
241250
@NonNull org.hypertrace.config.service.v1.Filter filter,
@@ -248,15 +257,15 @@ private Query buildQuery(
248257
queryBuilder.setPagination(
249258
Pagination.builder().offset(pagination.getOffset()).limit(pagination.getLimit()).build());
250259
}
251-
260+
queryBuilder.addSort(IdentifierExpression.of(VERSION_FIELD_NAME), SortOrder.DESC);
252261
if (!sortByList.isEmpty()) {
253262
sortByList.forEach(
254263
sortBy ->
255264
queryBuilder.addSort(
256265
IdentifierExpression.of(sortBy.getSelection().getConfigJsonPath()),
257266
convertSortOrder(sortBy)));
258267
} else {
259-
queryBuilder.addSort(IdentifierExpression.of(VERSION_FIELD_NAME), SortOrder.DESC);
268+
queryBuilder.addSort(IdentifierExpression.of(CREATION_TIMESTAMP_FIELD_NAME), SortOrder.DESC);
260269
}
261270
return queryBuilder.build();
262271
}

config-service-impl/src/test/java/org/hypertrace/config/service/ConfigServiceGrpcImplTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ void getAllConfigs() throws IOException {
206206
}
207207

208208
@Test
209-
void getAllConfigs_withFilterPaginationAndSorting() throws IOException {
209+
void getAllConfigs_withFilterPaginationSortingAndTotalCount() throws IOException {
210210
ConfigStore configStore = mock(ConfigStore.class);
211211

212212
// Define filter
@@ -236,6 +236,8 @@ void getAllConfigs_withFilterPaginationAndSorting() throws IOException {
236236
ContextSpecificConfig.newBuilder().setConfig(config1).build(),
237237
ContextSpecificConfig.newBuilder().setContext(CONTEXT1).setConfig(config2).build());
238238

239+
long mockedTotalCount = 42L;
240+
239241
// Mock behavior
240242
when(configStore.getAllConfigs(
241243
argThat(
@@ -248,18 +250,22 @@ void getAllConfigs_withFilterPaginationAndSorting() throws IOException {
248250
eq(pagination),
249251
eq(List.of(sortBy))))
250252
.thenReturn(contextSpecificConfigList);
253+
when(configStore.getMatchingConfigsCount(
254+
eq(new ConfigResource(RESOURCE_NAME, RESOURCE_NAMESPACE, TENANT_ID)), eq(filter)))
255+
.thenReturn(mockedTotalCount);
251256

252257
ConfigServiceGrpcImpl configServiceGrpc = new ConfigServiceGrpcImpl(configStore);
253258
StreamObserver<GetAllConfigsResponse> responseObserver = mock(StreamObserver.class);
254259

255-
// Build request with all fields
260+
// Build request with all fields and includeTotal = true
256261
GetAllConfigsRequest request =
257262
GetAllConfigsRequest.newBuilder()
258263
.setResourceName(RESOURCE_NAME)
259264
.setResourceNamespace(RESOURCE_NAMESPACE)
260265
.setFilter(filter)
261266
.setPagination(pagination)
262267
.addSortBy(sortBy)
268+
.setIncludeTotal(true)
263269
.build();
264270

265271
// Execute in tenant context
@@ -276,6 +282,7 @@ void getAllConfigs_withFilterPaginationAndSorting() throws IOException {
276282

277283
GetAllConfigsResponse actualResponse = responseCaptor.getValue();
278284
assertEquals(contextSpecificConfigList, actualResponse.getContextSpecificConfigsList());
285+
assertEquals(mockedTotalCount, actualResponse.getTotalCount());
279286
}
280287

281288
@Test

config-service-impl/src/test/java/org/hypertrace/config/service/store/DocumentConfigStoreTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ void getAllConfigs() throws IOException {
274274
Filter.getDefaultInstance(), // for empty filter
275275
Pagination.getDefaultInstance(),
276276
Collections.emptyList());
277+
277278
assertEquals(2, contextSpecificConfigList.size());
278279
assertEquals(
279280
ContextSpecificConfig.newBuilder()

0 commit comments

Comments
 (0)