mirror of
https://github.com/moby/buildkit.git
synced 2026-08-08 16:50:44 +00:00
Bind session RPC contexts to caller lifetime so ongoing RPCs fail when the session is canceled. Add an integration test that blocks the session tunnel and verifies the health monitor releases the hung build after timeout. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
216 lines
4.5 KiB
Go
216 lines
4.5 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// Caller can invoke requests on the session
|
|
type Caller interface {
|
|
Context(context.Context) context.Context
|
|
Supports(method string) bool
|
|
Conn() *grpc.ClientConn
|
|
SharedKey() string
|
|
}
|
|
|
|
type client struct {
|
|
Session
|
|
cc *grpc.ClientConn
|
|
supported map[string]struct{}
|
|
}
|
|
|
|
// Manager is a controller for accessing currently active sessions
|
|
type Manager struct {
|
|
sessions map[string]*client
|
|
mu sync.Mutex
|
|
updateCondition *sync.Cond
|
|
}
|
|
|
|
// NewManager returns a new Manager
|
|
func NewManager() (*Manager, error) {
|
|
sm := &Manager{
|
|
sessions: make(map[string]*client),
|
|
}
|
|
sm.updateCondition = sync.NewCond(&sm.mu)
|
|
return sm, nil
|
|
}
|
|
|
|
// HandleHTTPRequest handles an incoming HTTP request
|
|
func (sm *Manager) HandleHTTPRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
|
|
hijacker, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
return errors.New("handler does not support hijack")
|
|
}
|
|
|
|
id := r.Header.Get(headerSessionID)
|
|
|
|
proto := r.Header.Get("Upgrade")
|
|
|
|
sm.mu.Lock()
|
|
if _, ok := sm.sessions[id]; ok {
|
|
sm.mu.Unlock()
|
|
return errors.Errorf("session %s already exists", id)
|
|
}
|
|
|
|
if proto == "" {
|
|
sm.mu.Unlock()
|
|
return errors.New("no upgrade proto in request")
|
|
}
|
|
|
|
if proto != "h2c" {
|
|
sm.mu.Unlock()
|
|
return errors.Errorf("protocol %s not supported", proto)
|
|
}
|
|
|
|
conn, _, err := hijacker.Hijack()
|
|
if err != nil {
|
|
sm.mu.Unlock()
|
|
return errors.Wrap(err, "failed to hijack connection")
|
|
}
|
|
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusSwitchingProtocols,
|
|
ProtoMajor: 1,
|
|
ProtoMinor: 1,
|
|
Header: http.Header{},
|
|
}
|
|
resp.Header.Set("Connection", "Upgrade")
|
|
resp.Header.Set("Upgrade", proto)
|
|
|
|
// set raw mode
|
|
conn.Write([]byte{})
|
|
resp.Write(conn)
|
|
|
|
return sm.handleConn(ctx, conn, r.Header)
|
|
}
|
|
|
|
// HandleConn handles an incoming raw connection
|
|
func (sm *Manager) HandleConn(ctx context.Context, conn net.Conn, opts map[string][]string) error {
|
|
sm.mu.Lock()
|
|
return sm.handleConn(ctx, conn, opts)
|
|
}
|
|
|
|
// caller needs to take lock, this function will release it
|
|
func (sm *Manager) handleConn(ctx context.Context, conn net.Conn, opts map[string][]string) error {
|
|
ctx, cancel := context.WithCancelCause(ctx)
|
|
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
|
|
|
opts = canonicalHeaders(opts)
|
|
|
|
h := http.Header(opts)
|
|
id := h.Get(headerSessionID)
|
|
sharedKey := h.Get(headerSessionSharedKey)
|
|
|
|
ctx, cc, err := grpcClientConn(ctx, conn, opts)
|
|
if err != nil {
|
|
sm.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
c := &client{
|
|
Session: Session{
|
|
id: id,
|
|
sharedKey: sharedKey,
|
|
ctx: ctx,
|
|
cancelCtx: cancel,
|
|
done: make(chan struct{}),
|
|
},
|
|
cc: cc,
|
|
supported: make(map[string]struct{}),
|
|
}
|
|
|
|
for _, m := range opts[headerSessionMethod] {
|
|
c.supported[strings.ToLower(m)] = struct{}{}
|
|
}
|
|
sm.sessions[id] = c
|
|
sm.updateCondition.Broadcast()
|
|
sm.mu.Unlock()
|
|
|
|
defer func() {
|
|
sm.mu.Lock()
|
|
delete(sm.sessions, id)
|
|
sm.mu.Unlock()
|
|
}()
|
|
|
|
<-c.ctx.Done()
|
|
conn.Close()
|
|
close(c.done)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get returns a session by ID
|
|
func (sm *Manager) Get(ctx context.Context, id string, noWait bool) (Caller, error) {
|
|
// session prefix is used to identify vertexes with different contexts so
|
|
// they would not collide, but for lookup we don't need the prefix
|
|
if p := strings.SplitN(id, ":", 2); len(p) == 2 && len(p[1]) > 0 {
|
|
id = p[1]
|
|
}
|
|
|
|
ctx, cancel := context.WithCancelCause(ctx)
|
|
defer func() { cancel(errors.WithStack(context.Canceled)) }()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
sm.mu.Lock()
|
|
sm.updateCondition.Broadcast()
|
|
sm.mu.Unlock()
|
|
}()
|
|
|
|
var c *client
|
|
|
|
sm.mu.Lock()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
sm.mu.Unlock()
|
|
return nil, errors.Wrapf(context.Cause(ctx), "no active session for %s", id)
|
|
default:
|
|
}
|
|
var ok bool
|
|
c, ok = sm.sessions[id]
|
|
if (!ok || c.closed()) && !noWait {
|
|
sm.updateCondition.Wait()
|
|
continue
|
|
}
|
|
sm.mu.Unlock()
|
|
break
|
|
}
|
|
|
|
if c == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (c *client) Context(ctx context.Context) context.Context {
|
|
return contextWithCaller(ctx, c.context())
|
|
}
|
|
|
|
func (c *client) SharedKey() string {
|
|
return c.sharedKey
|
|
}
|
|
|
|
func (c *client) Supports(url string) bool {
|
|
_, ok := c.supported[strings.ToLower(url)]
|
|
return ok
|
|
}
|
|
func (c *client) Conn() *grpc.ClientConn {
|
|
return c.cc
|
|
}
|
|
|
|
func canonicalHeaders(in map[string][]string) map[string][]string {
|
|
out := map[string][]string{}
|
|
for k := range in {
|
|
out[http.CanonicalHeaderKey(k)] = in[k]
|
|
}
|
|
return out
|
|
}
|