代码之家  ›  专栏  ›  技术社区  ›  hendry

如何通过编程证明此代码具有竞争条件?

  •  2
  • hendry  · 技术社区  · 5 年前

    func (h *handler) loggingMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            h.Log = log.WithFields(log.Fields{
                "method":     r.Method,
                "requestURI": r.RequestURI,
            })
            next.ServeHTTP(w, r)
        })
    }
    

    我试过了 go build -race ,然后运行二进制文件: PORT=3000 ./main hey -n 10000 -c 200 http://localhost:3000 .

    下面是代码的其余部分: https://raw.githubusercontent.com/kaihendry/context-youtube/master/5/main.go

    type handler struct{ Log *log.Entry }
    
    func New() (h *handler) { return &handler{Log: log.WithFields(log.Fields{"test": "FAIL"})} }
    
    func (h *handler) index(w http.ResponseWriter, r *http.Request) {
        h.Log.Info("index")
        fmt.Fprintf(w, "hello")
    }
    
    func (h *handler) about(w http.ResponseWriter, r *http.Request) {
        h.Log.Info("about")
        fmt.Fprintf(w, "about")
    }
    
    func main() {
        h := New()
        app := mux.NewRouter()
        app.HandleFunc("/", h.index)
        app.HandleFunc("/about", h.about)
        app.Use(h.loggingMiddleware)
        if err := http.ListenAndServe(":"+os.Getenv("PORT"), app); err != nil {
            log.WithError(err).Fatal("error listening")
        }
    }
    

    h.Log

    2 回复  |  直到 5 年前
        1
  •  3
  •   icza    5 年前

    有一种编程方式,为此您必须做两件事:

    • 重现活泼的状态
    • 并使用 -race go 工具

    最好是为它编写一个单元测试,这样测试也是可复制的,并且在每次构建/部署时自动运行/检查。

    好的,那么如何复制它呢?

    只需编写一个启动2个goroutine的测试,其中一个调用 index 处理程序,以及调用 about

    使用 net/http/httptest httptest.NewServer() 给你一个准备就绪的服务器,用你传递给它的处理程序“武装”起来。

    下面是一个简单的测试示例,它将触发竞争条件。把它放在一个名为 main_test.go ,在您的 main.go 文件:

    package main
    
    import (
        "fmt"
        "net/http"
        "net/http/httptest"
        "sync"
        "testing"
    
        "github.com/gorilla/mux"
    )
    
    func TestRace(t *testing.T) {
        h := New()
        app := mux.NewRouter()
        app.HandleFunc("/", h.index)
        app.HandleFunc("/about", h.about)
        app.Use(h.loggingMiddleware)
    
        server := httptest.NewServer(app)
        defer server.Close()
    
        wg := &sync.WaitGroup{}
        for _, path := range []string{"/", "/about"} {
            path := path
            wg.Add(1)
            go func() {
                defer wg.Done()
                req, err := http.NewRequest(http.MethodGet, server.URL+path, nil)
                fmt.Println(server.URL + path)
                if err != nil {
                    panic(err)
                }
                res, err := http.DefaultClient.Do(req)
                if err != nil {
                    panic(err)
                }
                defer res.Body.Close()
            }()
        }
        wg.Wait()
    }
    

    你必须用它来运行它

    go test -race
    

    示例输出为:

    http://127.0.0.1:33007/
    http://127.0.0.1:33007/about
    ==================
    WARNING: DATA RACE
    Write at 0x00c000098030 by goroutine 17:
      play.(*handler).loggingMiddleware.func1()
          /home/icza/tmp/gows/src/play/main.go:16 +0x1ce
      net/http.HandlerFunc.ServeHTTP()
          /usr/local/go/src/net/http/server.go:1964 +0x51
      github.com/gorilla/mux.(*Router).ServeHTTP()
          /home/icza/tmp/gows/src/github.com/gorilla/mux/mux.go:212 +0x12e
      net/http.serverHandler.ServeHTTP()
          /usr/local/go/src/net/http/server.go:2741 +0xc4
      net/http.(*conn).serve()
          /usr/local/go/src/net/http/server.go:1847 +0x80a
    
    Previous write at 0x00c000098030 by goroutine 16:
      play.(*handler).loggingMiddleware.func1()
          /home/icza/tmp/gows/src/play/main.go:16 +0x1ce
      net/http.HandlerFunc.ServeHTTP()
          /usr/local/go/src/net/http/server.go:1964 +0x51
      github.com/gorilla/mux.(*Router).ServeHTTP()
          /home/icza/tmp/gows/src/github.com/gorilla/mux/mux.go:212 +0x12e
      net/http.serverHandler.ServeHTTP()
          /usr/local/go/src/net/http/server.go:2741 +0xc4
      net/http.(*conn).serve()
          /usr/local/go/src/net/http/server.go:1847 +0x80a
    
    Goroutine 17 (running) created at:
      net/http.(*Server).Serve()
          /usr/local/go/src/net/http/server.go:2851 +0x4c5
      net/http/httptest.(*Server).goServe.func1()
          /usr/local/go/src/net/http/httptest/server.go:280 +0xac
    
    Goroutine 16 (running) created at:
      net/http.(*Server).Serve()
          /usr/local/go/src/net/http/server.go:2851 +0x4c5
      net/http/httptest.(*Server).goServe.func1()
          /usr/local/go/src/net/http/httptest/server.go:280 +0xac
    ==================
    2019/01/06 14:58:50  info index                     method=GET requestURI=/
    2019/01/06 14:58:50  info about                     method=GET requestURI=/about
    --- FAIL: TestRace (0.00s)
        testing.go:771: race detected during execution of test
    FAIL
    exit status 1
    FAIL    play    0.011s
    

    测试失败,表明存在数据竞争。

    笔记:

    sync.WaitGroup 是等待2个启动的goroutine,而不是同步对处理程序记录器的访问(这会导致数据争用)。如果您修复了数据争用,测试将正常运行并结束(等待2个启动的测试goroutine完成)。

        2
  •  2
  •   David Maze    5 年前

    假设您几乎在同一时间收到两个命中同一处理程序的入站连接。第一个连接开始运行:

    h.Log = log.WithFields(log.Fields{
        "method":     rFirst.Method,
        "requestURI": rFirst.RequestURI,
    })
    

    但是等等!第二个连接出现了!也许运行时想要挂起这个goroutine并启动第二个连接。然后

    h.Log = log.WithFields(log.Fields{
        "method":     rSecond.Method,
        "requestURI": rSecond.RequestURI,
    })
    next.ServeHTTP(wSecond, rSecond)
    

    呸……这就完了。让我们回到我们的第一次狂欢。

    // What's in h.Log now, with this sequence of events?
    next.ServeHTTP(wFirst, rFirst)
    

    第二组示例不会改变 h.Log ,但一定要对其调用方法。在大多数情况下,这可能安全,也可能不安全。这个 documentation for log.Logger "github.com/sirupsen/logrus" log , that has a similar statement in its documentation

    我可以假设设置h.Log是安全的吗?

    没有 sync.Mutex The Go Memory Model 更精确地定义了什么样的副作用保证在出现时是可见的。