mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* fn: lb and pure-runner with non-blocking agent *) Removed pure-runner capacity tracking code. This did not play well with internal agent resource tracker. *) In LB and runner gRPC comm, removed ACK. Now, upon TryCall, pure-runner quickly proceeds to call Submit. This is good since at this stage pure-runner already has all relevant data to initiate the call. *) Unless pure-runner emits a NACK, LB immediately streams http body to runners. *) For retriable requests added a CachedReader for http.Request Body. *) Idempotenty/retry is similar to previous code. After initial success in Engament, after attempting a TryCall, unless we receive NACK, we cannot retry that call. *) ch and naive places now wraps each TryExec with a cancellable context to clean up gRPC contexts quicker. * fn: err for simpler one-time read GetBody approach This allows for a more flexible approach since we let users to define GetBody() to allow repetitive http body read. In default LB case, LB executes a one-time io.ReadAll and sets of GetBody, which is detected by RunnerCall.RequestBody(). * fn: additional check for non-nil req.body * fn: attempt to override IO errors with ctx for TryExec * fn: system-tests log dest * fn: LB: EOF send handling * fn: logging for partial IO * fn: use buffer pool for IO storage in lb agent * fn: pure runner should use chunks for data msgs * fn: required config validations and pass APIErrors * fn: additional tests and gRPC proto simplification *) remove ACK/NACK messages as Finish message type works OK for this purpose. *) return resp in api tests for check for status code *) empty body json test in api tests for lb & pure-runner * fn: buffer adjustments *) setRequestBody result handling correction *) switch to bytes.Reader for read-only safety *) io.EOF can be returned for non-nil Body in request. * fn: clarify detection of 503 / Server Too Busy
344 lines
8.1 KiB
Go
344 lines
8.1 KiB
Go
package tests
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
|
|
apimodels "github.com/fnproject/fn/api/models"
|
|
apiutils "github.com/fnproject/fn/test/fn-api-tests"
|
|
sdkmodels "github.com/fnproject/fn_go/models"
|
|
)
|
|
|
|
func LB() (string, error) {
|
|
lbURL := "http://127.0.0.1:8081"
|
|
|
|
u, err := url.Parse(lbURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return u.Host, nil
|
|
}
|
|
|
|
func getEchoContent(respBytes []byte) (string, error) {
|
|
|
|
var respJs map[string]interface{}
|
|
|
|
err := json.Unmarshal(respBytes, &respJs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
req, ok := respJs["request"].(map[string]interface{})
|
|
if !ok {
|
|
return "", errors.New("unexpected json: request map")
|
|
}
|
|
|
|
echo, ok := req["echoContent"].(string)
|
|
if !ok {
|
|
return "", errors.New("unexpected json: echoContent string")
|
|
}
|
|
|
|
return echo, nil
|
|
}
|
|
|
|
func TestCanExecuteFunction(t *testing.T) {
|
|
s := apiutils.SetupHarness()
|
|
s.GivenAppExists(t, &sdkmodels.App{Name: s.AppName})
|
|
defer s.Cleanup()
|
|
|
|
rt := s.BasicRoute()
|
|
rt.Image = "fnproject/fn-test-utils"
|
|
rt.Format = "json"
|
|
rt.Memory = 64
|
|
rt.Type = "sync"
|
|
|
|
s.GivenRouteExists(t, s.AppName, rt)
|
|
|
|
lb, err := LB()
|
|
if err != nil {
|
|
t.Fatalf("Got unexpected error: %v", err)
|
|
}
|
|
u := url.URL{
|
|
Scheme: "http",
|
|
Host: lb,
|
|
}
|
|
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
|
|
|
|
body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}`
|
|
content := bytes.NewBuffer([]byte(body))
|
|
output := &bytes.Buffer{}
|
|
|
|
resp, err := apiutils.CallFN(u.String(), content, output, "POST", []string{})
|
|
if err != nil {
|
|
t.Errorf("Got unexpected error: %v", err)
|
|
}
|
|
|
|
echo, err := getEchoContent(output.Bytes())
|
|
if err != nil || echo != "HelloWorld" {
|
|
t.Fatalf("getEchoContent/HelloWorld check failed on %v", output)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("StatusCode check failed on %v", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCanExecuteBigOutput(t *testing.T) {
|
|
s := apiutils.SetupHarness()
|
|
s.GivenAppExists(t, &sdkmodels.App{Name: s.AppName})
|
|
defer s.Cleanup()
|
|
|
|
rt := s.BasicRoute()
|
|
rt.Image = "fnproject/fn-test-utils"
|
|
rt.Format = "json"
|
|
rt.Memory = 64
|
|
rt.Type = "sync"
|
|
|
|
s.GivenRouteExists(t, s.AppName, rt)
|
|
|
|
lb, err := LB()
|
|
if err != nil {
|
|
t.Fatalf("Got unexpected error: %v", err)
|
|
}
|
|
u := url.URL{
|
|
Scheme: "http",
|
|
Host: lb,
|
|
}
|
|
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
|
|
|
|
// Approx 5.3MB output
|
|
body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true, "trailerRepeat": 410000}`
|
|
content := bytes.NewBuffer([]byte(body))
|
|
output := &bytes.Buffer{}
|
|
|
|
resp, err := apiutils.CallFN(u.String(), content, output, "POST", []string{})
|
|
if err != nil {
|
|
t.Errorf("Got unexpected error: %v", err)
|
|
}
|
|
|
|
t.Logf("getEchoContent/HelloWorld size %d", len(output.Bytes()))
|
|
|
|
echo, err := getEchoContent(output.Bytes())
|
|
if err != nil || echo != "HelloWorld" {
|
|
t.Fatalf("getEchoContent/HelloWorld check failed on %v", output)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("StatusCode check failed on %v", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCanExecuteTooBigOutput(t *testing.T) {
|
|
s := apiutils.SetupHarness()
|
|
s.GivenAppExists(t, &sdkmodels.App{Name: s.AppName})
|
|
defer s.Cleanup()
|
|
|
|
rt := s.BasicRoute()
|
|
rt.Image = "fnproject/fn-test-utils"
|
|
rt.Format = "json"
|
|
rt.Memory = 64
|
|
rt.Type = "sync"
|
|
|
|
s.GivenRouteExists(t, s.AppName, rt)
|
|
|
|
lb, err := LB()
|
|
if err != nil {
|
|
t.Fatalf("Got unexpected error: %v", err)
|
|
}
|
|
u := url.URL{
|
|
Scheme: "http",
|
|
Host: lb,
|
|
}
|
|
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
|
|
|
|
// > 6MB output
|
|
body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true, "trailerRepeat": 600000}`
|
|
content := bytes.NewBuffer([]byte(body))
|
|
output := &bytes.Buffer{}
|
|
|
|
resp, err := apiutils.CallFN(u.String(), content, output, "POST", []string{})
|
|
if err != nil {
|
|
t.Errorf("Got unexpected error: %v", err)
|
|
}
|
|
|
|
exp := "{\"error\":{\"message\":\"function response too large\"}}\n"
|
|
actual := output.String()
|
|
|
|
if !strings.Contains(exp, actual) || len(exp) != len(actual) {
|
|
t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", exp, output.String())
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusBadGateway {
|
|
t.Fatalf("StatusCode check failed on %v", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestCanExecuteEmptyOutput(t *testing.T) {
|
|
s := apiutils.SetupHarness()
|
|
s.GivenAppExists(t, &sdkmodels.App{Name: s.AppName})
|
|
defer s.Cleanup()
|
|
|
|
rt := s.BasicRoute()
|
|
rt.Image = "fnproject/fn-test-utils"
|
|
rt.Format = "json"
|
|
rt.Memory = 64
|
|
rt.Type = "sync"
|
|
|
|
s.GivenRouteExists(t, s.AppName, rt)
|
|
|
|
lb, err := LB()
|
|
if err != nil {
|
|
t.Fatalf("Got unexpected error: %v", err)
|
|
}
|
|
u := url.URL{
|
|
Scheme: "http",
|
|
Host: lb,
|
|
}
|
|
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
|
|
|
|
// empty body output
|
|
body := `{"sleepTime": 0, "isDebug": true, "isEmptyBody": true}`
|
|
content := bytes.NewBuffer([]byte(body))
|
|
output := &bytes.Buffer{}
|
|
|
|
resp, err := apiutils.CallFN(u.String(), content, output, "POST", []string{})
|
|
if err != nil {
|
|
t.Errorf("Got unexpected error: %v", err)
|
|
}
|
|
|
|
actual := output.String()
|
|
|
|
if 0 != len(actual) {
|
|
t.Errorf("Assertion error.\n\tExpected empty\n\tActual: %v", output.String())
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("StatusCode check failed on %v", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestBasicConcurrentExecution(t *testing.T) {
|
|
|
|
s := apiutils.SetupHarness()
|
|
|
|
s.GivenAppExists(t, &sdkmodels.App{Name: s.AppName})
|
|
defer s.Cleanup()
|
|
|
|
rt := s.BasicRoute()
|
|
rt.Image = "fnproject/fn-test-utils"
|
|
rt.Format = "json"
|
|
rt.Memory = 32
|
|
rt.Type = "sync"
|
|
|
|
s.GivenRouteExists(t, s.AppName, rt)
|
|
|
|
lb, err := LB()
|
|
if err != nil {
|
|
t.Fatalf("Got unexpected error: %v", err)
|
|
}
|
|
u := url.URL{
|
|
Scheme: "http",
|
|
Host: lb,
|
|
}
|
|
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
|
|
|
|
results := make(chan error)
|
|
concurrentFuncs := 10
|
|
for i := 0; i < concurrentFuncs; i++ {
|
|
go func() {
|
|
body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}`
|
|
content := bytes.NewBuffer([]byte(body))
|
|
output := &bytes.Buffer{}
|
|
resp, err := apiutils.CallFN(u.String(), content, output, "POST", []string{})
|
|
if err != nil {
|
|
results <- fmt.Errorf("Got unexpected error: %v", err)
|
|
return
|
|
}
|
|
|
|
echo, err := getEchoContent(output.Bytes())
|
|
if err != nil || echo != "HelloWorld" {
|
|
results <- fmt.Errorf("Assertion error.\n\tActual: %v", output.String())
|
|
return
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
results <- fmt.Errorf("StatusCode check failed on %v", resp.StatusCode)
|
|
return
|
|
}
|
|
|
|
results <- nil
|
|
}()
|
|
}
|
|
for i := 0; i < concurrentFuncs; i++ {
|
|
err := <-results
|
|
if err != nil {
|
|
t.Errorf("Error in basic concurrency execution test: %v", err)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestSaturatedSystem(t *testing.T) {
|
|
|
|
s := apiutils.SetupHarness()
|
|
|
|
s.GivenAppExists(t, &sdkmodels.App{Name: s.AppName})
|
|
defer s.Cleanup()
|
|
|
|
timeout := int32(5)
|
|
|
|
rt := s.BasicRoute()
|
|
rt.Image = "fnproject/fn-test-utils"
|
|
rt.Format = "json"
|
|
rt.Timeout = &timeout
|
|
rt.Memory = 300
|
|
rt.Type = "sync"
|
|
|
|
s.GivenRouteExists(t, s.AppName, rt)
|
|
|
|
lb, err := LB()
|
|
if err != nil {
|
|
t.Fatalf("Got unexpected error: %v", err)
|
|
}
|
|
u := url.URL{
|
|
Scheme: "http",
|
|
Host: lb,
|
|
}
|
|
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
|
|
|
|
body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}`
|
|
content := bytes.NewBuffer([]byte(body))
|
|
output := &bytes.Buffer{}
|
|
|
|
resp, err := apiutils.CallFN(u.String(), content, output, "POST", []string{})
|
|
if err != nil {
|
|
if err != apimodels.ErrCallTimeoutServerBusy {
|
|
t.Errorf("Got unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// LB may respond either with:
|
|
// timeout: a timeout during a call to a runner
|
|
// too busy: a timeout during LB retry loop
|
|
exp1 := "{\"error\":{\"message\":\"Timed out - server too busy\"}}\n"
|
|
exp2 := "{\"error\":{\"message\":\"Timed out\"}}\n"
|
|
|
|
actual := output.String()
|
|
|
|
if strings.Contains(exp1, actual) && len(exp1) == len(actual) {
|
|
} else if strings.Contains(exp2, actual) && len(exp2) == len(actual) {
|
|
} else {
|
|
t.Errorf("Assertion error.\n\tExpected: %v or %v\n\tActual: %v", exp1, exp2, output.String())
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusServiceUnavailable && resp.StatusCode != http.StatusGatewayTimeout {
|
|
t.Fatalf("StatusCode check failed on %v", resp.StatusCode)
|
|
}
|
|
}
|