2016-11-16 1 views
-1

Je peux voir deux problèmes principaux dans l'exemple de code ci-dessous, mais je ne sais pas comment les résoudre correctement.Go condition de course dans le gestionnaire de délai

Si le gestionnaire de dépassement de délai n'obtient pas le signal via l'errCh que le gestionnaire suivant a terminé ou qu'une erreur s'est produite, il répondra "408 Request timeout" à la demande.

Le problème ici est que le ResponseWriter n'est pas sûr d'être utilisé par plusieurs goroutines. Et le gestionnaire de délai d'attente démarre un nouveau goroutine lors de l'exécution du gestionnaire suivant.

Questions:

  1. Comment empêcher le prochain gestionnaire d'écrire dans le ResponseWriter lorsque vous avez terminé les temps de canal du CTX dans le gestionnaire de délai d'attente. Comment éviter que le gestionnaire de timeout ne réponde au code d'état 408 lorsque le gestionnaire suivant écrit dans le ResponseWriter mais qu'il n'est pas encore terminé et que le canal Terminé du ctx arrive à expiration dans le gestionnaire de dépassement de délai.


package main 

import (
    "context" 
    "fmt" 
    "net/http" 
    "time" 
) 

func main() { 
    http.Handle("/race", handlerFunc(timeoutHandler)) 
    http.ListenAndServe(":8080", nil) 
} 

func timeoutHandler(w http.ResponseWriter, r *http.Request) error { 
    const seconds = 1 
    ctx, cancel := context.WithTimeout(r.Context(), time.Duration(seconds)*time.Second) 
    defer cancel() 

    r = r.WithContext(ctx) 

    errCh := make(chan error, 1) 
    go func() { 
    // w is not safe for concurrent use by multiple goroutines 
    errCh <- nextHandler(w, r) 
    }() 

    select { 
    case err := <-errCh: 
    return err 
    case <-ctx.Done(): 
    // w is not safe for concurrent use by multiple goroutines 
    http.Error(w, "Request timeout", 408) 
    return nil 
    } 
} 

func nextHandler(w http.ResponseWriter, r *http.Request) error { 
    // just for fun to simulate a better race condition 
    const seconds = 1 
    time.Sleep(time.Duration(seconds) * time.Second) 
    fmt.Fprint(w, "nextHandler") 
    return nil 
} 

type handlerFunc func(w http.ResponseWriter, r *http.Request) error 

func (fn handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
    if err := fn(w, r); err != nil { 
    http.Error(w, "Server error", 500) 
    } 
} 
+2

Que diriez-vous passer autre chose que l'original 'ResponseWriter' à' nextHandler() '? Vous devrez alors recopier les résultats à l'origine 'ResponseWriter' dans le cas' <-errCh'. –

+0

Tout ce qui va écrire dans ResponseWriter doit également être responsable du délai d'expiration. Vous fournissez un contexte de délai à nextHandler, ce qui devrait permettre de gérer le timeout lui-même. En général, c'est plus facile si un seul gestionnaire est responsable de l'écriture de la réponse. – JimB

Répondre

0

Voici une solution possible, qui est basée sur @ commentaire de Andy.

Un nouveau responseRecorder sera transmis à la nextHandler et la réponse enregistrée seront recopiées au client:

func timeoutHandler(w http.ResponseWriter, r *http.Request) error { 
    const seconds = 1 
    ctx, cancel := context.WithTimeout(r.Context(), 
     time.Duration(seconds)*time.Second) 
    defer cancel() 

    r = r.WithContext(ctx) 

    errCh := make(chan error, 1) 
    w2 := newResponseRecorder() 
    go func() { 
     errCh <- nextHandler(w2, r) 
    }() 

    select { 
    case err := <-errCh: 
     if err != nil { 
      return err 
     } 

     w2.cloneHeader(w.Header()) 
     w.WriteHeader(w2.status) 
     w.Write(w2.buf.Bytes()) 
     return nil 
    case <-ctx.Done(): 
     http.Error(w, "Request timeout", 408) 
     return nil 
    } 
} 

Et voici le responseRecorder:

type responseRecorder struct { 
    http.ResponseWriter 
    header http.Header 
    buf *bytes.Buffer 
    status int 
} 

func newResponseRecorder() *responseRecorder { 
    return &responseRecorder{ 
     header: http.Header{}, 
     buf: &bytes.Buffer{}, 
    } 
} 

func (w *responseRecorder) Header() http.Header { 
    return w.header 
} 

func (w *responseRecorder) cloneHeader(dst http.Header) { 
    for k, v := range w.header { 
     tmp := make([]string, len(v)) 
     copy(tmp, v) 
     dst[k] = tmp 
    } 
} 

func (w *responseRecorder) Write(data []byte) (int, error) { 
    if w.status == 0 { 
     w.WriteHeader(http.StatusOK) 
    } 
    return w.buf.Write(data) 
} 

func (w *responseRecorder) WriteHeader(status int) { 
    w.status = status 
}