如何以编程方式证明此代码具有竞争条件?

时间:2019-01-06 01:56:27

标签: go race-condition httpserver

我被告知此代码在设计上具有竞争条件,尽管我尝试了一下,但我无法证明它确实存在。

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 个答案:

答案 0 :(得分:3)

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

  • 再现民主状况
  • 并在启动-race工具时使用go选项

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

好,那么如何重现呢?

只需编写一个测试,该测试将有意地在不同步的情况下启动两个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个已启动的test-goroutines完成)。

答案 1 :(得分:2)

想象一下,您几乎同时收到两个进入同一处理器的入站连接。第一个连接开始运行:

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)

Ph ...完成了。让我们回到第一个goroutine。

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

或者...

第二组示例不会更改h.Log的值,但是会在其上调用方法。在大多数情况下,这可能是安全的,也可能是不安全的。 documentation for log.Logger包含魔术短语:“可以同时从多个goroutine中使用Logger”。 (如果您实际上将"github.com/sirupsen/logrus"导入为logthat has a similar statement in its documentation。)

  

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

没有sync.Mutex或类似的东西来保护它,实际上不是。您绝对不能保证,如果在第1行上对其进行设置,如果其他一些goroutine可能会对其进行更改,那么它将在第2行上具有相同的值。 The Go Memory Model可以更精确地定义何时可以看到哪些副作用。