diff --git a/.github/workflows/complete.yml b/.github/workflows/complete.yml index 7f6c3fe..c33ff8f 100644 --- a/.github/workflows/complete.yml +++ b/.github/workflows/complete.yml @@ -42,6 +42,16 @@ jobs: integration-test: runs-on: ubuntu-latest needs: unit-test-java + services: + redis: + image: redis + ports: + - 6389:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v2 with: @@ -54,7 +64,7 @@ jobs: architecture: x64 - uses: actions/setup-python@v2 with: - python-version: '3.6' + python-version: '3.7' architecture: 'x64' - uses: actions/cache@v2 with: diff --git a/.github/workflows/master_only.yml b/.github/workflows/master_only.yml index 348362e..417a99d 100644 --- a/.github/workflows/master_only.yml +++ b/.github/workflows/master_only.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v2 with: submodules: 'true' - - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master + - uses: google-github-actions/setup-gcloud@master with: version: '290.0.1' export_default_credentials: true diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 2acd1cd..cf8527a 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -2,7 +2,8 @@ name: mirror on: push: - branches: master + branches: + - master tags: - 'v*.*.*' diff --git a/.gitmodules b/.gitmodules index a908f12..df8838f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "deps/feast"] path = deps/feast url = https://github.com/feast-dev/feast - branch = v0.9-branch + branch = v0.10-branch diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c05fb50..e4e04e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,6 +56,15 @@ make build-docker REGISTRY=gcr.io/kf-feast VERSION=develop ``` +#### IDE Setup +If you're using IntelliJ, some additional steps may be needed to make sure IntelliJ autocomplete works as expected. +Specifically, proto-generated code is not indexed by IntelliJ. To fix this, navigate to the following window in IntelliJ: +`Project Structure > Modules > datatypes-java`, and mark the following folders as `Source` directorys: +- target/generated-sources/protobuf/grpc-java +- target/generated-sources/protobuf/java +- target/generated-sources/annotations + + ## Feast Core ### Environment Setup Setting up your development environment for Feast Core: diff --git a/Makefile b/Makefile index b1f95dc..7d00913 100644 --- a/Makefile +++ b/Makefile @@ -71,10 +71,10 @@ push-serving-docker: docker push $(REGISTRY)/feast-serving:$(VERSION) build-core-docker: - docker build --build-arg VERSION=$(VERSION) -t $(REGISTRY)/feast-core:$(VERSION) -f infra/docker/core/Dockerfile . + docker build --no-cache --build-arg VERSION=$(VERSION) -t $(REGISTRY)/feast-core:$(VERSION) -f infra/docker/core/Dockerfile . build-serving-docker: - docker build --build-arg VERSION=$(VERSION) -t $(REGISTRY)/feast-serving:$(VERSION) -f infra/docker/serving/Dockerfile . + docker build --no-cache --build-arg VERSION=$(VERSION) -t $(REGISTRY)/feast-serving:$(VERSION) -f infra/docker/serving/Dockerfile . # Versions diff --git a/README.md b/README.md index cd2df58..98df035 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# Feast Java components +# Feast Java components (deprecated) [![complete](https://github.com/feast-dev/feast-java/actions/workflows/complete.yml/badge.svg)](https://github.com/feast-dev/feast-java/actions/workflows/complete.yml) +### Note: This repository worked with Feast 0.9 and before. Please look at http://github.com/feast-dev/feast for the more up to date version of this repo. + ### Overview This repository contains the following Feast components. diff --git a/common-test/src/main/java/feast/common/it/BaseIT.java b/common-test/src/main/java/feast/common/it/BaseIT.java index f82a804..8d49b38 100644 --- a/common-test/src/main/java/feast/common/it/BaseIT.java +++ b/common-test/src/main/java/feast/common/it/BaseIT.java @@ -115,7 +115,7 @@ public ConsumerFactory testConsumerFactory() { /** * Truncates all tables in Database (between tests or flows). Retries on deadlock * - * @throws SQLException + * @throws SQLException when a SQL exception occurs */ public static void cleanTables() throws SQLException { Connection connection = @@ -156,7 +156,12 @@ public static void cleanTables() throws SQLException { } } - /** Used to determine SequentialFlows */ + /** + * Used to determine SequentialFlows + * + * @param testInfo test info + * @return true if test is sequential + */ public Boolean isSequentialTest(TestInfo testInfo) { try { testInfo.getTestClass().get().asSubclass(SequentialFlow.class); diff --git a/common-test/src/main/java/feast/common/util/TestUtil.java b/common-test/src/main/java/feast/common/util/TestUtil.java index ee355d3..49e6cc7 100644 --- a/common-test/src/main/java/feast/common/util/TestUtil.java +++ b/common-test/src/main/java/feast/common/util/TestUtil.java @@ -44,6 +44,10 @@ public static void setupAuditLogger() { /** * Compare if two Feature Table specs are equal. Disregards order of features/entities in spec. + * + * @param spec one spec + * @param otherSpec the other spec + * @return true if specs equal */ public static boolean compareFeatureTableSpec(FeatureTableSpec spec, FeatureTableSpec otherSpec) { spec = diff --git a/common/src/main/java/feast/common/auth/credentials/GoogleAuthCredentials.java b/common/src/main/java/feast/common/auth/credentials/GoogleAuthCredentials.java index 0f42325..57aafa2 100644 --- a/common/src/main/java/feast/common/auth/credentials/GoogleAuthCredentials.java +++ b/common/src/main/java/feast/common/auth/credentials/GoogleAuthCredentials.java @@ -45,6 +45,7 @@ public class GoogleAuthCredentials extends CallCredentials { * * @param options a map of options, Required unless specified: audience - Optional, Sets the * target audience of the token obtained. + * @throws IOException if credentials are not available */ public GoogleAuthCredentials(Map options) throws IOException { String targetAudience = options.getOrDefault("audience", "https://localhost"); diff --git a/common/src/main/java/feast/common/logging/AuditLogger.java b/common/src/main/java/feast/common/logging/AuditLogger.java index 0b9901e..5f70fbf 100644 --- a/common/src/main/java/feast/common/logging/AuditLogger.java +++ b/common/src/main/java/feast/common/logging/AuditLogger.java @@ -65,6 +65,7 @@ public AuditLogger(LoggingProperties loggingProperties, BuildProperties buildPro /** * Log the handling of a Protobuf message by a service call. * + * @param level log level * @param entryBuilder with all fields set except instance. */ public static void logMessage(Level level, MessageAuditLogEntry.Builder entryBuilder) { diff --git a/common/src/main/java/feast/common/logging/entry/ActionAuditLogEntry.java b/common/src/main/java/feast/common/logging/entry/ActionAuditLogEntry.java index cec85b7..4fdeaee 100644 --- a/common/src/main/java/feast/common/logging/entry/ActionAuditLogEntry.java +++ b/common/src/main/java/feast/common/logging/entry/ActionAuditLogEntry.java @@ -21,10 +21,10 @@ /** ActionAuditLogEntry records an action being taken on a specific resource */ @AutoValue public abstract class ActionAuditLogEntry extends AuditLogEntry { - /** The name of the action taken on the resource. */ + /** @return The name of the action taken on the resource. */ public abstract String getAction(); - /** The target resource of which the action was taken on. */ + /** @return The target resource of which the action was taken on. */ public abstract LogResource getResource(); /** @@ -34,6 +34,7 @@ public abstract class ActionAuditLogEntry extends AuditLogEntry { * @param version The version of Feast producing this {@link AuditLogEntry}. * @param resource The target resource of which the action was taken on. * @param action The name of the action being taken on the given resource. + * @return log entry that records an action being taken on a specific resource */ public static ActionAuditLogEntry of( String component, String version, LogResource resource, String action) { diff --git a/common/src/main/java/feast/common/logging/entry/AuditLogEntry.java b/common/src/main/java/feast/common/logging/entry/AuditLogEntry.java index 9aa8fcb..8148c47 100644 --- a/common/src/main/java/feast/common/logging/entry/AuditLogEntry.java +++ b/common/src/main/java/feast/common/logging/entry/AuditLogEntry.java @@ -29,15 +29,27 @@ public abstract class AuditLogEntry { public final String application = "Feast"; - /** The name of the Feast component producing this {@link AuditLogEntry} */ + /** + * The name of the Feast component producing this {@link AuditLogEntry} + * + * @return the component + */ public abstract String getComponent(); - /** The version of Feast producing this {@link AuditLogEntry} */ + /** + * The version of Feast producing this {@link AuditLogEntry} + * + * @return version + */ public abstract String getVersion(); public abstract AuditLogEntryKind getKind(); - /** Return a structured JSON representation of this {@link AuditLogEntry} */ + /** + * Return a structured JSON representation of this {@link AuditLogEntry} + * + * @return structured JSON representation + */ public String toJSON() { Gson gson = new Gson(); return gson.toJson(this); diff --git a/common/src/main/java/feast/common/logging/entry/MessageAuditLogEntry.java b/common/src/main/java/feast/common/logging/entry/MessageAuditLogEntry.java index 745cc12..6e5072f 100644 --- a/common/src/main/java/feast/common/logging/entry/MessageAuditLogEntry.java +++ b/common/src/main/java/feast/common/logging/entry/MessageAuditLogEntry.java @@ -34,32 +34,35 @@ /** MessageAuditLogEntry records the handling of a Protobuf message by a service call. */ @AutoValue public abstract class MessageAuditLogEntry extends AuditLogEntry { - /** Id used to identify the service call that the log entry is recording */ + /** @return Id used to identify the service call that the log entry is recording */ public abstract UUID getId(); - /** The name of the service that was used to handle the service call. */ + /** @return The name of the service that was used to handle the service call. */ public abstract String getService(); - /** The name of the method that was used to handle the service call. */ + /** @return The name of the method that was used to handle the service call. */ public abstract String getMethod(); - /** The request Protobuf {@link Message} that was passed to the Service in the service call. */ + /** + * @return The request Protobuf {@link Message} that was passed to the Service in the service + * call. + */ public abstract Message getRequest(); /** - * The response Protobuf {@link Message} that was passed to the Service in the service call. May - * be an {@link Empty} protobuf no request could be collected due to an error. + * @return The response Protobuf {@link Message} that was passed to the Service in the service + * call. May be an {@link Empty} protobuf no request could be collected due to an error. */ public abstract Message getResponse(); /** - * The authenticated identity that was assumed during the handling of the service call. For - * example, the user id or email that identifies the user making the call. Empty if the service - * call is not authenticated. + * @return The authenticated identity that was assumed during the handling of the service call. + * For example, the user id or email that identifies the user making the call. Empty if the + * service call is not authenticated. */ public abstract String getIdentity(); - /** The result status code of the service call. */ + /** @return The result status code of the service call. */ public abstract Code getStatusCode(); @AutoValue.Builder diff --git a/common/src/main/java/feast/common/logging/entry/TransitionAuditLogEntry.java b/common/src/main/java/feast/common/logging/entry/TransitionAuditLogEntry.java index 0f139b7..224f10e 100644 --- a/common/src/main/java/feast/common/logging/entry/TransitionAuditLogEntry.java +++ b/common/src/main/java/feast/common/logging/entry/TransitionAuditLogEntry.java @@ -21,10 +21,10 @@ /** TransitionAuditLogEntry records a transition in state/status in a specific resource. */ @AutoValue public abstract class TransitionAuditLogEntry extends AuditLogEntry { - /** The resource which the state/status transition occured. */ + /** @return The resource which the state/status transition occured. */ public abstract LogResource getResource(); - /** The end status with the resource transition to. */ + /** @return The end status with the resource transition to. */ public abstract String getStatus(); /** @@ -35,6 +35,7 @@ public abstract class TransitionAuditLogEntry extends AuditLogEntry { * @param version The version of Feast producing this {@link AuditLogEntry}. * @param resource the resource which the transtion occured * @param status the end status which the resource transitioned to. + * @return log entry to record a transition in state/status in a specific resource */ public static TransitionAuditLogEntry of( String component, String version, LogResource resource, String status) { diff --git a/common/src/main/java/feast/common/models/FeatureTable.java b/common/src/main/java/feast/common/models/FeatureTable.java index d4712e7..88fac15 100644 --- a/common/src/main/java/feast/common/models/FeatureTable.java +++ b/common/src/main/java/feast/common/models/FeatureTable.java @@ -25,6 +25,7 @@ public class FeatureTable { * Accepts FeatureTableSpec object and returns its reference in String * "project/featuretable_name". * + * @param project project name * @param featureTableSpec {@link FeatureTableSpec} * @return String format of FeatureTableReference */ @@ -36,6 +37,7 @@ public static String getFeatureTableStringRef(String project, FeatureTableSpec f * Accepts FeatureReferenceV2 object and returns its reference in String * "project/featuretable_name". * + * @param project project name * @param featureReference {@link FeatureReferenceV2} * @return String format of FeatureTableReference */ diff --git a/common/src/main/java/feast/common/models/FeatureV2.java b/common/src/main/java/feast/common/models/FeatureV2.java index 8debca3..8420cca 100644 --- a/common/src/main/java/feast/common/models/FeatureV2.java +++ b/common/src/main/java/feast/common/models/FeatureV2.java @@ -34,4 +34,17 @@ public static String getFeatureStringRef(FeatureReferenceV2 featureReference) { } return ref; } + + /** + * Accepts either a feature reference of the form "featuretable_name:feature_name" or just a + * feature name, and returns just the feature name. For example, given either + * "driver_hourly_stats:conv_rate" or "conv_rate", "conv_rate" would be returned. + * + * @param featureReference {String} + * @return Base feature name of the feature reference + */ + public static String getFeatureName(String featureReference) { + String[] tokens = featureReference.split(":", 2); + return tokens[tokens.length - 1]; + } } diff --git a/common/src/main/java/feast/common/validators/OneOfStringValidator.java b/common/src/main/java/feast/common/validators/OneOfStringValidator.java index 42428bd..924953a 100644 --- a/common/src/main/java/feast/common/validators/OneOfStringValidator.java +++ b/common/src/main/java/feast/common/validators/OneOfStringValidator.java @@ -29,7 +29,7 @@ public class OneOfStringValidator implements ConstraintValidator[] groups() default {}; - /** An attribute payload that can be used to assign custom payload objects to a constraint. */ + /** + * @return An attribute payload that can be used to assign custom payload objects to a constraint. + */ Class[] payload() default {}; /** @return Default value that is returned if no allowed values are configured */ diff --git a/core/src/main/java/feast/core/config/WebSecurityConfig.java b/core/src/main/java/feast/core/config/WebSecurityConfig.java index 0f48111..5c66730 100644 --- a/core/src/main/java/feast/core/config/WebSecurityConfig.java +++ b/core/src/main/java/feast/core/config/WebSecurityConfig.java @@ -43,7 +43,7 @@ public WebSecurityConfig(FeastProperties feastProperties) { * Allows for custom web security rules to be applied. * * @param http {@link HttpSecurity} for configuring web based security - * @throws Exception + * @throws Exception unexpected exception */ @Override protected void configure(HttpSecurity http) throws Exception { diff --git a/core/src/main/java/feast/core/model/DataSource.java b/core/src/main/java/feast/core/model/DataSource.java index 67477da..2bfe20f 100644 --- a/core/src/main/java/feast/core/model/DataSource.java +++ b/core/src/main/java/feast/core/model/DataSource.java @@ -87,6 +87,7 @@ public DataSource(SourceType type) { * @param spec Protobuf representation of DataSource to construct from. * @throws IllegalArgumentException when provided with a invalid Protobuf spec * @throws UnsupportedOperationException if source type is unsupported. + * @return data source */ public static DataSource fromProto(DataSourceProto.DataSource spec) { DataSource source = new DataSource(spec.getType()); @@ -132,7 +133,11 @@ public static DataSource fromProto(DataSourceProto.DataSource spec) { return source; } - /** Convert this DataSource to its Protobuf representation. */ + /** + * Convert this DataSource to its Protobuf representation. + * + * @return protobuf representation + */ public DataSourceProto.DataSource toProto() { DataSourceProto.DataSource.Builder spec = DataSourceProto.DataSource.newBuilder(); spec.setType(getType()); diff --git a/core/src/main/java/feast/core/model/EntityV2.java b/core/src/main/java/feast/core/model/EntityV2.java index aeb6728..d72bef1 100644 --- a/core/src/main/java/feast/core/model/EntityV2.java +++ b/core/src/main/java/feast/core/model/EntityV2.java @@ -65,6 +65,11 @@ public EntityV2() { * *

This data model supports Scalar Entity and would allow ease of discovery of entities and * reasoning when used in association with FeatureTable. + * + * @param name name + * @param description description + * @param type type + * @param labels labels */ public EntityV2( String name, String description, ValueType.Enum type, Map labels) { diff --git a/core/src/main/java/feast/core/model/FeatureTable.java b/core/src/main/java/feast/core/model/FeatureTable.java index 479c11e..9b9bdef 100644 --- a/core/src/main/java/feast/core/model/FeatureTable.java +++ b/core/src/main/java/feast/core/model/FeatureTable.java @@ -155,7 +155,9 @@ public static FeatureTable fromProto( /** * Update the FeatureTable from the given Protobuf representation. * + * @param projectName project name * @param spec the Protobuf spec to update the FeatureTable from. + * @param entityRepo repository * @throws IllegalArgumentException if the update will make prohibited changes. */ public void updateFromProto( @@ -211,7 +213,11 @@ public void updateFromProto( this.revision++; } - /** Convert this Feature Table to its Protobuf representation */ + /** + * Convert this Feature Table to its Protobuf representation + * + * @return protobuf representation + */ public FeatureTableProto.FeatureTable toProto() { // Convert field types to Protobuf compatible types Timestamp creationTime = TypeConversion.convertTimestamp(getCreated()); @@ -319,6 +325,7 @@ private Map getFeaturesRefToFeaturesMap(List featu /** * Returns a list of Features if FeatureTable's Feature contains all labels in labelsFilter * + * @param features features * @param labelsFilter contain labels that should be attached to FeatureTable's features * @return List of Features */ diff --git a/core/src/main/java/feast/core/model/FeatureV2.java b/core/src/main/java/feast/core/model/FeatureV2.java index f25e951..d0a6082 100644 --- a/core/src/main/java/feast/core/model/FeatureV2.java +++ b/core/src/main/java/feast/core/model/FeatureV2.java @@ -75,7 +75,11 @@ public static FeatureV2 fromProto(FeatureTable table, FeatureSpecV2 spec) { return new FeatureV2(table, spec.getName(), spec.getValueType(), labelsJSON); } - /** Convert this Feature to its Protobuf representation. */ + /** + * Convert this Feature to its Protobuf representation. + * + * @return protobuf representation + */ public FeatureSpecV2 toProto() { Map labels = TypeConversion.convertJsonStringToMap(getLabelsJSON()); return FeatureSpecV2.newBuilder() diff --git a/core/src/main/java/feast/core/service/SpecService.java b/core/src/main/java/feast/core/service/SpecService.java index 4d00345..ff45dcd 100644 --- a/core/src/main/java/feast/core/service/SpecService.java +++ b/core/src/main/java/feast/core/service/SpecService.java @@ -262,6 +262,7 @@ public ListStoresResponse listStores(ListStoresRequest.Filter filter) { * * @param newEntitySpec EntitySpecV2 that will be used to create or update an Entity. * @param projectName Project namespace of Entity which is to be created/updated + * @return response of the operation */ @Transactional public ApplyEntityResponse applyEntity( @@ -314,6 +315,7 @@ public ApplyEntityResponse applyEntity( * Resolves the project name by returning name if given, autofilling default project otherwise. * * @param projectName name of the project to resolve. + * @return project name */ public static String resolveProjectName(String projectName) { return (projectName.isEmpty()) ? Project.DEFAULT_NAME : projectName; @@ -324,6 +326,7 @@ public static String resolveProjectName(String projectName) { * * @param updateStoreRequest containing the new store definition * @return UpdateStoreResponse containing the new store definition + * @throws InvalidProtocolBufferException if protobuf exception occurs */ @Transactional public UpdateStoreResponse updateStore(UpdateStoreRequest updateStoreRequest) diff --git a/core/src/main/java/feast/core/util/TypeConversion.java b/core/src/main/java/feast/core/util/TypeConversion.java index bbdfa94..d2a2d0a 100644 --- a/core/src/main/java/feast/core/util/TypeConversion.java +++ b/core/src/main/java/feast/core/util/TypeConversion.java @@ -79,7 +79,7 @@ public static Map convertJsonStringToEnumMap(String jsonString) { /** * Marshals a given map into its corresponding json string * - * @param map + * @param map map to be converted * @return json string corresponding to given map */ public static String convertMapToJsonString(Map map) { @@ -89,7 +89,7 @@ public static String convertMapToJsonString(Map map) { /** * Marshals a given Enum map into its corresponding json string * - * @param map + * @param map map to be converted * @return json string corresponding to given Enum map */ public static String convertEnumMapToJsonString(Map map) { diff --git a/core/src/main/java/feast/core/validators/DataSourceValidator.java b/core/src/main/java/feast/core/validators/DataSourceValidator.java index f36e360..548d18f 100644 --- a/core/src/main/java/feast/core/validators/DataSourceValidator.java +++ b/core/src/main/java/feast/core/validators/DataSourceValidator.java @@ -24,7 +24,11 @@ import feast.proto.core.DataSourceProto.DataSource; public class DataSourceValidator { - /** Validate if the given DataSource protobuf spec is valid. */ + /** + * Validate if the given DataSource protobuf spec is valid. + * + * @param spec spec to be validated + */ public static void validate(DataSource spec) { switch (spec.getType()) { case BATCH_FILE: diff --git a/core/src/test/java/feast/core/auth/CoreServiceAuthorizationIT.java b/core/src/test/java/feast/core/auth/CoreServiceAuthorizationIT.java deleted file mode 100644 index 41faee7..0000000 --- a/core/src/test/java/feast/core/auth/CoreServiceAuthorizationIT.java +++ /dev/null @@ -1,354 +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.core.auth; - -import static org.junit.jupiter.api.Assertions.*; -import static org.testcontainers.containers.wait.strategy.Wait.forHttp; - -import avro.shaded.com.google.common.collect.ImmutableMap; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit.WireMockClassRule; -import com.google.protobuf.InvalidProtocolBufferException; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWKSet; -import feast.common.it.BaseIT; -import feast.common.it.DataGenerator; -import feast.common.it.SimpleCoreClient; -import feast.core.auth.infra.JwtHelper; -import feast.core.config.FeastProperties; -import feast.proto.core.CoreServiceGrpc; -import feast.proto.core.EntityProto; -import feast.proto.types.ValueProto; -import io.grpc.CallCredentials; -import io.grpc.Channel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.StatusRuntimeException; -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.springframework.util.SocketUtils; -import org.testcontainers.containers.DockerComposeContainer; -import sh.ory.keto.ApiClient; -import sh.ory.keto.ApiException; -import sh.ory.keto.Configuration; -import sh.ory.keto.api.EnginesApi; -import sh.ory.keto.model.OryAccessControlPolicy; -import sh.ory.keto.model.OryAccessControlPolicyRole; - -@SpringBootTest( - properties = { - "feast.security.authentication.enabled=true", - "feast.security.authorization.enabled=true", - "feast.security.authorization.provider=http", - }) -public class CoreServiceAuthorizationIT extends BaseIT { - - @Autowired FeastProperties feastProperties; - - private static final String DEFAULT_FLAVOR = "glob"; - private static int KETO_PORT = 4466; - private static int KETO_ADAPTOR_PORT = 8080; - private static int feast_core_port; - private static int JWKS_PORT = SocketUtils.findAvailableTcpPort(); - - private static JwtHelper jwtHelper = new JwtHelper(); - - static String project = "myproject"; - static String subjectInProject = "good_member@example.com"; - static String subjectIsAdmin = "bossman@example.com"; - static String subjectClaim = "sub"; - - static SimpleCoreClient insecureApiClient; - - @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule(JWKS_PORT); - - @Rule public WireMockClassRule instanceRule = wireMockRule; - - @ClassRule - public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/keto/docker-compose.yml")) - .withExposedService("adaptor_1", KETO_ADAPTOR_PORT) - .withExposedService("keto_1", KETO_PORT, forHttp("/health/ready").forStatusCode(200)); - - @DynamicPropertySource - static void initialize(DynamicPropertyRegistry registry) { - - // Start Keto and with Docker Compose - environment.start(); - - // Seed Keto with data - String ketoExternalHost = environment.getServiceHost("keto_1", KETO_PORT); - Integer ketoExternalPort = environment.getServicePort("keto_1", KETO_PORT); - String ketoExternalUrl = String.format("http://%s:%s", ketoExternalHost, ketoExternalPort); - try { - seedKeto(ketoExternalUrl); - } catch (ApiException e) { - throw new RuntimeException(String.format("Could not seed Keto store %s", ketoExternalUrl)); - } - - // Start Wiremock Server to act as fake JWKS server - wireMockRule.start(); - JWKSet keySet = jwtHelper.getKeySet(); - String jwksJson = String.valueOf(keySet.toPublicJWKSet().toJSONObject()); - - // When Feast Core looks up a Json Web Token Key Set, we provide our self-signed public key - wireMockRule.stubFor( - WireMock.get(WireMock.urlPathEqualTo("/.well-known/jwks.json")) - .willReturn( - WireMock.aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(jwksJson))); - - String jwkEndpointURI = - String.format("http://localhost:%s/.well-known/jwks.json", wireMockRule.port()); - - // Get Keto Authorization Server (Adaptor) url - String ketoAdaptorHost = environment.getServiceHost("adaptor_1", KETO_ADAPTOR_PORT); - Integer ketoAdaptorPort = environment.getServicePort("adaptor_1", KETO_ADAPTOR_PORT); - String ketoAdaptorUrl = String.format("http://%s:%s", ketoAdaptorHost, ketoAdaptorPort); - - // Initialize dynamic properties - registry.add("feast.security.authentication.options.subjectClaim", () -> subjectClaim); - registry.add("feast.security.authentication.options.jwkEndpointURI", () -> jwkEndpointURI); - registry.add("feast.security.authorization.options.authorizationUrl", () -> ketoAdaptorUrl); - } - - @BeforeAll - public static void globalSetUp(@Value("${grpc.server.port}") int port) { - feast_core_port = port; - // Create insecure Feast Core gRPC client - Channel insecureChannel = - ManagedChannelBuilder.forAddress("localhost", feast_core_port).usePlaintext().build(); - CoreServiceGrpc.CoreServiceBlockingStub insecureCoreService = - CoreServiceGrpc.newBlockingStub(insecureChannel); - insecureApiClient = new SimpleCoreClient(insecureCoreService); - } - - @BeforeEach - public void setUp() { - SimpleCoreClient secureApiClient = getSecureApiClient(subjectIsAdmin); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity1", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - secureApiClient.simpleApplyEntity(project, expectedEntitySpec); - } - - @AfterAll - static void tearDown() { - environment.stop(); - wireMockRule.stop(); - } - - @Test - public void shouldGetVersionFromFeastCoreAlways() { - SimpleCoreClient secureApiClient = - getSecureApiClient("fakeUserThatIsAuthenticated@example.com"); - - String feastCoreVersionSecure = secureApiClient.getFeastCoreVersion(); - String feastCoreVersionInsecure = insecureApiClient.getFeastCoreVersion(); - - assertEquals(feastCoreVersionSecure, feastCoreVersionInsecure); - assertEquals(feastProperties.getVersion(), feastCoreVersionSecure); - } - - @Test - public void shouldNotAllowUnauthenticatedEntityListing() { - Exception exception = - assertThrows( - StatusRuntimeException.class, - () -> { - insecureApiClient.simpleListEntities("8"); - }); - - String expectedMessage = "UNAUTHENTICATED: Authentication failed"; - String actualMessage = exception.getMessage(); - assertEquals(actualMessage, expectedMessage); - } - - @Test - public void shouldAllowAuthenticatedEntityListing() { - SimpleCoreClient secureApiClient = - getSecureApiClient("AuthenticatedUserWithoutAuthorization@example.com"); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity1", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - List listEntitiesResponse = secureApiClient.simpleListEntities("myproject"); - EntityProto.Entity actualEntity = listEntitiesResponse.get(0); - - assert listEntitiesResponse.size() == 1; - assertEquals(actualEntity.getSpec().getName(), expectedEntitySpec.getName()); - } - - @Test - void cantApplyEntityIfNotProjectMember() throws InvalidProtocolBufferException { - String userName = "random_user@example.com"; - SimpleCoreClient secureApiClient = getSecureApiClient(userName); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity1", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - - StatusRuntimeException exception = - assertThrows( - StatusRuntimeException.class, - () -> secureApiClient.simpleApplyEntity(project, expectedEntitySpec)); - - String expectedMessage = - String.format( - "PERMISSION_DENIED: Access denied to project %s for subject %s", project, userName); - String actualMessage = exception.getMessage(); - assertEquals(actualMessage, expectedMessage); - } - - @Test - void canApplyEntityIfProjectMember() { - SimpleCoreClient secureApiClient = getSecureApiClient(subjectInProject); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity_6", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - - secureApiClient.simpleApplyEntity(project, expectedEntitySpec); - - EntityProto.Entity actualEntity = secureApiClient.simpleGetEntity(project, "entity_6"); - - assertEquals(expectedEntitySpec.getName(), actualEntity.getSpec().getName()); - assertEquals(expectedEntitySpec.getValueType(), actualEntity.getSpec().getValueType()); - } - - @Test - void canApplyEntityIfAdmin() { - SimpleCoreClient secureApiClient = getSecureApiClient(subjectIsAdmin); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity_7", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - - secureApiClient.simpleApplyEntity(project, expectedEntitySpec); - - EntityProto.Entity actualEntity = secureApiClient.simpleGetEntity(project, "entity_7"); - - assertEquals(expectedEntitySpec.getName(), actualEntity.getSpec().getName()); - assertEquals(expectedEntitySpec.getValueType(), actualEntity.getSpec().getValueType()); - } - - @TestConfiguration - public static class TestConfig extends BaseTestConfig {} - - private static void seedKeto(String url) throws ApiException { - ApiClient ketoClient = Configuration.getDefaultApiClient(); - ketoClient.setBasePath(url); - EnginesApi enginesApi = new EnginesApi(ketoClient); - - // Add policies - OryAccessControlPolicy adminPolicy = getAdminPolicy(); - enginesApi.upsertOryAccessControlPolicy(DEFAULT_FLAVOR, adminPolicy); - - OryAccessControlPolicy projectPolicy = getMyProjectMemberPolicy(); - enginesApi.upsertOryAccessControlPolicy(DEFAULT_FLAVOR, projectPolicy); - - // Add policy roles - OryAccessControlPolicyRole adminPolicyRole = getAdminPolicyRole(); - enginesApi.upsertOryAccessControlPolicyRole(DEFAULT_FLAVOR, adminPolicyRole); - - OryAccessControlPolicyRole myProjectMemberPolicyRole = getMyProjectMemberPolicyRole(); - enginesApi.upsertOryAccessControlPolicyRole(DEFAULT_FLAVOR, myProjectMemberPolicyRole); - } - - private static OryAccessControlPolicyRole getMyProjectMemberPolicyRole() { - OryAccessControlPolicyRole role = new OryAccessControlPolicyRole(); - role.setId(String.format("roles:%s-project-members", project)); - role.setMembers(Collections.singletonList("users:" + subjectInProject)); - return role; - } - - private static OryAccessControlPolicyRole getAdminPolicyRole() { - OryAccessControlPolicyRole role = new OryAccessControlPolicyRole(); - role.setId("roles:admin"); - role.setMembers(Collections.singletonList("users:" + subjectIsAdmin)); - return role; - } - - private static OryAccessControlPolicy getAdminPolicy() { - OryAccessControlPolicy policy = new OryAccessControlPolicy(); - policy.setId("policies:admin"); - policy.subjects(Collections.singletonList("roles:admin")); - policy.resources(Collections.singletonList("resources:**")); - policy.actions(Collections.singletonList("actions:**")); - policy.effect("allow"); - policy.conditions(null); - return policy; - } - - private static OryAccessControlPolicy getMyProjectMemberPolicy() { - OryAccessControlPolicy policy = new OryAccessControlPolicy(); - policy.setId(String.format("policies:%s-project-members-policy", project)); - policy.subjects(Collections.singletonList(String.format("roles:%s-project-members", project))); - policy.resources( - Arrays.asList( - String.format("resources:projects:%s", project), - String.format("resources:projects:%s:**", project))); - policy.actions(Collections.singletonList("actions:**")); - policy.effect("allow"); - policy.conditions(null); - return policy; - } - - // Create secure Feast Core gRPC client for a specific user - private static SimpleCoreClient getSecureApiClient(String subjectEmail) { - CallCredentials callCredentials = null; - try { - callCredentials = jwtHelper.getCallCredentials(subjectEmail); - } catch (JOSEException e) { - throw new RuntimeException( - String.format("Could not build call credentials: %s", e.getMessage())); - } - Channel secureChannel = - ManagedChannelBuilder.forAddress("localhost", feast_core_port).usePlaintext().build(); - - CoreServiceGrpc.CoreServiceBlockingStub secureCoreService = - CoreServiceGrpc.newBlockingStub(secureChannel).withCallCredentials(callCredentials); - - return new SimpleCoreClient(secureCoreService); - } -} diff --git a/core/src/test/java/feast/core/auth/CoreServiceKetoAuthorizationIT.java b/core/src/test/java/feast/core/auth/CoreServiceKetoAuthorizationIT.java deleted file mode 100644 index 0a09cce..0000000 --- a/core/src/test/java/feast/core/auth/CoreServiceKetoAuthorizationIT.java +++ /dev/null @@ -1,352 +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.core.auth; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.testcontainers.containers.wait.strategy.Wait.forHttp; - -import avro.shaded.com.google.common.collect.ImmutableMap; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.github.tomakehurst.wiremock.junit.WireMockClassRule; -import com.google.protobuf.InvalidProtocolBufferException; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.jwk.JWKSet; -import feast.common.it.BaseIT; -import feast.common.it.DataGenerator; -import feast.common.it.SimpleCoreClient; -import feast.core.auth.infra.JwtHelper; -import feast.core.config.FeastProperties; -import feast.proto.core.CoreServiceGrpc; -import feast.proto.core.EntityProto; -import feast.proto.types.ValueProto; -import io.grpc.CallCredentials; -import io.grpc.Channel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.StatusRuntimeException; -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.springframework.util.SocketUtils; -import org.testcontainers.containers.DockerComposeContainer; -import sh.ory.keto.ApiClient; -import sh.ory.keto.ApiException; -import sh.ory.keto.Configuration; -import sh.ory.keto.api.EnginesApi; -import sh.ory.keto.model.OryAccessControlPolicy; -import sh.ory.keto.model.OryAccessControlPolicyRole; - -@SpringBootTest( - properties = { - "feast.security.authentication.enabled=true", - "feast.security.authorization.enabled=true", - "feast.security.authorization.provider=keto", - "feast.security.authorization.options.action=actions:any", - "feast.security.authorization.options.subjectPrefix=users:", - "feast.security.authorization.options.resourcePrefix=resources:projects:", - }) -public class CoreServiceKetoAuthorizationIT extends BaseIT { - - @Autowired FeastProperties feastProperties; - - private static final String DEFAULT_FLAVOR = "glob"; - private static int KETO_PORT = 4466; - private static int feast_core_port; - private static int JWKS_PORT = SocketUtils.findAvailableTcpPort(); - - private static JwtHelper jwtHelper = new JwtHelper(); - - static String project = "myproject"; - static String subjectInProject = "good_member@example.com"; - static String subjectIsAdmin = "bossman@example.com"; - static String subjectClaim = "sub"; - - static SimpleCoreClient insecureApiClient; - - @ClassRule public static WireMockClassRule wireMockRule = new WireMockClassRule(JWKS_PORT); - - @Rule public WireMockClassRule instanceRule = wireMockRule; - - @ClassRule - public static DockerComposeContainer environment = - new DockerComposeContainer(new File("src/test/resources/keto/docker-compose.yml")) - .withExposedService("keto_1", KETO_PORT, forHttp("/health/ready").forStatusCode(200)); - - @DynamicPropertySource - static void initialize(DynamicPropertyRegistry registry) { - - // Start Keto and with Docker Compose - environment.start(); - - // Seed Keto with data - String ketoExternalHost = environment.getServiceHost("keto_1", KETO_PORT); - Integer ketoExternalPort = environment.getServicePort("keto_1", KETO_PORT); - String ketoExternalUrl = String.format("http://%s:%s", ketoExternalHost, ketoExternalPort); - try { - seedKeto(ketoExternalUrl); - } catch (ApiException e) { - throw new RuntimeException(String.format("Could not seed Keto store %s", ketoExternalUrl)); - } - - // Start Wiremock Server to act as fake JWKS server - wireMockRule.start(); - JWKSet keySet = jwtHelper.getKeySet(); - String jwksJson = String.valueOf(keySet.toPublicJWKSet().toJSONObject()); - - // When Feast Core looks up a Json Web Token Key Set, we provide our self-signed public key - wireMockRule.stubFor( - WireMock.get(WireMock.urlPathEqualTo("/.well-known/jwks.json")) - .willReturn( - WireMock.aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(jwksJson))); - - String jwkEndpointURI = - String.format("http://localhost:%s/.well-known/jwks.json", wireMockRule.port()); - - // Initialize dynamic properties - registry.add("feast.security.authentication.options.subjectClaim", () -> subjectClaim); - registry.add("feast.security.authentication.options.jwkEndpointURI", () -> jwkEndpointURI); - registry.add("feast.security.authorization.options.authorizationUrl", () -> ketoExternalUrl); - registry.add("feast.security.authorization.options.flavor", () -> DEFAULT_FLAVOR); - } - - @BeforeAll - public static void globalSetUp(@Value("${grpc.server.port}") int port) { - feast_core_port = port; - // Create insecure Feast Core gRPC client - Channel insecureChannel = - ManagedChannelBuilder.forAddress("localhost", feast_core_port).usePlaintext().build(); - CoreServiceGrpc.CoreServiceBlockingStub insecureCoreService = - CoreServiceGrpc.newBlockingStub(insecureChannel); - insecureApiClient = new SimpleCoreClient(insecureCoreService); - } - - @BeforeEach - public void setUp() { - SimpleCoreClient secureApiClient = getSecureApiClient(subjectIsAdmin); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity1", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - secureApiClient.simpleApplyEntity(project, expectedEntitySpec); - } - - @AfterAll - static void tearDown() { - environment.stop(); - wireMockRule.stop(); - } - - @Test - public void shouldGetVersionFromFeastCoreAlways() { - SimpleCoreClient secureApiClient = - getSecureApiClient("fakeUserThatIsAuthenticated@example.com"); - - String feastCoreVersionSecure = secureApiClient.getFeastCoreVersion(); - String feastCoreVersionInsecure = insecureApiClient.getFeastCoreVersion(); - - assertEquals(feastCoreVersionSecure, feastCoreVersionInsecure); - assertEquals(feastProperties.getVersion(), feastCoreVersionSecure); - } - - @Test - public void shouldNotAllowUnauthenticatedEntityListing() { - Exception exception = - assertThrows( - StatusRuntimeException.class, - () -> { - insecureApiClient.simpleListEntities("8"); - }); - - String expectedMessage = "UNAUTHENTICATED: Authentication failed"; - String actualMessage = exception.getMessage(); - assertEquals(actualMessage, expectedMessage); - } - - @Test - public void shouldAllowAuthenticatedEntityListing() { - SimpleCoreClient secureApiClient = - getSecureApiClient("AuthenticatedUserWithoutAuthorization@example.com"); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity1", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - List listEntitiesResponse = secureApiClient.simpleListEntities("myproject"); - EntityProto.Entity actualEntity = listEntitiesResponse.get(0); - - assert listEntitiesResponse.size() == 1; - assertEquals(actualEntity.getSpec().getName(), expectedEntitySpec.getName()); - } - - @Test - void cantApplyEntityIfNotProjectMember() throws InvalidProtocolBufferException { - String userName = "random_user@example.com"; - SimpleCoreClient secureApiClient = getSecureApiClient(userName); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity1", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - - StatusRuntimeException exception = - assertThrows( - StatusRuntimeException.class, - () -> secureApiClient.simpleApplyEntity(project, expectedEntitySpec)); - - String expectedMessage = - String.format( - "PERMISSION_DENIED: Access denied to project %s for subject %s", project, userName); - String actualMessage = exception.getMessage(); - assertEquals(actualMessage, expectedMessage); - } - - @Test - void canApplyEntityIfProjectMember() { - SimpleCoreClient secureApiClient = getSecureApiClient(subjectInProject); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity_6", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - - secureApiClient.simpleApplyEntity(project, expectedEntitySpec); - - EntityProto.Entity actualEntity = secureApiClient.simpleGetEntity(project, "entity_6"); - - assertEquals(expectedEntitySpec.getName(), actualEntity.getSpec().getName()); - assertEquals(expectedEntitySpec.getValueType(), actualEntity.getSpec().getValueType()); - } - - @Test - void canApplyEntityIfAdmin() { - SimpleCoreClient secureApiClient = getSecureApiClient(subjectIsAdmin); - EntityProto.EntitySpecV2 expectedEntitySpec = - DataGenerator.createEntitySpecV2( - "entity_7", - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - - secureApiClient.simpleApplyEntity(project, expectedEntitySpec); - - EntityProto.Entity actualEntity = secureApiClient.simpleGetEntity(project, "entity_7"); - - assertEquals(expectedEntitySpec.getName(), actualEntity.getSpec().getName()); - assertEquals(expectedEntitySpec.getValueType(), actualEntity.getSpec().getValueType()); - } - - @TestConfiguration - public static class TestConfig extends BaseTestConfig {} - - private static void seedKeto(String url) throws ApiException { - ApiClient ketoClient = Configuration.getDefaultApiClient(); - ketoClient.setBasePath(url); - EnginesApi enginesApi = new EnginesApi(ketoClient); - - // Add policies - OryAccessControlPolicy adminPolicy = getAdminPolicy(); - enginesApi.upsertOryAccessControlPolicy(DEFAULT_FLAVOR, adminPolicy); - - OryAccessControlPolicy projectPolicy = getMyProjectMemberPolicy(); - enginesApi.upsertOryAccessControlPolicy(DEFAULT_FLAVOR, projectPolicy); - - // Add policy roles - OryAccessControlPolicyRole adminPolicyRole = getAdminPolicyRole(); - enginesApi.upsertOryAccessControlPolicyRole(DEFAULT_FLAVOR, adminPolicyRole); - - OryAccessControlPolicyRole myProjectMemberPolicyRole = getMyProjectMemberPolicyRole(); - enginesApi.upsertOryAccessControlPolicyRole(DEFAULT_FLAVOR, myProjectMemberPolicyRole); - } - - private static OryAccessControlPolicyRole getMyProjectMemberPolicyRole() { - OryAccessControlPolicyRole role = new OryAccessControlPolicyRole(); - role.setId(String.format("roles:%s-project-members", project)); - role.setMembers(Collections.singletonList("users:" + subjectInProject)); - return role; - } - - private static OryAccessControlPolicyRole getAdminPolicyRole() { - OryAccessControlPolicyRole role = new OryAccessControlPolicyRole(); - role.setId("roles:admin"); - role.setMembers(Collections.singletonList("users:" + subjectIsAdmin)); - return role; - } - - private static OryAccessControlPolicy getAdminPolicy() { - OryAccessControlPolicy policy = new OryAccessControlPolicy(); - policy.setId("policies:admin"); - policy.subjects(Collections.singletonList("roles:admin")); - policy.resources(Collections.singletonList("resources:**")); - policy.actions(Collections.singletonList("actions:**")); - policy.effect("allow"); - policy.conditions(null); - return policy; - } - - private static OryAccessControlPolicy getMyProjectMemberPolicy() { - OryAccessControlPolicy policy = new OryAccessControlPolicy(); - policy.setId(String.format("policies:%s-project-members-policy", project)); - policy.subjects(Collections.singletonList(String.format("roles:%s-project-members", project))); - policy.resources( - Arrays.asList( - String.format("resources:projects:%s", project), - String.format("resources:projects:%s:**", project))); - policy.actions(Collections.singletonList("actions:**")); - policy.effect("allow"); - policy.conditions(null); - return policy; - } - - // Create secure Feast Core gRPC client for a specific user - private static SimpleCoreClient getSecureApiClient(String subjectEmail) { - CallCredentials callCredentials = null; - try { - callCredentials = jwtHelper.getCallCredentials(subjectEmail); - } catch (JOSEException e) { - throw new RuntimeException( - String.format("Could not build call credentials: %s", e.getMessage())); - } - Channel secureChannel = - ManagedChannelBuilder.forAddress("localhost", feast_core_port).usePlaintext().build(); - - CoreServiceGrpc.CoreServiceBlockingStub secureCoreService = - CoreServiceGrpc.newBlockingStub(secureChannel).withCallCredentials(callCredentials); - - return new SimpleCoreClient(secureCoreService); - } -} diff --git a/core/src/test/java/feast/core/logging/CoreLoggingIT.java b/core/src/test/java/feast/core/logging/CoreLoggingIT.java deleted file mode 100644 index 0f137b4..0000000 --- a/core/src/test/java/feast/core/logging/CoreLoggingIT.java +++ /dev/null @@ -1,229 +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.core.logging; - -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import com.google.common.collect.Streams; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; -import feast.common.it.BaseIT; -import feast.common.it.DataGenerator; -import feast.common.logging.entry.AuditLogEntryKind; -import feast.proto.core.CoreServiceGrpc; -import feast.proto.core.CoreServiceGrpc.CoreServiceBlockingStub; -import feast.proto.core.CoreServiceGrpc.CoreServiceFutureStub; -import feast.proto.core.CoreServiceProto.GetFeastCoreVersionRequest; -import feast.proto.core.CoreServiceProto.ListFeatureTablesRequest; -import feast.proto.core.CoreServiceProto.ListStoresRequest; -import feast.proto.core.CoreServiceProto.ListStoresResponse; -import feast.proto.core.CoreServiceProto.UpdateStoreRequest; -import feast.proto.core.CoreServiceProto.UpdateStoreResponse; -import io.grpc.Channel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Status.Code; -import io.grpc.StatusRuntimeException; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.LoggerContext; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest( - properties = { - "feast.logging.audit.enabled=true", - "feast.logging.audit.messageLogging.enabled=true", - "feast.logging.audit.messageLogging.destination=console" - }) -public class CoreLoggingIT extends BaseIT { - private static TestLogAppender testAuditLogAppender; - private static CoreServiceBlockingStub coreService; - private static CoreServiceFutureStub asyncCoreService; - - @BeforeAll - public static void globalSetUp(@Value("${grpc.server.port}") int coreGrpcPort) - throws InterruptedException, ExecutionException { - LoggerContext logContext = (LoggerContext) LogManager.getContext(false); - // NOTE: As log appender state is shared across tests use a different method - // for each test and filter by method name to ensure that you only get logs - // for a specific test. - testAuditLogAppender = logContext.getConfiguration().getAppender("TestAuditLogAppender"); - - // Connect to core service. - Channel channel = - ManagedChannelBuilder.forAddress("localhost", coreGrpcPort).usePlaintext().build(); - coreService = CoreServiceGrpc.newBlockingStub(channel); - asyncCoreService = CoreServiceGrpc.newFutureStub(channel); - - // Preflight a request to core service stubs to verify connection - coreService.getFeastCoreVersion(GetFeastCoreVersionRequest.getDefaultInstance()); - asyncCoreService.getFeastCoreVersion(GetFeastCoreVersionRequest.getDefaultInstance()).get(); - } - - /** Check that messsage audit log are produced on service call */ - @Test - public void shouldProduceMessageAuditLogsOnCall() - throws InterruptedException, InvalidProtocolBufferException { - // Generate artifical load on feast core. - UpdateStoreRequest request = - UpdateStoreRequest.newBuilder().setStore(DataGenerator.getDefaultStore()).build(); - UpdateStoreResponse response = coreService.updateStore(request); - - // Wait required to ensure audit logs are flushed into test audit log appender - Thread.sleep(1000); - // Check message audit logs are produced for each audit log. - JsonFormat.Parser protoJSONParser = JsonFormat.parser(); - // Pull message audit logs logs from test log appender - List logJsonObjects = - parseMessageJsonLogObjects(testAuditLogAppender.getLogs(), "UpdateStore"); - assertEquals(1, logJsonObjects.size()); - JsonObject logObj = logJsonObjects.get(0); - - // Extract & Check that request/response are returned correctly - String requestJson = logObj.getAsJsonObject("request").toString(); - UpdateStoreRequest.Builder gotRequest = UpdateStoreRequest.newBuilder(); - protoJSONParser.merge(requestJson, gotRequest); - - String responseJson = logObj.getAsJsonObject("response").toString(); - UpdateStoreResponse.Builder gotResponse = UpdateStoreResponse.newBuilder(); - protoJSONParser.merge(responseJson, gotResponse); - - assertThat(gotRequest.build(), equalTo(request)); - assertThat(gotResponse.build(), equalTo(response)); - } - - /** Check that message audit logs are produced when server encounters an error */ - @Test - public void shouldProduceMessageAuditLogsOnError() throws InterruptedException { - // Send a bad request which should cause Core to error - ListFeatureTablesRequest request = - ListFeatureTablesRequest.newBuilder() - .setFilter(ListFeatureTablesRequest.Filter.newBuilder().setProject("*").build()) - .build(); - - boolean hasExpectedException = false; - Code statusCode = null; - try { - coreService.listFeatureTables(request); - } catch (StatusRuntimeException e) { - hasExpectedException = true; - statusCode = e.getStatus().getCode(); - } - assertTrue(hasExpectedException); - - // Wait required to ensure audit logs are flushed into test audit log appender - Thread.sleep(1000); - // Pull message audit logs logs from test log appender - List logJsonObjects = - parseMessageJsonLogObjects(testAuditLogAppender.getLogs(), "ListFeatureTables"); - - assertEquals(1, logJsonObjects.size()); - JsonObject logJsonObject = logJsonObjects.get(0); - // Check correct status code is tracked on error. - assertEquals(logJsonObject.get("statusCode").getAsString(), statusCode.toString()); - } - - /** Check that expected message audit logs are produced when under load. */ - @Test - public void shouldProduceExpectedAuditLogsUnderLoad() - throws InterruptedException, ExecutionException { - // Generate artifical requests on core to simulate load. - int LOAD_SIZE = 40; // Total number of requests to send. - int BURST_SIZE = 5; // Number of requests to send at once. - - ListStoresRequest request = ListStoresRequest.getDefaultInstance(); - List responses = new LinkedList<>(); - for (int i = 0; i < LOAD_SIZE; i += 5) { - List> futures = new LinkedList<>(); - for (int j = 0; j < BURST_SIZE; j++) { - futures.add(asyncCoreService.listStores(request)); - } - - responses.addAll(Futures.allAsList(futures).get()); - } - // Wait required to ensure audit logs are flushed into test audit log appender - Thread.sleep(1000); - - // Pull message audit logs from test log appender - List logJsonObjects = - parseMessageJsonLogObjects(testAuditLogAppender.getLogs(), "ListStores"); - assertEquals(responses.size(), logJsonObjects.size()); - - // Extract & Check that request/response are returned correctly - JsonFormat.Parser protoJSONParser = JsonFormat.parser(); - Streams.zip( - responses.stream(), - logJsonObjects.stream(), - (response, logObj) -> Pair.of(response, logObj)) - .forEach( - responseLogJsonPair -> { - ListStoresResponse response = responseLogJsonPair.getLeft(); - JsonObject logObj = responseLogJsonPair.getRight(); - - ListStoresRequest.Builder gotRequest = null; - ListStoresResponse.Builder gotResponse = null; - try { - String requestJson = logObj.getAsJsonObject("request").toString(); - gotRequest = ListStoresRequest.newBuilder(); - protoJSONParser.merge(requestJson, gotRequest); - - String responseJson = logObj.getAsJsonObject("response").toString(); - gotResponse = ListStoresResponse.newBuilder(); - protoJSONParser.merge(responseJson, gotResponse); - } catch (InvalidProtocolBufferException e) { - throw new RuntimeException(e); - } - - assertThat(gotRequest.build(), equalTo(request)); - assertThat(gotResponse.build(), equalTo(response)); - }); - } - - /** - * Filter and Parse out Message Audit Logs from the given logsStrings for the given method name - */ - private List parseMessageJsonLogObjects(List logsStrings, String methodName) { - JsonParser jsonParser = new JsonParser(); - // copy to prevent concurrent modification. - return logsStrings.stream() - .map(logJSON -> jsonParser.parse(logJSON).getAsJsonObject()) - // Filter to only include message audit logs - .filter( - logObj -> - logObj - .getAsJsonPrimitive("kind") - .getAsString() - .equals(AuditLogEntryKind.MESSAGE.toString()) - // filter by method name to ensure logs from other tests do not interfere with - // test - && logObj.get("method").getAsString().equals(methodName)) - .collect(Collectors.toList()); - } -} diff --git a/core/src/test/java/feast/core/service/ProjectServiceTest.java b/core/src/test/java/feast/core/service/ProjectServiceTest.java index e09580f..afff1e5 100644 --- a/core/src/test/java/feast/core/service/ProjectServiceTest.java +++ b/core/src/test/java/feast/core/service/ProjectServiceTest.java @@ -16,10 +16,8 @@ */ package feast.core.service; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import feast.core.dao.ProjectRepository; @@ -29,16 +27,12 @@ import java.util.Optional; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.mockito.Mock; public class ProjectServiceTest { @Mock private ProjectRepository projectRepository; - @Rule public final ExpectedException expectedException = ExpectedException.none(); - private ProjectService projectService; @Before @@ -75,8 +69,9 @@ public void shouldArchiveProjectIfItExists() { @Test public void shouldNotArchiveDefaultProject() { - expectedException.expect(IllegalArgumentException.class); - this.projectService.archiveProject(Project.DEFAULT_NAME); + assertThrows( + IllegalArgumentException.class, + () -> this.projectService.archiveProject(Project.DEFAULT_NAME)); } @Test(expected = IllegalArgumentException.class) diff --git a/core/src/test/java/feast/core/util/TypeConversionTest.java b/core/src/test/java/feast/core/util/TypeConversionTest.java index c44bf50..ba965a7 100644 --- a/core/src/test/java/feast/core/util/TypeConversionTest.java +++ b/core/src/test/java/feast/core/util/TypeConversionTest.java @@ -17,8 +17,8 @@ package feast.core.util; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.*; import com.google.protobuf.Timestamp; import java.util.*; diff --git a/core/src/test/java/feast/core/validators/MatchersTest.java b/core/src/test/java/feast/core/validators/MatchersTest.java index 1733212..559330a 100644 --- a/core/src/test/java/feast/core/validators/MatchersTest.java +++ b/core/src/test/java/feast/core/validators/MatchersTest.java @@ -19,14 +19,12 @@ import static feast.core.validators.Matchers.checkLowerSnakeCase; import static feast.core.validators.Matchers.checkUpperSnakeCase; import static feast.core.validators.Matchers.checkValidClassPath; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.base.Strings; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; public class MatchersTest { - @Rule public final ExpectedException exception = ExpectedException.none(); @Test public void checkUpperSnakeCaseShouldPassForLegitUpperSnakeCase() { @@ -42,15 +40,15 @@ public void checkUpperSnakeCaseShouldPassForLegitUpperSnakeCaseWithNumbers() { @Test public void checkUpperSnakeCaseShouldThrowIllegalArgumentExceptionWithFieldForInvalidString() { - exception.expect(IllegalArgumentException.class); - exception.expectMessage( + String in = "redis"; + assertThrows( + IllegalArgumentException.class, + () -> checkUpperSnakeCase(in, "featuretable"), Strings.lenientFormat( "invalid value for %s resource, %s: %s", "featuretable", "redis", "argument must be in upper snake case, and cannot include any special characters.")); - String in = "redis"; - checkUpperSnakeCase(in, "featuretable"); } @Test @@ -61,15 +59,15 @@ public void checkLowerSnakeCaseShouldPassForLegitLowerSnakeCase() { @Test public void checkLowerSnakeCaseShouldThrowIllegalArgumentExceptionWithFieldForInvalidString() { - exception.expect(IllegalArgumentException.class); - exception.expectMessage( + String in = "Invalid_feature name"; + assertThrows( + IllegalArgumentException.class, + () -> checkLowerSnakeCase(in, "feature"), Strings.lenientFormat( "invalid value for %s resource, %s: %s", "feature", "Invalid_feature name", "argument must be in lower snake case, and cannot include any special characters.")); - String in = "Invalid_feature name"; - checkLowerSnakeCase(in, "feature"); } @Test @@ -80,13 +78,11 @@ public void checkValidClassPathSuccess() { @Test public void checkValidClassPathEmpty() { - exception.expect(IllegalArgumentException.class); - checkValidClassPath("", "FeatureTable"); + assertThrows(IllegalArgumentException.class, () -> checkValidClassPath("", "FeatureTable")); } @Test public void checkValidClassPathDigits() { - exception.expect(IllegalArgumentException.class); - checkValidClassPath("123", "FeatureTable"); + assertThrows(IllegalArgumentException.class, () -> checkValidClassPath("123", "FeatureTable")); } } diff --git a/deps/feast b/deps/feast index 8010d2f..2541c91 160000 --- a/deps/feast +++ b/deps/feast @@ -1 +1 @@ -Subproject commit 8010d2f35d3f876db54a31b8012b13009cd5eba2 +Subproject commit 2541c91c9238ef09ffd74e45e74116a31d7f2daa diff --git a/pom.xml b/pom.xml index 4171e57..50d9b95 100644 --- a/pom.xml +++ b/pom.xml @@ -269,7 +269,38 @@ ${grpc.version} test - + + + + org.apache.arrow + arrow-java-root + 5.0.0 + pom + + + + + org.apache.arrow + arrow-vector + 5.0.0 + + + + + org.apache.arrow + arrow-memory + 5.0.0 + pom + + + + + org.apache.arrow + arrow-memory-netty + 5.0.0 + runtime + + io.swagger @@ -543,6 +574,9 @@ + + feast.proto.*:io.grpc.*:org.tensorflow.* + com.diffplug.spotless @@ -691,7 +725,7 @@ - ${groupId}:${artifactId} + ${project.groupId}:${project.artifactId} ${project.build.outputDirectory} diff --git a/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java b/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java index bd3b34b..94c779c 100644 --- a/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java +++ b/sdk/java/src/main/java/com/gojek/feast/SecurityConfig.java @@ -26,15 +26,23 @@ public abstract class SecurityConfig { /** * Enables authentication If specified, the call credentials used to provide credentials to * authenticate with Feast. + * + * @return credentials */ public abstract Optional getCredentials(); - /** Whether to use TLS transport security is use when connecting to Feast. */ + /** + * Whether to use TLS transport security is use when connecting to Feast. + * + * @return true if enabled + */ public abstract boolean isTLSEnabled(); /** * If specified and TLS is enabled, provides path to TLS certificate use the verify Service * identity. + * + * @return certificate path */ public abstract Optional getCertificatePath(); diff --git a/serving/README.md b/serving/README.md index ab530bb..cce8c7d 100644 --- a/serving/README.md +++ b/serving/README.md @@ -88,3 +88,14 @@ with open("/tmp/000000000000.avro", "rb") as f: print(df.head(5)) EOF ``` +#### Working with Feast 0.10+ +Feast serving supports reading feature values materialized into Redis by feast 0.10+. To configure this, feast-serving +needs to be able to read the registry file for the project. +The location of the registry file can be specified in the `application.yml` like so: +```yaml +feast: + registry: "src/test/resources/docker-compose/feast10/registry.db" +``` + +This changes the behaviour of feast-serving to look up feature view definitions and specifications from the registry file instead +of the core service. \ No newline at end of file diff --git a/serving/pom.xml b/serving/pom.xml index dfcc6e5..981c23c 100644 --- a/serving/pom.xml +++ b/serving/pom.xml @@ -269,6 +269,37 @@ 1.10.2 + + + org.apache.arrow + arrow-java-root + 5.0.0 + pom + + + + + org.apache.arrow + arrow-vector + 5.0.0 + + + + + org.apache.arrow + arrow-memory + 5.0.0 + pom + + + + + org.apache.arrow + arrow-memory-netty + 5.0.0 + runtime + + com.fasterxml.jackson.dataformat diff --git a/serving/src/main/java/feast/serving/config/ContextClosedHandler.java b/serving/src/main/java/feast/serving/config/ContextClosedHandler.java index 2bc9743..cdf791c 100644 --- a/serving/src/main/java/feast/serving/config/ContextClosedHandler.java +++ b/serving/src/main/java/feast/serving/config/ContextClosedHandler.java @@ -18,11 +18,13 @@ import java.util.concurrent.ScheduledExecutorService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.stereotype.Component; @Component +@ConditionalOnBean(CoreCondition.class) public class ContextClosedHandler implements ApplicationListener { @Autowired ScheduledExecutorService executor; diff --git a/serving/src/main/java/feast/serving/config/CoreCondition.java b/serving/src/main/java/feast/serving/config/CoreCondition.java new file mode 100644 index 0000000..10dabfa --- /dev/null +++ b/serving/src/main/java/feast/serving/config/CoreCondition.java @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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 org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * A {@link Condition} to signal that the ServingService should get feature definitions and metadata + * from Core service. + */ +public class CoreCondition implements Condition { + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + final Environment env = context.getEnvironment(); + return env.getProperty("feast.registry") == null; + } +} diff --git a/serving/src/main/java/feast/serving/config/FeastProperties.java b/serving/src/main/java/feast/serving/config/FeastProperties.java index 1b62d84..82db07c 100644 --- a/serving/src/main/java/feast/serving/config/FeastProperties.java +++ b/serving/src/main/java/feast/serving/config/FeastProperties.java @@ -72,6 +72,26 @@ public FeastProperties() {} /* Feast Core port to connect to. */ @Positive private int coreGrpcPort; + private String registry; + + public String getRegistry() { + return registry; + } + + public void setRegistry(final String registry) { + this.registry = registry; + } + + private String transformationServiceEndpoint; + + public String getTransformationServiceEndpoint() { + return transformationServiceEndpoint; + } + + public void setTransformationServiceEndpoint(final String transformationServiceEndpoint) { + this.transformationServiceEndpoint = transformationServiceEndpoint; + } + private CoreAuthenticationProperties coreAuthentication; public CoreAuthenticationProperties getCoreAuthentication() { @@ -82,7 +102,6 @@ public void setCoreAuthentication(CoreAuthenticationProperties coreAuthenticatio this.coreAuthentication = coreAuthentication; } - /* Feast Core port to connect to. */ @Positive private int coreCacheRefreshInterval; private SecurityProperties security; @@ -303,14 +322,17 @@ public RedisClusterStoreConfig getRedisClusterConfig() { return new RedisClusterStoreConfig( this.config.get("connection_string"), ReadFrom.valueOf(this.config.get("read_from")), - Duration.parse(this.config.get("timeout"))); + Duration.parse(this.config.get("timeout")), + Boolean.valueOf(this.config.getOrDefault("ssl", "false")), + this.config.getOrDefault("password", "")); } public RedisStoreConfig getRedisConfig() { return new RedisStoreConfig( this.config.get("host"), Integer.valueOf(this.config.get("port")), - Boolean.valueOf(this.config.getOrDefault("ssl", "false"))); + Boolean.valueOf(this.config.getOrDefault("ssl", "false")), + this.config.getOrDefault("password", "")); } public BigTableStoreConfig getBigtableConfig() { @@ -372,7 +394,11 @@ public LoggingProperties getLogging() { return logging; } - /** Sets logging properties @@param logging the logging properties */ + /** + * Sets logging properties + * + * @param logging the logging properties + */ public void setLogging(LoggingProperties logging) { this.logging = logging; } diff --git a/serving/src/main/java/feast/serving/config/RegistryCondition.java b/serving/src/main/java/feast/serving/config/RegistryCondition.java new file mode 100644 index 0000000..621d124 --- /dev/null +++ b/serving/src/main/java/feast/serving/config/RegistryCondition.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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 org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * A {@link Condition} to signal that the ServingService should get feature definitions and metadata + * from the Registry object. This is needed for versions of the feature store written by feast + * 0.10+. + */ +public class RegistryCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + final Environment env = context.getEnvironment(); + return env.getProperty("feast.registry") != null; + } +} diff --git a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java index 9d50a6a..ce2aabf 100644 --- a/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java +++ b/serving/src/main/java/feast/serving/config/ServingServiceConfigV2.java @@ -20,9 +20,15 @@ import com.datastax.oss.driver.api.core.CqlSessionBuilder; import com.google.cloud.bigtable.data.v2.BigtableDataClient; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; +import com.google.protobuf.AbstractMessageLite; +import feast.serving.registry.LocalRegistryRepo; import feast.serving.service.OnlineServingServiceV2; +import feast.serving.service.OnlineTransformationService; import feast.serving.service.ServingServiceV2; import feast.serving.specs.CachedSpecService; +import feast.serving.specs.CoreFeatureSpecRetriever; +import feast.serving.specs.FeatureSpecRetriever; +import feast.serving.specs.RegistryFeatureSpecRetriever; import feast.storage.api.retriever.OnlineRetrieverV2; import feast.storage.connectors.bigtable.retriever.BigTableOnlineRetriever; import feast.storage.connectors.bigtable.retriever.BigTableStoreConfig; @@ -32,6 +38,7 @@ import io.opentracing.Tracer; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -39,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -64,27 +72,26 @@ public BigtableDataClient bigtableClient(FeastProperties feastProperties) throws } @Bean + @Conditional(CoreCondition.class) public ServingServiceV2 servingServiceV2( FeastProperties feastProperties, CachedSpecService specService, Tracer tracer) { - ServingServiceV2 servingService = null; - FeastProperties.Store store = feastProperties.getActiveStore(); + final ServingServiceV2 servingService; + final FeastProperties.Store store = feastProperties.getActiveStore(); + OnlineRetrieverV2 retrieverV2; switch (store.getType()) { case REDIS_CLUSTER: RedisClientAdapter redisClusterClient = RedisClusterClient.create(store.getRedisClusterConfig()); - OnlineRetrieverV2 redisClusterRetriever = new OnlineRetriever(redisClusterClient); - servingService = new OnlineServingServiceV2(redisClusterRetriever, specService, tracer); + retrieverV2 = new OnlineRetriever(redisClusterClient, (AbstractMessageLite::toByteArray)); break; case REDIS: RedisClientAdapter redisClient = RedisClient.create(store.getRedisConfig()); - OnlineRetrieverV2 redisRetriever = new OnlineRetriever(redisClient); - servingService = new OnlineServingServiceV2(redisRetriever, specService, tracer); + retrieverV2 = new OnlineRetriever(redisClient, (AbstractMessageLite::toByteArray)); break; case BIGTABLE: BigtableDataClient bigtableClient = context.getBean(BigtableDataClient.class); - OnlineRetrieverV2 bigtableRetriever = new BigTableOnlineRetriever(bigtableClient); - servingService = new OnlineServingServiceV2(bigtableRetriever, specService, tracer); + retrieverV2 = new BigTableOnlineRetriever(bigtableClient); break; case CASSANDRA: CassandraStoreConfig config = feastProperties.getActiveStore().getCassandraConfig(); @@ -109,11 +116,69 @@ public ServingServiceV2 servingServiceV2( .withLocalDatacenter(dataCenter) .withKeyspace(keySpace) .build(); - OnlineRetrieverV2 cassandraRetriever = new CassandraOnlineRetriever(session); - servingService = new OnlineServingServiceV2(cassandraRetriever, specService, tracer); + retrieverV2 = new CassandraOnlineRetriever(session); break; + default: + throw new RuntimeException( + String.format("Unable to identify online store type: %s", store.getType())); } + final FeatureSpecRetriever featureSpecRetriever; + log.info("Created CoreFeatureSpecRetriever"); + featureSpecRetriever = new CoreFeatureSpecRetriever(specService); + + final String transformationServiceEndpoint = feastProperties.getTransformationServiceEndpoint(); + final OnlineTransformationService onlineTransformationService = + new OnlineTransformationService(transformationServiceEndpoint, featureSpecRetriever); + + servingService = + new OnlineServingServiceV2( + retrieverV2, tracer, featureSpecRetriever, onlineTransformationService); + + return servingService; + } + + @Bean + @Conditional(RegistryCondition.class) + public ServingServiceV2 registryBasedServingServiceV2( + FeastProperties feastProperties, Tracer tracer) { + final ServingServiceV2 servingService; + final FeastProperties.Store store = feastProperties.getActiveStore(); + + OnlineRetrieverV2 retrieverV2; + // TODO: Support more store types, and potentially use a plugin model here. + switch (store.getType()) { + case REDIS_CLUSTER: + RedisClientAdapter redisClusterClient = + RedisClusterClient.create(store.getRedisClusterConfig()); + retrieverV2 = new OnlineRetriever(redisClusterClient, new EntityKeySerializerV2()); + break; + case REDIS: + RedisClientAdapter redisClient = RedisClient.create(store.getRedisConfig()); + log.info("Created EntityKeySerializerV2"); + retrieverV2 = new OnlineRetriever(redisClient, new EntityKeySerializerV2()); + break; + default: + throw new RuntimeException( + String.format( + "Unable to identify online store type: %s for Regsitry Backed Serving Service", + store.getType())); + } + + final FeatureSpecRetriever featureSpecRetriever; + log.info("Created RegistryFeatureSpecRetriever"); + log.info("Working Directory = " + System.getProperty("user.dir")); + final LocalRegistryRepo repo = new LocalRegistryRepo(Paths.get(feastProperties.getRegistry())); + featureSpecRetriever = new RegistryFeatureSpecRetriever(repo); + + final String transformationServiceEndpoint = feastProperties.getTransformationServiceEndpoint(); + final OnlineTransformationService onlineTransformationService = + new OnlineTransformationService(transformationServiceEndpoint, featureSpecRetriever); + + servingService = + new OnlineServingServiceV2( + retrieverV2, tracer, featureSpecRetriever, onlineTransformationService); + return servingService; } } diff --git a/serving/src/main/java/feast/serving/config/SpecServiceConfig.java b/serving/src/main/java/feast/serving/config/SpecServiceConfig.java index 369d543..29d3bf0 100644 --- a/serving/src/main/java/feast/serving/config/SpecServiceConfig.java +++ b/serving/src/main/java/feast/serving/config/SpecServiceConfig.java @@ -16,8 +16,6 @@ */ package feast.serving.config; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.google.protobuf.InvalidProtocolBufferException; import feast.serving.specs.CachedSpecService; import feast.serving.specs.CoreSpecService; import io.grpc.CallCredentials; @@ -28,15 +26,16 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @Configuration public class SpecServiceConfig { private static final Logger log = org.slf4j.LoggerFactory.getLogger(SpecServiceConfig.class); - private String feastCoreHost; - private int feastCorePort; - private int feastCachedSpecServiceRefreshInterval; + private final String feastCoreHost; + private final int feastCorePort; + private final int feastCachedSpecServiceRefreshInterval; @Autowired public SpecServiceConfig(FeastProperties feastProperties) { @@ -46,6 +45,7 @@ public SpecServiceConfig(FeastProperties feastProperties) { } @Bean + @Conditional(CoreCondition.class) public ScheduledExecutorService cachedSpecServiceScheduledExecutorService( CachedSpecService cachedSpecStorage) { ScheduledExecutorService scheduledExecutorService = @@ -60,8 +60,8 @@ public ScheduledExecutorService cachedSpecServiceScheduledExecutorService( } @Bean - public CachedSpecService specService(ObjectProvider callCredentials) - throws InvalidProtocolBufferException, JsonProcessingException { + @Conditional(CoreCondition.class) + public CachedSpecService specService(ObjectProvider callCredentials) { CoreSpecService coreService = new CoreSpecService(feastCoreHost, feastCorePort, callCredentials); CachedSpecService cachedSpecStorage = new CachedSpecService(coreService); diff --git a/serving/src/main/java/feast/serving/config/WebSecurityConfig.java b/serving/src/main/java/feast/serving/config/WebSecurityConfig.java index f7b24a7..04d3f4b 100644 --- a/serving/src/main/java/feast/serving/config/WebSecurityConfig.java +++ b/serving/src/main/java/feast/serving/config/WebSecurityConfig.java @@ -33,7 +33,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { * Allows for custom web security rules to be applied. * * @param http {@link HttpSecurity} for configuring web based security - * @throws Exception + * @throws Exception exception */ @Override protected void configure(HttpSecurity http) throws Exception { diff --git a/serving/src/main/java/feast/serving/controller/HealthServiceController.java b/serving/src/main/java/feast/serving/controller/HealthServiceController.java index 4bee981..ef675d4 100644 --- a/serving/src/main/java/feast/serving/controller/HealthServiceController.java +++ b/serving/src/main/java/feast/serving/controller/HealthServiceController.java @@ -19,7 +19,6 @@ import feast.proto.serving.ServingAPIProto.GetFeastServingInfoRequest; import feast.serving.interceptors.GrpcMonitoringInterceptor; import feast.serving.service.ServingServiceV2; -import feast.serving.specs.CachedSpecService; import io.grpc.health.v1.HealthGrpc.HealthImplBase; import io.grpc.health.v1.HealthProto.HealthCheckRequest; import io.grpc.health.v1.HealthProto.HealthCheckResponse; @@ -32,12 +31,10 @@ @GrpcService(interceptors = {GrpcMonitoringInterceptor.class}) public class HealthServiceController extends HealthImplBase { - private CachedSpecService specService; - private ServingServiceV2 servingService; + private final ServingServiceV2 servingService; @Autowired - public HealthServiceController(CachedSpecService specService, ServingServiceV2 servingService) { - this.specService = specService; + public HealthServiceController(final ServingServiceV2 servingService) { this.servingService = servingService; } @@ -47,7 +44,7 @@ public void check( // TODO: Implement proper logic to determine if ServingServiceV2 is healthy e.g. // if it's online service check that it the service can retrieve dummy/random // feature table. - // Implement similary for batch service. + // Implement similarly for batch service. try { servingService.getFeastServingInfo(GetFeastServingInfoRequest.getDefaultInstance()); diff --git a/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java b/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java index 531be39..81bbfd0 100644 --- a/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java +++ b/serving/src/main/java/feast/serving/controller/ServingServiceGRpcController.java @@ -25,6 +25,7 @@ import feast.proto.serving.ServingServiceGrpc.ServingServiceImplBase; import feast.serving.config.FeastProperties; import feast.serving.exception.SpecRetrievalException; +import feast.serving.interceptors.GrpcMonitoringContext; import feast.serving.interceptors.GrpcMonitoringInterceptor; import feast.serving.service.ServingServiceV2; import feast.serving.util.RequestHelper; @@ -86,6 +87,9 @@ public void getOnlineFeaturesV2( // project set at root level overrides the project set at feature table level this.authorizationService.authorizeRequest( SecurityContextHolder.getContext(), request.getProject()); + + // update monitoring context + GrpcMonitoringContext.getInstance().setProject(request.getProject()); } RequestHelper.validateOnlineRequest(request); Span span = tracer.buildSpan("getOnlineFeaturesV2").start(); diff --git a/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringContext.java b/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringContext.java new file mode 100644 index 0000000..48d8d76 --- /dev/null +++ b/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringContext.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.interceptors; + +import java.util.Optional; + +public class GrpcMonitoringContext { + private static GrpcMonitoringContext INSTANCE; + + final ThreadLocal project = new ThreadLocal(); + + private GrpcMonitoringContext() {} + + public static GrpcMonitoringContext getInstance() { + if (INSTANCE == null) { + INSTANCE = new GrpcMonitoringContext(); + } + + return INSTANCE; + } + + public void setProject(String name) { + this.project.set(name); + } + + public Optional getProject() { + return Optional.ofNullable(this.project.get()); + } + + public void clearProject() { + this.project.set(null); + } +} diff --git a/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringInterceptor.java b/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringInterceptor.java index bc7ed89..735f8c5 100644 --- a/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringInterceptor.java +++ b/serving/src/main/java/feast/serving/interceptors/GrpcMonitoringInterceptor.java @@ -24,6 +24,7 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; +import java.util.Optional; /** * GrpcMonitoringInterceptor intercepts GRPC calls to provide request latency histogram metrics in @@ -39,12 +40,16 @@ public Listener interceptCall( String fullMethodName = call.getMethodDescriptor().getFullMethodName(); String methodName = fullMethodName.substring(fullMethodName.indexOf("/") + 1); + GrpcMonitoringContext.getInstance().clearProject(); + return next.startCall( new SimpleForwardingServerCall(call) { @Override public void close(Status status, Metadata trailers) { + Optional projectName = GrpcMonitoringContext.getInstance().getProject(); + Metrics.requestLatency - .labels(methodName) + .labels(methodName, projectName.orElse("")) .observe((System.currentTimeMillis() - startCallMillis) / 1000f); Metrics.grpcRequestCount.labels(methodName, status.getCode().name()).inc(); super.close(status, trailers); diff --git a/serving/src/main/java/feast/serving/registry/LocalRegistryRepo.java b/serving/src/main/java/feast/serving/registry/LocalRegistryRepo.java new file mode 100644 index 0000000..4178541 --- /dev/null +++ b/serving/src/main/java/feast/serving/registry/LocalRegistryRepo.java @@ -0,0 +1,121 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.registry; + +import feast.proto.core.FeatureProto; +import feast.proto.core.FeatureViewProto; +import feast.proto.core.OnDemandFeatureViewProto; +import feast.proto.core.RegistryProto; +import feast.proto.serving.ServingAPIProto; +import feast.serving.exception.SpecRetrievalException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class LocalRegistryRepo implements RegistryRepository { + private final RegistryProto.Registry registry; + private Map featureViewNameToSpec; + private Map + onDemandFeatureViewNameToSpec; + + public LocalRegistryRepo(Path localRegistryPath) { + if (!localRegistryPath.toFile().exists()) { + throw new RuntimeException( + String.format("Local registry not found at path %s", localRegistryPath)); + } + try { + final byte[] registryContents = Files.readAllBytes(localRegistryPath); + this.registry = RegistryProto.Registry.parseFrom(registryContents); + } catch (final Exception e) { + throw new RuntimeException(e); + } + + final RegistryProto.Registry registry = this.getRegistry(); + List featureViewSpecs = + registry.getFeatureViewsList().stream() + .map(fv -> fv.getSpec()) + .collect(Collectors.toList()); + featureViewNameToSpec = + featureViewSpecs.stream() + .collect( + Collectors.toMap(FeatureViewProto.FeatureViewSpec::getName, Function.identity())); + List onDemandFeatureViewSpecs = + registry.getOnDemandFeatureViewsList().stream() + .map(odfv -> odfv.getSpec()) + .collect(Collectors.toList()); + onDemandFeatureViewNameToSpec = + onDemandFeatureViewSpecs.stream() + .collect( + Collectors.toMap( + OnDemandFeatureViewProto.OnDemandFeatureViewSpec::getName, + Function.identity())); + } + + @Override + public RegistryProto.Registry getRegistry() { + return this.registry; + } + + @Override + public FeatureViewProto.FeatureViewSpec getFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + String featureViewName = featureReference.getFeatureTable(); + if (featureViewNameToSpec.containsKey(featureViewName)) { + return featureViewNameToSpec.get(featureViewName); + } + throw new SpecRetrievalException( + String.format("Unable to find feature view with name: %s", featureViewName)); + } + + @Override + public FeatureProto.FeatureSpecV2 getFeatureSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + final FeatureViewProto.FeatureViewSpec spec = + this.getFeatureViewSpec(projectName, featureReference); + for (final FeatureProto.FeatureSpecV2 featureSpec : spec.getFeaturesList()) { + if (featureSpec.getName().equals(featureReference.getName())) { + return featureSpec; + } + } + + throw new SpecRetrievalException( + String.format( + "Unable to find feature with name: %s in feature view: %s", + featureReference.getName(), featureReference.getFeatureTable())); + } + + @Override + public OnDemandFeatureViewProto.OnDemandFeatureViewSpec getOnDemandFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + String onDemandFeatureViewName = featureReference.getFeatureTable(); + if (onDemandFeatureViewNameToSpec.containsKey(onDemandFeatureViewName)) { + return onDemandFeatureViewNameToSpec.get(onDemandFeatureViewName); + } + throw new SpecRetrievalException( + String.format( + "Unable to find on demand feature view with name: %s", onDemandFeatureViewName)); + } + + @Override + public boolean isOnDemandFeatureReference(ServingAPIProto.FeatureReferenceV2 featureReference) { + String onDemandFeatureViewName = featureReference.getFeatureTable(); + return onDemandFeatureViewNameToSpec.containsKey(onDemandFeatureViewName); + } +} diff --git a/serving/src/main/java/feast/serving/registry/RegistryRepository.java b/serving/src/main/java/feast/serving/registry/RegistryRepository.java new file mode 100644 index 0000000..21a2183 --- /dev/null +++ b/serving/src/main/java/feast/serving/registry/RegistryRepository.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.registry; + +import feast.proto.core.FeatureProto; +import feast.proto.core.FeatureViewProto; +import feast.proto.core.OnDemandFeatureViewProto; +import feast.proto.core.RegistryProto; +import feast.proto.serving.ServingAPIProto; + +/** + * RegistryRepository allows the ServingService to retrieve feature definitions from a Registry + * object. This approach is needed for a feature store created using feast 0.10+. + */ +public interface RegistryRepository { + RegistryProto.Registry getRegistry(); + + FeatureViewProto.FeatureViewSpec getFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + FeatureProto.FeatureSpecV2 getFeatureSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + OnDemandFeatureViewProto.OnDemandFeatureViewSpec getOnDemandFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + boolean isOnDemandFeatureReference(ServingAPIProto.FeatureReferenceV2 featureReference); +} diff --git a/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java b/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java index 1d35edd..2cd810c 100644 --- a/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java +++ b/serving/src/main/java/feast/serving/service/OnlineServingServiceV2.java @@ -26,16 +26,24 @@ import feast.proto.serving.ServingAPIProto.GetFeastServingInfoResponse; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; +import feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesRequest; +import feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesResponse; +import feast.proto.serving.TransformationServiceAPIProto.ValueType; import feast.proto.types.ValueProto; import feast.serving.exception.SpecRetrievalException; -import feast.serving.specs.CachedSpecService; +import feast.serving.specs.FeatureSpecRetriever; import feast.serving.util.Metrics; import feast.storage.api.retriever.Feature; import feast.storage.api.retriever.OnlineRetrieverV2; import io.grpc.Status; import io.opentracing.Span; import io.opentracing.Tracer; -import java.util.*; +import java.io.*; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -45,15 +53,20 @@ public class OnlineServingServiceV2 implements ServingServiceV2 { private static final Logger log = org.slf4j.LoggerFactory.getLogger(OnlineServingServiceV2.class); - private final CachedSpecService specService; private final Tracer tracer; private final OnlineRetrieverV2 retriever; + private final FeatureSpecRetriever featureSpecRetriever; + private final OnlineTransformationService onlineTransformationService; public OnlineServingServiceV2( - OnlineRetrieverV2 retriever, CachedSpecService specService, Tracer tracer) { + OnlineRetrieverV2 retriever, + Tracer tracer, + FeatureSpecRetriever featureSpecRetriever, + OnlineTransformationService onlineTransformationService) { this.retriever = retriever; - this.specService = specService; this.tracer = tracer; + this.featureSpecRetriever = featureSpecRetriever; + this.onlineTransformationService = onlineTransformationService; } /** {@inheritDoc} */ @@ -67,15 +80,51 @@ public GetFeastServingInfoResponse getFeastServingInfo( @Override public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 request) { - String projectName = request.getProject(); - List featureReferences = request.getFeaturesList(); - // Autofill default project if project is not specified + String projectName = request.getProject(); if (projectName.isEmpty()) { projectName = "default"; } - List entityRows = request.getEntityRowsList(); + // Split all feature references into non-ODFV (e.g. batch and stream) references and ODFV. + List allFeatureReferences = request.getFeaturesList(); + List featureReferences = + allFeatureReferences.stream() + .filter(r -> !this.featureSpecRetriever.isOnDemandFeatureReference(r)) + .collect(Collectors.toList()); + List onDemandFeatureReferences = + allFeatureReferences.stream() + .filter(r -> this.featureSpecRetriever.isOnDemandFeatureReference(r)) + .collect(Collectors.toList()); + + // Get the set of request data feature names and feature inputs from the ODFV references. + Pair, List> pair = + this.onlineTransformationService.extractRequestDataFeatureNamesAndOnDemandFeatureInputs( + onDemandFeatureReferences, projectName); + Set requestDataFeatureNames = pair.getLeft(); + List onDemandFeatureInputs = pair.getRight(); + + // Add on demand feature inputs to list of feature references to retrieve. + Set addedFeatureReferences = new HashSet(); + for (FeatureReferenceV2 onDemandFeatureInput : onDemandFeatureInputs) { + if (!featureReferences.contains(onDemandFeatureInput)) { + featureReferences.add(onDemandFeatureInput); + addedFeatureReferences.add(onDemandFeatureInput); + } + } + + // Separate entity rows into entity data and request feature data. + Pair, Map>> + entityRowsAndRequestDataFeatures = + this.onlineTransformationService.separateEntityRows(requestDataFeatureNames, request); + List entityRows = + entityRowsAndRequestDataFeatures.getLeft(); + Map> requestDataFeatures = + entityRowsAndRequestDataFeatures.getRight(); + // TODO: error checking on lengths of lists in entityRows and requestDataFeatures + + // Extract values and statuses to be used later in constructing FieldValues for the response. + // The online features retrieved will augment these two data structures. List> values = entityRows.stream().map(r -> new HashMap<>(r.getFieldsMap())).collect(Collectors.toList()); List> statuses = @@ -94,10 +143,10 @@ public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 re .collect( Collectors.toMap( Function.identity(), - ref -> specService.getFeatureTableSpec(finalProjectName, ref).getMaxAge())); + ref -> this.featureSpecRetriever.getMaxAge(finalProjectName, ref))); List entityNames = featureReferences.stream() - .map(ref -> specService.getFeatureTableSpec(finalProjectName, ref).getEntitiesList()) + .map(ref -> this.featureSpecRetriever.getEntitiesList(finalProjectName, ref)) .findFirst() .get(); @@ -109,7 +158,9 @@ public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 re Function.identity(), ref -> { try { - return specService.getFeatureSpec(finalProjectName, ref).getValueType(); + return this.featureSpecRetriever + .getFeatureSpec(finalProjectName, ref) + .getValueType(); } catch (SpecRetrievalException e) { return ValueProto.ValueType.Enum.INVALID; } @@ -188,6 +239,71 @@ public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 re populateHistogramMetrics(entityRows, featureReferences, projectName); populateFeatureCountMetrics(featureReferences, projectName); + // Handle ODFVs. For each ODFV reference, we send a TransformFeaturesRequest to the FTS. + // The request should contain the entity data, the retrieved features, and the request data. + if (!onDemandFeatureReferences.isEmpty()) { + // Augment values, which contains the entity data and retrieved features, with the request + // data. Also augment statuses. + for (int i = 0; i < values.size(); i++) { + Map rowValues = values.get(i); + Map rowStatuses = statuses.get(i); + + for (Map.Entry> entry : requestDataFeatures.entrySet()) { + String key = entry.getKey(); + List fieldValues = entry.getValue(); + rowValues.put(key, fieldValues.get(i)); + rowStatuses.put(key, GetOnlineFeaturesResponse.FieldStatus.PRESENT); + } + } + + // Serialize the augmented values. + ValueType transformationInput = + this.onlineTransformationService.serializeValuesIntoArrowIPC(values); + + // Send out requests to the FTS and process the responses. + Set onDemandFeatureStringReferences = + onDemandFeatureReferences.stream() + .map(r -> FeatureV2.getFeatureStringRef(r)) + .collect(Collectors.toSet()); + for (FeatureReferenceV2 featureReference : onDemandFeatureReferences) { + String onDemandFeatureViewName = featureReference.getFeatureTable(); + TransformFeaturesRequest transformFeaturesRequest = + TransformFeaturesRequest.newBuilder() + .setOnDemandFeatureViewName(onDemandFeatureViewName) + .setProject(projectName) + .setTransformationInput(transformationInput) + .build(); + + TransformFeaturesResponse transformFeaturesResponse = + this.onlineTransformationService.transformFeatures(transformFeaturesRequest); + + this.onlineTransformationService.processTransformFeaturesResponse( + transformFeaturesResponse, + onDemandFeatureViewName, + onDemandFeatureStringReferences, + values, + statuses); + } + + // Remove all features that were added as inputs for ODFVs. + Set addedFeatureStringReferences = + addedFeatureReferences.stream() + .map(r -> FeatureV2.getFeatureStringRef(r)) + .collect(Collectors.toSet()); + for (int i = 0; i < values.size(); i++) { + Map rowValues = values.get(i); + Map rowStatuses = statuses.get(i); + List keysToRemove = + rowValues.keySet().stream() + .filter(k -> addedFeatureStringReferences.contains(k)) + .collect(Collectors.toList()); + for (String key : keysToRemove) { + rowValues.remove(key); + rowStatuses.remove(key); + } + } + } + // 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 = @@ -199,6 +315,7 @@ public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequestV2 re .putAllStatuses(statuses.get(entityRowIdx)) .build()) .collect(Collectors.toList()); + return GetOnlineFeaturesResponse.newBuilder().addAllFieldValues(fieldValuesList).build(); } diff --git a/serving/src/main/java/feast/serving/service/OnlineTransformationService.java b/serving/src/main/java/feast/serving/service/OnlineTransformationService.java new file mode 100644 index 0000000..541fe46 --- /dev/null +++ b/serving/src/main/java/feast/serving/service/OnlineTransformationService.java @@ -0,0 +1,412 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.ByteString; +import feast.common.models.FeatureV2; +import feast.proto.core.DataSourceProto; +import feast.proto.core.FeatureProto; +import feast.proto.core.FeatureViewProto; +import feast.proto.core.OnDemandFeatureViewProto; +import feast.proto.serving.ServingAPIProto; +import feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesRequest; +import feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesResponse; +import feast.proto.serving.TransformationServiceAPIProto.ValueType; +import feast.proto.serving.TransformationServiceGrpc; +import feast.proto.types.ValueProto; +import feast.serving.specs.FeatureSpecRetriever; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Status; +import java.io.IOException; +import java.nio.channels.Channels; +import java.util.*; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.vector.*; +import org.apache.arrow.vector.ipc.ArrowFileReader; +import org.apache.arrow.vector.ipc.ArrowFileWriter; +import org.apache.arrow.vector.types.FloatingPointPrecision; +import org.apache.arrow.vector.types.pojo.ArrowType; +import org.apache.arrow.vector.types.pojo.Field; +import org.apache.arrow.vector.types.pojo.Schema; +import org.apache.arrow.vector.util.ByteArrayReadableSeekableByteChannel; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream; +import org.slf4j.Logger; + +public class OnlineTransformationService implements TransformationService { + + private static final Logger log = + org.slf4j.LoggerFactory.getLogger(OnlineTransformationService.class); + private final TransformationServiceGrpc.TransformationServiceBlockingStub stub; + private final FeatureSpecRetriever featureSpecRetriever; + static final int INT64_BITWIDTH = 64; + static final int INT32_BITWIDTH = 32; + + public OnlineTransformationService( + String transformationServiceEndpoint, FeatureSpecRetriever featureSpecRetriever) { + if (transformationServiceEndpoint != null) { + final ManagedChannel channel = + ManagedChannelBuilder.forTarget(transformationServiceEndpoint).usePlaintext().build(); + this.stub = TransformationServiceGrpc.newBlockingStub(channel); + } else { + this.stub = null; + } + this.featureSpecRetriever = featureSpecRetriever; + } + + /** {@inheritDoc} */ + @Override + public TransformFeaturesResponse transformFeatures( + TransformFeaturesRequest transformFeaturesRequest) { + return this.stub.transformFeatures(transformFeaturesRequest); + } + + /** {@inheritDoc} */ + @Override + public Pair, List> + extractRequestDataFeatureNamesAndOnDemandFeatureInputs( + List onDemandFeatureReferences, String projectName) { + Set requestDataFeatureNames = new HashSet(); + List onDemandFeatureInputs = + new ArrayList(); + for (ServingAPIProto.FeatureReferenceV2 featureReference : onDemandFeatureReferences) { + OnDemandFeatureViewProto.OnDemandFeatureViewSpec onDemandFeatureViewSpec = + this.featureSpecRetriever.getOnDemandFeatureViewSpec(projectName, featureReference); + Map inputs = + onDemandFeatureViewSpec.getInputsMap(); + + for (OnDemandFeatureViewProto.OnDemandInput input : inputs.values()) { + OnDemandFeatureViewProto.OnDemandInput.InputCase inputCase = input.getInputCase(); + switch (inputCase) { + case REQUEST_DATA_SOURCE: + DataSourceProto.DataSource requestDataSource = input.getRequestDataSource(); + DataSourceProto.DataSource.RequestDataOptions requestDataOptions = + requestDataSource.getRequestDataOptions(); + Set requestDataNames = requestDataOptions.getSchemaMap().keySet(); + requestDataFeatureNames.addAll(requestDataNames); + break; + case FEATURE_VIEW: + FeatureViewProto.FeatureView featureView = input.getFeatureView(); + FeatureViewProto.FeatureViewSpec featureViewSpec = featureView.getSpec(); + String featureViewName = featureViewSpec.getName(); + for (FeatureProto.FeatureSpecV2 featureSpec : featureViewSpec.getFeaturesList()) { + String featureName = featureSpec.getName(); + ServingAPIProto.FeatureReferenceV2 onDemandFeatureInput = + ServingAPIProto.FeatureReferenceV2.newBuilder() + .setFeatureTable(featureViewName) + .setName(featureName) + .build(); + onDemandFeatureInputs.add(onDemandFeatureInput); + } + break; + default: + throw Status.INTERNAL + .withDescription( + "OnDemandInput proto input field has an unexpected type: " + inputCase) + .asRuntimeException(); + } + } + } + Pair, List> pair = + new ImmutablePair, List>( + requestDataFeatureNames, onDemandFeatureInputs); + return pair; + } + + /** {@inheritDoc} */ + public Pair< + List, + Map>> + separateEntityRows( + Set requestDataFeatureNames, ServingAPIProto.GetOnlineFeaturesRequestV2 request) { + // Separate entity rows into entity data and request feature data. + List entityRows = + new ArrayList(); + Map> requestDataFeatures = + new HashMap>(); + + for (ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow entityRow : + request.getEntityRowsList()) { + Map fieldsMap = new HashMap(); + + for (Map.Entry entry : entityRow.getFieldsMap().entrySet()) { + String key = entry.getKey(); + ValueProto.Value value = entry.getValue(); + + if (requestDataFeatureNames.contains(key)) { + if (!requestDataFeatures.containsKey(key)) { + requestDataFeatures.put(key, new ArrayList()); + } + requestDataFeatures.get(key).add(value); + } else { + fieldsMap.put(key, value); + } + } + + // Construct new entity row containing the extracted entity data, if necessary. + if (!fieldsMap.isEmpty()) { + ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow newEntityRow = + ServingAPIProto.GetOnlineFeaturesRequestV2.EntityRow.newBuilder() + .setTimestamp(entityRow.getTimestamp()) + .putAllFields(fieldsMap) + .build(); + entityRows.add(newEntityRow); + } + } + + Pair< + List, + Map>> + pair = + new ImmutablePair< + List, + Map>>(entityRows, requestDataFeatures); + return pair; + } + + /** {@inheritDoc} */ + public void processTransformFeaturesResponse( + feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesResponse + transformFeaturesResponse, + String onDemandFeatureViewName, + Set onDemandFeatureStringReferences, + List> values, + List> statuses) { + try { + BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); + ArrowFileReader reader = + new ArrowFileReader( + new ByteArrayReadableSeekableByteChannel( + transformFeaturesResponse + .getTransformationOutput() + .getArrowValue() + .toByteArray()), + allocator); + reader.loadNextBatch(); + VectorSchemaRoot readBatch = reader.getVectorSchemaRoot(); + Schema responseSchema = readBatch.getSchema(); + List responseFields = responseSchema.getFields(); + + for (Field field : responseFields) { + String columnName = field.getName(); + String fullFeatureName = onDemandFeatureViewName + ":" + columnName; + ArrowType columnType = field.getType(); + + // The response will contain all features for the specified ODFV, so we + // skip the features that were not requested. + if (!onDemandFeatureStringReferences.contains(fullFeatureName)) { + continue; + } + + FieldVector fieldVector = readBatch.getVector(field); + int valueCount = fieldVector.getValueCount(); + + // TODO: support all Feast types + // TODO: clean up the switch statement + if (columnType instanceof ArrowType.Int) { + int bitWidth = ((ArrowType.Int) columnType).getBitWidth(); + switch (bitWidth) { + case INT64_BITWIDTH: + for (int i = 0; i < valueCount; i++) { + long int64Value = ((BigIntVector) fieldVector).get(i); + Map rowValues = values.get(i); + Map rowStatuses = + statuses.get(i); + ValueProto.Value value = + ValueProto.Value.newBuilder().setInt64Val(int64Value).build(); + rowValues.put(fullFeatureName, value); + rowStatuses.put( + fullFeatureName, ServingAPIProto.GetOnlineFeaturesResponse.FieldStatus.PRESENT); + } + break; + case INT32_BITWIDTH: + for (int i = 0; i < valueCount; i++) { + int intValue = ((IntVector) fieldVector).get(i); + Map rowValues = values.get(i); + Map rowStatuses = + statuses.get(i); + ValueProto.Value value = + ValueProto.Value.newBuilder().setInt32Val(intValue).build(); + rowValues.put(fullFeatureName, value); + rowStatuses.put( + fullFeatureName, ServingAPIProto.GetOnlineFeaturesResponse.FieldStatus.PRESENT); + } + break; + default: + throw Status.INTERNAL + .withDescription( + "Column " + + columnName + + " is of type ArrowType.Int but has bitWidth " + + bitWidth + + " which cannot be handled.") + .asRuntimeException(); + } + } else if (columnType instanceof ArrowType.FloatingPoint) { + FloatingPointPrecision precision = ((ArrowType.FloatingPoint) columnType).getPrecision(); + switch (precision) { + case DOUBLE: + for (int i = 0; i < valueCount; i++) { + double doubleValue = ((Float8Vector) fieldVector).get(i); + Map rowValues = values.get(i); + Map rowStatuses = + statuses.get(i); + ValueProto.Value value = + ValueProto.Value.newBuilder().setDoubleVal(doubleValue).build(); + rowValues.put(fullFeatureName, value); + rowStatuses.put( + fullFeatureName, ServingAPIProto.GetOnlineFeaturesResponse.FieldStatus.PRESENT); + } + break; + case SINGLE: + for (int i = 0; i < valueCount; i++) { + float floatValue = ((Float4Vector) fieldVector).get(i); + Map rowValues = values.get(i); + Map rowStatuses = + statuses.get(i); + ValueProto.Value value = + ValueProto.Value.newBuilder().setFloatVal(floatValue).build(); + rowValues.put(fullFeatureName, value); + rowStatuses.put( + fullFeatureName, ServingAPIProto.GetOnlineFeaturesResponse.FieldStatus.PRESENT); + } + break; + default: + throw Status.INTERNAL + .withDescription( + "Column " + + columnName + + " is of type ArrowType.FloatingPoint but has precision " + + precision + + " which cannot be handled.") + .asRuntimeException(); + } + } + } + } catch (IOException e) { + log.info(e.toString()); + throw Status.INTERNAL + .withDescription( + "Unable to correctly process transform features response: " + e.toString()) + .asRuntimeException(); + } + } + + /** {@inheritDoc} */ + public ValueType serializeValuesIntoArrowIPC(List> values) { + // In order to be serialized correctly, the data must be packaged in a VectorSchemaRoot. + // We first construct all the columns. + Map columnNameToColumn = new HashMap(); + BufferAllocator allocator = new RootAllocator(Long.MAX_VALUE); + Map firstAugmentedRowValues = values.get(0); + for (Map.Entry entry : firstAugmentedRowValues.entrySet()) { + // The Python FTS does not expect full feature names, so we extract the feature name. + String columnName = FeatureV2.getFeatureName(entry.getKey()); + ValueProto.Value.ValCase valCase = entry.getValue().getValCase(); + FieldVector column; + // TODO: support all Feast types + switch (valCase) { + case INT32_VAL: + column = new IntVector(columnName, allocator); + break; + case INT64_VAL: + column = new BigIntVector(columnName, allocator); + break; + case DOUBLE_VAL: + column = new Float8Vector(columnName, allocator); + break; + case FLOAT_VAL: + column = new Float4Vector(columnName, allocator); + break; + default: + throw Status.INTERNAL + .withDescription( + "Column " + columnName + " has a type that is currently not handled: " + valCase) + .asRuntimeException(); + } + column.allocateNew(); + columnNameToColumn.put(columnName, column); + } + + // Add the data, row by row. + for (int i = 0; i < values.size(); i++) { + Map augmentedRowValues = values.get(i); + + for (Map.Entry entry : augmentedRowValues.entrySet()) { + String columnName = FeatureV2.getFeatureName(entry.getKey()); + ValueProto.Value value = entry.getValue(); + ValueProto.Value.ValCase valCase = value.getValCase(); + FieldVector column = columnNameToColumn.get(columnName); + // TODO: support all Feast types + switch (valCase) { + case INT32_VAL: + ((IntVector) column).setSafe(i, value.getInt32Val()); + break; + case INT64_VAL: + ((BigIntVector) column).setSafe(i, value.getInt64Val()); + break; + case DOUBLE_VAL: + ((Float8Vector) column).setSafe(i, value.getDoubleVal()); + break; + case FLOAT_VAL: + ((Float4Vector) column).setSafe(i, value.getFloatVal()); + break; + default: + throw Status.INTERNAL + .withDescription( + "Column " + + columnName + + " has a type that is currently not handled: " + + valCase) + .asRuntimeException(); + } + } + } + + // Construct the VectorSchemaRoot. + List columnFields = new ArrayList(); + List columns = new ArrayList(); + for (FieldVector column : columnNameToColumn.values()) { + column.setValueCount(values.size()); + columnFields.add(column.getField()); + columns.add(column); + } + VectorSchemaRoot schemaRoot = new VectorSchemaRoot(columnFields, columns); + + // Serialize the VectorSchemaRoot into Arrow IPC format. + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ArrowFileWriter writer = new ArrowFileWriter(schemaRoot, null, Channels.newChannel(out)); + try { + writer.start(); + writer.writeBatch(); + writer.end(); + } catch (IOException e) { + log.info(e.toString()); + throw Status.INTERNAL + .withDescription( + "ArrowFileWriter could not write properly; failed with error: " + e.toString()) + .asRuntimeException(); + } + byte[] byteData = out.toByteArray(); + ByteString inputData = ByteString.copyFrom(byteData); + ValueType transformationInput = ValueType.newBuilder().setArrowValue(inputData).build(); + return transformationInput; + } +} diff --git a/serving/src/main/java/feast/serving/service/TransformationService.java b/serving/src/main/java/feast/serving/service/TransformationService.java new file mode 100644 index 0000000..caa5279 --- /dev/null +++ b/serving/src/main/java/feast/serving/service/TransformationService.java @@ -0,0 +1,88 @@ +/* + * 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; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; +import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; +import feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesRequest; +import feast.proto.serving.TransformationServiceAPIProto.TransformFeaturesResponse; +import feast.proto.serving.TransformationServiceAPIProto.ValueType; +import feast.proto.types.ValueProto; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.tuple.Pair; + +public interface TransformationService { + /** + * Apply on demand transformations for the specified ODFVs. + * + * @param transformFeaturesRequest proto containing the ODFV references and necessary data + * @return a proto object containing the response + */ + TransformFeaturesResponse transformFeatures(TransformFeaturesRequest transformFeaturesRequest); + + /** + * Extract the set of request data feature names and the list of on demand feature inputs from a + * list of ODFV references. + * + * @param onDemandFeatureReferences list of ODFV references to be parsed + * @param projectName project name + * @return a pair containing the set of request data feature names and list of on demand feature + * inputs + */ + Pair, List> + extractRequestDataFeatureNamesAndOnDemandFeatureInputs( + List onDemandFeatureReferences, String projectName); + + /** + * Separate the entity rows of a request into entity data and request feature data. + * + * @param requestDataFeatureNames set of feature names for the request data + * @param request the GetOnlineFeaturesRequestV2 containing the entity rows + * @return a pair containing the set of request data feature names and list of on demand feature + * inputs + */ + Pair, Map>> + separateEntityRows(Set requestDataFeatureNames, GetOnlineFeaturesRequestV2 request); + + /** + * Process a response from the feature transformation server by augmenting the given lists of + * field maps and status maps with the correct fields from the response. + * + * @param transformFeaturesResponse response to be processed + * @param onDemandFeatureViewName name of ODFV to which the response corresponds + * @param onDemandFeatureStringReferences set of all ODFV references that should be kept + * @param values list of field maps to be augmented with additional fields from the response + * @param statuses list of status maps to be augmented + */ + void processTransformFeaturesResponse( + TransformFeaturesResponse transformFeaturesResponse, + String onDemandFeatureViewName, + Set onDemandFeatureStringReferences, + List> values, + List> statuses); + + /** + * Serialize data into Arrow IPC format, to be sent to the Python feature transformation server. + * + * @param values list of field maps to be serialized + * @return the data packaged into a ValueType proto object + */ + ValueType serializeValuesIntoArrowIPC(List> values); +} diff --git a/serving/src/main/java/feast/serving/specs/CoreFeatureSpecRetriever.java b/serving/src/main/java/feast/serving/specs/CoreFeatureSpecRetriever.java new file mode 100644 index 0000000..2a88659 --- /dev/null +++ b/serving/src/main/java/feast/serving/specs/CoreFeatureSpecRetriever.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.specs; + +import com.google.protobuf.Duration; +import feast.proto.core.FeatureProto; +import feast.proto.core.FeatureViewProto; +import feast.proto.core.OnDemandFeatureViewProto; +import feast.proto.serving.ServingAPIProto; +import java.util.List; + +public class CoreFeatureSpecRetriever implements FeatureSpecRetriever { + private final CachedSpecService specService; + + public CoreFeatureSpecRetriever(CachedSpecService specService) { + this.specService = specService; + } + + @Override + public Duration getMaxAge( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.specService.getFeatureTableSpec(projectName, featureReference).getMaxAge(); + } + + @Override + public List getEntitiesList( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.specService.getFeatureTableSpec(projectName, featureReference).getEntitiesList(); + } + + @Override + public FeatureProto.FeatureSpecV2 getFeatureSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.specService.getFeatureSpec(projectName, featureReference); + } + + @Override + public FeatureViewProto.FeatureViewSpec getBatchFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + throw new UnsupportedOperationException( + String.format("Feast Core does not support getting feature view specs.")); + } + + @Override + public OnDemandFeatureViewProto.OnDemandFeatureViewSpec getOnDemandFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + throw new UnsupportedOperationException( + String.format("Feast Core does not support on demand feature views.")); + } + + @Override + public boolean isOnDemandFeatureReference(ServingAPIProto.FeatureReferenceV2 featureReference) { + return false; + } +} diff --git a/serving/src/main/java/feast/serving/specs/FeatureSpecRetriever.java b/serving/src/main/java/feast/serving/specs/FeatureSpecRetriever.java new file mode 100644 index 0000000..57e931c --- /dev/null +++ b/serving/src/main/java/feast/serving/specs/FeatureSpecRetriever.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.specs; + +import com.google.protobuf.Duration; +import feast.proto.core.FeatureProto; +import feast.proto.core.FeatureViewProto; +import feast.proto.core.OnDemandFeatureViewProto; +import feast.proto.serving.ServingAPIProto; +import java.util.List; + +public interface FeatureSpecRetriever { + + Duration getMaxAge(String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + List getEntitiesList( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + FeatureProto.FeatureSpecV2 getFeatureSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + FeatureViewProto.FeatureViewSpec getBatchFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + OnDemandFeatureViewProto.OnDemandFeatureViewSpec getOnDemandFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference); + + boolean isOnDemandFeatureReference(ServingAPIProto.FeatureReferenceV2 featureReference); +} diff --git a/serving/src/main/java/feast/serving/specs/RegistryFeatureSpecRetriever.java b/serving/src/main/java/feast/serving/specs/RegistryFeatureSpecRetriever.java new file mode 100644 index 0000000..0cd851e --- /dev/null +++ b/serving/src/main/java/feast/serving/specs/RegistryFeatureSpecRetriever.java @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.specs; + +import com.google.protobuf.Duration; +import feast.proto.core.FeatureProto; +import feast.proto.core.FeatureViewProto; +import feast.proto.core.OnDemandFeatureViewProto; +import feast.proto.core.RegistryProto; +import feast.proto.serving.ServingAPIProto; +import feast.serving.exception.SpecRetrievalException; +import feast.serving.registry.RegistryRepository; +import java.util.List; + +public class RegistryFeatureSpecRetriever implements FeatureSpecRetriever { + private final RegistryRepository registryRepository; + + public RegistryFeatureSpecRetriever(RegistryRepository registryRepository) { + this.registryRepository = registryRepository; + } + + @Override + public Duration getMaxAge( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + final RegistryProto.Registry registry = this.registryRepository.getRegistry(); + for (final FeatureViewProto.FeatureView featureView : registry.getFeatureViewsList()) { + if (featureView.getSpec().getName().equals(featureReference.getFeatureTable())) { + return featureView.getSpec().getTtl(); + } + } + throw new SpecRetrievalException( + String.format( + "Unable to find feature view with name: %s", featureReference.getFeatureTable())); + } + + @Override + public List getEntitiesList( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + final RegistryProto.Registry registry = this.registryRepository.getRegistry(); + for (final FeatureViewProto.FeatureView featureView : registry.getFeatureViewsList()) { + if (featureView.getSpec().getName().equals(featureReference.getFeatureTable())) { + return featureView.getSpec().getEntitiesList(); + } + } + throw new SpecRetrievalException( + String.format( + "Unable to find feature view with name: %s", featureReference.getFeatureTable())); + } + + @Override + public FeatureProto.FeatureSpecV2 getFeatureSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.registryRepository.getFeatureSpec(projectName, featureReference); + } + + @Override + public FeatureViewProto.FeatureViewSpec getBatchFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.registryRepository.getFeatureViewSpec(projectName, featureReference); + } + + @Override + public OnDemandFeatureViewProto.OnDemandFeatureViewSpec getOnDemandFeatureViewSpec( + String projectName, ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.registryRepository.getOnDemandFeatureViewSpec(projectName, featureReference); + } + + @Override + public boolean isOnDemandFeatureReference(ServingAPIProto.FeatureReferenceV2 featureReference) { + return this.registryRepository.isOnDemandFeatureReference(featureReference); + } +} diff --git a/serving/src/main/java/feast/serving/util/Metrics.java b/serving/src/main/java/feast/serving/util/Metrics.java index 90b9493..dca2b5e 100644 --- a/serving/src/main/java/feast/serving/util/Metrics.java +++ b/serving/src/main/java/feast/serving/util/Metrics.java @@ -26,7 +26,7 @@ public class Metrics { .name("request_latency_seconds") .subsystem("feast_serving") .help("Request latency in seconds") - .labelNames("method") + .labelNames("method", "project") .register(); public static final Histogram requestEntityCountDistribution = diff --git a/serving/src/main/resources/application.yml b/serving/src/main/resources/application.yml index f8187e9..3e4e07b 100644 --- a/serving/src/main/resources/application.yml +++ b/serving/src/main/resources/application.yml @@ -3,7 +3,7 @@ feast: # Feast Serving requires connection to Feast Core to retrieve and reload Feast metadata (e.g. FeatureSpecs, Store information) core-host: ${FEAST_CORE_HOST:localhost} core-grpc-port: ${FEAST_CORE_GRPC_PORT:6565} - + core-authentication: enabled: false # should be set to true if authentication is enabled on core. provider: google # can be set to `oauth` or `google` diff --git a/serving/src/test/java/feast/serving/it/ServingServiceCassandraIT.java b/serving/src/test/java/feast/serving/it/ServingServiceCassandraIT.java deleted file mode 100644 index 93ee5f5..0000000 --- a/serving/src/test/java/feast/serving/it/ServingServiceCassandraIT.java +++ /dev/null @@ -1,728 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2018-2021 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 com.datastax.oss.driver.api.core.CqlSession; -import com.datastax.oss.driver.api.core.cql.PreparedStatement; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.hash.Hashing; -import feast.common.it.DataGenerator; -import feast.common.models.FeatureV2; -import feast.proto.core.EntityProto; -import feast.proto.serving.ServingAPIProto.FeatureReferenceV2; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; -import feast.proto.serving.ServingServiceGrpc; -import feast.proto.types.ValueProto; -import io.grpc.ManagedChannel; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.apache.avro.Schema; -import org.apache.avro.SchemaBuilder; -import org.apache.avro.generic.GenericDatumWriter; -import org.apache.avro.generic.GenericRecord; -import org.apache.avro.generic.GenericRecordBuilder; -import org.apache.avro.io.Encoder; -import org.apache.avro.io.EncoderFactory; -import org.junit.ClassRule; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -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; -import org.testcontainers.junit.jupiter.Testcontainers; - -@ActiveProfiles("it") -@SpringBootTest( - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = { - "feast.core-cache-refresh-interval=1", - "feast.active_store=cassandra", - "spring.main.allow-bean-definition-overriding=true" - }) -@Testcontainers -public class ServingServiceCassandraIT extends BaseAuthIT { - - static final Map options = new HashMap<>(); - static CoreSimpleAPIClient coreClient; - static ServingServiceGrpc.ServingServiceBlockingStub servingStub; - - static CqlSession cqlSession; - static final int FEAST_SERVING_PORT = 6570; - - static final FeatureReferenceV2 feature1Reference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - static final FeatureReferenceV2 feature2Reference = - DataGenerator.createFeatureReference("rides", "trip_distance"); - static final FeatureReferenceV2 feature3Reference = - DataGenerator.createFeatureReference("rides", "trip_empty"); - static final FeatureReferenceV2 feature4Reference = - DataGenerator.createFeatureReference("rides", "trip_wrong_type"); - - @ClassRule @Container - public static DockerComposeContainer environment = - new DockerComposeContainer( - new File("src/test/resources/docker-compose/docker-compose-cassandra-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(CASSANDRA, CASSANDRA_PORT); - - @DynamicPropertySource - static void initialize(DynamicPropertyRegistry registry) { - registry.add("grpc.server.port", () -> FEAST_SERVING_PORT); - } - - @BeforeAll - static void globalSetup() throws IOException { - coreClient = TestUtils.getApiClientForCore(FEAST_CORE_PORT); - servingStub = TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - - cqlSession = - CqlSession.builder() - .addContactPoint( - new InetSocketAddress( - environment.getServiceHost("cassandra_1", CASSANDRA_PORT), - environment.getServicePort("cassandra_1", CASSANDRA_PORT))) - .withLocalDatacenter(CASSANDRA_DATACENTER) - .build(); - - /** Feast resource creation Workflow */ - String projectName = "default"; - // Apply Entity (driver_id) - String driverEntityName = "driver_id"; - String driverEntityDescription = "My driver id"; - ValueProto.ValueType.Enum driverEntityType = ValueProto.ValueType.Enum.INT64; - EntityProto.EntitySpecV2 driverEntitySpec = - EntityProto.EntitySpecV2.newBuilder() - .setName(driverEntityName) - .setDescription(driverEntityDescription) - .setValueType(driverEntityType) - .build(); - TestUtils.applyEntity(coreClient, projectName, driverEntitySpec); - - // Apply Entity (merchant_id) - String merchantEntityName = "merchant_id"; - String merchantEntityDescription = "My driver id"; - ValueProto.ValueType.Enum merchantEntityType = ValueProto.ValueType.Enum.INT64; - EntityProto.EntitySpecV2 merchantEntitySpec = - EntityProto.EntitySpecV2.newBuilder() - .setName(merchantEntityName) - .setDescription(merchantEntityDescription) - .setValueType(merchantEntityType) - .build(); - TestUtils.applyEntity(coreClient, projectName, merchantEntitySpec); - - // Apply FeatureTable (rides) - String ridesFeatureTableName = "rides"; - ImmutableList ridesEntities = ImmutableList.of(driverEntityName); - ImmutableMap ridesFeatures = - ImmutableMap.of( - "trip_cost", - ValueProto.ValueType.Enum.INT32, - "trip_distance", - ValueProto.ValueType.Enum.DOUBLE, - "trip_empty", - ValueProto.ValueType.Enum.DOUBLE, - "trip_wrong_type", - ValueProto.ValueType.Enum.STRING); - TestUtils.applyFeatureTable( - coreClient, projectName, ridesFeatureTableName, ridesEntities, ridesFeatures, 7200); - - // Apply FeatureTable (food) - String foodFeatureTableName = "food"; - ImmutableList foodEntities = ImmutableList.of(driverEntityName); - ImmutableMap foodFeatures = - ImmutableMap.of( - "trip_cost", - ValueProto.ValueType.Enum.INT32, - "trip_distance", - ValueProto.ValueType.Enum.DOUBLE); - TestUtils.applyFeatureTable( - coreClient, projectName, foodFeatureTableName, foodEntities, foodFeatures, 7200); - - // Apply FeatureTable (rides_merchant) - String rideMerchantFeatureTableName = "rides_merchant"; - ImmutableList ridesMerchantEntities = - ImmutableList.of(driverEntityName, merchantEntityName); - TestUtils.applyFeatureTable( - coreClient, - projectName, - rideMerchantFeatureTableName, - ridesMerchantEntities, - ridesFeatures, - 7200); - - /** Create Cassandra Tables Workflow */ - String cassandraTableName = String.format("%s__%s", projectName, driverEntityName); - String compoundCassandraTableName = - String.format("%s__%s", projectName, String.join("__", ridesMerchantEntities)); - - cqlSession.execute(String.format("DROP KEYSPACE IF EXISTS %s", CASSANDRA_KEYSPACE)); - cqlSession.execute( - String.format( - "CREATE KEYSPACE %s WITH replication = \n" - + "{'class':'SimpleStrategy','replication_factor':'1'};", - CASSANDRA_KEYSPACE)); - - // Create Cassandra Tables - createCassandraTable(cassandraTableName); - createCassandraTable(compoundCassandraTableName); - - // Add column families - addCassandraTableColumn(cassandraTableName, ridesFeatureTableName); - addCassandraTableColumn(cassandraTableName, foodFeatureTableName); - addCassandraTableColumn(compoundCassandraTableName, rideMerchantFeatureTableName); - - /** Single Entity Ingestion Workflow */ - Schema ftSchema = - SchemaBuilder.record("DriverData") - .namespace(ridesFeatureTableName) - .fields() - .requiredInt(feature1Reference.getName()) - .requiredDouble(feature2Reference.getName()) - .nullableString(feature3Reference.getName(), "null") - .requiredString(feature4Reference.getName()) - .endRecord(); - byte[] schemaReference = - Hashing.murmur3_32().hashBytes(ftSchema.toString().getBytes()).asBytes(); - byte[] schemaKey = createSchemaKey(schemaReference); - - ingestBulk(ridesFeatureTableName, cassandraTableName, ftSchema, 20); - - Schema foodFtSchema = - SchemaBuilder.record("FoodDriverData") - .namespace(foodFeatureTableName) - .fields() - .requiredInt(feature1Reference.getName()) - .requiredDouble(feature2Reference.getName()) - .nullableString(feature3Reference.getName(), "null") - .requiredString(feature4Reference.getName()) - .endRecord(); - byte[] foodSchemaReference = - Hashing.murmur3_32().hashBytes(foodFtSchema.toString().getBytes()).asBytes(); - byte[] foodSchemaKey = createSchemaKey(foodSchemaReference); - - ingestBulk(foodFeatureTableName, cassandraTableName, foodFtSchema, 20); - - /** Compound Entity Ingestion Workflow */ - Schema compoundFtSchema = - SchemaBuilder.record("DriverMerchantData") - .namespace(rideMerchantFeatureTableName) - .fields() - .requiredLong(feature1Reference.getName()) - .requiredDouble(feature2Reference.getName()) - .nullableString(feature3Reference.getName(), "null") - .requiredString(feature4Reference.getName()) - .endRecord(); - byte[] compoundSchemaReference = - Hashing.murmur3_32().hashBytes(compoundFtSchema.toString().getBytes()).asBytes(); - - GenericRecord compoundEntityRecord = - new GenericRecordBuilder(compoundFtSchema) - .set("trip_cost", 10L) - .set("trip_distance", 5.5) - .set("trip_empty", null) - .set("trip_wrong_type", "wrong_type") - .build(); - ValueProto.Value driverEntityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); - ValueProto.Value merchantEntityValue = ValueProto.Value.newBuilder().setInt64Val(1234).build(); - ImmutableMap compoundEntityMap = - ImmutableMap.of( - driverEntityName, driverEntityValue, merchantEntityName, merchantEntityValue); - GetOnlineFeaturesRequestV2.EntityRow entityRow = - DataGenerator.createCompoundEntityRow(compoundEntityMap, 100); - byte[] compoundEntityFeatureKey = - ridesMerchantEntities.stream() - .map(entity -> DataGenerator.valueToString(entityRow.getFieldsMap().get(entity))) - .collect(Collectors.joining("#")) - .getBytes(); - byte[] compoundEntityFeatureValue = createEntityValue(compoundFtSchema, compoundEntityRecord); - byte[] compoundSchemaKey = createSchemaKey(compoundSchemaReference); - - ingestData( - rideMerchantFeatureTableName, - compoundCassandraTableName, - compoundEntityFeatureKey, - compoundEntityFeatureValue, - compoundSchemaKey); - - /** Schema Ingestion Workflow */ - cqlSession.execute( - String.format( - "CREATE TABLE %s.%s (schema_ref BLOB PRIMARY KEY, avro_schema BLOB);", - CASSANDRA_KEYSPACE, CASSANDRA_SCHEMA_TABLE)); - - ingestSchema(schemaKey, ftSchema); - ingestSchema(foodSchemaKey, foodFtSchema); - ingestSchema(compoundSchemaKey, compoundFtSchema); - - // 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); - } - - private static byte[] createSchemaKey(byte[] schemaReference) throws IOException { - ByteArrayOutputStream concatOutputStream = new ByteArrayOutputStream(); - concatOutputStream.write(schemaReference); - byte[] schemaKey = concatOutputStream.toByteArray(); - - return schemaKey; - } - - private static byte[] createEntityValue(Schema schema, GenericRecord record) throws IOException { - // Entity-Feature Row - byte[] avroSerializedFeatures = recordToAvro(record, schema); - - ByteArrayOutputStream concatOutputStream = new ByteArrayOutputStream(); - concatOutputStream.write(avroSerializedFeatures); - byte[] entityFeatureValue = concatOutputStream.toByteArray(); - - return entityFeatureValue; - } - - private static void createCassandraTable(String cassandraTableName) { - cqlSession.execute( - String.format( - "CREATE TABLE %s.%s (key BLOB PRIMARY KEY);", CASSANDRA_KEYSPACE, cassandraTableName)); - } - - private static void addCassandraTableColumn(String cassandraTableName, String featureTableName) { - cqlSession.execute( - String.format( - "ALTER TABLE %s.%s ADD (%s BLOB, %s__schema_ref BLOB);", - CASSANDRA_KEYSPACE, cassandraTableName, featureTableName, featureTableName)); - } - - private static void ingestData( - String featureTableName, - String cassandraTableName, - byte[] entityFeatureKey, - byte[] entityFeatureValue, - byte[] schemaKey) { - PreparedStatement statement = - cqlSession.prepare( - String.format( - "INSERT INTO %s.%s (%s, %s__schema_ref, %s) VALUES (?, ?, ?)", - CASSANDRA_KEYSPACE, - cassandraTableName, - CASSANDRA_ENTITY_KEY, - featureTableName, - featureTableName)); - - cqlSession.execute( - statement.bind( - ByteBuffer.wrap(entityFeatureKey), - ByteBuffer.wrap(schemaKey), - ByteBuffer.wrap(entityFeatureValue))); - } - - private static void ingestBulk( - String featureTableName, String cassandraTableName, Schema schema, Integer counts) { - - IntStream.range(0, counts) - .forEach( - i -> { - try { - GenericRecord record = - new GenericRecordBuilder(schema) - .set("trip_cost", i) - .set("trip_distance", (double) i) - .set("trip_empty", null) - .set("trip_wrong_type", "test") - .build(); - byte[] schemaReference = - Hashing.murmur3_32().hashBytes(schema.toString().getBytes()).asBytes(); - - byte[] entityFeatureKey = - String.valueOf(DataGenerator.createInt64Value(i).getInt64Val()).getBytes(); - byte[] entityFeatureValue = createEntityValue(schema, record); - - byte[] schemaKey = createSchemaKey(schemaReference); - ingestData( - featureTableName, - cassandraTableName, - entityFeatureKey, - entityFeatureValue, - schemaKey); - } catch (IOException e) { - e.printStackTrace(); - } - }); - } - - private static void ingestSchema(byte[] schemaKey, Schema schema) { - PreparedStatement schemaStatement = - cqlSession.prepare( - String.format( - "INSERT INTO %s.%s (schema_ref, avro_schema) VALUES (?, ?);", - CASSANDRA_KEYSPACE, CASSANDRA_SCHEMA_TABLE)); - cqlSession.execute( - schemaStatement.bind( - ByteBuffer.wrap(schemaKey), ByteBuffer.wrap(schema.toString().getBytes()))); - } - - private static byte[] recordToAvro(GenericRecord datum, Schema schema) throws IOException { - GenericDatumWriter writer = new GenericDatumWriter<>(schema); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Encoder encoder = EncoderFactory.get().binaryEncoder(output, null); - writer.write(datum, encoder); - encoder.flush(); - - return output.toByteArray(); - } - - @AfterAll - static void tearDown() { - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } - - @Test - public void shouldRegisterSingleEntityAndGetOnlineFeatures() { - String projectName = "default"; - String entityName = "driver_id"; - ValueProto.Value entityValue = DataGenerator.createInt64Value(1); - - // Instantiate EntityRows - GetOnlineFeaturesRequestV2.EntityRow entityRow = - DataGenerator.createEntityRow(entityName, entityValue, 100); - ImmutableList entityRows = ImmutableList.of(entityRow); - - // Instantiate FeatureReferences - FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - FeatureReferenceV2 notFoundFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_transaction"); - - ImmutableList featureReferences = - ImmutableList.of(featureReference, notFoundFeatureReference); - - // Build GetOnlineFeaturesRequestV2 - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - - ImmutableMap expectedValueMap = - ImmutableMap.of( - entityName, - entityValue, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt32Value(1), - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - DataGenerator.createEmptyValue()); - - ImmutableMap expectedStatusMap = - ImmutableMap.of( - entityName, - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(featureReference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } - - @Test - public void shouldRegisterCompoundEntityAndGetOnlineFeatures() { - String projectName = "default"; - String driverEntityName = "driver_id"; - String merchantEntityName = "merchant_id"; - ValueProto.Value driverEntityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); - ValueProto.Value merchantEntityValue = ValueProto.Value.newBuilder().setInt64Val(1234).build(); - - ImmutableMap compoundEntityMap = - ImmutableMap.of( - driverEntityName, driverEntityValue, merchantEntityName, merchantEntityValue); - - // Instantiate EntityRows - GetOnlineFeaturesRequestV2.EntityRow entityRow = - DataGenerator.createCompoundEntityRow(compoundEntityMap, 100); - ImmutableList entityRows = ImmutableList.of(entityRow); - - // Instantiate FeatureReferences - FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - FeatureReferenceV2 notFoundFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_transaction"); - - ImmutableList featureReferences = - ImmutableList.of(featureReference, notFoundFeatureReference); - - // Build GetOnlineFeaturesRequestV2 - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - - ImmutableMap expectedValueMap = - ImmutableMap.of( - driverEntityName, - driverEntityValue, - merchantEntityName, - merchantEntityValue, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt32Value(1), - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - DataGenerator.createEmptyValue()); - - ImmutableMap expectedStatusMap = - ImmutableMap.of( - driverEntityName, - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - merchantEntityName, - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(featureReference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - GetOnlineFeaturesResponse.FieldStatus.NOT_FOUND); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } - - @Test - public void shouldReturnCorrectRowCountAndOrder() { - // getOnlineFeatures Information - String projectName = "default"; - String entityName = "driver_id"; - ValueProto.Value entityValue1 = ValueProto.Value.newBuilder().setInt64Val(1).build(); - ValueProto.Value entityValue2 = ValueProto.Value.newBuilder().setInt64Val(2).build(); - ValueProto.Value entityValue3 = ValueProto.Value.newBuilder().setInt64Val(3).build(); - ValueProto.Value entityValue4 = ValueProto.Value.newBuilder().setInt64Val(4).build(); - - // Instantiate EntityRows - GetOnlineFeaturesRequestV2.EntityRow entityRow1 = - DataGenerator.createEntityRow(entityName, entityValue1, 100); - GetOnlineFeaturesRequestV2.EntityRow entityRow2 = - DataGenerator.createEntityRow(entityName, entityValue2, 100); - GetOnlineFeaturesRequestV2.EntityRow entityRow3 = - DataGenerator.createEntityRow(entityName, entityValue3, 100); - GetOnlineFeaturesRequestV2.EntityRow entityRow4 = - DataGenerator.createEntityRow(entityName, entityValue4, 100); - ImmutableList entityRows = - ImmutableList.of(entityRow1, entityRow2, entityRow4, entityRow3); - - // Instantiate FeatureReferences - FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - FeatureReferenceV2 notFoundFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_transaction"); - FeatureReferenceV2 emptyFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_empty"); - - ImmutableList featureReferences = - ImmutableList.of(featureReference, notFoundFeatureReference, emptyFeatureReference); - - // Build GetOnlineFeaturesRequestV2 - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - - ImmutableMap expectedValueMap = - ImmutableMap.of( - entityName, - entityValue1, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt32Value(1), - 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() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - - ImmutableMap expectedValueMap2 = - ImmutableMap.of( - entityName, - entityValue2, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt32Value(2), - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - DataGenerator.createEmptyValue(), - FeatureV2.getFeatureStringRef(emptyFeatureReference), - DataGenerator.createEmptyValue()); - - ImmutableMap expectedValueMap3 = - ImmutableMap.of( - entityName, - entityValue3, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt32Value(3), - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - DataGenerator.createEmptyValue(), - FeatureV2.getFeatureStringRef(emptyFeatureReference), - DataGenerator.createEmptyValue()); - - ImmutableMap expectedValueMap4 = - ImmutableMap.of( - entityName, - entityValue4, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt32Value(4), - FeatureV2.getFeatureStringRef(notFoundFeatureReference), - DataGenerator.createEmptyValue(), - FeatureV2.getFeatureStringRef(emptyFeatureReference), - DataGenerator.createEmptyValue()); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues2 = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap2) - .putAllStatuses(expectedStatusMap) - .build(); - GetOnlineFeaturesResponse.FieldValues expectedFieldValues3 = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap3) - .putAllStatuses(expectedStatusMap) - .build(); - GetOnlineFeaturesResponse.FieldValues expectedFieldValues4 = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap4) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of( - expectedFieldValues, expectedFieldValues2, expectedFieldValues4, expectedFieldValues3); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } - - @Test - public void shouldReturnFeaturesFromDiffFeatureTable() { - String projectName = "default"; - String entityName = "driver_id"; - ValueProto.Value entityValue = DataGenerator.createInt64Value(1); - - // Instantiate EntityRows - GetOnlineFeaturesRequestV2.EntityRow entityRow = - DataGenerator.createEntityRow(entityName, entityValue, 100); - ImmutableList entityRows = ImmutableList.of(entityRow); - - // Instantiate FeatureReferences - FeatureReferenceV2 rideFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - FeatureReferenceV2 rideFeatureReference2 = - DataGenerator.createFeatureReference("rides", "trip_distance"); - FeatureReferenceV2 foodFeatureReference = - DataGenerator.createFeatureReference("food", "trip_cost"); - FeatureReferenceV2 foodFeatureReference2 = - DataGenerator.createFeatureReference("food", "trip_distance"); - - ImmutableList featureReferences = - ImmutableList.of( - rideFeatureReference, - rideFeatureReference2, - foodFeatureReference, - foodFeatureReference2); - - // Build GetOnlineFeaturesRequestV2 - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - - ImmutableMap expectedValueMap = - ImmutableMap.of( - entityName, - entityValue, - FeatureV2.getFeatureStringRef(rideFeatureReference), - DataGenerator.createInt32Value(1), - FeatureV2.getFeatureStringRef(rideFeatureReference2), - DataGenerator.createDoubleValue(1.0), - FeatureV2.getFeatureStringRef(foodFeatureReference), - DataGenerator.createInt32Value(1), - FeatureV2.getFeatureStringRef(foodFeatureReference2), - DataGenerator.createDoubleValue(1.0)); - - ImmutableMap expectedStatusMap = - ImmutableMap.of( - entityName, - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(rideFeatureReference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(rideFeatureReference2), - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(foodFeatureReference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(foodFeatureReference2), - GetOnlineFeaturesResponse.FieldStatus.PRESENT); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } -} diff --git a/serving/src/test/java/feast/serving/it/ServingServiceIT.java b/serving/src/test/java/feast/serving/it/ServingServiceIT.java deleted file mode 100644 index 8e0a82e..0000000 --- a/serving/src/test/java/feast/serving/it/ServingServiceIT.java +++ /dev/null @@ -1,497 +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.serving.it; - -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; -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; -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 io.grpc.ManagedChannel; -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 org.junit.ClassRule; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -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; -import org.testcontainers.junit.jupiter.Testcontainers; - -@ActiveProfiles("it") -@SpringBootTest( - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = { - "feast.core-cache-refresh-interval=1", - }) -@Testcontainers -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; - - static final int FEAST_SERVING_PORT = 6568; - @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(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); - servingStub = TestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - - 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"; - ImmutableList entities = ImmutableList.of(entityName); - - ServingAPIProto.FeatureReferenceV2 feature1Reference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - ServingAPIProto.FeatureReferenceV2 feature2Reference = - DataGenerator.createFeatureReference("rides", "trip_distance"); - ServingAPIProto.FeatureReferenceV2 feature3Reference = - DataGenerator.createFeatureReference("rides", "trip_empty"); - ServingAPIProto.FeatureReferenceV2 feature4Reference = - DataGenerator.createFeatureReference("rides", "trip_wrong_type"); - - // Event Timestamp - String eventTimestampKey = timestampPrefix + ":" + featureTableName; - Timestamp eventTimestampValue = Timestamp.newBuilder().setSeconds(100).build(); - - ImmutableMap features = - ImmutableMap.of( - "trip_cost", - ValueProto.ValueType.Enum.INT64, - "trip_distance", - ValueProto.ValueType.Enum.DOUBLE, - "trip_empty", - ValueProto.ValueType.Enum.DOUBLE, - "trip_wrong_type", - ValueProto.ValueType.Enum.STRING); - - TestUtils.applyFeatureTable( - coreClient, projectName, featureTableName, entities, features, 7200); - - // Serialize Redis Key with Entity i.e - RedisProto.RedisKeyV2 redisKey = - RedisProto.RedisKeyV2.newBuilder() - .setProject(projectName) - .addEntityNames(entityName) - .addEntityValues(entityValue) - .build(); - - ImmutableMap featureReferenceValueMap = - ImmutableMap.of( - feature1Reference, - DataGenerator.createInt64Value(42), - feature2Reference, - DataGenerator.createDoubleValue(42.2), - feature3Reference, - DataGenerator.createEmptyValue(), - feature4Reference, - DataGenerator.createDoubleValue(42.2)); - - // Insert timestamp into Redis and isTimestampMap only once - syncCommands.hset( - redisKey.toByteArray(), eventTimestampKey.getBytes(), eventTimestampValue.toByteArray()); - featureReferenceValueMap.forEach( - (featureReference, featureValue) -> { - // Murmur hash Redis Feature Field i.e murmur() - 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()); - }); - - // 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); - } - - @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 { - 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 shouldRegisterAndGetOnlineFeatures() { - // getOnlineFeatures Information - 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), 100); - ImmutableList entityRows = ImmutableList.of(entityRow1); - - // Instantiate FeatureReferences - ServingAPIProto.FeatureReferenceV2 feature1Reference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - ImmutableList featureReferences = - ImmutableList.of(feature1Reference); - - // Build GetOnlineFeaturesRequestV2 - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - - ImmutableMap expectedValueMap = - ImmutableMap.of( - entityName, - entityValue, - FeatureV2.getFeatureStringRef(feature1Reference), - DataGenerator.createInt64Value(42)); - - ImmutableMap expectedStatusMap = - ImmutableMap.of( - entityName, - GetOnlineFeaturesResponse.FieldStatus.PRESENT, - FeatureV2.getFeatureStringRef(feature1Reference), - GetOnlineFeaturesResponse.FieldStatus.PRESENT); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } - - @Test - public void shouldRegisterAndGetOnlineFeaturesWithNotFound() { - // getOnlineFeatures Information - 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), 100); - ImmutableList entityRows = ImmutableList.of(entityRow1); - - // Instantiate FeatureReferences - ServingAPIProto.FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - ServingAPIProto.FeatureReferenceV2 notFoundFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_transaction"); - ServingAPIProto.FeatureReferenceV2 emptyFeatureReference = - DataGenerator.createFeatureReference("rides", "trip_empty"); - - ImmutableList featureReferences = - ImmutableList.of(featureReference, notFoundFeatureReference, emptyFeatureReference); - - // Build GetOnlineFeaturesRequestV2 - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - TestUtils.createOnlineFeatureRequest(projectName, featureReferences, entityRows); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - - ImmutableMap expectedValueMap = - ImmutableMap.of( - entityName, - entityValue, - FeatureV2.getFeatureStringRef(featureReference), - DataGenerator.createInt64Value(42), - 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.NOT_FOUND); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - 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); - ImmutableList entityRows = ImmutableList.of(entityRow1); - - // Instantiate FeatureReferences - ServingAPIProto.FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_cost"); - - ImmutableList featureReferences = - ImmutableList.of(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(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } - - @Test - public void shouldReturnNotFoundForDiffType() { - 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), 100); - ImmutableList entityRows = ImmutableList.of(entityRow1); - - // Instantiate FeatureReferences - ServingAPIProto.FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_wrong_type"); - - ImmutableList featureReferences = - ImmutableList.of(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.NOT_FOUND); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } - - @Test - public void shouldReturnNotFoundForUpdatedType() { - String projectName = "default"; - String entityName = "driver_id"; - String featureTableName = "rides"; - - ImmutableList entities = ImmutableList.of(entityName); - ImmutableMap features = - ImmutableMap.of( - "trip_cost", - ValueProto.ValueType.Enum.INT64, - "trip_distance", - ValueProto.ValueType.Enum.STRING, - "trip_empty", - ValueProto.ValueType.Enum.DOUBLE, - "trip_wrong_type", - ValueProto.ValueType.Enum.STRING); - - TestUtils.applyFeatureTable( - coreClient, projectName, featureTableName, entities, features, 7200); - - // Sleep is necessary to ensure caching (every 1s) of updated FeatureTable is done - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - } - - ValueProto.Value entityValue = ValueProto.Value.newBuilder().setInt64Val(1).build(); - // Instantiate EntityRows - GetOnlineFeaturesRequestV2.EntityRow entityRow1 = - DataGenerator.createEntityRow(entityName, DataGenerator.createInt64Value(1), 100); - ImmutableList entityRows = ImmutableList.of(entityRow1); - - // Instantiate FeatureReferences - ServingAPIProto.FeatureReferenceV2 featureReference = - DataGenerator.createFeatureReference("rides", "trip_distance"); - - ImmutableList featureReferences = - ImmutableList.of(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.NOT_FOUND); - - GetOnlineFeaturesResponse.FieldValues expectedFieldValues = - GetOnlineFeaturesResponse.FieldValues.newBuilder() - .putAllFields(expectedValueMap) - .putAllStatuses(expectedStatusMap) - .build(); - ImmutableList expectedFieldValuesList = - ImmutableList.of(expectedFieldValues); - - assertEquals(expectedFieldValuesList, featureResponse.getFieldValuesList()); - } -} diff --git a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java b/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java deleted file mode 100644 index 8f2440d..0000000 --- a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthenticationIT.java +++ /dev/null @@ -1,190 +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.serving.it; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.testcontainers.containers.wait.strategy.Wait.forHttp; - -import com.google.common.collect.ImmutableMap; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import feast.common.it.DataGenerator; -import feast.proto.core.EntityProto; -import feast.proto.core.FeatureTableProto; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; -import feast.proto.serving.ServingServiceGrpc.ServingServiceBlockingStub; -import feast.proto.types.ValueProto; -import feast.proto.types.ValueProto.Value; -import io.grpc.ManagedChannel; -import java.io.File; -import java.io.IOException; -import java.time.Duration; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import org.junit.ClassRule; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.runners.model.InitializationError; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -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 = WebEnvironment.RANDOM_PORT, - properties = { - "feast.core-authentication.enabled=true", - "feast.core-authentication.provider=oauth", - "feast.security.authentication.enabled=true", - "feast.security.authorization.enabled=false" - }) -@Testcontainers -public class ServingServiceOauthAuthenticationIT extends BaseAuthIT { - - CoreSimpleAPIClient coreClient; - FeatureTableProto.FeatureTableSpec expectedFeatureTableSpec; - static final Map options = new HashMap<>(); - - 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-hydra.yml"), - new File("src/test/resources/docker-compose/docker-compose-it.yml")) - .withExposedService(HYDRA, HYDRA_PORT, forHttp("/health/alive").forStatusCode(200)) - .withExposedService( - CORE, - FEAST_CORE_PORT, - Wait.forLogMessage(".*gRPC Server started.*\\n", 1) - .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))); - - @BeforeAll - static void globalSetup() throws IOException, InitializationError, InterruptedException { - String hydraExternalHost = environment.getServiceHost(HYDRA, HYDRA_PORT); - Integer hydraExternalPort = environment.getServicePort(HYDRA, HYDRA_PORT); - String hydraExternalUrl = String.format("http://%s:%s", hydraExternalHost, hydraExternalPort); - AuthTestUtils.seedHydra(hydraExternalUrl, CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); - - // 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); - } - - @BeforeEach - public void initState() { - coreClient = AuthTestUtils.getSecureApiClientForCore(FEAST_CORE_PORT, options); - EntityProto.EntitySpecV2 entitySpec = - DataGenerator.createEntitySpecV2( - ENTITY_ID, - "Entity 1 description", - ValueProto.ValueType.Enum.STRING, - ImmutableMap.of("label_key", "label_value")); - coreClient.simpleApplyEntity(PROJECT_NAME, entitySpec); - - expectedFeatureTableSpec = - DataGenerator.createFeatureTableSpec( - FEATURE_TABLE_NAME, - Arrays.asList(ENTITY_ID), - new HashMap<>() { - { - put(FEATURE_NAME, ValueProto.ValueType.Enum.STRING); - } - }, - 7200, - ImmutableMap.of("feat_key2", "feat_value2")) - .toBuilder() - .setBatchSource( - DataGenerator.createFileDataSourceSpec("file:///path/to/file", "ts_col", "")) - .build(); - coreClient.simpleApplyFeatureTable(PROJECT_NAME, expectedFeatureTableSpec); - } - - /** 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() { - FeatureTableProto.FeatureTable actualFeatureTable = - coreClient.simpleGetFeatureTable(PROJECT_NAME, FEATURE_TABLE_NAME); - assertEquals(expectedFeatureTableSpec.getName(), actualFeatureTable.getSpec().getName()); - assertEquals( - expectedFeatureTableSpec.getBatchSource(), actualFeatureTable.getSpec().getBatchSource()); - - ServingServiceBlockingStub servingStub = - AuthTestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - GetOnlineFeaturesRequestV2 onlineFeatureRequestV2 = - AuthTestUtils.createOnlineFeatureRequest( - PROJECT_NAME, FEATURE_TABLE_NAME, FEATURE_NAME, ENTITY_ID, 1); - GetOnlineFeaturesResponse featureResponse = - servingStub.getOnlineFeaturesV2(onlineFeatureRequestV2); - - assertEquals(1, featureResponse.getFieldValuesCount()); - Map fieldsMap = featureResponse.getFieldValues(0).getFieldsMap(); - assertTrue(fieldsMap.containsKey(ENTITY_ID)); - assertTrue(fieldsMap.containsKey(FEATURE_TABLE_NAME + ":" + FEATURE_NAME)); - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } - - @Test - void canGetOnlineFeaturesIfAuthenticated() { - FeatureTableProto.FeatureTable actualFeatureTable = - coreClient.simpleGetFeatureTable(PROJECT_NAME, FEATURE_TABLE_NAME); - assertEquals(expectedFeatureTableSpec.getName(), actualFeatureTable.getSpec().getName()); - assertEquals( - expectedFeatureTableSpec.getBatchSource(), actualFeatureTable.getSpec().getBatchSource()); - - ServingServiceBlockingStub servingStub = - AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, options); - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - AuthTestUtils.createOnlineFeatureRequest( - PROJECT_NAME, FEATURE_TABLE_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_TABLE_NAME + ":" + FEATURE_NAME)); - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } -} diff --git a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthorizationIT.java b/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthorizationIT.java deleted file mode 100644 index 64fe44b..0000000 --- a/serving/src/test/java/feast/serving/it/ServingServiceOauthAuthorizationIT.java +++ /dev/null @@ -1,227 +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.serving.it; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.testcontainers.containers.wait.strategy.Wait.forHttp; - -import feast.common.it.DataGenerator; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequestV2; -import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; -import feast.proto.serving.ServingServiceGrpc.ServingServiceBlockingStub; -import feast.proto.types.ValueProto; -import feast.proto.types.ValueProto.Value; -import io.grpc.ManagedChannel; -import io.grpc.StatusRuntimeException; -import java.io.File; -import java.io.IOException; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -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.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; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.shaded.com.google.common.collect.ImmutableList; -import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; -import sh.ory.keto.ApiException; - -@ActiveProfiles("it") -@SpringBootTest( - properties = { - "feast.core-authentication.enabled=true", - "feast.core-authentication.provider=oauth", - "feast.security.authentication.enabled=true", - "feast.security.authorization.enabled=true" - }) -@Testcontainers -public class ServingServiceOauthAuthorizationIT extends BaseAuthIT { - - static final Map adminCredentials = new HashMap<>(); - static final Map memberCredentials = new HashMap<>(); - static final String PROJECT_MEMBER_CLIENT_ID = "client_id_1"; - static final String NOT_PROJECT_MEMBER_CLIENT_ID = "client_id_2"; - private static int KETO_PORT = 4466; - private static int KETO_ADAPTOR_PORT = 8080; - static String subjectClaim = "sub"; - static CoreSimpleAPIClient coreClient; - static final int FEAST_SERVING_PORT = 6766; - - @ClassRule @Container - public static DockerComposeContainer environment = - new DockerComposeContainer( - new File("src/test/resources/docker-compose/docker-compose-it-hydra.yml"), - new File("src/test/resources/docker-compose/docker-compose-it.yml"), - new File("src/test/resources/docker-compose/docker-compose-it-keto.yml")) - .withExposedService(HYDRA, HYDRA_PORT, forHttp("/health/alive").forStatusCode(200)) - .withExposedService( - CORE, - FEAST_CORE_PORT, - Wait.forLogMessage(".*gRPC Server started.*\\n", 1) - .withStartupTimeout(Duration.ofMinutes(SERVICE_START_MAX_WAIT_TIME_IN_MINUTES))) - .withExposedService("adaptor_1", KETO_ADAPTOR_PORT) - .withExposedService("keto_1", KETO_PORT, forHttp("/health/ready").forStatusCode(200)); - - @DynamicPropertySource - static void initialize(DynamicPropertyRegistry registry) { - - // Seed Keto with data - String ketoExternalHost = environment.getServiceHost("keto_1", KETO_PORT); - Integer ketoExternalPort = environment.getServicePort("keto_1", KETO_PORT); - String ketoExternalUrl = String.format("http://%s:%s", ketoExternalHost, ketoExternalPort); - try { - AuthTestUtils.seedKeto(ketoExternalUrl, PROJECT_NAME, PROJECT_MEMBER_CLIENT_ID, CLIENT_ID); - } catch (ApiException e) { - throw new RuntimeException(String.format("Could not seed Keto store %s", ketoExternalUrl)); - } - - // Get Keto Authorization Server (Adaptor) url - String ketoAdaptorHost = environment.getServiceHost("adaptor_1", KETO_ADAPTOR_PORT); - Integer ketoAdaptorPort = environment.getServicePort("adaptor_1", KETO_ADAPTOR_PORT); - String ketoAdaptorUrl = String.format("http://%s:%s", ketoAdaptorHost, ketoAdaptorPort); - - // Initialize dynamic properties - registry.add("feast.security.authentication.options.subjectClaim", () -> subjectClaim); - registry.add("feast.security.authentication.options.jwkEndpointURI", () -> JWK_URI); - registry.add("feast.security.authorization.options.authorizationUrl", () -> ketoAdaptorUrl); - registry.add("grpc.server.port", () -> FEAST_SERVING_PORT); - } - - @BeforeAll - static void globalSetup() throws IOException, InitializationError, InterruptedException { - String hydraExternalHost = environment.getServiceHost(HYDRA, HYDRA_PORT); - Integer hydraExternalPort = environment.getServicePort(HYDRA, HYDRA_PORT); - String hydraExternalUrl = String.format("http://%s:%s", hydraExternalHost, hydraExternalPort); - AuthTestUtils.seedHydra(hydraExternalUrl, CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); - AuthTestUtils.seedHydra( - hydraExternalUrl, PROJECT_MEMBER_CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); - AuthTestUtils.seedHydra( - hydraExternalUrl, NOT_PROJECT_MEMBER_CLIENT_ID, CLIENT_SECRET, AUDIENCE, GRANT_TYPE); - // set up options for call credentials - adminCredentials.put("oauth_url", TOKEN_URL); - adminCredentials.put(CLIENT_ID, CLIENT_ID); - adminCredentials.put(CLIENT_SECRET, CLIENT_SECRET); - adminCredentials.put("jwkEndpointURI", JWK_URI); - adminCredentials.put("audience", AUDIENCE); - adminCredentials.put("grant_type", GRANT_TYPE); - - coreClient = AuthTestUtils.getSecureApiClientForCore(FEAST_CORE_PORT, adminCredentials); - coreClient.simpleApplyEntity( - PROJECT_NAME, - DataGenerator.createEntitySpecV2( - ENTITY_ID, "", ValueProto.ValueType.Enum.STRING, Collections.emptyMap())); - coreClient.simpleApplyFeatureTable( - PROJECT_NAME, - DataGenerator.createFeatureTableSpec( - FEATURE_TABLE_NAME, - ImmutableList.of(ENTITY_ID), - ImmutableMap.of(FEATURE_NAME, ValueProto.ValueType.Enum.STRING), - 0, - Collections.emptyMap())); - } - - @Test - public void shouldNotAllowUnauthenticatedGetOnlineFeatures() { - ServingServiceBlockingStub servingStub = - AuthTestUtils.getServingServiceStub(false, FEAST_SERVING_PORT, null); - - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - AuthTestUtils.createOnlineFeatureRequest( - PROJECT_NAME, FEATURE_TABLE_NAME, FEATURE_NAME, ENTITY_ID, 1); - Exception exception = - assertThrows( - StatusRuntimeException.class, - () -> { - servingStub.getOnlineFeaturesV2(onlineFeatureRequest); - }); - - String expectedMessage = "UNAUTHENTICATED: Authentication failed"; - String actualMessage = exception.getMessage(); - assertEquals(actualMessage, expectedMessage); - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } - - @Test - void canGetOnlineFeaturesIfAdmin() { - ServingServiceBlockingStub servingStub = - AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, adminCredentials); - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - AuthTestUtils.createOnlineFeatureRequest( - PROJECT_NAME, FEATURE_TABLE_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_TABLE_NAME + ":" + FEATURE_NAME)); - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } - - @Test - void canGetOnlineFeaturesIfProjectMember() { - Map memberCredsOptions = new HashMap<>(); - memberCredsOptions.putAll(adminCredentials); - memberCredsOptions.put(CLIENT_ID, PROJECT_MEMBER_CLIENT_ID); - ServingServiceBlockingStub servingStub = - AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, memberCredsOptions); - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - AuthTestUtils.createOnlineFeatureRequest( - PROJECT_NAME, FEATURE_TABLE_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_TABLE_NAME + ":" + FEATURE_NAME)); - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } - - @Test - void cantGetOnlineFeaturesIfNotProjectMember() { - Map notMemberCredsOptions = new HashMap<>(); - notMemberCredsOptions.putAll(adminCredentials); - notMemberCredsOptions.put(CLIENT_ID, NOT_PROJECT_MEMBER_CLIENT_ID); - ServingServiceBlockingStub servingStub = - AuthTestUtils.getServingServiceStub(true, FEAST_SERVING_PORT, notMemberCredsOptions); - GetOnlineFeaturesRequestV2 onlineFeatureRequest = - AuthTestUtils.createOnlineFeatureRequest( - PROJECT_NAME, FEATURE_TABLE_NAME, FEATURE_NAME, ENTITY_ID, 1); - StatusRuntimeException exception = - assertThrows( - StatusRuntimeException.class, - () -> servingStub.getOnlineFeaturesV2(onlineFeatureRequest)); - - String expectedMessage = - String.format( - "PERMISSION_DENIED: Access denied to project %s for subject %s", - PROJECT_NAME, NOT_PROJECT_MEMBER_CLIENT_ID); - String actualMessage = exception.getMessage(); - assertEquals(actualMessage, expectedMessage); - ((ManagedChannel) servingStub.getChannel()).shutdown(); - } -} diff --git a/serving/src/test/java/feast/serving/service/OnlineServingServiceTest.java b/serving/src/test/java/feast/serving/service/OnlineServingServiceTest.java index 83dbdf0..d3e62b4 100644 --- a/serving/src/test/java/feast/serving/service/OnlineServingServiceTest.java +++ b/serving/src/test/java/feast/serving/service/OnlineServingServiceTest.java @@ -34,6 +34,7 @@ import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse.FieldValues; import feast.proto.types.ValueProto; import feast.serving.specs.CachedSpecService; +import feast.serving.specs.CoreFeatureSpecRetriever; import feast.storage.api.retriever.Feature; import feast.storage.api.retriever.ProtoFeature; import feast.storage.connectors.redis.retriever.OnlineRetriever; @@ -52,6 +53,7 @@ public class OnlineServingServiceTest { @Mock CachedSpecService specService; @Mock Tracer tracer; @Mock OnlineRetriever retrieverV2; + private String transformationServiceEndpoint; private OnlineServingServiceV2 onlineServingServiceV2; @@ -61,7 +63,12 @@ public class OnlineServingServiceTest { @Before public void setUp() { initMocks(this); - onlineServingServiceV2 = new OnlineServingServiceV2(retrieverV2, specService, tracer); + CoreFeatureSpecRetriever coreFeatureSpecRetriever = new CoreFeatureSpecRetriever(specService); + OnlineTransformationService onlineTransformationService = + new OnlineTransformationService(transformationServiceEndpoint, coreFeatureSpecRetriever); + onlineServingServiceV2 = + new OnlineServingServiceV2( + retrieverV2, tracer, coreFeatureSpecRetriever, onlineTransformationService); mockedFeatureRows = new ArrayList<>(); mockedFeatureRows.add( diff --git a/serving/src/test/resources/docker-compose/docker-compose-feast10-it.yml b/serving/src/test/resources/docker-compose/docker-compose-feast10-it.yml new file mode 100644 index 0000000..33d65f4 --- /dev/null +++ b/serving/src/test/resources/docker-compose/docker-compose-feast10-it.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + db: + image: postgres:12-alpine + environment: + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + redis: + image: redis:5-alpine + ports: + - "6379:6379" + materialize: + build: feast10 + links: + - redis + diff --git a/serving/src/test/resources/docker-compose/feast10/Dockerfile b/serving/src/test/resources/docker-compose/feast10/Dockerfile new file mode 100644 index 0000000..bde9f11 --- /dev/null +++ b/serving/src/test/resources/docker-compose/feast10/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.7 + +WORKDIR /usr/src/ + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD [ "python", "./materialize.py" ] diff --git a/serving/src/test/resources/docker-compose/feast10/driver_stats.parquet b/serving/src/test/resources/docker-compose/feast10/driver_stats.parquet new file mode 100644 index 0000000..df8cbba Binary files /dev/null and b/serving/src/test/resources/docker-compose/feast10/driver_stats.parquet differ diff --git a/serving/src/test/resources/docker-compose/feast10/feature_store.yaml b/serving/src/test/resources/docker-compose/feast10/feature_store.yaml new file mode 100644 index 0000000..87e3310 --- /dev/null +++ b/serving/src/test/resources/docker-compose/feast10/feature_store.yaml @@ -0,0 +1,9 @@ +project: feast_project +provider: local +online_store: + type: redis + connection_string: "redis:6379" +offline_store: {} +flags: + alpha_features: true + on_demand_transforms: true diff --git a/serving/src/test/resources/docker-compose/feast10/materialize.py b/serving/src/test/resources/docker-compose/feast10/materialize.py new file mode 100644 index 0000000..6338c16 --- /dev/null +++ b/serving/src/test/resources/docker-compose/feast10/materialize.py @@ -0,0 +1,45 @@ +# This is an example feature definition file + +from google.protobuf.duration_pb2 import Duration + +from datetime import datetime +from feast import Entity, Feature, FeatureView, FileSource, ValueType, FeatureService, FeatureStore + +print("Running materialize.py") + +# Read data from parquet files. Parquet is convenient for local development mode. For +# production, you can use your favorite DWH, such as BigQuery. See Feast documentation +# for more info. +file_path = "driver_stats.parquet" +driver_hourly_stats = FileSource( + path=file_path, + event_timestamp_column="event_timestamp", + created_timestamp_column="created", +) + +# Define an entity for the driver. You can think of entity as a primary key used to +# fetch features. +driver = Entity(name="driver_id", value_type=ValueType.INT64, description="driver id",) + +# Our parquet files contain sample data that includes a driver_id column, timestamps and +# three feature column. Here we define a Feature View that will allow us to serve this +# data to our model online. +driver_hourly_stats_view = FeatureView( + name="driver_hourly_stats", + entities=["driver_id"], + ttl=Duration(seconds=86400 * 365), + features=[ + Feature(name="conv_rate", dtype=ValueType.DOUBLE), + Feature(name="acc_rate", dtype=ValueType.FLOAT), + Feature(name="avg_daily_trips", dtype=ValueType.INT64), + ], + online=True, + batch_source=driver_hourly_stats, + tags={}, +) + +fs = FeatureStore(".") +fs.apply([driver_hourly_stats_view, driver]) + +now = datetime.now() +fs.materialize_incremental(now) diff --git a/serving/src/test/resources/docker-compose/feast10/registry.db b/serving/src/test/resources/docker-compose/feast10/registry.db new file mode 100644 index 0000000..774b493 Binary files /dev/null and b/serving/src/test/resources/docker-compose/feast10/registry.db differ diff --git a/serving/src/test/resources/docker-compose/feast10/requirements.txt b/serving/src/test/resources/docker-compose/feast10/requirements.txt new file mode 100644 index 0000000..cb579a2 --- /dev/null +++ b/serving/src/test/resources/docker-compose/feast10/requirements.txt @@ -0,0 +1 @@ +feast[redis]>=0.13,<1 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 index 35b3321..a49ab3f 100644 --- a/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetrieverV2.java +++ b/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetrieverV2.java @@ -33,6 +33,7 @@ public interface OnlineRetrieverV2 { * @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 + * @param entityNames name of entities * @return list of {@link Feature}s corresponding to data retrieved for each entity row from * FeatureTable specified in FeatureTable request. */ 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 f78e22d..e24b3bd 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 @@ -34,12 +34,13 @@ public class RedisHashDecoder { * * @param redisHashValues retrieved Redis Hash values based on EntityRows * @param byteToFeatureReferenceMap map to decode bytes back to FeatureReference + * @param timestampPrefix timestamp prefix * @return List of {@link Feature} - * @throws InvalidProtocolBufferException + * @throws InvalidProtocolBufferException if a protocol buffer exception occurs */ public static List retrieveFeature( List> redisHashValues, - Map byteToFeatureReferenceMap, + Map byteToFeatureReferenceMap, String timestampPrefix) throws InvalidProtocolBufferException { List allFeatures = new ArrayList<>(); @@ -57,7 +58,7 @@ public static List retrieveFeature( featureTableTimestampMap.put(new String(redisValueK), eventTimestamp); } else { ServingAPIProto.FeatureReferenceV2 featureReference = - byteToFeatureReferenceMap.get(redisValueK.toString()); + byteToFeatureReferenceMap.get(redisValueK); ValueProto.Value featureValue = ValueProto.Value.parseFrom(redisValueV); featureMap.put(featureReference, featureValue); diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/EntityKeySerializer.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/EntityKeySerializer.java new file mode 100644 index 0000000..6220dd2 --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/EntityKeySerializer.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.proto.storage.RedisProto; + +@FunctionalInterface +public interface EntityKeySerializer { + byte[] serialize(final RedisProto.RedisKeyV2 entityKey); +} diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/EntityKeySerializerV2.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/EntityKeySerializerV2.java new file mode 100644 index 0000000..922a09d --- /dev/null +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/EntityKeySerializerV2.java @@ -0,0 +1,123 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2021 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.primitives.UnsignedBytes; +import com.google.protobuf.ProtocolStringList; +import feast.proto.storage.RedisProto; +import feast.proto.types.ValueProto; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; + +// This is derived from +// https://github.com/feast-dev/feast/blob/b1ccf8dd1535f721aee8bea937ee38feff80bec5/sdk/python/feast/infra/key_encoding_utils.py#L22 +// and must be kept up to date with any changes in that logic. +public class EntityKeySerializerV2 implements EntityKeySerializer { + + @Override + public byte[] serialize(RedisProto.RedisKeyV2 entityKey) { + final ProtocolStringList joinKeys = entityKey.getEntityNamesList(); + final List values = entityKey.getEntityValuesList(); + + assert joinKeys.size() == values.size(); + + final List buffer = new ArrayList<>(); + + final List> tuples = new ArrayList<>(joinKeys.size()); + for (int i = 0; i < joinKeys.size(); i++) { + tuples.add(Pair.of(joinKeys.get(i), values.get(i))); + } + tuples.sort(Comparator.comparing(Pair::getLeft)); + + ByteBuffer stringBytes = ByteBuffer.allocate(Integer.BYTES); + stringBytes.order(ByteOrder.LITTLE_ENDIAN); + stringBytes.putInt(ValueProto.ValueType.Enum.STRING.getNumber()); + + for (Pair pair : tuples) { + for (final byte b : stringBytes.array()) { + buffer.add(b); + } + for (final byte b : pair.getLeft().getBytes(StandardCharsets.UTF_8)) { + buffer.add(b); + } + } + + for (Pair pair : tuples) { + final ValueProto.Value val = pair.getRight(); + switch (val.getValCase()) { + case STRING_VAL: + buffer.add(UnsignedBytes.checkedCast(ValueProto.ValueType.Enum.STRING.getNumber())); + buffer.add( + UnsignedBytes.checkedCast( + val.getStringVal().getBytes(StandardCharsets.UTF_8).length)); + for (final byte b : val.getStringVal().getBytes(StandardCharsets.UTF_8)) { + buffer.add(b); + } + break; + case BYTES_VAL: + buffer.add(UnsignedBytes.checkedCast(ValueProto.ValueType.Enum.BYTES.getNumber())); + for (final byte b : val.getBytesVal().toByteArray()) { + buffer.add(b); + } + break; + case INT32_VAL: + ByteBuffer int32ByteBuffer = + ByteBuffer.allocate(Integer.BYTES + Integer.BYTES + Integer.BYTES); + int32ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + int32ByteBuffer.putInt(ValueProto.ValueType.Enum.INT32.getNumber()); + int32ByteBuffer.putInt(Integer.BYTES); + int32ByteBuffer.putInt(val.getInt32Val()); + for (final byte b : int32ByteBuffer.array()) { + buffer.add(b); + } + break; + case INT64_VAL: + ByteBuffer int64ByteBuffer = + ByteBuffer.allocate(Integer.BYTES + Integer.BYTES + Integer.BYTES); + int64ByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + int64ByteBuffer.putInt(ValueProto.ValueType.Enum.INT64.getNumber()); + int64ByteBuffer.putInt(Integer.BYTES); + /* This is super dumb - but in https://github.com/feast-dev/feast/blob/dcae1606f53028ce5413567fb8b66f92cfef0f8e/sdk/python/feast/infra/key_encoding_utils.py#L9 + we use `struct.pack("> getFeaturesFromRedis( 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 = - redisKeys.stream().map(redisKey -> redisKey.toByteArray()).collect(Collectors.toList()); + redisKeys.stream().map(this.keySerializer::serialize).collect(Collectors.toList()); List featureReferenceWithTsByteList = new ArrayList<>(); featureReferences.stream() @@ -73,7 +78,7 @@ private List> getFeaturesFromRedis( byte[] featureReferenceBytes = RedisHashDecoder.getFeatureReferenceRedisHashKeyBytes(featureReference); featureReferenceWithTsByteList.add(featureReferenceBytes); - byteToFeatureReferenceMap.put(featureReferenceBytes.toString(), featureReference); + byteToFeatureReferenceMap.put(featureReferenceBytes, featureReference); // eg. <_ts:featuretable_name> byte[] featureTableTsBytes = @@ -97,6 +102,7 @@ private List> getFeaturesFromRedis( future -> { try { List> redisValuesList = future.get(); + List curRedisKeyFeatures = RedisHashDecoder.retrieveFeature( redisValuesList, byteToFeatureReferenceMap, timestampPrefix); 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 faa8e96..e1699bb 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 @@ -52,6 +52,11 @@ public static RedisClientAdapter create(RedisStoreConfig config) { if (config.getSsl()) { uri.setSsl(true); } + + if (!config.getPassword().isEmpty()) { + uri.setPassword(config.getPassword()); + } + StatefulRedisConnection connection = io.lettuce.core.RedisClient.create(uri).connect(new ByteArrayCodec()); 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 5395b72..e5bad29 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 @@ -22,8 +22,17 @@ import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.core.cluster.api.async.RedisAdvancedClusterAsyncCommands; import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.resource.ClientResources; +import io.lettuce.core.resource.DnsResolvers; +import io.lettuce.core.resource.MappingSocketAddressResolver; +import io.lettuce.core.resource.NettyCustomizer; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.epoll.EpollChannelOption; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class RedisClusterClient implements RedisClientAdapter { @@ -62,18 +71,81 @@ private RedisClusterClient(Builder builder) { this.asyncCommands.setAutoFlushCommands(false); } + public static String getAddressString(String host) { + try { + return InetAddress.getByName(host).getHostAddress(); + } catch (UnknownHostException e) { + throw new RuntimeException(String.format("getAllByName() failed: %s", e.getMessage())); + } + } + + public static MappingSocketAddressResolver customSocketAddressResolver( + RedisClusterStoreConfig config) { + + List configuredHosts = + Arrays.stream(config.getConnectionString().split(",")) + .map( + hostPort -> { + return hostPort.trim().split(":")[0]; + }) + .collect(Collectors.toList()); + + Map mapAddressHost = + configuredHosts.stream() + .collect( + Collectors.toMap(host -> ((String) getAddressString(host)), host -> (String) host)); + + return MappingSocketAddressResolver.create( + DnsResolvers.UNRESOLVED, + hostAndPort -> + mapAddressHost.keySet().stream().anyMatch(i -> i.equals(hostAndPort.getHostText())) + ? hostAndPort.of( + mapAddressHost.get(hostAndPort.getHostText()), hostAndPort.getPort()) + : hostAndPort); + } + + public static ClientResources customClientResources(RedisClusterStoreConfig config) { + ClientResources clientResources = + ClientResources.builder() + .nettyCustomizer( + new NettyCustomizer() { + @Override + public void afterBootstrapInitialized(Bootstrap bootstrap) { + bootstrap.option(EpollChannelOption.TCP_KEEPIDLE, 15); + bootstrap.option(EpollChannelOption.TCP_KEEPINTVL, 5); + bootstrap.option(EpollChannelOption.TCP_KEEPCNT, 3); + // Socket Timeout (milliseconds) + bootstrap.option(EpollChannelOption.TCP_USER_TIMEOUT, 60000); + } + }) + .socketAddressResolver(customSocketAddressResolver(config)) + .build(); + return clientResources; + } + public static RedisClientAdapter create(RedisClusterStoreConfig config) { + List redisURIList = Arrays.stream(config.getConnectionString().split(",")) .map( hostPort -> { String[] hostPortSplit = hostPort.trim().split(":"); - return RedisURI.create(hostPortSplit[0], Integer.parseInt(hostPortSplit[1])); + RedisURI redisURI = + RedisURI.create(hostPortSplit[0], Integer.parseInt(hostPortSplit[1])); + if (!config.getPassword().isEmpty()) { + redisURI.setPassword(config.getPassword()); + } + if (config.getSsl()) { + redisURI.setSsl(true); + } + return redisURI; }) .collect(Collectors.toList()); io.lettuce.core.cluster.RedisClusterClient client = - io.lettuce.core.cluster.RedisClusterClient.create(redisURIList); + io.lettuce.core.cluster.RedisClusterClient.create( + customClientResources(config), redisURIList); + client.setOptions( ClusterClientOptions.builder() .socketOptions(SocketOptions.builder().keepAlive(true).tcpNoDelay(true).build()) diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterStoreConfig.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterStoreConfig.java index c179ffe..a7278bd 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterStoreConfig.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterStoreConfig.java @@ -23,11 +23,16 @@ public class RedisClusterStoreConfig { private final String connectionString; private final ReadFrom readFrom; private final Duration timeout; + private final String password; + private final Boolean ssl; - public RedisClusterStoreConfig(String connectionString, ReadFrom readFrom, Duration timeout) { + public RedisClusterStoreConfig( + String connectionString, ReadFrom readFrom, Duration timeout, Boolean ssl, String password) { this.connectionString = connectionString; this.readFrom = readFrom; this.timeout = timeout; + this.password = password; + this.ssl = ssl; } public String getConnectionString() { @@ -41,4 +46,12 @@ public ReadFrom getReadFrom() { public Duration getTimeout() { return this.timeout; } + + public String getPassword() { + return this.password; + } + + public Boolean getSsl() { + return this.ssl; + } } diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisStoreConfig.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisStoreConfig.java index 5e4560a..3045235 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisStoreConfig.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisStoreConfig.java @@ -20,11 +20,13 @@ public class RedisStoreConfig { private final String host; private final Integer port; private final Boolean ssl; + private final String password; - public RedisStoreConfig(String host, Integer port, Boolean ssl) { + public RedisStoreConfig(String host, Integer port, Boolean ssl, String password) { this.host = host; this.port = port; this.ssl = ssl; + this.password = password; } public String getHost() { @@ -38,4 +40,8 @@ public Integer getPort() { public Boolean getSsl() { return this.ssl; } + + public String getPassword() { + return this.password; + } }