1
0
mirror of https://github.com/golang/go synced 2024-11-18 14:04:45 -07:00

internal/jsonrpc2: change the concurrency strategy

This abandons the limited size queue
Instead we kick a go-routine per request, but have each request wait for the
previous request to say it is okay to continue. This allows each request to
control when it is finished with tasks that required strict ordering without
every blocking the routine that handles replies.
It also protects against repeated or missing replies.

Fixes golang/go#32631
Fixes golang/go#32589
Fixes golang/go#32467
Fixes golang/go#32360
Fixes golang/go#31977

Change-Id: Icd071620052351ec7f8fac136f1b8e3f97d4bb2d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/183718
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-06-24 09:16:28 -04:00
parent 619de4ed67
commit 4874f863e6

View File

@ -35,12 +35,26 @@ type Conn struct {
pendingMu sync.Mutex // protects the pending map pendingMu sync.Mutex // protects the pending map
pending map[ID]chan *wireResponse pending map[ID]chan *wireResponse
handlingMu sync.Mutex // protects the handling map handlingMu sync.Mutex // protects the handling map
handling map[ID]handling handling map[ID]*Request
} }
type requestState int
const (
requestWaiting = requestState(iota)
requestSerial
requestParallel
requestReplied
requestDone
)
// Request is sent to a server to represent a Call or Notify operaton. // Request is sent to a server to represent a Call or Notify operaton.
type Request struct { type Request struct {
conn *Conn conn *Conn
cancel context.CancelFunc
start time.Time
state requestState
nextRequest chan struct{}
// Method is a string containing the method name to invoke. // Method is a string containing the method name to invoke.
Method string Method string
@ -52,12 +66,6 @@ type Request struct {
ID *ID ID *ID
} }
type queueEntry struct {
ctx context.Context
r *Request
size int64
}
// Handler is an option you can pass to NewConn to handle incoming requests. // Handler is an option you can pass to NewConn to handle incoming requests.
// If the request returns false from IsNotify then the Handler must eventually // If the request returns false from IsNotify then the Handler must eventually
// call Reply on the Conn with the supplied request. // call Reply on the Conn with the supplied request.
@ -75,7 +83,6 @@ type Canceler func(context.Context, *Conn, ID)
type rpcStats struct { type rpcStats struct {
server bool server bool
method string method string
ctx context.Context
span trace.Span span trace.Span
start time.Time start time.Time
received int64 received int64
@ -87,13 +94,15 @@ type statsKeyType string
const rpcStatsKey = statsKeyType("rpcStatsKey") const rpcStatsKey = statsKeyType("rpcStatsKey")
func start(ctx context.Context, server bool, method string, id *ID) (context.Context, *rpcStats) { func start(ctx context.Context, server bool, method string, id *ID) (context.Context, *rpcStats) {
if method == "" {
panic("no method in rpc stats")
}
s := &rpcStats{ s := &rpcStats{
server: server, server: server,
method: method, method: method,
ctx: ctx,
start: time.Now(), start: time.Now(),
} }
s.ctx = context.WithValue(s.ctx, rpcStatsKey, s) ctx = context.WithValue(ctx, rpcStatsKey, s)
tags := make([]tag.Mutator, 0, 4) tags := make([]tag.Mutator, 0, 4)
tags = append(tags, tag.Upsert(telemetry.KeyMethod, method)) tags = append(tags, tag.Upsert(telemetry.KeyMethod, method))
mode := telemetry.Outbound mode := telemetry.Outbound
@ -106,10 +115,10 @@ func start(ctx context.Context, server bool, method string, id *ID) (context.Con
if id != nil { if id != nil {
tags = append(tags, tag.Upsert(telemetry.KeyRPCID, id.String())) tags = append(tags, tag.Upsert(telemetry.KeyRPCID, id.String()))
} }
s.ctx, s.span = trace.StartSpan(ctx, method, trace.WithSpanKind(spanKind)) ctx, s.span = trace.StartSpan(ctx, method, trace.WithSpanKind(spanKind))
s.ctx, _ = tag.New(s.ctx, tags...) ctx, _ = tag.New(ctx, tags...)
stats.Record(s.ctx, telemetry.Started.M(1)) stats.Record(ctx, telemetry.Started.M(1))
return s.ctx, s return ctx, s
} }
func (s *rpcStats) end(ctx context.Context, err *error) { func (s *rpcStats) end(ctx context.Context, err *error) {
@ -145,11 +154,11 @@ func NewConn(s Stream) *Conn {
conn := &Conn{ conn := &Conn{
stream: s, stream: s,
pending: make(map[ID]chan *wireResponse), pending: make(map[ID]chan *wireResponse),
handling: make(map[ID]handling), handling: make(map[ID]*Request),
} }
// the default handler reports a method error // the default handler reports a method error
conn.Handler = func(ctx context.Context, r *Request) { conn.Handler = func(ctx context.Context, r *Request) {
if r.IsNotify() { if !r.IsNotify() {
r.Reply(ctx, nil, NewErrorf(CodeMethodNotFound, "method %q not found", r.Method)) r.Reply(ctx, nil, NewErrorf(CodeMethodNotFound, "method %q not found", r.Method))
} }
} }
@ -273,28 +282,38 @@ func (r *Request) IsNotify() bool {
return r.ID == nil return r.ID == nil
} }
// Parallel indicates that the system is now allowed to process other requests
// in parallel with this one.
// It is safe to call any number of times, but must only be called from the
// request handling go routine.
// It is implied by both reply and by the handler returning.
func (r *Request) Parallel() {
if r.state >= requestParallel {
return
}
r.state = requestParallel
close(r.nextRequest)
}
// Reply sends a reply to the given request. // Reply sends a reply to the given request.
// It is an error to call this if request was not a call. // It is an error to call this if request was not a call.
// You must call this exactly once for any given request. // You must call this exactly once for any given request.
// It should only be called from the handler go routine.
// If err is set then result will be ignored. // If err is set then result will be ignored.
func (r *Request) Reply(ctx context.Context, result interface{}, err error) error { func (r *Request) Reply(ctx context.Context, result interface{}, err error) error {
ctx, st := trace.StartSpan(ctx, r.Method+":reply", trace.WithSpanKind(trace.SpanKindClient)) if r.state >= requestReplied {
defer st.End() return fmt.Errorf("reply invoked more than once")
}
if r.IsNotify() { if r.IsNotify() {
return fmt.Errorf("reply not invoked with a valid call") return fmt.Errorf("reply not invoked with a valid call")
} }
r.conn.handlingMu.Lock() ctx, st := trace.StartSpan(ctx, r.Method+":reply", trace.WithSpanKind(trace.SpanKindClient))
handling, found := r.conn.handling[*r.ID] defer st.End()
if found {
delete(r.conn.handling, *r.ID)
}
r.conn.handlingMu.Unlock()
if !found {
return fmt.Errorf("not a call in progress: %v", r.ID)
}
elapsed := time.Since(handling.start) r.Parallel()
r.state = requestReplied
elapsed := time.Since(r.start)
var raw *json.RawMessage var raw *json.RawMessage
if err == nil { if err == nil {
raw, err = marshalToRaw(result) raw, err = marshalToRaw(result)
@ -319,10 +338,9 @@ func (r *Request) Reply(ctx context.Context, result interface{}, err error) erro
v := ctx.Value(rpcStatsKey) v := ctx.Value(rpcStatsKey)
if v != nil { if v != nil {
s := v.(*rpcStats) v.(*rpcStats).sent += n
s.sent += n
} else { } else {
//panic("no stats available in reply") panic("no stats available in reply")
} }
if err != nil { if err != nil {
@ -333,10 +351,17 @@ func (r *Request) Reply(ctx context.Context, result interface{}, err error) erro
return nil return nil
} }
type handling struct { func (c *Conn) setHandling(r *Request, active bool) {
request *Request if r.ID == nil {
cancel context.CancelFunc return
start time.Time }
r.conn.handlingMu.Lock()
defer r.conn.handlingMu.Unlock()
if active {
r.conn.handling[*r.ID] = r
} else {
delete(r.conn.handling, *r.ID)
}
} }
// combined has all the fields of both Request and Response. // combined has all the fields of both Request and Response.
@ -350,40 +375,13 @@ type combined struct {
Error *Error `json:"error,omitempty"` Error *Error `json:"error,omitempty"`
} }
func (c *Conn) deliver(ctx context.Context, q chan queueEntry, request *Request, size int64) bool {
e := queueEntry{ctx: ctx, r: request, size: size}
if !c.RejectIfOverloaded {
q <- e
return true
}
select {
case q <- e:
return true
default:
return false
}
}
// Run blocks until the connection is terminated, and returns any error that // Run blocks until the connection is terminated, and returns any error that
// caused the termination. // caused the termination.
// It must be called exactly once for each Conn. // It must be called exactly once for each Conn.
// It returns only when the reader is closed or there is an error in the stream. // It returns only when the reader is closed or there is an error in the stream.
func (c *Conn) Run(ctx context.Context) error { func (c *Conn) Run(ctx context.Context) error {
q := make(chan queueEntry, c.Capacity) nextRequest := make(chan struct{})
defer close(q) close(nextRequest)
// start the queue processor
go func() {
// TODO: idle notification?
for e := range q {
if e.ctx.Err() != nil {
continue
}
ctx, rpcStats := start(ctx, true, e.r.Method, e.r.ID)
rpcStats.received += e.size
c.Handler(ctx, e.r)
rpcStats.end(ctx, nil)
}
}()
for { for {
// get the data for a message // get the data for a message
data, n, err := c.stream.Read(ctx) data, n, err := c.stream.Read(ctx)
@ -403,33 +401,36 @@ func (c *Conn) Run(ctx context.Context) error {
switch { switch {
case msg.Method != "": case msg.Method != "":
// if method is set it must be a request // if method is set it must be a request
request := &Request{ reqCtx, cancelReq := context.WithCancel(ctx)
conn: c, reqCtx, rpcStats := start(reqCtx, true, msg.Method, msg.ID)
Method: msg.Method, rpcStats.received += n
Params: msg.Params, thisRequest := nextRequest
ID: msg.ID, nextRequest = make(chan struct{})
} req := &Request{
if request.IsNotify() { conn: c,
c.Logger(Receive, request.ID, -1, request.Method, request.Params, nil) cancel: cancelReq,
// we have a Notify, add to the processor queue nextRequest: nextRequest,
c.deliver(ctx, q, request, n) start: time.Now(),
//TODO: log when we drop a message? Method: msg.Method,
} else { Params: msg.Params,
// we have a Call, add to the processor queue ID: msg.ID,
reqCtx, cancelReq := context.WithCancel(ctx)
c.handlingMu.Lock()
c.handling[*request.ID] = handling{
request: request,
cancel: cancelReq,
start: time.Now(),
}
c.handlingMu.Unlock()
c.Logger(Receive, request.ID, -1, request.Method, request.Params, nil)
if !c.deliver(reqCtx, q, request, n) {
// queue is full, reject the message by directly replying
request.Reply(ctx, nil, NewErrorf(CodeServerOverloaded, "no room in queue"))
}
} }
c.setHandling(req, true)
go func() {
<-thisRequest
req.state = requestSerial
defer func() {
c.setHandling(req, false)
if !req.IsNotify() && req.state < requestReplied {
req.Reply(reqCtx, nil, NewErrorf(CodeInternalError, "method %q did not reply", req.Method))
}
req.Parallel()
rpcStats.end(reqCtx, nil)
cancelReq()
}()
c.Logger(Receive, req.ID, -1, req.Method, req.Params, nil)
c.Handler(reqCtx, req)
}()
case msg.ID != nil: case msg.ID != nil:
// we have a response, get the pending entry from the map // we have a response, get the pending entry from the map
c.pendingMu.Lock() c.pendingMu.Lock()