diff --git a/coverage.html b/coverage.html
new file mode 100644
index 0000000000..31f1f9cd62
--- /dev/null
+++ b/coverage.html
@@ -0,0 +1,4627 @@
+
+
+
+
+
+
+
package feast
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/apache/arrow/go/v8/arrow/memory"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+
+ "github.com/feast-dev/feast/go/internal/feast/model"
+ "github.com/feast-dev/feast/go/internal/feast/onlineserving"
+ "github.com/feast-dev/feast/go/internal/feast/onlinestore"
+ "github.com/feast-dev/feast/go/internal/feast/registry"
+ "github.com/feast-dev/feast/go/internal/feast/transformation"
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ prototypes "github.com/feast-dev/feast/go/protos/feast/types"
+)
+
+type FeatureStoreInterface interface {
+ GetOnlineFeatures(
+ ctx context.Context,
+ featureRefs []string,
+ featureService *model.FeatureService,
+ joinKeyToEntityValues map[string]*prototypes.RepeatedValue,
+ requestData map[string]*prototypes.RepeatedValue,
+ fullFeatureNames bool) ([]*onlineserving.FeatureVector, error)
+}
+type FeatureStore struct {
+ config *registry.RepoConfig
+ registry *registry.Registry
+ onlineStore onlinestore.OnlineStore
+ transformationCallback transformation.TransformationCallback
+ transformationService *transformation.GrpcTransformationService
+}
+
+// A Features struct specifies a list of features to be retrieved from the online store. These features
+// can be specified either as a list of string feature references or as a feature service. String
+// feature references must have format "feature_view:feature", e.g. "customer_fv:daily_transactions".
+type Features struct {
+ FeaturesRefs []string
+ FeatureService *model.FeatureService
+}
+
+func (fs *FeatureStore) Registry() *registry.Registry {
+ return fs.registry
+}
+
+func (fs *FeatureStore) GetRepoConfig() *registry.RepoConfig {
+ return fs.config
+}
+
+// NewFeatureStore constructs a feature store fat client using the
+// repo config (contents of feature_store.yaml converted to JSON map).
+func NewFeatureStore(config *registry.RepoConfig, callback transformation.TransformationCallback) (*FeatureStore, error) {
+ onlineStore, err := onlinestore.NewOnlineStore(config)
+ if err != nil {
+ return nil, err
+ }
+ registryConfig, err := config.GetRegistryConfig()
+ if err != nil {
+ return nil, err
+ }
+ registry, err := registry.NewRegistry(registryConfig, config.RepoPath, config.Project)
+ if err != nil {
+ return nil, err
+ }
+ err = registry.InitializeRegistry()
+ if err != nil {
+ return nil, err
+ }
+ sanitizedProjectName := strings.Replace(config.Project, "_", "-", -1)
+ productName := os.Getenv("PRODUCT")
+ endpoint := fmt.Sprintf("%s-transformations.%s.svc.cluster.local:80", sanitizedProjectName, productName)
+ transformationService, _ := transformation.NewGrpcTransformationService(config, endpoint)
+
+ return &FeatureStore{
+ config: config,
+ registry: registry,
+ onlineStore: onlineStore,
+ transformationCallback: callback,
+ transformationService: transformationService,
+ }, nil
+}
+
+// TODO: Review all functions that use ODFV and Request FV since these have not been tested
+// ToDo: Split GetOnlineFeatures interface into two: GetOnlinFeaturesByFeatureService and GetOnlineFeaturesByFeatureRefs
+func (fs *FeatureStore) GetOnlineFeatures(
+ ctx context.Context,
+ featureRefs []string,
+ featureService *model.FeatureService,
+ joinKeyToEntityValues map[string]*prototypes.RepeatedValue,
+ requestData map[string]*prototypes.RepeatedValue,
+ fullFeatureNames bool) ([]*onlineserving.FeatureVector, error) {
+ fvs, odFvs, err := fs.ListAllViews()
+ if err != nil {
+ return nil, err
+ }
+
+ entities, err := fs.ListEntities(false)
+ if err != nil {
+ return nil, err
+ }
+
+ var requestedFeatureViews []*onlineserving.FeatureViewAndRefs
+ var requestedOnDemandFeatureViews []*model.OnDemandFeatureView
+ if featureService != nil {
+ requestedFeatureViews, requestedOnDemandFeatureViews, err =
+ onlineserving.GetFeatureViewsToUseByService(featureService, fvs, odFvs)
+ } else {
+ requestedFeatureViews, requestedOnDemandFeatureViews, err =
+ onlineserving.GetFeatureViewsToUseByFeatureRefs(featureRefs, fvs, odFvs)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ entityNameToJoinKeyMap, expectedJoinKeysSet, err := onlineserving.GetEntityMaps(requestedFeatureViews, entities)
+ if err != nil {
+ return nil, err
+ }
+
+ err = onlineserving.ValidateFeatureRefs(requestedFeatureViews, fullFeatureNames)
+ if err != nil {
+ return nil, err
+ }
+
+ numRows, err := onlineserving.ValidateEntityValues(joinKeyToEntityValues, requestData, expectedJoinKeysSet)
+ if err != nil {
+ return nil, err
+ }
+
+ err = transformation.EnsureRequestedDataExist(requestedOnDemandFeatureViews, requestData)
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]*onlineserving.FeatureVector, 0)
+ arrowMemory := memory.NewGoAllocator()
+ featureViews := make([]*model.FeatureView, len(requestedFeatureViews))
+ index := 0
+ for _, featuresAndView := range requestedFeatureViews {
+ featureViews[index] = featuresAndView.View
+ index += 1
+ }
+
+ entitylessCase := false
+ for _, featureView := range featureViews {
+ if featureView.HasEntity(model.DUMMY_ENTITY_NAME) {
+ entitylessCase = true
+ break
+ }
+ }
+
+ if entitylessCase {
+ dummyEntityColumn := &prototypes.RepeatedValue{Val: make([]*prototypes.Value, numRows)}
+ for index := 0; index < numRows; index++ {
+ dummyEntityColumn.Val[index] = &model.DUMMY_ENTITY_VALUE
+ }
+ joinKeyToEntityValues[model.DUMMY_ENTITY_ID] = dummyEntityColumn
+ }
+
+ groupedRefs, err := onlineserving.GroupFeatureRefs(requestedFeatureViews, joinKeyToEntityValues, entityNameToJoinKeyMap, fullFeatureNames)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, groupRef := range groupedRefs {
+ featureData, err := fs.readFromOnlineStore(ctx, groupRef.EntityKeys, groupRef.FeatureViewNames, groupRef.FeatureNames)
+ if err != nil {
+ return nil, err
+ }
+
+ vectors, err := onlineserving.TransposeFeatureRowsIntoColumns(
+ featureData,
+ groupRef,
+ requestedFeatureViews,
+ arrowMemory,
+ numRows,
+ )
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, vectors...)
+ }
+
+ if fs.transformationCallback != nil || fs.transformationService != nil {
+ onDemandFeatures, err := transformation.AugmentResponseWithOnDemandTransforms(
+ ctx,
+ requestedOnDemandFeatureViews,
+ requestData,
+ joinKeyToEntityValues,
+ result,
+ fs.transformationCallback,
+ fs.transformationService,
+ arrowMemory,
+ numRows,
+ fullFeatureNames,
+ )
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, onDemandFeatures...)
+ }
+
+ result, err = onlineserving.KeepOnlyRequestedFeatures(result, featureRefs, featureService, fullFeatureNames)
+ if err != nil {
+ return nil, err
+ }
+
+ entityColumns, err := onlineserving.EntitiesToFeatureVectors(joinKeyToEntityValues, arrowMemory, numRows)
+ result = append(entityColumns, result...)
+ return result, nil
+}
+
+func (fs *FeatureStore) DestructOnlineStore() {
+ fs.onlineStore.Destruct()
+}
+
+// ParseFeatures parses the kind field of a GetOnlineFeaturesRequest protobuf message
+// and populates a Features struct with the result.
+func (fs *FeatureStore) ParseFeatures(kind interface{}) (*Features, error) {
+ if featureList, ok := kind.(*serving.GetOnlineFeaturesRequest_Features); ok {
+ return &Features{FeaturesRefs: featureList.Features.GetVal(), FeatureService: nil}, nil
+ }
+ if featureServiceRequest, ok := kind.(*serving.GetOnlineFeaturesRequest_FeatureService); ok {
+ featureService, err := fs.registry.GetFeatureService(fs.config.Project, featureServiceRequest.FeatureService)
+ if err != nil {
+ return nil, err
+ }
+ return &Features{FeaturesRefs: nil, FeatureService: featureService}, nil
+ }
+ return nil, errors.New("cannot parse kind from GetOnlineFeaturesRequest")
+}
+
+func (fs *FeatureStore) GetFeatureService(name string) (*model.FeatureService, error) {
+ return fs.registry.GetFeatureService(fs.config.Project, name)
+}
+
+func (fs *FeatureStore) ListAllViews() (map[string]*model.FeatureView, map[string]*model.OnDemandFeatureView, error) {
+ fvs := make(map[string]*model.FeatureView)
+ odFvs := make(map[string]*model.OnDemandFeatureView)
+
+ featureViews, err := fs.ListFeatureViews()
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, featureView := range featureViews {
+ fvs[featureView.Base.Name] = featureView
+ }
+
+ streamFeatureViews, err := fs.ListStreamFeatureViews()
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, streamFeatureView := range streamFeatureViews {
+ fvs[streamFeatureView.Base.Name] = streamFeatureView
+ }
+
+ onDemandFeatureViews, err := fs.registry.ListOnDemandFeatureViews(fs.config.Project)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, onDemandFeatureView := range onDemandFeatureViews {
+ odFvs[onDemandFeatureView.Base.Name] = onDemandFeatureView
+ }
+ return fvs, odFvs, nil
+}
+
+func (fs *FeatureStore) ListFeatureViews() ([]*model.FeatureView, error) {
+ featureViews, err := fs.registry.ListFeatureViews(fs.config.Project)
+ if err != nil {
+ return featureViews, err
+ }
+ return featureViews, nil
+}
+
+func (fs *FeatureStore) ListStreamFeatureViews() ([]*model.FeatureView, error) {
+ streamFeatureViews, err := fs.registry.ListStreamFeatureViews(fs.config.Project)
+ if err != nil {
+ return streamFeatureViews, err
+ }
+ return streamFeatureViews, nil
+}
+
+func (fs *FeatureStore) ListEntities(hideDummyEntity bool) ([]*model.Entity, error) {
+
+ allEntities, err := fs.registry.ListEntities(fs.config.Project)
+ if err != nil {
+ return allEntities, err
+ }
+ entities := make([]*model.Entity, 0)
+ for _, entity := range allEntities {
+ if entity.Name != model.DUMMY_ENTITY_NAME || !hideDummyEntity {
+ entities = append(entities, entity)
+ }
+ }
+ return entities, nil
+}
+
+func (fs *FeatureStore) GetEntity(entityName string, hideDummyEntity bool) (*model.Entity, error) {
+
+ entity, err := fs.registry.GetEntity(fs.config.Project, entityName)
+ if err != nil {
+ return nil, err
+ }
+ return entity, nil
+}
+func (fs *FeatureStore) GetRequestSources(odfvList []*model.OnDemandFeatureView) (map[string]prototypes.ValueType_Enum, error) {
+
+ requestSources := make(map[string]prototypes.ValueType_Enum, 0)
+ if len(odfvList) > 0 {
+ for _, odfv := range odfvList {
+ schema := odfv.GetRequestDataSchema()
+ for name, dtype := range schema {
+ requestSources[name] = dtype
+ }
+ }
+ }
+ return requestSources, nil
+}
+
+func (fs *FeatureStore) ListOnDemandFeatureViews() ([]*model.OnDemandFeatureView, error) {
+ return fs.registry.ListOnDemandFeatureViews(fs.config.Project)
+}
+
+/*
+Group feature views that share the same set of join keys. For each group, we store only unique rows and save indices to retrieve those
+rows for each requested feature
+*/
+
+func (fs *FeatureStore) GetFeatureView(featureViewName string, hideDummyEntity bool) (*model.FeatureView, error) {
+ fv, err := fs.registry.GetFeatureView(fs.config.Project, featureViewName)
+ if err != nil {
+ return nil, err
+ }
+ if fv.HasEntity(model.DUMMY_ENTITY_NAME) && hideDummyEntity {
+ fv.EntityNames = []string{}
+ }
+ return fv, nil
+}
+
+func (fs *FeatureStore) GetOnDemandFeatureView(featureViewName string) (*model.OnDemandFeatureView, error) {
+ fv, err := fs.registry.GetOnDemandFeatureView(fs.config.Project, featureViewName)
+ if err != nil {
+ return nil, err
+ }
+ return fv, nil
+}
+
+func (fs *FeatureStore) readFromOnlineStore(ctx context.Context, entityRows []*prototypes.EntityKey,
+ requestedFeatureViewNames []string,
+ requestedFeatureNames []string,
+) ([][]onlinestore.FeatureData, error) {
+ // Create a Datadog span from context
+ span, _ := tracer.StartSpanFromContext(ctx, "fs.readFromOnlineStore")
+ defer span.Finish()
+
+ numRows := len(entityRows)
+ entityRowsValue := make([]*prototypes.EntityKey, numRows)
+ for index, entityKey := range entityRows {
+ entityRowsValue[index] = &prototypes.EntityKey{JoinKeys: entityKey.JoinKeys, EntityValues: entityKey.EntityValues}
+ }
+ return fs.onlineStore.OnlineRead(ctx, entityRowsValue, requestedFeatureViewNames, requestedFeatureNames)
+}
+
+func (fs *FeatureStore) GetFcosMap() (map[string]*model.Entity, map[string]*model.FeatureView, map[string]*model.OnDemandFeatureView, error) {
+ odfvs, err := fs.ListOnDemandFeatureViews()
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ fvs, err := fs.ListFeatureViews()
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ entities, err := fs.ListEntities(true)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ entityMap := make(map[string]*model.Entity)
+ for _, entity := range entities {
+ entityMap[entity.Name] = entity
+ }
+ fvMap := make(map[string]*model.FeatureView)
+ for _, fv := range fvs {
+ fvMap[fv.Base.Name] = fv
+ }
+ odfvMap := make(map[string]*model.OnDemandFeatureView)
+ for _, odfv := range odfvs {
+ odfvMap[odfv.Base.Name] = odfv
+ }
+ return entityMap, fvMap, odfvMap, nil
+}
+
+
+
package onlineserving
+
+import (
+ "crypto/sha256"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/apache/arrow/go/v8/arrow"
+ "github.com/apache/arrow/go/v8/arrow/memory"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/durationpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/feast-dev/feast/go/internal/feast/model"
+ "github.com/feast-dev/feast/go/internal/feast/onlinestore"
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ prototypes "github.com/feast-dev/feast/go/protos/feast/types"
+ "github.com/feast-dev/feast/go/types"
+)
+
+/*
+FeatureVector type represent result of retrieving single feature for multiple rows.
+It can be imagined as a column in output dataframe / table.
+It contains of feature name, list of values (across all rows),
+list of statuses and list of timestamp. All these lists have equal length.
+And this length is also equal to number of entity rows received in request.
+*/
+type FeatureVector struct {
+ Name string
+ Values arrow.Array
+ Statuses []serving.FieldStatus
+ Timestamps []*timestamppb.Timestamp
+}
+
+type FeatureViewAndRefs struct {
+ View *model.FeatureView
+ FeatureRefs []string
+}
+
+/*
+We group all features from a single request by entities they attached to.
+Thus, we will be able to call online retrieval per entity and not per each feature View.
+In this struct we collect all features and views that belongs to a group.
+We also store here projected entity keys (only ones that needed to retrieve these features)
+and indexes to map result of retrieval into output response.
+*/
+type GroupedFeaturesPerEntitySet struct {
+ // A list of requested feature references of the form featureViewName:featureName that share this entity set
+ FeatureNames []string
+ FeatureViewNames []string
+ // full feature references as they supposed to appear in response
+ AliasedFeatureNames []string
+ // Entity set as a list of EntityKeys to pass to OnlineRead
+ EntityKeys []*prototypes.EntityKey
+ // Reversed mapping to project result of retrieval from storage to response
+ Indices [][]int
+}
+
+/*
+Return
+
+ (1) requested feature views and features grouped per View
+ (2) requested on demand feature views
+
+existed in the registry
+*/
+func GetFeatureViewsToUseByService(
+ featureService *model.FeatureService,
+ featureViews map[string]*model.FeatureView,
+ onDemandFeatureViews map[string]*model.OnDemandFeatureView) ([]*FeatureViewAndRefs, []*model.OnDemandFeatureView, error) {
+
+ viewNameToViewAndRefs := make(map[string]*FeatureViewAndRefs)
+ odFvsToUse := make([]*model.OnDemandFeatureView, 0)
+
+ for _, featureProjection := range featureService.Projections {
+ // Create copies of FeatureView that may contains the same *FeatureView but
+ // each differentiated by a *FeatureViewProjection
+ featureViewName := featureProjection.Name
+ if fv, ok := featureViews[featureViewName]; ok {
+ base, err := fv.Base.WithProjection(featureProjection)
+ if err != nil {
+ return nil, nil, err
+ }
+ if _, ok := viewNameToViewAndRefs[featureProjection.NameToUse()]; !ok {
+ viewNameToViewAndRefs[featureProjection.NameToUse()] = &FeatureViewAndRefs{
+ View: fv.NewFeatureViewFromBase(base),
+ FeatureRefs: []string{},
+ }
+ }
+
+ for _, feature := range featureProjection.Features {
+ viewNameToViewAndRefs[featureProjection.NameToUse()].FeatureRefs =
+ addStringIfNotContains(viewNameToViewAndRefs[featureProjection.NameToUse()].FeatureRefs,
+ feature.Name)
+ }
+
+ } else if odFv, ok := onDemandFeatureViews[featureViewName]; ok {
+ projectedOdFv, err := odFv.NewWithProjection(featureProjection)
+ if err != nil {
+ return nil, nil, err
+ }
+ odFvsToUse = append(odFvsToUse, projectedOdFv)
+ err = extractOdFvDependencies(
+ projectedOdFv,
+ featureViews,
+ viewNameToViewAndRefs)
+ if err != nil {
+ return nil, nil, err
+ }
+ } else {
+ return nil, nil, fmt.Errorf("the provided feature service %s contains a reference to a feature View"+
+ "%s which doesn't exist, please make sure that you have created the feature View"+
+ "%s and that you have registered it by running \"apply\"", featureService.Name, featureViewName, featureViewName)
+ }
+ }
+
+ fvsToUse := make([]*FeatureViewAndRefs, 0)
+ for _, viewAndRef := range viewNameToViewAndRefs {
+ fvsToUse = append(fvsToUse, viewAndRef)
+ }
+
+ return fvsToUse, odFvsToUse, nil
+}
+
+/*
+Return
+
+ (1) requested feature views and features grouped per View
+ (2) requested on demand feature views
+
+existed in the registry
+*/
+func GetFeatureViewsToUseByFeatureRefs(
+ features []string,
+ featureViews map[string]*model.FeatureView,
+ onDemandFeatureViews map[string]*model.OnDemandFeatureView) ([]*FeatureViewAndRefs, []*model.OnDemandFeatureView, error) {
+ viewNameToViewAndRefs := make(map[string]*FeatureViewAndRefs)
+ odFvToFeatures := make(map[string][]string)
+
+ for _, featureRef := range features {
+ featureViewName, featureName, err := ParseFeatureReference(featureRef)
+ if err != nil {
+ return nil, nil, err
+ }
+ if fv, ok := featureViews[featureViewName]; ok {
+ if viewAndRef, ok := viewNameToViewAndRefs[fv.Base.Name]; ok {
+ viewAndRef.FeatureRefs = addStringIfNotContains(viewAndRef.FeatureRefs, featureName)
+ } else {
+ viewNameToViewAndRefs[fv.Base.Name] = &FeatureViewAndRefs{
+ View: fv,
+ FeatureRefs: []string{featureName},
+ }
+ }
+ } else if odfv, ok := onDemandFeatureViews[featureViewName]; ok {
+ if _, ok := odFvToFeatures[odfv.Base.Name]; !ok {
+ odFvToFeatures[odfv.Base.Name] = []string{featureName}
+ } else {
+ odFvToFeatures[odfv.Base.Name] = append(
+ odFvToFeatures[odfv.Base.Name], featureName)
+ }
+ } else {
+ return nil, nil, fmt.Errorf("feature View %s doesn't exist, please make sure that you have created the"+
+ " feature View %s and that you have registered it by running \"apply\"", featureViewName, featureViewName)
+ }
+ }
+
+ odFvsToUse := make([]*model.OnDemandFeatureView, 0)
+
+ for odFvName, featureNames := range odFvToFeatures {
+ projectedOdFv, err := onDemandFeatureViews[odFvName].ProjectWithFeatures(featureNames)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ err = extractOdFvDependencies(
+ projectedOdFv,
+ featureViews,
+ viewNameToViewAndRefs)
+ if err != nil {
+ return nil, nil, err
+ }
+ odFvsToUse = append(odFvsToUse, projectedOdFv)
+ }
+
+ fvsToUse := make([]*FeatureViewAndRefs, 0)
+ for _, viewAndRefs := range viewNameToViewAndRefs {
+ fvsToUse = append(fvsToUse, viewAndRefs)
+ }
+
+ return fvsToUse, odFvsToUse, nil
+}
+
+func extractOdFvDependencies(
+ odFv *model.OnDemandFeatureView,
+ sourceFvs map[string]*model.FeatureView,
+ requestedFeatures map[string]*FeatureViewAndRefs,
+) error {
+
+ for _, sourceFvProjection := range odFv.SourceFeatureViewProjections {
+ fv := sourceFvs[sourceFvProjection.Name]
+ base, err := fv.Base.WithProjection(sourceFvProjection)
+ if err != nil {
+ return err
+ }
+ newFv := fv.NewFeatureViewFromBase(base)
+
+ if _, ok := requestedFeatures[sourceFvProjection.NameToUse()]; !ok {
+ requestedFeatures[sourceFvProjection.NameToUse()] = &FeatureViewAndRefs{
+ View: newFv,
+ FeatureRefs: []string{},
+ }
+ }
+
+ for _, feature := range sourceFvProjection.Features {
+ requestedFeatures[sourceFvProjection.NameToUse()].FeatureRefs = addStringIfNotContains(
+ requestedFeatures[sourceFvProjection.NameToUse()].FeatureRefs, feature.Name)
+ }
+ }
+
+ return nil
+}
+
+func addStringIfNotContains(slice []string, element string) []string {
+ found := false
+ for _, item := range slice {
+ if element == item {
+ found = true
+ }
+ }
+ if !found {
+ slice = append(slice, element)
+ }
+ return slice
+}
+
+func GetEntityMaps(requestedFeatureViews []*FeatureViewAndRefs, entities []*model.Entity) (map[string]string, map[string]interface{}, error) {
+ entityNameToJoinKeyMap := make(map[string]string)
+ expectedJoinKeysSet := make(map[string]interface{})
+
+ entitiesByName := make(map[string]*model.Entity)
+
+ for _, entity := range entities {
+ entitiesByName[entity.Name] = entity
+ }
+
+ for _, featuresAndView := range requestedFeatureViews {
+ featureView := featuresAndView.View
+ var joinKeyToAliasMap map[string]string
+ if featureView.Base.Projection != nil && featureView.Base.Projection.JoinKeyMap != nil {
+ joinKeyToAliasMap = featureView.Base.Projection.JoinKeyMap
+ } else {
+ joinKeyToAliasMap = map[string]string{}
+ }
+
+ for _, entityName := range featureView.EntityNames {
+ joinKey := entitiesByName[entityName].JoinKey
+ entityNameToJoinKeyMap[entityName] = joinKey
+
+ if alias, ok := joinKeyToAliasMap[joinKey]; ok {
+ expectedJoinKeysSet[alias] = nil
+ } else {
+ expectedJoinKeysSet[joinKey] = nil
+ }
+ }
+ }
+ return entityNameToJoinKeyMap, expectedJoinKeysSet, nil
+}
+
+func ValidateEntityValues(joinKeyValues map[string]*prototypes.RepeatedValue,
+ requestData map[string]*prototypes.RepeatedValue,
+ expectedJoinKeysSet map[string]interface{}) (int, error) {
+ numRows := -1
+
+ for joinKey, values := range joinKeyValues {
+ if _, ok := expectedJoinKeysSet[joinKey]; !ok {
+ requestData[joinKey] = values
+ delete(joinKeyValues, joinKey)
+ // ToDo: when request data will be passed correctly (not as part of entity rows)
+ // ToDo: throw this error instead
+ // return 0, fmt.Errorf("JoinKey is not expected in this request: %s\n%v", JoinKey, expectedJoinKeysSet)
+ } else {
+ if numRows < 0 {
+ numRows = len(values.Val)
+ } else if len(values.Val) != numRows {
+ return -1, errors.New("valueError: All entity rows must have the same columns")
+ }
+
+ }
+ }
+
+ return numRows, nil
+}
+
+func ValidateFeatureRefs(requestedFeatures []*FeatureViewAndRefs, fullFeatureNames bool) error {
+ featureRefCounter := make(map[string]int)
+ featureRefs := make([]string, 0)
+ for _, viewAndFeatures := range requestedFeatures {
+ for _, feature := range viewAndFeatures.FeatureRefs {
+ projectedViewName := viewAndFeatures.View.Base.Name
+ if viewAndFeatures.View.Base.Projection != nil {
+ projectedViewName = viewAndFeatures.View.Base.Projection.NameToUse()
+ }
+
+ featureRefs = append(featureRefs,
+ fmt.Sprintf("%s:%s", projectedViewName, feature))
+ }
+ }
+
+ for _, featureRef := range featureRefs {
+ if fullFeatureNames {
+ featureRefCounter[featureRef]++
+ } else {
+ _, featureName, _ := ParseFeatureReference(featureRef)
+ featureRefCounter[featureName]++
+ }
+
+ }
+ for featureName, occurrences := range featureRefCounter {
+ if occurrences == 1 {
+ delete(featureRefCounter, featureName)
+ }
+ }
+ if len(featureRefCounter) >= 1 {
+ collidedFeatureRefs := make([]string, 0)
+ for collidedFeatureRef := range featureRefCounter {
+ if fullFeatureNames {
+ collidedFeatureRefs = append(collidedFeatureRefs, collidedFeatureRef)
+ } else {
+ for _, featureRef := range featureRefs {
+ _, featureName, _ := ParseFeatureReference(featureRef)
+ if featureName == collidedFeatureRef {
+ collidedFeatureRefs = append(collidedFeatureRefs, featureRef)
+ }
+ }
+ }
+ }
+ return featureNameCollisionError{collidedFeatureRefs, fullFeatureNames}
+ }
+
+ return nil
+}
+
+func TransposeFeatureRowsIntoColumns(featureData2D [][]onlinestore.FeatureData,
+ groupRef *GroupedFeaturesPerEntitySet,
+ requestedFeatureViews []*FeatureViewAndRefs,
+ arrowAllocator memory.Allocator,
+ numRows int) ([]*FeatureVector, error) {
+
+ numFeatures := len(groupRef.AliasedFeatureNames)
+ fvs := make(map[string]*model.FeatureView)
+ for _, viewAndRefs := range requestedFeatureViews {
+ fvs[viewAndRefs.View.Base.Name] = viewAndRefs.View
+ }
+
+ var value *prototypes.Value
+ var status serving.FieldStatus
+ var eventTimeStamp *timestamppb.Timestamp
+ var featureData *onlinestore.FeatureData
+ var fv *model.FeatureView
+ var featureViewName string
+
+ vectors := make([]*FeatureVector, 0)
+
+ for featureIndex := 0; featureIndex < numFeatures; featureIndex++ {
+ currentVector := &FeatureVector{
+ Name: groupRef.AliasedFeatureNames[featureIndex],
+ Statuses: make([]serving.FieldStatus, numRows),
+ Timestamps: make([]*timestamppb.Timestamp, numRows),
+ }
+ vectors = append(vectors, currentVector)
+ protoValues := make([]*prototypes.Value, numRows)
+
+ for rowEntityIndex, outputIndexes := range groupRef.Indices {
+ if featureData2D[rowEntityIndex] == nil {
+ value = nil
+ status = serving.FieldStatus_NOT_FOUND
+ eventTimeStamp = ×tamppb.Timestamp{}
+ } else {
+ featureData = &featureData2D[rowEntityIndex][featureIndex]
+ eventTimeStamp = ×tamppb.Timestamp{Seconds: featureData.Timestamp.Seconds, Nanos: featureData.Timestamp.Nanos}
+ featureViewName = featureData.Reference.FeatureViewName
+ fv = fvs[featureViewName]
+ if _, ok := featureData.Value.Val.(*prototypes.Value_NullVal); ok {
+ value = nil
+ status = serving.FieldStatus_NOT_FOUND
+ } else if checkOutsideTtl(eventTimeStamp, timestamppb.Now(), fv.Ttl) {
+ value = &prototypes.Value{Val: featureData.Value.Val}
+ status = serving.FieldStatus_OUTSIDE_MAX_AGE
+ } else {
+ value = &prototypes.Value{Val: featureData.Value.Val}
+ status = serving.FieldStatus_PRESENT
+ }
+ }
+ for _, rowIndex := range outputIndexes {
+ protoValues[rowIndex] = value
+ currentVector.Statuses[rowIndex] = status
+ currentVector.Timestamps[rowIndex] = eventTimeStamp
+ }
+ }
+ arrowValues, err := types.ProtoValuesToArrowArray(protoValues, arrowAllocator, numRows)
+ if err != nil {
+ return nil, err
+ }
+ currentVector.Values = arrowValues
+ }
+
+ return vectors, nil
+
+}
+
+func KeepOnlyRequestedFeatures(
+ vectors []*FeatureVector,
+ requestedFeatureRefs []string,
+ featureService *model.FeatureService,
+ fullFeatureNames bool) ([]*FeatureVector, error) {
+ vectorsByName := make(map[string]*FeatureVector)
+ expectedVectors := make([]*FeatureVector, 0)
+
+ usedVectors := make(map[string]bool)
+
+ for _, vector := range vectors {
+ vectorsByName[vector.Name] = vector
+ }
+
+ if featureService != nil {
+ for _, projection := range featureService.Projections {
+ for _, f := range projection.Features {
+ requestedFeatureRefs = append(requestedFeatureRefs,
+ fmt.Sprintf("%s:%s", projection.NameToUse(), f.Name))
+ }
+ }
+ }
+
+ for _, featureRef := range requestedFeatureRefs {
+ viewName, featureName, err := ParseFeatureReference(featureRef)
+ if err != nil {
+ return nil, err
+ }
+ qualifiedName := getQualifiedFeatureName(viewName, featureName, fullFeatureNames)
+ if _, ok := vectorsByName[qualifiedName]; !ok {
+ return nil, fmt.Errorf("requested feature %s can't be retrieved", featureRef)
+ }
+ expectedVectors = append(expectedVectors, vectorsByName[qualifiedName])
+ usedVectors[qualifiedName] = true
+ }
+
+ // Free arrow arrays for vectors that were not used.
+ for _, vector := range vectors {
+ if _, ok := usedVectors[vector.Name]; !ok {
+ vector.Values.Release()
+ }
+ }
+
+ return expectedVectors, nil
+}
+
+func EntitiesToFeatureVectors(entityColumns map[string]*prototypes.RepeatedValue, arrowAllocator memory.Allocator, numRows int) ([]*FeatureVector, error) {
+ vectors := make([]*FeatureVector, 0)
+ presentVector := make([]serving.FieldStatus, numRows)
+ timestampVector := make([]*timestamppb.Timestamp, numRows)
+ for idx := 0; idx < numRows; idx++ {
+ presentVector[idx] = serving.FieldStatus_PRESENT
+ timestampVector[idx] = timestamppb.Now()
+ }
+ for entityName, values := range entityColumns {
+ arrowColumn, err := types.ProtoValuesToArrowArray(values.Val, arrowAllocator, numRows)
+ if err != nil {
+ return nil, err
+ }
+ vectors = append(vectors, &FeatureVector{
+ Name: entityName,
+ Values: arrowColumn,
+ Statuses: presentVector,
+ Timestamps: timestampVector,
+ })
+ }
+ return vectors, nil
+}
+
+func ParseFeatureReference(featureRef string) (featureViewName, featureName string, e error) {
+ parsedFeatureName := strings.Split(featureRef, ":")
+
+ if len(parsedFeatureName) == 0 {
+ e = errors.New("featureReference should be in the format: 'FeatureViewName:FeatureName'")
+ } else if len(parsedFeatureName) == 1 {
+ featureName = parsedFeatureName[0]
+ } else {
+ featureViewName = parsedFeatureName[0]
+ featureName = parsedFeatureName[1]
+ }
+ return
+}
+
+func entityKeysToProtos(joinKeyValues map[string]*prototypes.RepeatedValue) []*prototypes.EntityKey {
+ keys := make([]string, len(joinKeyValues))
+ index := 0
+ var numRows int
+ for k, v := range joinKeyValues {
+ keys[index] = k
+ index += 1
+ numRows = len(v.Val)
+ }
+ sort.Strings(keys)
+ entityKeys := make([]*prototypes.EntityKey, numRows)
+ numJoinKeys := len(keys)
+ // Construct each EntityKey object
+ for index = 0; index < numRows; index++ {
+ entityKeys[index] = &prototypes.EntityKey{JoinKeys: keys, EntityValues: make([]*prototypes.Value, numJoinKeys)}
+ }
+
+ for colIndex, key := range keys {
+ for index, value := range joinKeyValues[key].GetVal() {
+ entityKeys[index].EntityValues[colIndex] = value
+ }
+ }
+ return entityKeys
+}
+
+func GroupFeatureRefs(requestedFeatureViews []*FeatureViewAndRefs,
+ joinKeyValues map[string]*prototypes.RepeatedValue,
+ entityNameToJoinKeyMap map[string]string,
+ fullFeatureNames bool,
+) (map[string]*GroupedFeaturesPerEntitySet,
+ error,
+) {
+ groups := make(map[string]*GroupedFeaturesPerEntitySet)
+
+ for _, featuresAndView := range requestedFeatureViews {
+ joinKeys := make([]string, 0)
+ fv := featuresAndView.View
+ featureNames := featuresAndView.FeatureRefs
+ for _, entityName := range fv.EntityNames {
+ joinKeys = append(joinKeys, entityNameToJoinKeyMap[entityName])
+ }
+
+ groupKeyBuilder := make([]string, 0)
+ joinKeysValuesProjection := make(map[string]*prototypes.RepeatedValue)
+
+ joinKeyToAliasMap := make(map[string]string)
+ if fv.Base.Projection != nil && fv.Base.Projection.JoinKeyMap != nil {
+ joinKeyToAliasMap = fv.Base.Projection.JoinKeyMap
+ }
+
+ for _, joinKey := range joinKeys {
+ var joinKeyOrAlias string
+
+ if alias, ok := joinKeyToAliasMap[joinKey]; ok {
+ groupKeyBuilder = append(groupKeyBuilder, fmt.Sprintf("%s[%s]", joinKey, alias))
+ joinKeyOrAlias = alias
+ } else {
+ groupKeyBuilder = append(groupKeyBuilder, joinKey)
+ joinKeyOrAlias = joinKey
+ }
+
+ if _, ok := joinKeyValues[joinKeyOrAlias]; !ok {
+ return nil, fmt.Errorf("key %s is missing in provided entity rows", joinKey)
+ }
+ joinKeysValuesProjection[joinKey] = joinKeyValues[joinKeyOrAlias]
+ }
+
+ sort.Strings(groupKeyBuilder)
+ groupKey := strings.Join(groupKeyBuilder, ",")
+
+ aliasedFeatureNames := make([]string, 0)
+ featureViewNames := make([]string, 0)
+ var viewNameToUse string
+ if fv.Base.Projection != nil {
+ viewNameToUse = fv.Base.Projection.NameToUse()
+ } else {
+ viewNameToUse = fv.Base.Name
+ }
+
+ for _, featureName := range featureNames {
+ aliasedFeatureNames = append(aliasedFeatureNames,
+ getQualifiedFeatureName(viewNameToUse, featureName, fullFeatureNames))
+ featureViewNames = append(featureViewNames, fv.Base.Name)
+ }
+
+ if _, ok := groups[groupKey]; !ok {
+ joinKeysProto := entityKeysToProtos(joinKeysValuesProjection)
+ uniqueEntityRows, mappingIndices, err := getUniqueEntityRows(joinKeysProto)
+ if err != nil {
+ return nil, err
+ }
+
+ groups[groupKey] = &GroupedFeaturesPerEntitySet{
+ FeatureNames: featureNames,
+ FeatureViewNames: featureViewNames,
+ AliasedFeatureNames: aliasedFeatureNames,
+ Indices: mappingIndices,
+ EntityKeys: uniqueEntityRows,
+ }
+
+ } else {
+ groups[groupKey].FeatureNames = append(groups[groupKey].FeatureNames, featureNames...)
+ groups[groupKey].AliasedFeatureNames = append(groups[groupKey].AliasedFeatureNames, aliasedFeatureNames...)
+ groups[groupKey].FeatureViewNames = append(groups[groupKey].FeatureViewNames, featureViewNames...)
+ }
+ }
+ return groups, nil
+}
+
+func getUniqueEntityRows(joinKeysProto []*prototypes.EntityKey) ([]*prototypes.EntityKey, [][]int, error) {
+ uniqueValues := make(map[[sha256.Size]byte]*prototypes.EntityKey, 0)
+ positions := make(map[[sha256.Size]byte][]int, 0)
+
+ for index, entityKey := range joinKeysProto {
+ serializedRow, err := proto.Marshal(entityKey)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ rowHash := sha256.Sum256(serializedRow)
+ if _, ok := uniqueValues[rowHash]; !ok {
+ uniqueValues[rowHash] = entityKey
+ positions[rowHash] = []int{index}
+ } else {
+ positions[rowHash] = append(positions[rowHash], index)
+ }
+ }
+
+ mappingIndices := make([][]int, len(uniqueValues))
+ uniqueEntityRows := make([]*prototypes.EntityKey, 0)
+ for rowHash, row := range uniqueValues {
+ nextIdx := len(uniqueEntityRows)
+
+ mappingIndices[nextIdx] = positions[rowHash]
+ uniqueEntityRows = append(uniqueEntityRows, row)
+ }
+ return uniqueEntityRows, mappingIndices, nil
+}
+
+func checkOutsideTtl(featureTimestamp *timestamppb.Timestamp, currentTimestamp *timestamppb.Timestamp, ttl *durationpb.Duration) bool {
+ if ttl.Seconds == 0 {
+ return false
+ }
+ return currentTimestamp.GetSeconds()-featureTimestamp.GetSeconds() > ttl.Seconds
+}
+
+func getQualifiedFeatureName(viewName string, featureName string, fullFeatureNames bool) string {
+ if fullFeatureNames {
+ return fmt.Sprintf("%s__%s", viewName, featureName)
+ } else {
+ return featureName
+ }
+}
+
+type featureNameCollisionError struct {
+ featureRefCollisions []string
+ fullFeatureNames bool
+}
+
+func (e featureNameCollisionError) Error() string {
+ return fmt.Sprintf("featureNameCollisionError: %s; %t", strings.Join(e.featureRefCollisions, ", "), e.fullFeatureNames)
+}
+
+
+
package onlinestore
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/feast-dev/feast/go/internal/feast/registry"
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ "github.com/feast-dev/feast/go/protos/feast/types"
+ "github.com/golang/protobuf/ptypes/timestamp"
+)
+
+type FeatureData struct {
+ Reference serving.FeatureReferenceV2
+ Timestamp timestamp.Timestamp
+ Value types.Value
+}
+
+type OnlineStore interface {
+ // OnlineRead reads multiple features (specified in featureReferences) for multiple
+ // entity keys (specified in entityKeys) and returns an array of array of features,
+ // where each feature contains 3 fields:
+ // 1. feature Reference
+ // 2. feature event timestamp
+ // 3. feature value
+ // The inner array will have the same size as featureReferences,
+ // while the outer array will have the same size as entityKeys.
+
+ // TODO: Can we return [][]FeatureData, []timstamps, error
+ // instead and remove timestamp from FeatureData struct to mimic Python's code
+ // and reduces repeated memory storage for the same timstamp (which is stored as value and not as a pointer).
+ // Should each attribute in FeatureData be stored as a pointer instead since the current
+ // design forces value copied in OnlineRead + GetOnlineFeatures
+ // (array is destructed so we cannot use the same fields in each
+ // Feature object as pointers in GetOnlineFeaturesResponse)
+ // => allocate memory for each field once in OnlineRead
+ // and reuse them in GetOnlineFeaturesResponse?
+ OnlineRead(ctx context.Context, entityKeys []*types.EntityKey, featureViewNames []string, featureNames []string) ([][]FeatureData, error)
+ // Destruct must be call once user is done using OnlineStore
+ // This is to comply with the Connector since we have to close the plugin
+ Destruct()
+}
+
+func getOnlineStoreType(onlineStoreConfig map[string]interface{}) (string, bool) {
+ if onlineStoreType, ok := onlineStoreConfig["type"]; !ok {
+ // If online store type isn't specified, default to sqlite
+ return "sqlite", true
+ } else {
+ result, ok := onlineStoreType.(string)
+ return result, ok
+ }
+}
+
+func NewOnlineStore(config *registry.RepoConfig) (OnlineStore, error) {
+ onlineStoreType, ok := getOnlineStoreType(config.OnlineStore)
+ if !ok {
+ return nil, fmt.Errorf("could not get online store type from online store config: %+v", config.OnlineStore)
+ } else if onlineStoreType == "sqlite" {
+ onlineStore, err := NewSqliteOnlineStore(config.Project, config, config.OnlineStore)
+ return onlineStore, err
+ } else if onlineStoreType == "redis" {
+ onlineStore, err := NewRedisOnlineStore(config.Project, config, config.OnlineStore)
+ return onlineStore, err
+ } else {
+ return nil, fmt.Errorf("%s online store type is currently not supported; only redis and sqlite are supported", onlineStoreType)
+ }
+}
+
+
+
package onlinestore
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+
+ "github.com/feast-dev/feast/go/internal/feast/registry"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+
+ "github.com/redis/go-redis/v9"
+ "github.com/spaolacci/murmur3"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ "github.com/feast-dev/feast/go/protos/feast/types"
+ redistrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/redis/go-redis.v9"
+)
+
+type redisType int
+
+const (
+ redisNode redisType = 0
+ redisCluster redisType = 1
+)
+
+type RedisOnlineStore struct {
+
+ // Feast project name
+ // TODO (woop): Should we remove project as state that is tracked at the store level?
+ project string
+
+ // Redis database type, either a single node server (RedisType.Redis) or a cluster (RedisType.RedisCluster)
+ t redisType
+
+ // Redis client connector
+ client *redis.Client
+
+ // Redis cluster client connector
+ clusterClient *redis.ClusterClient
+
+ config *registry.RepoConfig
+}
+
+func NewRedisOnlineStore(project string, config *registry.RepoConfig, onlineStoreConfig map[string]interface{}) (*RedisOnlineStore, error) {
+ store := RedisOnlineStore{
+ project: project,
+ config: config,
+ }
+
+ var address []string
+ var password string
+ var tlsConfig *tls.Config
+ var db int // Default to 0
+
+ // Parse redis_type and write it into conf.redisStoreType
+ redisStoreType, err := getRedisType(onlineStoreConfig)
+ if err != nil {
+ return nil, err
+ }
+ store.t = redisStoreType
+
+ // Parse connection_string and write it into conf.address, conf.password, and conf.ssl
+ redisConnJson, ok := onlineStoreConfig["connection_string"]
+ if !ok {
+ // Default to "localhost:6379"
+ redisConnJson = "localhost:6379"
+ }
+ if redisConnStr, ok := redisConnJson.(string); !ok {
+ return nil, fmt.Errorf("failed to convert connection_string to string: %+v", redisConnJson)
+ } else {
+ parts := strings.Split(redisConnStr, ",")
+ for _, part := range parts {
+ if strings.Contains(part, ":") {
+ address = append(address, part)
+ } else if strings.Contains(part, "=") {
+ kv := strings.SplitN(part, "=", 2)
+ if kv[0] == "password" {
+ password = kv[1]
+ } else if kv[0] == "ssl" {
+ result, err := strconv.ParseBool(kv[1])
+ if err != nil {
+ return nil, err
+ } else if result {
+ tlsConfig = &tls.Config{}
+ }
+ } else if kv[0] == "db" {
+ db, err = strconv.Atoi(kv[1])
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, fmt.Errorf("unrecognized option in connection_string: %s. Must be one of 'password', 'ssl'", kv[0])
+ }
+ } else {
+ return nil, fmt.Errorf("unable to parse a part of connection_string: %s. Must contain either ':' (addresses) or '=' (options", part)
+ }
+ }
+ }
+
+ // Metrics are not showing up when the service name is set to DD_SERVICE
+ redisTraceServiceName := os.Getenv("DD_SERVICE") + "-redis"
+ if redisTraceServiceName == "" {
+ redisTraceServiceName = "redis.client" // default service name if DD_SERVICE is not set
+ }
+
+ if redisStoreType == redisNode {
+ store.client = redis.NewClient(&redis.Options{
+ Addr: address[0],
+ Password: password, // No password set
+ DB: db,
+ TLSConfig: tlsConfig,
+ })
+ if strings.ToLower(os.Getenv("ENABLE_DATADOG_REDIS_TRACING")) == "true" {
+ redistrace.WrapClient(store.client, redistrace.WithServiceName(redisTraceServiceName))
+ }
+ } else if redisStoreType == redisCluster {
+ store.clusterClient = redis.NewClusterClient(&redis.ClusterOptions{
+ Addrs: []string{address[0]},
+ Password: password, // No password set
+ TLSConfig: tlsConfig,
+ ReadOnly: true,
+ })
+ if strings.ToLower(os.Getenv("ENABLE_DATADOG_REDIS_TRACING")) == "true" {
+ redistrace.WrapClient(store.clusterClient, redistrace.WithServiceName(redisTraceServiceName))
+ }
+ }
+
+ return &store, nil
+}
+
+func getRedisType(onlineStoreConfig map[string]interface{}) (redisType, error) {
+ var t redisType
+
+ redisTypeJson, ok := onlineStoreConfig["redis_type"]
+ if !ok {
+ // Default to "redis"
+ redisTypeJson = "redis"
+ } else if redisTypeStr, ok := redisTypeJson.(string); !ok {
+ return -1, fmt.Errorf("failed to convert redis_type to string: %+v", redisTypeJson)
+ } else {
+ if redisTypeStr == "redis" {
+ t = redisNode
+ } else if redisTypeStr == "redis_cluster" {
+ t = redisCluster
+ } else {
+ return -1, fmt.Errorf("failed to convert redis_type to enum: %s. Must be one of 'redis', 'redis_cluster'", redisTypeStr)
+ }
+ }
+ return t, nil
+}
+
+func (r *RedisOnlineStore) buildFeatureViewIndices(featureViewNames []string, featureNames []string) (map[string]int, map[int]string, int) {
+ featureViewIndices := make(map[string]int)
+ indicesFeatureView := make(map[int]string)
+ index := len(featureNames)
+ for _, featureViewName := range featureViewNames {
+ if _, ok := featureViewIndices[featureViewName]; !ok {
+ featureViewIndices[featureViewName] = index
+ indicesFeatureView[index] = featureViewName
+ index += 1
+ }
+ }
+ return featureViewIndices, indicesFeatureView, index
+}
+
+func (r *RedisOnlineStore) buildHsetKeys(featureViewNames []string, featureNames []string, indicesFeatureView map[int]string, index int) ([]string, []string) {
+ featureCount := len(featureNames)
+ var hsetKeys = make([]string, index)
+ h := murmur3.New32()
+ intBuffer := h.Sum32()
+ byteBuffer := make([]byte, 4)
+
+ for i := 0; i < featureCount; i++ {
+ h.Write([]byte(fmt.Sprintf("%s:%s", featureViewNames[i], featureNames[i])))
+ intBuffer = h.Sum32()
+ binary.LittleEndian.PutUint32(byteBuffer, intBuffer)
+ hsetKeys[i] = string(byteBuffer)
+ h.Reset()
+ }
+ for i := featureCount; i < index; i++ {
+ view := indicesFeatureView[i]
+ tsKey := fmt.Sprintf("_ts:%s", view)
+ hsetKeys[i] = tsKey
+ featureNames = append(featureNames, tsKey)
+ }
+ return hsetKeys, featureNames
+}
+
+func (r *RedisOnlineStore) buildRedisKeys(entityKeys []*types.EntityKey) ([]*[]byte, map[string]int, error) {
+ redisKeys := make([]*[]byte, len(entityKeys))
+ redisKeyToEntityIndex := make(map[string]int)
+ for i := 0; i < len(entityKeys); i++ {
+ var key, err = buildRedisKey(r.project, entityKeys[i], r.config.EntityKeySerializationVersion)
+ if err != nil {
+ return nil, nil, err
+ }
+ redisKeys[i] = key
+ redisKeyToEntityIndex[string(*key)] = i
+ }
+ return redisKeys, redisKeyToEntityIndex, nil
+}
+
+func (r *RedisOnlineStore) OnlineRead(ctx context.Context, entityKeys []*types.EntityKey, featureViewNames []string, featureNames []string) ([][]FeatureData, error) {
+ span, _ := tracer.StartSpanFromContext(ctx, "redis.OnlineRead")
+ defer span.Finish()
+
+ featureCount := len(featureNames)
+ featureViewIndices, indicesFeatureView, index := r.buildFeatureViewIndices(featureViewNames, featureNames)
+ hsetKeys, featureNamesWithTimeStamps := r.buildHsetKeys(featureViewNames, featureNames, indicesFeatureView, index)
+ redisKeys, redisKeyToEntityIndex, err := r.buildRedisKeys(entityKeys)
+ if err != nil {
+ return nil, err
+ }
+
+ results := make([][]FeatureData, len(entityKeys))
+ commands := map[string]*redis.SliceCmd{}
+
+ if r.t == redisNode {
+ pipe := r.client.Pipeline()
+ for _, redisKey := range redisKeys {
+ keyString := string(*redisKey)
+ commands[keyString] = pipe.HMGet(ctx, keyString, hsetKeys...)
+ }
+ _, err = pipe.Exec(ctx)
+ if err != nil {
+ return nil, err
+ }
+ } else if r.t == redisCluster {
+ pipe := r.clusterClient.Pipeline()
+ for _, redisKey := range redisKeys {
+ keyString := string(*redisKey)
+ commands[keyString] = pipe.HMGet(ctx, keyString, hsetKeys...)
+ }
+ _, err = pipe.Exec(ctx)
+ if err != nil {
+ return nil, err
+ }
+ }
+ var entityIndex int
+ var resContainsNonNil bool
+ for redisKey, values := range commands {
+
+ entityIndex = redisKeyToEntityIndex[redisKey]
+ resContainsNonNil = false
+
+ results[entityIndex] = make([]FeatureData, featureCount)
+ res, err := values.Result()
+ if err != nil {
+ return nil, err
+ }
+
+ var timeStamp timestamppb.Timestamp
+
+ for featureIndex, resString := range res {
+ if featureIndex == featureCount {
+ break
+ }
+
+ if resString == nil {
+ // TODO (Ly): Can there be nil result within each feature or they will all be returned as string proto of types.Value_NullVal proto?
+ featureName := featureNamesWithTimeStamps[featureIndex]
+ featureViewName := featureViewNames[featureIndex]
+ timeStampIndex := featureViewIndices[featureViewName]
+ timeStampInterface := res[timeStampIndex]
+ if timeStampInterface != nil {
+ if timeStampString, ok := timeStampInterface.(string); !ok {
+ return nil, errors.New("error parsing value from redis")
+ } else {
+ if err := proto.Unmarshal([]byte(timeStampString), &timeStamp); err != nil {
+ return nil, errors.New("error converting parsed redis value to timestamppb.Timestamp")
+ }
+ }
+ }
+
+ results[entityIndex][featureIndex] = FeatureData{Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: featureName},
+ Timestamp: timestamppb.Timestamp{Seconds: timeStamp.Seconds, Nanos: timeStamp.Nanos},
+ Value: types.Value{Val: &types.Value_NullVal{NullVal: types.Null_NULL}},
+ }
+
+ } else if valueString, ok := resString.(string); !ok {
+ return nil, errors.New("error parsing Value from redis")
+ } else {
+ resContainsNonNil = true
+ var value types.Value
+ if err := proto.Unmarshal([]byte(valueString), &value); err != nil {
+ return nil, errors.New("error converting parsed redis Value to types.Value")
+ } else {
+ featureName := featureNamesWithTimeStamps[featureIndex]
+ featureViewName := featureViewNames[featureIndex]
+ timeStampIndex := featureViewIndices[featureViewName]
+ timeStampInterface := res[timeStampIndex]
+ if timeStampInterface != nil {
+ if timeStampString, ok := timeStampInterface.(string); !ok {
+ return nil, errors.New("error parsing Value from redis")
+ } else {
+ if err := proto.Unmarshal([]byte(timeStampString), &timeStamp); err != nil {
+ return nil, errors.New("error converting parsed redis Value to timestamppb.Timestamp")
+ }
+ }
+ }
+ results[entityIndex][featureIndex] = FeatureData{Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: featureName},
+ Timestamp: timestamppb.Timestamp{Seconds: timeStamp.Seconds, Nanos: timeStamp.Nanos},
+ Value: types.Value{Val: value.Val},
+ }
+ }
+ }
+ }
+
+ if !resContainsNonNil {
+ results[entityIndex] = nil
+ }
+
+ }
+
+ return results, nil
+}
+
+// Dummy destruct function to conform with plugin OnlineStore interface
+func (r *RedisOnlineStore) Destruct() {
+
+}
+
+func buildRedisKey(project string, entityKey *types.EntityKey, entityKeySerializationVersion int64) (*[]byte, error) {
+ serKey, err := serializeEntityKey(entityKey, entityKeySerializationVersion)
+ if err != nil {
+ return nil, err
+ }
+ fullKey := append(*serKey, []byte(project)...)
+ return &fullKey, nil
+}
+
+func serializeEntityKey(entityKey *types.EntityKey, entityKeySerializationVersion int64) (*[]byte, error) {
+ // Serialize entity key to a bytestring so that it can be used as a lookup key in a hash table.
+
+ // Ensure that we have the right amount of join keys and entity values
+ if len(entityKey.JoinKeys) != len(entityKey.EntityValues) {
+ return nil, fmt.Errorf("the amount of join key names and entity values don't match: %s vs %s", entityKey.JoinKeys, entityKey.EntityValues)
+ }
+
+ // Make sure that join keys are sorted so that we have consistent key building
+ m := make(map[string]*types.Value)
+
+ for i := 0; i < len(entityKey.JoinKeys); i++ {
+ m[entityKey.JoinKeys[i]] = entityKey.EntityValues[i]
+ }
+
+ keys := make([]string, 0, len(m))
+ for k := range entityKey.JoinKeys {
+ keys = append(keys, entityKey.JoinKeys[k])
+ }
+ sort.Strings(keys)
+
+ // Build the key
+ length := 5 * len(keys)
+ bufferList := make([][]byte, length)
+
+ for i := 0; i < len(keys); i++ {
+ offset := i * 2
+ byteBuffer := make([]byte, 4)
+ binary.LittleEndian.PutUint32(byteBuffer, uint32(types.ValueType_Enum_value["STRING"]))
+ bufferList[offset] = byteBuffer
+ bufferList[offset+1] = []byte(keys[i])
+ }
+
+ for i := 0; i < len(keys); i++ {
+ offset := (2 * len(keys)) + (i * 3)
+ value := m[keys[i]].GetVal()
+
+ valueBytes, valueTypeBytes, err := serializeValue(value, entityKeySerializationVersion)
+ if err != nil {
+ return valueBytes, err
+ }
+
+ typeBuffer := make([]byte, 4)
+ binary.LittleEndian.PutUint32(typeBuffer, uint32(valueTypeBytes))
+
+ lenBuffer := make([]byte, 4)
+ binary.LittleEndian.PutUint32(lenBuffer, uint32(len(*valueBytes)))
+
+ bufferList[offset+0] = typeBuffer
+ bufferList[offset+1] = lenBuffer
+ bufferList[offset+2] = *valueBytes
+ }
+
+ // Convert from an array of byte arrays to a single byte array
+ var entityKeyBuffer []byte
+ for i := 0; i < len(bufferList); i++ {
+ entityKeyBuffer = append(entityKeyBuffer, bufferList[i]...)
+ }
+
+ return &entityKeyBuffer, nil
+}
+
+func serializeValue(value interface{}, entityKeySerializationVersion int64) (*[]byte, types.ValueType_Enum, error) {
+ // TODO: Implement support for other types (at least the major types like ints, strings, bytes)
+ switch x := (value).(type) {
+ case *types.Value_StringVal:
+ valueString := []byte(x.StringVal)
+ return &valueString, types.ValueType_STRING, nil
+ case *types.Value_BytesVal:
+ return &x.BytesVal, types.ValueType_BYTES, nil
+ case *types.Value_Int32Val:
+ valueBuffer := make([]byte, 4)
+ binary.LittleEndian.PutUint32(valueBuffer, uint32(x.Int32Val))
+ return &valueBuffer, types.ValueType_INT32, nil
+ case *types.Value_Int64Val:
+ if entityKeySerializationVersion <= 1 {
+ // We unfortunately have to use 32 bit here for backward compatibility :(
+ valueBuffer := make([]byte, 4)
+ binary.LittleEndian.PutUint32(valueBuffer, uint32(x.Int64Val))
+ return &valueBuffer, types.ValueType_INT64, nil
+ } else {
+ valueBuffer := make([]byte, 8)
+ binary.LittleEndian.PutUint64(valueBuffer, uint64(x.Int64Val))
+ return &valueBuffer, types.ValueType_INT64, nil
+ }
+ case nil:
+ return nil, types.ValueType_INVALID, fmt.Errorf("could not detect type for %v", x)
+ default:
+ return nil, types.ValueType_INVALID, fmt.Errorf("could not detect type for %v", x)
+ }
+}
+
+
+
package onlinestore
+
+import (
+ "crypto/sha1"
+ "database/sql"
+ "encoding/hex"
+ "errors"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/feast-dev/feast/go/internal/feast/registry"
+
+ "context"
+ "fmt"
+
+ _ "github.com/mattn/go-sqlite3"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ "github.com/feast-dev/feast/go/protos/feast/types"
+)
+
+type SqliteOnlineStore struct {
+ // Feast project name
+ project string
+ path string
+ db *sql.DB
+ db_mu sync.Mutex
+ repoConfig *registry.RepoConfig
+}
+
+// Creates a new sqlite online store object. onlineStoreConfig should have relative path of database file with respect to repoConfig.repoPath.
+func NewSqliteOnlineStore(project string, repoConfig *registry.RepoConfig, onlineStoreConfig map[string]interface{}) (*SqliteOnlineStore, error) {
+ store := SqliteOnlineStore{project: project, repoConfig: repoConfig}
+ if db_path, ok := onlineStoreConfig["path"]; !ok {
+ return nil, fmt.Errorf("cannot find sqlite path %s", db_path)
+ } else {
+ if dbPathStr, ok := db_path.(string); !ok {
+ return nil, fmt.Errorf("cannot find convert sqlite path to string %s", db_path)
+ } else {
+ store.path = fmt.Sprintf("%s/%s", repoConfig.RepoPath, dbPathStr)
+
+ db, err := initializeConnection(store.path)
+ if err != nil {
+ return nil, err
+ }
+ store.db = db
+ }
+ }
+
+ return &store, nil
+}
+
+func (s *SqliteOnlineStore) Destruct() {
+ s.db.Close()
+}
+
+// Returns FeatureData 2D array. Each row corresponds to one entity Value and each column corresponds to a single feature where the number of columns should be
+// same length as the length of featureNames. Reads from every table in featureViewNames with the entity keys described.
+func (s *SqliteOnlineStore) OnlineRead(ctx context.Context, entityKeys []*types.EntityKey, featureViewNames []string, featureNames []string) ([][]FeatureData, error) {
+ featureCount := len(featureNames)
+ _, err := s.getConnection()
+ if err != nil {
+ return nil, err
+ }
+ project := s.project
+ results := make([][]FeatureData, len(entityKeys))
+ entityNameToEntityIndex := make(map[string]int)
+ in_query := make([]string, len(entityKeys))
+ serialized_entities := make([]interface{}, len(entityKeys))
+ for i := 0; i < len(entityKeys); i++ {
+ serKey, err := serializeEntityKey(entityKeys[i], s.repoConfig.EntityKeySerializationVersion)
+ if err != nil {
+ return nil, err
+ }
+ // TODO: fix this, string conversion is not safe
+ entityNameToEntityIndex[hashSerializedEntityKey(serKey)] = i
+ // for IN clause in read query
+ in_query[i] = "?"
+ serialized_entities[i] = *serKey
+ }
+ featureNamesToIdx := make(map[string]int)
+ for idx, name := range featureNames {
+ featureNamesToIdx[name] = idx
+ }
+
+ for _, featureViewName := range featureViewNames {
+ query_string := fmt.Sprintf(`SELECT entity_key, feature_name, Value, event_ts
+ FROM %s
+ WHERE entity_key IN (%s)
+ ORDER BY entity_key`, tableId(project, featureViewName), strings.Join(in_query, ","))
+ rows, err := s.db.Query(query_string, serialized_entities...)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ for rows.Next() {
+ var entity_key []byte
+ var feature_name string
+ var valueString []byte
+ var event_ts time.Time
+ var value types.Value
+ err = rows.Scan(&entity_key, &feature_name, &valueString, &event_ts)
+ if err != nil {
+ return nil, errors.New("error could not resolve row in query (entity key, feature name, value, event ts)")
+ }
+ if err := proto.Unmarshal(valueString, &value); err != nil {
+ return nil, errors.New("error converting parsed value to types.Value")
+ }
+ rowIdx := entityNameToEntityIndex[hashSerializedEntityKey(&entity_key)]
+ if results[rowIdx] == nil {
+ results[rowIdx] = make([]FeatureData, featureCount)
+ }
+ results[rowIdx][featureNamesToIdx[feature_name]] = FeatureData{Reference: serving.FeatureReferenceV2{FeatureViewName: featureViewName, FeatureName: feature_name},
+ Timestamp: *timestamppb.New(event_ts),
+ Value: types.Value{Val: value.Val},
+ }
+ }
+ }
+ return results, nil
+}
+
+// Gets a sqlite connection and sets it to the online store and also returns a pointer to the connection.
+func (s *SqliteOnlineStore) getConnection() (*sql.DB, error) {
+ s.db_mu.Lock()
+ defer s.db_mu.Unlock()
+ if s.db == nil {
+ if s.path == "" {
+ return nil, errors.New("no database path available")
+ }
+ db, err := initializeConnection(s.path)
+ s.db = db
+ if err != nil {
+ return nil, err
+ }
+ }
+ return s.db, nil
+}
+
+// Constructs the table id from the project and table(featureViewName) string.
+func tableId(project string, featureViewName string) string {
+ return fmt.Sprintf("%s_%s", project, featureViewName)
+}
+
+// Creates a connection to the sqlite database and returns the connection.
+func initializeConnection(db_path string) (*sql.DB, error) {
+ db, err := sql.Open("sqlite3", db_path)
+ if err != nil {
+ return nil, err
+ }
+ return db, nil
+}
+
+func hashSerializedEntityKey(serializedEntityKey *[]byte) string {
+ if serializedEntityKey == nil {
+ return ""
+ }
+ h := sha1.New()
+ h.Write(*serializedEntityKey)
+ sha1_hash := hex.EncodeToString(h.Sum(nil))
+ return sha1_hash
+}
+
+
+
package registry
+
+import (
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net/http"
+ "time"
+
+ "github.com/feast-dev/feast/go/protos/feast/core"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/protobuf/proto"
+)
+
+type HttpRegistryStore struct {
+ project string
+ endpoint string
+ clientId string
+ client http.Client
+}
+
+// NotImplementedError represents an error for a function that is not yet implemented.
+type NotImplementedError struct {
+ FunctionName string
+}
+
+// Error implements the error interface for NotImplementedError.
+func (e *NotImplementedError) Error() string {
+ return fmt.Sprintf("Function '%s' not implemented", e.FunctionName)
+}
+
+func NewHttpRegistryStore(config *RegistryConfig, project string) (*HttpRegistryStore, error) {
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ IdleConnTimeout: 60 * time.Second,
+ }
+
+ hrs := &HttpRegistryStore{
+ project: project,
+ endpoint: config.Path,
+ clientId: config.ClientId,
+ client: http.Client{
+ Transport: tr,
+ Timeout: 5 * time.Second,
+ },
+ }
+
+ if err := hrs.TestConnectivity(); err != nil {
+ return nil, err
+ }
+
+ return hrs, nil
+}
+
+func (hrs *HttpRegistryStore) TestConnectivity() error {
+ resp, err := hrs.client.Get(hrs.endpoint)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("HTTP Registry connectivity check failed with status code: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+func (r *HttpRegistryStore) makeHttpRequest(url string) (*http.Response, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Add("Accept", "application/x-protobuf")
+ req.Header.Add("Client-Id", r.clientId)
+
+ resp, err := r.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("HTTP Error: %s", resp.Status)
+ }
+
+ return resp, nil
+}
+
+func (r *HttpRegistryStore) loadProtobufMessages(url string, messageProcessor func([]byte) error) error {
+ resp, err := r.makeHttpRequest(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ buffer, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ if err := messageProcessor(buffer); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (r *HttpRegistryStore) loadEntities(registry *core.Registry) error {
+ url := fmt.Sprintf("%s/projects/%s/entities?allow_cache=true", r.endpoint, r.project)
+ return r.loadProtobufMessages(url, func(data []byte) error {
+ entity_list := &core.EntityList{}
+ if err := proto.Unmarshal(data, entity_list); err != nil {
+ return err
+ }
+ if len(entity_list.GetEntities()) == 0 {
+ log.Warn().Msg(fmt.Sprintf("Feature Registry has no associated Entities for project %s.", r.project))
+ }
+ registry.Entities = append(registry.Entities, entity_list.GetEntities()...)
+ return nil
+ })
+}
+
+func (r *HttpRegistryStore) loadDatasources(registry *core.Registry) error {
+ url := fmt.Sprintf("%s/projects/%s/data_sources?allow_cache=true", r.endpoint, r.project)
+ return r.loadProtobufMessages(url, func(data []byte) error {
+ data_source_list := &core.DataSourceList{}
+ if err := proto.Unmarshal(data, data_source_list); err != nil {
+ return err
+ }
+ if len(data_source_list.GetDatasources()) == 0 {
+ log.Warn().Msg(fmt.Sprintf("Feature Registry has no associated Datasources for project %s.", r.project))
+ }
+ registry.DataSources = append(registry.DataSources, data_source_list.GetDatasources()...)
+ return nil
+ })
+}
+
+func (r *HttpRegistryStore) loadFeatureViews(registry *core.Registry) error {
+ url := fmt.Sprintf("%s/projects/%s/feature_views?allow_cache=true", r.endpoint, r.project)
+ return r.loadProtobufMessages(url, func(data []byte) error {
+ feature_view_list := &core.FeatureViewList{}
+ if err := proto.Unmarshal(data, feature_view_list); err != nil {
+ return err
+ }
+ if len(feature_view_list.GetFeatureviews()) == 0 {
+ log.Warn().Msg(fmt.Sprintf("Feature Registry has no associated FeatureViews for project %s.", r.project))
+ }
+ registry.FeatureViews = append(registry.FeatureViews, feature_view_list.GetFeatureviews()...)
+ return nil
+ })
+}
+
+func (r *HttpRegistryStore) loadOnDemandFeatureViews(registry *core.Registry) error {
+ url := fmt.Sprintf("%s/projects/%s/on_demand_feature_views?allow_cache=true", r.endpoint, r.project)
+ return r.loadProtobufMessages(url, func(data []byte) error {
+ od_feature_view_list := &core.OnDemandFeatureViewList{}
+ if err := proto.Unmarshal(data, od_feature_view_list); err != nil {
+ return err
+ }
+ registry.OnDemandFeatureViews = append(registry.OnDemandFeatureViews, od_feature_view_list.GetOndemandfeatureviews()...)
+ return nil
+ })
+}
+
+func (r *HttpRegistryStore) loadFeatureServices(registry *core.Registry) error {
+ url := fmt.Sprintf("%s/projects/%s/feature_services?allow_cache=true", r.endpoint, r.project)
+ return r.loadProtobufMessages(url, func(data []byte) error {
+ feature_service_list := &core.FeatureServiceList{}
+ if err := proto.Unmarshal(data, feature_service_list); err != nil {
+ return err
+ }
+ registry.FeatureServices = append(registry.FeatureServices, feature_service_list.GetFeatureservices()...)
+ return nil
+ })
+}
+
+func (r *HttpRegistryStore) GetRegistryProto() (*core.Registry, error) {
+
+ registry := core.Registry{}
+
+ if err := r.loadEntities(®istry); err != nil {
+ return nil, err
+ }
+
+ if err := r.loadDatasources(®istry); err != nil {
+ return nil, err
+ }
+
+ if err := r.loadFeatureViews(®istry); err != nil {
+ return nil, err
+ }
+
+ if err := r.loadOnDemandFeatureViews(®istry); err != nil {
+ return nil, err
+ }
+
+ if err := r.loadFeatureServices(®istry); err != nil {
+ return nil, err
+ }
+
+ return ®istry, nil
+}
+
+func (r *HttpRegistryStore) UpdateRegistryProto(rp *core.Registry) error {
+ return &NotImplementedError{FunctionName: "UpdateRegistryProto"}
+}
+
+func (r *HttpRegistryStore) Teardown() error {
+ return &NotImplementedError{FunctionName: "Teardown"}
+}
+
+
+
package registry
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/google/uuid"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/feast-dev/feast/go/protos/feast/core"
+)
+
+// A FileRegistryStore is a file-based implementation of the RegistryStore interface.
+type FileRegistryStore struct {
+ filePath string
+}
+
+// NewFileRegistryStore creates a FileRegistryStore with the given configuration and infers
+// the file path from the repo path and registry path.
+func NewFileRegistryStore(config *RegistryConfig, repoPath string) *FileRegistryStore {
+ lr := FileRegistryStore{}
+ registryPath := config.Path
+ if filepath.IsAbs(registryPath) {
+ lr.filePath = registryPath
+ } else {
+ lr.filePath = filepath.Join(repoPath, registryPath)
+ }
+ return &lr
+}
+
+// GetRegistryProto reads and parses the registry proto from the file path.
+func (r *FileRegistryStore) GetRegistryProto() (*core.Registry, error) {
+ registry := &core.Registry{}
+ in, err := ioutil.ReadFile(r.filePath)
+ if err != nil {
+ return nil, err
+ }
+ if err := proto.Unmarshal(in, registry); err != nil {
+ return nil, err
+ }
+ return registry, nil
+}
+
+func (r *FileRegistryStore) UpdateRegistryProto(rp *core.Registry) error {
+ return r.writeRegistry(rp)
+}
+
+func (r *FileRegistryStore) Teardown() error {
+ return os.Remove(r.filePath)
+}
+
+func (r *FileRegistryStore) writeRegistry(rp *core.Registry) error {
+ rp.VersionId = uuid.New().String()
+ rp.LastUpdated = timestamppb.Now()
+ bytes, err := proto.Marshal(rp)
+ if err != nil {
+ return err
+ }
+ err = ioutil.WriteFile(r.filePath, bytes, 0644)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+
+
package registry
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "sync"
+ "time"
+
+ "github.com/feast-dev/feast/go/internal/feast/model"
+ "github.com/rs/zerolog/log"
+
+ "github.com/feast-dev/feast/go/protos/feast/core"
+)
+
+var REGISTRY_SCHEMA_VERSION string = "1"
+var REGISTRY_STORE_CLASS_FOR_SCHEME map[string]string = map[string]string{
+ "gs": "GCSRegistryStore",
+ "s3": "S3RegistryStore",
+ "file": "FileRegistryStore",
+ "http": "HttpRegistryStore",
+ "https": "HttpRegistryStore",
+ "": "FileRegistryStore",
+}
+
+/*
+ Store protos of FeatureView, FeatureService, Entity, OnDemandFeatureView
+ but return to user copies of non-proto versions of these objects
+*/
+
+type Registry struct {
+ project string
+ registryStore RegistryStore
+ cachedFeatureServices map[string]map[string]*core.FeatureService
+ cachedEntities map[string]map[string]*core.Entity
+ cachedFeatureViews map[string]map[string]*core.FeatureView
+ cachedStreamFeatureViews map[string]map[string]*core.StreamFeatureView
+ CachedOnDemandFeatureViews map[string]map[string]*core.OnDemandFeatureView
+ cachedRegistry *core.Registry
+ cachedRegistryProtoLastUpdated time.Time
+ cachedRegistryProtoTtl time.Duration
+ mu sync.RWMutex
+}
+
+func NewRegistry(registryConfig *RegistryConfig, repoPath string, project string) (*Registry, error) {
+ registryStoreType := registryConfig.RegistryStoreType
+ registryPath := registryConfig.Path
+ r := &Registry{
+ project: project,
+ cachedRegistryProtoTtl: time.Duration(registryConfig.CacheTtlSeconds) * time.Second,
+ }
+
+ if len(registryStoreType) == 0 {
+ registryStore, err := getRegistryStoreFromScheme(registryPath, registryConfig, repoPath, project)
+ if err != nil {
+ return nil, err
+ }
+ r.registryStore = registryStore
+ } else {
+ registryStore, err := getRegistryStoreFromType(registryStoreType, registryConfig, repoPath, project)
+ if err != nil {
+ return nil, err
+ }
+ r.registryStore = registryStore
+ }
+
+ return r, nil
+}
+
+func (r *Registry) InitializeRegistry() error {
+ _, err := r.getRegistryProto()
+ if err != nil {
+ if _, ok := r.registryStore.(*HttpRegistryStore); ok {
+ log.Error().Err(err).Msg("Registry Initialization Failed")
+ return err
+ }
+ registryProto := &core.Registry{RegistrySchemaVersion: REGISTRY_SCHEMA_VERSION}
+ r.registryStore.UpdateRegistryProto(registryProto)
+ }
+ go r.RefreshRegistryOnInterval()
+ return nil
+}
+
+func (r *Registry) RefreshRegistryOnInterval() {
+ ticker := time.NewTicker(r.cachedRegistryProtoTtl)
+ for ; true; <-ticker.C {
+ err := r.refresh()
+ if err != nil {
+ log.Error().Stack().Err(err).Msg("Registry refresh Failed")
+ }
+ }
+}
+
+func (r *Registry) refresh() error {
+ _, err := r.getRegistryProto()
+ return err
+}
+
+func (r *Registry) getRegistryProto() (*core.Registry, error) {
+ expired := r.cachedRegistry == nil || (r.cachedRegistryProtoTtl > 0 && time.Now().After(r.cachedRegistryProtoLastUpdated.Add(r.cachedRegistryProtoTtl)))
+ if !expired {
+ return r.cachedRegistry, nil
+ }
+ registryProto, err := r.registryStore.GetRegistryProto()
+ if err != nil {
+ return nil, err
+ }
+ r.load(registryProto)
+ return registryProto, nil
+}
+
+func (r *Registry) load(registry *core.Registry) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ r.cachedRegistry = registry
+ r.cachedFeatureServices = make(map[string]map[string]*core.FeatureService)
+ r.cachedEntities = make(map[string]map[string]*core.Entity)
+ r.cachedFeatureViews = make(map[string]map[string]*core.FeatureView)
+ r.cachedStreamFeatureViews = make(map[string]map[string]*core.StreamFeatureView)
+ r.CachedOnDemandFeatureViews = make(map[string]map[string]*core.OnDemandFeatureView)
+ r.loadEntities(registry)
+ r.loadFeatureServices(registry)
+ r.loadFeatureViews(registry)
+ r.loadStreamFeatureViews(registry)
+ r.loadOnDemandFeatureViews(registry)
+ r.cachedRegistryProtoLastUpdated = time.Now()
+}
+
+func (r *Registry) loadEntities(registry *core.Registry) {
+ entities := registry.Entities
+ for _, entity := range entities {
+ if _, ok := r.cachedEntities[r.project]; !ok {
+ r.cachedEntities[r.project] = make(map[string]*core.Entity)
+ }
+ r.cachedEntities[r.project][entity.Spec.Name] = entity
+ }
+}
+
+func (r *Registry) loadFeatureServices(registry *core.Registry) {
+ featureServices := registry.FeatureServices
+ for _, featureService := range featureServices {
+ if _, ok := r.cachedFeatureServices[r.project]; !ok {
+ r.cachedFeatureServices[r.project] = make(map[string]*core.FeatureService)
+ }
+ r.cachedFeatureServices[r.project][featureService.Spec.Name] = featureService
+ }
+}
+
+func (r *Registry) loadFeatureViews(registry *core.Registry) {
+ featureViews := registry.FeatureViews
+ for _, featureView := range featureViews {
+ if _, ok := r.cachedFeatureViews[r.project]; !ok {
+ r.cachedFeatureViews[r.project] = make(map[string]*core.FeatureView)
+ }
+ r.cachedFeatureViews[r.project][featureView.Spec.Name] = featureView
+ }
+}
+
+func (r *Registry) loadStreamFeatureViews(registry *core.Registry) {
+ streamFeatureViews := registry.StreamFeatureViews
+ for _, streamFeatureView := range streamFeatureViews {
+ if _, ok := r.cachedStreamFeatureViews[r.project]; !ok {
+ r.cachedStreamFeatureViews[r.project] = make(map[string]*core.StreamFeatureView)
+ }
+ r.cachedStreamFeatureViews[r.project][streamFeatureView.Spec.Name] = streamFeatureView
+ }
+}
+
+func (r *Registry) loadOnDemandFeatureViews(registry *core.Registry) {
+ onDemandFeatureViews := registry.OnDemandFeatureViews
+ for _, onDemandFeatureView := range onDemandFeatureViews {
+ if _, ok := r.CachedOnDemandFeatureViews[r.project]; !ok {
+ r.CachedOnDemandFeatureViews[r.project] = make(map[string]*core.OnDemandFeatureView)
+ }
+ r.CachedOnDemandFeatureViews[r.project][onDemandFeatureView.Spec.Name] = onDemandFeatureView
+ }
+}
+
+/*
+ Look up Entities inside project
+ Returns empty list if project not found
+*/
+
+func (r *Registry) ListEntities(project string) ([]*model.Entity, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedEntities, ok := r.cachedEntities[project]; !ok {
+ return []*model.Entity{}, nil
+ } else {
+ entities := make([]*model.Entity, len(cachedEntities))
+ index := 0
+ for _, entityProto := range cachedEntities {
+ entities[index] = model.NewEntityFromProto(entityProto)
+ index += 1
+ }
+ return entities, nil
+ }
+}
+
+/*
+ Look up Feature Views inside project
+ Returns empty list if project not found
+*/
+
+func (r *Registry) ListFeatureViews(project string) ([]*model.FeatureView, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedFeatureViews, ok := r.cachedFeatureViews[project]; !ok {
+ return []*model.FeatureView{}, nil
+ } else {
+ featureViews := make([]*model.FeatureView, len(cachedFeatureViews))
+ index := 0
+ for _, featureViewProto := range cachedFeatureViews {
+ featureViews[index] = model.NewFeatureViewFromProto(featureViewProto)
+ index += 1
+ }
+ return featureViews, nil
+ }
+}
+
+/*
+ Look up Stream Feature Views inside project
+ Returns empty list if project not found
+*/
+
+func (r *Registry) ListStreamFeatureViews(project string) ([]*model.FeatureView, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedStreamFeatureViews, ok := r.cachedStreamFeatureViews[project]; !ok {
+ return []*model.FeatureView{}, nil
+ } else {
+ streamFeatureViews := make([]*model.FeatureView, len(cachedStreamFeatureViews))
+ index := 0
+ for _, streamFeatureViewProto := range cachedStreamFeatureViews {
+ streamFeatureViews[index] = model.NewFeatureViewFromStreamFeatureViewProto(streamFeatureViewProto)
+ index += 1
+ }
+ return streamFeatureViews, nil
+ }
+}
+
+/*
+ Look up Feature Services inside project
+ Returns empty list if project not found
+*/
+
+func (r *Registry) ListFeatureServices(project string) ([]*model.FeatureService, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedFeatureServices, ok := r.cachedFeatureServices[project]; !ok {
+ return []*model.FeatureService{}, nil
+ } else {
+ featureServices := make([]*model.FeatureService, len(cachedFeatureServices))
+ index := 0
+ for _, featureServiceProto := range cachedFeatureServices {
+ featureServices[index] = model.NewFeatureServiceFromProto(featureServiceProto)
+ index += 1
+ }
+ return featureServices, nil
+ }
+}
+
+/*
+ Look up On Demand Feature Views inside project
+ Returns empty list if project not found
+*/
+
+func (r *Registry) ListOnDemandFeatureViews(project string) ([]*model.OnDemandFeatureView, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedOnDemandFeatureViews, ok := r.CachedOnDemandFeatureViews[project]; !ok {
+ return []*model.OnDemandFeatureView{}, nil
+ } else {
+ onDemandFeatureViews := make([]*model.OnDemandFeatureView, len(cachedOnDemandFeatureViews))
+ index := 0
+ for _, onDemandFeatureViewProto := range cachedOnDemandFeatureViews {
+ onDemandFeatureViews[index] = model.NewOnDemandFeatureViewFromProto(onDemandFeatureViewProto)
+ index += 1
+ }
+ return onDemandFeatureViews, nil
+ }
+}
+
+func (r *Registry) GetEntity(project, entityName string) (*model.Entity, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedEntities, ok := r.cachedEntities[project]; !ok {
+ return nil, fmt.Errorf("no cached entities found for project %s", project)
+ } else {
+ if entity, ok := cachedEntities[entityName]; !ok {
+ return nil, fmt.Errorf("no cached entity %s found for project %s", entityName, project)
+ } else {
+ return model.NewEntityFromProto(entity), nil
+ }
+ }
+}
+
+func (r *Registry) GetFeatureView(project, featureViewName string) (*model.FeatureView, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedFeatureViews, ok := r.cachedFeatureViews[project]; !ok {
+ return nil, fmt.Errorf("no cached feature views found for project %s", project)
+ } else {
+ if featureViewProto, ok := cachedFeatureViews[featureViewName]; !ok {
+ return nil, fmt.Errorf("no cached feature view %s found for project %s", featureViewName, project)
+ } else {
+ return model.NewFeatureViewFromProto(featureViewProto), nil
+ }
+ }
+}
+
+func (r *Registry) GetStreamFeatureView(project, streamFeatureViewName string) (*model.FeatureView, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedStreamFeatureViews, ok := r.cachedStreamFeatureViews[project]; !ok {
+ return nil, fmt.Errorf("no cached stream feature views found for project %s", project)
+ } else {
+ if streamFeatureViewProto, ok := cachedStreamFeatureViews[streamFeatureViewName]; !ok {
+ return nil, fmt.Errorf("no cached stream feature view %s found for project %s", streamFeatureViewName, project)
+ } else {
+ return model.NewFeatureViewFromStreamFeatureViewProto(streamFeatureViewProto), nil
+ }
+ }
+}
+
+func (r *Registry) GetFeatureService(project, featureServiceName string) (*model.FeatureService, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedFeatureServices, ok := r.cachedFeatureServices[project]; !ok {
+ return nil, fmt.Errorf("no cached feature services found for project %s", project)
+ } else {
+ if featureServiceProto, ok := cachedFeatureServices[featureServiceName]; !ok {
+ return nil, fmt.Errorf("no cached feature service %s found for project %s", featureServiceName, project)
+ } else {
+ return model.NewFeatureServiceFromProto(featureServiceProto), nil
+ }
+ }
+}
+
+func (r *Registry) GetOnDemandFeatureView(project, onDemandFeatureViewName string) (*model.OnDemandFeatureView, error) {
+ r.mu.RLock()
+ defer r.mu.RUnlock()
+ if cachedOnDemandFeatureViews, ok := r.CachedOnDemandFeatureViews[project]; !ok {
+ return nil, fmt.Errorf("no cached on demand feature views found for project %s", project)
+ } else {
+ if onDemandFeatureViewProto, ok := cachedOnDemandFeatureViews[onDemandFeatureViewName]; !ok {
+ return nil, fmt.Errorf("no cached on demand feature view %s found for project %s", onDemandFeatureViewName, project)
+ } else {
+ return model.NewOnDemandFeatureViewFromProto(onDemandFeatureViewProto), nil
+ }
+ }
+}
+
+func getRegistryStoreFromScheme(registryPath string, registryConfig *RegistryConfig, repoPath string, project string) (RegistryStore, error) {
+ uri, err := url.Parse(registryPath)
+ if err != nil {
+ return nil, err
+ }
+ if registryStoreType, ok := REGISTRY_STORE_CLASS_FOR_SCHEME[uri.Scheme]; ok {
+ return getRegistryStoreFromType(registryStoreType, registryConfig, repoPath, project)
+ }
+ return nil, fmt.Errorf("registry path %s has unsupported scheme %s. Supported schemes are file, s3 and gs", registryPath, uri.Scheme)
+}
+
+func getRegistryStoreFromType(registryStoreType string, registryConfig *RegistryConfig, repoPath string, project string) (RegistryStore, error) {
+ switch registryStoreType {
+ case "FileRegistryStore":
+ return NewFileRegistryStore(registryConfig, repoPath), nil
+ case "HttpRegistryStore":
+ return NewHttpRegistryStore(registryConfig, project)
+ }
+ return nil, errors.New("only FileRegistryStore or HttpRegistryStore as a RegistryStore is supported at this moment")
+}
+
+
+
package registry
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/feast-dev/feast/go/internal/feast/server/logging"
+ "github.com/ghodss/yaml"
+)
+
+const (
+ defaultCacheTtlSeconds = int64(600)
+ defaultClientID = "Unknown"
+)
+
+type RepoConfig struct {
+ // Feast project name
+ Project string `json:"project"`
+ // Feast provider name
+ Provider string `json:"provider"`
+ // Path to the registry. Custom registry loaders are not yet supported
+ // Registry string `json:"registry"`
+ Registry interface{} `json:"registry"`
+ // Online store config
+ OnlineStore map[string]interface{} `json:"online_store"`
+ // Offline store config
+ OfflineStore map[string]interface{} `json:"offline_store"`
+ // Feature server config (currently unrelated to Go server)
+ FeatureServer map[string]interface{} `json:"feature_server"`
+ // Feature flags for experimental features
+ Flags map[string]interface{} `json:"flags"`
+ // RepoPath
+ RepoPath string `json:"repo_path"`
+ // EntityKeySerializationVersion
+ EntityKeySerializationVersion int64 `json:"entity_key_serialization_version"`
+}
+
+type RegistryConfig struct {
+ RegistryStoreType string `json:"registry_store_type"`
+ Path string `json:"path"`
+ ClientId string `json:"client_id" default:"Unknown"`
+ CacheTtlSeconds int64 `json:"cache_ttl_seconds" default:"600"`
+}
+
+// NewRepoConfigFromJSON converts a JSON string into a RepoConfig struct and also sets the repo path.
+func NewRepoConfigFromJSON(repoPath, configJSON string) (*RepoConfig, error) {
+ config := RepoConfig{}
+ if err := json.Unmarshal([]byte(configJSON), &config); err != nil {
+ return nil, err
+ }
+ repoPath, err := filepath.Abs(repoPath)
+ if err != nil {
+ return nil, err
+ }
+ config.RepoPath = repoPath
+ return &config, nil
+}
+
+// NewRepoConfigFromFile reads the `feature_store.yaml` file in the repo path and converts it
+// into a RepoConfig struct.
+func NewRepoConfigFromFile(repoPath string) (*RepoConfig, error) {
+ data, err := os.ReadFile(filepath.Join(repoPath, "feature_store.yaml"))
+ if err != nil {
+ return nil, err
+ }
+ repoPath, err = filepath.Abs(repoPath)
+ if err != nil {
+ return nil, err
+ }
+
+ repoConfigWithEnv := os.ExpandEnv(string(data))
+
+ config := RepoConfig{}
+ if err = yaml.Unmarshal([]byte(repoConfigWithEnv), &config); err != nil {
+ return nil, err
+ }
+ config.RepoPath = repoPath
+ return &config, nil
+}
+
+func (r *RepoConfig) GetLoggingOptions() (*logging.LoggingOptions, error) {
+ loggingOptions := logging.LoggingOptions{}
+ if loggingOptionsMap, ok := r.FeatureServer["feature_logging"].(map[string]interface{}); ok {
+ loggingOptions = logging.DefaultOptions
+ for k, v := range loggingOptionsMap {
+ switch k {
+ case "queue_capacity":
+ if value, ok := v.(int); ok {
+ loggingOptions.ChannelCapacity = value
+ }
+ case "emit_timeout_micro_secs":
+ if value, ok := v.(int); ok {
+ loggingOptions.EmitTimeout = time.Duration(value) * time.Microsecond
+ }
+ case "write_to_disk_interval_secs":
+ if value, ok := v.(int); ok {
+ loggingOptions.WriteInterval = time.Duration(value) * time.Second
+ }
+ case "flush_interval_secs":
+ if value, ok := v.(int); ok {
+ loggingOptions.FlushInterval = time.Duration(value) * time.Second
+ }
+ }
+ }
+ }
+ return &loggingOptions, nil
+}
+
+func (r *RepoConfig) GetRegistryConfig() (*RegistryConfig, error) {
+ if registryConfigMap, ok := r.Registry.(map[string]interface{}); ok {
+ registryConfig := RegistryConfig{CacheTtlSeconds: defaultCacheTtlSeconds, ClientId: defaultClientID}
+ for k, v := range registryConfigMap {
+ switch k {
+ case "path":
+ if value, ok := v.(string); ok {
+ registryConfig.Path = value
+ }
+ case "registry_store_type":
+ if value, ok := v.(string); ok {
+ registryConfig.RegistryStoreType = value
+ }
+ case "client_id":
+ if value, ok := v.(string); ok {
+ registryConfig.ClientId = value
+ }
+ case "cache_ttl_seconds":
+ // cache_ttl_seconds defaulted to type float64. Ex: "cache_ttl_seconds": 60 in registryConfigMap
+ switch value := v.(type) {
+ case float64:
+ registryConfig.CacheTtlSeconds = int64(value)
+ case int:
+ registryConfig.CacheTtlSeconds = int64(value)
+ case int32:
+ registryConfig.CacheTtlSeconds = int64(value)
+ case int64:
+ registryConfig.CacheTtlSeconds = value
+ default:
+ return nil, fmt.Errorf("unexpected type %T for CacheTtlSeconds", v)
+ }
+ }
+ }
+ return ®istryConfig, nil
+ } else {
+ return &RegistryConfig{Path: r.Registry.(string), ClientId: defaultClientID, CacheTtlSeconds: defaultCacheTtlSeconds}, nil
+ }
+}
+
+
+
package server
+
+import (
+ "context"
+ "fmt"
+ "github.com/feast-dev/feast/go/internal/feast"
+ "github.com/feast-dev/feast/go/internal/feast/server/logging"
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ prototypes "github.com/feast-dev/feast/go/protos/feast/types"
+ "github.com/feast-dev/feast/go/types"
+ "github.com/google/uuid"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+)
+
+const feastServerVersion = "0.0.1"
+
+type grpcServingServiceServer struct {
+ fs *feast.FeatureStore
+ loggingService *logging.LoggingService
+ serving.UnimplementedServingServiceServer
+}
+
+func NewGrpcServingServiceServer(fs *feast.FeatureStore, loggingService *logging.LoggingService) *grpcServingServiceServer {
+ return &grpcServingServiceServer{fs: fs, loggingService: loggingService}
+}
+
+func (s *grpcServingServiceServer) GetFeastServingInfo(ctx context.Context, request *serving.GetFeastServingInfoRequest) (*serving.GetFeastServingInfoResponse, error) {
+ return &serving.GetFeastServingInfoResponse{
+ Version: feastServerVersion,
+ }, nil
+}
+
+// GetOnlineFeatures Returns an object containing the response to GetOnlineFeatures.
+// Metadata contains feature names that corresponds to the number of rows in response.Results.
+// Results contains values including the value of the feature, the event timestamp, and feature status in a columnar format.
+func (s *grpcServingServiceServer) GetOnlineFeatures(ctx context.Context, request *serving.GetOnlineFeaturesRequest) (*serving.GetOnlineFeaturesResponse, error) {
+
+ span, ctx := tracer.StartSpanFromContext(ctx, "getOnlineFeatures", tracer.ResourceName("ServingService/GetOnlineFeatures"))
+ defer span.Finish()
+
+ logSpanContext := LogWithSpanContext(span)
+
+ requestId := GenerateRequestId()
+ featuresOrService, err := s.fs.ParseFeatures(request.GetKind())
+
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error parsing feature service or feature list from request")
+ return nil, err
+ }
+
+ featureVectors, err := s.fs.GetOnlineFeatures(
+ ctx,
+ featuresOrService.FeaturesRefs,
+ featuresOrService.FeatureService,
+ request.GetEntities(),
+ request.GetRequestContext(),
+ request.GetFullFeatureNames())
+
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error getting online features")
+ return nil, err
+ }
+
+ resp := &serving.GetOnlineFeaturesResponse{
+ Results: make([]*serving.GetOnlineFeaturesResponse_FeatureVector, 0),
+ Metadata: &serving.GetOnlineFeaturesResponseMetadata{
+ FeatureNames: &serving.FeatureList{Val: make([]string, 0)},
+ },
+ }
+ // JoinKeys are currently part of the features as a value and the order that we add it to the resp MetaData
+ // Need to figure out a way to map the correct entities to the correct ordering
+ entityValuesMap := make(map[string][]*prototypes.Value, 0)
+ featureNames := make([]string, len(featureVectors))
+ for idx, vector := range featureVectors {
+ resp.Metadata.FeatureNames.Val = append(resp.Metadata.FeatureNames.Val, vector.Name)
+ featureNames[idx] = vector.Name
+ values, err := types.ArrowValuesToProtoValues(vector.Values)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error converting Arrow values to proto values")
+ return nil, err
+ }
+ if _, ok := request.Entities[vector.Name]; ok {
+ entityValuesMap[vector.Name] = values
+ }
+
+ resp.Results = append(resp.Results, &serving.GetOnlineFeaturesResponse_FeatureVector{
+ Values: values,
+ Statuses: vector.Statuses,
+ EventTimestamps: vector.Timestamps,
+ })
+ }
+
+ featureService := featuresOrService.FeatureService
+ if featureService != nil && featureService.LoggingConfig != nil && s.loggingService != nil {
+ logger, err := s.loggingService.GetOrCreateLogger(featureService)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error to instantiating logger for feature service: " + featuresOrService.FeatureService.Name)
+ fmt.Printf("Couldn't instantiate logger for feature service %s: %+v", featuresOrService.FeatureService.Name, err)
+ }
+
+ err = logger.Log(request.Entities, resp.Results[len(request.Entities):], resp.Metadata.FeatureNames.Val[len(request.Entities):], request.RequestContext, requestId)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error to logging to feature service: " + featuresOrService.FeatureService.Name)
+ fmt.Printf("LoggerImpl error[%s]: %+v", featuresOrService.FeatureService.Name, err)
+ }
+ }
+ return resp, nil
+}
+
+func GenerateRequestId() string {
+ id := uuid.New()
+ return id.String()
+}
+
+
+
package server
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/feast-dev/feast/go/internal/feast"
+ "github.com/feast-dev/feast/go/internal/feast/model"
+ "github.com/feast-dev/feast/go/internal/feast/onlineserving"
+ "github.com/feast-dev/feast/go/internal/feast/server/logging"
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ prototypes "github.com/feast-dev/feast/go/protos/feast/types"
+ "github.com/feast-dev/feast/go/types"
+ "github.com/rs/zerolog/log"
+ httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+)
+
+type httpServer struct {
+ fs *feast.FeatureStore
+ loggingService *logging.LoggingService
+ server *http.Server
+}
+
+// Some Feast types aren't supported during JSON conversion
+type repeatedValue struct {
+ stringVal []string
+ int32Val []int32
+ int64Val []int64
+ doubleVal []float64
+ boolVal []bool
+ stringListVal [][]string
+ int32ListVal [][]int32
+ int64ListVal [][]int64
+ doubleListVal [][]float64
+ boolListVal [][]bool
+}
+
+func (u *repeatedValue) UnmarshalJSON(data []byte) error {
+ isString := false
+ isDouble := false
+ isInt64 := false
+ isArray := false
+ openBraketCounter := 0
+ for _, b := range data {
+ if b == '"' {
+ isString = true
+ }
+ if b == '.' {
+ isDouble = true
+ }
+ if b >= '0' && b <= '9' {
+ isInt64 = true
+ }
+ if b == '[' {
+ openBraketCounter++
+ if openBraketCounter > 1 {
+ isArray = true
+ }
+ }
+ }
+ var err error
+ if !isArray {
+ if isString {
+ err = json.Unmarshal(data, &u.stringVal)
+ } else if isDouble {
+ err = json.Unmarshal(data, &u.doubleVal)
+ } else if isInt64 {
+ err = json.Unmarshal(data, &u.int64Val)
+ } else {
+ err = json.Unmarshal(data, &u.boolVal)
+ }
+ } else {
+ if isString {
+ err = json.Unmarshal(data, &u.stringListVal)
+ } else if isDouble {
+ err = json.Unmarshal(data, &u.doubleListVal)
+ } else if isInt64 {
+ err = json.Unmarshal(data, &u.int64ListVal)
+ } else {
+ err = json.Unmarshal(data, &u.boolListVal)
+ }
+ }
+ return err
+}
+
+func (u *repeatedValue) ToProto() *prototypes.RepeatedValue {
+ proto := new(prototypes.RepeatedValue)
+ if u.stringVal != nil {
+ for _, val := range u.stringVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_StringVal{StringVal: val}})
+ }
+ }
+ if u.int64Val != nil {
+ for _, val := range u.int64Val {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int64Val{Int64Val: val}})
+ }
+ }
+ if u.int32Val != nil {
+ for _, val := range u.int32Val {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int32Val{Int32Val: val}})
+ }
+ }
+ if u.doubleVal != nil {
+ for _, val := range u.doubleVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_DoubleVal{DoubleVal: val}})
+ }
+ }
+ if u.boolVal != nil {
+ for _, val := range u.boolVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_BoolVal{BoolVal: val}})
+ }
+ }
+ if u.stringListVal != nil {
+ for _, val := range u.stringListVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_StringListVal{StringListVal: &prototypes.StringList{Val: val}}})
+ }
+ }
+ if u.int32ListVal != nil {
+ for _, val := range u.int32ListVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int32ListVal{Int32ListVal: &prototypes.Int32List{Val: val}}})
+ }
+ }
+ if u.int64ListVal != nil {
+ for _, val := range u.int64ListVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int64ListVal{Int64ListVal: &prototypes.Int64List{Val: val}}})
+ }
+ }
+ if u.doubleListVal != nil {
+ for _, val := range u.doubleListVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_DoubleListVal{DoubleListVal: &prototypes.DoubleList{Val: val}}})
+ }
+ }
+ if u.boolListVal != nil {
+ for _, val := range u.boolListVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_BoolListVal{BoolListVal: &prototypes.BoolList{Val: val}}})
+ }
+ }
+ return proto
+}
+
+type getOnlineFeaturesRequest struct {
+ FeatureService *string `json:"feature_service"`
+ Features []string `json:"features"`
+ Entities map[string]repeatedValue `json:"entities"`
+ FullFeatureNames bool `json:"full_feature_names"`
+ RequestContext map[string]repeatedValue `json:"request_context"`
+}
+
+func NewHttpServer(fs *feast.FeatureStore, loggingService *logging.LoggingService) *httpServer {
+ return &httpServer{fs: fs, loggingService: loggingService}
+}
+
+/*
+*
+Used to align a field specified in the request with its defined schema type.
+*/
+func typecastToFieldSchemaType(val *repeatedValue, fieldType prototypes.ValueType_Enum) {
+ if val.int64Val != nil {
+ if fieldType == prototypes.ValueType_INT32 {
+ for _, v := range val.int64Val {
+ val.int32Val = append(val.int32Val, int32(v))
+ }
+ val.int64Val = nil
+ }
+ }
+}
+
+func (s *httpServer) getOnlineFeatures(w http.ResponseWriter, r *http.Request) {
+ var err error
+
+ span, ctx := tracer.StartSpanFromContext(r.Context(), "getOnlineFeatures", tracer.ResourceName("/get-online-features"))
+ defer span.Finish(tracer.WithError(err))
+
+ logSpanContext := LogWithSpanContext(span)
+
+ if r.Method != "POST" {
+ http.NotFound(w, r)
+ return
+ }
+
+ statusQuery := r.URL.Query().Get("status")
+
+ status := false
+ if statusQuery != "" {
+ status, err = strconv.ParseBool(statusQuery)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error parsing status query parameter")
+ writeJSONError(w, fmt.Errorf("Error parsing status query parameter: %+v", err), http.StatusBadRequest)
+ return
+ }
+ }
+
+ decoder := json.NewDecoder(r.Body)
+ var request getOnlineFeaturesRequest
+ err = decoder.Decode(&request)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error decoding JSON request data")
+ writeJSONError(w, fmt.Errorf("Error decoding JSON request data: %+v", err), http.StatusInternalServerError)
+ return
+ }
+ var featureService *model.FeatureService
+ var entitiesProto = make(map[string]*prototypes.RepeatedValue)
+ var requestContextProto = make(map[string]*prototypes.RepeatedValue)
+ var odfVList = make([]*model.OnDemandFeatureView, 0)
+ var requestSources = make(map[string]prototypes.ValueType_Enum)
+
+ if request.FeatureService != nil && *request.FeatureService != "" {
+ featureService, err = s.fs.GetFeatureService(*request.FeatureService)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error getting feature service from registry")
+ writeJSONError(w, fmt.Errorf("Error getting feature service from registry: %+v", err), http.StatusInternalServerError)
+ return
+ }
+ for _, fv := range featureService.Projections {
+ odfv, _ := s.fs.GetOnDemandFeatureView(fv.Name)
+ if odfv != nil {
+ odfVList = append(odfVList, odfv)
+ }
+ }
+ } else if len(request.Features) > 0 {
+ log.Info().Msgf("request.Features %v", request.Features)
+ for _, featureName := range request.Features {
+ _, _, err := onlineserving.ParseFeatureReference(featureName)
+ if err != nil {
+ logSpanContext.Error().Err(err)
+ writeJSONError(w, fmt.Errorf("Error parsing feature reference %s", featureName), http.StatusBadRequest)
+ return
+ }
+ fv, odfv, _ := s.fs.ListAllViews()
+ if _, ok1 := odfv[featureName]; ok1 {
+ odfVList = append(odfVList, odfv[featureName])
+ } else if _, ok1 := fv[featureName]; !ok1 {
+ logSpanContext.Error().Msg("Feature View not found")
+ writeJSONError(w, fmt.Errorf("Feature View %s not found", featureName), http.StatusInternalServerError)
+ return
+ }
+ }
+ } else {
+ logSpanContext.Error().Msg("No Feature Views or Feature Services specified in the request")
+ writeJSONError(w, errors.New("No Feature Views or Feature Services specified in the request"), http.StatusBadRequest)
+ return
+ }
+ if odfVList != nil {
+ requestSources, _ = s.fs.GetRequestSources(odfVList)
+ }
+ if len(request.Entities) > 0 {
+ var entityType prototypes.ValueType_Enum
+ for key, value := range request.Entities {
+ entity, err := s.fs.GetEntity(key, false)
+ if err != nil {
+ if requestSources == nil {
+ logSpanContext.Error().Msgf("Entity %s not found ", key)
+ writeJSONError(w, fmt.Errorf("Entity %s not found ", key), http.StatusNotFound)
+ return
+ }
+ requestSourceType, ok := requestSources[key]
+ if !ok {
+ logSpanContext.Error().Msgf("Entity nor Request Source of name %s not found ", key)
+ writeJSONError(w, fmt.Errorf("Entity nor Request Source of name %s not found ", key), http.StatusNotFound)
+ return
+ }
+ entityType = requestSourceType
+ } else {
+ entityType = entity.ValueType
+ }
+ typecastToFieldSchemaType(&value, entityType)
+ entitiesProto[key] = value.ToProto()
+ }
+ }
+ if request.RequestContext != nil && len(request.RequestContext) > 0 {
+ for key, value := range request.RequestContext {
+ requestSourceType, ok := requestSources[key]
+ if !ok {
+ logSpanContext.Error().Msgf("Request Source %s not found ", key)
+ writeJSONError(w, fmt.Errorf("Request Source %s not found ", key), http.StatusNotFound)
+ return
+ }
+ typecastToFieldSchemaType(&value, requestSourceType)
+ requestContextProto[key] = value.ToProto()
+ }
+ }
+
+ featureVectors, err := s.fs.GetOnlineFeatures(
+ ctx,
+ request.Features,
+ featureService,
+ entitiesProto,
+ requestContextProto,
+ request.FullFeatureNames)
+
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error getting feature vector")
+ writeJSONError(w, fmt.Errorf("Error getting feature vector: %+v", err), http.StatusInternalServerError)
+ return
+ }
+
+ var featureNames []string
+ var results []map[string]interface{}
+ for _, vector := range featureVectors {
+ featureNames = append(featureNames, vector.Name)
+ result := make(map[string]interface{})
+ if status {
+ var statuses []string
+ for _, status := range vector.Statuses {
+ statuses = append(statuses, status.String())
+ }
+ var timestamps []string
+ for _, timestamp := range vector.Timestamps {
+ timestamps = append(timestamps, timestamp.AsTime().Format(time.RFC3339))
+ }
+
+ result["statuses"] = statuses
+ result["event_timestamps"] = timestamps
+ }
+ // Note, that vector.Values is an Arrow Array, but this type implements JSON Marshaller.
+ // So, it's not necessary to pre-process it in any way.
+ result["values"] = vector.Values
+
+ results = append(results, result)
+ }
+
+ response := map[string]interface{}{
+ "metadata": map[string]interface{}{
+ "feature_names": featureNames,
+ },
+ "results": results,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+
+ err = json.NewEncoder(w).Encode(response)
+
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Error encoding response")
+ writeJSONError(w, fmt.Errorf("Error encoding response: %+v", err), http.StatusInternalServerError)
+ return
+ }
+
+ if featureService != nil && featureService.LoggingConfig != nil && s.loggingService != nil {
+ logger, err := s.loggingService.GetOrCreateLogger(featureService)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msgf("Couldn't instantiate logger for feature service %s", featureService.Name)
+ writeJSONError(w, fmt.Errorf("Couldn't instantiate logger for feature service %s: %+v", featureService.Name, err), http.StatusInternalServerError)
+ return
+ }
+
+ requestId := GenerateRequestId()
+
+ // Note: we're converting arrow to proto for feature logging. In the future we should
+ // base feature logging on arrow so that we don't have to do this extra conversion.
+ var featureVectorProtos []*serving.GetOnlineFeaturesResponse_FeatureVector
+ for _, vector := range featureVectors[len(request.Entities):] {
+ values, err := types.ArrowValuesToProtoValues(vector.Values)
+ if err != nil {
+ logSpanContext.Error().Err(err).Msg("Couldn't convert arrow values into protobuf")
+ writeJSONError(w, fmt.Errorf("Couldn't convert arrow values into protobuf: %+v", err), http.StatusInternalServerError)
+ return
+ }
+ featureVectorProtos = append(featureVectorProtos, &serving.GetOnlineFeaturesResponse_FeatureVector{
+ Values: values,
+ Statuses: vector.Statuses,
+ EventTimestamps: vector.Timestamps,
+ })
+ }
+
+ err = logger.Log(entitiesProto, featureVectorProtos, featureNames[len(request.Entities):], requestContextProto, requestId)
+ if err != nil {
+ writeJSONError(w, fmt.Errorf("LoggerImpl error[%s]: %+v", featureService.Name, err), http.StatusInternalServerError)
+ return
+ }
+ }
+
+ go releaseCGOMemory(featureVectors)
+}
+
+func releaseCGOMemory(featureVectors []*onlineserving.FeatureVector) {
+ for _, vector := range featureVectors {
+ vector.Values.Release()
+ }
+}
+
+func logStackTrace() {
+ // Start with a small buffer and grow it until the full stack trace fits.
+ buf := make([]byte, 1024)
+ for {
+ stackSize := runtime.Stack(buf, false)
+ if stackSize < len(buf) {
+ // The stack trace fits in the buffer, so we can log it now.
+ log.Error().Str("stack_trace", string(buf[:stackSize])).Msg("")
+ return
+ }
+ // The stack trace doesn't fit in the buffer, so we need to grow the buffer and try again.
+ buf = make([]byte, 2*len(buf))
+ }
+}
+
+func writeJSONError(w http.ResponseWriter, err error, statusCode int) {
+ errMap := map[string]interface{}{
+ "error": fmt.Sprintf("%+v", err),
+ "status_code": statusCode,
+ }
+ errJSON, _ := json.Marshal(errMap)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(statusCode)
+ w.Write(errJSON)
+}
+
+func recoverMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Error().Err(fmt.Errorf("Panic recovered: %v", r)).Msg("A panic occurred in the server")
+ // Log the stack trace
+ logStackTrace()
+
+ writeJSONError(w, fmt.Errorf("Internal Server Error: %v", r), http.StatusInternalServerError)
+ }
+ }()
+ next.ServeHTTP(w, r)
+ })
+}
+
+func (s *httpServer) Serve(host string, port int) error {
+ if strings.ToLower(os.Getenv("ENABLE_DATADOG_TRACING")) == "true" {
+ tracer.Start(tracer.WithRuntimeMetrics())
+ defer tracer.Stop()
+ }
+ mux := httptrace.NewServeMux()
+ mux.Handle("/get-online-features", recoverMiddleware(http.HandlerFunc(s.getOnlineFeatures)))
+ mux.HandleFunc("/health", healthCheckHandler)
+ s.server = &http.Server{Addr: fmt.Sprintf("%s:%d", host, port), Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 15 * time.Second}
+ err := s.server.ListenAndServe()
+ // Don't return the error if it's caused by graceful shutdown using Stop()
+ if err == http.ErrServerClosed {
+ return nil
+ }
+ log.Fatal().Stack().Err(err).Msg("Failed to start HTTP server")
+ return err
+}
+
+func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "Healthy")
+}
+func (s *httpServer) Stop() error {
+ if s.server != nil {
+ return s.server.Shutdown(context.Background())
+ }
+ return nil
+}
+
+
+
package logging
+
+import (
+ "fmt"
+
+ "github.com/feast-dev/feast/go/internal/feast/model"
+ "github.com/feast-dev/feast/go/protos/feast/types"
+)
+
+type FeatureServiceSchema struct {
+ JoinKeys []string
+ Features []string
+ RequestData []string
+
+ JoinKeysTypes map[string]types.ValueType_Enum
+ FeaturesTypes map[string]types.ValueType_Enum
+ RequestDataTypes map[string]types.ValueType_Enum
+}
+
+func GenerateSchemaFromFeatureService(fs FeatureStore, featureServiceName string) (*FeatureServiceSchema, error) {
+ entityMap, fvMap, odFvMap, err := fs.GetFcosMap()
+ if err != nil {
+ return nil, err
+ }
+
+ featureService, err := fs.GetFeatureService(featureServiceName)
+ if err != nil {
+ return nil, err
+ }
+
+ return generateSchema(featureService, entityMap, fvMap, odFvMap)
+}
+
+func generateSchema(featureService *model.FeatureService, entityMap map[string]*model.Entity, fvMap map[string]*model.FeatureView, odFvMap map[string]*model.OnDemandFeatureView) (*FeatureServiceSchema, error) {
+ joinKeys := make([]string, 0)
+ features := make([]string, 0)
+ requestData := make([]string, 0)
+
+ joinKeysSet := make(map[string]interface{})
+
+ entityJoinKeyToType := make(map[string]types.ValueType_Enum)
+ allFeatureTypes := make(map[string]types.ValueType_Enum)
+ requestDataTypes := make(map[string]types.ValueType_Enum)
+
+ for _, featureProjection := range featureService.Projections {
+ // Create copies of FeatureView that may contains the same *FeatureView but
+ // each differentiated by a *FeatureViewProjection
+ featureViewName := featureProjection.Name
+ if fv, ok := fvMap[featureViewName]; ok {
+ for _, f := range featureProjection.Features {
+ fullFeatureName := getFullFeatureName(featureProjection.NameToUse(), f.Name)
+ features = append(features, fullFeatureName)
+ allFeatureTypes[fullFeatureName] = f.Dtype
+ }
+ for _, entityColumn := range fv.EntityColumns {
+ var joinKey string
+ if joinKeyAlias, ok := featureProjection.JoinKeyMap[entityColumn.Name]; ok {
+ joinKey = joinKeyAlias
+ } else {
+ joinKey = entityColumn.Name
+ }
+
+ if _, ok := joinKeysSet[joinKey]; !ok {
+ joinKeys = append(joinKeys, joinKey)
+ }
+
+ joinKeysSet[joinKey] = nil
+ entityJoinKeyToType[joinKey] = entityColumn.Dtype
+ }
+ } else if odFv, ok := odFvMap[featureViewName]; ok {
+ for _, f := range featureProjection.Features {
+ fullFeatureName := getFullFeatureName(featureProjection.NameToUse(), f.Name)
+ features = append(features, fullFeatureName)
+ allFeatureTypes[fullFeatureName] = f.Dtype
+ }
+ for paramName, paramType := range odFv.GetRequestDataSchema() {
+ requestData = append(requestData, paramName)
+ requestDataTypes[paramName] = paramType
+ }
+ } else {
+ return nil, fmt.Errorf("no such feature view %s found (referenced from feature service %s)",
+ featureViewName, featureService.Name)
+ }
+ }
+
+ schema := &FeatureServiceSchema{
+ JoinKeys: joinKeys,
+ Features: features,
+ RequestData: requestData,
+
+ JoinKeysTypes: entityJoinKeyToType,
+ FeaturesTypes: allFeatureTypes,
+ RequestDataTypes: requestDataTypes,
+ }
+ return schema, nil
+}
+
+
+
package logging
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/pkg/errors"
+
+ "github.com/apache/arrow/go/v8/arrow"
+ "github.com/google/uuid"
+
+ "github.com/apache/arrow/go/v8/arrow/array"
+ "github.com/apache/arrow/go/v8/parquet"
+ "github.com/apache/arrow/go/v8/parquet/pqarrow"
+)
+
+type FileLogSink struct {
+ path string
+}
+
+// FileLogSink is currently only used for testing. It will be instantiated during go unit tests to log to file
+// and the parquet files will be cleaned up after the test is run.
+func NewFileLogSink(path string) (*FileLogSink, error) {
+ if path == "" {
+ return nil, errors.New("need path for file log sink")
+ }
+
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ return nil, err
+ }
+ return &FileLogSink{path: absPath}, nil
+}
+
+func (s *FileLogSink) Write(records []arrow.Record) error {
+ fileName, _ := uuid.NewUUID()
+
+ var writer io.Writer
+ writer, err := os.Create(filepath.Join(s.path, fmt.Sprintf("%s.parquet", fileName.String())))
+ if err != nil {
+ return err
+ }
+ table := array.NewTableFromRecords(records[0].Schema(), records)
+
+ props := parquet.NewWriterProperties(parquet.WithDictionaryDefault(false))
+ arrProps := pqarrow.DefaultWriterProps()
+ return pqarrow.WriteTable(table, writer, 100, props, arrProps)
+}
+
+func (s *FileLogSink) Flush(featureServiceName string) error {
+ // files are already flushed during Write
+ return nil
+}
+
+
+
package logging
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/apache/arrow/go/v8/arrow"
+ "github.com/pkg/errors"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ "github.com/feast-dev/feast/go/protos/feast/types"
+)
+
+type Log struct {
+ // Example: val{int64_val: 5017}, val{int64_val: 1003}
+ EntityValue []*types.Value
+ RequestData []*types.Value
+
+ FeatureValues []*types.Value
+ FeatureStatuses []serving.FieldStatus
+ EventTimestamps []*timestamppb.Timestamp
+
+ RequestId string
+ LogTimestamp time.Time
+}
+
+type LogSink interface {
+ // Write is used to unload logs from memory buffer.
+ // Logs are not guaranteed to be flushed to sink on this point.
+ // The data can just be written to local disk (depending on implementation).
+ Write(data []arrow.Record) error
+
+ // Flush actually send data to a sink.
+ // We want to control amount to interaction with sink, since it could be a costly operation.
+ // Also, some sinks like BigQuery might have quotes and physically limit amount of write requests per day.
+ Flush(featureServiceName string) error
+}
+
+type Logger interface {
+ Log(joinKeyToEntityValues map[string]*types.RepeatedValue, featureVectors []*serving.GetOnlineFeaturesResponse_FeatureVector, featureNames []string, requestData map[string]*types.RepeatedValue, requestId string) error
+}
+
+type LoggerImpl struct {
+ featureServiceName string
+
+ buffer *MemoryBuffer
+ schema *FeatureServiceSchema
+
+ logCh chan *Log
+ signalCh chan interface{}
+
+ sink LogSink
+ config LoggerConfig
+
+ isStopped bool
+ cond *sync.Cond
+}
+
+type LoggerConfig struct {
+ LoggingOptions
+
+ SampleRate float32
+}
+
+func NewLoggerConfig(sampleRate float32, opts LoggingOptions) LoggerConfig {
+ return LoggerConfig{
+ LoggingOptions: opts,
+ SampleRate: sampleRate,
+ }
+}
+
+func NewLogger(schema *FeatureServiceSchema, featureServiceName string, sink LogSink, config LoggerConfig) (*LoggerImpl, error) {
+ buffer, err := NewMemoryBuffer(schema)
+ if err != nil {
+ return nil, err
+ }
+ logger := &LoggerImpl{
+ featureServiceName: featureServiceName,
+
+ logCh: make(chan *Log, config.ChannelCapacity),
+ signalCh: make(chan interface{}, 2),
+ sink: sink,
+
+ buffer: buffer,
+ schema: schema,
+ config: config,
+
+ isStopped: false,
+ cond: sync.NewCond(&sync.Mutex{}),
+ }
+
+ logger.startLoggerLoop()
+ return logger, nil
+}
+
+func (l *LoggerImpl) EmitLog(log *Log) error {
+ select {
+ case l.logCh <- log:
+ return nil
+ case <-time.After(l.config.EmitTimeout):
+ return fmt.Errorf("could not add to log channel with capacity %d. Operation timed out. Current log channel length is %d", cap(l.logCh), len(l.logCh))
+ }
+}
+
+func (l *LoggerImpl) startLoggerLoop() {
+ go func() {
+ for {
+ if err := l.loggerLoop(); err != nil {
+ log.Printf("LoggerImpl[%s] recovered from panic: %+v", l.featureServiceName, err)
+
+ // Sleep for a couple of milliseconds to avoid CPU load from a potential infinite panic-recovery loop
+ time.Sleep(5 * time.Millisecond)
+ continue // try again
+ }
+
+ // graceful stop
+ return
+ }
+ }()
+}
+
+// Select that either ingests new logs that are added to the logging channel, one at a time to add
+// to the in-memory buffer or flushes all of them synchronously to the OfflineStorage on a time interval.
+func (l *LoggerImpl) loggerLoop() (lErr error) {
+ defer func() {
+ // Recover from panic in the logger loop, so that it doesn't bring down the entire feature server
+ if r := recover(); r != nil {
+ rErr, ok := r.(error)
+ if !ok {
+ rErr = fmt.Errorf("%v", r)
+ }
+ lErr = errors.WithStack(rErr)
+ }
+ }()
+
+ writeTicker := time.NewTicker(l.config.WriteInterval)
+ flushTicker := time.NewTicker(l.config.FlushInterval)
+
+ for {
+ shouldStop := false
+
+ select {
+ case <-l.signalCh:
+ err := l.buffer.writeBatch(l.sink)
+ if err != nil {
+ log.Printf("Log write failed: %+v", err)
+ }
+ err = l.sink.Flush(l.featureServiceName)
+ if err != nil {
+ log.Printf("Log flush failed: %+v", err)
+ }
+ shouldStop = true
+ case <-writeTicker.C:
+ err := l.buffer.writeBatch(l.sink)
+ if err != nil {
+ log.Printf("Log write failed: %+v", err)
+ }
+ case <-flushTicker.C:
+ err := l.sink.Flush(l.featureServiceName)
+ if err != nil {
+ log.Printf("Log flush failed: %+v", err)
+ }
+ case logItem := <-l.logCh:
+ err := l.buffer.Append(logItem)
+ if err != nil {
+ log.Printf("Append log failed: %+v", err)
+ }
+ }
+
+ if shouldStop {
+ break
+ }
+ }
+
+ writeTicker.Stop()
+ flushTicker.Stop()
+
+ // Notify all waiters for graceful stop
+ l.cond.L.Lock()
+ l.isStopped = true
+ l.cond.Broadcast()
+ l.cond.L.Unlock()
+ return nil
+}
+
+// Stop the loop goroutine gracefully
+func (l *LoggerImpl) Stop() {
+ select {
+ case l.signalCh <- nil:
+ default:
+ }
+}
+
+func (l *LoggerImpl) WaitUntilStopped() {
+ l.cond.L.Lock()
+ defer l.cond.L.Unlock()
+ for !l.isStopped {
+ l.cond.Wait()
+ }
+}
+
+func getFullFeatureName(featureViewName string, featureName string) string {
+ return fmt.Sprintf("%s__%s", featureViewName, featureName)
+}
+
+func (l *LoggerImpl) Log(joinKeyToEntityValues map[string]*types.RepeatedValue, featureVectors []*serving.GetOnlineFeaturesResponse_FeatureVector, featureNames []string, requestData map[string]*types.RepeatedValue, requestId string) error {
+ if len(featureVectors) == 0 {
+ return nil
+ }
+
+ if rand.Float32() > l.config.SampleRate {
+ return nil
+ }
+
+ numFeatures := len(l.schema.Features)
+ // Should be equivalent to how many entities there are(each feature row has (entity) number of features)
+ numRows := len(featureVectors[0].Values)
+
+ featureNameToVectorIdx := make(map[string]int)
+ for idx, name := range featureNames {
+ featureNameToVectorIdx[name] = idx
+ }
+
+ for rowIdx := 0; rowIdx < numRows; rowIdx++ {
+ featureValues := make([]*types.Value, numFeatures)
+ featureStatuses := make([]serving.FieldStatus, numFeatures)
+ eventTimestamps := make([]*timestamppb.Timestamp, numFeatures)
+
+ for idx, featureName := range l.schema.Features {
+ featureIdx, ok := featureNameToVectorIdx[featureName]
+ if !ok {
+ featureNameParts := strings.Split(featureName, "__")
+ featureIdx, ok = featureNameToVectorIdx[featureNameParts[1]]
+ if !ok {
+ return errors.Errorf("Missing feature %s in log data", featureName)
+ }
+ }
+ featureValues[idx] = featureVectors[featureIdx].Values[rowIdx]
+ featureStatuses[idx] = featureVectors[featureIdx].Statuses[rowIdx]
+ eventTimestamps[idx] = featureVectors[featureIdx].EventTimestamps[rowIdx]
+ }
+
+ entityValues := make([]*types.Value, len(l.schema.JoinKeys))
+ for idx, joinKey := range l.schema.JoinKeys {
+ rows, ok := joinKeyToEntityValues[joinKey]
+ if !ok {
+ return errors.Errorf("Missing join key %s in log data", joinKey)
+ }
+ entityValues[idx] = rows.Val[rowIdx]
+ }
+
+ requestDataValues := make([]*types.Value, len(l.schema.RequestData))
+ for idx, requestParam := range l.schema.RequestData {
+ rows, ok := requestData[requestParam]
+ if !ok {
+ return errors.Errorf("Missing request parameter %s in log data", requestParam)
+ }
+ requestDataValues[idx] = rows.Val[rowIdx]
+ }
+
+ newLog := Log{
+ EntityValue: entityValues,
+ RequestData: requestDataValues,
+
+ FeatureValues: featureValues,
+ FeatureStatuses: featureStatuses,
+ EventTimestamps: eventTimestamps,
+
+ RequestId: requestId,
+ LogTimestamp: time.Now().UTC(),
+ }
+ err := l.EmitLog(&newLog)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type DummyLoggerImpl struct{}
+
+func (l *DummyLoggerImpl) Log(joinKeyToEntityValues map[string]*types.RepeatedValue, featureVectors []*serving.GetOnlineFeaturesResponse_FeatureVector, featureNames []string, requestData map[string]*types.RepeatedValue, requestId string) error {
+ return nil
+}
+
+
+
package logging
+
+import (
+ "fmt"
+
+ "github.com/apache/arrow/go/v8/arrow"
+ "github.com/apache/arrow/go/v8/arrow/array"
+ "github.com/apache/arrow/go/v8/arrow/memory"
+
+ "github.com/feast-dev/feast/go/protos/feast/types"
+ gotypes "github.com/feast-dev/feast/go/types"
+)
+
+type MemoryBuffer struct {
+ logs []*Log
+ schema *FeatureServiceSchema
+
+ arrowSchema *arrow.Schema
+ records []arrow.Record
+}
+
+const (
+ LOG_TIMESTAMP_FIELD = "__log_timestamp"
+ LOG_DATE_FIELD = "__log_date"
+ LOG_REQUEST_ID_FIELD = "__request_id"
+ RECORD_SIZE = 1000
+)
+
+func NewMemoryBuffer(schema *FeatureServiceSchema) (*MemoryBuffer, error) {
+ arrowSchema, err := getArrowSchema(schema)
+ if err != nil {
+ return nil, err
+ }
+ return &MemoryBuffer{
+ logs: make([]*Log, 0),
+ records: make([]arrow.Record, 0),
+ schema: schema,
+ arrowSchema: arrowSchema,
+ }, nil
+}
+
+// Acquires the logging schema from the feature service, converts the memory buffer array of rows of logs and flushes
+// them to the offline storage.
+func (b *MemoryBuffer) writeBatch(sink LogSink) error {
+ if len(b.logs) > 0 {
+ err := b.Compact()
+ if err != nil {
+ return err
+ }
+ }
+
+ if len(b.records) == 0 {
+ return nil
+ }
+
+ err := sink.Write(b.records)
+ if err != nil {
+ return err
+ }
+
+ b.records = b.records[:0]
+ return nil
+}
+
+func (b *MemoryBuffer) Append(log *Log) error {
+ b.logs = append(b.logs, log)
+
+ if len(b.logs) == RECORD_SIZE {
+ return b.Compact()
+ }
+
+ return nil
+}
+
+func (b *MemoryBuffer) Compact() error {
+ rec, err := b.convertToArrowRecord()
+ if err != nil {
+ return err
+ }
+ b.records = append(b.records, rec)
+ b.logs = b.logs[:0]
+ return nil
+}
+
+func getArrowSchema(schema *FeatureServiceSchema) (*arrow.Schema, error) {
+ fields := make([]arrow.Field, 0)
+
+ for _, joinKey := range schema.JoinKeys {
+ arrowType, err := gotypes.ValueTypeEnumToArrowType(schema.JoinKeysTypes[joinKey])
+ if err != nil {
+ return nil, err
+ }
+
+ fields = append(fields, arrow.Field{Name: joinKey, Type: arrowType})
+ }
+
+ for _, requestParam := range schema.RequestData {
+ arrowType, err := gotypes.ValueTypeEnumToArrowType(schema.RequestDataTypes[requestParam])
+ if err != nil {
+ return nil, err
+ }
+
+ fields = append(fields, arrow.Field{Name: requestParam, Type: arrowType})
+ }
+
+ for _, featureName := range schema.Features {
+ arrowType, err := gotypes.ValueTypeEnumToArrowType(schema.FeaturesTypes[featureName])
+ if err != nil {
+ return nil, err
+ }
+
+ fields = append(fields, arrow.Field{Name: featureName, Type: arrowType})
+ fields = append(fields, arrow.Field{
+ Name: fmt.Sprintf("%s__timestamp", featureName),
+ Type: arrow.FixedWidthTypes.Timestamp_s})
+ fields = append(fields, arrow.Field{
+ Name: fmt.Sprintf("%s__status", featureName),
+ Type: arrow.PrimitiveTypes.Int32})
+ }
+
+ fields = append(fields, arrow.Field{Name: LOG_TIMESTAMP_FIELD, Type: arrow.FixedWidthTypes.Timestamp_us})
+ fields = append(fields, arrow.Field{Name: LOG_DATE_FIELD, Type: arrow.FixedWidthTypes.Date32})
+ fields = append(fields, arrow.Field{Name: LOG_REQUEST_ID_FIELD, Type: arrow.BinaryTypes.String})
+
+ return arrow.NewSchema(fields, nil), nil
+}
+
+// convertToArrowRecord Takes memory buffer of logs in array row and converts them to columnar with generated fcoschema generated by GetFcoSchema
+// and writes them to arrow table.
+// Returns arrow table that contains all of the logs in columnar format.
+func (b *MemoryBuffer) convertToArrowRecord() (arrow.Record, error) {
+ arrowMemory := memory.NewGoAllocator()
+ numRows := len(b.logs)
+
+ columns := make(map[string][]*types.Value)
+ fieldNameToIdx := make(map[string]int)
+ for idx, field := range b.arrowSchema.Fields() {
+ fieldNameToIdx[field.Name] = idx
+ }
+
+ builder := array.NewRecordBuilder(arrowMemory, b.arrowSchema)
+ defer builder.Release()
+
+ builder.Reserve(numRows)
+
+ for rowIdx, logRow := range b.logs {
+ for colIdx, joinKey := range b.schema.JoinKeys {
+ if _, ok := columns[joinKey]; !ok {
+ columns[joinKey] = make([]*types.Value, numRows)
+ }
+ columns[joinKey][rowIdx] = logRow.EntityValue[colIdx]
+ }
+ for colIdx, requestParam := range b.schema.RequestData {
+ if _, ok := columns[requestParam]; !ok {
+ columns[requestParam] = make([]*types.Value, numRows)
+ }
+ columns[requestParam][rowIdx] = logRow.RequestData[colIdx]
+ }
+ for colIdx, featureName := range b.schema.Features {
+ if _, ok := columns[featureName]; !ok {
+ columns[featureName] = make([]*types.Value, numRows)
+ }
+ columns[featureName][rowIdx] = logRow.FeatureValues[colIdx]
+
+ timestamp := arrow.Timestamp(logRow.EventTimestamps[colIdx].GetSeconds())
+ timestampFieldIdx := fieldNameToIdx[fmt.Sprintf("%s__timestamp", featureName)]
+ statusFieldIdx := fieldNameToIdx[fmt.Sprintf("%s__status", featureName)]
+
+ builder.Field(timestampFieldIdx).(*array.TimestampBuilder).UnsafeAppend(timestamp)
+ builder.Field(statusFieldIdx).(*array.Int32Builder).UnsafeAppend(int32(logRow.FeatureStatuses[colIdx]))
+ }
+
+ logTimestamp := arrow.Timestamp(logRow.LogTimestamp.UnixMicro())
+ logDate := arrow.Date32FromTime(logRow.LogTimestamp)
+
+ builder.Field(fieldNameToIdx[LOG_TIMESTAMP_FIELD]).(*array.TimestampBuilder).UnsafeAppend(logTimestamp)
+ builder.Field(fieldNameToIdx[LOG_DATE_FIELD]).(*array.Date32Builder).UnsafeAppend(logDate)
+ builder.Field(fieldNameToIdx[LOG_REQUEST_ID_FIELD]).(*array.StringBuilder).Append(logRow.RequestId)
+ }
+
+ for columnName, protoArray := range columns {
+ fieldIdx := fieldNameToIdx[columnName]
+ err := gotypes.CopyProtoValuesToArrowArray(builder.Field(fieldIdx), protoArray)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return builder.NewRecord(), nil
+}
+
+
+
package logging
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+
+ "github.com/apache/arrow/go/v8/arrow"
+ "github.com/apache/arrow/go/v8/arrow/array"
+ "github.com/apache/arrow/go/v8/parquet"
+ "github.com/apache/arrow/go/v8/parquet/pqarrow"
+ "github.com/google/uuid"
+)
+
+type OfflineStoreWriteCallback func(featureServiceName, datasetDir string) string
+
+type OfflineStoreSink struct {
+ datasetDir string
+ writeCallback OfflineStoreWriteCallback
+}
+
+func NewOfflineStoreSink(writeCallback OfflineStoreWriteCallback) (*OfflineStoreSink, error) {
+ return &OfflineStoreSink{
+ datasetDir: "",
+ writeCallback: writeCallback,
+ }, nil
+}
+
+func (s *OfflineStoreSink) getOrCreateDatasetDir() (string, error) {
+ if s.datasetDir != "" {
+ return s.datasetDir, nil
+ }
+ dir, err := ioutil.TempDir("", "*")
+ if err != nil {
+ return "", err
+ }
+ s.datasetDir = dir
+ return s.datasetDir, nil
+}
+
+func (s *OfflineStoreSink) Write(records []arrow.Record) error {
+ fileName, _ := uuid.NewUUID()
+ datasetDir, err := s.getOrCreateDatasetDir()
+ if err != nil {
+ return err
+ }
+
+ var writer io.Writer
+ writer, err = os.Create(filepath.Join(datasetDir, fmt.Sprintf("%s.parquet", fileName.String())))
+ if err != nil {
+ return err
+ }
+ table := array.NewTableFromRecords(records[0].Schema(), records)
+
+ props := parquet.NewWriterProperties(parquet.WithDictionaryDefault(false))
+ arrProps := pqarrow.DefaultWriterProps()
+ return pqarrow.WriteTable(table, writer, 1000, props, arrProps)
+}
+
+func (s *OfflineStoreSink) Flush(featureServiceName string) error {
+ if s.datasetDir == "" {
+ return nil
+ }
+
+ datasetDir := s.datasetDir
+ s.datasetDir = ""
+
+ go func() {
+ errMsg := s.writeCallback(featureServiceName, datasetDir)
+ if errMsg != "" {
+ log.Println(errMsg)
+ }
+ os.RemoveAll(datasetDir)
+ }()
+
+ return nil
+}
+
+
+
package logging
+
+import (
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/feast-dev/feast/go/internal/feast/model"
+)
+
+type FeatureStore interface {
+ GetFcosMap() (map[string]*model.Entity, map[string]*model.FeatureView, map[string]*model.OnDemandFeatureView, error)
+ GetFeatureService(name string) (*model.FeatureService, error)
+}
+
+type LoggingOptions struct {
+ // How many log items can be buffered in channel
+ ChannelCapacity int
+
+ // Waiting time when inserting new log into the channel
+ EmitTimeout time.Duration
+
+ // Interval on which logs buffered in memory will be written to sink
+ WriteInterval time.Duration
+
+ // Interval on which sink will be flushed
+ // (see LogSink interface for better explanation on differences with Write)
+ FlushInterval time.Duration
+}
+
+type LoggingService struct {
+ // feature service name -> LoggerImpl
+ loggers map[string]*LoggerImpl
+
+ fs FeatureStore
+ sink LogSink
+ opts LoggingOptions
+
+ creationLock *sync.Mutex
+}
+
+var (
+ DefaultOptions = LoggingOptions{
+ ChannelCapacity: 100000,
+ FlushInterval: 10 * time.Minute,
+ WriteInterval: 10 * time.Second,
+ EmitTimeout: 10 * time.Millisecond,
+ }
+)
+
+func NewLoggingService(fs FeatureStore, sink LogSink, opts ...LoggingOptions) (*LoggingService, error) {
+ if len(opts) == 0 {
+ opts = append(opts, DefaultOptions)
+ }
+
+ return &LoggingService{
+ fs: fs,
+ loggers: make(map[string]*LoggerImpl),
+ sink: sink,
+ opts: opts[0],
+ creationLock: &sync.Mutex{},
+ }, nil
+}
+
+func (s *LoggingService) GetOrCreateLogger(featureService *model.FeatureService) (Logger, error) {
+ if logger, ok := s.loggers[featureService.Name]; ok {
+ return logger, nil
+ }
+
+ if featureService.LoggingConfig == nil {
+ return nil, errors.New("Only feature services with configured logging can be used")
+ }
+
+ s.creationLock.Lock()
+ defer s.creationLock.Unlock()
+
+ // could be created by another go-routine on this point
+ if logger, ok := s.loggers[featureService.Name]; ok {
+ return logger, nil
+ }
+
+ if s.sink == nil {
+ return &DummyLoggerImpl{}, nil
+ }
+
+ config := NewLoggerConfig(featureService.LoggingConfig.SampleRate, s.opts)
+ schema, err := GenerateSchemaFromFeatureService(s.fs, featureService.Name)
+ if err != nil {
+ return nil, err
+ }
+
+ logger, err := NewLogger(schema, featureService.Name, s.sink, config)
+ if err != nil {
+ return nil, err
+ }
+ s.loggers[featureService.Name] = logger
+
+ return logger, nil
+}
+
+func (s *LoggingService) Stop() {
+ for _, logger := range s.loggers {
+ logger.Stop()
+ logger.WaitUntilStopped()
+ }
+}
+
+
+
package server
+
+import (
+ "github.com/rs/zerolog"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+ "os"
+)
+
+func LogWithSpanContext(span tracer.Span) zerolog.Logger {
+ spanContext := span.Context()
+
+ var logger = zerolog.New(os.Stderr).With().
+ Int64("trace_id", int64(spanContext.TraceID())).
+ Int64("span_id", int64(spanContext.SpanID())).
+ Timestamp().
+ Logger()
+
+ return logger
+}
+
+
+
package main
+
+import (
+ "flag"
+ "fmt"
+ "net"
+ "os"
+ "os/signal"
+ "strings"
+ "syscall"
+
+ "github.com/feast-dev/feast/go/internal/feast"
+ "github.com/feast-dev/feast/go/internal/feast/registry"
+ "github.com/feast-dev/feast/go/internal/feast/server"
+ "github.com/feast-dev/feast/go/internal/feast/server/logging"
+ "github.com/feast-dev/feast/go/protos/feast/serving"
+ "github.com/rs/zerolog/log"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/health"
+ "google.golang.org/grpc/health/grpc_health_v1"
+
+ grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+)
+
+type ServerStarter interface {
+ StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error
+ StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error
+}
+
+type RealServerStarter struct{}
+
+func (s *RealServerStarter) StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error {
+ return StartHttpServer(fs, host, port, writeLoggedFeaturesCallback, loggingOpts)
+}
+
+func (s *RealServerStarter) StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error {
+ return StartGrpcServer(fs, host, port, writeLoggedFeaturesCallback, loggingOpts)
+}
+
+func main() {
+ // Default values
+ serverType := "http"
+ host := ""
+ port := 8080
+ server := RealServerStarter{}
+ // Current Directory
+ repoPath, err := os.Getwd()
+ if err != nil {
+ log.Error().Stack().Err(err).Msg("Failed to get current directory")
+ }
+
+ flag.StringVar(&serverType, "type", serverType, "Specify the server type (http or grpc)")
+ flag.StringVar(&repoPath, "chdir", repoPath, "Repository path where feature store yaml file is stored")
+
+ flag.StringVar(&host, "host", host, "Specify a host for the server")
+ flag.IntVar(&port, "port", port, "Specify a port for the server")
+ flag.Parse()
+
+ repoConfig, err := registry.NewRepoConfigFromFile(repoPath)
+ if err != nil {
+ log.Fatal().Stack().Err(err).Msg("Failed to convert to RepoConfig")
+ }
+
+ fs, err := feast.NewFeatureStore(repoConfig, nil)
+ if err != nil {
+ log.Fatal().Stack().Err(err).Msg("Failed to create NewFeatureStore")
+ }
+
+ loggingOptions, err := repoConfig.GetLoggingOptions()
+ if err != nil {
+ log.Fatal().Stack().Err(err).Msg("Failed to get LoggingOptions")
+ }
+
+ // TODO: writeLoggedFeaturesCallback is defaulted to nil. write_logged_features functionality needs to be
+ // implemented in Golang specific to OfflineStoreSink. Python Feature Server doesn't support this.
+ if serverType == "http" {
+ err = server.StartHttpServer(fs, host, port, nil, loggingOptions)
+ } else if serverType == "grpc" {
+ err = server.StartGrpcServer(fs, host, port, nil, loggingOptions)
+ } else {
+ fmt.Println("Unknown server type. Please specify 'http' or 'grpc'.")
+ }
+
+ if err != nil {
+ log.Fatal().Stack().Err(err).Msg("Failed to start server")
+ }
+
+}
+
+func constructLoggingService(fs *feast.FeatureStore, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) (*logging.LoggingService, error) {
+ var loggingService *logging.LoggingService = nil
+ if writeLoggedFeaturesCallback != nil {
+ sink, err := logging.NewOfflineStoreSink(writeLoggedFeaturesCallback)
+ if err != nil {
+ return nil, err
+ }
+
+ loggingService, err = logging.NewLoggingService(fs, sink, logging.LoggingOptions{
+ ChannelCapacity: loggingOpts.ChannelCapacity,
+ EmitTimeout: loggingOpts.EmitTimeout,
+ WriteInterval: loggingOpts.WriteInterval,
+ FlushInterval: loggingOpts.FlushInterval,
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+ return loggingService, nil
+}
+
+// StartGprcServerWithLogging starts gRPC server with enabled feature logging
+func StartGrpcServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error {
+ if strings.ToLower(os.Getenv("ENABLE_DATADOG_TRACING")) == "true" {
+ tracer.Start(tracer.WithRuntimeMetrics())
+ defer tracer.Stop()
+ }
+ loggingService, err := constructLoggingService(fs, writeLoggedFeaturesCallback, loggingOpts)
+ if err != nil {
+ return err
+ }
+ ser := server.NewGrpcServingServiceServer(fs, loggingService)
+ log.Info().Msgf("Starting a gRPC server on host %s port %d", host, port)
+ lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
+ if err != nil {
+ return err
+ }
+
+ grpcServer := grpc.NewServer(grpc.UnaryInterceptor(grpctrace.UnaryServerInterceptor()))
+ serving.RegisterServingServiceServer(grpcServer, ser)
+ healthService := health.NewServer()
+ grpc_health_v1.RegisterHealthServer(grpcServer, healthService)
+
+ stop := make(chan os.Signal, 1)
+ signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ // As soon as these signals are received from OS, try to gracefully stop the gRPC server
+ <-stop
+ log.Info().Msg("Stopping the gRPC server...")
+ grpcServer.GracefulStop()
+ if loggingService != nil {
+ loggingService.Stop()
+ }
+ log.Info().Msg("gRPC server terminated")
+ }()
+
+ return grpcServer.Serve(lis)
+}
+
+// StartHttpServerWithLogging starts HTTP server with enabled feature logging
+// Go does not allow direct assignment to package-level functions as a way to
+// mock them for tests
+func StartHttpServer(fs *feast.FeatureStore, host string, port int, writeLoggedFeaturesCallback logging.OfflineStoreWriteCallback, loggingOpts *logging.LoggingOptions) error {
+ loggingService, err := constructLoggingService(fs, writeLoggedFeaturesCallback, loggingOpts)
+ if err != nil {
+ return err
+ }
+ ser := server.NewHttpServer(fs, loggingService)
+ log.Info().Msgf("Starting a HTTP server on host %s, port %d", host, port)
+
+ stop := make(chan os.Signal, 1)
+ signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ // As soon as these signals are received from OS, try to gracefully stop the gRPC server
+ <-stop
+ log.Info().Msg("Stopping the HTTP server...")
+ err := ser.Stop()
+ if err != nil {
+ log.Error().Err(err).Msg("Error when stopping the HTTP server")
+ }
+ if loggingService != nil {
+ loggingService.Stop()
+ }
+ log.Info().Msg("HTTP server terminated")
+ }()
+
+ return ser.Serve(host, port)
+}
+
+
+
package types
+
+import (
+ "fmt"
+
+ "github.com/apache/arrow/go/v8/arrow"
+ "github.com/apache/arrow/go/v8/arrow/array"
+ "github.com/apache/arrow/go/v8/arrow/memory"
+
+ "github.com/feast-dev/feast/go/protos/feast/types"
+)
+
+func ProtoTypeToArrowType(sample *types.Value) (arrow.DataType, error) {
+ if sample.Val == nil {
+ return nil, nil
+ }
+ switch sample.Val.(type) {
+ case *types.Value_BytesVal:
+ return arrow.BinaryTypes.Binary, nil
+ case *types.Value_StringVal:
+ return arrow.BinaryTypes.String, nil
+ case *types.Value_Int32Val:
+ return arrow.PrimitiveTypes.Int32, nil
+ case *types.Value_Int64Val:
+ return arrow.PrimitiveTypes.Int64, nil
+ case *types.Value_FloatVal:
+ return arrow.PrimitiveTypes.Float32, nil
+ case *types.Value_DoubleVal:
+ return arrow.PrimitiveTypes.Float64, nil
+ case *types.Value_BoolVal:
+ return arrow.FixedWidthTypes.Boolean, nil
+ case *types.Value_BoolListVal:
+ return arrow.ListOf(arrow.FixedWidthTypes.Boolean), nil
+ case *types.Value_StringListVal:
+ return arrow.ListOf(arrow.BinaryTypes.String), nil
+ case *types.Value_BytesListVal:
+ return arrow.ListOf(arrow.BinaryTypes.Binary), nil
+ case *types.Value_Int32ListVal:
+ return arrow.ListOf(arrow.PrimitiveTypes.Int32), nil
+ case *types.Value_Int64ListVal:
+ return arrow.ListOf(arrow.PrimitiveTypes.Int64), nil
+ case *types.Value_FloatListVal:
+ return arrow.ListOf(arrow.PrimitiveTypes.Float32), nil
+ case *types.Value_DoubleListVal:
+ return arrow.ListOf(arrow.PrimitiveTypes.Float64), nil
+ case *types.Value_UnixTimestampVal:
+ return arrow.FixedWidthTypes.Timestamp_s, nil
+ case *types.Value_UnixTimestampListVal:
+ return arrow.ListOf(arrow.FixedWidthTypes.Timestamp_s), nil
+ default:
+ return nil,
+ fmt.Errorf("unsupported proto type in proto to arrow conversion: %s", sample.Val)
+ }
+}
+
+func ValueTypeEnumToArrowType(t types.ValueType_Enum) (arrow.DataType, error) {
+ switch t {
+ case types.ValueType_BYTES:
+ return arrow.BinaryTypes.Binary, nil
+ case types.ValueType_STRING:
+ return arrow.BinaryTypes.String, nil
+ case types.ValueType_INT32:
+ return arrow.PrimitiveTypes.Int32, nil
+ case types.ValueType_INT64:
+ return arrow.PrimitiveTypes.Int64, nil
+ case types.ValueType_FLOAT:
+ return arrow.PrimitiveTypes.Float32, nil
+ case types.ValueType_DOUBLE:
+ return arrow.PrimitiveTypes.Float64, nil
+ case types.ValueType_BOOL:
+ return arrow.FixedWidthTypes.Boolean, nil
+ case types.ValueType_BOOL_LIST:
+ return arrow.ListOf(arrow.FixedWidthTypes.Boolean), nil
+ case types.ValueType_STRING_LIST:
+ return arrow.ListOf(arrow.BinaryTypes.String), nil
+ case types.ValueType_BYTES_LIST:
+ return arrow.ListOf(arrow.BinaryTypes.Binary), nil
+ case types.ValueType_INT32_LIST:
+ return arrow.ListOf(arrow.PrimitiveTypes.Int32), nil
+ case types.ValueType_INT64_LIST:
+ return arrow.ListOf(arrow.PrimitiveTypes.Int64), nil
+ case types.ValueType_FLOAT_LIST:
+ return arrow.ListOf(arrow.PrimitiveTypes.Float32), nil
+ case types.ValueType_DOUBLE_LIST:
+ return arrow.ListOf(arrow.PrimitiveTypes.Float64), nil
+ case types.ValueType_UNIX_TIMESTAMP:
+ return arrow.FixedWidthTypes.Timestamp_s, nil
+ case types.ValueType_UNIX_TIMESTAMP_LIST:
+ return arrow.ListOf(arrow.FixedWidthTypes.Timestamp_s), nil
+ default:
+ return nil,
+ fmt.Errorf("unsupported value type enum in enum to arrow type conversion: %s", t)
+ }
+}
+
+func CopyProtoValuesToArrowArray(builder array.Builder, values []*types.Value) error {
+ for _, value := range values {
+ if value == nil || value.Val == nil {
+ builder.AppendNull()
+ continue
+ }
+
+ switch fieldBuilder := builder.(type) {
+
+ case *array.BooleanBuilder:
+ fieldBuilder.Append(value.GetBoolVal())
+ case *array.BinaryBuilder:
+ fieldBuilder.Append(value.GetBytesVal())
+ case *array.StringBuilder:
+ fieldBuilder.Append(value.GetStringVal())
+ case *array.Int32Builder:
+ fieldBuilder.Append(value.GetInt32Val())
+ case *array.Int64Builder:
+ fieldBuilder.Append(value.GetInt64Val())
+ case *array.Float32Builder:
+ fieldBuilder.Append(value.GetFloatVal())
+ case *array.Float64Builder:
+ fieldBuilder.Append(value.GetDoubleVal())
+ case *array.TimestampBuilder:
+ fieldBuilder.Append(arrow.Timestamp(value.GetUnixTimestampVal()))
+ case *array.ListBuilder:
+ fieldBuilder.Append(true)
+
+ switch valueBuilder := fieldBuilder.ValueBuilder().(type) {
+
+ case *array.BooleanBuilder:
+ for _, v := range value.GetBoolListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.BinaryBuilder:
+ for _, v := range value.GetBytesListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.StringBuilder:
+ for _, v := range value.GetStringListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.Int32Builder:
+ for _, v := range value.GetInt32ListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.Int64Builder:
+ for _, v := range value.GetInt64ListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.Float32Builder:
+ for _, v := range value.GetFloatListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.Float64Builder:
+ for _, v := range value.GetDoubleListVal().GetVal() {
+ valueBuilder.Append(v)
+ }
+ case *array.TimestampBuilder:
+ for _, v := range value.GetUnixTimestampListVal().GetVal() {
+ valueBuilder.Append(arrow.Timestamp(v))
+ }
+ }
+ default:
+ return fmt.Errorf("unsupported array builder: %s", builder)
+ }
+ }
+ return nil
+}
+
+func ArrowValuesToProtoValues(arr arrow.Array) ([]*types.Value, error) {
+ values := make([]*types.Value, 0)
+
+ if listArr, ok := arr.(*array.List); ok {
+ listValues := listArr.ListValues()
+ offsets := listArr.Offsets()[1:]
+ pos := 0
+ for idx := 0; idx < listArr.Len(); idx++ {
+ switch listValues.DataType() {
+ case arrow.PrimitiveTypes.Int32:
+ vals := make([]int32, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.Int32).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_Int32ListVal{Int32ListVal: &types.Int32List{Val: vals}}})
+ case arrow.PrimitiveTypes.Int64:
+ vals := make([]int64, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.Int64).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_Int64ListVal{Int64ListVal: &types.Int64List{Val: vals}}})
+ case arrow.PrimitiveTypes.Float32:
+ vals := make([]float32, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.Float32).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_FloatListVal{FloatListVal: &types.FloatList{Val: vals}}})
+ case arrow.PrimitiveTypes.Float64:
+ vals := make([]float64, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.Float64).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_DoubleListVal{DoubleListVal: &types.DoubleList{Val: vals}}})
+ case arrow.BinaryTypes.Binary:
+ vals := make([][]byte, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.Binary).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_BytesListVal{BytesListVal: &types.BytesList{Val: vals}}})
+ case arrow.BinaryTypes.String:
+ vals := make([]string, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.String).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_StringListVal{StringListVal: &types.StringList{Val: vals}}})
+ case arrow.FixedWidthTypes.Boolean:
+ vals := make([]bool, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = listValues.(*array.Boolean).Value(j)
+ }
+ values = append(values,
+ &types.Value{Val: &types.Value_BoolListVal{BoolListVal: &types.BoolList{Val: vals}}})
+ case arrow.FixedWidthTypes.Timestamp_s:
+ vals := make([]int64, int(offsets[idx])-pos)
+ for j := pos; j < int(offsets[idx]); j++ {
+ vals[j-pos] = int64(listValues.(*array.Timestamp).Value(j))
+ }
+
+ values = append(values,
+ &types.Value{Val: &types.Value_UnixTimestampListVal{
+ UnixTimestampListVal: &types.Int64List{Val: vals}}})
+
+ }
+
+ // set the end of current element as start of the next
+ pos = int(offsets[idx])
+ }
+
+ return values, nil
+ }
+
+ switch arr.DataType() {
+ case arrow.PrimitiveTypes.Int32:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_Int32Val{Int32Val: arr.(*array.Int32).Value(idx)}})
+ }
+ }
+ case arrow.PrimitiveTypes.Int64:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_Int64Val{Int64Val: arr.(*array.Int64).Value(idx)}})
+ }
+ }
+ case arrow.PrimitiveTypes.Float32:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_FloatVal{FloatVal: arr.(*array.Float32).Value(idx)}})
+ }
+ }
+ case arrow.PrimitiveTypes.Float64:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_DoubleVal{DoubleVal: arr.(*array.Float64).Value(idx)}})
+ }
+ }
+ case arrow.FixedWidthTypes.Boolean:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_BoolVal{BoolVal: arr.(*array.Boolean).Value(idx)}})
+ }
+ }
+ case arrow.BinaryTypes.Binary:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_BytesVal{BytesVal: arr.(*array.Binary).Value(idx)}})
+ }
+ }
+ case arrow.BinaryTypes.String:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_StringVal{StringVal: arr.(*array.String).Value(idx)}})
+ }
+ }
+ case arrow.FixedWidthTypes.Timestamp_s:
+ for idx := 0; idx < arr.Len(); idx++ {
+ if arr.IsNull(idx) {
+ values = append(values, &types.Value{})
+ } else {
+ values = append(values, &types.Value{Val: &types.Value_UnixTimestampVal{UnixTimestampVal: int64(arr.(*array.Timestamp).Value(idx))}})
+ }
+ }
+ case arrow.Null:
+ for idx := 0; idx < arr.Len(); idx++ {
+ values = append(values, &types.Value{})
+ }
+ default:
+ return nil, fmt.Errorf("unsupported arrow to proto conversion for type %s", arr.DataType())
+ }
+
+ return values, nil
+}
+
+func ProtoValuesToArrowArray(protoValues []*types.Value, arrowAllocator memory.Allocator, numRows int) (arrow.Array, error) {
+ var fieldType arrow.DataType
+ var err error
+
+ for _, val := range protoValues {
+ if val != nil {
+ fieldType, err = ProtoTypeToArrowType(val)
+ if err != nil {
+ return nil, err
+ }
+ if fieldType != nil {
+ break
+ }
+ }
+ }
+
+ if fieldType != nil {
+ builder := array.NewBuilder(arrowAllocator, fieldType)
+ err = CopyProtoValuesToArrowArray(builder, protoValues)
+ if err != nil {
+ return nil, err
+ }
+
+ return builder.NewArray(), nil
+ } else {
+ return array.NewNull(numRows), nil
+ }
+}
+
+
+
+
+
+
diff --git a/coverage.out b/coverage.out
new file mode 100644
index 0000000000..197c71b8a5
--- /dev/null
+++ b/coverage.out
@@ -0,0 +1,950 @@
+mode: set
+github.com/feast-dev/feast/go/types/typeconversion.go:13.72,14.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:17.2,17.27 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:14.23,16.3 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:18.29,19.39 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:20.30,21.39 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:22.29,23.41 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:24.29,25.41 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:26.29,27.43 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:28.30,29.43 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:30.28,31.44 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:32.32,33.58 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:34.34,35.53 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:36.33,37.53 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:38.33,39.55 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:40.33,41.55 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:42.33,43.57 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:44.34,45.57 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:46.37,47.48 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:48.41,49.62 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:50.10,52.85 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:56.79,57.11 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:58.29,59.39 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:60.30,61.39 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:62.29,63.41 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:64.29,65.41 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:66.29,67.43 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:68.30,69.43 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:70.28,71.44 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:72.33,73.58 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:74.35,75.53 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:76.34,77.53 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:78.34,79.55 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:80.34,81.55 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:82.34,83.57 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:84.35,85.57 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:86.38,87.48 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:88.43,89.62 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:90.10,92.85 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:96.86,97.31 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:163.2,163.12 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:97.31,98.39 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:103.3,103.41 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:98.39,100.12 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:105.30,106.43 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:107.29,108.44 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:109.29,110.45 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:111.28,112.44 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:113.28,114.44 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:115.30,116.44 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:117.30,118.45 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:119.32,120.69 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:121.27,124.62 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:159.11,160.63 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:126.31,127.55 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:130.30,131.56 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:134.30,135.57 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:138.29,139.56 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:142.29,143.56 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:146.31,147.56 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:150.31,151.57 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:154.33,155.64 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:127.55,129.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:131.56,133.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:135.57,137.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:139.56,141.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:143.56,145.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:147.56,149.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:151.57,153.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:155.64,157.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:166.72,169.42 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:243.2,243.24 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:316.2,316.20 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:169.42,173.44 4 1
+github.com/feast-dev/feast/go/types/typeconversion.go:240.3,240.21 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:173.44,174.33 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:237.4,237.27 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:175.36,177.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:180.5,181.94 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:182.36,184.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:187.5,188.94 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:189.38,191.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:194.5,195.94 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:196.38,198.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:201.5,202.97 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:203.34,205.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:208.5,209.94 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:210.34,212.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:215.5,216.97 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:217.39,219.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:222.5,223.91 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:224.43,226.46 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:230.5,232.59 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:177.46,179.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:184.46,186.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:191.46,193.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:198.46,200.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:205.46,207.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:212.46,214.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:219.46,221.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:226.46,228.6 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:244.34,245.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:252.34,253.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:260.36,261.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:268.36,269.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:276.37,277.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:284.32,285.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:292.32,293.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:300.41,301.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:308.18,309.40 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:312.10,313.94 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:245.40,246.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:246.23,248.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:248.10,250.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:253.40,254.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:254.23,256.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:256.10,258.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:261.40,262.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:262.23,264.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:264.10,266.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:269.40,270.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:270.23,272.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:272.10,274.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:277.40,278.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:278.23,280.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:280.10,282.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:285.40,286.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:286.23,288.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:288.10,290.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:293.40,294.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:294.23,296.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:296.10,298.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:301.40,302.23 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:302.23,304.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:304.10,306.5 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:309.40,311.4 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:319.125,323.34 3 1
+github.com/feast-dev/feast/go/types/typeconversion.go:335.2,335.22 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:323.34,324.17 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:324.17,326.18 2 1
+github.com/feast-dev/feast/go/types/typeconversion.go:329.4,329.24 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:326.18,328.5 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:329.24,330.10 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:335.22,338.17 3 1
+github.com/feast-dev/feast/go/types/typeconversion.go:342.3,342.33 1 1
+github.com/feast-dev/feast/go/types/typeconversion.go:338.17,340.4 1 0
+github.com/feast-dev/feast/go/types/typeconversion.go:343.8,345.3 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:25.56,26.16 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:30.2,31.16 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:34.2,34.41 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:26.16,28.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:31.16,33.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:37.59,42.16 4 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:45.2,49.64 4 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:42.16,44.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/filelogsink.go:52.62,55.2 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:70.76,75.2 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:77.129,79.16 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:82.2,98.20 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:79.16,81.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:101.46,102.9 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:103.22,104.13 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:105.42,106.152 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:110.40,111.12 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:111.12,112.7 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:112.7,113.41 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:122.4,122.10 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:113.41,118.13 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:129.48,130.15 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:141.2,144.6 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:180.2,188.12 7 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:130.15,132.31 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:132.31,134.11 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:137.4,137.33 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:134.11,136.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:144.6,147.10 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:175.3,175.17 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:148.21,150.18 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:153.4,154.18 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:157.4,157.21 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:158.24,160.18 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:163.24,165.18 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:168.29,170.18 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:150.18,152.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:154.18,156.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:160.18,162.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:165.18,167.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:170.18,172.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:175.17,176.9 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:192.29,193.9 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:194.25,194.25 0 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:195.10,195.10 0 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:199.41,202.19 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:202.19,204.3 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:207.76,209.2 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:211.240,212.30 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:216.2,216.42 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:220.2,225.38 4 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:229.2,229.46 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:282.2,282.12 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:212.30,214.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:216.42,218.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:225.38,227.3 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:229.46,234.51 4 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:248.3,249.47 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:257.3,258.55 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:266.3,278.17 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:234.51,236.11 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:243.4,245.77 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:236.11,239.12 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:239.12,241.6 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:249.47,251.11 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:254.4,254.40 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:251.11,253.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:258.55,260.11 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:263.4,263.45 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:260.11,262.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:278.17,280.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/logger.go:287.245,289.2 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:29.75,31.16 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:34.2,39.8 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:31.16,33.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:44.55,45.21 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:52.2,52.25 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:56.2,57.16 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:61.2,62.12 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:45.21,47.17 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:47.17,49.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:52.25,54.3 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:57.16,59.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:65.47,68.32 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:72.2,72.12 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:68.32,70.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:75.40,77.16 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:80.2,82.12 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:77.16,79.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:85.74,88.42 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:97.2,97.50 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:106.2,106.46 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:121.2,125.42 4 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:88.42,90.17 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:94.3,94.71 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:90.17,92.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:97.50,99.17 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:103.3,103.76 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:99.17,101.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:106.46,108.17 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:112.3,118.38 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:108.17,110.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:131.69,137.49 5 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:141.2,146.37 4 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:181.2,181.46 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:189.2,189.33 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:137.49,139.3 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:146.37,147.50 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:153.3,153.58 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:159.3,159.54 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:173.3,178.102 5 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:147.50,148.38 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:151.4,151.57 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:148.38,150.5 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:153.58,154.43 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:157.4,157.62 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:154.43,156.5 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:159.54,160.42 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:163.4,170.107 6 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:160.42,162.5 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:181.46,184.17 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/memorybuffer.go:184.17,186.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:25.94,30.2 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:32.68,33.24 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:36.2,37.16 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:40.2,41.26 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:33.24,35.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:37.16,39.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:44.64,47.16 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:51.2,53.16 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:56.2,60.65 4 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:47.16,49.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:53.16,55.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:63.67,64.24 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:68.2,71.12 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:79.2,79.12 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:64.24,66.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:71.12,73.19 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:76.3,76.27 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/offlinestoresink.go:73.19,75.4 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:52.104,53.20 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:57.2,63.8 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:53.20,55.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:66.98,67.54 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:71.2,71.41 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:75.2,79.54 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:83.2,83.19 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:87.2,89.16 3 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:93.2,94.16 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:97.2,99.20 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:67.54,69.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:71.41,73.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:79.54,81.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:83.19,85.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:89.16,91.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:94.16,96.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:102.33,103.35 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/service.go:103.35,106.3 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:20.114,22.16 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:26.2,27.16 2 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:31.2,31.66 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:22.16,24.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:27.16,29.3 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:34.210,45.63 8 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:86.2,95.20 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:45.63,49.43 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:49.43,50.49 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:55.4,55.50 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:50.49,54.5 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:55.50,57.80 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:63.5,63.43 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:67.5,68.54 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:57.80,59.6 1 0
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:59.11,61.6 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:63.43,65.6 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:70.9,70.54 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:70.54,71.49 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:76.4,76.66 1 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:71.49,75.5 3 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:76.66,79.5 2 1
+github.com/feast-dev/feast/go/internal/feast/server/logging/featureserviceschema.go:80.9,83.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:28.46,30.2 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:32.95,48.47 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:52.2,52.17 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:48.47,50.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:55.56,57.16 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:60.2,62.38 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:66.2,66.12 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:57.16,59.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:62.38,64.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:69.81,71.16 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:75.2,79.16 4 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:83.2,83.38 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:87.2,87.18 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:71.16,73.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:79.16,81.3 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:83.38,85.3 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:90.105,92.16 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:95.2,98.16 3 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:102.2,102.49 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:106.2,106.12 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:92.16,94.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:98.16,100.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:102.49,104.3 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:109.73,111.61 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:111.61,113.60 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:116.3,116.42 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:119.3,120.13 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:113.60,115.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:116.42,118.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:124.76,126.61 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:126.61,128.65 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:131.3,131.50 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:134.3,135.13 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:128.65,130.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:131.50,133.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:139.77,141.61 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:141.61,143.66 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:146.3,146.52 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:149.3,150.13 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:143.66,145.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:146.52,148.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:154.85,156.61 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:156.61,158.69 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:161.3,162.13 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:158.69,160.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:166.80,168.61 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:168.61,170.69 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:173.3,174.13 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:170.69,172.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:178.72,182.50 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:186.2,186.53 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:190.2,190.54 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:194.2,194.62 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:198.2,198.57 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:202.2,202.23 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:182.50,184.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:186.53,188.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:190.54,192.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:194.62,196.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:198.57,200.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:205.74,207.2 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/http.go:209.46,211.2 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:22.87,25.34 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:30.2,30.12 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:25.34,27.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:27.8,29.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:34.72,37.16 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:40.2,40.54 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:43.2,43.22 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:37.16,39.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:40.54,42.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:46.74,48.2 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:50.46,52.2 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:54.68,58.16 4 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:61.2,62.16 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:65.2,65.12 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:58.16,60.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/local.go:62.16,64.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:45.102,53.33 4 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:67.2,67.15 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:53.33,55.17 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:58.3,58.34 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:55.17,57.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:59.8,61.17 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:64.3,64.34 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:61.17,63.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:70.47,72.16 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:80.2,81.12 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:72.16,73.56 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:77.3,78.53 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:73.56,76.4 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:84.48,86.25 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:86.25,88.17 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:88.17,90.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:94.36,97.2 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:99.63,101.14 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:104.2,105.16 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:108.2,109.27 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:101.14,103.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:105.16,107.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:112.50,127.2 14 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:129.58,131.34 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:131.34,132.48 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:135.3,135.57 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:132.48,134.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:139.65,141.49 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:141.49,142.55 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:145.3,145.80 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:142.55,144.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:149.62,151.43 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:151.43,152.52 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:155.3,155.71 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:152.52,154.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:159.68,161.55 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:161.55,162.58 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:165.3,165.89 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:162.58,164.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:169.70,171.59 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:171.59,172.60 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:175.3,175.95 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:172.60,174.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:184.74,187.58 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:187.58,189.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:189.8,192.46 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:196.3,196.23 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:192.46,195.4 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:205.83,208.66 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:208.66,210.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:210.8,213.55 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:217.3,217.27 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:213.55,216.4 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:226.89,229.78 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:229.78,231.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:231.8,234.67 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:238.3,238.33 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:234.67,237.4 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:247.89,250.72 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:250.72,252.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:252.8,255.61 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:259.3,259.30 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:255.61,258.4 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:268.99,271.82 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:271.82,273.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:273.8,276.71 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:280.3,280.35 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:276.71,279.4 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:284.81,287.58 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:287.58,289.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:289.8,290.52 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:290.52,292.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:292.9,294.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:298.96,301.66 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:301.66,303.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:303.8,304.71 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:304.71,306.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:306.9,308.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:312.108,315.78 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:315.78,317.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:317.8,318.89 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:318.89,320.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:320.9,322.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:326.105,329.72 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:329.72,331.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:331.8,332.80 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:332.80,334.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:334.9,336.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:340.120,343.82 3 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:343.82,345.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:345.8,346.95 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:346.95,348.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:348.9,350.4 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:354.142,356.16 2 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:359.2,359.78 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:362.2,362.135 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:356.16,358.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:359.78,361.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:365.145,366.27 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:372.2,372.118 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:367.27,368.61 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/registry.go:369.27,370.55 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:49.78,51.68 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:54.2,55.16 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:58.2,59.21 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:51.68,53.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:55.16,57.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:64.66,66.16 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:69.2,70.16 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:74.2,77.74 3 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:80.2,81.21 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:66.16,68.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:70.16,72.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:77.74,79.3 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:84.75,86.94 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:109.2,109.29 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:86.94,88.39 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:88.39,89.13 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:90.26,91.33 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:94.35,95.33 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:98.39,99.33 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:102.31,103.33 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:91.33,93.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:95.33,97.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:99.33,101.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:103.33,105.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:112.67,113.70 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:113.70,115.39 2 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:145.3,145.30 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:115.39,116.13 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:117.16,118.36 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:121.31,122.36 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:125.21,126.36 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:129.29,131.30 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:118.36,120.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:122.36,124.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:126.36,128.6 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:132.18,133.51 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:134.14,135.51 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:136.16,137.51 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:138.16,139.44 1 1
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:140.13,141.73 1 0
+github.com/feast-dev/feast/go/internal/feast/registry/repoconfig.go:146.8,148.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:72.123,77.63 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:119.2,120.51 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:124.2,124.34 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:77.63,81.50 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:81.50,83.18 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:86.4,86.74 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:93.4,93.55 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:83.18,85.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:86.74,91.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:93.55,97.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:99.9,99.67 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:99.67,101.18 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:104.4,109.18 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:101.18,103.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:109.18,111.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:112.9,116.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:120.51,122.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:138.123,142.38 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:169.2,171.53 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:187.2,188.52 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:192.2,192.34 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:142.38,144.17 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:147.3,147.50 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:144.17,146.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:147.50,148.65 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:148.65,150.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:150.10,155.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:156.9,156.67 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:156.67,157.52 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:157.52,159.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:159.10,162.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:163.9,166.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:171.53,173.17 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:177.3,181.17 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:184.3,184.49 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:173.17,175.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:181.17,183.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:188.52,190.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:199.9,201.71 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:222.2,222.12 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:201.71,204.17 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:207.3,209.70 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:216.3,216.55 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:204.17,206.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:209.70,214.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:216.55,219.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:225.70,227.29 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:232.2,232.12 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:235.2,235.14 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:227.29,228.22 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:228.22,230.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:232.12,234.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:238.142,244.34 4 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:248.2,248.56 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:268.2,268.57 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:244.34,246.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:248.56,251.90 3 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:257.3,257.54 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:251.90,253.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:253.9,255.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:257.54,261.51 3 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:261.51,263.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:263.10,265.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:273.59,276.45 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:293.2,293.21 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:276.45,277.49 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:277.49,283.4 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:283.9,284.19 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:284.19,286.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:286.10,286.41 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:286.41,288.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:296.96,299.52 3 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:311.2,311.41 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:320.2,320.58 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:325.2,325.33 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:342.2,342.12 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:299.52,300.55 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:300.55,302.51 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:306.4,307.54 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:302.51,304.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:311.41,312.23 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:312.23,314.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:314.9,317.4 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:320.58,321.23 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:321.23,323.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:325.33,327.53 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:339.3,339.74 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:327.53,328.24 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:328.24,330.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:330.10,331.44 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:331.44,333.43 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:333.43,335.7 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:349.41,353.52 3 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:357.2,366.68 8 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:409.2,409.21 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:353.52,355.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:366.68,375.63 4 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:402.3,403.17 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:406.3,406.37 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:375.63,376.44 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:396.4,396.43 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:376.44,380.5 3 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:380.10,385.71 5 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:385.71,388.6 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:388.11,388.74 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:388.74,391.6 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:391.11,394.6 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:396.43,400.5 3 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:403.17,405.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:417.51,423.33 4 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:427.2,427.27 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:436.2,436.50 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:450.2,450.33 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:456.2,456.29 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:423.33,425.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:427.27,428.57 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:428.57,429.42 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:429.42,432.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:436.50,438.17 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:441.3,442.49 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:445.3,446.36 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:438.17,440.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:442.49,444.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:450.33,451.45 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:451.45,453.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:459.155,463.37 4 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:467.2,467.48 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:479.2,479.21 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:463.37,466.3 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:467.48,469.17 2 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:472.3,477.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:469.17,471.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:482.94,485.33 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:493.2,493.8 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:485.33,487.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:487.8,487.40 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:487.40,489.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:489.8,492.3 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:496.101,500.34 4 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:505.2,509.42 4 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:513.2,513.34 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:518.2,518.19 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:500.34,504.3 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:509.42,511.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:513.34,514.57 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:514.57,516.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:527.3,530.56 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:602.2,602.20 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:530.56,534.45 4 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:538.3,542.72 4 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:546.3,546.36 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:563.3,569.32 6 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:575.3,575.44 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:581.3,581.37 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:534.45,536.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:542.72,544.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:546.36,549.51 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:557.4,557.51 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:560.4,560.69 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:549.51,552.5 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:552.10,555.5 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:557.51,559.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:569.32,571.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:571.9,573.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:575.44,579.4 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:581.37,584.18 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:588.4,594.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:584.18,586.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:596.9,600.4 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:605.107,609.46 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:624.2,626.41 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:632.2,632.46 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:609.46,611.17 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:615.3,616.42 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:611.17,613.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:616.42,619.4 2 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:619.9,621.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:626.41,631.3 3 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:635.135,636.22 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:639.2,639.82 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:636.22,638.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:642.97,643.22 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:643.22,645.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:645.8,647.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlineserving/serving.go:655.51,657.2 1 0
+github.com/feast-dev/feast/go/main.go:33.198,35.2 1 0
+github.com/feast-dev/feast/go/main.go:37.198,39.2 1 0
+github.com/feast-dev/feast/go/main.go:41.13,49.16 6 0
+github.com/feast-dev/feast/go/main.go:53.2,61.16 7 0
+github.com/feast-dev/feast/go/main.go:65.2,66.16 2 0
+github.com/feast-dev/feast/go/main.go:70.2,71.16 2 0
+github.com/feast-dev/feast/go/main.go:77.2,77.26 1 0
+github.com/feast-dev/feast/go/main.go:85.2,85.16 1 0
+github.com/feast-dev/feast/go/main.go:49.16,51.3 1 0
+github.com/feast-dev/feast/go/main.go:61.16,63.3 1 0
+github.com/feast-dev/feast/go/main.go:66.16,68.3 1 0
+github.com/feast-dev/feast/go/main.go:71.16,73.3 1 0
+github.com/feast-dev/feast/go/main.go:77.26,79.3 1 0
+github.com/feast-dev/feast/go/main.go:79.8,79.33 1 0
+github.com/feast-dev/feast/go/main.go:79.33,81.3 1 0
+github.com/feast-dev/feast/go/main.go:81.8,83.3 1 0
+github.com/feast-dev/feast/go/main.go:85.16,87.3 1 0
+github.com/feast-dev/feast/go/main.go:91.187,93.40 2 1
+github.com/feast-dev/feast/go/main.go:109.2,109.28 1 1
+github.com/feast-dev/feast/go/main.go:93.40,95.17 2 0
+github.com/feast-dev/feast/go/main.go:99.3,105.17 2 0
+github.com/feast-dev/feast/go/main.go:95.17,97.4 1 0
+github.com/feast-dev/feast/go/main.go:105.17,107.4 1 0
+github.com/feast-dev/feast/go/main.go:113.175,114.68 1 0
+github.com/feast-dev/feast/go/main.go:118.2,119.16 2 0
+github.com/feast-dev/feast/go/main.go:122.2,125.16 4 0
+github.com/feast-dev/feast/go/main.go:129.2,137.12 7 0
+github.com/feast-dev/feast/go/main.go:148.2,148.30 1 0
+github.com/feast-dev/feast/go/main.go:114.68,117.3 2 0
+github.com/feast-dev/feast/go/main.go:119.16,121.3 1 0
+github.com/feast-dev/feast/go/main.go:125.16,127.3 1 0
+github.com/feast-dev/feast/go/main.go:137.12,142.28 4 0
+github.com/feast-dev/feast/go/main.go:145.3,145.43 1 0
+github.com/feast-dev/feast/go/main.go:142.28,144.4 1 0
+github.com/feast-dev/feast/go/main.go:154.175,156.16 2 0
+github.com/feast-dev/feast/go/main.go:159.2,165.12 5 0
+github.com/feast-dev/feast/go/main.go:179.2,179.30 1 0
+github.com/feast-dev/feast/go/main.go:156.16,158.3 1 0
+github.com/feast-dev/feast/go/main.go:165.12,170.17 4 0
+github.com/feast-dev/feast/go/main.go:173.3,173.28 1 0
+github.com/feast-dev/feast/go/main.go:176.3,176.43 1 0
+github.com/feast-dev/feast/go/main.go:170.17,172.4 1 0
+github.com/feast-dev/feast/go/main.go:173.28,175.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:44.82,45.59 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:45.59,48.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:48.8,51.3 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:54.71,56.9 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:56.9,58.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:58.8,58.40 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:58.40,61.3 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:61.8,61.39 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:61.39,64.3 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/onlinestore.go:64.8,66.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:52.140,65.16 7 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:68.2,72.9 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:76.2,76.53 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:109.2,110.33 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:114.2,114.33 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:136.2,136.20 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:65.16,67.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:72.9,75.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:76.53,78.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:78.8,80.30 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:80.30,81.35 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:81.35,83.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:83.10,83.42 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:83.42,85.28 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:85.28,87.6 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:87.11,87.30 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:87.30,89.20 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:89.20,91.7 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:91.12,91.23 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:91.23,93.7 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:94.11,94.29 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:94.29,96.20 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:96.20,98.7 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:99.11,101.6 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:102.10,104.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:110.33,112.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:114.33,121.75 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:121.75,123.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:124.8,124.43 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:124.43,131.75 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:131.75,133.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:139.80,143.9 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:157.2,157.15 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:143.9,146.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:146.8,146.60 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:146.60,148.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:148.8,149.30 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:149.30,151.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:151.9,151.45 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:151.45,153.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:153.9,155.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:160.140,164.51 4 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:171.2,171.54 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:164.51,165.56 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:165.56,169.4 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:174.159,181.36 6 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:188.2,188.40 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:194.2,194.31 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:181.36,187.3 5 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:188.40,193.3 4 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:197.109,200.39 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:208.2,208.46 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:200.39,202.17 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:205.3,206.42 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:202.17,204.4 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:211.166,219.16 7 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:223.2,226.22 3 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:247.2,249.41 3 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:323.2,323.21 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:219.16,221.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:226.22,228.38 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:232.3,233.17 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:228.38,231.4 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:233.17,235.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:236.8,236.32 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:236.32,238.38 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:242.3,243.17 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:238.38,241.4 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:243.17,245.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:249.41,256.17 5 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:260.3,262.44 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:317.3,317.25 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:256.17,258.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:262.44,263.36 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:267.4,267.24 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:263.36,264.10 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:267.24,273.34 5 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:283.5,286.6 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:273.34,274.65 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:274.65,276.7 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:276.12,277.82 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:277.82,279.8 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:288.10,288.57 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:288.57,290.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:290.10,293.72 3 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:293.72,295.6 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:295.11,300.35 5 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:309.6,312.7 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:300.35,301.66 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:301.66,303.8 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:303.13,304.83 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:304.83,306.9 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:317.25,319.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:327.40,329.2 0 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:331.118,333.16 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:336.2,337.22 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:333.16,335.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:340.107,344.60 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:349.2,351.47 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:355.2,356.36 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:359.2,365.33 4 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:373.2,373.33 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:394.2,395.39 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:399.2,399.30 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:344.60,346.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:351.47,353.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:356.36,358.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:365.33,371.3 5 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:373.33,378.17 4 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:382.3,390.37 7 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:378.17,380.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:395.39,397.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:402.116,404.29 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:405.30,407.51 2 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:408.29,409.49 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:410.29,413.50 3 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:414.29,415.41 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:425.11,426.85 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:427.10,428.85 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:415.41,420.4 3 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/redisonlinestore.go:420.9,424.4 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:35.146,37.51 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:53.2,53.20 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:37.51,39.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:39.8,40.45 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:40.45,42.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:42.9,46.18 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:49.4,49.17 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:46.18,48.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:56.40,58.2 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:62.167,65.16 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:68.2,73.39 6 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:84.2,85.38 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:89.2,89.51 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:122.2,122.21 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:65.16,67.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:73.39,75.17 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:79.3,82.35 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:75.17,77.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:85.38,87.3 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:89.51,95.17 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:98.3,99.19 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:95.17,97.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:99.19,106.18 7 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:109.4,109.63 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:112.4,113.30 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:116.4,119.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:106.18,108.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:109.63,111.5 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:113.30,115.5 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:126.62,129.17 3 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:139.2,139.18 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:129.17,130.19 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:133.3,135.17 3 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:130.19,132.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:135.17,137.4 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:143.61,145.2 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:148.60,150.16 2 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:153.2,153.16 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:150.16,152.3 1 0
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:156.66,157.32 1 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:160.2,163.18 4 1
+github.com/feast-dev/feast/go/internal/feast/onlinestore/sqliteonlinestore.go:157.32,159.3 1 0
diff --git a/go/internal/feast/featurestore.go b/go/internal/feast/featurestore.go
index 390b011851..4b0f1494d1 100644
--- a/go/internal/feast/featurestore.go
+++ b/go/internal/feast/featurestore.go
@@ -19,6 +19,15 @@ import (
prototypes "github.com/feast-dev/feast/go/protos/feast/types"
)
+type FeatureStoreInterface interface {
+ GetOnlineFeatures(
+ ctx context.Context,
+ featureRefs []string,
+ featureService *model.FeatureService,
+ joinKeyToEntityValues map[string]*prototypes.RepeatedValue,
+ requestData map[string]*prototypes.RepeatedValue,
+ fullFeatureNames bool) ([]*onlineserving.FeatureVector, error)
+}
type FeatureStore struct {
config *registry.RepoConfig
registry *registry.Registry
@@ -85,7 +94,7 @@ func (fs *FeatureStore) GetOnlineFeatures(
joinKeyToEntityValues map[string]*prototypes.RepeatedValue,
requestData map[string]*prototypes.RepeatedValue,
fullFeatureNames bool) ([]*onlineserving.FeatureVector, error) {
- fvs, odFvs, err := fs.listAllViews()
+ fvs, odFvs, err := fs.ListAllViews()
if err != nil {
return nil, err
}
@@ -230,7 +239,7 @@ func (fs *FeatureStore) GetFeatureService(name string) (*model.FeatureService, e
return fs.registry.GetFeatureService(fs.config.Project, name)
}
-func (fs *FeatureStore) listAllViews() (map[string]*model.FeatureView, map[string]*model.OnDemandFeatureView, error) {
+func (fs *FeatureStore) ListAllViews() (map[string]*model.FeatureView, map[string]*model.OnDemandFeatureView, error) {
fvs := make(map[string]*model.FeatureView)
odFvs := make(map[string]*model.OnDemandFeatureView)
@@ -291,6 +300,33 @@ func (fs *FeatureStore) ListEntities(hideDummyEntity bool) ([]*model.Entity, err
return entities, nil
}
+func (fs *FeatureStore) GetEntityByKey(entityKey string) (*model.Entity, error) {
+
+ entities, err := fs.ListEntities(false)
+ if err != nil {
+ return nil, err
+ }
+ for _, entity := range entities {
+ if entity.JoinKey == entityKey {
+ return entity, nil
+ }
+ }
+ return nil, fmt.Errorf("Entity with key %s not found", entityKey)
+}
+func (fs *FeatureStore) GetRequestSources(odfvList []*model.OnDemandFeatureView) (map[string]prototypes.ValueType_Enum, error) {
+
+ requestSources := make(map[string]prototypes.ValueType_Enum, 0)
+ if len(odfvList) > 0 {
+ for _, odfv := range odfvList {
+ schema := odfv.GetRequestDataSchema()
+ for name, dtype := range schema {
+ requestSources[name] = dtype
+ }
+ }
+ }
+ return requestSources, nil
+}
+
func (fs *FeatureStore) ListOnDemandFeatureViews() ([]*model.OnDemandFeatureView, error) {
return fs.registry.ListOnDemandFeatureViews(fs.config.Project)
}
@@ -311,6 +347,14 @@ func (fs *FeatureStore) GetFeatureView(featureViewName string, hideDummyEntity b
return fv, nil
}
+func (fs *FeatureStore) GetOnDemandFeatureView(featureViewName string) (*model.OnDemandFeatureView, error) {
+ fv, err := fs.registry.GetOnDemandFeatureView(fs.config.Project, featureViewName)
+ if err != nil {
+ return nil, err
+ }
+ return fv, nil
+}
+
func (fs *FeatureStore) readFromOnlineStore(ctx context.Context, entityRows []*prototypes.EntityKey,
requestedFeatureViewNames []string,
requestedFeatureNames []string,
diff --git a/go/internal/feast/featurestore_test.go b/go/internal/feast/featurestore_test.go
index dd08bc287e..3f755eb999 100644
--- a/go/internal/feast/featurestore_test.go
+++ b/go/internal/feast/featurestore_test.go
@@ -2,30 +2,17 @@ package feast
import (
"context"
- "path/filepath"
- "runtime"
+ "github.com/feast-dev/feast/go/internal/feast/model"
+ "github.com/feast-dev/feast/go/protos/feast/core"
"testing"
"github.com/stretchr/testify/assert"
"github.com/feast-dev/feast/go/internal/feast/onlinestore"
"github.com/feast-dev/feast/go/internal/feast/registry"
- "github.com/feast-dev/feast/go/protos/feast/types"
+ types "github.com/feast-dev/feast/go/protos/feast/types"
)
-// Return absolute path to the test_repo registry regardless of the working directory
-func getRegistryPath() map[string]interface{} {
- // Get the file path of this source file, regardless of the working directory
- _, filename, _, ok := runtime.Caller(0)
- if !ok {
- panic("couldn't find file path of the test file")
- }
- registry := map[string]interface{}{
- "path": filepath.Join(filename, "..", "..", "..", "feature_repo/data/registry.db"),
- }
- return registry
-}
-
func TestNewFeatureStore(t *testing.T) {
t.Skip("@todo(achals): feature_repo isn't checked in yet")
config := registry.RepoConfig{
@@ -70,3 +57,68 @@ func TestGetOnlineFeaturesRedis(t *testing.T) {
assert.Nil(t, err)
assert.Len(t, response, 4) // 3 Features + 1 entity = 4 columns (feature vectors) in response
}
+func TestGetRequestSources(t *testing.T) {
+ config := GetRepoConfig()
+ fs, _ := NewFeatureStore(&config, nil)
+
+ odfv := &core.OnDemandFeatureView{
+ Spec: &core.OnDemandFeatureViewSpec{
+ Name: "odfv1",
+ Project: "feature_repo",
+ Sources: map[string]*core.OnDemandSource{
+ "odfv1": {
+ Source: &core.OnDemandSource_RequestDataSource{
+ RequestDataSource: &core.DataSource{
+ Name: "request_source_1",
+ Type: core.DataSource_REQUEST_SOURCE,
+ Options: &core.DataSource_RequestDataOptions_{
+ RequestDataOptions: &core.DataSource_RequestDataOptions{
+ DeprecatedSchema: map[string]types.ValueType_Enum{
+ "feature1": types.ValueType_INT64,
+ },
+ Schema: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+
+ cached_odfv := &model.OnDemandFeatureView{
+ Base: model.NewBaseFeatureView("odfv1", []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ }),
+ SourceFeatureViewProjections: make(map[string]*model.FeatureViewProjection),
+ SourceRequestDataSources: map[string]*core.DataSource_RequestDataOptions{
+ "request_source_1": {
+ Schema: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ },
+ },
+ }
+ fVList := make([]*model.OnDemandFeatureView, 0)
+ fVList = append(fVList, cached_odfv)
+ cachedOnDemandFVs := make(map[string]map[string]*core.OnDemandFeatureView)
+ cachedOnDemandFVs["feature_repo"] = make(map[string]*core.OnDemandFeatureView)
+ cachedOnDemandFVs["feature_repo"]["odfv1"] = odfv
+ fs.registry.CachedOnDemandFeatureViews = cachedOnDemandFVs
+ requestSources, err := fs.GetRequestSources(fVList)
+
+ assert.Nil(t, err)
+ assert.Equal(t, 1, len(requestSources))
+ assert.Equal(t, types.ValueType_INT64.Enum(), requestSources["feat1"].Enum())
+}
diff --git a/go/internal/feast/model/entity.go b/go/internal/feast/model/entity.go
index 5a09edb655..736920346d 100644
--- a/go/internal/feast/model/entity.go
+++ b/go/internal/feast/model/entity.go
@@ -2,16 +2,19 @@ package model
import (
"github.com/feast-dev/feast/go/protos/feast/core"
+ "github.com/feast-dev/feast/go/protos/feast/types"
)
type Entity struct {
- Name string
- JoinKey string
+ Name string
+ JoinKey string
+ ValueType types.ValueType_Enum
}
func NewEntityFromProto(proto *core.Entity) *Entity {
return &Entity{
- Name: proto.Spec.Name,
- JoinKey: proto.Spec.JoinKey,
+ Name: proto.Spec.Name,
+ JoinKey: proto.Spec.JoinKey,
+ ValueType: proto.Spec.ValueType,
}
}
diff --git a/go/internal/feast/registry/registry.go b/go/internal/feast/registry/registry.go
index 6ff6f20641..60c55c8380 100644
--- a/go/internal/feast/registry/registry.go
+++ b/go/internal/feast/registry/registry.go
@@ -31,11 +31,11 @@ var REGISTRY_STORE_CLASS_FOR_SCHEME map[string]string = map[string]string{
type Registry struct {
project string
registryStore RegistryStore
- cachedFeatureServices map[string]map[string]*core.FeatureService
- cachedEntities map[string]map[string]*core.Entity
- cachedFeatureViews map[string]map[string]*core.FeatureView
+ CachedFeatureServices map[string]map[string]*core.FeatureService
+ CachedEntities map[string]map[string]*core.Entity
+ CachedFeatureViews map[string]map[string]*core.FeatureView
cachedStreamFeatureViews map[string]map[string]*core.StreamFeatureView
- cachedOnDemandFeatureViews map[string]map[string]*core.OnDemandFeatureView
+ CachedOnDemandFeatureViews map[string]map[string]*core.OnDemandFeatureView
cachedRegistry *core.Registry
cachedRegistryProtoLastUpdated time.Time
cachedRegistryProtoTtl time.Duration
@@ -113,11 +113,11 @@ func (r *Registry) load(registry *core.Registry) {
r.mu.Lock()
defer r.mu.Unlock()
r.cachedRegistry = registry
- r.cachedFeatureServices = make(map[string]map[string]*core.FeatureService)
- r.cachedEntities = make(map[string]map[string]*core.Entity)
- r.cachedFeatureViews = make(map[string]map[string]*core.FeatureView)
+ r.CachedFeatureServices = make(map[string]map[string]*core.FeatureService)
+ r.CachedEntities = make(map[string]map[string]*core.Entity)
+ r.CachedFeatureViews = make(map[string]map[string]*core.FeatureView)
r.cachedStreamFeatureViews = make(map[string]map[string]*core.StreamFeatureView)
- r.cachedOnDemandFeatureViews = make(map[string]map[string]*core.OnDemandFeatureView)
+ r.CachedOnDemandFeatureViews = make(map[string]map[string]*core.OnDemandFeatureView)
r.loadEntities(registry)
r.loadFeatureServices(registry)
r.loadFeatureViews(registry)
@@ -129,30 +129,30 @@ func (r *Registry) load(registry *core.Registry) {
func (r *Registry) loadEntities(registry *core.Registry) {
entities := registry.Entities
for _, entity := range entities {
- if _, ok := r.cachedEntities[r.project]; !ok {
- r.cachedEntities[r.project] = make(map[string]*core.Entity)
+ if _, ok := r.CachedEntities[r.project]; !ok {
+ r.CachedEntities[r.project] = make(map[string]*core.Entity)
}
- r.cachedEntities[r.project][entity.Spec.Name] = entity
+ r.CachedEntities[r.project][entity.Spec.Name] = entity
}
}
func (r *Registry) loadFeatureServices(registry *core.Registry) {
featureServices := registry.FeatureServices
for _, featureService := range featureServices {
- if _, ok := r.cachedFeatureServices[r.project]; !ok {
- r.cachedFeatureServices[r.project] = make(map[string]*core.FeatureService)
+ if _, ok := r.CachedFeatureServices[r.project]; !ok {
+ r.CachedFeatureServices[r.project] = make(map[string]*core.FeatureService)
}
- r.cachedFeatureServices[r.project][featureService.Spec.Name] = featureService
+ r.CachedFeatureServices[r.project][featureService.Spec.Name] = featureService
}
}
func (r *Registry) loadFeatureViews(registry *core.Registry) {
featureViews := registry.FeatureViews
for _, featureView := range featureViews {
- if _, ok := r.cachedFeatureViews[r.project]; !ok {
- r.cachedFeatureViews[r.project] = make(map[string]*core.FeatureView)
+ if _, ok := r.CachedFeatureViews[r.project]; !ok {
+ r.CachedFeatureViews[r.project] = make(map[string]*core.FeatureView)
}
- r.cachedFeatureViews[r.project][featureView.Spec.Name] = featureView
+ r.CachedFeatureViews[r.project][featureView.Spec.Name] = featureView
}
}
@@ -169,10 +169,10 @@ func (r *Registry) loadStreamFeatureViews(registry *core.Registry) {
func (r *Registry) loadOnDemandFeatureViews(registry *core.Registry) {
onDemandFeatureViews := registry.OnDemandFeatureViews
for _, onDemandFeatureView := range onDemandFeatureViews {
- if _, ok := r.cachedOnDemandFeatureViews[r.project]; !ok {
- r.cachedOnDemandFeatureViews[r.project] = make(map[string]*core.OnDemandFeatureView)
+ if _, ok := r.CachedOnDemandFeatureViews[r.project]; !ok {
+ r.CachedOnDemandFeatureViews[r.project] = make(map[string]*core.OnDemandFeatureView)
}
- r.cachedOnDemandFeatureViews[r.project][onDemandFeatureView.Spec.Name] = onDemandFeatureView
+ r.CachedOnDemandFeatureViews[r.project][onDemandFeatureView.Spec.Name] = onDemandFeatureView
}
}
@@ -184,7 +184,7 @@ func (r *Registry) loadOnDemandFeatureViews(registry *core.Registry) {
func (r *Registry) ListEntities(project string) ([]*model.Entity, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedEntities, ok := r.cachedEntities[project]; !ok {
+ if cachedEntities, ok := r.CachedEntities[project]; !ok {
return []*model.Entity{}, nil
} else {
entities := make([]*model.Entity, len(cachedEntities))
@@ -205,7 +205,7 @@ func (r *Registry) ListEntities(project string) ([]*model.Entity, error) {
func (r *Registry) ListFeatureViews(project string) ([]*model.FeatureView, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedFeatureViews, ok := r.cachedFeatureViews[project]; !ok {
+ if cachedFeatureViews, ok := r.CachedFeatureViews[project]; !ok {
return []*model.FeatureView{}, nil
} else {
featureViews := make([]*model.FeatureView, len(cachedFeatureViews))
@@ -247,7 +247,7 @@ func (r *Registry) ListStreamFeatureViews(project string) ([]*model.FeatureView,
func (r *Registry) ListFeatureServices(project string) ([]*model.FeatureService, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedFeatureServices, ok := r.cachedFeatureServices[project]; !ok {
+ if cachedFeatureServices, ok := r.CachedFeatureServices[project]; !ok {
return []*model.FeatureService{}, nil
} else {
featureServices := make([]*model.FeatureService, len(cachedFeatureServices))
@@ -268,7 +268,7 @@ func (r *Registry) ListFeatureServices(project string) ([]*model.FeatureService,
func (r *Registry) ListOnDemandFeatureViews(project string) ([]*model.OnDemandFeatureView, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedOnDemandFeatureViews, ok := r.cachedOnDemandFeatureViews[project]; !ok {
+ if cachedOnDemandFeatureViews, ok := r.CachedOnDemandFeatureViews[project]; !ok {
return []*model.OnDemandFeatureView{}, nil
} else {
onDemandFeatureViews := make([]*model.OnDemandFeatureView, len(cachedOnDemandFeatureViews))
@@ -284,7 +284,7 @@ func (r *Registry) ListOnDemandFeatureViews(project string) ([]*model.OnDemandFe
func (r *Registry) GetEntity(project, entityName string) (*model.Entity, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedEntities, ok := r.cachedEntities[project]; !ok {
+ if cachedEntities, ok := r.CachedEntities[project]; !ok {
return nil, fmt.Errorf("no cached entities found for project %s", project)
} else {
if entity, ok := cachedEntities[entityName]; !ok {
@@ -298,7 +298,7 @@ func (r *Registry) GetEntity(project, entityName string) (*model.Entity, error)
func (r *Registry) GetFeatureView(project, featureViewName string) (*model.FeatureView, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedFeatureViews, ok := r.cachedFeatureViews[project]; !ok {
+ if cachedFeatureViews, ok := r.CachedFeatureViews[project]; !ok {
return nil, fmt.Errorf("no cached feature views found for project %s", project)
} else {
if featureViewProto, ok := cachedFeatureViews[featureViewName]; !ok {
@@ -326,7 +326,7 @@ func (r *Registry) GetStreamFeatureView(project, streamFeatureViewName string) (
func (r *Registry) GetFeatureService(project, featureServiceName string) (*model.FeatureService, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedFeatureServices, ok := r.cachedFeatureServices[project]; !ok {
+ if cachedFeatureServices, ok := r.CachedFeatureServices[project]; !ok {
return nil, fmt.Errorf("no cached feature services found for project %s", project)
} else {
if featureServiceProto, ok := cachedFeatureServices[featureServiceName]; !ok {
@@ -340,7 +340,7 @@ func (r *Registry) GetFeatureService(project, featureServiceName string) (*model
func (r *Registry) GetOnDemandFeatureView(project, onDemandFeatureViewName string) (*model.OnDemandFeatureView, error) {
r.mu.RLock()
defer r.mu.RUnlock()
- if cachedOnDemandFeatureViews, ok := r.cachedOnDemandFeatureViews[project]; !ok {
+ if cachedOnDemandFeatureViews, ok := r.CachedOnDemandFeatureViews[project]; !ok {
return nil, fmt.Errorf("no cached on demand feature views found for project %s", project)
} else {
if onDemandFeatureViewProto, ok := cachedOnDemandFeatureViews[onDemandFeatureViewName]; !ok {
diff --git a/go/internal/feast/server/http_server.go b/go/internal/feast/server/http_server.go
index 2e5f766c7d..bb7774942f 100644
--- a/go/internal/feast/server/http_server.go
+++ b/go/internal/feast/server/http_server.go
@@ -3,6 +3,7 @@ package server
import (
"context"
"encoding/json"
+ "errors"
"fmt"
"net/http"
"os"
@@ -32,10 +33,12 @@ type httpServer struct {
// Some Feast types aren't supported during JSON conversion
type repeatedValue struct {
stringVal []string
+ int32Val []int32
int64Val []int64
doubleVal []float64
boolVal []bool
stringListVal [][]string
+ int32ListVal [][]int32
int64ListVal [][]int64
doubleListVal [][]float64
boolListVal [][]bool
@@ -101,6 +104,11 @@ func (u *repeatedValue) ToProto() *prototypes.RepeatedValue {
proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int64Val{Int64Val: val}})
}
}
+ if u.int32Val != nil {
+ for _, val := range u.int32Val {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int32Val{Int32Val: val}})
+ }
+ }
if u.doubleVal != nil {
for _, val := range u.doubleVal {
proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_DoubleVal{DoubleVal: val}})
@@ -116,6 +124,11 @@ func (u *repeatedValue) ToProto() *prototypes.RepeatedValue {
proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_StringListVal{StringListVal: &prototypes.StringList{Val: val}}})
}
}
+ if u.int32ListVal != nil {
+ for _, val := range u.int32ListVal {
+ proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int32ListVal{Int32ListVal: &prototypes.Int32List{Val: val}}})
+ }
+ }
if u.int64ListVal != nil {
for _, val := range u.int64ListVal {
proto.Val = append(proto.Val, &prototypes.Value{Val: &prototypes.Value_Int64ListVal{Int64ListVal: &prototypes.Int64List{Val: val}}})
@@ -146,6 +159,21 @@ func NewHttpServer(fs *feast.FeatureStore, loggingService *logging.LoggingServic
return &httpServer{fs: fs, loggingService: loggingService}
}
+/*
+*
+Used to align a field specified in the request with its defined schema type.
+*/
+func typecastToFieldSchemaType(val *repeatedValue, fieldType prototypes.ValueType_Enum) {
+ if val.int64Val != nil {
+ if fieldType == prototypes.ValueType_INT32 {
+ for _, v := range val.int64Val {
+ val.int32Val = append(val.int32Val, int32(v))
+ }
+ val.int64Val = nil
+ }
+ }
+}
+
func (s *httpServer) getOnlineFeatures(w http.ResponseWriter, r *http.Request) {
var err error
@@ -180,21 +208,85 @@ func (s *httpServer) getOnlineFeatures(w http.ResponseWriter, r *http.Request) {
return
}
var featureService *model.FeatureService
- if request.FeatureService != nil {
+ var entitiesProto = make(map[string]*prototypes.RepeatedValue)
+ var requestContextProto = make(map[string]*prototypes.RepeatedValue)
+ var odfVList = make([]*model.OnDemandFeatureView, 0)
+ var requestSources = make(map[string]prototypes.ValueType_Enum)
+
+ if request.FeatureService != nil && *request.FeatureService != "" {
featureService, err = s.fs.GetFeatureService(*request.FeatureService)
if err != nil {
logSpanContext.Error().Err(err).Msg("Error getting feature service from registry")
writeJSONError(w, fmt.Errorf("Error getting feature service from registry: %+v", err), http.StatusInternalServerError)
return
}
+ for _, fv := range featureService.Projections {
+ odfv, _ := s.fs.GetOnDemandFeatureView(fv.Name)
+ if odfv != nil {
+ odfVList = append(odfVList, odfv)
+ }
+ }
+ } else if len(request.Features) > 0 {
+ log.Info().Msgf("request.Features %v", request.Features)
+ for _, featureName := range request.Features {
+ fVName, _, err := onlineserving.ParseFeatureReference(featureName)
+ if err != nil {
+ logSpanContext.Error().Err(err)
+ writeJSONError(w, fmt.Errorf("Error parsing feature reference %s", featureName), http.StatusBadRequest)
+ return
+ }
+ fv, odfv, _ := s.fs.ListAllViews()
+ if _, ok1 := odfv[fVName]; ok1 {
+ odfVList = append(odfVList, odfv[fVName])
+ } else if _, ok1 := fv[fVName]; !ok1 {
+ logSpanContext.Error().Msg("Feature View not found")
+ writeJSONError(w, fmt.Errorf("Feature View %s not found", featureName), http.StatusInternalServerError)
+ return
+ }
+ }
+ } else {
+ logSpanContext.Error().Msg("No Feature Views or Feature Services specified in the request")
+ writeJSONError(w, errors.New("No Feature Views or Feature Services specified in the request"), http.StatusBadRequest)
+ return
}
- entitiesProto := make(map[string]*prototypes.RepeatedValue)
- for key, value := range request.Entities {
- entitiesProto[key] = value.ToProto()
+ if odfVList != nil {
+ requestSources, _ = s.fs.GetRequestSources(odfVList)
}
- requestContextProto := make(map[string]*prototypes.RepeatedValue)
- for key, value := range request.RequestContext {
- requestContextProto[key] = value.ToProto()
+ if len(request.Entities) > 0 {
+ var entityType prototypes.ValueType_Enum
+ for key, value := range request.Entities {
+ entity, err := s.fs.GetEntityByKey(key)
+ if err != nil {
+ if len(requestSources) == 0 {
+ logSpanContext.Error().Err(err)
+ writeJSONError(w, err, http.StatusNotFound)
+ return
+ }
+ requestSourceType, ok := requestSources[key]
+ if !ok {
+ logSpanContext.Error().Msgf("Entity with key or Request Source with name %s not found ", key)
+ writeJSONError(w, fmt.Errorf("Entity with key or Request Source with name %s not found ", key), http.StatusNotFound)
+ return
+ }
+ entityType = requestSourceType
+ } else {
+ entityType = entity.ValueType
+ }
+ typecastToFieldSchemaType(&value, entityType)
+ entitiesProto[key] = value.ToProto()
+ }
+ }
+ if request.RequestContext != nil && len(request.RequestContext) > 0 {
+ for key, value := range request.RequestContext {
+ requestSourceType, ok := requestSources[key]
+ if !ok {
+ logSpanContext.Error().Msgf("Request Source %s not found ", key)
+ writeJSONError(w, fmt.Errorf("Request Source %s not found ", key), http.StatusNotFound)
+ return
+ }
+ typecastToFieldSchemaType(&value, requestSourceType)
+ requestContextProto[key] = value.ToProto()
+ }
}
featureVectors, err := s.fs.GetOnlineFeatures(
diff --git a/go/internal/feast/server/http_server_test.go b/go/internal/feast/server/http_server_test.go
index e0d474a9f3..721fcdc693 100644
--- a/go/internal/feast/server/http_server_test.go
+++ b/go/internal/feast/server/http_server_test.go
@@ -1,13 +1,17 @@
package server
import (
- "encoding/json"
- "testing"
-
- "github.com/apache/arrow/go/v17/arrow"
- "github.com/apache/arrow/go/v17/arrow/array"
- "github.com/apache/arrow/go/v17/arrow/memory"
+ "github.com/feast-dev/feast/go/internal/feast"
+ "github.com/feast-dev/feast/go/protos/feast/core"
+ "github.com/feast-dev/feast/go/protos/feast/types"
"github.com/stretchr/testify/assert"
+ "google.golang.org/protobuf/types/known/durationpb"
+ "google.golang.org/protobuf/types/known/timestamppb"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
)
func TestUnmarshalJSON(t *testing.T) {
@@ -43,36 +47,1120 @@ func TestUnmarshalJSON(t *testing.T) {
assert.Nil(t, u.UnmarshalJSON([]byte("[[true, false, true], [false, true, false]]")))
assert.Equal(t, [][]bool{{true, false, true}, {false, true, false}}, u.boolListVal)
}
-func TestMarshalInt32JSON(t *testing.T) {
- var arrowArray arrow.Array
- memoryPool := memory.NewGoAllocator()
- builder := array.NewInt32Builder(memoryPool)
- defer builder.Release()
- builder.AppendValues([]int32{1, 2, 3, 4}, nil)
- arrowArray = builder.NewArray()
- defer arrowArray.Release()
- expectedJSON := `[1,2,3,4]`
-
- jsonData, err := json.Marshal(arrowArray)
- assert.NoError(t, err, "Error marshaling Arrow array")
-
- assert.Equal(t, expectedJSON, string(jsonData), "JSON output does not match expected")
- assert.IsType(t, &array.Int32{}, arrowArray, "arrowArray is not of type *array.Int32")
+
+func TestGetOnlineFeaturesWithValidRequest(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+ jsonRequest := `{
+ "features": ["fv1:feat1", "fv1:feat2"],
+ "entities": {
+ "join_key_1": [1, 2],
+ "join_key_2": ["value1", "value2"]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1", "entity2"},
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ entity2 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity2",
+ Project: "feature_repo",
+ ValueType: types.ValueType_STRING,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_2",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["fv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+ cachedEntities["feature_repo"]["entity2"] = entity2
+
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ req.Header.Set("Content-Type", "application/json")
+ s.getOnlineFeatures(rr, req)
+
+ // Retrieve connection error string (as currently there's no mock for OnlineStore)
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ // Error is only due to connection error resulting from not mocking
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+ strings.Contains(bodyString, "connection refused")
+}
+func TestGetOnlineFeaturesWithEmptyFeatures(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "entities": {
+ "entity1": [1, 2, 3],
+ "entity2": ["value1", "value2"]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1", "entity2"},
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ entity2 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity2",
+ Project: "feature_repo",
+ ValueType: types.ValueType_STRING,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_2",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["fv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+ cachedEntities["feature_repo"]["entity2"] = entity2
+
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ assert.Equal(t, http.StatusBadRequest, rr.Code)
+}
+
+func TestGetOnlineFeaturesWithEmptyEntities(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "features": ["fv1:feat1", "fv1:feat2"]
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1", "entity2"},
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ entity2 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity2",
+ Project: "feature_repo",
+ ValueType: types.ValueType_STRING,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_2",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["fv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+ cachedEntities["feature_repo"]["entity2"] = entity2
+
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+}
+
+func TestGetOnlineFeaturesWithValidFeatureService(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "feature_service": "fs1",
+ "entities": {
+ "join_key_1": [1, 2],
+ "join_key_2": ["value1", "value2"]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1", "entity2"},
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ entity2 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity2",
+ Project: "feature_repo",
+ ValueType: types.ValueType_STRING,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_2",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ featureService := &core.FeatureService{
+ Spec: &core.FeatureServiceSpec{
+ Name: "fs1",
+ Project: "feature_repo",
+ Description: "This is a sample feature service",
+ Owner: "sample_owner",
+ Features: []*core.FeatureViewProjection{
+ {
+ FeatureViewName: "fv1",
+ FeatureColumns: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ //JoinKeyMap: map[string]string{
+ // "join_key_1": "",
+ // "join_key_2": "",
+ //},
+ },
+ },
+ },
+ Meta: &core.FeatureServiceMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["fv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+ cachedEntities["feature_repo"]["entity2"] = entity2
+
+ cachedFSs := make(map[string]map[string]*core.FeatureService)
+ cachedFSs["feature_repo"] = make(map[string]*core.FeatureService)
+ cachedFSs["feature_repo"]["fs1"] = featureService
+
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedFeatureServices = cachedFSs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ // Retrieve connection error string (as currently there's no mock for OnlineStore)
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ // Error is only due to connection error resulting from not mocking
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+ strings.Contains(bodyString, "connection refused")
+
+}
+
+func TestGetOnlineFeaturesWithInvalidFeatureService(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "feature_service": "invalid_fs",
+ "entities": {
+ "join_key_1": [1, 2],
+ "join_key_2": ["value1", "value2"]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1", "entity2"},
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ entity2 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity2",
+ Project: "feature_repo",
+ ValueType: types.ValueType_STRING,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_2",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ featureService := &core.FeatureService{
+ Spec: &core.FeatureServiceSpec{
+ Name: "fs1",
+ Project: "feature_repo",
+ Description: "This is a sample feature service",
+ Owner: "sample_owner",
+ Features: []*core.FeatureViewProjection{
+ {
+ FeatureViewName: "fv1",
+ FeatureColumns: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ //JoinKeyMap: map[string]string{
+ // "join_key_1": "",
+ // "join_key_2": "",
+ //},
+ },
+ },
+ },
+ Meta: &core.FeatureServiceMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["fv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+ cachedEntities["feature_repo"]["entity2"] = entity2
+
+ cachedFSs := make(map[string]map[string]*core.FeatureService)
+ cachedFSs["feature_repo"] = make(map[string]*core.FeatureService)
+ cachedFSs["feature_repo"]["fs1"] = featureService
+
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedFeatureServices = cachedFSs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ // Retrieve error string
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+ assert.Equal(t, strings.Contains(bodyString, "no cached feature service"), true)
+}
+
+func TestGetOnlineFeaturesWithInvalidEntities(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "features": ["fv1:feat1", "fv1:feat2"],
+ "entities": {
+ "join_key_invalid": [1, 2],
+ "join_key_2": ["value1", "value2"]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1", "entity2"},
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ entity2 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity2",
+ Project: "feature_repo",
+ ValueType: types.ValueType_STRING,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_2",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["fv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+ cachedEntities["feature_repo"]["entity2"] = entity2
+
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ // Retrieve error string
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ assert.Equal(t, http.StatusNotFound, rr.Code)
+ assert.Equal(t, strings.Contains(bodyString, "Entity with key join_key_invalid not found"), true)
}
-func TestMarshalInt64JSON(t *testing.T) {
- var arrowArray arrow.Array
- memoryPool := memory.NewGoAllocator()
- builder := array.NewInt64Builder(memoryPool)
- defer builder.Release()
- builder.AppendValues([]int64{-9223372036854775808, 9223372036854775807}, nil)
- arrowArray = builder.NewArray()
- defer arrowArray.Release()
- expectedJSON := `[-9223372036854775808,9223372036854775807]`
-
- jsonData, err := json.Marshal(arrowArray)
- assert.NoError(t, err, "Error marshaling Arrow array")
-
- assert.Equal(t, expectedJSON, string(jsonData), "JSON output does not match expected")
- assert.IsType(t, &array.Int64{}, arrowArray, "arrowArray is not of type *array.Int64")
+func TestGetOnlineFeaturesWithEntities(t *testing.T) {
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "features": ["odfv1:feat1"],
+ "entities": {
+ "join_key_1": [1, 2],
+ "rq_field": [1, 2]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1"},
+ },
+ }
+
+ odfv := &core.OnDemandFeatureView{
+ Spec: &core.OnDemandFeatureViewSpec{
+ Name: "odfv1",
+ Project: "feature_repo",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ Sources: map[string]*core.OnDemandSource{
+ "rqsource1": {
+ Source: &core.OnDemandSource_RequestDataSource{
+ RequestDataSource: &core.DataSource{
+ Name: "rqsource1",
+ Type: core.DataSource_REQUEST_SOURCE,
+ Options: &core.DataSource_RequestDataOptions_{
+ RequestDataOptions: &core.DataSource_RequestDataOptions{
+ Schema: []*core.FeatureSpecV2{
+ {
+ Name: "rq_field",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "fv1": {
+ Source: &core.OnDemandSource_FeatureView{
+ FeatureView: &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1"},
+ },
+ },
+ },
+ },
+ },
+ FeatureTransformation: &core.FeatureTransformationV2{
+ Transformation: &core.FeatureTransformationV2_UserDefinedFunction{
+ UserDefinedFunction: &core.UserDefinedFunctionV2{
+ Name: "Sample User Defined Function V2",
+ Body: []byte("function body"),
+ BodyText: "function body text",
+ },
+ },
+ },
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedODFVs := make(map[string]map[string]*core.OnDemandFeatureView)
+ cachedODFVs["feature_repo"] = make(map[string]*core.OnDemandFeatureView)
+ cachedODFVs["feature_repo"]["odfv1"] = odfv
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["odfv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+
+ s.fs.Registry().CachedOnDemandFeatureViews = cachedODFVs
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ // Retrieve connection error string (as currently there's no mock for OnlineStore)
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ // Error is only due to connection error resulting from not mocking
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+ assert.Equal(t, strings.Contains(bodyString, "connection refused"), true)
+}
+
+func TestGetOnlineFeaturesWithRequestContextOnly(t *testing.T) {
+
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "features": ["odfv1:feat1"],
+ "entities": {
+ "join_key_1": [1, 2]
+ },
+ "request_context": {
+ "rq_field": [1, 2]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1"},
+ },
+ }
+
+ odfv := &core.OnDemandFeatureView{
+ Spec: &core.OnDemandFeatureViewSpec{
+ Name: "odfv1",
+ Project: "feature_repo",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ Sources: map[string]*core.OnDemandSource{
+ "rqsource1": {
+ Source: &core.OnDemandSource_RequestDataSource{
+ RequestDataSource: &core.DataSource{
+ Name: "rqsource1",
+ Type: core.DataSource_REQUEST_SOURCE,
+ Options: &core.DataSource_RequestDataOptions_{
+ RequestDataOptions: &core.DataSource_RequestDataOptions{
+ Schema: []*core.FeatureSpecV2{
+ {
+ Name: "rq_field",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "fv1": {
+ Source: &core.OnDemandSource_FeatureView{
+ FeatureView: &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1"},
+ },
+ },
+ },
+ },
+ },
+ FeatureTransformation: &core.FeatureTransformationV2{
+ Transformation: &core.FeatureTransformationV2_UserDefinedFunction{
+ UserDefinedFunction: &core.UserDefinedFunctionV2{
+ Name: "Sample User Defined Function V2",
+ Body: []byte("function body"),
+ BodyText: "function body text",
+ },
+ },
+ },
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedODFVs := make(map[string]map[string]*core.OnDemandFeatureView)
+ cachedODFVs["feature_repo"] = make(map[string]*core.OnDemandFeatureView)
+ cachedODFVs["feature_repo"]["odfv1"] = odfv
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["odfv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+
+ s.fs.Registry().CachedOnDemandFeatureViews = cachedODFVs
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ // Retrieve connection error string (as currently there's no mock for OnlineStore)
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ // Error is only due to connection error resulting from not mocking
+ assert.Equal(t, http.StatusInternalServerError, rr.Code)
+ assert.Equal(t, strings.Contains(bodyString, "connection refused"), true)
+
+}
+
+func TestGetOnlineFeaturesWithInvalidRequestContext(t *testing.T) {
+
+ s := NewHttpServer(nil, nil)
+
+ config := feast.GetRepoConfig()
+ s.fs, _ = feast.NewFeatureStore(&config, nil)
+
+ jsonRequest := `{
+ "features": ["odfv1:feat1"],
+ "entities": {
+ "join_key_1": [1, 2]
+ },
+ "request_context": {
+ "rq_field_1": [1, 2]
+ }
+ }`
+
+ fv := &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1"},
+ },
+ }
+
+ odfv := &core.OnDemandFeatureView{
+ Spec: &core.OnDemandFeatureViewSpec{
+ Name: "odfv1",
+ Project: "feature_repo",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ Sources: map[string]*core.OnDemandSource{
+ "rqsource1": {
+ Source: &core.OnDemandSource_RequestDataSource{
+ RequestDataSource: &core.DataSource{
+ Name: "rqsource1",
+ Type: core.DataSource_REQUEST_SOURCE,
+ Options: &core.DataSource_RequestDataOptions_{
+ RequestDataOptions: &core.DataSource_RequestDataOptions{
+ Schema: []*core.FeatureSpecV2{
+ {
+ Name: "rq_field",
+ ValueType: types.ValueType_INT64,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "fv1": {
+ Source: &core.OnDemandSource_FeatureView{
+ FeatureView: &core.FeatureView{
+ Spec: &core.FeatureViewSpec{
+ Name: "fv1",
+ Features: []*core.FeatureSpecV2{
+ {
+ Name: "feat1",
+ ValueType: types.ValueType_INT64,
+ },
+ {
+ Name: "feat2",
+ ValueType: types.ValueType_STRING,
+ },
+ },
+ Ttl: &durationpb.Duration{
+ Seconds: 3600, // 1 hour
+ Nanos: 0,
+ },
+ Entities: []string{"entity1"},
+ },
+ },
+ },
+ },
+ },
+ FeatureTransformation: &core.FeatureTransformationV2{
+ Transformation: &core.FeatureTransformationV2_UserDefinedFunction{
+ UserDefinedFunction: &core.UserDefinedFunctionV2{
+ Name: "Sample User Defined Function V2",
+ Body: []byte("function body"),
+ BodyText: "function body text",
+ },
+ },
+ },
+ },
+ }
+
+ entity1 := &core.Entity{
+ Spec: &core.EntitySpecV2{
+ Name: "entity1",
+ Project: "feature_repo",
+ ValueType: types.ValueType_INT32,
+ Description: "This is a sample entity",
+ JoinKey: "join_key_1",
+ Tags: map[string]string{
+ "tag1": "value1",
+ "tag2": "value2",
+ },
+ Owner: "sample_owner",
+ },
+ Meta: &core.EntityMeta{
+ CreatedTimestamp: timestamppb.Now(),
+ LastUpdatedTimestamp: timestamppb.Now(),
+ },
+ }
+
+ cachedODFVs := make(map[string]map[string]*core.OnDemandFeatureView)
+ cachedODFVs["feature_repo"] = make(map[string]*core.OnDemandFeatureView)
+ cachedODFVs["feature_repo"]["odfv1"] = odfv
+
+ cachedFVs := make(map[string]map[string]*core.FeatureView)
+ cachedFVs["feature_repo"] = make(map[string]*core.FeatureView)
+ cachedFVs["feature_repo"]["odfv1"] = fv
+
+ cachedEntities := make(map[string]map[string]*core.Entity)
+ cachedEntities["feature_repo"] = make(map[string]*core.Entity)
+ cachedEntities["feature_repo"]["entity1"] = entity1
+
+ s.fs.Registry().CachedOnDemandFeatureViews = cachedODFVs
+ s.fs.Registry().CachedFeatureViews = cachedFVs
+ s.fs.Registry().CachedEntities = cachedEntities
+
+ req, _ := http.NewRequest("POST", "/get-online-features", strings.NewReader(jsonRequest))
+ rr := httptest.NewRecorder()
+
+ s.getOnlineFeatures(rr, req)
+
+ bodyBytes, _ := io.ReadAll(rr.Body)
+ bodyString := string(bodyBytes)
+
+ assert.Equal(t, http.StatusNotFound, rr.Code)
+ assert.Equal(t, strings.Contains(bodyString, "Request Source rq_field_1 not found"), true)
+
}
diff --git a/go/internal/feast/test_utils.go b/go/internal/feast/test_utils.go
new file mode 100644
index 0000000000..8870db0b9f
--- /dev/null
+++ b/go/internal/feast/test_utils.go
@@ -0,0 +1,32 @@
+package feast
+
+import (
+ "github.com/feast-dev/feast/go/internal/feast/registry"
+ "path/filepath"
+ "runtime"
+)
+
+func GetRepoConfig() (config registry.RepoConfig) {
+ return registry.RepoConfig{
+ Project: "feature_repo",
+ Registry: getRegistryPath(),
+ Provider: "local",
+ OnlineStore: map[string]interface{}{
+ "type": "redis",
+ "connection_string": "localhost:6379",
+ },
+ }
+}
+
+// Return absolute path to the test_repo registry regardless of the working directory
+func getRegistryPath() map[string]interface{} {
+ // Get the file path of this source file, regardless of the working directory
+ _, filename, _, ok := runtime.Caller(0)
+ if !ok {
+ panic("couldn't find file path of the test file")
+ }
+ registry := map[string]interface{}{
+ "path": filepath.Join(filename, "..", "..", "..", "feature_repo/data/registry.db"),
+ }
+ return registry
+}