Initial Refactor (#1234)

* Inital Refactor

Removing the repeated logic exposed some problems with the reponse
writers.

Currently, the trigger writer was overlaid on part of the header
writing. The main invoke blog writing into the different levels of the
overlays at different points in the logic.

Instead, by extending the types and embedded structs, the writer is
more transparent. So, at the end of the flow it goes over all the
headers available and removes our prefixes. This lets the invoke logic
just write to the top level.

Going to continue after lunch to try and remove some of the layers and
param passing.

* Try and repeat concurrency failure

* Nested FromHTTPFnRequest inside FromHTTPTriggerRequest

* Consolidate buffer pooling logic

* go fmt yourself

* fix import
This commit is contained in:
Tom Coupland
2018-09-24 12:20:30 +01:00
committed by Richard Connon
parent 430314710d
commit d454ff9aa4
4 changed files with 78 additions and 181 deletions

View File

@@ -1,20 +1,15 @@
package server
import (
"bytes"
"io"
"net/http"
"strconv"
"time"
"strings"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/agent"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// handleHTTPTriggerCall executes the function, for router handlers
@@ -65,12 +60,11 @@ func (s *Server) handleTriggerHTTPFunctionCall2(c *gin.Context) error {
}
type triggerResponseWriter struct {
w http.ResponseWriter
headers http.Header
syncResponseWriter
committed bool
}
var _ http.ResponseWriter = new(triggerResponseWriter)
var _ ResponseBufferingWriter = new(triggerResponseWriter)
func (trw *triggerResponseWriter) Header() http.Header {
return trw.headers
@@ -80,7 +74,7 @@ func (trw *triggerResponseWriter) Write(b []byte) (int, error) {
if !trw.committed {
trw.WriteHeader(http.StatusOK)
}
return trw.w.Write(b)
return trw.GetBuffer().Write(b)
}
func (trw *triggerResponseWriter) WriteHeader(statusCode int) {
@@ -88,13 +82,27 @@ func (trw *triggerResponseWriter) WriteHeader(statusCode int) {
return
}
trw.committed = true
for k, vs := range trw.Header() {
if strings.HasPrefix(k, "Fn-Http-H-") {
// TODO strip out content-length and stuff here.
realHeader := strings.TrimPrefix(k, "Fn-Http-H-")
if realHeader != "" { // case where header is exactly the prefix
for _, v := range vs {
trw.Header().Del(k)
trw.Header().Add(realHeader, v)
}
}
}
}
gatewayStatus := 200
if statusCode >= 400 {
gatewayStatus = 502
}
status := trw.headers.Get("Fn-Http-Status")
status := trw.Header().Get("Fn-Http-Status")
if status != "" {
statusInt, err := strconv.Atoi(status)
if err == nil {
@@ -102,39 +110,16 @@ func (trw *triggerResponseWriter) WriteHeader(statusCode int) {
}
}
for k, vs := range trw.headers {
if strings.HasPrefix(k, "Fn-Http-H-") {
// TODO strip out content-length and stuff here.
realHeader := strings.TrimPrefix(k, "Fn-Http-H-")
if realHeader != "" { // case where header is exactly the prefix
for _, v := range vs {
trw.w.Header().Add(realHeader, v)
}
}
}
}
contentType := trw.headers.Get("Content-Type")
if contentType != "" {
trw.w.Header().Add("Content-Type", contentType)
}
trw.w.WriteHeader(gatewayStatus)
trw.WriteHeader(gatewayStatus)
}
//ServeHTTPTr igger serves an HTTP trigger for a given app/fn/trigger based on the current request
// This is exported to allow extensions to handle their own trigger naming and publishing
func (s *Server) ServeHTTPTrigger(c *gin.Context, app *models.App, fn *models.Fn, trigger *models.Trigger) error {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
writer := &syncResponseWriter{
Buffer: buf,
headers: c.Writer.Header(), // copy ref
}
defer bufPool.Put(buf) // TODO need to ensure this is safe with Dispatch?
triggerWriter := &triggerResponseWriter{
w: writer,
headers: make(http.Header),
syncResponseWriter{
headers: c.Writer.Header()},
false,
}
// GetCall can mod headers, assign an id, look up the route/app (cached),
// strip params, etc.
@@ -142,75 +127,12 @@ func (s *Server) ServeHTTPTrigger(c *gin.Context, app *models.App, fn *models.Fn
// GetCall can mod headers, assign an id, look up the route/app (cached),
// strip params, etc.
call, err := s.agent.GetCall(
agent.WithWriter(triggerWriter), // XXX (reed): order matters [for now]
agent.FromHTTPTriggerRequest(app, fn, trigger, c.Request),
)
call, err := s.agent.GetCall(agent.WithWriter(triggerWriter), // XXX (reed): order matters [for now]
agent.FromHTTPTriggerRequest(app, fn, trigger, c.Request))
if err != nil {
return err
}
model := call.Model()
{ // scope this, to disallow ctx use outside of this scope. add id for handleV1ErrorResponse logger
ctx, _ := common.LoggerWithFields(c.Request.Context(), logrus.Fields{"id": model.ID})
c.Request = c.Request.WithContext(ctx)
}
writer.Header().Add("Fn_call_id", model.ID)
// TODO TRIGGERWIP not clear this makes sense here - but it works so...
if model.Type == "async" {
// TODO we should push this into GetCall somehow (CallOpt maybe) or maybe agent.Queue(Call) ?
if c.Request.ContentLength > 0 {
buf.Grow(int(c.Request.ContentLength))
}
_, err := buf.ReadFrom(c.Request.Body)
if err != nil {
return models.ErrInvalidPayload
}
model.Payload = buf.String()
err = s.lbEnqueue.Enqueue(c.Request.Context(), model)
if err != nil {
return err
}
c.JSON(http.StatusAccepted, map[string]string{"call_id": model.ID})
return nil
}
err = s.agent.Submit(call)
if err != nil {
// NOTE if they cancel the request then it will stop the call (kind of cool),
// we could filter that error out here too as right now it yells a little
if err == models.ErrCallTimeoutServerBusy || err == models.ErrCallTimeout {
// TODO maneuver
// add this, since it means that start may not have been called [and it's relevant]
c.Writer.Header().Add("XXX-FXLB-WAIT", time.Now().Sub(time.Time(model.CreatedAt)).String())
}
return err
}
// if they don't set a content-type - detect it
if writer.Header().Get("Content-Type") == "" {
// see http.DetectContentType, the go server is supposed to do this for us but doesn't appear to?
var contentType string
jsonPrefix := [1]byte{'{'} // stack allocated
if bytes.HasPrefix(buf.Bytes(), jsonPrefix[:]) {
// try to detect json, since DetectContentType isn't a hipster.
contentType = "application/json; charset=utf-8"
} else {
contentType = http.DetectContentType(buf.Bytes())
}
writer.Header().Set("Content-Type", contentType)
}
writer.Header().Set("Content-Length", strconv.Itoa(int(buf.Len())))
if writer.status > 0 {
c.Writer.WriteHeader(writer.status)
}
io.Copy(c.Writer, writer)
return nil
return s.fnInvoke(c, app, fn, triggerWriter, call)
}