functions: hot containers (#332)

* functions: modify datastore to accomodate hot containers support

* functions: protocol between functions and hot containers

* functions: add hot containers clockwork

* fn: add hot containers support
This commit is contained in:
C Cirello
2016-11-28 18:45:35 +01:00
committed by Pedro Nasser
parent d0429c3dfd
commit ac0044f7d9
31 changed files with 809 additions and 170 deletions

View File

@@ -0,0 +1,19 @@
package protocol
import (
"context"
"github.com/iron-io/functions/api/runner/task"
)
// DefaultProtocol is the protocol used by cold-containers
type DefaultProtocol struct {
}
func (p *DefaultProtocol) IsStreamable() bool {
return false
}
func (p *DefaultProtocol) Dispatch(ctx context.Context, t task.Request) error {
return nil
}

View File

@@ -0,0 +1,52 @@
package protocol
import (
"context"
"errors"
"io"
"github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/api/runner/task"
)
var errInvalidProtocol = errors.New("Invalid Protocol")
// ContainerIO defines the interface used to talk to a hot container.
// Internally, a protocol must know when to alternate between stdin and stdout.
// It returns any protocol error, if present.
type ContainerIO interface {
IsStreamable() bool
Dispatch(ctx context.Context, t task.Request) error
}
// Protocol defines all protocols that operates a ContainerIO.
type Protocol string
// Hot container protocols
const (
Default Protocol = models.FormatDefault
HTTP Protocol = models.FormatHTTP
)
// New creates a valid protocol handler from a I/O pipe representing containers
// stdin/stdout.
func New(p Protocol, in io.Writer, out io.Reader) (ContainerIO, error) {
switch p {
case HTTP:
return &HTTPProtocol{in, out}, nil
case Default:
return &DefaultProtocol{}, nil
default:
return nil, errInvalidProtocol
}
}
// IsStreamable says whether the given protocol can be used for streaming into
// hot containers.
func IsStreamable(p string) (bool, error) {
proto, err := New(Protocol(p), nil, nil)
if err != nil {
return false, err
}
return proto.IsStreamable(), nil
}

View File

@@ -0,0 +1,73 @@
package protocol
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httputil"
"time"
"github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/api/runner/task"
)
// HTTPProtocol converts stdin/stdout streams into HTTP/1.1 compliant
// communication. It relies on Content-Length to know when to stop reading from
// containers stdout. It also mandates valid HTTP headers back and forth, thus
// returning errors in case of parsing problems.
type HTTPProtocol struct {
in io.Writer
out io.Reader
}
func (p *HTTPProtocol) IsStreamable() bool {
return true
}
func (p *HTTPProtocol) Dispatch(ctx context.Context, t task.Request) error {
var retErr error
done := make(chan struct{})
go func() {
var body bytes.Buffer
io.Copy(&body, t.Config.Stdin)
req, err := http.NewRequest("GET", "/", &body)
if err != nil {
retErr = err
return
}
for k, v := range t.Config.Env {
req.Header.Set(k, v)
}
req.Header.Set("Content-Length", fmt.Sprint(body.Len()))
req.Header.Set("Task-ID", t.Config.ID)
raw, err := httputil.DumpRequest(req, true)
if err != nil {
retErr = err
return
}
p.in.Write(raw)
res, err := http.ReadResponse(bufio.NewReader(p.out), req)
if err != nil {
retErr = err
return
}
io.Copy(t.Config.Stdout, res.Body)
done <- struct{}{}
}()
timeout := time.After(t.Config.Timeout)
select {
case <-ctx.Done():
return ctx.Err()
case <-timeout:
return models.ErrRunnerTimeout
case <-done:
return retErr
}
}