Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/go_integration_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: go-integration-test

on:
workflow_run:
workflows:
- unit-tests
types:
- completed
jobs:
integration-test-go-local:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
submodules: recursive
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Setup Python
uses: actions/setup-python@v5
id: setup-python
with:
python-version: "3.11"
architecture: x64
- name: Test local integration tests
run: make test-go-integration
6 changes: 3 additions & 3 deletions .github/workflows/go_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ jobs:
submodules: recursive
- name: Setup Go
id: setup-go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: 1.22.5
go-version: '1.23'
- name: Setup Python
uses: actions/setup-python@v5
id: setup-python
Expand Down Expand Up @@ -82,4 +82,4 @@ jobs:
with:
submodules: 'true'
- name: Build image
run: make build-${{ matrix.component }}-docker REGISTRY=${REGISTRY} VERSION=${GITHUB_SHA}
run: make build-${{ matrix.component }}-docker REGISTRY=${REGISTRY} VERSION=${GITHUB_SHA}
10 changes: 6 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,14 @@ build-go: compile-protos-go
go build -o feast ./go/main.go

test-go: compile-protos-go compile-protos-python install-feast-ci-locally
CGO_ENABLED=1 go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out -o coverage.html
CGO_ENABLED=1 go test -tags=unit -coverprofile=coverage.out ./... && go tool cover -html=coverage.out -o coverage.html

test-go-integration: compile-protos-go compile-protos-python install-feast-ci-locally
docker compose -f go/integration_tests/docker-compose.yaml up -d
go test -tags=integration ./...
docker compose -f go/integration_tests/docker-compose.yaml down
docker compose -f go/integration_tests/valkey/docker-compose.yaml up -d
docker compose -f go/integration_tests/scylladb/docker-compose.yaml up -d
go test -tags=integration ./go/internal/...
docker compose -f go/integration_tests/valkey/docker-compose.yaml down
docker compose -f go/integration_tests/scylladb/docker-compose.yaml down

format-go:
gofmt -s -w go/
Expand Down
2 changes: 1 addition & 1 deletion go/embedded/online_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (s *OnlineFeatureService) GetOnlineFeatures(

tsColumnBuilder := array.NewInt64Builder(pool)
for _, ts := range featureVector.Timestamps {
tsColumnBuilder.Append(ts.GetSeconds())
tsColumnBuilder.Append(types.GetTimestampMillis(ts))
}
tsColumn := tsColumnBuilder.NewArray()
outputColumns = append(outputColumns, tsColumn)
Expand Down
File renamed without changes.
18 changes: 18 additions & 0 deletions go/integration_tests/scylladb/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
scylla:
image: artifactory-edge.expedia.biz/all-docker-virtual/scylladb/scylla:latest
container_name: scylla
ports:
- "9042:9042"
- "10000:10000"
init-scylla:
image: artifactory-edge.expedia.biz/all-docker-virtual/scylladb/scylla:latest
depends_on:
- scylla
entrypoint: ["/bin/sh", "-c"]
command: >
"until cqlsh scylla -e 'DESC KEYSPACES'; do echo 'Waiting for Scylla...'; sleep 2; done &&
cqlsh scylla -e \"
CREATE KEYSPACE IF NOT EXISTS feast_keyspace
WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
\""
Binary file not shown.
95 changes: 95 additions & 0 deletions go/integration_tests/scylladb/feature_repo/example_repo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# This is an example feature definition file

from datetime import timedelta

from feast import Entity, FeatureView, Field, FileSource, Project, SortedFeatureView
from feast.sort_key import SortKey
from feast.protos.feast.core.SortedFeatureView_pb2 import SortOrder
from feast.types import (
Array,
Bool,
Bytes,
Float32,
Float64,
Int32,
Int64,
String,
UnixTimestamp,
)
from feast.value_type import ValueType

tags = {"team": "Feast"}

owner = "test@test.com"

# Define a project for the feature repo
project = Project(
name="feature_integration_repo", description="A project for integration tests"
)

index_entity: Entity = Entity(
name="index",
description="Index for the random data",
join_keys=["index_id"],
value_type=ValueType.INT64,
tags=tags,
owner=owner,
)

mlpfs_test_all_datatypes_source: FileSource = FileSource(
path="data.parquet", timestamp_field="event_timestamp"
)

mlpfs_test_all_datatypes_view: SortedFeatureView = SortedFeatureView(
name="all_dtypes_sorted",
entities=[index_entity],
ttl=timedelta(),
source=mlpfs_test_all_datatypes_source,
tags=tags,
description="Feature View with all supported feast datatypes",
owner=owner,
online=True,
sort_keys=[
SortKey(
name="event_timestamp",
value_type=ValueType.UNIX_TIMESTAMP,
default_sort_order=SortOrder.DESC,
)
],
schema=[
Field(name="index_id", dtype=Int64),
Field(name="int_val", dtype=Int32),
Field(name="long_val", dtype=Int64),
Field(name="float_val", dtype=Float32),
Field(name="double_val", dtype=Float64),
Field(name="byte_val", dtype=Bytes),
Field(name="string_val", dtype=String),
Field(name="timestamp_val", dtype=UnixTimestamp),
Field(name="boolean_val", dtype=Bool),
Field(name="array_int_val", dtype=Array(Int32)),
Field(name="array_long_val", dtype=Array(Int64)),
Field(name="array_float_val", dtype=Array(Float32)),
Field(name="array_double_val", dtype=Array(Float64)),
Field(name="array_byte_val", dtype=Array(Bytes)),
Field(name="array_string_val", dtype=Array(String)),
Field(name="array_timestamp_val", dtype=Array(UnixTimestamp)),
Field(name="array_boolean_val", dtype=Array(Bool)),
Field(name="null_int_val", dtype=Int32),
Field(name="null_long_val", dtype=Int64),
Field(name="null_float_val", dtype=Float32),
Field(name="null_double_val", dtype=Float64),
Field(name="null_byte_val", dtype=Bytes),
Field(name="null_string_val", dtype=String),
Field(name="null_timestamp_val", dtype=UnixTimestamp),
Field(name="null_boolean_val", dtype=Bool),
Field(name="null_array_int_val", dtype=Array(Int32)),
Field(name="null_array_long_val", dtype=Array(Int64)),
Field(name="null_array_float_val", dtype=Array(Float32)),
Field(name="null_array_double_val", dtype=Array(Float64)),
Field(name="null_array_byte_val", dtype=Array(Bytes)),
Field(name="null_array_string_val", dtype=Array(String)),
Field(name="null_array_timestamp_val", dtype=Array(UnixTimestamp)),
Field(name="null_array_boolean_val", dtype=Array(Bool)),
Field(name="event_timestamp", dtype=UnixTimestamp),
],
)
23 changes: 23 additions & 0 deletions go/integration_tests/scylladb/feature_repo/feature_store.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
project: feature_integration_repo
# By default, the registry is a file (but can be turned into a more scalable SQL-backed registry)
registry: data/registry.db
# The provider primarily specifies default offline / online stores & storing the registry in a given cloud
provider: local
online_store:
type: scylladb
hosts:
- localhost
load_balancing:
load_balancing_policy: "TokenAwarePolicy(DCAwareRoundRobinPolicy)"
local_dc: datacenter1
lazy_table_creation: True
table_name_format_version: 2
read_batch_size: 2
write_batch_size: 100
write_concurrency: 2
write_rate_limit: 10
protocol_version: 4
entity_key_serialization_version: 2
# By default, no_auth for authentication and authorization, other possible values kubernetes and oidc. Refer the documentation for more details.
auth:
type: no_auth
5 changes: 5 additions & 0 deletions go/integration_tests/valkey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Run Integration Tests:

```bash
make test-go-integration
```
Empty file.
2 changes: 2 additions & 0 deletions go/internal/feast/featurestore_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !integration

package feast

import (
Expand Down
3 changes: 2 additions & 1 deletion go/internal/feast/model/groupedrangefeaturerefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ type GroupedRangeFeatureRefs struct {
EntityKeys []*types.EntityKey
// Reversed mapping to project result of retrieval from storage to response
Indices [][]int

// Sort key filters to pass to OnlineReadRange
SortKeyFilters []*SortKeyFilter
// Limit to pass to OnlineReadRange
Limit int32
// Reverse sort order to pass to OnlineReadRange
IsReverseSortOrder bool
// Sort key names to pass to OnlineReadRange
SortKeyNames map[string]bool
}
9 changes: 6 additions & 3 deletions go/internal/feast/onlineserving/serving.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,17 +751,17 @@ func processFeatureRowData(
rangeValues[i] = protoVal
}

timestamp := getEventTimestamp(featureData.EventTimestamps, i)
eventTimestamp := getEventTimestamp(featureData.EventTimestamps, i)

status := serving.FieldStatus_PRESENT
if i < len(featureData.Statuses) {
status = featureData.Statuses[i]
} else if timestamp.GetSeconds() > 0 && checkOutsideTtl(timestamp, timestamppb.Now(), sfv.FeatureView.Ttl) {
} else if eventTimestamp.GetSeconds() > 0 && checkOutsideTtl(eventTimestamp, timestamppb.Now(), sfv.FeatureView.Ttl) {
status = serving.FieldStatus_OUTSIDE_MAX_AGE
}

rangeStatuses[i] = status
rangeTimestamps[i] = timestamp
rangeTimestamps[i] = eventTimestamp
}

return rangeValues, rangeStatuses, rangeTimestamps, nil
Expand Down Expand Up @@ -1074,7 +1074,9 @@ func GroupSortedFeatureRefs(
}

sortKeyFilterModels := make([]*model.SortKeyFilter, 0)
sortKeyNamesMap := make(map[string]bool)
for _, sortKey := range featuresAndView.View.SortKeys {
sortKeyNamesMap[sortKey.FieldName] = true
var sortOrder *core.SortOrder_Enum
if reverseSortOrder {
flipped := core.SortOrder_DESC
Expand Down Expand Up @@ -1113,6 +1115,7 @@ func GroupSortedFeatureRefs(
SortKeyFilters: sortKeyFilterModels,
Limit: limit,
IsReverseSortOrder: reverseSortOrder,
SortKeyNames: sortKeyNamesMap,
}

} else {
Expand Down
2 changes: 2 additions & 0 deletions go/internal/feast/onlineserving/serving_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !integration

package onlineserving

import (
Expand Down
56 changes: 45 additions & 11 deletions go/internal/feast/onlinestore/cassandraonlinestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ func (c *CassandraOnlineStore) buildRangeQueryCQL(
numKeys int,
sortKeyFilters []*model.SortKeyFilter,
limit int32,
reverseSortOrder bool,
isReverseSortOrder bool,
) (string, []interface{}) {
quotedFeatures := make([]string, len(featureNames))
for i, name := range featureNames {
Expand Down Expand Up @@ -625,8 +625,8 @@ func (c *CassandraOnlineStore) buildRangeQueryCQL(
whereClause = " AND " + strings.Join(rangeFilters, " AND ")
}

// Only add ORDER BY if single key and using reverse sort order
if len(orderBy) > 0 && reverseSortOrder {
// Only add ORDER BY if IsReverseSortOrder is true
if isReverseSortOrder && len(orderBy) > 0 {
orderByClause = " ORDER BY " + strings.Join(orderBy, ", ")
}
}
Expand Down Expand Up @@ -755,16 +755,50 @@ func (c *CassandraOnlineStore) OnlineReadRange(ctx context.Context, groupedRefs

for _, featName := range groupedRefs.FeatureNames {
idx := prepCtx.featureNamesToIdx[featName]
val, exists := readValues[featName]

var val interface{}
var status serving.FieldStatus
if !exists {
status = serving.FieldStatus_NOT_FOUND
val = nil
} else if val == nil {
status = serving.FieldStatus_NULL_VALUE

if _, isSortKey := groupedRefs.SortKeyNames[featName]; isSortKey {
var exists bool
val, exists = readValues[featName]
if !exists {
status = serving.FieldStatus_NOT_FOUND
val = nil
} else if val == nil {
status = serving.FieldStatus_NULL_VALUE
} else {
status = serving.FieldStatus_PRESENT
}
} else {
status = serving.FieldStatus_PRESENT
if valueStr, ok := readValues[featName]; ok {
var message types.Value
if err := proto.Unmarshal(valueStr.([]byte), &message); err != nil {
errorsChannel <- errors.New("error converting parsed Cassandra Value to types.Value")
return
}
if message.Val == nil {
val = nil
status = serving.FieldStatus_NULL_VALUE
} else {
switch message.Val.(type) {
case *types.Value_UnixTimestampVal:
// null timestamps are read as min int64, so we convert them to nil
if message.Val.(*types.Value_UnixTimestampVal).UnixTimestampVal == math.MinInt64 {
val = nil
status = serving.FieldStatus_NULL_VALUE
} else {
val = &types.Value{Val: message.Val}
status = serving.FieldStatus_PRESENT
}
default:
val = &types.Value{Val: message.Val}
status = serving.FieldStatus_PRESENT
}
}
} else {
val = nil
status = serving.FieldStatus_NOT_FOUND
}
}

appendRangeFeature(&rowData[idx], featName, prepCtx.featureViewName, val, status, eventTs)
Expand Down
Loading
Loading