From 6ee0b9f3b948c649af8fa6d04510577d295e6fdb Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 12 Oct 2020 14:47:22 +0800 Subject: [PATCH 01/17] Update protos Signed-off-by: Terence --- protos/feast/serving/ServingService.proto | 36 +++++++++++++++++++++++ protos/feast/storage/Redis.proto | 9 ++++++ 2 files changed, 45 insertions(+) diff --git a/protos/feast/serving/ServingService.proto b/protos/feast/serving/ServingService.proto index 4a37df2c94..aa6523deca 100644 --- a/protos/feast/serving/ServingService.proto +++ b/protos/feast/serving/ServingService.proto @@ -33,6 +33,9 @@ service ServingService { // Get online features synchronously. rpc GetOnlineFeatures (GetOnlineFeaturesRequest) returns (GetOnlineFeaturesResponse); + // Get online features (v2) synchronously. + rpc GetOnlineFeaturesV2 (GetOnlineFeaturesRequestV2) returns (GetOnlineFeaturesResponse); + // Get batch features asynchronously. // // The client should check the status of the returned job periodically by @@ -78,6 +81,14 @@ message FeatureReference { reserved 3, 4; } +message FeatureReferenceV2 { + // Name of the Feature Table to retrieve the feature from. + string feature_table = 1; + + // Name of the Feature to retrieve the feature from. + string name = 2; +} + message GetOnlineFeaturesRequest { // List of features that are being retrieved repeated FeatureReference features = 4; @@ -106,6 +117,31 @@ message GetOnlineFeaturesRequest { } } +message GetOnlineFeaturesRequestV2 { + // List of features that are being retrieved + repeated FeatureReferenceV2 features = 4; + + // List of entity rows, containing entity id and timestamp data. + // Used during retrieval of feature rows and for joining feature + // rows into a final dataset + repeated EntityRow entity_rows = 2; + + // Optional field to specify project name override. If specified, uses the + // given project for retrieval. Overrides the projects specified in + // Feature References if both are specified. + string project = 5; + + message EntityRow { + // Request timestamp of this row. This value will be used, + // together with maxAge, to determine feature staleness. + google.protobuf.Timestamp timestamp = 1; + + // Map containing mapping of entity name to entity value. + map fields = 2; + } + } + + message GetBatchFeaturesRequest { // List of features that are being retrieved repeated FeatureReference features = 3; diff --git a/protos/feast/storage/Redis.proto b/protos/feast/storage/Redis.proto index 04052aa800..fe7d480509 100644 --- a/protos/feast/storage/Redis.proto +++ b/protos/feast/storage/Redis.proto @@ -17,6 +17,7 @@ syntax = "proto3"; import "feast/types/Field.proto"; +import "feast/types/Value.proto"; package feast.storage; @@ -36,3 +37,11 @@ message RedisKey { // by the entity name alphabetically in ascending order. repeated feast.types.Field entities = 3; } + +message RedisKeyV2 { + string project = 1; + + repeated string entity_names = 2; + + repeated feast.types.Value entity_values = 3; +} From bf5ec75593b3dd16b9e52474aa38baff67c9d6c3 Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 12 Oct 2020 14:47:35 +0800 Subject: [PATCH 02/17] Add common functions Signed-off-by: Terence --- .../feast/common/models/FeatureTable.java | 33 +++++++++++++++++ .../java/feast/common/models/FeatureV2.java | 37 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 common/src/main/java/feast/common/models/FeatureTable.java create mode 100644 common/src/main/java/feast/common/models/FeatureV2.java diff --git a/common/src/main/java/feast/common/models/FeatureTable.java b/common/src/main/java/feast/common/models/FeatureTable.java new file mode 100644 index 0000000000..e4dade9962 --- /dev/null +++ b/common/src/main/java/feast/common/models/FeatureTable.java @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.common.models; + +import feast.proto.core.FeatureTableProto.FeatureTableSpec; + +public class FeatureTable { + + /** + * Accepts FeatureTableSpec object and returns its reference in String + * "project/featuretable_name". + * + * @param featureTableSpec {@link FeatureTableSpec} + * @return String format of FeatureTableReference + */ + public static String getFeatureTableStringRef(String project, FeatureTableSpec featureTableSpec) { + return String.format("%s/%s", project, featureTableSpec.getName()); + } +} diff --git a/common/src/main/java/feast/common/models/FeatureV2.java b/common/src/main/java/feast/common/models/FeatureV2.java new file mode 100644 index 0000000000..8debca33b9 --- /dev/null +++ b/common/src/main/java/feast/common/models/FeatureV2.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.common.models; + +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; + +public class FeatureV2 { + + /** + * Accepts FeatureReferenceV2 object and returns its reference in String + * "featuretable_name:feature_name". + * + * @param featureReference {@link FeatureReferenceV2} + * @return String format of FeatureReferenceV2 + */ + public static String getFeatureStringRef(FeatureReferenceV2 featureReference) { + String ref = featureReference.getName(); + if (!featureReference.getFeatureTable().isEmpty()) { + ref = featureReference.getFeatureTable() + ":" + ref; + } + return ref; + } +} From 1483c40160235d6f877bb79d7cb38006c3b23aec Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 12 Oct 2020 15:03:41 +0800 Subject: [PATCH 03/17] Add serving with new encoding and IT Signed-off-by: Terence --- .../config/ServingServiceConfigV2.java | 63 ++ .../ServingServiceGRpcController.java | 39 ++ .../service/OnlineServingServiceV2.java | 267 +++++++++ .../serving/service/ServingServiceV2.java | 44 ++ .../serving/specs/CachedSpecService.java | 74 +++ .../feast/serving/specs/CoreSpecService.java | 13 + .../feast/serving/util/RequestHelper.java | 10 + .../ServingServiceGRpcControllerTest.java | 5 +- .../java/feast/serving/it/BaseAuthIT.java | 1 + .../feast/serving/it/CoreSimpleAPIClient.java | 29 + .../feast/serving/it/ServingServiceIT.java | 555 ++++++++++++++++++ .../test/java/feast/serving/it/TestUtils.java | 151 +++++ .../docker-compose/docker-compose-it.yml | 2 +- .../feast/storage/api/retriever/Feature.java | 48 ++ .../api/retriever/FeatureTableRequest.java | 61 ++ .../api/retriever/OnlineRetrieverV2.java | 44 ++ storage/connectors/pom.xml | 6 + .../retriever/RedisOnlineRetrieverV2.java | 195 ++++++ 18 files changed, 1605 insertions(+), 2 deletions(-) create mode 100644 serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java create mode 100644 serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java create mode 100644 serving/src/main/java/feast/serving/service/ServingServiceV2.java create mode 100644 serving/src/test/java/feast/serving/it/ServingServiceIT.java create mode 100644 serving/src/test/java/feast/serving/it/TestUtils.java create mode 100644 storage/api/src/main/java/feast/storage/api/retriever/Feature.java create mode 100644 storage/api/src/main/java/feast/storage/api/retriever/FeatureTableRequest.java create mode 100644 storage/api/src/main/java/feast/storage/api/retriever/OnlineRetrieverV2.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java diff --git a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java new file mode 100644 index 0000000000..7a096d8ec4 --- /dev/null +++ b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.serving.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.protobuf.InvalidProtocolBufferException; +import feast.proto.core.StoreProto; +import feast.serving.service.OnlineServingServiceV2; +import feast.serving.service.ServingServiceV2; +import feast.serving.specs.CachedSpecService; +import feast.storage.api.retriever.OnlineRetrieverV2; +import feast.storage.connectors.redis.retriever.RedisOnlineRetrieverV2; +import io.opentracing.Tracer; +import java.util.Map; +import org.slf4j.Logger; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ServingServiceConfigV2 { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(ServingServiceConfigV2.class); + + @Bean + public ServingServiceV2 servingServiceV2( + FeastProperties feastProperties, CachedSpecService specService, Tracer tracer) + throws InvalidProtocolBufferException, JsonProcessingException { + ServingServiceV2 servingService = null; + FeastProperties.Store store = feastProperties.getActiveStore(); + StoreProto.Store.StoreType storeType = store.toProto().getType(); + Map config = store.getConfig(); + + switch (storeType) { + case REDIS: + OnlineRetrieverV2 redisRetriever = RedisOnlineRetrieverV2.create(config); + servingService = new OnlineServingServiceV2(redisRetriever, specService, tracer); + break; + case REDIS_CLUSTER: + case CASSANDRA: + case UNRECOGNIZED: + case INVALID: + throw new IllegalArgumentException( + String.format( + "Unsupported store type '%s' for store name '%s'", + store.getType(), store.getName())); + } + + return servingService; + } +} diff --git a/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java b/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java index 01702d9f3e..3c8a10abed 100644 --- a/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java +++ b/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java @@ -18,6 +18,7 @@ import feast.common.auth.service.AuthorizationService; import feast.common.logging.interceptors.GrpcMessageInterceptor; +import feast.proto.serving.ServingAPIProto; import feast.proto.serving.ServingAPIProto.FeatureReference; import feast.proto.serving.ServingAPIProto.GetBatchFeaturesRequest; import feast.proto.serving.ServingAPIProto.GetBatchFeaturesResponse; @@ -32,6 +33,7 @@ import feast.serving.exception.SpecRetrievalException; import feast.serving.interceptors.GrpcMonitoringInterceptor; import feast.serving.service.ServingService; +import feast.serving.service.ServingServiceV2; import feast.serving.util.RequestHelper; import io.grpc.Status; import io.grpc.stub.StreamObserver; @@ -53,6 +55,7 @@ public class ServingServiceGRpcController extends ServingServiceImplBase { private static final Logger log = org.slf4j.LoggerFactory.getLogger(ServingServiceGRpcController.class); private final ServingService servingService; + private final ServingServiceV2 servingServiceV2; private final String version; private final Tracer tracer; private final AuthorizationService authorizationService; @@ -61,10 +64,12 @@ public class ServingServiceGRpcController extends ServingServiceImplBase { public ServingServiceGRpcController( AuthorizationService authorizationService, ServingService servingService, + ServingServiceV2 servingServiceV2, FeastProperties feastProperties, Tracer tracer) { this.authorizationService = authorizationService; this.servingService = servingService; + this.servingServiceV2 = servingServiceV2; this.version = feastProperties.getVersion(); this.tracer = tracer; } @@ -169,4 +174,38 @@ private void checkProjectAccess(List featureList) { }); } } + + @Override + public void getOnlineFeaturesV2( + ServingAPIProto.GetOnlineFeaturesRequestV2 request, + StreamObserver responseObserver) { + Span span = tracer.buildSpan("getOnlineFeaturesV2").start(); + try (Scope scope = tracer.scopeManager().activate(span, false)) { + // authorize for the project in request object. + if (request.getProject() != null && !request.getProject().isEmpty()) { + // project set at root level overrides the project set at feature table level + this.authorizationService.authorizeRequest( + SecurityContextHolder.getContext(), request.getProject()); + } + RequestHelper.validateOnlineRequest(request); + GetOnlineFeaturesResponse onlineFeatures = servingServiceV2.getOnlineFeatures(request); + responseObserver.onNext(onlineFeatures); + responseObserver.onCompleted(); + } catch (SpecRetrievalException e) { + log.error("Failed to retrieve specs in SpecService", e); + responseObserver.onError( + Status.NOT_FOUND.withDescription(e.getMessage()).withCause(e).asException()); + } catch (AccessDeniedException e) { + log.info(String.format("User prevented from accessing one of the projects in request")); + responseObserver.onError( + Status.PERMISSION_DENIED + .withDescription(e.getMessage()) + .withCause(e) + .asRuntimeException()); + } catch (Exception e) { + log.warn("Failed to get Online Features", e); + responseObserver.onError(e); + } + span.finish(); + } } diff --git a/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java b/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java new file mode 100644 index 0000000000..03b1515512 --- /dev/null +++ b/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java @@ -0,0 +1,267 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.serving.service; + +import com.google.protobuf.Duration; +import feast.common.models.FeatureV2; +import feast.proto.core.FeatureTableProto.FeatureTableSpec; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; +import feast.proto.types.ValueProto; +import feast.serving.specs.CachedSpecService; +import feast.serving.util.Metrics; +import feast.storage.api.retriever.Feature; +import feast.storage.api.retriever.OnlineRetrieverV2; +import io.grpc.Status; +import io.opentracing.Scope; +import io.opentracing.Tracer; +import java.util.*; +import java.util.stream.Collectors; +import org.slf4j.Logger; + +public class OnlineServingServiceV2 implements ServingServiceV2 { + + private static final Logger log = org.slf4j.LoggerFactory.getLogger(OnlineServingService.class); + private final CachedSpecService specService; + private final Tracer tracer; + private final OnlineRetrieverV2 retriever; + + public OnlineServingServiceV2( + OnlineRetrieverV2 retriever, CachedSpecService specService, Tracer tracer) { + this.retriever = retriever; + this.specService = specService; + this.tracer = tracer; + } + + @Override + public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 request) { + String projectName = request.getProject(); + List featureReferences = request.getFeaturesList(); + + // Autofill default project if project is not specified + if (projectName.isEmpty()) { + projectName = "default"; + } + + try (Scope scope = tracer.buildSpan("getOnlineFeaturesV2").startActive(true)) { + List entityRows = request.getEntityRowsList(); + // Collect the feature/entity value for each entity row in entityValueMap + Map> entityValuesMap = + entityRows.stream().collect(Collectors.toMap(row -> row, row -> new HashMap<>())); + // Collect the feature/entity status metadata for each entity row in entityValueMap + Map> + entityStatusesMap = + entityRows.stream().collect(Collectors.toMap(row -> row, row -> new HashMap<>())); + + entityRows.forEach( + entityRow -> { + Map valueMap = entityRow.getFieldsMap(); + entityValuesMap.get(entityRow).putAll(valueMap); + entityStatusesMap.get(entityRow).putAll(getMetadataMap(valueMap, false, false)); + }); + + List>> entityRowsFeatures = + retriever.getOnlineFeatures(projectName, entityRows, featureReferences); + + if (entityRowsFeatures.size() != entityRows.size()) { + throw Status.INTERNAL + .withDescription( + "The no. of FeatureRow obtained from OnlineRetriever" + + "does not match no. of entityRow passed.") + .asRuntimeException(); + } + + for (int i = 0; i < entityRows.size(); i++) { + GetOnlineFeaturesRequestV2.EntityRow entityRow = entityRows.get(i); + List> curEntityRowFeatures = entityRowsFeatures.get(i); + + Map> featureReferenceFeatureMap = + getFeatureRefFeatureMap(curEntityRowFeatures); + + Map allValueMaps = new HashMap<>(); + Map allStatusMaps = new HashMap<>(); + + for (FeatureReferenceV2 featureReference : featureReferences) { + if (featureReferenceFeatureMap.containsKey(featureReference)) { + Optional feature = featureReferenceFeatureMap.get(featureReference); + + FeatureTableSpec featureTableSpec = + specService.getFeatureTableSpec(projectName, feature.get().getFeatureReference()); + + boolean isOutsideMaxAge = checkOutsideMaxAge(featureTableSpec, entityRow, feature); + Map valueMap = unpackValueMap(feature, isOutsideMaxAge); + allValueMaps.putAll(valueMap); + + // Generate metadata for feature values and merge into entityFieldsMap + Map statusMap = + getMetadataMap(valueMap, false, isOutsideMaxAge); + allStatusMaps.putAll(statusMap); + + // Populate metrics/log request + populateCountMetrics(statusMap, projectName); + } else { + Map valueMap = + new HashMap<>() { + { + put( + FeatureV2.getFeatureStringRef(featureReference), + ValueProto.Value.newBuilder().build()); + } + }; + allValueMaps.putAll(valueMap); + + Map statusMap = + getMetadataMap(valueMap, true, false); + allStatusMaps.putAll(statusMap); + + // Populate metrics/log request + populateCountMetrics(statusMap, projectName); + } + } + entityValuesMap.get(entityRow).putAll(allValueMaps); + entityStatusesMap.get(entityRow).putAll(allStatusMaps); + } + + // Build response field values from entityValuesMap and entityStatusesMap + // Response field values should be in the same order as the entityRows provided by the user. + List fieldValuesList = + entityRows.stream() + .map( + entityRow -> { + return GetOnlineFeaturesResponse.FieldValues.newBuilder() + .putAllFields(entityValuesMap.get(entityRow)) + .putAllStatuses(entityStatusesMap.get(entityRow)) + .build(); + }) + .collect(Collectors.toList()); + return GetOnlineFeaturesResponse.newBuilder().addAllFieldValues(fieldValuesList).build(); + } + } + + private static Map> getFeatureRefFeatureMap( + List> features) { + Map> featureReferenceFeatureMap = new HashMap<>(); + features.forEach( + feature -> { + FeatureReferenceV2 featureReference = feature.get().getFeatureReference(); + featureReferenceFeatureMap.put(featureReference, feature); + }); + + return featureReferenceFeatureMap; + } + + /** + * Generate Field level Status metadata for the given valueMap. + * + * @param valueMap map of field name to value to generate metadata for. + * @param isNotFound whether the given valueMap represents values that were not found in the + * online retriever. + * @param isOutsideMaxAge whether the given valueMap contains values with age outside + * FeatureTable's max age. + * @return a 1:1 map keyed by field name containing field status metadata instead of values in the + * given valueMap. + */ + private static Map getMetadataMap( + Map valueMap, boolean isNotFound, boolean isOutsideMaxAge) { + return valueMap.entrySet().stream() + .collect( + Collectors.toMap( + es -> es.getKey(), + es -> { + ValueProto.Value fieldValue = es.getValue(); + if (isNotFound) { + return GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND; + } else if (isOutsideMaxAge) { + return GetOnlineFeaturesResponse.FieldStatus.OUTSIDE_MAX_AGE; + } else if (fieldValue.getValCase().equals(ValueProto.Value.ValCase.VAL_NOT_SET)) { + return GetOnlineFeaturesResponse.FieldStatus.NULL_VALUE; + } + return GetOnlineFeaturesResponse.FieldStatus.PRESENT; + })); + } + + private static Map unpackValueMap( + Optional feature, boolean isOutsideMaxAge) { + Map valueMap = new HashMap<>(); + + if (feature.isPresent()) { + if (!isOutsideMaxAge) { + valueMap.put( + FeatureV2.getFeatureStringRef(feature.get().getFeatureReference()), + feature.get().getFeatureValue()); + } else { + valueMap.put( + FeatureV2.getFeatureStringRef(feature.get().getFeatureReference()), + ValueProto.Value.newBuilder().build()); + } + } + return valueMap; + } + + /** + * Determine if the feature data in the given feature row is outside maxAge. Data is outside + * maxAge to be when the difference ingestion time set in feature row and the retrieval time set + * in entity row exceeds FeatureTable max age. + * + * @param featureTableSpec contains the spec where feature's max age is extracted. + * @param entityRow contains the retrieval timing of when features are pulled. + * @param feature contains the ingestion timing and feature data. + */ + private static boolean checkOutsideMaxAge( + FeatureTableSpec featureTableSpec, + GetOnlineFeaturesRequestV2.EntityRow entityRow, + Optional feature) { + Duration maxAge = featureTableSpec.getMaxAge(); + if (feature.isEmpty()) { // no data to consider + return false; + } + if (maxAge.equals(Duration.getDefaultInstance())) { // max age is not set + return false; + } + + long givenTimestamp = entityRow.getTimestamp().getSeconds(); + if (givenTimestamp == 0) { + givenTimestamp = System.currentTimeMillis() / 1000; + } + long timeDifference = givenTimestamp - feature.get().getEventTimestamp().getSeconds(); + return timeDifference > maxAge.getSeconds(); + } + + /** + * Populate count metrics that can be used for analysing online retrieval calls + * + * @param statusMap Statuses of features which have been requested + * @param project Project where request for features was called from + */ + private void populateCountMetrics( + Map statusMap, String project) { + statusMap + .entrySet() + .forEach( + es -> { + String featureRefString = es.getKey(); + GetOnlineFeaturesResponse.FieldStatus status = es.getValue(); + if (status == GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND) { + Metrics.notFoundKeyCount.labels(project, featureRefString).inc(); + } + if (status == GetOnlineFeaturesResponse.FieldStatus.OUTSIDE_MAX_AGE) { + Metrics.staleKeyCount.labels(project, featureRefString).inc(); + } + }); + } +} diff --git a/serving/src/main/java/feast/serving/service/ServingServiceV2.java b/serving/src/main/java/feast/serving/service/ServingServiceV2.java new file mode 100644 index 0000000000..6164b93eb5 --- /dev/null +++ b/serving/src/main/java/feast/serving/service/ServingServiceV2.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.serving.service; + +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; + +public interface ServingServiceV2 { + /** + * Get features from an online serving store, given a list of {@link + * feast.proto.serving.ServingAPIProto.FeatureReferenceV2}s to retrieve, and list of {@link + * feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow}s to join the + * retrieved values to. + * + *

Features can be queried across feature sets, but each {@link + * feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow} must contain all + * entities for all feature sets included in the request. + * + *

This request is fulfilled synchronously. + * + * @param getFeaturesRequest {@link GetOnlineFeaturesRequestV2} containing list of {@link + * feast.proto.serving.ServingAPIProto.FeatureReferenceV2}s to retrieve and list of {@link + * feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow}s to join the + * retrieved values to. + * @return {@link GetOnlineFeaturesResponse} with list of {@link + * feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse.FieldValues} for each {@link + * feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow} supplied. + */ + GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 getFeaturesRequest); +} diff --git a/serving/src/main/java/feast/serving/specs/CachedSpecService.java b/serving/src/main/java/feast/serving/specs/CachedSpecService.java index 6d2d2d543a..91b99bfd83 100644 --- a/serving/src/main/java/feast/serving/specs/CachedSpecService.java +++ b/serving/src/main/java/feast/serving/specs/CachedSpecService.java @@ -18,6 +18,7 @@ import static feast.common.models.Feature.getFeatureStringWithProjectRef; import static feast.common.models.FeatureSet.getFeatureSetStringRef; +import static feast.common.models.FeatureTable.getFeatureTableStringRef; import static java.util.stream.Collectors.groupingBy; import com.google.common.cache.CacheBuilder; @@ -25,12 +26,18 @@ import com.google.common.cache.LoadingCache; import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest; import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse; +import feast.proto.core.CoreServiceProto.ListFeatureTablesRequest; +import feast.proto.core.CoreServiceProto.ListFeatureTablesResponse; +import feast.proto.core.CoreServiceProto.ListProjectsRequest; import feast.proto.core.FeatureSetProto.FeatureSet; import feast.proto.core.FeatureSetProto.FeatureSetSpec; import feast.proto.core.FeatureSetProto.FeatureSpec; +import feast.proto.core.FeatureTableProto.FeatureTable; +import feast.proto.core.FeatureTableProto.FeatureTableSpec; import feast.proto.core.StoreProto; import feast.proto.core.StoreProto.Store; import feast.proto.core.StoreProto.Store.Subscription; +import feast.proto.serving.ServingAPIProto; import feast.proto.serving.ServingAPIProto.FeatureReference; import feast.serving.exception.SpecRetrievalException; import feast.storage.api.retriever.FeatureSetRequest; @@ -77,6 +84,14 @@ public class CachedSpecService { .help("epoch time of the last time the cache was updated") .register(); + private final LoadingCache featureTableCache; + private static Gauge featureTablesCount = + Gauge.build() + .name("feature_table_count") + .subsystem("feast_serving") + .help("number of feature sets served by this instance") + .register(); + public CachedSpecService(CoreSpecService coreService, StoreProto.Store store) { this.coreService = coreService; this.store = coreService.registerStore(store); @@ -88,6 +103,13 @@ public CachedSpecService(CoreSpecService coreService, StoreProto.Store store) { featureSetCache = CacheBuilder.newBuilder().maximumSize(MAX_SPEC_COUNT).build(featureSetCacheLoader); featureSetCache.putAll(featureSets); + + Map featureTables = getFeatureTableMap(); + CacheLoader featureTableCacheLoader = + CacheLoader.from(featureTables::get); + featureTableCache = + CacheBuilder.newBuilder().maximumSize(MAX_SPEC_COUNT).build(featureTableCacheLoader); + featureTableCache.putAll(featureTables); } /** @@ -218,6 +240,13 @@ public void populateCache() { featureToFeatureSetMapping = getFeatureToFeatureSetMapping(featureSetMap); featureSetsCount.set(featureSetCache.size()); + + Map featureTableMap = getFeatureTableMap(); + + featureTableCache.invalidateAll(); + featureTableCache.putAll(featureTableMap); + + featureTablesCount.set(featureTableCache.size()); cacheLastUpdated.set(System.currentTimeMillis()); } @@ -334,4 +363,49 @@ private Pair generateFeatureToFeatureSetMapping( return Pair.of( getFeatureStringWithProjectRef(featureRef.build()), getFeatureSetStringRef(featureSetSpec)); } + + /** + * Provides a map for easy retrieval of FeatureTable spec using FeatureTable reference + * + * @return Map in the format of + */ + private Map getFeatureTableMap() { + HashMap featureTables = new HashMap<>(); + + List projects = + coreService.listProjects(ListProjectsRequest.newBuilder().build()).getProjectsList(); + + for (String project : projects) { + try { + ListFeatureTablesResponse featureTablesResponse = + coreService.listFeatureTables( + ListFeatureTablesRequest.newBuilder() + .setFilter(ListFeatureTablesRequest.Filter.newBuilder().setProject(project)) + .build()); + for (FeatureTable featureTable : featureTablesResponse.getTablesList()) { + FeatureTableSpec spec = featureTable.getSpec(); + // Key of Map is in the form of + featureTables.put(getFeatureTableStringRef(project, spec), spec); + } + } catch (StatusRuntimeException e) { + throw new RuntimeException( + String.format("Unable to retrieve specs matching project %s", project), e); + } + } + return featureTables; + } + + public FeatureTableSpec getFeatureTableSpec( + String project, ServingAPIProto.FeatureReferenceV2 featureReference) { + String featureTableRefStr = project + "/" + featureReference.getFeatureTable(); + FeatureTableSpec featureTableSpec; + try { + featureTableSpec = featureTableCache.get(featureTableRefStr); + } catch (ExecutionException e) { + throw new SpecRetrievalException( + String.format("Unable to find FeatureTable with name: %s", featureTableRefStr), e); + } + + return featureTableSpec; + } } diff --git a/serving/src/main/java/feast/serving/specs/CoreSpecService.java b/serving/src/main/java/feast/serving/specs/CoreSpecService.java index 8dcfd0695e..8bdf4fcdc0 100644 --- a/serving/src/main/java/feast/serving/specs/CoreSpecService.java +++ b/serving/src/main/java/feast/serving/specs/CoreSpecService.java @@ -21,6 +21,10 @@ import feast.proto.core.CoreServiceProto.GetFeatureSetResponse; import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest; import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse; +import feast.proto.core.CoreServiceProto.ListFeatureTablesRequest; +import feast.proto.core.CoreServiceProto.ListFeatureTablesResponse; +import feast.proto.core.CoreServiceProto.ListProjectsRequest; +import feast.proto.core.CoreServiceProto.ListProjectsResponse; import feast.proto.core.CoreServiceProto.UpdateStoreRequest; import feast.proto.core.CoreServiceProto.UpdateStoreResponse; import feast.proto.core.StoreProto.Store; @@ -79,4 +83,13 @@ public Store registerStore(Store store) { throw new RuntimeException("Unable to update store configuration", e); } } + + public ListProjectsResponse listProjects(ListProjectsRequest listProjectsRequest) { + return blockingStub.listProjects(listProjectsRequest); + } + + public ListFeatureTablesResponse listFeatureTables( + ListFeatureTablesRequest listFeatureTablesRequest) { + return blockingStub.listFeatureTables(listFeatureTablesRequest); + } } diff --git a/serving/src/main/java/feast/serving/util/RequestHelper.java b/serving/src/main/java/feast/serving/util/RequestHelper.java index 16073e9b5c..1caadafbb7 100644 --- a/serving/src/main/java/feast/serving/util/RequestHelper.java +++ b/serving/src/main/java/feast/serving/util/RequestHelper.java @@ -19,6 +19,7 @@ import feast.proto.serving.ServingAPIProto.FeatureReference; import feast.proto.serving.ServingAPIProto.GetBatchFeaturesRequest; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequest; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; import io.grpc.Status; import java.util.Set; import java.util.stream.Collectors; @@ -34,6 +35,15 @@ public static void validateOnlineRequest(GetOnlineFeaturesRequest request) { } } + public static void validateOnlineRequest(GetOnlineFeaturesRequestV2 request) { + // EntityDataSetRow shall not be empty + if (request.getEntityRowsCount() <= 0) { + throw Status.INVALID_ARGUMENT + .withDescription("Entity value must be provided") + .asRuntimeException(); + } + } + public static void validateBatchRequest(GetBatchFeaturesRequest getFeaturesRequest) { if (!getFeaturesRequest.hasDatasetSource()) { throw Status.INVALID_ARGUMENT diff --git a/serving/src/test/java/feast/serving/controller/ServingServiceGRpcControllerTest.java b/serving/src/test/java/feast/serving/controller/ServingServiceGRpcControllerTest.java index 8e5068264d..671b57b2f6 100644 --- a/serving/src/test/java/feast/serving/controller/ServingServiceGRpcControllerTest.java +++ b/serving/src/test/java/feast/serving/controller/ServingServiceGRpcControllerTest.java @@ -37,6 +37,7 @@ import feast.proto.types.ValueProto.Value; import feast.serving.config.FeastProperties; import feast.serving.service.ServingService; +import feast.serving.service.ServingServiceV2; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import io.jaegertracing.Configuration; @@ -53,6 +54,8 @@ public class ServingServiceGRpcControllerTest { @Mock private ServingService mockServingService; + @Mock private ServingServiceV2 mockServingServiceV2; + @Mock private StreamObserver mockStreamObserver; private GetOnlineFeaturesRequest validRequest; @@ -94,7 +97,7 @@ private ServingServiceGRpcController getServingServiceGRpcController(boolean ena AuthorizationService authorizationservice = new AuthorizationService(feastProperties.getSecurity(), authProvider); return new ServingServiceGRpcController( - authorizationservice, mockServingService, feastProperties, tracer); + authorizationservice, mockServingService, mockServingServiceV2, feastProperties, tracer); } @Test diff --git a/serving/src/test/java/feast/serving/it/BaseAuthIT.java b/serving/src/test/java/feast/serving/it/BaseAuthIT.java index be327814fe..98836f9156 100644 --- a/serving/src/test/java/feast/serving/it/BaseAuthIT.java +++ b/serving/src/test/java/feast/serving/it/BaseAuthIT.java @@ -49,6 +49,7 @@ public class BaseAuthIT { static CoreSimpleAPIClient insecureApiClient; + static final String REDIS = "redis_1"; static final int REDIS_PORT = 6379; static final int FEAST_CORE_PORT = 6565; diff --git a/serving/src/test/java/feast/serving/it/CoreSimpleAPIClient.java b/serving/src/test/java/feast/serving/it/CoreSimpleAPIClient.java index 7d9313150d..3886bfbb8e 100644 --- a/serving/src/test/java/feast/serving/it/CoreSimpleAPIClient.java +++ b/serving/src/test/java/feast/serving/it/CoreSimpleAPIClient.java @@ -18,7 +18,9 @@ import feast.proto.core.CoreServiceGrpc; import feast.proto.core.CoreServiceProto; +import feast.proto.core.EntityProto; import feast.proto.core.FeatureSetProto; +import feast.proto.core.FeatureTableProto; public class CoreSimpleAPIClient { private CoreServiceGrpc.CoreServiceBlockingStub stub; @@ -40,4 +42,31 @@ public FeatureSetProto.FeatureSet simpleGetFeatureSet(String projectName, String .build()) .getFeatureSet(); } + + public void simpleApplyEntity(EntityProto.EntitySpecV2 entitySpec) { + stub.applyEntity(CoreServiceProto.ApplyEntityRequest.newBuilder().setSpec(entitySpec).build()); + } + + public EntityProto.Entity getEntity(String projectName, String name) { + return stub.getEntity( + CoreServiceProto.GetEntityRequest.newBuilder() + .setProject(projectName) + .setName(name) + .build()) + .getEntity(); + } + + public void simpleApplyFeatureTable(FeatureTableProto.FeatureTableSpec featureTable) { + stub.applyFeatureTable( + CoreServiceProto.ApplyFeatureTableRequest.newBuilder().setTableSpec(featureTable).build()); + } + + public FeatureTableProto.FeatureTable simpleGetFeatureTable(String projectName, String name) { + return stub.getFeatureTable( + CoreServiceProto.GetFeatureTableRequest.newBuilder() + .setName(name) + .setProject(projectName) + .build()) + .getTable(); + } } diff --git a/serving/src/test/java/feast/serving/it/ServingServiceIT.java b/serving/src/test/java/feast/serving/it/ServingServiceIT.java new file mode 100644 index 0000000000..ef2ecbf0cd --- /dev/null +++ b/serving/src/test/java/feast/serving/it/ServingServiceIT.java @@ -0,0 +1,555 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.serving.it; + +import static org.junit.jupiter.api.Assertions.*; + +import com.google.common.hash.Hashing; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Timestamp; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import feast.common.models.FeatureV2; +import feast.proto.core.EntityProto; +import feast.proto.serving.ServingAPIProto; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; +import feast.proto.serving.ServingServiceGrpc; +import feast.proto.storage.RedisProto; +import feast.proto.types.ValueProto; +import feast.storage.api.retriever.Feature; +import io.grpc.ManagedChannel; +import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.stream.Collectors; +import org.junit.ClassRule; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.runners.model.InitializationError; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@ActiveProfiles("it") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +public class ServingServiceIT extends BaseAuthIT { + + static final Map options = new HashMap<>(); + static final String timestampPrefix = "_ts"; + static CoreSimpleAPIClient coreClient; + static RedisCommands syncCommands; + + // To decode bytes back to Feature Reference + static Map byteToFeatureReferenceMap = + new HashMap<>(); + // To check whether redis ValueK is a timestamp field + static Map isTimestampMap = new HashMap<>(); + static List> featureReferenceWithTsByteArrays = new ArrayList<>(); + static RedisProto.RedisKeyV2 redisKey = null; + + static final int FEAST_SERVING_PORT = 6566; + @LocalServerPort private int metricsPort; + + @ClassRule @Container + public static DockerComposeContainer environment = + new DockerComposeContainer( + new File("src/test/resources/docker-compose/docker-compose-it.yml")) + .withExposedService( + CORE, + FEAST_CORE_PORT, + Wait.forLogMessage(".*gRPC Server started.*\\n", 1) + .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))) + .withExposedService( + JOB_CONTROLLER, + FEAST_JOB_CONTROLLER_PORT, + Wait.forLogMessage(".*gRPC Server started.*\\n", 1) + .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))) + .withExposedService(REDIS, REDIS_PORT); + + @BeforeAll + static void globalSetup() throws IOException, InitializationError, InterruptedException { + // Create Core client + coreClient = TestUtils.getApiClientForCore(FEAST_CORE_PORT); + + // Create Redis client + RedisClient redisClient = + RedisClient.create( + new RedisURI( + environment.getServiceHost("redis_1", REDIS_PORT), + environment.getServicePort("redis_1", REDIS_PORT), + java.time.Duration.ofMillis(2000))); + StatefulRedisConnection connection = redisClient.connect(new ByteArrayCodec()); + syncCommands = connection.sync(); + + String projectName = "default"; + // Apply Entity + String entityName = "driver_id"; + ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); + String description = "My driver id"; + ValueProto.ValueType.Enum entityType = ValueProto.ValueType.Enum.INT64; + EntityProto.EntitySpecV2 entitySpec = + EntityProto.EntitySpecV2.newBuilder() + .setName(entityName) + .setDescription(description) + .setValueType(entityType) + .build(); + TestUtils.applyEntity(coreClient, projectName, entitySpec); + + // Apply FeatureTable + String featureTableName = "rides"; + List entities = + new ArrayList<>() { + { + add(entityName); + } + }; + HashMap features = new HashMap<>(); + + // Feature 1 + String feature1 = "trip_cost"; + ValueProto.Value feature1Value = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); + ServingAPIProto.FeatureReferenceV2 feature1Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature1) + .build(); + + // Feature 2 + String feature2 = "trip_distance"; + ValueProto.Value feature2Value = ValueProto.Value.newBuilder().setInt64Val(42).build(); + ServingAPIProto.FeatureReferenceV2 feature2Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature2) + .build(); + + // Feature 2 + String feature3 = "trip_empty"; + ValueProto.Value feature3Value = ValueProto.Value.newBuilder().build(); + ServingAPIProto.FeatureReferenceV2 feature3Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature3) + .build(); + + // Event Timestamp + String eventTimestampKey = timestampPrefix + ":" + featureTableName; + Timestamp eventTimestampValue = Timestamp.newBuilder().setSeconds(100).build(); + + features.put(feature1, ValueProto.ValueType.Enum.INT64); + features.put(feature2, ValueProto.ValueType.Enum.DOUBLE); + features.put(feature3, ValueProto.ValueType.Enum.DOUBLE); + + TestUtils.applyFeatureTable(coreClient, projectName, featureTableName, entities, features); + + // Serialize Redis Key with Entity i.e + redisKey = + RedisProto.RedisKeyV2.newBuilder() + .setProject(projectName) + .addEntityNames(entityName) + .addEntityValues(entityValue) + .build(); + + // Murmur hash Redis Value Field i.e murmur() + Map featureReferenceValueMap = + new HashMap<>() { + { + put(feature1Reference, feature1Value); + put(feature2Reference, feature2Value); + put(feature3Reference, feature3Value); + } + }; + + // Insert timestamp into Redis and isTimestampMap only once + syncCommands.hset( + redisKey.toByteArray(), eventTimestampKey.getBytes(), eventTimestampValue.toByteArray()); + isTimestampMap.put(Arrays.toString(eventTimestampKey.getBytes()), true); + featureReferenceValueMap.forEach( + (featureReference, featureValue) -> { + List currentFeatureWithTsByteArrays = new ArrayList<>(); + String delimitedFeatureReference = + featureReference.getFeatureTable() + ":" + featureReference.getName(); + byte[] featureReferenceBytes = + Hashing.murmur3_32() + .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) + .asBytes(); + // Insert features into Redis + syncCommands.hset( + redisKey.toByteArray(), featureReferenceBytes, featureValue.toByteArray()); + currentFeatureWithTsByteArrays.add(featureReferenceBytes); + isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); + byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); + + currentFeatureWithTsByteArrays.add(eventTimestampKey.getBytes()); + + featureReferenceWithTsByteArrays.add(currentFeatureWithTsByteArrays); + }); + + // set up options for call credentials + options.put("oauth_url", TOKEN_URL); + options.put(CLIENT_ID, CLIENT_ID); + options.put(CLIENT_SECRET, CLIENT_SECRET); + options.put("jwkEndpointURI", JWK_URI); + options.put("audience", AUDIENCE); + options.put("grant_type", GRANT_TYPE); + } + + /** Test that Feast Serving metrics endpoint can be accessed with authentication enabled */ + @Test + public void shouldAllowUnauthenticatedAccessToMetricsEndpoint() throws IOException { + Request request = + new Request.Builder() + .url(String.format("http://localhost:%d/metrics", metricsPort)) + .get() + .build(); + Response response = new OkHttpClient().newCall(request).execute(); + assertTrue(response.isSuccessful()); + assertTrue(!response.body().string().isEmpty()); + } + + // @Test + // public void shouldAllowUnauthenticatedGetOnlineFeatures() { + // // apply feature set + // CoreSimpleAPIClient coreClient = + // TestUtils.getSecureApiClientForCore(FEAST_CORE_PORT, options); + // TestUtils.applyFeatureTable(coreClient, PROJECT_NAME, ENTITY_ID, FEATURE_NAME); + // ServingServiceGrpc.ServingServiceBlockingStub servingStub = + // TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); + // + // GetOnlineFeaturesRequestV2 onlineFeatureRequest = + // TestUtils.createOnlineFeatureRequest(PROJECT_NAME, FEATURE_NAME, ENTITY_ID, 1); + // GetOnlineFeaturesResponse featureResponse = + // servingStub.getOnlineFeaturesV2(onlineFeatureRequest); + // assertEquals(1, featureResponse.getFieldValuesCount()); + // Map fieldsMap = featureResponse.getFieldValues(0).getFieldsMap(); + // + // assertTrue(fieldsMap.containsKey(ENTITY_ID)); + // assertTrue(fieldsMap.containsKey(FEATURE_NAME)); + // + // ((ManagedChannel) servingStub.getChannel()).shutdown(); + // } + + @Test + public void shouldRegisterAndRetrieveFromRedis() throws InvalidProtocolBufferException { + String featureTableName = "rides"; + + // Feature 1 + String feature1 = "trip_cost"; + ValueProto.Value feature1Value = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); + ServingAPIProto.FeatureReferenceV2 feature1Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature1) + .build(); + + // Feature 2 + String feature2 = "trip_distance"; + ValueProto.Value feature2Value = ValueProto.Value.newBuilder().setInt64Val(42).build(); + ServingAPIProto.FeatureReferenceV2 feature2Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature2) + .build(); + + // Feature 2 + String feature3 = "trip_empty"; + ValueProto.Value feature3Value = ValueProto.Value.newBuilder().build(); + ServingAPIProto.FeatureReferenceV2 feature3Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature3) + .build(); + + // Retrieve multiple value from Redis + List> retrievedFeatures = new ArrayList<>(); + for (List currentFeatureReferenceWithTs : featureReferenceWithTsByteArrays) { + ServingAPIProto.FeatureReferenceV2 featureReference = null; + ValueProto.Value featureValue = null; + Timestamp eventTimestamp = null; + + List> currentRedisValuesList = + syncCommands.hmget( + redisKey.toByteArray(), + currentFeatureReferenceWithTs.get(0), + currentFeatureReferenceWithTs.get(1)); + + for (int i = 0; i < currentRedisValuesList.size(); i++) { + if (currentRedisValuesList.get(i).hasValue()) { + try { + byte[] redisValueK = currentRedisValuesList.get(i).getKey(); + byte[] redisValueV = currentRedisValuesList.get(i).getValue(); + + // Decode data from Redis into Feature object fields + if (isTimestampMap.get(Arrays.toString(redisValueK))) { + eventTimestamp = Timestamp.parseFrom(redisValueV); + } else { + featureReference = byteToFeatureReferenceMap.get(redisValueK.toString()); + featureValue = ValueProto.Value.parseFrom(redisValueV); + } + } catch (InvalidProtocolBufferException e) { + e.printStackTrace(); + } + } + } + // Check for null featureReference i.e key is not found + if (featureReference != null) { + Feature feature = + Feature.builder() + .setFeatureReference(featureReference) + .setFeatureValue(featureValue) + .setEventTimestamp(eventTimestamp) + .build(); + retrievedFeatures.add(Optional.of(feature)); + } + } + + List expectedValues = + new ArrayList<>() { + { + add(feature1Value); + add(feature2Value); + add(feature3Value); + } + }; + List expectedFeatureReferences = + new ArrayList<>() { + { + add(feature1Reference); + add(feature2Reference); + add(feature3Reference); + } + }; + assertEquals(3, retrievedFeatures.size()); + assertEquals( + expectedFeatureReferences.stream() + .map(featureReference -> featureReference.getName()) + .sorted() + .collect(Collectors.toList()), + retrievedFeatures.stream() + .map(feature -> feature.get().getFeatureReference().getName()) + .sorted() + .collect(Collectors.toList())); + assertEquals( + expectedFeatureReferences.stream() + .map(featureReference -> featureReference.getFeatureTable()) + .collect(Collectors.toList()), + retrievedFeatures.stream() + .map(feature -> feature.get().getFeatureReference().getFeatureTable()) + .collect(Collectors.toList())); + } + + @Test + public void shouldRegisterAndGetOnlineFeatures() { + ServingServiceGrpc.ServingServiceBlockingStub servingStub = + TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); + + // getOnlineFeatures Information + String projectName = "default"; + String featureTableName = "rides"; + String entityName = "driver_id"; + ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); + String feature1 = "trip_cost"; + + // Instantiate EntityRows + GetOnlineFeaturesRequestV2.EntityRow entityRow1 = + GetOnlineFeaturesRequestV2.EntityRow.newBuilder() + .setTimestamp(Timestamp.newBuilder().setSeconds(100)) + .putFields(entityName, entityValue) + .build(); + List entityRows = + new ArrayList<>() { + { + add(entityRow1); + } + }; + + // Instantiate FeatureReferences + ValueProto.Value feature1Value = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); + ServingAPIProto.FeatureReferenceV2 feature1Reference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature1) + .build(); + List featureReferences = + new ArrayList<>() { + { + add(feature1Reference); + } + }; + + // Build GetOnlineFeaturesRequestV2 + GetOnlineFeaturesRequestV2 onlineFeatureRequest = + TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); + GetOnlineFeaturesResponse featureResponse = + servingStub.getOnlineFeaturesV2(onlineFeatureRequest); + + Map expectedValueMap = + new HashMap<>() { + { + put(entityName, entityValue); + put(FeatureV2.getFeatureStringRef(feature1Reference), feature1Value); + } + }; + + Map expectedStatusMap = + new HashMap<>() { + { + put(entityName, GetOnlineFeaturesResponse.FieldStatus.PRESENT); + put( + FeatureV2.getFeatureStringRef(feature1Reference), + GetOnlineFeaturesResponse.FieldStatus.PRESENT); + } + }; + + GetOnlineFeaturesResponse.FieldValues expectedFieldValues = + GetOnlineFeaturesResponse.FieldValues.newBuilder() + .putAllFields(expectedValueMap) + .putAllStatuses(expectedStatusMap) + .build(); + List expectedFieldValuesList = + new ArrayList<>() { + { + add(expectedFieldValues); + } + }; + + assertEquals(1, 1); + assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); + + ((ManagedChannel) servingStub.getChannel()).shutdown(); + } + + @Test + public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { + ServingServiceGrpc.ServingServiceBlockingStub servingStub = + TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); + + // getOnlineFeatures Information + String projectName = "default"; + String featureTableName = "rides"; + String entityName = "driver_id"; + ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); + String feature1 = "trip_cost"; + String notFoundFeature = "trip_transaction"; + String emptyFeature = "trip_empty"; + + // Instantiate EntityRows + GetOnlineFeaturesRequestV2.EntityRow entityRow1 = + GetOnlineFeaturesRequestV2.EntityRow.newBuilder() + .setTimestamp(Timestamp.newBuilder().setSeconds(100)) + .putFields(entityName, entityValue) + .build(); + List entityRows = + new ArrayList<>() { + { + add(entityRow1); + } + }; + + // Instantiate FeatureReferences + ValueProto.Value featureValue = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); + ServingAPIProto.FeatureReferenceV2 featureReference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(feature1) + .build(); + ValueProto.Value notFoundFeatureValue = ValueProto.Value.newBuilder().build(); + ServingAPIProto.FeatureReferenceV2 notFoundFeatureReference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(notFoundFeature) + .build(); + ValueProto.Value emptyFeatureValue = ValueProto.Value.newBuilder().build(); + ServingAPIProto.FeatureReferenceV2 emptyFeatureReference = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(emptyFeature) + .build(); + List featureReferences = + new ArrayList<>() { + { + add(featureReference); + add(notFoundFeatureReference); + add(emptyFeatureReference); + } + }; + + // Build GetOnlineFeaturesRequestV2 + GetOnlineFeaturesRequestV2 onlineFeatureRequest = + TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); + GetOnlineFeaturesResponse featureResponse = + servingStub.getOnlineFeaturesV2(onlineFeatureRequest); + + Map expectedValueMap = + new HashMap<>() { + { + put(entityName, entityValue); + put(FeatureV2.getFeatureStringRef(featureReference), featureValue); + put(FeatureV2.getFeatureStringRef(notFoundFeatureReference), notFoundFeatureValue); + put(FeatureV2.getFeatureStringRef(emptyFeatureReference), emptyFeatureValue); + } + }; + + Map expectedStatusMap = + new HashMap<>() { + { + put(entityName, GetOnlineFeaturesResponse.FieldStatus.PRESENT); + put( + FeatureV2.getFeatureStringRef(featureReference), + GetOnlineFeaturesResponse.FieldStatus.PRESENT); + put( + FeatureV2.getFeatureStringRef(notFoundFeatureReference), + GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND); + put( + FeatureV2.getFeatureStringRef(emptyFeatureReference), + GetOnlineFeaturesResponse.FieldStatus.NULL_VALUE); + } + }; + + GetOnlineFeaturesResponse.FieldValues expectedFieldValues = + GetOnlineFeaturesResponse.FieldValues.newBuilder() + .putAllFields(expectedValueMap) + .putAllStatuses(expectedStatusMap) + .build(); + List expectedFieldValuesList = + new ArrayList<>() { + { + add(expectedFieldValues); + } + }; + + assertEquals(1, 1); + assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); + + ((ManagedChannel) servingStub.getChannel()).shutdown(); + } +} diff --git a/serving/src/test/java/feast/serving/it/TestUtils.java b/serving/src/test/java/feast/serving/it/TestUtils.java new file mode 100644 index 0000000000..b39d405193 --- /dev/null +++ b/serving/src/test/java/feast/serving/it/TestUtils.java @@ -0,0 +1,151 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.serving.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import avro.shaded.com.google.common.collect.ImmutableMap; +import com.google.protobuf.Duration; +import feast.common.auth.credentials.OAuthCredentials; +import feast.proto.core.CoreServiceGrpc; +import feast.proto.core.CoreServiceGrpc.CoreServiceBlockingStub; +import feast.proto.core.DataSourceProto.DataSource; +import feast.proto.core.EntityProto.Entity; +import feast.proto.core.EntityProto.EntitySpecV2; +import feast.proto.core.FeatureProto.FeatureSpecV2; +import feast.proto.core.FeatureTableProto.FeatureTable; +import feast.proto.core.FeatureTableProto.FeatureTableSpec; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; +import feast.proto.serving.ServingServiceGrpc; +import feast.proto.types.ValueProto; +import io.grpc.CallCredentials; +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; +import java.util.*; +import java.util.stream.Collectors; + +public class TestUtils { + + public static ServingServiceGrpc.ServingServiceBlockingStub getServingServiceStub( + boolean isSecure, int feastServingPort, Map options) { + Channel secureChannel = + ManagedChannelBuilder.forAddress("localhost", feastServingPort).usePlaintext().build(); + + if (isSecure) { + CallCredentials callCredentials = null; + callCredentials = new OAuthCredentials(options); + return ServingServiceGrpc.newBlockingStub(secureChannel).withCallCredentials(callCredentials); + } else { + return ServingServiceGrpc.newBlockingStub(secureChannel); + } + } + + public static CoreSimpleAPIClient getApiClientForCore(int feastCorePort) { + Channel channel = + ManagedChannelBuilder.forAddress("localhost", feastCorePort).usePlaintext().build(); + + CoreServiceBlockingStub coreService = CoreServiceGrpc.newBlockingStub(channel); + + return new CoreSimpleAPIClient(coreService); + } + + public static FeatureTableSpec createFeatureTableSpec( + String name, + List entities, + Map features, + int maxAgeSecs, + Map labels) { + return FeatureTableSpec.newBuilder() + .setName(name) + .setMaxAge(Duration.newBuilder().setSeconds(maxAgeSecs).build()) + .addAllEntities(entities) + .addAllFeatures( + features.entrySet().stream() + .map( + entry -> + FeatureSpecV2.newBuilder() + .setName(entry.getKey()) + .setValueType(entry.getValue()) + .putAllLabels(labels) + .build()) + .collect(Collectors.toList())) + .setMaxAge(Duration.newBuilder().setSeconds(3600).build()) + .putAllLabels(labels) + .build(); + } + + public static DataSource createFileDataSourceSpec( + String fileURL, String fileFormat, String timestampColumn, String datePartitionColumn) { + return DataSource.newBuilder() + .setType(DataSource.SourceType.BATCH_FILE) + .setFileOptions( + DataSource.FileOptions.newBuilder() + .setFileFormat(fileFormat) + .setFileUrl(fileURL) + .build()) + .setTimestampColumn(timestampColumn) + .setDatePartitionColumn(datePartitionColumn) + .build(); + } + + public static GetOnlineFeaturesRequestV2 createOnlineFeatureRequest( + String projectName, + List featureReferences, + List entityRows) { + return GetOnlineFeaturesRequestV2.newBuilder() + .setProject(projectName) + .addAllFeatures(featureReferences) + .addAllEntityRows(entityRows) + .build(); + } + + public static void applyFeatureTable( + CoreSimpleAPIClient secureApiClient, + String projectName, + String featureTableName, + List entities, + HashMap features) { + FeatureTableSpec expectedFeatureTableSpec = + createFeatureTableSpec( + featureTableName, + entities, + new HashMap<>() { + { + putAll(features); + } + }, + 7200, + ImmutableMap.of("feat_key2", "feat_value2")) + .toBuilder() + .setBatchSource( + createFileDataSourceSpec("file:///path/to/file", "parquet", "ts_col", "")) + .build(); + secureApiClient.simpleApplyFeatureTable(expectedFeatureTableSpec); + FeatureTable actualFeatureTable = + secureApiClient.simpleGetFeatureTable(projectName, featureTableName); + assertEquals(expectedFeatureTableSpec.getName(), actualFeatureTable.getSpec().getName()); + } + + public static void applyEntity( + CoreSimpleAPIClient coreApiClient, String projectName, EntitySpecV2 entitySpec) { + coreApiClient.simpleApplyEntity(entitySpec); + String entityName = entitySpec.getName(); + Entity actualEntity = coreApiClient.getEntity(projectName, entityName); + assertEquals(entitySpec.getName(), actualEntity.getSpec().getName()); + } +} diff --git a/serving/src/test/resources/docker-compose/docker-compose-it.yml b/serving/src/test/resources/docker-compose/docker-compose-it.yml index 7f994e6d9e..edef066fd5 100644 --- a/serving/src/test/resources/docker-compose/docker-compose-it.yml +++ b/serving/src/test/resources/docker-compose/docker-compose-it.yml @@ -2,7 +2,7 @@ version: '3' services: core: - image: gcr.io/kf-feast/feast-core:latest + image: gcr.io/kf-feast/feast-core:develop volumes: - ./core/application-it.yml:/etc/feast/application.yml environment: diff --git a/storage/api/src/main/java/feast/storage/api/retriever/Feature.java b/storage/api/src/main/java/feast/storage/api/retriever/Feature.java new file mode 100644 index 0000000000..c6cee0883e --- /dev/null +++ b/storage/api/src/main/java/feast/storage/api/retriever/Feature.java @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.api.retriever; + +import com.google.auto.value.AutoValue; +import com.google.protobuf.Timestamp; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; +import feast.proto.types.ValueProto.Value; + +@AutoValue +public abstract class Feature { + + public abstract FeatureReferenceV2 getFeatureReference(); + + public abstract Value getFeatureValue(); + + public abstract Timestamp getEventTimestamp(); + + public static Builder builder() { + return new AutoValue_Feature.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setFeatureReference(FeatureReferenceV2 featureReference); + + public abstract Builder setFeatureValue(Value featureValue); + + public abstract Builder setEventTimestamp(Timestamp eventTimestamp); + + public abstract Feature build(); + } +} diff --git a/storage/api/src/main/java/feast/storage/api/retriever/FeatureTableRequest.java b/storage/api/src/main/java/feast/storage/api/retriever/FeatureTableRequest.java new file mode 100644 index 0000000000..6188a270c4 --- /dev/null +++ b/storage/api/src/main/java/feast/storage/api/retriever/FeatureTableRequest.java @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.api.retriever; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import feast.proto.core.FeatureTableProto.FeatureTableSpec; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@AutoValue +public abstract class FeatureTableRequest { + public abstract FeatureTableSpec getSpec(); + + public abstract ImmutableSet getFeatureReferences(); + + public static Builder newBuilder() { + return new AutoValue_FeatureTableRequest.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setSpec(FeatureTableSpec spec); + + abstract ImmutableSet.Builder featureReferencesBuilder(); + + public Builder addAllFeatureReferences(List featureReferenceList) { + featureReferencesBuilder().addAll(featureReferenceList); + return this; + } + + public Builder addFeatureReference(FeatureReferenceV2 featureReference) { + featureReferencesBuilder().add(featureReference); + return this; + } + + public abstract FeatureTableRequest build(); + } + + public Map getFeatureRefsByName() { + return getFeatureReferences().stream() + .collect( + Collectors.toMap(FeatureReferenceV2::getName, featureReference -> featureReference)); + } +} diff --git a/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetrieverV2.java b/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetrieverV2.java new file mode 100644 index 0000000000..224cf5fe44 --- /dev/null +++ b/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetrieverV2.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.api.retriever; + +import feast.proto.serving.ServingAPIProto; +import java.util.List; +import java.util.Optional; + +public interface OnlineRetrieverV2 { + /** + * Get online features for the given entity rows using data retrieved from the Feature references + * specified in FeatureTable request. + * + *

Each {@link Feature} optional in the returned list then corresponds to an {@link + * ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow} provided by the user. If feature for a + * given entity row is not found, will return an empty optional instead. The no. of {@link + * Feature} returned should match the no. of given {@link + * ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow}s + * + * @param project name of project to request features from. + * @param entityRows list of entity rows to request features for. + * @param featureReferences specifies the FeatureTable to retrieve data from + * @return list of {@link Feature}s corresponding to data retrieved for each entity row from + * FeatureTable specified in FeatureTable request. + */ + List>> getOnlineFeatures( + String project, + List entityRows, + List featureReferences); +} diff --git a/storage/connectors/pom.xml b/storage/connectors/pom.xml index 6cd949acfd..219680b1f5 100644 --- a/storage/connectors/pom.xml +++ b/storage/connectors/pom.xml @@ -46,6 +46,12 @@ ${project.version} + + dev.feast + feast-common + ${project.version} + + dev.feast feast-storage-api diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java new file mode 100644 index 0000000000..7ab5be32b3 --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java @@ -0,0 +1,195 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.retriever; + +import com.google.common.hash.Hashing; +import com.google.protobuf.Timestamp; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow; +import feast.proto.storage.RedisProto.RedisKeyV2; +import feast.proto.types.ValueProto; +import feast.storage.api.retriever.Feature; +import feast.storage.api.retriever.OnlineRetrieverV2; +import io.grpc.Status; +import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +public class RedisOnlineRetrieverV2 implements OnlineRetrieverV2 { + + private static final String timestampPrefix = "_ts"; + private final RedisCommands syncCommands; + + private RedisOnlineRetrieverV2(StatefulRedisConnection connection) { + this.syncCommands = connection.sync(); + } + + public static OnlineRetrieverV2 create(Map config) { + + StatefulRedisConnection connection = + RedisClient.create( + RedisURI.create(config.get("host"), Integer.parseInt(config.get("port")))) + .connect(new ByteArrayCodec()); + + return new RedisOnlineRetrieverV2(connection); + } + + public static OnlineRetrieverV2 create(StatefulRedisConnection connection) { + return new RedisOnlineRetrieverV2(connection); + } + + @Override + public List>> getOnlineFeatures( + String project, List entityRows, List featureReferences) { + + List redisKeys = buildRedisKeys(project, entityRows); + List>> features = getFeaturesFromRedis(redisKeys, featureReferences); + + return features; + } + + private List buildRedisKeys(String project, List entityRows) { + List redisKeys = + entityRows.stream() + .map(entityRow -> makeRedisKey(project, entityRow)) + .collect(Collectors.toList()); + + return redisKeys; + } + + /** + * Create {@link RedisKeyV2} + * + * @param project Project where request for features was called from + * @param entityRow {@link EntityRow} + * @return {@link RedisKeyV2} + */ + private RedisKeyV2 makeRedisKey(String project, EntityRow entityRow) { + RedisKeyV2.Builder builder = RedisKeyV2.newBuilder().setProject(project); + Map fieldsMap = entityRow.getFieldsMap(); + List entityNames = new ArrayList<>(new HashSet<>(fieldsMap.keySet())); + + // Sort entity names by alphabetical order + entityNames.sort(String::compareTo); + + for (String entityName : entityNames) { + builder.addEntityNames(entityName); + builder.addEntityValues(fieldsMap.get(entityName)); + } + return builder.build(); + } + + private List>> getFeaturesFromRedis( + List redisKeys, List featureReferences) { + List>> features = new ArrayList<>(); + // To decode bytes back to Feature Reference + Map byteToFeatureReferenceMap = new HashMap<>(); + // To check whether redis ValueK is a timestamp field + Map isTimestampMap = new HashMap<>(); + + // Serialize using proto + List binaryRedisKeys = + redisKeys.stream().map(redisKey -> redisKey.toByteArray()).collect(Collectors.toList()); + + try { + List> featureReferenceWithTsByteArrays = new ArrayList<>(); + featureReferences.stream() + .forEach( + featureReference -> { + List currentFeatureWithTsByteArrays = new ArrayList<>(); + + // eg. murmur() + String delimitedFeatureReference = + featureReference.getFeatureTable() + ":" + featureReference.getName(); + byte[] featureReferenceBytes = + Hashing.murmur3_32() + .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) + .asBytes(); + currentFeatureWithTsByteArrays.add(featureReferenceBytes); + isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); + byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); + + // eg. <_ts:featuretable_name> + String timestampFeatureTableReference = + timestampPrefix + ":" + featureReference.getFeatureTable(); + byte[] featureTableTsBytes = timestampFeatureTableReference.getBytes(); + isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); + currentFeatureWithTsByteArrays.add(featureTableTsBytes); + + featureReferenceWithTsByteArrays.add(currentFeatureWithTsByteArrays); + }); + + // Access redis keys and extract features + for (byte[] binaryRedisKey : binaryRedisKeys) { + List> curRedisKeyFeatures = new ArrayList<>(); + // Loop according to each FeatureReferenceV2 bytes + for (List currentFeatureReferenceWithTsByteArray : + featureReferenceWithTsByteArrays) { + FeatureReferenceV2 featureReference = null; + ValueProto.Value featureValue = null; + Timestamp eventTimestamp = null; + + // Always 2 fields (i.e feature and timestamp) + List> redisValuesList = + syncCommands.hmget( + binaryRedisKey, + currentFeatureReferenceWithTsByteArray.get(0), + currentFeatureReferenceWithTsByteArray.get(1)); + + for (int i = 0; i < redisValuesList.size(); i++) { + if (redisValuesList.get(i).hasValue()) { + byte[] redisValueK = redisValuesList.get(i).getKey(); + byte[] redisValueV = redisValuesList.get(i).getValue(); + + // Decode data from Redis into Feature object fields + if (isTimestampMap.get(Arrays.toString(redisValueK))) { + redisValueV = redisValuesList.get(i).getValueOrElse(new byte[0]); + eventTimestamp = Timestamp.parseFrom(redisValueV); + } else { + featureReference = byteToFeatureReferenceMap.get(redisValueK.toString()); + featureValue = ValueProto.Value.parseFrom(redisValueV); + } + } + } + // Check for null featureReference i.e key is not found + if (featureReference != null) { + Feature feature = + Feature.builder() + .setFeatureReference(featureReference) + .setFeatureValue(featureValue) + .setEventTimestamp(eventTimestamp) + .build(); + curRedisKeyFeatures.add(Optional.of(feature)); + } + } + features.add(curRedisKeyFeatures); + } + } catch (Exception e) { + throw Status.UNKNOWN + .withDescription("Unexpected error when pulling data from from Redis.") + .withCause(e) + .asRuntimeException(); + } + return features; + } +} From afd997fe4e54687f38c5a8d11004257cd5b7836e Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 12 Oct 2020 15:28:45 +0800 Subject: [PATCH 04/17] Update golang protos and mocks Signed-off-by: Terence --- go.mod | 2 +- go.sum | 4 +- sdk/go/mocks/serving_mock.go | 23 +- .../protos/feast/serving/ServingService.pb.go | 854 ++++++++++++------ sdk/go/protos/feast/storage/Redis.pb.go | 132 ++- 5 files changed, 717 insertions(+), 298 deletions(-) diff --git a/go.mod b/go.mod index fa19e694c8..6bb0e0a33c 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect - golang.org/x/tools v0.0.0-20201013053347-2db1cd791039 // indirect + golang.org/x/tools v0.0.0-20201011145850-ed2f50202694 // indirect google.golang.org/grpc v1.29.1 google.golang.org/protobuf v1.25.0 // indirect gopkg.in/russross/blackfriday.v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 5b6fd4a78e..14d81fc1b8 100644 --- a/go.sum +++ b/go.sum @@ -490,8 +490,8 @@ golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f h1:7+Nz9MyPqt2qMCTvNiRy1G0 golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b h1:07IVqnnzaip3TGyl/cy32V5YP3FguWG4BybYDTBNpm0= golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201013053347-2db1cd791039 h1:kLBxO4OPBgPwjg8Vvu+/0DCHIfDwYIGNFcD66NU9kpo= -golang.org/x/tools v0.0.0-20201013053347-2db1cd791039/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201011145850-ed2f50202694 h1:BANdcOVw3KTuUiyfDp7wrzCpkCe8UP3lowugJngxBTg= +golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/sdk/go/mocks/serving_mock.go b/sdk/go/mocks/serving_mock.go index bea754c1bf..de2020f602 100644 --- a/sdk/go/mocks/serving_mock.go +++ b/sdk/go/mocks/serving_mock.go @@ -6,10 +6,11 @@ package mock_serving import ( context "context" + reflect "reflect" + serving "github.com/feast-dev/feast/sdk/go/protos/feast/serving" gomock "github.com/golang/mock/gomock" grpc "google.golang.org/grpc" - reflect "reflect" ) // MockServingServiceClient is a mock of ServingServiceClient interface @@ -114,3 +115,23 @@ func (mr *MockServingServiceClientMockRecorder) GetOnlineFeatures(arg0, arg1 int varargs := append([]interface{}{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOnlineFeatures", reflect.TypeOf((*MockServingServiceClient)(nil).GetOnlineFeatures), varargs...) } + +// GetOnlineFeaturesV2 mocks base method +func (m *MockServingServiceClient) GetOnlineFeaturesV2(arg0 context.Context, arg1 *serving.GetOnlineFeaturesRequestV2, arg2 ...grpc.CallOption) (*serving.GetOnlineFeaturesResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetOnlineFeaturesV2", varargs...) + ret0, _ := ret[0].(*serving.GetOnlineFeaturesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOnlineFeaturesV2 indicates an expected call of GetOnlineFeaturesV2 +func (mr *MockServingServiceClientMockRecorder) GetOnlineFeaturesV2(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOnlineFeaturesV2", reflect.TypeOf((*MockServingServiceClient)(nil).GetOnlineFeaturesV2), varargs...) +} diff --git a/sdk/go/protos/feast/serving/ServingService.pb.go b/sdk/go/protos/feast/serving/ServingService.pb.go index 32a663d9ca..dc99f28510 100644 --- a/sdk/go/protos/feast/serving/ServingService.pb.go +++ b/sdk/go/protos/feast/serving/ServingService.pb.go @@ -305,7 +305,7 @@ func (x GetOnlineFeaturesResponse_FieldStatus) Number() protoreflect.EnumNumber // Deprecated: Use GetOnlineFeaturesResponse_FieldStatus.Descriptor instead. func (GetOnlineFeaturesResponse_FieldStatus) EnumDescriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{5, 0} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{7, 0} } type GetFeastServingInfoRequest struct { @@ -482,6 +482,63 @@ func (x *FeatureReference) GetFeatureSet() string { return "" } +type FeatureReferenceV2 struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Name of the Feature Table to retrieve the feature from. + FeatureTable string `protobuf:"bytes,1,opt,name=feature_table,json=featureTable,proto3" json:"feature_table,omitempty"` + // Name of the Feature to retrieve the feature from. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *FeatureReferenceV2) Reset() { + *x = FeatureReferenceV2{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_serving_ServingService_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FeatureReferenceV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeatureReferenceV2) ProtoMessage() {} + +func (x *FeatureReferenceV2) ProtoReflect() protoreflect.Message { + mi := &file_feast_serving_ServingService_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeatureReferenceV2.ProtoReflect.Descriptor instead. +func (*FeatureReferenceV2) Descriptor() ([]byte, []int) { + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{3} +} + +func (x *FeatureReferenceV2) GetFeatureTable() string { + if x != nil { + return x.FeatureTable + } + return "" +} + +func (x *FeatureReferenceV2) GetName() string { + if x != nil { + return x.Name + } + return "" +} + type GetOnlineFeaturesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -505,7 +562,7 @@ type GetOnlineFeaturesRequest struct { func (x *GetOnlineFeaturesRequest) Reset() { *x = GetOnlineFeaturesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[3] + mi := &file_feast_serving_ServingService_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -518,7 +575,7 @@ func (x *GetOnlineFeaturesRequest) String() string { func (*GetOnlineFeaturesRequest) ProtoMessage() {} func (x *GetOnlineFeaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[3] + mi := &file_feast_serving_ServingService_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -531,7 +588,7 @@ func (x *GetOnlineFeaturesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOnlineFeaturesRequest.ProtoReflect.Descriptor instead. func (*GetOnlineFeaturesRequest) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{3} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4} } func (x *GetOnlineFeaturesRequest) GetFeatures() []*FeatureReference { @@ -562,6 +619,76 @@ func (x *GetOnlineFeaturesRequest) GetProject() string { return "" } +type GetOnlineFeaturesRequestV2 struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // List of features that are being retrieved + Features []*FeatureReferenceV2 `protobuf:"bytes,4,rep,name=features,proto3" json:"features,omitempty"` + // List of entity rows, containing entity id and timestamp data. + // Used during retrieval of feature rows and for joining feature + // rows into a final dataset + EntityRows []*GetOnlineFeaturesRequestV2_EntityRow `protobuf:"bytes,2,rep,name=entity_rows,json=entityRows,proto3" json:"entity_rows,omitempty"` + // Optional field to specify project name override. If specified, uses the + // given project for retrieval. Overrides the projects specified in + // Feature References if both are specified. + Project string `protobuf:"bytes,5,opt,name=project,proto3" json:"project,omitempty"` +} + +func (x *GetOnlineFeaturesRequestV2) Reset() { + *x = GetOnlineFeaturesRequestV2{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_serving_ServingService_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetOnlineFeaturesRequestV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOnlineFeaturesRequestV2) ProtoMessage() {} + +func (x *GetOnlineFeaturesRequestV2) ProtoReflect() protoreflect.Message { + mi := &file_feast_serving_ServingService_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOnlineFeaturesRequestV2.ProtoReflect.Descriptor instead. +func (*GetOnlineFeaturesRequestV2) Descriptor() ([]byte, []int) { + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{5} +} + +func (x *GetOnlineFeaturesRequestV2) GetFeatures() []*FeatureReferenceV2 { + if x != nil { + return x.Features + } + return nil +} + +func (x *GetOnlineFeaturesRequestV2) GetEntityRows() []*GetOnlineFeaturesRequestV2_EntityRow { + if x != nil { + return x.EntityRows + } + return nil +} + +func (x *GetOnlineFeaturesRequestV2) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + type GetBatchFeaturesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -579,7 +706,7 @@ type GetBatchFeaturesRequest struct { func (x *GetBatchFeaturesRequest) Reset() { *x = GetBatchFeaturesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[4] + mi := &file_feast_serving_ServingService_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -592,7 +719,7 @@ func (x *GetBatchFeaturesRequest) String() string { func (*GetBatchFeaturesRequest) ProtoMessage() {} func (x *GetBatchFeaturesRequest) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[4] + mi := &file_feast_serving_ServingService_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -605,7 +732,7 @@ func (x *GetBatchFeaturesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBatchFeaturesRequest.ProtoReflect.Descriptor instead. func (*GetBatchFeaturesRequest) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{6} } func (x *GetBatchFeaturesRequest) GetFeatures() []*FeatureReference { @@ -641,7 +768,7 @@ type GetOnlineFeaturesResponse struct { func (x *GetOnlineFeaturesResponse) Reset() { *x = GetOnlineFeaturesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[5] + mi := &file_feast_serving_ServingService_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -654,7 +781,7 @@ func (x *GetOnlineFeaturesResponse) String() string { func (*GetOnlineFeaturesResponse) ProtoMessage() {} func (x *GetOnlineFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[5] + mi := &file_feast_serving_ServingService_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -667,7 +794,7 @@ func (x *GetOnlineFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetOnlineFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetOnlineFeaturesResponse) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{5} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{7} } func (x *GetOnlineFeaturesResponse) GetFieldValues() []*GetOnlineFeaturesResponse_FieldValues { @@ -688,7 +815,7 @@ type GetBatchFeaturesResponse struct { func (x *GetBatchFeaturesResponse) Reset() { *x = GetBatchFeaturesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[6] + mi := &file_feast_serving_ServingService_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -701,7 +828,7 @@ func (x *GetBatchFeaturesResponse) String() string { func (*GetBatchFeaturesResponse) ProtoMessage() {} func (x *GetBatchFeaturesResponse) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[6] + mi := &file_feast_serving_ServingService_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -714,7 +841,7 @@ func (x *GetBatchFeaturesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetBatchFeaturesResponse.ProtoReflect.Descriptor instead. func (*GetBatchFeaturesResponse) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{6} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{8} } func (x *GetBatchFeaturesResponse) GetJob() *Job { @@ -735,7 +862,7 @@ type GetJobRequest struct { func (x *GetJobRequest) Reset() { *x = GetJobRequest{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[7] + mi := &file_feast_serving_ServingService_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -748,7 +875,7 @@ func (x *GetJobRequest) String() string { func (*GetJobRequest) ProtoMessage() {} func (x *GetJobRequest) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[7] + mi := &file_feast_serving_ServingService_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -761,7 +888,7 @@ func (x *GetJobRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetJobRequest.ProtoReflect.Descriptor instead. func (*GetJobRequest) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{7} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{9} } func (x *GetJobRequest) GetJob() *Job { @@ -782,7 +909,7 @@ type GetJobResponse struct { func (x *GetJobResponse) Reset() { *x = GetJobResponse{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[8] + mi := &file_feast_serving_ServingService_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -795,7 +922,7 @@ func (x *GetJobResponse) String() string { func (*GetJobResponse) ProtoMessage() {} func (x *GetJobResponse) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[8] + mi := &file_feast_serving_ServingService_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -808,7 +935,7 @@ func (x *GetJobResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetJobResponse.ProtoReflect.Descriptor instead. func (*GetJobResponse) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{8} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{10} } func (x *GetJobResponse) GetJob() *Job { @@ -844,7 +971,7 @@ type Job struct { func (x *Job) Reset() { *x = Job{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[9] + mi := &file_feast_serving_ServingService_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -857,7 +984,7 @@ func (x *Job) String() string { func (*Job) ProtoMessage() {} func (x *Job) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[9] + mi := &file_feast_serving_ServingService_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -870,7 +997,7 @@ func (x *Job) ProtoReflect() protoreflect.Message { // Deprecated: Use Job.ProtoReflect.Descriptor instead. func (*Job) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{9} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{11} } func (x *Job) GetId() string { @@ -935,7 +1062,7 @@ type DatasetSource struct { func (x *DatasetSource) Reset() { *x = DatasetSource{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[10] + mi := &file_feast_serving_ServingService_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -948,7 +1075,7 @@ func (x *DatasetSource) String() string { func (*DatasetSource) ProtoMessage() {} func (x *DatasetSource) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[10] + mi := &file_feast_serving_ServingService_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -961,7 +1088,7 @@ func (x *DatasetSource) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasetSource.ProtoReflect.Descriptor instead. func (*DatasetSource) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{10} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{12} } func (m *DatasetSource) GetDatasetSource() isDatasetSource_DatasetSource { @@ -1004,7 +1131,7 @@ type GetOnlineFeaturesRequest_EntityRow struct { func (x *GetOnlineFeaturesRequest_EntityRow) Reset() { *x = GetOnlineFeaturesRequest_EntityRow{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[11] + mi := &file_feast_serving_ServingService_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1017,7 +1144,7 @@ func (x *GetOnlineFeaturesRequest_EntityRow) String() string { func (*GetOnlineFeaturesRequest_EntityRow) ProtoMessage() {} func (x *GetOnlineFeaturesRequest_EntityRow) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[11] + mi := &file_feast_serving_ServingService_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1030,7 +1157,7 @@ func (x *GetOnlineFeaturesRequest_EntityRow) ProtoReflect() protoreflect.Message // Deprecated: Use GetOnlineFeaturesRequest_EntityRow.ProtoReflect.Descriptor instead. func (*GetOnlineFeaturesRequest_EntityRow) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{3, 0} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{4, 0} } func (x *GetOnlineFeaturesRequest_EntityRow) GetEntityTimestamp() *timestamp.Timestamp { @@ -1047,6 +1174,64 @@ func (x *GetOnlineFeaturesRequest_EntityRow) GetFields() map[string]*types.Value return nil } +type GetOnlineFeaturesRequestV2_EntityRow struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Request timestamp of this row. This value will be used, + // together with maxAge, to determine feature staleness. + Timestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // Map containing mapping of entity name to entity value. + Fields map[string]*types.Value `protobuf:"bytes,2,rep,name=fields,proto3" json:"fields,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *GetOnlineFeaturesRequestV2_EntityRow) Reset() { + *x = GetOnlineFeaturesRequestV2_EntityRow{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_serving_ServingService_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetOnlineFeaturesRequestV2_EntityRow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetOnlineFeaturesRequestV2_EntityRow) ProtoMessage() {} + +func (x *GetOnlineFeaturesRequestV2_EntityRow) ProtoReflect() protoreflect.Message { + mi := &file_feast_serving_ServingService_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetOnlineFeaturesRequestV2_EntityRow.ProtoReflect.Descriptor instead. +func (*GetOnlineFeaturesRequestV2_EntityRow) Descriptor() ([]byte, []int) { + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *GetOnlineFeaturesRequestV2_EntityRow) GetTimestamp() *timestamp.Timestamp { + if x != nil { + return x.Timestamp + } + return nil +} + +func (x *GetOnlineFeaturesRequestV2_EntityRow) GetFields() map[string]*types.Value { + if x != nil { + return x.Fields + } + return nil +} + type GetOnlineFeaturesResponse_FieldValues struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1062,7 +1247,7 @@ type GetOnlineFeaturesResponse_FieldValues struct { func (x *GetOnlineFeaturesResponse_FieldValues) Reset() { *x = GetOnlineFeaturesResponse_FieldValues{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[13] + mi := &file_feast_serving_ServingService_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1075,7 +1260,7 @@ func (x *GetOnlineFeaturesResponse_FieldValues) String() string { func (*GetOnlineFeaturesResponse_FieldValues) ProtoMessage() {} func (x *GetOnlineFeaturesResponse_FieldValues) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[13] + mi := &file_feast_serving_ServingService_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1088,7 +1273,7 @@ func (x *GetOnlineFeaturesResponse_FieldValues) ProtoReflect() protoreflect.Mess // Deprecated: Use GetOnlineFeaturesResponse_FieldValues.ProtoReflect.Descriptor instead. func (*GetOnlineFeaturesResponse_FieldValues) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{5, 0} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{7, 0} } func (x *GetOnlineFeaturesResponse_FieldValues) GetFields() map[string]*types.Value { @@ -1121,7 +1306,7 @@ type DatasetSource_FileSource struct { func (x *DatasetSource_FileSource) Reset() { *x = DatasetSource_FileSource{} if protoimpl.UnsafeEnabled { - mi := &file_feast_serving_ServingService_proto_msgTypes[16] + mi := &file_feast_serving_ServingService_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1134,7 +1319,7 @@ func (x *DatasetSource_FileSource) String() string { func (*DatasetSource_FileSource) ProtoMessage() {} func (x *DatasetSource_FileSource) ProtoReflect() protoreflect.Message { - mi := &file_feast_serving_ServingService_proto_msgTypes[16] + mi := &file_feast_serving_ServingService_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1147,7 +1332,7 @@ func (x *DatasetSource_FileSource) ProtoReflect() protoreflect.Message { // Deprecated: Use DatasetSource_FileSource.ProtoReflect.Descriptor instead. func (*DatasetSource_FileSource) Descriptor() ([]byte, []int) { - return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{10, 0} + return file_feast_serving_ServingService_proto_rawDescGZIP(), []int{12, 0} } func (x *DatasetSource_FileSource) GetFileUris() []string { @@ -1195,189 +1380,229 @@ var file_feast_serving_ServingService_proto_rawDesc = []byte{ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x4a, 0x04, - 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0xfb, 0x03, 0x0a, 0x18, 0x47, - 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, - 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x12, 0x52, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x72, - 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, - 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x52, 0x0a, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x39, 0x0a, 0x19, 0x6f, 0x6d, 0x69, 0x74, - 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x5f, 0x72, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x6f, 0x6d, 0x69, - 0x74, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xf8, 0x01, - 0x0a, 0x09, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x12, 0x45, 0x0a, 0x10, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x12, 0x55, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, - 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x69, - 0x74, 0x79, 0x52, 0x6f, 0x77, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xca, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, - 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, - 0x73, 0x12, 0x43, 0x0a, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x73, 0x65, - 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, - 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, - 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x11, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x69, - 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xdd, 0x04, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, - 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, - 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, - 0x0b, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x89, 0x03, 0x0a, - 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x06, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, - 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x73, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x5e, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x08, 0x03, 0x10, 0x04, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x4d, 0x0a, 0x12, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x56, 0x32, + 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x74, 0x61, 0x62, 0x6c, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x54, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xfb, 0x03, 0x0a, 0x18, 0x47, 0x65, + 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x12, 0x52, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x72, 0x6f, + 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, - 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x1a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, + 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x52, 0x0a, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x39, 0x0a, 0x19, 0x6f, 0x6d, 0x69, 0x74, 0x5f, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x5f, 0x69, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x16, 0x6f, 0x6d, 0x69, 0x74, + 0x45, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x49, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xf8, 0x01, 0x0a, + 0x09, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x12, 0x45, 0x0a, 0x10, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x12, 0x55, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x3d, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x6f, 0x77, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xbb, 0x03, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x4f, + 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x56, 0x32, 0x12, 0x3d, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x56, 0x32, 0x52, 0x08, 0x66, 0x65, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x54, 0x0a, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, + 0x72, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x66, 0x65, 0x61, + 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, + 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x56, 0x32, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x52, + 0x0a, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x6f, 0x77, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x1a, 0xed, 0x01, 0x0a, 0x09, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x52, 0x6f, 0x77, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x57, 0x0a, + 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3f, 0x2e, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, + 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x56, 0x32, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, + 0x6f, 0x77, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x71, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xca, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, + 0x63, 0x68, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x3b, 0x0a, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x43, + 0x0a, 0x0e, 0x64, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x0d, 0x64, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x11, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x22, 0xdd, 0x04, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x57, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x0b, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x89, 0x03, 0x0a, 0x0b, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x06, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, + 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x73, 0x12, 0x5e, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x65, 0x73, 0x1a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x71, 0x0a, 0x0d, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, + 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x45, 0x53, 0x45, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x0e, + 0x0a, 0x0a, 0x4e, 0x55, 0x4c, 0x4c, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, 0x02, 0x12, 0x0d, + 0x0a, 0x09, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x03, 0x12, 0x13, 0x0a, + 0x0f, 0x4f, 0x55, 0x54, 0x53, 0x49, 0x44, 0x45, 0x5f, 0x4d, 0x41, 0x58, 0x5f, 0x41, 0x47, 0x45, + 0x10, 0x04, 0x22, 0x40, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, + 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x52, + 0x03, 0x6a, 0x6f, 0x62, 0x22, 0x35, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0x36, 0x0a, 0x0e, 0x47, + 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, + 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, + 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, + 0x6a, 0x6f, 0x62, 0x22, 0xdf, 0x02, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, + 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x69, 0x73, 0x12, 0x3a, 0x0a, 0x0b, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x19, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x0a, 0x64, 0x61, + 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x7b, 0x0a, 0x1f, 0x64, 0x61, 0x74, 0x61, + 0x73, 0x65, 0x74, 0x5f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x76, 0x30, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x73, + 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, + 0x69, 0x63, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x1c, 0x64, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x4c, 0x69, 0x73, 0x74, 0x22, 0xd4, 0x01, 0x0a, 0x0d, 0x44, 0x61, 0x74, 0x61, 0x73, 0x65, + 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x66, 0x69, 0x6c, 0x65, 0x5f, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x61, 0x74, + 0x61, 0x73, 0x65, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x65, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x1a, 0x65, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x69, 0x73, 0x12, 0x3a, + 0x0a, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x0a, + 0x64, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x42, 0x10, 0x0a, 0x0e, 0x64, 0x61, + 0x74, 0x61, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2a, 0x6f, 0x0a, 0x10, + 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x1e, 0x0a, 0x1a, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, + 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, + 0x12, 0x1d, 0x0a, 0x19, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, + 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x01, 0x12, + 0x1c, 0x0a, 0x18, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x41, 0x54, 0x43, 0x48, 0x10, 0x02, 0x2a, 0x36, 0x0a, + 0x07, 0x4a, 0x6f, 0x62, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x4a, 0x4f, 0x42, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x15, + 0x0a, 0x11, 0x4a, 0x4f, 0x42, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, + 0x4f, 0x41, 0x44, 0x10, 0x01, 0x2a, 0x68, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4a, 0x4f, + 0x42, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, + 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x4a, 0x4f, + 0x42, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x4f, 0x4e, 0x45, 0x10, 0x03, 0x2a, + 0x3b, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x17, 0x0a, + 0x13, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x49, 0x4e, 0x56, + 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, + 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x41, 0x56, 0x52, 0x4f, 0x10, 0x01, 0x32, 0xfe, 0x03, 0x0a, + 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x6c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2a, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, + 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, + 0x11, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, + 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, + 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x56, 0x32, 0x12, 0x29, 0x2e, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, + 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x56, 0x32, 0x1a, 0x28, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x5b, 0x0a, 0x0b, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, - 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x52, 0x45, 0x53, 0x45, 0x4e, 0x54, 0x10, - 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x4e, 0x55, 0x4c, 0x4c, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, - 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x54, 0x5f, 0x46, 0x4f, 0x55, 0x4e, 0x44, 0x10, 0x03, - 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x55, 0x54, 0x53, 0x49, 0x44, 0x45, 0x5f, 0x4d, 0x41, 0x58, 0x5f, - 0x41, 0x47, 0x45, 0x10, 0x04, 0x22, 0x40, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, - 0x68, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, - 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, - 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0x35, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4a, 0x6f, - 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0x36, - 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x24, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, - 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0xdf, 0x02, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x0e, - 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2a, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x18, 0x2e, 0x66, 0x65, 0x61, - 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x4a, 0x6f, 0x62, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x69, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x69, 0x73, 0x12, - 0x3a, 0x0a, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, - 0x0a, 0x64, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x7b, 0x0a, 0x1f, 0x64, - 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x5f, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x6e, 0x73, 0x6f, 0x72, 0x66, 0x6c, 0x6f, - 0x77, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x76, 0x30, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x73, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x69, 0x73, 0x74, 0x69, 0x63, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x1c, 0x64, 0x61, 0x74, 0x61, - 0x73, 0x65, 0x74, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x74, 0x61, 0x74, 0x69, 0x73, - 0x74, 0x69, 0x63, 0x73, 0x4c, 0x69, 0x73, 0x74, 0x22, 0xd4, 0x01, 0x0a, 0x0d, 0x44, 0x61, 0x74, - 0x61, 0x73, 0x65, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x66, 0x69, - 0x6c, 0x65, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x27, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, - 0x44, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x66, 0x69, 0x6c, 0x65, - 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x65, 0x0a, 0x0a, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x69, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x72, 0x69, - 0x73, 0x12, 0x3a, 0x0a, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x42, 0x10, 0x0a, - 0x0e, 0x64, 0x61, 0x74, 0x61, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2a, - 0x6f, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x1e, 0x0a, 0x1a, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, - 0x56, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, - 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, - 0x56, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4f, 0x4e, 0x4c, 0x49, 0x4e, 0x45, - 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x45, 0x41, 0x53, 0x54, 0x5f, 0x53, 0x45, 0x52, 0x56, - 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x41, 0x54, 0x43, 0x48, 0x10, 0x02, - 0x2a, 0x36, 0x0a, 0x07, 0x4a, 0x6f, 0x62, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x4a, - 0x4f, 0x42, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, - 0x00, 0x12, 0x15, 0x0a, 0x11, 0x4a, 0x4f, 0x42, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x4f, - 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x10, 0x01, 0x2a, 0x68, 0x0a, 0x09, 0x4a, 0x6f, 0x62, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x16, 0x0a, - 0x12, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, - 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, - 0x0f, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x4f, 0x4e, 0x45, - 0x10, 0x03, 0x2a, 0x3b, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, - 0x12, 0x17, 0x0a, 0x13, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, - 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x41, 0x54, - 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x41, 0x56, 0x52, 0x4f, 0x10, 0x01, 0x32, - 0x92, 0x03, 0x0a, 0x0e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x29, 0x2e, 0x66, 0x65, 0x61, 0x73, - 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, - 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x65, 0x61, 0x73, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x27, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, - 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, + 0x65, 0x12, 0x63, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, 0x61, + 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, + 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, + 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x06, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, + 0x12, 0x1c, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, + 0x2e, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, - 0x65, 0x74, 0x4f, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x66, - 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, - 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x61, 0x74, 0x63, 0x68, 0x46, 0x65, 0x61, - 0x74, 0x75, 0x72, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, - 0x06, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x12, 0x1c, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x6e, 0x67, 0x2e, 0x47, 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x5e, 0x0a, 0x13, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x42, 0x0f, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x41, 0x50, 0x49, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x36, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, - 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x74, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x5e, 0x0a, + 0x13, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x6e, 0x67, 0x42, 0x0f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x41, 0x50, 0x49, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, + 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1393,7 +1618,7 @@ func file_feast_serving_ServingService_proto_rawDescGZIP() []byte { } var file_feast_serving_ServingService_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_feast_serving_ServingService_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_feast_serving_ServingService_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_feast_serving_ServingService_proto_goTypes = []interface{}{ (FeastServingType)(0), // 0: feast.serving.FeastServingType (JobType)(0), // 1: feast.serving.JobType @@ -1403,60 +1628,71 @@ var file_feast_serving_ServingService_proto_goTypes = []interface{}{ (*GetFeastServingInfoRequest)(nil), // 5: feast.serving.GetFeastServingInfoRequest (*GetFeastServingInfoResponse)(nil), // 6: feast.serving.GetFeastServingInfoResponse (*FeatureReference)(nil), // 7: feast.serving.FeatureReference - (*GetOnlineFeaturesRequest)(nil), // 8: feast.serving.GetOnlineFeaturesRequest - (*GetBatchFeaturesRequest)(nil), // 9: feast.serving.GetBatchFeaturesRequest - (*GetOnlineFeaturesResponse)(nil), // 10: feast.serving.GetOnlineFeaturesResponse - (*GetBatchFeaturesResponse)(nil), // 11: feast.serving.GetBatchFeaturesResponse - (*GetJobRequest)(nil), // 12: feast.serving.GetJobRequest - (*GetJobResponse)(nil), // 13: feast.serving.GetJobResponse - (*Job)(nil), // 14: feast.serving.Job - (*DatasetSource)(nil), // 15: feast.serving.DatasetSource - (*GetOnlineFeaturesRequest_EntityRow)(nil), // 16: feast.serving.GetOnlineFeaturesRequest.EntityRow - nil, // 17: feast.serving.GetOnlineFeaturesRequest.EntityRow.FieldsEntry - (*GetOnlineFeaturesResponse_FieldValues)(nil), // 18: feast.serving.GetOnlineFeaturesResponse.FieldValues - nil, // 19: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry - nil, // 20: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry - (*DatasetSource_FileSource)(nil), // 21: feast.serving.DatasetSource.FileSource - (*v0.DatasetFeatureStatisticsList)(nil), // 22: tensorflow.metadata.v0.DatasetFeatureStatisticsList - (*timestamp.Timestamp)(nil), // 23: google.protobuf.Timestamp - (*types.Value)(nil), // 24: feast.types.Value + (*FeatureReferenceV2)(nil), // 8: feast.serving.FeatureReferenceV2 + (*GetOnlineFeaturesRequest)(nil), // 9: feast.serving.GetOnlineFeaturesRequest + (*GetOnlineFeaturesRequestV2)(nil), // 10: feast.serving.GetOnlineFeaturesRequestV2 + (*GetBatchFeaturesRequest)(nil), // 11: feast.serving.GetBatchFeaturesRequest + (*GetOnlineFeaturesResponse)(nil), // 12: feast.serving.GetOnlineFeaturesResponse + (*GetBatchFeaturesResponse)(nil), // 13: feast.serving.GetBatchFeaturesResponse + (*GetJobRequest)(nil), // 14: feast.serving.GetJobRequest + (*GetJobResponse)(nil), // 15: feast.serving.GetJobResponse + (*Job)(nil), // 16: feast.serving.Job + (*DatasetSource)(nil), // 17: feast.serving.DatasetSource + (*GetOnlineFeaturesRequest_EntityRow)(nil), // 18: feast.serving.GetOnlineFeaturesRequest.EntityRow + nil, // 19: feast.serving.GetOnlineFeaturesRequest.EntityRow.FieldsEntry + (*GetOnlineFeaturesRequestV2_EntityRow)(nil), // 20: feast.serving.GetOnlineFeaturesRequestV2.EntityRow + nil, // 21: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry + (*GetOnlineFeaturesResponse_FieldValues)(nil), // 22: feast.serving.GetOnlineFeaturesResponse.FieldValues + nil, // 23: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry + nil, // 24: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry + (*DatasetSource_FileSource)(nil), // 25: feast.serving.DatasetSource.FileSource + (*v0.DatasetFeatureStatisticsList)(nil), // 26: tensorflow.metadata.v0.DatasetFeatureStatisticsList + (*timestamp.Timestamp)(nil), // 27: google.protobuf.Timestamp + (*types.Value)(nil), // 28: feast.types.Value } var file_feast_serving_ServingService_proto_depIdxs = []int32{ 0, // 0: feast.serving.GetFeastServingInfoResponse.type:type_name -> feast.serving.FeastServingType 7, // 1: feast.serving.GetOnlineFeaturesRequest.features:type_name -> feast.serving.FeatureReference - 16, // 2: feast.serving.GetOnlineFeaturesRequest.entity_rows:type_name -> feast.serving.GetOnlineFeaturesRequest.EntityRow - 7, // 3: feast.serving.GetBatchFeaturesRequest.features:type_name -> feast.serving.FeatureReference - 15, // 4: feast.serving.GetBatchFeaturesRequest.dataset_source:type_name -> feast.serving.DatasetSource - 18, // 5: feast.serving.GetOnlineFeaturesResponse.field_values:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues - 14, // 6: feast.serving.GetBatchFeaturesResponse.job:type_name -> feast.serving.Job - 14, // 7: feast.serving.GetJobRequest.job:type_name -> feast.serving.Job - 14, // 8: feast.serving.GetJobResponse.job:type_name -> feast.serving.Job - 1, // 9: feast.serving.Job.type:type_name -> feast.serving.JobType - 2, // 10: feast.serving.Job.status:type_name -> feast.serving.JobStatus - 3, // 11: feast.serving.Job.data_format:type_name -> feast.serving.DataFormat - 22, // 12: feast.serving.Job.dataset_feature_statistics_list:type_name -> tensorflow.metadata.v0.DatasetFeatureStatisticsList - 21, // 13: feast.serving.DatasetSource.file_source:type_name -> feast.serving.DatasetSource.FileSource - 23, // 14: feast.serving.GetOnlineFeaturesRequest.EntityRow.entity_timestamp:type_name -> google.protobuf.Timestamp - 17, // 15: feast.serving.GetOnlineFeaturesRequest.EntityRow.fields:type_name -> feast.serving.GetOnlineFeaturesRequest.EntityRow.FieldsEntry - 24, // 16: feast.serving.GetOnlineFeaturesRequest.EntityRow.FieldsEntry.value:type_name -> feast.types.Value - 19, // 17: feast.serving.GetOnlineFeaturesResponse.FieldValues.fields:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry - 20, // 18: feast.serving.GetOnlineFeaturesResponse.FieldValues.statuses:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry - 24, // 19: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry.value:type_name -> feast.types.Value - 4, // 20: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry.value:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldStatus - 3, // 21: feast.serving.DatasetSource.FileSource.data_format:type_name -> feast.serving.DataFormat - 5, // 22: feast.serving.ServingService.GetFeastServingInfo:input_type -> feast.serving.GetFeastServingInfoRequest - 8, // 23: feast.serving.ServingService.GetOnlineFeatures:input_type -> feast.serving.GetOnlineFeaturesRequest - 9, // 24: feast.serving.ServingService.GetBatchFeatures:input_type -> feast.serving.GetBatchFeaturesRequest - 12, // 25: feast.serving.ServingService.GetJob:input_type -> feast.serving.GetJobRequest - 6, // 26: feast.serving.ServingService.GetFeastServingInfo:output_type -> feast.serving.GetFeastServingInfoResponse - 10, // 27: feast.serving.ServingService.GetOnlineFeatures:output_type -> feast.serving.GetOnlineFeaturesResponse - 11, // 28: feast.serving.ServingService.GetBatchFeatures:output_type -> feast.serving.GetBatchFeaturesResponse - 13, // 29: feast.serving.ServingService.GetJob:output_type -> feast.serving.GetJobResponse - 26, // [26:30] is the sub-list for method output_type - 22, // [22:26] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 18, // 2: feast.serving.GetOnlineFeaturesRequest.entity_rows:type_name -> feast.serving.GetOnlineFeaturesRequest.EntityRow + 8, // 3: feast.serving.GetOnlineFeaturesRequestV2.features:type_name -> feast.serving.FeatureReferenceV2 + 20, // 4: feast.serving.GetOnlineFeaturesRequestV2.entity_rows:type_name -> feast.serving.GetOnlineFeaturesRequestV2.EntityRow + 7, // 5: feast.serving.GetBatchFeaturesRequest.features:type_name -> feast.serving.FeatureReference + 17, // 6: feast.serving.GetBatchFeaturesRequest.dataset_source:type_name -> feast.serving.DatasetSource + 22, // 7: feast.serving.GetOnlineFeaturesResponse.field_values:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues + 16, // 8: feast.serving.GetBatchFeaturesResponse.job:type_name -> feast.serving.Job + 16, // 9: feast.serving.GetJobRequest.job:type_name -> feast.serving.Job + 16, // 10: feast.serving.GetJobResponse.job:type_name -> feast.serving.Job + 1, // 11: feast.serving.Job.type:type_name -> feast.serving.JobType + 2, // 12: feast.serving.Job.status:type_name -> feast.serving.JobStatus + 3, // 13: feast.serving.Job.data_format:type_name -> feast.serving.DataFormat + 26, // 14: feast.serving.Job.dataset_feature_statistics_list:type_name -> tensorflow.metadata.v0.DatasetFeatureStatisticsList + 25, // 15: feast.serving.DatasetSource.file_source:type_name -> feast.serving.DatasetSource.FileSource + 27, // 16: feast.serving.GetOnlineFeaturesRequest.EntityRow.entity_timestamp:type_name -> google.protobuf.Timestamp + 19, // 17: feast.serving.GetOnlineFeaturesRequest.EntityRow.fields:type_name -> feast.serving.GetOnlineFeaturesRequest.EntityRow.FieldsEntry + 28, // 18: feast.serving.GetOnlineFeaturesRequest.EntityRow.FieldsEntry.value:type_name -> feast.types.Value + 27, // 19: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.timestamp:type_name -> google.protobuf.Timestamp + 21, // 20: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.fields:type_name -> feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry + 28, // 21: feast.serving.GetOnlineFeaturesRequestV2.EntityRow.FieldsEntry.value:type_name -> feast.types.Value + 23, // 22: feast.serving.GetOnlineFeaturesResponse.FieldValues.fields:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry + 24, // 23: feast.serving.GetOnlineFeaturesResponse.FieldValues.statuses:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry + 28, // 24: feast.serving.GetOnlineFeaturesResponse.FieldValues.FieldsEntry.value:type_name -> feast.types.Value + 4, // 25: feast.serving.GetOnlineFeaturesResponse.FieldValues.StatusesEntry.value:type_name -> feast.serving.GetOnlineFeaturesResponse.FieldStatus + 3, // 26: feast.serving.DatasetSource.FileSource.data_format:type_name -> feast.serving.DataFormat + 5, // 27: feast.serving.ServingService.GetFeastServingInfo:input_type -> feast.serving.GetFeastServingInfoRequest + 9, // 28: feast.serving.ServingService.GetOnlineFeatures:input_type -> feast.serving.GetOnlineFeaturesRequest + 10, // 29: feast.serving.ServingService.GetOnlineFeaturesV2:input_type -> feast.serving.GetOnlineFeaturesRequestV2 + 11, // 30: feast.serving.ServingService.GetBatchFeatures:input_type -> feast.serving.GetBatchFeaturesRequest + 14, // 31: feast.serving.ServingService.GetJob:input_type -> feast.serving.GetJobRequest + 6, // 32: feast.serving.ServingService.GetFeastServingInfo:output_type -> feast.serving.GetFeastServingInfoResponse + 12, // 33: feast.serving.ServingService.GetOnlineFeatures:output_type -> feast.serving.GetOnlineFeaturesResponse + 12, // 34: feast.serving.ServingService.GetOnlineFeaturesV2:output_type -> feast.serving.GetOnlineFeaturesResponse + 13, // 35: feast.serving.ServingService.GetBatchFeatures:output_type -> feast.serving.GetBatchFeaturesResponse + 15, // 36: feast.serving.ServingService.GetJob:output_type -> feast.serving.GetJobResponse + 32, // [32:37] is the sub-list for method output_type + 27, // [27:32] is the sub-list for method input_type + 27, // [27:27] is the sub-list for extension type_name + 27, // [27:27] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name } func init() { file_feast_serving_ServingService_proto_init() } @@ -1502,7 +1738,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetOnlineFeaturesRequest); i { + switch v := v.(*FeatureReferenceV2); i { case 0: return &v.state case 1: @@ -1514,7 +1750,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetBatchFeaturesRequest); i { + switch v := v.(*GetOnlineFeaturesRequest); i { case 0: return &v.state case 1: @@ -1526,7 +1762,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetOnlineFeaturesResponse); i { + switch v := v.(*GetOnlineFeaturesRequestV2); i { case 0: return &v.state case 1: @@ -1538,7 +1774,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetBatchFeaturesResponse); i { + switch v := v.(*GetBatchFeaturesRequest); i { case 0: return &v.state case 1: @@ -1550,7 +1786,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetJobRequest); i { + switch v := v.(*GetOnlineFeaturesResponse); i { case 0: return &v.state case 1: @@ -1562,7 +1798,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetJobResponse); i { + switch v := v.(*GetBatchFeaturesResponse); i { case 0: return &v.state case 1: @@ -1574,7 +1810,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Job); i { + switch v := v.(*GetJobRequest); i { case 0: return &v.state case 1: @@ -1586,7 +1822,7 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DatasetSource); i { + switch v := v.(*GetJobResponse); i { case 0: return &v.state case 1: @@ -1598,7 +1834,19 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetOnlineFeaturesRequest_EntityRow); i { + switch v := v.(*Job); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_serving_ServingService_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DatasetSource); i { case 0: return &v.state case 1: @@ -1610,6 +1858,30 @@ func file_feast_serving_ServingService_proto_init() { } } file_feast_serving_ServingService_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetOnlineFeaturesRequest_EntityRow); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_serving_ServingService_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetOnlineFeaturesRequestV2_EntityRow); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_feast_serving_ServingService_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetOnlineFeaturesResponse_FieldValues); i { case 0: return &v.state @@ -1621,7 +1893,7 @@ func file_feast_serving_ServingService_proto_init() { return nil } } - file_feast_serving_ServingService_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_feast_serving_ServingService_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DatasetSource_FileSource); i { case 0: return &v.state @@ -1634,7 +1906,7 @@ func file_feast_serving_ServingService_proto_init() { } } } - file_feast_serving_ServingService_proto_msgTypes[10].OneofWrappers = []interface{}{ + file_feast_serving_ServingService_proto_msgTypes[12].OneofWrappers = []interface{}{ (*DatasetSource_FileSource_)(nil), } type x struct{} @@ -1643,7 +1915,7 @@ func file_feast_serving_ServingService_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_feast_serving_ServingService_proto_rawDesc, NumEnums: 5, - NumMessages: 17, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, @@ -1674,6 +1946,8 @@ type ServingServiceClient interface { GetFeastServingInfo(ctx context.Context, in *GetFeastServingInfoRequest, opts ...grpc.CallOption) (*GetFeastServingInfoResponse, error) // Get online features synchronously. GetOnlineFeatures(ctx context.Context, in *GetOnlineFeaturesRequest, opts ...grpc.CallOption) (*GetOnlineFeaturesResponse, error) + // Get online features (v2) synchronously. + GetOnlineFeaturesV2(ctx context.Context, in *GetOnlineFeaturesRequestV2, opts ...grpc.CallOption) (*GetOnlineFeaturesResponse, error) // Get batch features asynchronously. // // The client should check the status of the returned job periodically by @@ -1713,6 +1987,15 @@ func (c *servingServiceClient) GetOnlineFeatures(ctx context.Context, in *GetOnl return out, nil } +func (c *servingServiceClient) GetOnlineFeaturesV2(ctx context.Context, in *GetOnlineFeaturesRequestV2, opts ...grpc.CallOption) (*GetOnlineFeaturesResponse, error) { + out := new(GetOnlineFeaturesResponse) + err := c.cc.Invoke(ctx, "/feast.serving.ServingService/GetOnlineFeaturesV2", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *servingServiceClient) GetBatchFeatures(ctx context.Context, in *GetBatchFeaturesRequest, opts ...grpc.CallOption) (*GetBatchFeaturesResponse, error) { out := new(GetBatchFeaturesResponse) err := c.cc.Invoke(ctx, "/feast.serving.ServingService/GetBatchFeatures", in, out, opts...) @@ -1737,6 +2020,8 @@ type ServingServiceServer interface { GetFeastServingInfo(context.Context, *GetFeastServingInfoRequest) (*GetFeastServingInfoResponse, error) // Get online features synchronously. GetOnlineFeatures(context.Context, *GetOnlineFeaturesRequest) (*GetOnlineFeaturesResponse, error) + // Get online features (v2) synchronously. + GetOnlineFeaturesV2(context.Context, *GetOnlineFeaturesRequestV2) (*GetOnlineFeaturesResponse, error) // Get batch features asynchronously. // // The client should check the status of the returned job periodically by @@ -1760,6 +2045,9 @@ func (*UnimplementedServingServiceServer) GetFeastServingInfo(context.Context, * func (*UnimplementedServingServiceServer) GetOnlineFeatures(context.Context, *GetOnlineFeaturesRequest) (*GetOnlineFeaturesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetOnlineFeatures not implemented") } +func (*UnimplementedServingServiceServer) GetOnlineFeaturesV2(context.Context, *GetOnlineFeaturesRequestV2) (*GetOnlineFeaturesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetOnlineFeaturesV2 not implemented") +} func (*UnimplementedServingServiceServer) GetBatchFeatures(context.Context, *GetBatchFeaturesRequest) (*GetBatchFeaturesResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetBatchFeatures not implemented") } @@ -1807,6 +2095,24 @@ func _ServingService_GetOnlineFeatures_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } +func _ServingService_GetOnlineFeaturesV2_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetOnlineFeaturesRequestV2) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ServingServiceServer).GetOnlineFeaturesV2(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/feast.serving.ServingService/GetOnlineFeaturesV2", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ServingServiceServer).GetOnlineFeaturesV2(ctx, req.(*GetOnlineFeaturesRequestV2)) + } + return interceptor(ctx, in, info, handler) +} + func _ServingService_GetBatchFeatures_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetBatchFeaturesRequest) if err := dec(in); err != nil { @@ -1855,6 +2161,10 @@ var _ServingService_serviceDesc = grpc.ServiceDesc{ MethodName: "GetOnlineFeatures", Handler: _ServingService_GetOnlineFeatures_Handler, }, + { + MethodName: "GetOnlineFeaturesV2", + Handler: _ServingService_GetOnlineFeaturesV2_Handler, + }, { MethodName: "GetBatchFeatures", Handler: _ServingService_GetBatchFeatures_Handler, diff --git a/sdk/go/protos/feast/storage/Redis.pb.go b/sdk/go/protos/feast/storage/Redis.pb.go index b75f608547..d169c8bf32 100644 --- a/sdk/go/protos/feast/storage/Redis.pb.go +++ b/sdk/go/protos/feast/storage/Redis.pb.go @@ -100,6 +100,69 @@ func (x *RedisKey) GetEntities() []*types.Field { return nil } +type RedisKeyV2 struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + EntityNames []string `protobuf:"bytes,2,rep,name=entity_names,json=entityNames,proto3" json:"entity_names,omitempty"` + EntityValues []*types.Value `protobuf:"bytes,3,rep,name=entity_values,json=entityValues,proto3" json:"entity_values,omitempty"` +} + +func (x *RedisKeyV2) Reset() { + *x = RedisKeyV2{} + if protoimpl.UnsafeEnabled { + mi := &file_feast_storage_Redis_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RedisKeyV2) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RedisKeyV2) ProtoMessage() {} + +func (x *RedisKeyV2) ProtoReflect() protoreflect.Message { + mi := &file_feast_storage_Redis_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RedisKeyV2.ProtoReflect.Descriptor instead. +func (*RedisKeyV2) Descriptor() ([]byte, []int) { + return file_feast_storage_Redis_proto_rawDescGZIP(), []int{1} +} + +func (x *RedisKeyV2) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +func (x *RedisKeyV2) GetEntityNames() []string { + if x != nil { + return x.EntityNames + } + return nil +} + +func (x *RedisKeyV2) GetEntityValues() []*types.Value { + if x != nil { + return x.EntityValues + } + return nil +} + var File_feast_storage_Redis_proto protoreflect.FileDescriptor var file_feast_storage_Redis_proto_rawDesc = []byte{ @@ -107,19 +170,29 @@ var file_feast_storage_Redis_proto_rawDesc = []byte{ 0x52, 0x65, 0x64, 0x69, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x1a, 0x17, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x5b, 0x0a, 0x08, 0x52, 0x65, 0x64, 0x69, 0x73, 0x4b, 0x65, 0x79, 0x12, - 0x1f, 0x0a, 0x0b, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, - 0x12, 0x2e, 0x0a, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, - 0x42, 0x59, 0x0a, 0x13, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x0a, 0x52, 0x65, 0x64, 0x69, 0x73, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, - 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, - 0x61, 0x73, 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2f, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5b, 0x0a, 0x08, + 0x52, 0x65, 0x64, 0x69, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x65, 0x61, 0x74, + 0x75, 0x72, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x66, + 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x08, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, + 0x08, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x0a, 0x52, 0x65, + 0x64, 0x69, 0x73, 0x4b, 0x65, 0x79, 0x56, 0x32, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0d, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x66, + 0x65, 0x61, 0x73, 0x74, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x0c, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x42, 0x59, + 0x0a, 0x13, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x42, 0x0a, 0x52, 0x65, 0x64, 0x69, 0x73, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, + 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -134,18 +207,21 @@ func file_feast_storage_Redis_proto_rawDescGZIP() []byte { return file_feast_storage_Redis_proto_rawDescData } -var file_feast_storage_Redis_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_feast_storage_Redis_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_feast_storage_Redis_proto_goTypes = []interface{}{ (*RedisKey)(nil), // 0: feast.storage.RedisKey - (*types.Field)(nil), // 1: feast.types.Field + (*RedisKeyV2)(nil), // 1: feast.storage.RedisKeyV2 + (*types.Field)(nil), // 2: feast.types.Field + (*types.Value)(nil), // 3: feast.types.Value } var file_feast_storage_Redis_proto_depIdxs = []int32{ - 1, // 0: feast.storage.RedisKey.entities:type_name -> feast.types.Field - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 2, // 0: feast.storage.RedisKey.entities:type_name -> feast.types.Field + 3, // 1: feast.storage.RedisKeyV2.entity_values:type_name -> feast.types.Value + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_feast_storage_Redis_proto_init() } @@ -166,6 +242,18 @@ func file_feast_storage_Redis_proto_init() { return nil } } + file_feast_storage_Redis_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RedisKeyV2); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -173,7 +261,7 @@ func file_feast_storage_Redis_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_feast_storage_Redis_proto_rawDesc, NumEnums: 0, - NumMessages: 1, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, From a80e50a2d3cf9cba4803ad10a8f05f30343b8570 Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 12 Oct 2020 15:29:40 +0800 Subject: [PATCH 05/17] Remove additional line Signed-off-by: Terence --- .../connectors/redis/retriever/RedisOnlineRetrieverV2.java | 1 - 1 file changed, 1 deletion(-) diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java index 7ab5be32b3..3a8d8e792f 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java @@ -163,7 +163,6 @@ private List>> getFeaturesFromRedis( // Decode data from Redis into Feature object fields if (isTimestampMap.get(Arrays.toString(redisValueK))) { - redisValueV = redisValuesList.get(i).getValueOrElse(new byte[0]); eventTimestamp = Timestamp.parseFrom(redisValueV); } else { featureReference = byteToFeatureReferenceMap.get(redisValueK.toString()); From 4cdee55de1aa6e12818521fc3796a6831d8cbfbb Mon Sep 17 00:00:00 2001 From: Terence Date: Mon, 12 Oct 2020 18:54:59 +0800 Subject: [PATCH 06/17] Use single hmget Signed-off-by: Terence --- .../java/feast/common/it/DataGenerator.java | 30 ++ serving/pom.xml | 6 + .../feast/serving/it/ServingServiceIT.java | 348 ++++-------------- .../test/java/feast/serving/it/TestUtils.java | 4 +- .../service/CachedSpecServiceTest.java | 52 +++ .../retriever/RedisOnlineRetrieverV2.java | 121 +++--- 6 files changed, 226 insertions(+), 335 deletions(-) diff --git a/common-test/src/main/java/feast/common/it/DataGenerator.java b/common-test/src/main/java/feast/common/it/DataGenerator.java index 5fc4f6aa12..62626b89ab 100644 --- a/common-test/src/main/java/feast/common/it/DataGenerator.java +++ b/common-test/src/main/java/feast/common/it/DataGenerator.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableList; import com.google.protobuf.Duration; +import com.google.protobuf.Timestamp; import feast.proto.core.DataSourceProto.DataSource; import feast.proto.core.DataSourceProto.DataSource.BigQueryOptions; import feast.proto.core.DataSourceProto.DataSource.FileOptions; @@ -29,6 +30,7 @@ import feast.proto.core.FeatureTableProto.FeatureTableSpec; import feast.proto.core.SourceProto; import feast.proto.core.StoreProto; +import feast.proto.serving.ServingAPIProto; import feast.proto.types.ValueProto; import java.util.Collections; import java.util.HashMap; @@ -271,4 +273,32 @@ public static DataSource createKafkaDataSourceSpec( .setEventTimestampColumn(timestampColumn) .build(); } + + public static ValueProto.Value createEmptyValue() { + return ValueProto.Value.newBuilder().build(); + } + + public static ValueProto.Value createDoubleValue(double value) { + return ValueProto.Value.newBuilder().setDoubleVal(value).build(); + } + + public static ValueProto.Value createInt64Value(long value) { + return ValueProto.Value.newBuilder().setInt64Val(value).build(); + } + + public static ServingAPIProto.FeatureReferenceV2 createFeatureReference( + String featureTableName, String featureName) { + return ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureTableName) + .setName(featureName) + .build(); + } + + public static ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow createEntityRow( + String entityName, ValueProto.Value entityValue, long seconds) { + return ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow.newBuilder() + .setTimestamp(Timestamp.newBuilder().setSeconds(seconds)) + .putFields(entityName, entityValue) + .build(); + } } diff --git a/serving/pom.xml b/serving/pom.xml index 100eed3d37..b216aa29ec 100644 --- a/serving/pom.xml +++ b/serving/pom.xml @@ -328,6 +328,12 @@ 0.4.4-alpha.1 test + + dev.feast + feast-common-test + ${project.version} + test + diff --git a/serving/src/test/java/feast/serving/it/ServingServiceIT.java b/serving/src/test/java/feast/serving/it/ServingServiceIT.java index ef2ecbf0cd..d1693e42c8 100644 --- a/serving/src/test/java/feast/serving/it/ServingServiceIT.java +++ b/serving/src/test/java/feast/serving/it/ServingServiceIT.java @@ -18,12 +18,13 @@ import static org.junit.jupiter.api.Assertions.*; +import com.google.common.collect.ImmutableMap; import com.google.common.hash.Hashing; -import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Timestamp; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; +import feast.common.it.DataGenerator; import feast.common.models.FeatureV2; import feast.proto.core.EntityProto; import feast.proto.serving.ServingAPIProto; @@ -32,9 +33,7 @@ import feast.proto.serving.ServingServiceGrpc; import feast.proto.storage.RedisProto; import feast.proto.types.ValueProto; -import feast.storage.api.retriever.Feature; import io.grpc.ManagedChannel; -import io.lettuce.core.KeyValue; import io.lettuce.core.RedisClient; import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; @@ -45,8 +44,8 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.*; -import java.util.stream.Collectors; import org.junit.ClassRule; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.runners.model.InitializationError; @@ -66,16 +65,9 @@ public class ServingServiceIT extends BaseAuthIT { static final Map options = new HashMap<>(); static final String timestampPrefix = "_ts"; static CoreSimpleAPIClient coreClient; + static ServingServiceGrpc.ServingServiceBlockingStub servingStub; static RedisCommands syncCommands; - // To decode bytes back to Feature Reference - static Map byteToFeatureReferenceMap = - new HashMap<>(); - // To check whether redis ValueK is a timestamp field - static Map isTimestampMap = new HashMap<>(); - static List> featureReferenceWithTsByteArrays = new ArrayList<>(); - static RedisProto.RedisKeyV2 redisKey = null; - static final int FEAST_SERVING_PORT = 6566; @LocalServerPort private int metricsPort; @@ -88,19 +80,13 @@ public class ServingServiceIT extends BaseAuthIT { FEAST_CORE_PORT, Wait.forLogMessage(".*gRPC Server started.*\\n", 1) .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))) - .withExposedService( - JOB_CONTROLLER, - FEAST_JOB_CONTROLLER_PORT, - Wait.forLogMessage(".*gRPC Server started.*\\n", 1) - .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))) .withExposedService(REDIS, REDIS_PORT); @BeforeAll static void globalSetup() throws IOException, InitializationError, InterruptedException { - // Create Core client coreClient = TestUtils.getApiClientForCore(FEAST_CORE_PORT); + servingStub = TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - // Create Redis client RedisClient redisClient = RedisClient.create( new RedisURI( @@ -132,70 +118,46 @@ static void globalSetup() throws IOException, InitializationError, InterruptedEx add(entityName); } }; - HashMap features = new HashMap<>(); - // Feature 1 - String feature1 = "trip_cost"; - ValueProto.Value feature1Value = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); ServingAPIProto.FeatureReferenceV2 feature1Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature1) - .build(); - - // Feature 2 - String feature2 = "trip_distance"; - ValueProto.Value feature2Value = ValueProto.Value.newBuilder().setInt64Val(42).build(); + DataGenerator.createFeatureReference("rides", "trip_cost"); ServingAPIProto.FeatureReferenceV2 feature2Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature2) - .build(); - - // Feature 2 - String feature3 = "trip_empty"; - ValueProto.Value feature3Value = ValueProto.Value.newBuilder().build(); + DataGenerator.createFeatureReference("rides", "trip_distance"); ServingAPIProto.FeatureReferenceV2 feature3Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature3) - .build(); + DataGenerator.createFeatureReference("rides", "trip_empty"); // Event Timestamp String eventTimestampKey = timestampPrefix + ":" + featureTableName; Timestamp eventTimestampValue = Timestamp.newBuilder().setSeconds(100).build(); - features.put(feature1, ValueProto.ValueType.Enum.INT64); - features.put(feature2, ValueProto.ValueType.Enum.DOUBLE); - features.put(feature3, ValueProto.ValueType.Enum.DOUBLE); + ImmutableMap features = + ImmutableMap.of( + "trip_cost", ValueProto.ValueType.Enum.INT64, + "trip_distance", ValueProto.ValueType.Enum.DOUBLE, + "trip_empty", ValueProto.ValueType.Enum.DOUBLE); TestUtils.applyFeatureTable(coreClient, projectName, featureTableName, entities, features); // Serialize Redis Key with Entity i.e - redisKey = + RedisProto.RedisKeyV2 redisKey = RedisProto.RedisKeyV2.newBuilder() .setProject(projectName) .addEntityNames(entityName) .addEntityValues(entityValue) .build(); - // Murmur hash Redis Value Field i.e murmur() - Map featureReferenceValueMap = - new HashMap<>() { - { - put(feature1Reference, feature1Value); - put(feature2Reference, feature2Value); - put(feature3Reference, feature3Value); - } - }; + ImmutableMap featureReferenceValueMap = + ImmutableMap.of( + feature1Reference, DataGenerator.createDoubleValue(42.2), + feature2Reference, DataGenerator.createInt64Value(42), + feature3Reference, DataGenerator.createEmptyValue()); // Insert timestamp into Redis and isTimestampMap only once syncCommands.hset( redisKey.toByteArray(), eventTimestampKey.getBytes(), eventTimestampValue.toByteArray()); - isTimestampMap.put(Arrays.toString(eventTimestampKey.getBytes()), true); featureReferenceValueMap.forEach( (featureReference, featureValue) -> { - List currentFeatureWithTsByteArrays = new ArrayList<>(); + // Murmur hash Redis Feature Field i.e murmur() String delimitedFeatureReference = featureReference.getFeatureTable() + ":" + featureReference.getName(); byte[] featureReferenceBytes = @@ -205,13 +167,6 @@ static void globalSetup() throws IOException, InitializationError, InterruptedEx // Insert features into Redis syncCommands.hset( redisKey.toByteArray(), featureReferenceBytes, featureValue.toByteArray()); - currentFeatureWithTsByteArrays.add(featureReferenceBytes); - isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); - byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); - - currentFeatureWithTsByteArrays.add(eventTimestampKey.getBytes()); - - featureReferenceWithTsByteArrays.add(currentFeatureWithTsByteArrays); }); // set up options for call credentials @@ -223,6 +178,11 @@ static void globalSetup() throws IOException, InitializationError, InterruptedEx options.put("grant_type", GRANT_TYPE); } + @AfterAll + static void tearDown() { + ((ManagedChannel) servingStub.getChannel()).shutdown(); + } + /** Test that Feast Serving metrics endpoint can be accessed with authentication enabled */ @Test public void shouldAllowUnauthenticatedAccessToMetricsEndpoint() throws IOException { @@ -236,155 +196,16 @@ public void shouldAllowUnauthenticatedAccessToMetricsEndpoint() throws IOExcepti assertTrue(!response.body().string().isEmpty()); } - // @Test - // public void shouldAllowUnauthenticatedGetOnlineFeatures() { - // // apply feature set - // CoreSimpleAPIClient coreClient = - // TestUtils.getSecureApiClientForCore(FEAST_CORE_PORT, options); - // TestUtils.applyFeatureTable(coreClient, PROJECT_NAME, ENTITY_ID, FEATURE_NAME); - // ServingServiceGrpc.ServingServiceBlockingStub servingStub = - // TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - // - // GetOnlineFeaturesRequestV2 onlineFeatureRequest = - // TestUtils.createOnlineFeatureRequest(PROJECT_NAME, FEATURE_NAME, ENTITY_ID, 1); - // GetOnlineFeaturesResponse featureResponse = - // servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - // assertEquals(1, featureResponse.getFieldValuesCount()); - // Map fieldsMap = featureResponse.getFieldValues(0).getFieldsMap(); - // - // assertTrue(fieldsMap.containsKey(ENTITY_ID)); - // assertTrue(fieldsMap.containsKey(FEATURE_NAME)); - // - // ((ManagedChannel) servingStub.getChannel()).shutdown(); - // } - - @Test - public void shouldRegisterAndRetrieveFromRedis() throws InvalidProtocolBufferException { - String featureTableName = "rides"; - - // Feature 1 - String feature1 = "trip_cost"; - ValueProto.Value feature1Value = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); - ServingAPIProto.FeatureReferenceV2 feature1Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature1) - .build(); - - // Feature 2 - String feature2 = "trip_distance"; - ValueProto.Value feature2Value = ValueProto.Value.newBuilder().setInt64Val(42).build(); - ServingAPIProto.FeatureReferenceV2 feature2Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature2) - .build(); - - // Feature 2 - String feature3 = "trip_empty"; - ValueProto.Value feature3Value = ValueProto.Value.newBuilder().build(); - ServingAPIProto.FeatureReferenceV2 feature3Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature3) - .build(); - - // Retrieve multiple value from Redis - List> retrievedFeatures = new ArrayList<>(); - for (List currentFeatureReferenceWithTs : featureReferenceWithTsByteArrays) { - ServingAPIProto.FeatureReferenceV2 featureReference = null; - ValueProto.Value featureValue = null; - Timestamp eventTimestamp = null; - - List> currentRedisValuesList = - syncCommands.hmget( - redisKey.toByteArray(), - currentFeatureReferenceWithTs.get(0), - currentFeatureReferenceWithTs.get(1)); - - for (int i = 0; i < currentRedisValuesList.size(); i++) { - if (currentRedisValuesList.get(i).hasValue()) { - try { - byte[] redisValueK = currentRedisValuesList.get(i).getKey(); - byte[] redisValueV = currentRedisValuesList.get(i).getValue(); - - // Decode data from Redis into Feature object fields - if (isTimestampMap.get(Arrays.toString(redisValueK))) { - eventTimestamp = Timestamp.parseFrom(redisValueV); - } else { - featureReference = byteToFeatureReferenceMap.get(redisValueK.toString()); - featureValue = ValueProto.Value.parseFrom(redisValueV); - } - } catch (InvalidProtocolBufferException e) { - e.printStackTrace(); - } - } - } - // Check for null featureReference i.e key is not found - if (featureReference != null) { - Feature feature = - Feature.builder() - .setFeatureReference(featureReference) - .setFeatureValue(featureValue) - .setEventTimestamp(eventTimestamp) - .build(); - retrievedFeatures.add(Optional.of(feature)); - } - } - - List expectedValues = - new ArrayList<>() { - { - add(feature1Value); - add(feature2Value); - add(feature3Value); - } - }; - List expectedFeatureReferences = - new ArrayList<>() { - { - add(feature1Reference); - add(feature2Reference); - add(feature3Reference); - } - }; - assertEquals(3, retrievedFeatures.size()); - assertEquals( - expectedFeatureReferences.stream() - .map(featureReference -> featureReference.getName()) - .sorted() - .collect(Collectors.toList()), - retrievedFeatures.stream() - .map(feature -> feature.get().getFeatureReference().getName()) - .sorted() - .collect(Collectors.toList())); - assertEquals( - expectedFeatureReferences.stream() - .map(featureReference -> featureReference.getFeatureTable()) - .collect(Collectors.toList()), - retrievedFeatures.stream() - .map(feature -> feature.get().getFeatureReference().getFeatureTable()) - .collect(Collectors.toList())); - } - @Test public void shouldRegisterAndGetOnlineFeatures() { - ServingServiceGrpc.ServingServiceBlockingStub servingStub = - TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - // getOnlineFeatures Information String projectName = "default"; - String featureTableName = "rides"; String entityName = "driver_id"; ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); - String feature1 = "trip_cost"; // Instantiate EntityRows GetOnlineFeaturesRequestV2.EntityRow entityRow1 = - GetOnlineFeaturesRequestV2.EntityRow.newBuilder() - .setTimestamp(Timestamp.newBuilder().setSeconds(100)) - .putFields(entityName, entityValue) - .build(); + DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 100); List entityRows = new ArrayList<>() { { @@ -393,12 +214,8 @@ public void shouldRegisterAndGetOnlineFeatures() { }; // Instantiate FeatureReferences - ValueProto.Value feature1Value = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); ServingAPIProto.FeatureReferenceV2 feature1Reference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature1) - .build(); + DataGenerator.createFeatureReference("rides", "trip_cost"); List featureReferences = new ArrayList<>() { { @@ -412,23 +229,19 @@ public void shouldRegisterAndGetOnlineFeatures() { GetOnlineFeaturesResponse featureResponse = servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - Map expectedValueMap = - new HashMap<>() { - { - put(entityName, entityValue); - put(FeatureV2.getFeatureStringRef(feature1Reference), feature1Value); - } - }; + ImmutableMap expectedValueMap = + ImmutableMap.of( + entityName, + entityValue, + FeatureV2.getFeatureStringRef(feature1Reference), + DataGenerator.createDoubleValue(42.2)); - Map expectedStatusMap = - new HashMap<>() { - { - put(entityName, GetOnlineFeaturesResponse.FieldStatus.PRESENT); - put( - FeatureV2.getFeatureStringRef(feature1Reference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT); - } - }; + ImmutableMap expectedStatusMap = + ImmutableMap.of( + entityName, + GetOnlineFeaturesResponse.FieldStatus.PRESENT, + FeatureV2.getFeatureStringRef(feature1Reference), + GetOnlineFeaturesResponse.FieldStatus.PRESENT); GetOnlineFeaturesResponse.FieldValues expectedFieldValues = GetOnlineFeaturesResponse.FieldValues.newBuilder() @@ -442,32 +255,19 @@ public void shouldRegisterAndGetOnlineFeatures() { } }; - assertEquals(1, 1); assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - - ((ManagedChannel) servingStub.getChannel()).shutdown(); } @Test public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { - ServingServiceGrpc.ServingServiceBlockingStub servingStub = - TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - // getOnlineFeatures Information String projectName = "default"; - String featureTableName = "rides"; String entityName = "driver_id"; ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); - String feature1 = "trip_cost"; - String notFoundFeature = "trip_transaction"; - String emptyFeature = "trip_empty"; // Instantiate EntityRows GetOnlineFeaturesRequestV2.EntityRow entityRow1 = - GetOnlineFeaturesRequestV2.EntityRow.newBuilder() - .setTimestamp(Timestamp.newBuilder().setSeconds(100)) - .putFields(entityName, entityValue) - .build(); + DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 100); List entityRows = new ArrayList<>() { { @@ -476,24 +276,13 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { }; // Instantiate FeatureReferences - ValueProto.Value featureValue = ValueProto.Value.newBuilder().setDoubleVal(42.2).build(); ServingAPIProto.FeatureReferenceV2 featureReference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(feature1) - .build(); - ValueProto.Value notFoundFeatureValue = ValueProto.Value.newBuilder().build(); + DataGenerator.createFeatureReference("rides", "trip_cost"); ServingAPIProto.FeatureReferenceV2 notFoundFeatureReference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(notFoundFeature) - .build(); - ValueProto.Value emptyFeatureValue = ValueProto.Value.newBuilder().build(); + DataGenerator.createFeatureReference("rides", "trip_transaction"); ServingAPIProto.FeatureReferenceV2 emptyFeatureReference = - ServingAPIProto.FeatureReferenceV2.newBuilder() - .setFeatureTable(featureTableName) - .setName(emptyFeature) - .build(); + DataGenerator.createFeatureReference("rides", "trip_empty"); + List featureReferences = new ArrayList<>() { { @@ -509,31 +298,27 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { GetOnlineFeaturesResponse featureResponse = servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - Map expectedValueMap = - new HashMap<>() { - { - put(entityName, entityValue); - put(FeatureV2.getFeatureStringRef(featureReference), featureValue); - put(FeatureV2.getFeatureStringRef(notFoundFeatureReference), notFoundFeatureValue); - put(FeatureV2.getFeatureStringRef(emptyFeatureReference), emptyFeatureValue); - } - }; - - Map expectedStatusMap = - new HashMap<>() { - { - put(entityName, GetOnlineFeaturesResponse.FieldStatus.PRESENT); - put( - FeatureV2.getFeatureStringRef(featureReference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT); - put( - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND); - put( - FeatureV2.getFeatureStringRef(emptyFeatureReference), - GetOnlineFeaturesResponse.FieldStatus.NULL_VALUE); - } - }; + ImmutableMap expectedValueMap = + ImmutableMap.of( + entityName, + entityValue, + FeatureV2.getFeatureStringRef(featureReference), + DataGenerator.createDoubleValue(42.2), + FeatureV2.getFeatureStringRef(notFoundFeatureReference), + DataGenerator.createEmptyValue(), + FeatureV2.getFeatureStringRef(emptyFeatureReference), + DataGenerator.createEmptyValue()); + + ImmutableMap expectedStatusMap = + ImmutableMap.of( + entityName, + GetOnlineFeaturesResponse.FieldStatus.PRESENT, + FeatureV2.getFeatureStringRef(featureReference), + GetOnlineFeaturesResponse.FieldStatus.PRESENT, + FeatureV2.getFeatureStringRef(notFoundFeatureReference), + GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND, + FeatureV2.getFeatureStringRef(emptyFeatureReference), + GetOnlineFeaturesResponse.FieldStatus.NULL_VALUE); GetOnlineFeaturesResponse.FieldValues expectedFieldValues = GetOnlineFeaturesResponse.FieldValues.newBuilder() @@ -547,9 +332,6 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { } }; - assertEquals(1, 1); assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - - ((ManagedChannel) servingStub.getChannel()).shutdown(); } } diff --git a/serving/src/test/java/feast/serving/it/TestUtils.java b/serving/src/test/java/feast/serving/it/TestUtils.java index b39d405193..b5d7ae4f08 100644 --- a/serving/src/test/java/feast/serving/it/TestUtils.java +++ b/serving/src/test/java/feast/serving/it/TestUtils.java @@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import avro.shaded.com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.Duration; import feast.common.auth.credentials.OAuthCredentials; import feast.proto.core.CoreServiceGrpc; @@ -119,7 +119,7 @@ public static void applyFeatureTable( String projectName, String featureTableName, List entities, - HashMap features) { + ImmutableMap features) { FeatureTableSpec expectedFeatureTableSpec = createFeatureTableSpec( featureTableName, diff --git a/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java b/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java index 85f590d523..eb228ee5b2 100644 --- a/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java +++ b/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java @@ -24,18 +24,28 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import com.google.common.collect.ImmutableMap; +import feast.common.it.DataGenerator; import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest; import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse; +import feast.proto.core.CoreServiceProto.ListFeatureTablesRequest; +import feast.proto.core.CoreServiceProto.ListFeatureTablesResponse; +import feast.proto.core.CoreServiceProto.ListProjectsRequest; +import feast.proto.core.CoreServiceProto.ListProjectsResponse; import feast.proto.core.FeatureSetProto; import feast.proto.core.FeatureSetProto.FeatureSetSpec; import feast.proto.core.FeatureSetProto.FeatureSpec; +import feast.proto.core.FeatureTableProto; +import feast.proto.core.FeatureTableProto.FeatureTableSpec; import feast.proto.core.StoreProto.Store; import feast.proto.core.StoreProto.Store.Subscription; import feast.proto.serving.ServingAPIProto.FeatureReference; +import feast.proto.types.ValueProto; import feast.serving.exception.SpecRetrievalException; import feast.serving.specs.CachedSpecService; import feast.serving.specs.CoreSpecService; import feast.storage.api.retriever.FeatureSetRequest; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -81,10 +91,52 @@ public void setUp() { this.setupFeatureSetAndStoreSubscription( "default", "fs3", List.of(FeatureSpec.newBuilder().setName("feature4").build())); + this.setupProject("default"); + this.setupFeatureTableAndProject("default"); + when(this.coreService.registerStore(store)).thenReturn(store); cachedSpecService = new CachedSpecService(this.coreService, this.store); } + private void setupProject(String project) { + when(coreService.listProjects(ListProjectsRequest.newBuilder().build())) + .thenReturn(ListProjectsResponse.newBuilder().addProjects(project).build()); + } + + private void setupFeatureTableAndProject(String project) { + String featureTableName = "tablename"; + List entities = + new ArrayList<>() { + { + add("entity1"); + } + }; + ImmutableMap features = + ImmutableMap.of( + "trip_cost", ValueProto.ValueType.Enum.INT64, + "trip_distance", ValueProto.ValueType.Enum.DOUBLE, + "trip_empty", ValueProto.ValueType.Enum.DOUBLE); + FeatureTableSpec ftSpec = + DataGenerator.createFeatureTableSpec( + featureTableName, + entities, + new HashMap<>() { + { + putAll(features); + } + }, + 7200, + ImmutableMap.of("feat_key2", "feat_value2")); + FeatureTableProto.FeatureTable featureTable = + FeatureTableProto.FeatureTable.newBuilder().setSpec(ftSpec).build(); + + when(coreService.listFeatureTables( + ListFeatureTablesRequest.newBuilder() + .setFilter(ListFeatureTablesRequest.Filter.newBuilder().setProject(project).build()) + .build())) + .thenReturn(ListFeatureTablesResponse.newBuilder().addTables(featureTable).build()); + } + private void setupFeatureSetAndStoreSubscription( String project, String name, List featureSpecs) { FeatureSetSpec fsSpec = diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java index 3a8d8e792f..7435c71a7e 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java @@ -17,6 +17,7 @@ package feast.storage.connectors.redis.retriever; import com.google.common.hash.Hashing; +import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Timestamp; import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow; @@ -112,11 +113,10 @@ private List>> getFeaturesFromRedis( redisKeys.stream().map(redisKey -> redisKey.toByteArray()).collect(Collectors.toList()); try { - List> featureReferenceWithTsByteArrays = new ArrayList<>(); + List featureReferenceWithTsByteList = new ArrayList<>(); featureReferences.stream() .forEach( featureReference -> { - List currentFeatureWithTsByteArrays = new ArrayList<>(); // eg. murmur() String delimitedFeatureReference = @@ -125,65 +125,28 @@ private List>> getFeaturesFromRedis( Hashing.murmur3_32() .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) .asBytes(); - currentFeatureWithTsByteArrays.add(featureReferenceBytes); + featureReferenceWithTsByteList.add(featureReferenceBytes); isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); // eg. <_ts:featuretable_name> - String timestampFeatureTableReference = - timestampPrefix + ":" + featureReference.getFeatureTable(); - byte[] featureTableTsBytes = timestampFeatureTableReference.getBytes(); + byte[] featureTableTsBytes = getTimestampRedisHashKeyBytes(featureReference); isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); - currentFeatureWithTsByteArrays.add(featureTableTsBytes); - - featureReferenceWithTsByteArrays.add(currentFeatureWithTsByteArrays); + featureReferenceWithTsByteList.add(featureTableTsBytes); }); // Access redis keys and extract features for (byte[] binaryRedisKey : binaryRedisKeys) { - List> curRedisKeyFeatures = new ArrayList<>(); - // Loop according to each FeatureReferenceV2 bytes - for (List currentFeatureReferenceWithTsByteArray : - featureReferenceWithTsByteArrays) { - FeatureReferenceV2 featureReference = null; - ValueProto.Value featureValue = null; - Timestamp eventTimestamp = null; - - // Always 2 fields (i.e feature and timestamp) - List> redisValuesList = - syncCommands.hmget( - binaryRedisKey, - currentFeatureReferenceWithTsByteArray.get(0), - currentFeatureReferenceWithTsByteArray.get(1)); - - for (int i = 0; i < redisValuesList.size(); i++) { - if (redisValuesList.get(i).hasValue()) { - byte[] redisValueK = redisValuesList.get(i).getKey(); - byte[] redisValueV = redisValuesList.get(i).getValue(); - - // Decode data from Redis into Feature object fields - if (isTimestampMap.get(Arrays.toString(redisValueK))) { - eventTimestamp = Timestamp.parseFrom(redisValueV); - } else { - featureReference = byteToFeatureReferenceMap.get(redisValueK.toString()); - featureValue = ValueProto.Value.parseFrom(redisValueV); - } - } - } - // Check for null featureReference i.e key is not found - if (featureReference != null) { - Feature feature = - Feature.builder() - .setFeatureReference(featureReference) - .setFeatureValue(featureValue) - .setEventTimestamp(eventTimestamp) - .build(); - curRedisKeyFeatures.add(Optional.of(feature)); - } - } + byte[][] featureReferenceWithTsByteArrays = + featureReferenceWithTsByteList.toArray(new byte[0][]); + // Always 2 fields (i.e feature and timestamp) + List> redisValuesList = + syncCommands.hmget(binaryRedisKey, featureReferenceWithTsByteArrays); + List> curRedisKeyFeatures = + retrieveFeature(redisValuesList, isTimestampMap, byteToFeatureReferenceMap); features.add(curRedisKeyFeatures); } - } catch (Exception e) { + } catch (InvalidProtocolBufferException e) { throw Status.UNKNOWN .withDescription("Unexpected error when pulling data from from Redis.") .withCause(e) @@ -191,4 +154,62 @@ private List>> getFeaturesFromRedis( } return features; } + + /** + * Converts all retrieved Redis Hash values based on EntityRows into {@link Feature} + * + * @param redisHashValues retrieved Redis Hash values based on EntityRows + * @param isTimestampMap map to determine if Redis Hash key is a timestamp field + * @param byteToFeatureReferenceMap map to decode bytes back to FeatureReference + * @return List of {@link Feature} + * @throws InvalidProtocolBufferException + */ + private List> retrieveFeature( + List> redisHashValues, + Map isTimestampMap, + Map byteToFeatureReferenceMap) + throws InvalidProtocolBufferException { + List> allFeatures = new ArrayList<>(); + Map> allFeaturesBuilderMap = new HashMap<>(); + Map featureTableTimestampMap = new HashMap<>(); + + for (int i = 0; i < redisHashValues.size(); i++) { + if (redisHashValues.get(i).hasValue()) { + byte[] redisValueK = redisHashValues.get(i).getKey(); + byte[] redisValueV = redisHashValues.get(i).getValue(); + + // Decode data from Redis into Feature object fields + if (isTimestampMap.get(Arrays.toString(redisValueK))) { + Timestamp eventTimestamp = Timestamp.parseFrom(redisValueV); + featureTableTimestampMap.put(Arrays.toString(redisValueK), eventTimestamp); + } else { + FeatureReferenceV2 featureReference = + byteToFeatureReferenceMap.get(redisValueK.toString()); + ValueProto.Value featureValue = ValueProto.Value.parseFrom(redisValueV); + + Feature.Builder featureBuilder = + Feature.builder().setFeatureReference(featureReference).setFeatureValue(featureValue); + allFeaturesBuilderMap.put(featureReference, Optional.of(featureBuilder)); + } + } + } + + // Add timestamp to features + if (allFeaturesBuilderMap.size() > 0) { + for (Map.Entry> entry : + allFeaturesBuilderMap.entrySet()) { + byte[] timestampFeatureTableHashKeyBytes = getTimestampRedisHashKeyBytes(entry.getKey()); + Timestamp curFeatureTimestamp = + featureTableTimestampMap.get(Arrays.toString(timestampFeatureTableHashKeyBytes)); + Feature curFeature = entry.getValue().get().setEventTimestamp(curFeatureTimestamp).build(); + allFeatures.add(Optional.of(curFeature)); + } + } + return allFeatures; + } + + private byte[] getTimestampRedisHashKeyBytes(FeatureReferenceV2 featureReference) { + String timestampRedisHashKeyStr = timestampPrefix + ":" + featureReference.getFeatureTable(); + return timestampRedisHashKeyStr.getBytes(); + } } From a1887a79fb70f16d0c4440584f08140a7f6e7af8 Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 10:32:41 +0800 Subject: [PATCH 07/17] Optimize redis hmget call and add maxage test Signed-off-by: Terence --- .../feast/serving/it/ServingServiceIT.java | 68 ++++++++++++- .../test/java/feast/serving/it/TestUtils.java | 6 +- .../retriever/RedisOnlineRetrieverV2.java | 96 +++++++++++-------- 3 files changed, 123 insertions(+), 47 deletions(-) diff --git a/serving/src/test/java/feast/serving/it/ServingServiceIT.java b/serving/src/test/java/feast/serving/it/ServingServiceIT.java index d1693e42c8..f840a5737d 100644 --- a/serving/src/test/java/feast/serving/it/ServingServiceIT.java +++ b/serving/src/test/java/feast/serving/it/ServingServiceIT.java @@ -48,7 +48,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.runners.model.InitializationError; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.ActiveProfiles; @@ -83,7 +82,7 @@ public class ServingServiceIT extends BaseAuthIT { .withExposedService(REDIS, REDIS_PORT); @BeforeAll - static void globalSetup() throws IOException, InitializationError, InterruptedException { + static void globalSetup() { coreClient = TestUtils.getApiClientForCore(FEAST_CORE_PORT); servingStub = TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); @@ -136,7 +135,8 @@ static void globalSetup() throws IOException, InitializationError, InterruptedEx "trip_distance", ValueProto.ValueType.Enum.DOUBLE, "trip_empty", ValueProto.ValueType.Enum.DOUBLE); - TestUtils.applyFeatureTable(coreClient, projectName, featureTableName, entities, features); + TestUtils.applyFeatureTable( + coreClient, projectName, featureTableName, entities, features, 7200); // Serialize Redis Key with Entity i.e RedisProto.RedisKeyV2 redisKey = @@ -334,4 +334,66 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); } + + @Test + public void shouldGetOnlineFeaturesOutsideMaxAge() { + String projectName = "default"; + String entityName = "driver_id"; + ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); + + // Instantiate EntityRows + GetOnlineFeaturesRequestV2.EntityRow entityRow1 = + DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 7400); + List entityRows = + new ArrayList<>() { + { + add(entityRow1); + } + }; + + // Instantiate FeatureReferences + ServingAPIProto.FeatureReferenceV2 featureReference = + DataGenerator.createFeatureReference("rides", "trip_cost"); + + List featureReferences = + new ArrayList<>() { + { + add(featureReference); + } + }; + + // Build GetOnlineFeaturesRequestV2 + GetOnlineFeaturesRequestV2 onlineFeatureRequest = + TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); + GetOnlineFeaturesResponse featureResponse = + servingStub.getOnlineFeaturesV2(onlineFeatureRequest); + + ImmutableMap expectedValueMap = + ImmutableMap.of( + entityName, + entityValue, + FeatureV2.getFeatureStringRef(featureReference), + DataGenerator.createEmptyValue()); + + ImmutableMap expectedStatusMap = + ImmutableMap.of( + entityName, + GetOnlineFeaturesResponse.FieldStatus.PRESENT, + FeatureV2.getFeatureStringRef(featureReference), + GetOnlineFeaturesResponse.FieldStatus.OUTSIDE_MAX_AGE); + + GetOnlineFeaturesResponse.FieldValues expectedFieldValues = + GetOnlineFeaturesResponse.FieldValues.newBuilder() + .putAllFields(expectedValueMap) + .putAllStatuses(expectedStatusMap) + .build(); + List expectedFieldValuesList = + new ArrayList<>() { + { + add(expectedFieldValues); + } + }; + + assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); + } } diff --git a/serving/src/test/java/feast/serving/it/TestUtils.java b/serving/src/test/java/feast/serving/it/TestUtils.java index b5d7ae4f08..a7698d5f27 100644 --- a/serving/src/test/java/feast/serving/it/TestUtils.java +++ b/serving/src/test/java/feast/serving/it/TestUtils.java @@ -84,7 +84,6 @@ public static FeatureTableSpec createFeatureTableSpec( .putAllLabels(labels) .build()) .collect(Collectors.toList())) - .setMaxAge(Duration.newBuilder().setSeconds(3600).build()) .putAllLabels(labels) .build(); } @@ -119,7 +118,8 @@ public static void applyFeatureTable( String projectName, String featureTableName, List entities, - ImmutableMap features) { + ImmutableMap features, + int maxAgeSecs) { FeatureTableSpec expectedFeatureTableSpec = createFeatureTableSpec( featureTableName, @@ -129,7 +129,7 @@ public static void applyFeatureTable( putAll(features); } }, - 7200, + maxAgeSecs, ImmutableMap.of("feat_key2", "feat_value2")) .toBuilder() .setBatchSource( diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java index 7435c71a7e..e1fbd965f2 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java @@ -16,6 +16,7 @@ */ package feast.storage.connectors.redis.retriever; +import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Timestamp; @@ -28,21 +29,23 @@ import io.grpc.Status; import io.lettuce.core.KeyValue; import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisFuture; import io.lettuce.core.RedisURI; import io.lettuce.core.api.StatefulRedisConnection; -import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.codec.ByteArrayCodec; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; public class RedisOnlineRetrieverV2 implements OnlineRetrieverV2 { private static final String timestampPrefix = "_ts"; - private final RedisCommands syncCommands; + private final RedisAsyncCommands asyncCommands; private RedisOnlineRetrieverV2(StatefulRedisConnection connection) { - this.syncCommands = connection.sync(); + this.asyncCommands = connection.async(); } public static OnlineRetrieverV2 create(Map config) { @@ -112,46 +115,57 @@ private List>> getFeaturesFromRedis( List binaryRedisKeys = redisKeys.stream().map(redisKey -> redisKey.toByteArray()).collect(Collectors.toList()); - try { - List featureReferenceWithTsByteList = new ArrayList<>(); - featureReferences.stream() - .forEach( - featureReference -> { - - // eg. murmur() - String delimitedFeatureReference = - featureReference.getFeatureTable() + ":" + featureReference.getName(); - byte[] featureReferenceBytes = - Hashing.murmur3_32() - .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) - .asBytes(); - featureReferenceWithTsByteList.add(featureReferenceBytes); - isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); - byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); - - // eg. <_ts:featuretable_name> - byte[] featureTableTsBytes = getTimestampRedisHashKeyBytes(featureReference); - isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); - featureReferenceWithTsByteList.add(featureTableTsBytes); - }); - + List featureReferenceWithTsByteList = new ArrayList<>(); + featureReferences.stream() + .forEach( + featureReference -> { + + // eg. murmur() + String delimitedFeatureReference = + featureReference.getFeatureTable() + ":" + featureReference.getName(); + byte[] featureReferenceBytes = + Hashing.murmur3_32() + .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) + .asBytes(); + featureReferenceWithTsByteList.add(featureReferenceBytes); + isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); + byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); + + // eg. <_ts:featuretable_name> + byte[] featureTableTsBytes = getTimestampRedisHashKeyBytes(featureReference); + isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); + featureReferenceWithTsByteList.add(featureTableTsBytes); + }); + + // Disable auto-flushing + asyncCommands.setAutoFlushCommands(false); + + // Perform a series of independent calls + List>>> futures = Lists.newArrayList(); + for (byte[] binaryRedisKey : binaryRedisKeys) { + byte[][] featureReferenceWithTsByteArrays = + featureReferenceWithTsByteList.toArray(new byte[0][]); // Access redis keys and extract features - for (byte[] binaryRedisKey : binaryRedisKeys) { - byte[][] featureReferenceWithTsByteArrays = - featureReferenceWithTsByteList.toArray(new byte[0][]); - // Always 2 fields (i.e feature and timestamp) - List> redisValuesList = - syncCommands.hmget(binaryRedisKey, featureReferenceWithTsByteArrays); - List> curRedisKeyFeatures = - retrieveFeature(redisValuesList, isTimestampMap, byteToFeatureReferenceMap); - features.add(curRedisKeyFeatures); - } - } catch (InvalidProtocolBufferException e) { - throw Status.UNKNOWN - .withDescription("Unexpected error when pulling data from from Redis.") - .withCause(e) - .asRuntimeException(); + futures.add(asyncCommands.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); } + + // Write all commands to the transport layer + asyncCommands.flushCommands(); + + futures.forEach( + future -> { + try { + List> redisValuesList = future.get(); + List> curRedisKeyFeatures = + retrieveFeature(redisValuesList, isTimestampMap, byteToFeatureReferenceMap); + features.add(curRedisKeyFeatures); + } catch (InterruptedException | ExecutionException | InvalidProtocolBufferException e) { + throw Status.UNKNOWN + .withDescription("Unexpected error when pulling data from from Redis.") + .withCause(e) + .asRuntimeException(); + } + }); return features; } From 1ad32c4f28e6a16ba5bd2e427ec60d78f14fc6fc Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 11:16:15 +0800 Subject: [PATCH 08/17] Add redis cluster and refactor code duplication Signed-off-by: Terence --- .../config/ServingServiceConfigV2.java | 6 +- .../redis/common/RedisHashDecoder.java | 90 +++++++ .../redis/common/RedisKeyGenerator.java | 62 +++++ .../RedisClusterOnlineRetrieverV2.java | 220 ++++++++++++++++++ .../retriever/RedisOnlineRetrieverV2.java | 101 +------- .../RedisKeyPrefixSerializerV2.java | 41 ++++ .../serializer/RedisKeyProtoSerializerV2.java | 26 +++ .../serializer/RedisKeySerializerV2.java | 24 ++ 8 files changed, 475 insertions(+), 95 deletions(-) create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisKeyGenerator.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyPrefixSerializerV2.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyProtoSerializerV2.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeySerializerV2.java diff --git a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java index 7a096d8ec4..def3dd86bd 100644 --- a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java +++ b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java @@ -23,6 +23,7 @@ import feast.serving.service.ServingServiceV2; import feast.serving.specs.CachedSpecService; import feast.storage.api.retriever.OnlineRetrieverV2; +import feast.storage.connectors.redis.retriever.RedisClusterOnlineRetrieverV2; import feast.storage.connectors.redis.retriever.RedisOnlineRetrieverV2; import io.opentracing.Tracer; import java.util.Map; @@ -44,11 +45,14 @@ public ServingServiceV2 servingServiceV2( Map config = store.getConfig(); switch (storeType) { + case REDIS_CLUSTER: + OnlineRetrieverV2 redisClusterRetriever = RedisClusterOnlineRetrieverV2.create(config); + servingService = new OnlineServingServiceV2(redisClusterRetriever, specService, tracer); + break; case REDIS: OnlineRetrieverV2 redisRetriever = RedisOnlineRetrieverV2.create(config); servingService = new OnlineServingServiceV2(redisRetriever, specService, tracer); break; - case REDIS_CLUSTER: case CASSANDRA: case UNRECOGNIZED: case INVALID: diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java new file mode 100644 index 0000000000..d41fcf94fb --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.common; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Timestamp; +import feast.proto.serving.ServingAPIProto; +import feast.proto.types.ValueProto; +import feast.storage.api.retriever.Feature; +import io.lettuce.core.KeyValue; +import java.util.*; + +public class RedisHashDecoder { + + /** + * Converts all retrieved Redis Hash values based on EntityRows into {@link Feature} + * + * @param redisHashValues retrieved Redis Hash values based on EntityRows + * @param isTimestampMap map to determine if Redis Hash key is a timestamp field + * @param byteToFeatureReferenceMap map to decode bytes back to FeatureReference + * @return List of {@link Feature} + * @throws InvalidProtocolBufferException + */ + public static List> retrieveFeature( + List> redisHashValues, + Map isTimestampMap, + Map byteToFeatureReferenceMap, + String timestampPrefix) + throws InvalidProtocolBufferException { + List> allFeatures = new ArrayList<>(); + Map> allFeaturesBuilderMap = + new HashMap<>(); + Map featureTableTimestampMap = new HashMap<>(); + + for (int i = 0; i < redisHashValues.size(); i++) { + if (redisHashValues.get(i).hasValue()) { + byte[] redisValueK = redisHashValues.get(i).getKey(); + byte[] redisValueV = redisHashValues.get(i).getValue(); + + // Decode data from Redis into Feature object fields + if (isTimestampMap.get(Arrays.toString(redisValueK))) { + Timestamp eventTimestamp = Timestamp.parseFrom(redisValueV); + featureTableTimestampMap.put(Arrays.toString(redisValueK), eventTimestamp); + } else { + ServingAPIProto.FeatureReferenceV2 featureReference = + byteToFeatureReferenceMap.get(redisValueK.toString()); + ValueProto.Value featureValue = ValueProto.Value.parseFrom(redisValueV); + + Feature.Builder featureBuilder = + Feature.builder().setFeatureReference(featureReference).setFeatureValue(featureValue); + allFeaturesBuilderMap.put(featureReference, Optional.of(featureBuilder)); + } + } + } + + // Add timestamp to features + if (allFeaturesBuilderMap.size() > 0) { + for (Map.Entry> entry : + allFeaturesBuilderMap.entrySet()) { + byte[] timestampFeatureTableHashKeyBytes = + RedisHashDecoder.getTimestampRedisHashKeyBytes(entry.getKey(), timestampPrefix); + Timestamp curFeatureTimestamp = + featureTableTimestampMap.get(Arrays.toString(timestampFeatureTableHashKeyBytes)); + Feature curFeature = entry.getValue().get().setEventTimestamp(curFeatureTimestamp).build(); + allFeatures.add(Optional.of(curFeature)); + } + } + return allFeatures; + } + + public static byte[] getTimestampRedisHashKeyBytes( + ServingAPIProto.FeatureReferenceV2 featureReference, String timestampPrefix) { + String timestampRedisHashKeyStr = timestampPrefix + ":" + featureReference.getFeatureTable(); + return timestampRedisHashKeyStr.getBytes(); + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisKeyGenerator.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisKeyGenerator.java new file mode 100644 index 0000000000..797dd52215 --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisKeyGenerator.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.common; + +import feast.proto.serving.ServingAPIProto; +import feast.proto.storage.RedisProto; +import feast.proto.types.ValueProto; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class RedisKeyGenerator { + + public static List buildRedisKeys( + String project, List entityRows) { + List redisKeys = + entityRows.stream() + .map(entityRow -> makeRedisKey(project, entityRow)) + .collect(Collectors.toList()); + + return redisKeys; + } + + /** + * Create {@link RedisProto.RedisKeyV2} + * + * @param project Project where request for features was called from + * @param entityRow {@link ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow} + * @return {@link RedisProto.RedisKeyV2} + */ + private static RedisProto.RedisKeyV2 makeRedisKey( + String project, ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow entityRow) { + RedisProto.RedisKeyV2.Builder builder = RedisProto.RedisKeyV2.newBuilder().setProject(project); + Map fieldsMap = entityRow.getFieldsMap(); + List entityNames = new ArrayList<>(new HashSet<>(fieldsMap.keySet())); + + // Sort entity names by alphabetical order + entityNames.sort(String::compareTo); + + for (String entityName : entityNames) { + builder.addEntityNames(entityName); + builder.addEntityValues(fieldsMap.get(entityName)); + } + return builder.build(); + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java new file mode 100644 index 0000000000..c8fb26deb0 --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java @@ -0,0 +1,220 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.retriever; + +import com.google.common.collect.Lists; +import com.google.common.hash.Hashing; +import com.google.protobuf.InvalidProtocolBufferException; +import feast.proto.serving.ServingAPIProto; +import feast.proto.storage.RedisProto; +import feast.proto.types.ValueProto; +import feast.storage.api.retriever.Feature; +import feast.storage.api.retriever.OnlineRetrieverV2; +import feast.storage.connectors.redis.common.RedisHashDecoder; +import feast.storage.connectors.redis.serializer.RedisKeyPrefixSerializerV2; +import feast.storage.connectors.redis.serializer.RedisKeySerializerV2; +import io.grpc.Status; +import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisURI; +import io.lettuce.core.cluster.RedisClusterClient; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** Defines a storage retriever */ +public class RedisClusterOnlineRetrieverV2 implements OnlineRetrieverV2 { + + private static final String timestampPrefix = "_ts"; + private final RedisAdvancedClusterAsyncCommands asyncCommands; + private final RedisKeySerializerV2 serializer; + @Nullable private final RedisKeySerializerV2 fallbackSerializer; + + static class Builder { + private final StatefulRedisClusterConnection connection; + private final RedisKeySerializerV2 serializer; + @Nullable private RedisKeySerializerV2 fallbackSerializer; + + Builder( + StatefulRedisClusterConnection connection, + RedisKeySerializerV2 serializer) { + this.connection = connection; + this.serializer = serializer; + } + + Builder withFallbackSerializer(RedisKeySerializerV2 fallbackSerializer) { + this.fallbackSerializer = fallbackSerializer; + return this; + } + + RedisClusterOnlineRetrieverV2 build() { + return new RedisClusterOnlineRetrieverV2(this); + } + } + + private RedisClusterOnlineRetrieverV2(Builder builder) { + this.asyncCommands = builder.connection.async(); + this.serializer = builder.serializer; + this.fallbackSerializer = builder.fallbackSerializer; + } + + public static OnlineRetrieverV2 create(Map config) { + List redisURIList = + Arrays.stream(config.get("connection_string").split(",")) + .map( + hostPort -> { + String[] hostPortSplit = hostPort.trim().split(":"); + return RedisURI.create(hostPortSplit[0], Integer.parseInt(hostPortSplit[1])); + }) + .collect(Collectors.toList()); + StatefulRedisClusterConnection connection = + RedisClusterClient.create(redisURIList).connect(new ByteArrayCodec()); + + RedisKeySerializerV2 serializer = + new RedisKeyPrefixSerializerV2(config.getOrDefault("key_prefix", "")); + + Builder builder = new Builder(connection, serializer); + + if (Boolean.parseBoolean(config.getOrDefault("enable_fallback", "false"))) { + RedisKeySerializerV2 fallbackSerializer = + new RedisKeyPrefixSerializerV2(config.getOrDefault("fallback_prefix", "")); + builder = builder.withFallbackSerializer(fallbackSerializer); + } + + return builder.build(); + } + + @Override + public List>> getOnlineFeatures( + String project, + List entityRows, + List featureReferences) { + + List redisKeys = buildRedisKeys(project, entityRows); + List>> features = getFeaturesFromRedis(redisKeys, featureReferences); + + return features; + } + + private List buildRedisKeys( + String project, List entityRows) { + List redisKeys = + entityRows.stream() + .map(entityRow -> makeRedisKey(project, entityRow)) + .collect(Collectors.toList()); + + return redisKeys; + } + + /** + * Create {@link RedisProto.RedisKeyV2} + * + * @param project Project where request for features was called from + * @param entityRow {@link ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow} + * @return {@link RedisProto.RedisKeyV2} + */ + private RedisProto.RedisKeyV2 makeRedisKey( + String project, ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow entityRow) { + RedisProto.RedisKeyV2.Builder builder = RedisProto.RedisKeyV2.newBuilder().setProject(project); + Map fieldsMap = entityRow.getFieldsMap(); + List entityNames = new ArrayList<>(new HashSet<>(fieldsMap.keySet())); + + // Sort entity names by alphabetical order + entityNames.sort(String::compareTo); + + for (String entityName : entityNames) { + builder.addEntityNames(entityName); + builder.addEntityValues(fieldsMap.get(entityName)); + } + return builder.build(); + } + + private List>> getFeaturesFromRedis( + List redisKeys, + List featureReferences) { + List>> features = new ArrayList<>(); + // To decode bytes back to Feature Reference + Map byteToFeatureReferenceMap = new HashMap<>(); + // To check whether redis ValueK is a timestamp field + Map isTimestampMap = new HashMap<>(); + + // Serialize using proto + List binaryRedisKeys = + redisKeys.stream().map(redisKey -> redisKey.toByteArray()).collect(Collectors.toList()); + + List featureReferenceWithTsByteList = new ArrayList<>(); + featureReferences.stream() + .forEach( + featureReference -> { + + // eg. murmur() + String delimitedFeatureReference = + featureReference.getFeatureTable() + ":" + featureReference.getName(); + byte[] featureReferenceBytes = + Hashing.murmur3_32() + .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) + .asBytes(); + featureReferenceWithTsByteList.add(featureReferenceBytes); + isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); + byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); + + // eg. <_ts:featuretable_name> + byte[] featureTableTsBytes = + RedisHashDecoder.getTimestampRedisHashKeyBytes(featureReference, timestampPrefix); + isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); + featureReferenceWithTsByteList.add(featureTableTsBytes); + }); + + // Disable auto-flushing + asyncCommands.setAutoFlushCommands(false); + + // Perform a series of independent calls + List>>> futures = Lists.newArrayList(); + for (byte[] binaryRedisKey : binaryRedisKeys) { + byte[][] featureReferenceWithTsByteArrays = + featureReferenceWithTsByteList.toArray(new byte[0][]); + // Access redis keys and extract features + futures.add(asyncCommands.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); + } + + // Write all commands to the transport layer + asyncCommands.flushCommands(); + + // TODO: Add in redis key prefix logic + futures.forEach( + future -> { + try { + List> redisValuesList = future.get(); + List> curRedisKeyFeatures = + RedisHashDecoder.retrieveFeature( + redisValuesList, isTimestampMap, byteToFeatureReferenceMap, timestampPrefix); + features.add(curRedisKeyFeatures); + } catch (InterruptedException | ExecutionException | InvalidProtocolBufferException e) { + throw Status.UNKNOWN + .withDescription("Unexpected error when pulling data from from Redis.") + .withCause(e) + .asRuntimeException(); + } + }); + return features; + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java index e1fbd965f2..6bf5fce52e 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java @@ -19,13 +19,13 @@ import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Timestamp; import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow; import feast.proto.storage.RedisProto.RedisKeyV2; -import feast.proto.types.ValueProto; import feast.storage.api.retriever.Feature; import feast.storage.api.retriever.OnlineRetrieverV2; +import feast.storage.connectors.redis.common.RedisHashDecoder; +import feast.storage.connectors.redis.common.RedisKeyGenerator; import io.grpc.Status; import io.lettuce.core.KeyValue; import io.lettuce.core.RedisClient; @@ -66,43 +66,12 @@ public static OnlineRetrieverV2 create(StatefulRedisConnection c public List>> getOnlineFeatures( String project, List entityRows, List featureReferences) { - List redisKeys = buildRedisKeys(project, entityRows); + List redisKeys = RedisKeyGenerator.buildRedisKeys(project, entityRows); List>> features = getFeaturesFromRedis(redisKeys, featureReferences); return features; } - private List buildRedisKeys(String project, List entityRows) { - List redisKeys = - entityRows.stream() - .map(entityRow -> makeRedisKey(project, entityRow)) - .collect(Collectors.toList()); - - return redisKeys; - } - - /** - * Create {@link RedisKeyV2} - * - * @param project Project where request for features was called from - * @param entityRow {@link EntityRow} - * @return {@link RedisKeyV2} - */ - private RedisKeyV2 makeRedisKey(String project, EntityRow entityRow) { - RedisKeyV2.Builder builder = RedisKeyV2.newBuilder().setProject(project); - Map fieldsMap = entityRow.getFieldsMap(); - List entityNames = new ArrayList<>(new HashSet<>(fieldsMap.keySet())); - - // Sort entity names by alphabetical order - entityNames.sort(String::compareTo); - - for (String entityName : entityNames) { - builder.addEntityNames(entityName); - builder.addEntityValues(fieldsMap.get(entityName)); - } - return builder.build(); - } - private List>> getFeaturesFromRedis( List redisKeys, List featureReferences) { List>> features = new ArrayList<>(); @@ -132,7 +101,8 @@ private List>> getFeaturesFromRedis( byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); // eg. <_ts:featuretable_name> - byte[] featureTableTsBytes = getTimestampRedisHashKeyBytes(featureReference); + byte[] featureTableTsBytes = + RedisHashDecoder.getTimestampRedisHashKeyBytes(featureReference, timestampPrefix); isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); featureReferenceWithTsByteList.add(featureTableTsBytes); }); @@ -157,7 +127,8 @@ private List>> getFeaturesFromRedis( try { List> redisValuesList = future.get(); List> curRedisKeyFeatures = - retrieveFeature(redisValuesList, isTimestampMap, byteToFeatureReferenceMap); + RedisHashDecoder.retrieveFeature( + redisValuesList, isTimestampMap, byteToFeatureReferenceMap, timestampPrefix); features.add(curRedisKeyFeatures); } catch (InterruptedException | ExecutionException | InvalidProtocolBufferException e) { throw Status.UNKNOWN @@ -168,62 +139,4 @@ private List>> getFeaturesFromRedis( }); return features; } - - /** - * Converts all retrieved Redis Hash values based on EntityRows into {@link Feature} - * - * @param redisHashValues retrieved Redis Hash values based on EntityRows - * @param isTimestampMap map to determine if Redis Hash key is a timestamp field - * @param byteToFeatureReferenceMap map to decode bytes back to FeatureReference - * @return List of {@link Feature} - * @throws InvalidProtocolBufferException - */ - private List> retrieveFeature( - List> redisHashValues, - Map isTimestampMap, - Map byteToFeatureReferenceMap) - throws InvalidProtocolBufferException { - List> allFeatures = new ArrayList<>(); - Map> allFeaturesBuilderMap = new HashMap<>(); - Map featureTableTimestampMap = new HashMap<>(); - - for (int i = 0; i < redisHashValues.size(); i++) { - if (redisHashValues.get(i).hasValue()) { - byte[] redisValueK = redisHashValues.get(i).getKey(); - byte[] redisValueV = redisHashValues.get(i).getValue(); - - // Decode data from Redis into Feature object fields - if (isTimestampMap.get(Arrays.toString(redisValueK))) { - Timestamp eventTimestamp = Timestamp.parseFrom(redisValueV); - featureTableTimestampMap.put(Arrays.toString(redisValueK), eventTimestamp); - } else { - FeatureReferenceV2 featureReference = - byteToFeatureReferenceMap.get(redisValueK.toString()); - ValueProto.Value featureValue = ValueProto.Value.parseFrom(redisValueV); - - Feature.Builder featureBuilder = - Feature.builder().setFeatureReference(featureReference).setFeatureValue(featureValue); - allFeaturesBuilderMap.put(featureReference, Optional.of(featureBuilder)); - } - } - } - - // Add timestamp to features - if (allFeaturesBuilderMap.size() > 0) { - for (Map.Entry> entry : - allFeaturesBuilderMap.entrySet()) { - byte[] timestampFeatureTableHashKeyBytes = getTimestampRedisHashKeyBytes(entry.getKey()); - Timestamp curFeatureTimestamp = - featureTableTimestampMap.get(Arrays.toString(timestampFeatureTableHashKeyBytes)); - Feature curFeature = entry.getValue().get().setEventTimestamp(curFeatureTimestamp).build(); - allFeatures.add(Optional.of(curFeature)); - } - } - return allFeatures; - } - - private byte[] getTimestampRedisHashKeyBytes(FeatureReferenceV2 featureReference) { - String timestampRedisHashKeyStr = timestampPrefix + ":" + featureReference.getFeatureTable(); - return timestampRedisHashKeyStr.getBytes(); - } } diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyPrefixSerializerV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyPrefixSerializerV2.java new file mode 100644 index 0000000000..1c869b4fcc --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyPrefixSerializerV2.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.serializer; + +import feast.proto.storage.RedisProto.RedisKeyV2; + +public class RedisKeyPrefixSerializerV2 implements RedisKeySerializerV2 { + + private final byte[] prefixBytes; + + public RedisKeyPrefixSerializerV2(String prefix) { + this.prefixBytes = prefix.getBytes(); + } + + public byte[] serialize(RedisKeyV2 redisKey) { + byte[] key = redisKey.toByteArray(); + + if (prefixBytes.length == 0) { + return key; + } + + byte[] keyWithPrefix = new byte[prefixBytes.length + key.length]; + System.arraycopy(prefixBytes, 0, keyWithPrefix, 0, prefixBytes.length); + System.arraycopy(key, 0, keyWithPrefix, prefixBytes.length, key.length); + return keyWithPrefix; + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyProtoSerializerV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyProtoSerializerV2.java new file mode 100644 index 0000000000..252d6d1422 --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeyProtoSerializerV2.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.serializer; + +import feast.proto.storage.RedisProto.RedisKeyV2; + +public class RedisKeyProtoSerializerV2 implements RedisKeySerializerV2 { + + public byte[] serialize(RedisKeyV2 redisKey) { + return redisKey.toByteArray(); + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeySerializerV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeySerializerV2.java new file mode 100644 index 0000000000..b79e1581da --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/serializer/RedisKeySerializerV2.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.serializer; + +import feast.proto.storage.RedisProto.RedisKeyV2; + +public interface RedisKeySerializerV2 { + + byte[] serialize(RedisKeyV2 key); +} From bad03426244a4901055f803e5454f520ede96115 Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 11:18:35 +0800 Subject: [PATCH 09/17] Attempt to fix IT port in use error Signed-off-by: Terence --- .../src/test/java/feast/serving/it/ServingServiceIT.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/serving/src/test/java/feast/serving/it/ServingServiceIT.java b/serving/src/test/java/feast/serving/it/ServingServiceIT.java index f840a5737d..4c5b6dfbe2 100644 --- a/serving/src/test/java/feast/serving/it/ServingServiceIT.java +++ b/serving/src/test/java/feast/serving/it/ServingServiceIT.java @@ -51,6 +51,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.DockerComposeContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; @@ -67,7 +69,7 @@ public class ServingServiceIT extends BaseAuthIT { static ServingServiceGrpc.ServingServiceBlockingStub servingStub; static RedisCommands syncCommands; - static final int FEAST_SERVING_PORT = 6566; + static final int FEAST_SERVING_PORT = 6568; @LocalServerPort private int metricsPort; @ClassRule @Container @@ -81,6 +83,11 @@ public class ServingServiceIT extends BaseAuthIT { .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))) .withExposedService(REDIS, REDIS_PORT); + @DynamicPropertySource + static void initialize(DynamicPropertyRegistry registry) { + registry.add("grpc.server.port", () -> FEAST_SERVING_PORT); + } + @BeforeAll static void globalSetup() { coreClient = TestUtils.getApiClientForCore(FEAST_CORE_PORT); From 68973c9f48b16726183f32d41bbd5662919c6ee5 Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 16:59:40 +0800 Subject: [PATCH 10/17] Some cleanups Signed-off-by: Terence --- .../redis/common/RedisHashDecoder.java | 35 +++++++++++-------- .../RedisClusterOnlineRetrieverV2.java | 20 +++-------- .../retriever/RedisOnlineRetrieverV2.java | 20 +++-------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java index d41fcf94fb..3412384a45 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java @@ -16,12 +16,14 @@ */ package feast.storage.connectors.redis.common; +import com.google.common.hash.Hashing; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Timestamp; import feast.proto.serving.ServingAPIProto; import feast.proto.types.ValueProto; import feast.storage.api.retriever.Feature; import io.lettuce.core.KeyValue; +import java.nio.charset.StandardCharsets; import java.util.*; public class RedisHashDecoder { @@ -30,14 +32,12 @@ public class RedisHashDecoder { * Converts all retrieved Redis Hash values based on EntityRows into {@link Feature} * * @param redisHashValues retrieved Redis Hash values based on EntityRows - * @param isTimestampMap map to determine if Redis Hash key is a timestamp field * @param byteToFeatureReferenceMap map to decode bytes back to FeatureReference * @return List of {@link Feature} * @throws InvalidProtocolBufferException */ public static List> retrieveFeature( List> redisHashValues, - Map isTimestampMap, Map byteToFeatureReferenceMap, String timestampPrefix) throws InvalidProtocolBufferException { @@ -52,9 +52,9 @@ public static List> retrieveFeature( byte[] redisValueV = redisHashValues.get(i).getValue(); // Decode data from Redis into Feature object fields - if (isTimestampMap.get(Arrays.toString(redisValueK))) { + if (new String(redisValueK).startsWith(timestampPrefix)) { Timestamp eventTimestamp = Timestamp.parseFrom(redisValueV); - featureTableTimestampMap.put(Arrays.toString(redisValueK), eventTimestamp); + featureTableTimestampMap.put(new String(redisValueK), eventTimestamp); } else { ServingAPIProto.FeatureReferenceV2 featureReference = byteToFeatureReferenceMap.get(redisValueK.toString()); @@ -68,17 +68,15 @@ public static List> retrieveFeature( } // Add timestamp to features - if (allFeaturesBuilderMap.size() > 0) { - for (Map.Entry> entry : - allFeaturesBuilderMap.entrySet()) { - byte[] timestampFeatureTableHashKeyBytes = - RedisHashDecoder.getTimestampRedisHashKeyBytes(entry.getKey(), timestampPrefix); - Timestamp curFeatureTimestamp = - featureTableTimestampMap.get(Arrays.toString(timestampFeatureTableHashKeyBytes)); - Feature curFeature = entry.getValue().get().setEventTimestamp(curFeatureTimestamp).build(); - allFeatures.add(Optional.of(curFeature)); - } + for (Map.Entry> entry : + allFeaturesBuilderMap.entrySet()) { + String timestampRedisHashKeyStr = timestampPrefix + ":" + entry.getKey().getFeatureTable(); + Timestamp curFeatureTimestamp = featureTableTimestampMap.get(timestampRedisHashKeyStr); + + Feature curFeature = entry.getValue().get().setEventTimestamp(curFeatureTimestamp).build(); + allFeatures.add(Optional.of(curFeature)); } + return allFeatures; } @@ -87,4 +85,13 @@ public static byte[] getTimestampRedisHashKeyBytes( String timestampRedisHashKeyStr = timestampPrefix + ":" + featureReference.getFeatureTable(); return timestampRedisHashKeyStr.getBytes(); } + + public static byte[] getFeatureReferenceRedisHashKeyBytes( + ServingAPIProto.FeatureReferenceV2 featureReference) { + String delimitedFeatureReference = + featureReference.getFeatureTable() + ":" + featureReference.getName(); + return Hashing.murmur3_32() + .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) + .asBytes(); + } } diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java index c8fb26deb0..829498dd6c 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java @@ -17,7 +17,6 @@ package feast.storage.connectors.redis.retriever; import com.google.common.collect.Lists; -import com.google.common.hash.Hashing; import com.google.protobuf.InvalidProtocolBufferException; import feast.proto.serving.ServingAPIProto; import feast.proto.storage.RedisProto; @@ -35,7 +34,6 @@ import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; import io.lettuce.core.codec.ByteArrayCodec; -import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -75,6 +73,9 @@ private RedisClusterOnlineRetrieverV2(Builder builder) { this.asyncCommands = builder.connection.async(); this.serializer = builder.serializer; this.fallbackSerializer = builder.fallbackSerializer; + + // Disable auto-flushing + this.asyncCommands.setAutoFlushCommands(false); } public static OnlineRetrieverV2 create(Map config) { @@ -154,8 +155,6 @@ private List>> getFeaturesFromRedis( List>> features = new ArrayList<>(); // To decode bytes back to Feature Reference Map byteToFeatureReferenceMap = new HashMap<>(); - // To check whether redis ValueK is a timestamp field - Map isTimestampMap = new HashMap<>(); // Serialize using proto List binaryRedisKeys = @@ -167,26 +166,17 @@ private List>> getFeaturesFromRedis( featureReference -> { // eg. murmur() - String delimitedFeatureReference = - featureReference.getFeatureTable() + ":" + featureReference.getName(); byte[] featureReferenceBytes = - Hashing.murmur3_32() - .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) - .asBytes(); + RedisHashDecoder.getFeatureReferenceRedisHashKeyBytes(featureReference); featureReferenceWithTsByteList.add(featureReferenceBytes); - isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); // eg. <_ts:featuretable_name> byte[] featureTableTsBytes = RedisHashDecoder.getTimestampRedisHashKeyBytes(featureReference, timestampPrefix); - isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); featureReferenceWithTsByteList.add(featureTableTsBytes); }); - // Disable auto-flushing - asyncCommands.setAutoFlushCommands(false); - // Perform a series of independent calls List>>> futures = Lists.newArrayList(); for (byte[] binaryRedisKey : binaryRedisKeys) { @@ -206,7 +196,7 @@ private List>> getFeaturesFromRedis( List> redisValuesList = future.get(); List> curRedisKeyFeatures = RedisHashDecoder.retrieveFeature( - redisValuesList, isTimestampMap, byteToFeatureReferenceMap, timestampPrefix); + redisValuesList, byteToFeatureReferenceMap, timestampPrefix); features.add(curRedisKeyFeatures); } catch (InterruptedException | ExecutionException | InvalidProtocolBufferException e) { throw Status.UNKNOWN diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java index 6bf5fce52e..6c8976de41 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java @@ -17,7 +17,6 @@ package feast.storage.connectors.redis.retriever; import com.google.common.collect.Lists; -import com.google.common.hash.Hashing; import com.google.protobuf.InvalidProtocolBufferException; import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow; @@ -34,7 +33,6 @@ import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.async.RedisAsyncCommands; import io.lettuce.core.codec.ByteArrayCodec; -import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -46,6 +44,9 @@ public class RedisOnlineRetrieverV2 implements OnlineRetrieverV2 { private RedisOnlineRetrieverV2(StatefulRedisConnection connection) { this.asyncCommands = connection.async(); + + // Disable auto-flushing + this.asyncCommands.setAutoFlushCommands(false); } public static OnlineRetrieverV2 create(Map config) { @@ -77,8 +78,6 @@ private List>> getFeaturesFromRedis( List>> features = new ArrayList<>(); // To decode bytes back to Feature Reference Map byteToFeatureReferenceMap = new HashMap<>(); - // To check whether redis ValueK is a timestamp field - Map isTimestampMap = new HashMap<>(); // Serialize using proto List binaryRedisKeys = @@ -90,26 +89,17 @@ private List>> getFeaturesFromRedis( featureReference -> { // eg. murmur() - String delimitedFeatureReference = - featureReference.getFeatureTable() + ":" + featureReference.getName(); byte[] featureReferenceBytes = - Hashing.murmur3_32() - .hashString(delimitedFeatureReference, StandardCharsets.UTF_8) - .asBytes(); + RedisHashDecoder.getFeatureReferenceRedisHashKeyBytes(featureReference); featureReferenceWithTsByteList.add(featureReferenceBytes); - isTimestampMap.put(Arrays.toString(featureReferenceBytes), false); byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); // eg. <_ts:featuretable_name> byte[] featureTableTsBytes = RedisHashDecoder.getTimestampRedisHashKeyBytes(featureReference, timestampPrefix); - isTimestampMap.put(Arrays.toString(featureTableTsBytes), true); featureReferenceWithTsByteList.add(featureTableTsBytes); }); - // Disable auto-flushing - asyncCommands.setAutoFlushCommands(false); - // Perform a series of independent calls List>>> futures = Lists.newArrayList(); for (byte[] binaryRedisKey : binaryRedisKeys) { @@ -128,7 +118,7 @@ private List>> getFeaturesFromRedis( List> redisValuesList = future.get(); List> curRedisKeyFeatures = RedisHashDecoder.retrieveFeature( - redisValuesList, isTimestampMap, byteToFeatureReferenceMap, timestampPrefix); + redisValuesList, byteToFeatureReferenceMap, timestampPrefix); features.add(curRedisKeyFeatures); } catch (InterruptedException | ExecutionException | InvalidProtocolBufferException e) { throw Status.UNKNOWN From aacce7190b2ba70e026d99288eec33d850491d63 Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 21:10:40 +0800 Subject: [PATCH 11/17] Refactor duplicated online retriever code Signed-off-by: Terence --- .../config/ServingServiceConfigV2.java | 9 +- ...eRetrieverV2.java => OnlineRetriever.java} | 50 ++--- .../redis/retriever/RedisClient.java | 57 +++++ .../redis/retriever/RedisClientWrapper.java | 26 +++ .../redis/retriever/RedisClusterClient.java | 106 +++++++++ .../RedisClusterOnlineRetrieverV2.java | 210 ------------------ 6 files changed, 209 insertions(+), 249 deletions(-) rename storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/{RedisOnlineRetrieverV2.java => OnlineRetriever.java} (68%) create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java create mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java delete mode 100644 storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java diff --git a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java index def3dd86bd..b37fb48789 100644 --- a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java +++ b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java @@ -23,8 +23,7 @@ import feast.serving.service.ServingServiceV2; import feast.serving.specs.CachedSpecService; import feast.storage.api.retriever.OnlineRetrieverV2; -import feast.storage.connectors.redis.retriever.RedisClusterOnlineRetrieverV2; -import feast.storage.connectors.redis.retriever.RedisOnlineRetrieverV2; +import feast.storage.connectors.redis.retriever.*; import io.opentracing.Tracer; import java.util.Map; import org.slf4j.Logger; @@ -46,11 +45,13 @@ public ServingServiceV2 servingServiceV2( switch (storeType) { case REDIS_CLUSTER: - OnlineRetrieverV2 redisClusterRetriever = RedisClusterOnlineRetrieverV2.create(config); + RedisClientWrapper redisClusterClient = RedisClusterClient.create(config); + OnlineRetrieverV2 redisClusterRetriever = new OnlineRetriever(redisClusterClient); servingService = new OnlineServingServiceV2(redisClusterRetriever, specService, tracer); break; case REDIS: - OnlineRetrieverV2 redisRetriever = RedisOnlineRetrieverV2.create(config); + RedisClientWrapper redisClient = RedisClient.create(config); + OnlineRetrieverV2 redisRetriever = new OnlineRetriever(redisClient); servingService = new OnlineServingServiceV2(redisRetriever, specService, tracer); break; case CASSANDRA: diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java similarity index 68% rename from storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java rename to storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java index 6c8976de41..310b915837 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverV2.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java @@ -18,66 +18,46 @@ import com.google.common.collect.Lists; import com.google.protobuf.InvalidProtocolBufferException; -import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow; -import feast.proto.storage.RedisProto.RedisKeyV2; +import feast.proto.serving.ServingAPIProto; +import feast.proto.storage.RedisProto; import feast.storage.api.retriever.Feature; import feast.storage.api.retriever.OnlineRetrieverV2; import feast.storage.connectors.redis.common.RedisHashDecoder; import feast.storage.connectors.redis.common.RedisKeyGenerator; import io.grpc.Status; import io.lettuce.core.KeyValue; -import io.lettuce.core.RedisClient; import io.lettuce.core.RedisFuture; -import io.lettuce.core.RedisURI; -import io.lettuce.core.api.StatefulRedisConnection; -import io.lettuce.core.api.async.RedisAsyncCommands; -import io.lettuce.core.codec.ByteArrayCodec; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -public class RedisOnlineRetrieverV2 implements OnlineRetrieverV2 { +public class OnlineRetriever implements OnlineRetrieverV2 { private static final String timestampPrefix = "_ts"; - private final RedisAsyncCommands asyncCommands; + RedisClientWrapper redisClientWrapper; - private RedisOnlineRetrieverV2(StatefulRedisConnection connection) { - this.asyncCommands = connection.async(); - - // Disable auto-flushing - this.asyncCommands.setAutoFlushCommands(false); - } - - public static OnlineRetrieverV2 create(Map config) { - - StatefulRedisConnection connection = - RedisClient.create( - RedisURI.create(config.get("host"), Integer.parseInt(config.get("port")))) - .connect(new ByteArrayCodec()); - - return new RedisOnlineRetrieverV2(connection); - } - - public static OnlineRetrieverV2 create(StatefulRedisConnection connection) { - return new RedisOnlineRetrieverV2(connection); + public OnlineRetriever(RedisClientWrapper redisClientWrapper) { + this.redisClientWrapper = redisClientWrapper; } @Override public List>> getOnlineFeatures( - String project, List entityRows, List featureReferences) { + String project, + List entityRows, + List featureReferences) { - List redisKeys = RedisKeyGenerator.buildRedisKeys(project, entityRows); + List redisKeys = RedisKeyGenerator.buildRedisKeys(project, entityRows); List>> features = getFeaturesFromRedis(redisKeys, featureReferences); return features; } private List>> getFeaturesFromRedis( - List redisKeys, List featureReferences) { + List redisKeys, + List featureReferences) { List>> features = new ArrayList<>(); // To decode bytes back to Feature Reference - Map byteToFeatureReferenceMap = new HashMap<>(); + Map byteToFeatureReferenceMap = new HashMap<>(); // Serialize using proto List binaryRedisKeys = @@ -106,11 +86,11 @@ private List>> getFeaturesFromRedis( byte[][] featureReferenceWithTsByteArrays = featureReferenceWithTsByteList.toArray(new byte[0][]); // Access redis keys and extract features - futures.add(asyncCommands.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); + futures.add(redisClientWrapper.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); } // Write all commands to the transport layer - asyncCommands.flushCommands(); + redisClientWrapper.flushCommands(); futures.forEach( future -> { diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java new file mode 100644 index 0000000000..9ae0e02a8f --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.retriever; + +import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisURI; +import io.lettuce.core.api.StatefulRedisConnection; +import io.lettuce.core.api.async.RedisAsyncCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import java.util.List; +import java.util.Map; + +public class RedisClient implements RedisClientWrapper { + + public final RedisAsyncCommands asyncCommands; + + @Override + public RedisFuture>> hmget(byte[] key, byte[]... fields) { + return asyncCommands.hmget(key, fields); + } + + @Override + public void flushCommands() { + asyncCommands.flushCommands(); + } + + private RedisClient(StatefulRedisConnection connection) { + this.asyncCommands = connection.async(); + + // Disable auto-flushing + this.asyncCommands.setAutoFlushCommands(false); + } + + public static RedisClientWrapper create(Map config) { + StatefulRedisConnection connection = + io.lettuce.core.RedisClient.create( + RedisURI.create(config.get("host"), Integer.parseInt(config.get("port")))) + .connect(new ByteArrayCodec()); + + return new RedisClient(connection); + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java new file mode 100644 index 0000000000..7dedf4318c --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.retriever; + +import io.lettuce.core.*; +import java.util.List; + +public interface RedisClientWrapper { + RedisFuture>> hmget(byte[] key, byte[]... fields); + + void flushCommands(); +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java new file mode 100644 index 0000000000..0568d3b354 --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.storage.connectors.redis.retriever; + +import feast.storage.connectors.redis.serializer.RedisKeyPrefixSerializerV2; +import feast.storage.connectors.redis.serializer.RedisKeySerializerV2; +import io.lettuce.core.KeyValue; +import io.lettuce.core.RedisFuture; +import io.lettuce.core.RedisURI; +import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; +import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; +import io.lettuce.core.codec.ByteArrayCodec; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +public class RedisClusterClient implements RedisClientWrapper { + + public final RedisAdvancedClusterAsyncCommands asyncCommands; + public final RedisKeySerializerV2 serializer; + @Nullable public final RedisKeySerializerV2 fallbackSerializer; + + @Override + public RedisFuture>> hmget(byte[] key, byte[]... fields) { + return asyncCommands.hmget(key, fields); + } + + @Override + public void flushCommands() { + asyncCommands.flushCommands(); + } + + static class Builder { + private final StatefulRedisClusterConnection connection; + private final RedisKeySerializerV2 serializer; + @Nullable private RedisKeySerializerV2 fallbackSerializer; + + Builder( + StatefulRedisClusterConnection connection, + RedisKeySerializerV2 serializer) { + this.connection = connection; + this.serializer = serializer; + } + + Builder withFallbackSerializer(RedisKeySerializerV2 fallbackSerializer) { + this.fallbackSerializer = fallbackSerializer; + return this; + } + + RedisClusterClient build() { + return new RedisClusterClient(this); + } + } + + private RedisClusterClient(Builder builder) { + this.asyncCommands = builder.connection.async(); + this.serializer = builder.serializer; + this.fallbackSerializer = builder.fallbackSerializer; + + // Disable auto-flushing + this.asyncCommands.setAutoFlushCommands(false); + } + + public static RedisClientWrapper create(Map config) { + List redisURIList = + Arrays.stream(config.get("connection_string").split(",")) + .map( + hostPort -> { + String[] hostPortSplit = hostPort.trim().split(":"); + return RedisURI.create(hostPortSplit[0], Integer.parseInt(hostPortSplit[1])); + }) + .collect(Collectors.toList()); + StatefulRedisClusterConnection connection = + io.lettuce.core.cluster.RedisClusterClient.create(redisURIList) + .connect(new ByteArrayCodec()); + + RedisKeySerializerV2 serializer = + new RedisKeyPrefixSerializerV2(config.getOrDefault("key_prefix", "")); + + Builder builder = new Builder(connection, serializer); + + if (Boolean.parseBoolean(config.getOrDefault("enable_fallback", "false"))) { + RedisKeySerializerV2 fallbackSerializer = + new RedisKeyPrefixSerializerV2(config.getOrDefault("fallback_prefix", "")); + builder = builder.withFallbackSerializer(fallbackSerializer); + } + + return builder.build(); + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java deleted file mode 100644 index 829498dd6c..0000000000 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverV2.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2018-2020 The Feast Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feast.storage.connectors.redis.retriever; - -import com.google.common.collect.Lists; -import com.google.protobuf.InvalidProtocolBufferException; -import feast.proto.serving.ServingAPIProto; -import feast.proto.storage.RedisProto; -import feast.proto.types.ValueProto; -import feast.storage.api.retriever.Feature; -import feast.storage.api.retriever.OnlineRetrieverV2; -import feast.storage.connectors.redis.common.RedisHashDecoder; -import feast.storage.connectors.redis.serializer.RedisKeyPrefixSerializerV2; -import feast.storage.connectors.redis.serializer.RedisKeySerializerV2; -import io.grpc.Status; -import io.lettuce.core.KeyValue; -import io.lettuce.core.RedisFuture; -import io.lettuce.core.RedisURI; -import io.lettuce.core.cluster.RedisClusterClient; -import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; -import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; -import io.lettuce.core.codec.ByteArrayCodec; -import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; -import javax.annotation.Nullable; - -/** Defines a storage retriever */ -public class RedisClusterOnlineRetrieverV2 implements OnlineRetrieverV2 { - - private static final String timestampPrefix = "_ts"; - private final RedisAdvancedClusterAsyncCommands asyncCommands; - private final RedisKeySerializerV2 serializer; - @Nullable private final RedisKeySerializerV2 fallbackSerializer; - - static class Builder { - private final StatefulRedisClusterConnection connection; - private final RedisKeySerializerV2 serializer; - @Nullable private RedisKeySerializerV2 fallbackSerializer; - - Builder( - StatefulRedisClusterConnection connection, - RedisKeySerializerV2 serializer) { - this.connection = connection; - this.serializer = serializer; - } - - Builder withFallbackSerializer(RedisKeySerializerV2 fallbackSerializer) { - this.fallbackSerializer = fallbackSerializer; - return this; - } - - RedisClusterOnlineRetrieverV2 build() { - return new RedisClusterOnlineRetrieverV2(this); - } - } - - private RedisClusterOnlineRetrieverV2(Builder builder) { - this.asyncCommands = builder.connection.async(); - this.serializer = builder.serializer; - this.fallbackSerializer = builder.fallbackSerializer; - - // Disable auto-flushing - this.asyncCommands.setAutoFlushCommands(false); - } - - public static OnlineRetrieverV2 create(Map config) { - List redisURIList = - Arrays.stream(config.get("connection_string").split(",")) - .map( - hostPort -> { - String[] hostPortSplit = hostPort.trim().split(":"); - return RedisURI.create(hostPortSplit[0], Integer.parseInt(hostPortSplit[1])); - }) - .collect(Collectors.toList()); - StatefulRedisClusterConnection connection = - RedisClusterClient.create(redisURIList).connect(new ByteArrayCodec()); - - RedisKeySerializerV2 serializer = - new RedisKeyPrefixSerializerV2(config.getOrDefault("key_prefix", "")); - - Builder builder = new Builder(connection, serializer); - - if (Boolean.parseBoolean(config.getOrDefault("enable_fallback", "false"))) { - RedisKeySerializerV2 fallbackSerializer = - new RedisKeyPrefixSerializerV2(config.getOrDefault("fallback_prefix", "")); - builder = builder.withFallbackSerializer(fallbackSerializer); - } - - return builder.build(); - } - - @Override - public List>> getOnlineFeatures( - String project, - List entityRows, - List featureReferences) { - - List redisKeys = buildRedisKeys(project, entityRows); - List>> features = getFeaturesFromRedis(redisKeys, featureReferences); - - return features; - } - - private List buildRedisKeys( - String project, List entityRows) { - List redisKeys = - entityRows.stream() - .map(entityRow -> makeRedisKey(project, entityRow)) - .collect(Collectors.toList()); - - return redisKeys; - } - - /** - * Create {@link RedisProto.RedisKeyV2} - * - * @param project Project where request for features was called from - * @param entityRow {@link ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow} - * @return {@link RedisProto.RedisKeyV2} - */ - private RedisProto.RedisKeyV2 makeRedisKey( - String project, ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow entityRow) { - RedisProto.RedisKeyV2.Builder builder = RedisProto.RedisKeyV2.newBuilder().setProject(project); - Map fieldsMap = entityRow.getFieldsMap(); - List entityNames = new ArrayList<>(new HashSet<>(fieldsMap.keySet())); - - // Sort entity names by alphabetical order - entityNames.sort(String::compareTo); - - for (String entityName : entityNames) { - builder.addEntityNames(entityName); - builder.addEntityValues(fieldsMap.get(entityName)); - } - return builder.build(); - } - - private List>> getFeaturesFromRedis( - List redisKeys, - List featureReferences) { - List>> features = new ArrayList<>(); - // To decode bytes back to Feature Reference - Map byteToFeatureReferenceMap = new HashMap<>(); - - // Serialize using proto - List binaryRedisKeys = - redisKeys.stream().map(redisKey -> redisKey.toByteArray()).collect(Collectors.toList()); - - List featureReferenceWithTsByteList = new ArrayList<>(); - featureReferences.stream() - .forEach( - featureReference -> { - - // eg. murmur() - byte[] featureReferenceBytes = - RedisHashDecoder.getFeatureReferenceRedisHashKeyBytes(featureReference); - featureReferenceWithTsByteList.add(featureReferenceBytes); - byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); - - // eg. <_ts:featuretable_name> - byte[] featureTableTsBytes = - RedisHashDecoder.getTimestampRedisHashKeyBytes(featureReference, timestampPrefix); - featureReferenceWithTsByteList.add(featureTableTsBytes); - }); - - // Perform a series of independent calls - List>>> futures = Lists.newArrayList(); - for (byte[] binaryRedisKey : binaryRedisKeys) { - byte[][] featureReferenceWithTsByteArrays = - featureReferenceWithTsByteList.toArray(new byte[0][]); - // Access redis keys and extract features - futures.add(asyncCommands.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); - } - - // Write all commands to the transport layer - asyncCommands.flushCommands(); - - // TODO: Add in redis key prefix logic - futures.forEach( - future -> { - try { - List> redisValuesList = future.get(); - List> curRedisKeyFeatures = - RedisHashDecoder.retrieveFeature( - redisValuesList, byteToFeatureReferenceMap, timestampPrefix); - features.add(curRedisKeyFeatures); - } catch (InterruptedException | ExecutionException | InvalidProtocolBufferException e) { - throw Status.UNKNOWN - .withDescription("Unexpected error when pulling data from from Redis.") - .withCause(e) - .asRuntimeException(); - } - }); - return features; - } -} From 7526f61ca8e7a4b32437abf8b6886fdc4ca443ee Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 21:24:47 +0800 Subject: [PATCH 12/17] Revert accidental deletion in go.sum Signed-off-by: Terence --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 14d81fc1b8..4fcb8cd56d 100644 --- a/go.sum +++ b/go.sum @@ -492,6 +492,8 @@ golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b h1:07IVqnnzaip3TGyl/cy32V5 golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201011145850-ed2f50202694 h1:BANdcOVw3KTuUiyfDp7wrzCpkCe8UP3lowugJngxBTg= golang.org/x/tools v0.0.0-20201011145850-ed2f50202694/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201013053347-2db1cd791039 h1:kLBxO4OPBgPwjg8Vvu+/0DCHIfDwYIGNFcD66NU9kpo= +golang.org/x/tools v0.0.0-20201013053347-2db1cd791039/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= From ff9b84136bc0fe8904006063a4bda55c0a795267 Mon Sep 17 00:00:00 2001 From: Terence Date: Tue, 13 Oct 2020 21:41:21 +0800 Subject: [PATCH 13/17] Fix test utils Signed-off-by: Terence --- serving/src/test/java/feast/serving/it/TestUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serving/src/test/java/feast/serving/it/TestUtils.java b/serving/src/test/java/feast/serving/it/TestUtils.java index a7698d5f27..4903a8c4d0 100644 --- a/serving/src/test/java/feast/serving/it/TestUtils.java +++ b/serving/src/test/java/feast/serving/it/TestUtils.java @@ -97,7 +97,7 @@ public static DataSource createFileDataSourceSpec( .setFileFormat(fileFormat) .setFileUrl(fileURL) .build()) - .setTimestampColumn(timestampColumn) + .setEventTimestampColumn(timestampColumn) .setDatePartitionColumn(datePartitionColumn) .build(); } From 53f7c68615ad55b09b10e319e3496c53a49f8444 Mon Sep 17 00:00:00 2001 From: Terence Date: Wed, 14 Oct 2020 08:28:21 +0800 Subject: [PATCH 14/17] Use immutable map Signed-off-by: Terence --- .../feast/serving/it/ServingServiceIT.java | 79 ++++--------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/serving/src/test/java/feast/serving/it/ServingServiceIT.java b/serving/src/test/java/feast/serving/it/ServingServiceIT.java index 4c5b6dfbe2..19a827c730 100644 --- a/serving/src/test/java/feast/serving/it/ServingServiceIT.java +++ b/serving/src/test/java/feast/serving/it/ServingServiceIT.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.*; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.hash.Hashing; import com.google.protobuf.Timestamp; @@ -118,12 +119,7 @@ static void globalSetup() { // Apply FeatureTable String featureTableName = "rides"; - List entities = - new ArrayList<>() { - { - add(entityName); - } - }; + ImmutableList entities = ImmutableList.of(entityName); ServingAPIProto.FeatureReferenceV2 feature1Reference = DataGenerator.createFeatureReference("rides", "trip_cost"); @@ -213,22 +209,13 @@ public void shouldRegisterAndGetOnlineFeatures() { // Instantiate EntityRows GetOnlineFeaturesRequestV2.EntityRow entityRow1 = DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 100); - List entityRows = - new ArrayList<>() { - { - add(entityRow1); - } - }; + ImmutableList entityRows = ImmutableList.of(entityRow1); // Instantiate FeatureReferences ServingAPIProto.FeatureReferenceV2 feature1Reference = DataGenerator.createFeatureReference("rides", "trip_cost"); - List featureReferences = - new ArrayList<>() { - { - add(feature1Reference); - } - }; + ImmutableList featureReferences = + ImmutableList.of(feature1Reference); // Build GetOnlineFeaturesRequestV2 GetOnlineFeaturesRequestV2 onlineFeatureRequest = @@ -255,12 +242,8 @@ public void shouldRegisterAndGetOnlineFeatures() { .putAllFields(expectedValueMap) .putAllStatuses(expectedStatusMap) .build(); - List expectedFieldValuesList = - new ArrayList<>() { - { - add(expectedFieldValues); - } - }; + ImmutableList expectedFieldValuesList = + ImmutableList.of(expectedFieldValues); assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); } @@ -275,12 +258,7 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { // Instantiate EntityRows GetOnlineFeaturesRequestV2.EntityRow entityRow1 = DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 100); - List entityRows = - new ArrayList<>() { - { - add(entityRow1); - } - }; + ImmutableList entityRows = ImmutableList.of(entityRow1); // Instantiate FeatureReferences ServingAPIProto.FeatureReferenceV2 featureReference = @@ -290,14 +268,8 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { ServingAPIProto.FeatureReferenceV2 emptyFeatureReference = DataGenerator.createFeatureReference("rides", "trip_empty"); - List featureReferences = - new ArrayList<>() { - { - add(featureReference); - add(notFoundFeatureReference); - add(emptyFeatureReference); - } - }; + ImmutableList featureReferences = + ImmutableList.of(featureReference, notFoundFeatureReference, emptyFeatureReference); // Build GetOnlineFeaturesRequestV2 GetOnlineFeaturesRequestV2 onlineFeatureRequest = @@ -332,12 +304,8 @@ public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { .putAllFields(expectedValueMap) .putAllStatuses(expectedStatusMap) .build(); - List expectedFieldValuesList = - new ArrayList<>() { - { - add(expectedFieldValues); - } - }; + ImmutableList expectedFieldValuesList = + ImmutableList.of(expectedFieldValues); assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); } @@ -351,23 +319,14 @@ public void shouldGetOnlineFeaturesOutsideMaxAge() { // Instantiate EntityRows GetOnlineFeaturesRequestV2.EntityRow entityRow1 = DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 7400); - List entityRows = - new ArrayList<>() { - { - add(entityRow1); - } - }; + ImmutableList entityRows = ImmutableList.of(entityRow1); // Instantiate FeatureReferences ServingAPIProto.FeatureReferenceV2 featureReference = DataGenerator.createFeatureReference("rides", "trip_cost"); - List featureReferences = - new ArrayList<>() { - { - add(featureReference); - } - }; + ImmutableList featureReferences = + ImmutableList.of(featureReference); // Build GetOnlineFeaturesRequestV2 GetOnlineFeaturesRequestV2 onlineFeatureRequest = @@ -394,12 +353,8 @@ public void shouldGetOnlineFeaturesOutsideMaxAge() { .putAllFields(expectedValueMap) .putAllStatuses(expectedStatusMap) .build(); - List expectedFieldValuesList = - new ArrayList<>() { - { - add(expectedFieldValues); - } - }; + ImmutableList expectedFieldValuesList = + ImmutableList.of(expectedFieldValues); assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); } From c2ccc39baa98aa1cc48f6b7a6fc3faf32d918b87 Mon Sep 17 00:00:00 2001 From: Terence Date: Wed, 14 Oct 2020 11:41:30 +0800 Subject: [PATCH 15/17] Rename redis client adapter and add more tests Signed-off-by: Terence --- .../java/feast/common/it/DataGenerator.java | 26 +++++ .../config/ServingServiceConfigV2.java | 4 +- .../feast/serving/util/RequestHelper.java | 20 +++- .../service/CachedSpecServiceTest.java | 104 +++++++++++++----- .../feast/serving/util/RequestHelperTest.java | 54 +++++++++ .../redis/retriever/OnlineRetriever.java | 10 +- .../redis/retriever/RedisClient.java | 4 +- ...ntWrapper.java => RedisClientAdapter.java} | 2 +- .../redis/retriever/RedisClusterClient.java | 4 +- 9 files changed, 185 insertions(+), 43 deletions(-) create mode 100644 serving/src/test/java/feast/serving/util/RequestHelperTest.java rename storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/{RedisClientWrapper.java => RedisClientAdapter.java} (95%) diff --git a/common-test/src/main/java/feast/common/it/DataGenerator.java b/common-test/src/main/java/feast/common/it/DataGenerator.java index 62626b89ab..487455814a 100644 --- a/common-test/src/main/java/feast/common/it/DataGenerator.java +++ b/common-test/src/main/java/feast/common/it/DataGenerator.java @@ -17,6 +17,7 @@ package feast.common.it; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import feast.proto.core.DataSourceProto.DataSource; @@ -239,6 +240,31 @@ public static FeatureTableSpec createFeatureTableSpec( .build(); } + public static FeatureTableSpec createFeatureTableSpec( + String name, + List entities, + ImmutableMap features, + int maxAgeSecs, + Map labels) { + + return FeatureTableSpec.newBuilder() + .setName(name) + .addAllEntities(entities) + .addAllFeatures( + features.entrySet().stream() + .map( + entry -> + FeatureSpecV2.newBuilder() + .setName(entry.getKey()) + .setValueType(entry.getValue()) + .putAllLabels(labels) + .build()) + .collect(Collectors.toList())) + .setMaxAge(Duration.newBuilder().setSeconds(maxAgeSecs).build()) + .putAllLabels(labels) + .build(); + } + public static DataSource createFileDataSourceSpec( String fileURL, String fileFormat, String timestampColumn, String datePartitionColumn) { return DataSource.newBuilder() diff --git a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java index b37fb48789..3508c64648 100644 --- a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java +++ b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java @@ -45,12 +45,12 @@ public ServingServiceV2 servingServiceV2( switch (storeType) { case REDIS_CLUSTER: - RedisClientWrapper redisClusterClient = RedisClusterClient.create(config); + RedisClientAdapter redisClusterClient = RedisClusterClient.create(config); OnlineRetrieverV2 redisClusterRetriever = new OnlineRetriever(redisClusterClient); servingService = new OnlineServingServiceV2(redisClusterRetriever, specService, tracer); break; case REDIS: - RedisClientWrapper redisClient = RedisClient.create(config); + RedisClientAdapter redisClient = RedisClient.create(config); OnlineRetrieverV2 redisRetriever = new OnlineRetriever(redisClient); servingService = new OnlineServingServiceV2(redisRetriever, specService, tracer); break; diff --git a/serving/src/main/java/feast/serving/util/RequestHelper.java b/serving/src/main/java/feast/serving/util/RequestHelper.java index 1caadafbb7..6a3a79b03e 100644 --- a/serving/src/main/java/feast/serving/util/RequestHelper.java +++ b/serving/src/main/java/feast/serving/util/RequestHelper.java @@ -17,6 +17,7 @@ package feast.serving.util; import feast.proto.serving.ServingAPIProto.FeatureReference; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; import feast.proto.serving.ServingAPIProto.GetBatchFeaturesRequest; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequest; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; @@ -36,11 +37,13 @@ public static void validateOnlineRequest(GetOnlineFeaturesRequest request) { } public static void validateOnlineRequest(GetOnlineFeaturesRequestV2 request) { - // EntityDataSetRow shall not be empty + // All EntityRows should not be empty if (request.getEntityRowsCount() <= 0) { - throw Status.INVALID_ARGUMENT - .withDescription("Entity value must be provided") - .asRuntimeException(); + throw new IllegalArgumentException("Entity value must be provided"); + } + // All FeatureReferences should have FeatureTable name and Feature name + for (FeatureReferenceV2 featureReference : request.getFeaturesList()) { + validateOnlineRequestFeatureReference(featureReference); } } @@ -67,4 +70,13 @@ public static void validateBatchRequest(GetBatchFeaturesRequest getFeaturesReque .asRuntimeException(); } } + + public static void validateOnlineRequestFeatureReference(FeatureReferenceV2 featureReference) { + if (featureReference.getFeatureTable().isEmpty()) { + throw new IllegalArgumentException("FeatureTable name must be provided in FeatureReference"); + } + if (featureReference.getName().isEmpty()) { + throw new IllegalArgumentException("Feature name must be provided in FeatureReference"); + } + } } diff --git a/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java b/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java index eb228ee5b2..b41a75918f 100644 --- a/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java +++ b/serving/src/test/java/feast/serving/service/CachedSpecServiceTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import feast.common.it.DataGenerator; import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest; @@ -40,12 +41,12 @@ import feast.proto.core.StoreProto.Store; import feast.proto.core.StoreProto.Store.Subscription; import feast.proto.serving.ServingAPIProto.FeatureReference; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; import feast.proto.types.ValueProto; import feast.serving.exception.SpecRetrievalException; import feast.serving.specs.CachedSpecService; import feast.serving.specs.CoreSpecService; import feast.storage.api.retriever.FeatureSetRequest; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,6 +67,12 @@ public class CachedSpecServiceTest { private Map featureSetSpecs; private CachedSpecService cachedSpecService; + private ImmutableList featureTableEntities; + private ImmutableMap featureTable1Features; + private ImmutableMap featureTable2Features; + private FeatureTableSpec featureTable1Spec; + private FeatureTableSpec featureTable2Spec; + @Before public void setUp() { initMocks(this); @@ -92,6 +99,32 @@ public void setUp() { "default", "fs3", List.of(FeatureSpec.newBuilder().setName("feature4").build())); this.setupProject("default"); + this.featureTableEntities = ImmutableList.of("entity1"); + this.featureTable1Features = + ImmutableMap.of( + "trip_cost1", ValueProto.ValueType.Enum.INT64, + "trip_distance1", ValueProto.ValueType.Enum.DOUBLE, + "trip_empty1", ValueProto.ValueType.Enum.DOUBLE); + this.featureTable2Features = + ImmutableMap.of( + "trip_cost2", ValueProto.ValueType.Enum.INT64, + "trip_distance2", ValueProto.ValueType.Enum.DOUBLE, + "trip_empty2", ValueProto.ValueType.Enum.DOUBLE); + this.featureTable1Spec = + DataGenerator.createFeatureTableSpec( + "featuretable1", + this.featureTableEntities, + featureTable1Features, + 7200, + ImmutableMap.of()); + this.featureTable2Spec = + DataGenerator.createFeatureTableSpec( + "featuretable2", + this.featureTableEntities, + featureTable2Features, + 7200, + ImmutableMap.of()); + this.setupFeatureTableAndProject("default"); when(this.coreService.registerStore(store)).thenReturn(store); @@ -104,37 +137,23 @@ private void setupProject(String project) { } private void setupFeatureTableAndProject(String project) { - String featureTableName = "tablename"; - List entities = - new ArrayList<>() { - { - add("entity1"); - } - }; - ImmutableMap features = - ImmutableMap.of( - "trip_cost", ValueProto.ValueType.Enum.INT64, - "trip_distance", ValueProto.ValueType.Enum.DOUBLE, - "trip_empty", ValueProto.ValueType.Enum.DOUBLE); - FeatureTableSpec ftSpec = - DataGenerator.createFeatureTableSpec( - featureTableName, - entities, - new HashMap<>() { - { - putAll(features); - } - }, - 7200, - ImmutableMap.of("feat_key2", "feat_value2")); - FeatureTableProto.FeatureTable featureTable = - FeatureTableProto.FeatureTable.newBuilder().setSpec(ftSpec).build(); + ImmutableMap featureTable1Features = + this.featureTable1Features; + + FeatureTableProto.FeatureTable featureTable1 = + FeatureTableProto.FeatureTable.newBuilder().setSpec(this.featureTable1Spec).build(); + FeatureTableProto.FeatureTable featureTable2 = + FeatureTableProto.FeatureTable.newBuilder().setSpec(this.featureTable2Spec).build(); when(coreService.listFeatureTables( ListFeatureTablesRequest.newBuilder() .setFilter(ListFeatureTablesRequest.Filter.newBuilder().setProject(project).build()) .build())) - .thenReturn(ListFeatureTablesResponse.newBuilder().addTables(featureTable).build()); + .thenReturn( + ListFeatureTablesResponse.newBuilder() + .addTables(featureTable1) + .addTables(featureTable2) + .build()); } private void setupFeatureSetAndStoreSubscription( @@ -180,6 +199,37 @@ public void shouldPopulateAndReturnStore() { assertThat(actual, equalTo(store)); } + @Test + public void shouldPopulateAndReturnDifferentFeatureTables() { + // test that CachedSpecService can retrieve fully qualified feature references. + cachedSpecService.populateCache(); + FeatureReferenceV2 featureReference1 = + FeatureReferenceV2.newBuilder() + .setFeatureTable("featuretable1") + .setName("trip_cost1") + .build(); + FeatureReferenceV2 featureReference2 = + FeatureReferenceV2.newBuilder() + .setFeatureTable("featuretable1") + .setName("trip_distance1") + .build(); + FeatureReferenceV2 featureReference3 = + FeatureReferenceV2.newBuilder() + .setFeatureTable("featuretable2") + .setName("trip_empty2") + .build(); + + assertThat( + cachedSpecService.getFeatureTableSpec("default", featureReference1), + equalTo(this.featureTable1Spec)); + assertThat( + cachedSpecService.getFeatureTableSpec("default", featureReference2), + equalTo(this.featureTable1Spec)); + assertThat( + cachedSpecService.getFeatureTableSpec("default", featureReference3), + equalTo(this.featureTable2Spec)); + } + @Test public void shouldPopulateAndReturnFeatureSets() { // test that CachedSpecService can retrieve fully qualified feature references. diff --git a/serving/src/test/java/feast/serving/util/RequestHelperTest.java b/serving/src/test/java/feast/serving/util/RequestHelperTest.java new file mode 100644 index 0000000000..140d46cd56 --- /dev/null +++ b/serving/src/test/java/feast/serving/util/RequestHelperTest.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.serving.util; + +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; +import org.junit.Test; + +public class RequestHelperTest { + + @Test(expected = IllegalArgumentException.class) + public void shouldErrorIfEntityRowEmpty() { + FeatureReferenceV2 featureReference = + FeatureReferenceV2.newBuilder() + .setFeatureTable("featuretablename") + .setName("featurename") + .build(); + GetOnlineFeaturesRequestV2 getOnlineFeaturesRequestV2 = + GetOnlineFeaturesRequestV2.newBuilder().addFeatures(featureReference).build(); + RequestHelper.validateOnlineRequest(getOnlineFeaturesRequestV2); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldErrorIfFeatureReferenceTableEmpty() { + FeatureReferenceV2 featureReference = + FeatureReferenceV2.newBuilder().setName("featurename").build(); + GetOnlineFeaturesRequestV2 getOnlineFeaturesRequestV2 = + GetOnlineFeaturesRequestV2.newBuilder().addFeatures(featureReference).build(); + RequestHelper.validateOnlineRequest(getOnlineFeaturesRequestV2); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldErrorIfFeatureReferenceNameEmpty() { + FeatureReferenceV2 featureReference = + FeatureReferenceV2.newBuilder().setFeatureTable("featuretablename").build(); + GetOnlineFeaturesRequestV2 getOnlineFeaturesRequestV2 = + GetOnlineFeaturesRequestV2.newBuilder().addFeatures(featureReference).build(); + RequestHelper.validateOnlineRequest(getOnlineFeaturesRequestV2); + } +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java index 310b915837..d58fabb9b9 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/OnlineRetriever.java @@ -34,10 +34,10 @@ public class OnlineRetriever implements OnlineRetrieverV2 { private static final String timestampPrefix = "_ts"; - RedisClientWrapper redisClientWrapper; + private RedisClientAdapter redisClientAdapter; - public OnlineRetriever(RedisClientWrapper redisClientWrapper) { - this.redisClientWrapper = redisClientWrapper; + public OnlineRetriever(RedisClientAdapter redisClientAdapter) { + this.redisClientAdapter = redisClientAdapter; } @Override @@ -86,11 +86,11 @@ private List>> getFeaturesFromRedis( byte[][] featureReferenceWithTsByteArrays = featureReferenceWithTsByteList.toArray(new byte[0][]); // Access redis keys and extract features - futures.add(redisClientWrapper.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); + futures.add(redisClientAdapter.hmget(binaryRedisKey, featureReferenceWithTsByteArrays)); } // Write all commands to the transport layer - redisClientWrapper.flushCommands(); + redisClientAdapter.flushCommands(); futures.forEach( future -> { diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java index 9ae0e02a8f..d8a035f29f 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Map; -public class RedisClient implements RedisClientWrapper { +public class RedisClient implements RedisClientAdapter { public final RedisAsyncCommands asyncCommands; @@ -46,7 +46,7 @@ private RedisClient(StatefulRedisConnection connection) { this.asyncCommands.setAutoFlushCommands(false); } - public static RedisClientWrapper create(Map config) { + public static RedisClientAdapter create(Map config) { StatefulRedisConnection connection = io.lettuce.core.RedisClient.create( RedisURI.create(config.get("host"), Integer.parseInt(config.get("port")))) diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientAdapter.java similarity index 95% rename from storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java rename to storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientAdapter.java index 7dedf4318c..65e730ae93 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientWrapper.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClientAdapter.java @@ -19,7 +19,7 @@ import io.lettuce.core.*; import java.util.List; -public interface RedisClientWrapper { +public interface RedisClientAdapter { RedisFuture>> hmget(byte[] key, byte[]... fields); void flushCommands(); diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java index 0568d3b354..97541d3cdb 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java @@ -30,7 +30,7 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; -public class RedisClusterClient implements RedisClientWrapper { +public class RedisClusterClient implements RedisClientAdapter { public final RedisAdvancedClusterAsyncCommands asyncCommands; public final RedisKeySerializerV2 serializer; @@ -77,7 +77,7 @@ private RedisClusterClient(Builder builder) { this.asyncCommands.setAutoFlushCommands(false); } - public static RedisClientWrapper create(Map config) { + public static RedisClientAdapter create(Map config) { List redisURIList = Arrays.stream(config.get("connection_string").split(",")) .map( From 72204825080f47229e399908d90d80d535e6e0dd Mon Sep 17 00:00:00 2001 From: Terence Date: Wed, 14 Oct 2020 12:26:56 +0800 Subject: [PATCH 16/17] Make properties private Signed-off-by: Terence --- .../storage/connectors/redis/retriever/RedisClient.java | 2 +- .../connectors/redis/retriever/RedisClusterClient.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java index d8a035f29f..fdf26b98bc 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClient.java @@ -27,7 +27,7 @@ public class RedisClient implements RedisClientAdapter { - public final RedisAsyncCommands asyncCommands; + private final RedisAsyncCommands asyncCommands; @Override public RedisFuture>> hmget(byte[] key, byte[]... fields) { diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java index 97541d3cdb..ec6a2cff64 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterClient.java @@ -32,9 +32,9 @@ public class RedisClusterClient implements RedisClientAdapter { - public final RedisAdvancedClusterAsyncCommands asyncCommands; - public final RedisKeySerializerV2 serializer; - @Nullable public final RedisKeySerializerV2 fallbackSerializer; + private final RedisAdvancedClusterAsyncCommands asyncCommands; + private final RedisKeySerializerV2 serializer; + @Nullable private final RedisKeySerializerV2 fallbackSerializer; @Override public RedisFuture>> hmget(byte[] key, byte[]... fields) { From f867b52bbcdabc659b89e7d7efc9d4a6ffc7fc10 Mon Sep 17 00:00:00 2001 From: Terence Date: Wed, 14 Oct 2020 12:52:01 +0800 Subject: [PATCH 17/17] Cleanup code Signed-off-by: Terence --- .../main/java/feast/common/models/FeatureTable.java | 13 +++++++++++++ .../java/feast/serving/specs/CachedSpecService.java | 2 +- .../connectors/redis/common/RedisHashDecoder.java | 8 ++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/feast/common/models/FeatureTable.java b/common/src/main/java/feast/common/models/FeatureTable.java index e4dade9962..d4712e7a01 100644 --- a/common/src/main/java/feast/common/models/FeatureTable.java +++ b/common/src/main/java/feast/common/models/FeatureTable.java @@ -17,6 +17,7 @@ package feast.common.models; import feast.proto.core.FeatureTableProto.FeatureTableSpec; +import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; public class FeatureTable { @@ -30,4 +31,16 @@ public class FeatureTable { public static String getFeatureTableStringRef(String project, FeatureTableSpec featureTableSpec) { return String.format("%s/%s", project, featureTableSpec.getName()); } + + /** + * Accepts FeatureReferenceV2 object and returns its reference in String + * "project/featuretable_name". + * + * @param featureReference {@link FeatureReferenceV2} + * @return String format of FeatureTableReference + */ + public static String getFeatureTableStringRef( + String project, FeatureReferenceV2 featureReference) { + return String.format("%s/%s", project, featureReference.getFeatureTable()); + } } diff --git a/serving/src/main/java/feast/serving/specs/CachedSpecService.java b/serving/src/main/java/feast/serving/specs/CachedSpecService.java index 91b99bfd83..d41d4bd817 100644 --- a/serving/src/main/java/feast/serving/specs/CachedSpecService.java +++ b/serving/src/main/java/feast/serving/specs/CachedSpecService.java @@ -397,7 +397,7 @@ private Map getFeatureTableMap() { public FeatureTableSpec getFeatureTableSpec( String project, ServingAPIProto.FeatureReferenceV2 featureReference) { - String featureTableRefStr = project + "/" + featureReference.getFeatureTable(); + String featureTableRefStr = getFeatureTableStringRef(project, featureReference); FeatureTableSpec featureTableSpec; try { featureTableSpec = featureTableCache.get(featureTableRefStr); diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java index 3412384a45..3a64538942 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/common/RedisHashDecoder.java @@ -46,10 +46,10 @@ public static List> retrieveFeature( new HashMap<>(); Map featureTableTimestampMap = new HashMap<>(); - for (int i = 0; i < redisHashValues.size(); i++) { - if (redisHashValues.get(i).hasValue()) { - byte[] redisValueK = redisHashValues.get(i).getKey(); - byte[] redisValueV = redisHashValues.get(i).getValue(); + for (KeyValue entity : redisHashValues) { + if (entity.hasValue()) { + byte[] redisValueK = entity.getKey(); + byte[] redisValueV = entity.getValue(); // Decode data from Redis into Feature object fields if (new String(redisValueK).startsWith(timestampPrefix)) {