Skip to content

Commit 82b798d

Browse files
authored
Merge pull request #3 from khorshuheng/configurable-load
Configurable load and Feast 0.8 compatibility
2 parents 9fb90d0 + e1a80cb commit 82b798d

File tree

8 files changed

+667
-57
lines changed

8 files changed

+667
-57
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ This simple Go service generates load as part of the Feast testing suite. It sit
77
```
88

99
### Usage
10+
Create a specification file for the load. Refer to the example specification for details.
11+
```
12+
LOAD_SPECIFICATION_PATH=example/loadSpec.yml
13+
```
14+
1015
Start the proxy
1116
```
1217
LOAD_FEAST_SERVING_HOST=feast.serving.example.com LOAD_FEAST_SERVING_PORT=6566 go run main.go
@@ -21,8 +26,8 @@ The following command simply echos the version of Feast Serving. Useful for test
2126
curl localhost:8080/echo
2227
```
2328

24-
This command will send a single GetOnlineFeatures request to the configured Feast serving instance. The `entity_count` parameter is used to set how many entities will be sent (unique users in this case). The higher the number of entities the higher the expected latency.
29+
This command will send a single or multiple GetOnlineFeatures request(s) depending on the load specification.
2530

2631
```
27-
curl localhost:8080/send?entity_count=30
32+
curl localhost:8080/send
2833
```

example/loadSpec.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Define the requested entities
2+
entities:
3+
# Entity name
4+
- name: "merchant_id"
5+
# Type of entity. Supported types: int64, int32, float, double, bool, string
6+
type: "int64"
7+
# Generate values from file
8+
fileSource:
9+
# Path to a file that contains the possible values of the entity. The number of
10+
# lines must be greater than the entity count for the request.
11+
path: "example/restaurant_id.txt"
12+
- name: "customer_id"
13+
type: "int64"
14+
# Applicable only for integer type entities, and if a source file is not defined.
15+
# The entity value will be generated randomly based on the min and max values (inclusive).
16+
randInt:
17+
min: 1
18+
max: 100
19+
20+
21+
# Each entry defines a request that would be send when the /send endpoint is called.
22+
# If they are multiple requests, each request will be sent within a goroutine, and the call
23+
# will return when all requests succesfully receive a response.
24+
requests:
25+
# Entity(s) that corresponds to the features to be retrieved
26+
- entities:
27+
- "customer_id"
28+
# Retrieved features
29+
features:
30+
- "merchant_orders:lifetime_avg_basket_size"
31+
- "merchant_orders:weekday_completed_order"
32+
- "merchant_orders:weekend_completed_order"
33+
# Number of entities in the request. The entity value are chosen randomly from the source
34+
# file defined above.
35+
entityCount: 5
36+
- entities:
37+
- "merchant_id"
38+
features:
39+
- "customer_orders:cust_total_orders_3months"
40+
- "customer_orders:cust_orders_uniq_restaurants_3months"
41+
entityCount: 5
42+
43+
- entities:
44+
- "customer_id"
45+
- "merchant_id"
46+
features:
47+
- "merchant_customer_orders:days_since_last_order"
48+
- "merchant_customer_orders:int_order_count_90days"
49+
entityCount: 5

example/restaurant_id.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
1
2+
2
3+
3
4+
4
5+
5
6+
6
7+
7
8+
8
9+
9
10+
10

generator/generator.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package generator
2+
3+
import (
4+
"bufio"
5+
"errors"
6+
"fmt"
7+
feast "github.com/feast-dev/feast/sdk/go"
8+
"github.com/feast-dev/feast/sdk/go/protos/feast/types"
9+
"math/rand"
10+
"os"
11+
"strconv"
12+
"time"
13+
)
14+
15+
type LoadSpec struct {
16+
EntitySpec []EntitySpec `yaml:"entities"`
17+
RequestSpecs []RequestSpec `yaml:"requests"`
18+
}
19+
20+
type EntitySpec struct {
21+
Name string `yaml:"name"`
22+
Type string `yaml:"type"`
23+
FileSource FileSource `yaml:"fileSource"`
24+
RandInt RandInt `yaml:"randInt"`
25+
}
26+
27+
type FileSource struct {
28+
Path string `yaml:"path"`
29+
}
30+
31+
32+
type RandInt struct {
33+
Min int64 `yaml:"min"`
34+
Max int64 `yaml:"max"`
35+
}
36+
37+
// Generate all possible values for an entity
38+
type EntityPoolGenerator interface {
39+
GenerateEntityValues() ([]*types.Value, error)
40+
}
41+
42+
// Generate all possible values for an entity from a file source
43+
type FileSourceEntityValueGenerator struct {
44+
entity EntitySpec
45+
}
46+
47+
func (generator FileSourceEntityValueGenerator) GenerateEntityValues() ([]*types.Value, error) {
48+
var entityValues []*types.Value
49+
file, err := os.Open(generator.entity.FileSource.Path)
50+
if err != nil {
51+
return nil, err
52+
}
53+
scanner := bufio.NewScanner(file)
54+
scanner.Split(bufio.ScanLines)
55+
for scanner.Scan() {
56+
parsedValue, err := generator.parseStrToEntityValue(generator.entity.Type, scanner.Text())
57+
if err != nil {
58+
return nil, err
59+
}
60+
entityValues = append(entityValues, parsedValue)
61+
}
62+
return entityValues, nil
63+
}
64+
65+
func (generator FileSourceEntityValueGenerator) parseStrToEntityValue(valueType string, valueStr string) (*types.Value, error) {
66+
switch valueType {
67+
case "string":
68+
return feast.StrVal(valueStr), nil
69+
case "int32":
70+
parsedValue, err := strconv.ParseInt(valueStr, 10, 32)
71+
if err != nil {
72+
return nil, err
73+
}
74+
return feast.Int32Val(int32(parsedValue)), nil
75+
case "int64":
76+
parsedValue, err := strconv.ParseInt(valueStr, 10, 64)
77+
if err != nil {
78+
return nil, err
79+
}
80+
return feast.Int64Val(parsedValue), nil
81+
case "float":
82+
parsedValue, err := strconv.ParseFloat(valueStr, 32)
83+
if err != nil {
84+
return nil, err
85+
}
86+
return feast.FloatVal(float32(parsedValue)), nil
87+
case "double":
88+
parsedValue, err := strconv.ParseFloat(valueStr, 64)
89+
if err != nil {
90+
return nil, err
91+
}
92+
return feast.DoubleVal(parsedValue), nil
93+
case "bool":
94+
parsedValue, err := strconv.ParseBool(valueStr)
95+
if err != nil {
96+
return nil, err
97+
}
98+
return feast.BoolVal(parsedValue), nil
99+
}
100+
101+
return nil, errors.New(fmt.Sprintf("Unrecognized value type: %s", valueType))
102+
}
103+
104+
// Generate all possible values for an entity based on a range of integer
105+
type RandIntEntityValueGenerator struct {
106+
entity EntitySpec
107+
}
108+
109+
func (generator RandIntEntityValueGenerator) GenerateEntityValues() ([]*types.Value, error) {
110+
entityType := generator.entity.Type
111+
switch entityType {
112+
case "int64":
113+
minValue := generator.entity.RandInt.Min
114+
maxValue := generator.entity.RandInt.Max
115+
poolSize := maxValue - minValue + 1
116+
entityValues := make([]*types.Value, poolSize)
117+
for i := int64(0); i < poolSize; i++ {
118+
entityValues[i] = feast.Int64Val(i + minValue)
119+
}
120+
return entityValues, nil
121+
case "int32":
122+
minValue := int32(generator.entity.RandInt.Min)
123+
maxValue := int32(generator.entity.RandInt.Max)
124+
poolSize := maxValue - minValue + 1
125+
entityValues := make([]*types.Value, poolSize)
126+
for i := int32(0); i < poolSize; i++ {
127+
entityValues[i] = feast.Int32Val(i + minValue)
128+
}
129+
return entityValues, nil
130+
default:
131+
return nil, errors.New(fmt.Sprintf("Unsupported entity type: %s", entityType))
132+
}
133+
}
134+
135+
type RequestSpec struct {
136+
Entities []string `yaml:"entities"`
137+
Features []string `yaml:"features"`
138+
EntityCount int32 `yaml:"entityCount"`
139+
}
140+
141+
type RequestGenerator struct {
142+
entityToValuePoolMap map[string][]*types.Value
143+
requests []RequestSpec
144+
project string
145+
}
146+
147+
func NewRequestGenerator(loadSpec LoadSpec, project string) (RequestGenerator, error) {
148+
entityToValuePoolMap := map[string][]*types.Value{}
149+
for _, entity := range loadSpec.EntitySpec {
150+
var poolGenerator EntityPoolGenerator
151+
if entity.FileSource != (FileSource{}) {
152+
poolGenerator = FileSourceEntityValueGenerator{entity}
153+
} else {
154+
poolGenerator = RandIntEntityValueGenerator{entity}
155+
}
156+
pool, err := poolGenerator.GenerateEntityValues()
157+
if err != nil {
158+
return RequestGenerator{}, err
159+
}
160+
entityToValuePoolMap[entity.Name] = pool
161+
}
162+
return RequestGenerator{
163+
entityToValuePoolMap: entityToValuePoolMap,
164+
requests: loadSpec.RequestSpecs,
165+
project: project,
166+
}, nil
167+
}
168+
169+
170+
func (generator *RequestGenerator) GenerateRandomRows(entities []string, entityCount int32) []feast.Row {
171+
rows := make([]feast.Row, entityCount)
172+
for _, entity := range entities {
173+
valuePool := generator.entityToValuePoolMap[entity]
174+
rand.Seed(time.Now().UnixNano())
175+
rand.Shuffle(len(valuePool), func(i, j int) { valuePool[i], valuePool[j] = valuePool[j], valuePool[i] })
176+
}
177+
178+
for i := int32(0); i < entityCount; i++ {
179+
row := feast.Row{}
180+
for _, entity := range entities {
181+
valuePool := generator.entityToValuePoolMap[entity]
182+
row[entity] = valuePool[i]
183+
}
184+
rows[i]=row
185+
}
186+
187+
return rows
188+
}
189+
190+
func (generator *RequestGenerator) GenerateRequests() []feast.OnlineFeaturesRequest {
191+
var onlineFeatureRequests []feast.OnlineFeaturesRequest
192+
for _, request := range generator.requests {
193+
entityRows := generator.GenerateRandomRows(request.Entities, request.EntityCount)
194+
onlineFeatureRequests = append(onlineFeatureRequests, feast.OnlineFeaturesRequest{
195+
Features: request.Features,
196+
Entities: entityRows,
197+
Project: generator.project,
198+
})
199+
}
200+
return onlineFeatureRequests
201+
}

generator/generator_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package generator
2+
3+
import (
4+
"gopkg.in/yaml.v2"
5+
"io/ioutil"
6+
"testing"
7+
)
8+
9+
func TestGenerateRequests(t *testing.T) {
10+
yamlSpec, err := ioutil.ReadFile("../example/loadSpec.yml")
11+
if err != nil {
12+
t.Errorf(err.Error())
13+
t.FailNow()
14+
}
15+
loadSpec := LoadSpec{}
16+
err = yaml.Unmarshal(yamlSpec, &loadSpec)
17+
if err != nil {
18+
t.Errorf(err.Error())
19+
t.FailNow()
20+
}
21+
loadSpec.EntitySpec[0].FileSource.Path = "../example/restaurant_id.txt"
22+
requestGenerator, err := NewRequestGenerator(loadSpec, "default")
23+
if err != nil {
24+
t.Errorf(err.Error())
25+
t.FailNow()
26+
}
27+
requests := requestGenerator.GenerateRequests()
28+
if len(requests) != 3 {
29+
t.Errorf("Request length not equals to 3")
30+
t.FailNow()
31+
}
32+
33+
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
module feast-load-generator
22

3-
go 1.14
3+
go 1.15
44

55
require (
6-
github.com/feast-dev/feast/sdk/go v0.0.0-20200724013123-d07875d4efa1
6+
github.com/feast-dev/feast/sdk/go v0.8.2
77
github.com/kelseyhightower/envconfig v1.4.0
8+
gopkg.in/yaml.v2 v2.2.2
89
)

0 commit comments

Comments
 (0)