mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* fn: reduce lbagent and agent dependency lbagent and agent code is too dependent. This causes any changed in agent to break lbagent. In reality, for LB there should be no delegated agent. Splitting these two will cause some code duplication, but it reduces dependency and complexity (eg. agent without docker) * fn: post rebase fixup * fn: runner/runnercall should use lbDeadline * fn: fixup ln agent test * fn: remove agent create option for common.WaitGroup
161 lines
4.7 KiB
Go
161 lines
4.7 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// handleFunctionCall executes the function, for router handlers
|
|
func (s *Server) handleFunctionCall(c *gin.Context) {
|
|
err := s.handleFunctionCall2(c)
|
|
if err != nil {
|
|
handleErrorResponse(c, err)
|
|
}
|
|
}
|
|
|
|
// handleFunctionCall2 executes the function and returns an error
|
|
// Requires the following in the context:
|
|
// * "app_name"
|
|
// * "path"
|
|
func (s *Server) handleFunctionCall2(c *gin.Context) error {
|
|
ctx := c.Request.Context()
|
|
var p string
|
|
r := ctx.Value(api.Path)
|
|
if r == nil {
|
|
p = "/"
|
|
} else {
|
|
p = r.(string)
|
|
}
|
|
|
|
appID := c.MustGet(api.AppID).(string)
|
|
app, err := s.agent.GetAppByID(ctx, appID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// gin sets this to 404 on NoRoute, so we'll just ensure it's 200 by default.
|
|
c.Status(200) // this doesn't write the header yet
|
|
|
|
return s.serve(c, app, path.Clean(p))
|
|
}
|
|
|
|
var (
|
|
bufPool = &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
|
|
)
|
|
|
|
// TODO it would be nice if we could make this have nothing to do with the gin.Context but meh
|
|
// TODO make async store an *http.Request? would be sexy until we have different api format...
|
|
func (s *Server) serve(c *gin.Context, app *models.App, path string) 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?
|
|
|
|
// GetCall can mod headers, assign an id, look up the route/app (cached),
|
|
// strip params, etc.
|
|
// this should happen ASAP to turn app name to app ID
|
|
|
|
// GetCall can mod headers, assign an id, look up the route/app (cached),
|
|
// strip params, etc.
|
|
|
|
call, err := s.agent.GetCall(
|
|
agent.WithWriter(&writer), // XXX (reed): order matters [for now]
|
|
agent.FromRequest(s.agent, app, path, c.Request),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
model := call.Model()
|
|
{ // scope this, to disallow ctx use outside of this scope. add id for handleErrorResponse logger
|
|
ctx, _ := common.LoggerWithFields(c.Request.Context(), logrus.Fields{"id": model.ID})
|
|
c.Request = c.Request.WithContext(ctx)
|
|
}
|
|
|
|
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()
|
|
|
|
// TODO idk where to put this, but agent is all runner really has...
|
|
err = s.agent.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
|
|
}
|
|
|
|
var _ http.ResponseWriter = new(syncResponseWriter)
|
|
|
|
// implements http.ResponseWriter
|
|
// this little guy buffers responses from user containers and lets them still
|
|
// set headers and such without us risking writing partial output [as much, the
|
|
// server could still die while we're copying the buffer]. this lets us set
|
|
// content length and content type nicely, as a bonus. it is sad, yes.
|
|
type syncResponseWriter struct {
|
|
headers http.Header
|
|
status int
|
|
*bytes.Buffer
|
|
}
|
|
|
|
func (s *syncResponseWriter) Header() http.Header { return s.headers }
|
|
func (s *syncResponseWriter) WriteHeader(code int) { s.status = code }
|