Как убедиться, что в коде нет гонок данных в Go? - PullRequest
2 голосов
/ 18 февраля 2020

Я пишу микросервис, который вызывает другие микросервисы, для данных, которые редко обновляются (один раз в день или один раз в месяц). Поэтому я решил создать кеш и реализовал этот интерфейс:

type StringCache interface {
    Get(string) (string, bool)
    Put(string, string)
}

внутренне это просто map[string]cacheItem, где

type cacheItem struct {
    data      string
    expire_at time.Time
}

Мой коллега говорит, что это небезопасно, и мне нужно добавить мьютекс блокирует в моих методах, потому что он будет использоваться параллельно различными экземплярами функций обработчика http. У меня есть тест для него, но он не обнаруживает гонки данных, потому что он использует кэш в одной процедуре:

func TestStringCache(t *testing.T) {
    testDuration := time.Millisecond * 10
    cache := NewStringCache(testDuration / 2)

    cache.Put("here", "this")

    // Value put in cache should be in cache
    res, ok := cache.Get("here")
    assert.Equal(t, res, "this")
    assert.True(t, ok)

    // Values put in cache will eventually expire
    time.Sleep(testDuration)

    res, ok = cache.Get("here")
    assert.Equal(t, res, "")
    assert.False(t, ok)
}

Итак, мой вопрос: как переписать этот тест, чтобы он обнаружил гонку данных (если он присутствует) при работе с go test -race?

1 Ответ

3 голосов
/ 18 февраля 2020

Во-первых, детектор гонки данных в Go - это не какой-то формальный прувер, который использует анализ кода stati c, а скорее динамический c инструмент, который обрабатывает скомпилированный код специальным образом, чтобы попытаться обнаружить гонки данных во время выполнения.
Это означает, что если детектор гонки удачлив и обнаруживает гонку данных, вы должны быть уверены, что - это гонка данных в сообщенном месте. Но это также означает, что если фактический поток программы не привел к возникновению определенного существующего условия гонки данных , детектор гонки не обнаружит и не сообщит об этом.
Другими словами, детектор гонки не обнаружит есть ложные срабатывания, но это всего лишь инструмент с максимальными усилиями.

Итак, для написания кода без рас нужно действительно переосмыслить свой подход.
Лучше всего начать с этого класса. c эссе о топи c, написанное автором Go детектора расы, и как только вы осознаете, что нет добросовестных гонок с данными, вы в основном просто приучаете себя думать о одновременном проведении жаб Выполнение доступа к вашим данным каждый раз, когда вы разрабатываете данные и алгоритмы для их обработки.

Например, вы знаете (по крайней мере, вы должны знать, прочитали ли вы документы), что каждый входящий запрос к HTTP-серверу, реализованный с помощью net/http, обрабатывается отдельной программой.
Это означает, что если у вас есть центральный (совместно используемая) структура данных, такая как кэш, доступ к которому осуществляется кодом, обрабатывающим запросы клиентов, у вас do имеется несколько процедур, потенциально обеспечивающих одновременный доступ к этим общим данным.

Теперь, если у вас есть другая процедура, которая обновляет эти данные, у вас есть потенциал для гонки данных classi c: пока одна программа обновляет данные, другая может прочитать ее.

Что касается вопроса под рукой две вещи:

Во-первых, Никогда не используют таймеры для тестирования. Это не работает.

Во-вторых, для такого простого случая, как ваш, достаточно использовать всего две goroutines:

package main

import (
    "testing"
    "time"
)

type cacheItem struct {
    data      string
    expire_at time.Time
}

type stringCache struct {
    m   map[string]cacheItem
    exp time.Duration
}

func (sc *stringCache) Get(key string) (string, bool) {
    if item, ok := sc.m[key]; !ok {
        return "", false
    } else {
        return item.data, true
    }
}

func (sc *stringCache) Put(key, data string) {
    sc.m[key] = cacheItem{
        data:      data,
        expire_at: time.Now().Add(sc.exp),
    }
}

func NewStringCache(d time.Duration) *stringCache {
    return &stringCache{
        m:   make(map[string]cacheItem),
        exp: d,
    }
}

func TestStringCache(t *testing.T) {
    cache := NewStringCache(time.Minute)

    ch := make(chan struct{})

    go func() {
        cache.Put("here", "this")
        close(ch)
    }()

    _, _ = cache.Get("here")

    <-ch
}

Сохраните это как sc_test.go, а затем

tmp$ go test -race -c -o sc_test ./sc_test.go 
tmp$ ./sc_test 
==================
WARNING: DATA RACE
Write at 0x00c00009e270 by goroutine 8:
  runtime.mapassign_faststr()
      /home/kostix/devel/golang-1.13.6/src/runtime/map_faststr.go:202 +0x0
  command-line-arguments.(*stringCache).Put()
      /home/kostix/tmp/sc_test.go:27 +0x144
  command-line-arguments.TestStringCache.func1()
      /home/kostix/tmp/sc_test.go:46 +0x62

Previous read at 0x00c00009e270 by goroutine 7:
  runtime.mapaccess2_faststr()
      /home/kostix/devel/golang-1.13.6/src/runtime/map_faststr.go:107 +0x0
  command-line-arguments.TestStringCache()
      /home/kostix/tmp/sc_test.go:19 +0x125
  testing.tRunner()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199

Goroutine 8 (running) created at:
  command-line-arguments.TestStringCache()
      /home/kostix/tmp/sc_test.go:45 +0xe4
  testing.tRunner()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199

Goroutine 7 (running) created at:
  testing.(*T).Run()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:960 +0x651
  testing.runTests.func1()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:1202 +0xa6
  testing.tRunner()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:909 +0x199
  testing.runTests()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:1200 +0x521
  testing.(*M).Run()
      /home/kostix/devel/golang-1.13.6/src/testing/testing.go:1117 +0x2ff
  main.main()
      _testmain.go:44 +0x223
==================
--- FAIL: TestStringCache (0.00s)
    testing.go:853: race detected during execution of test
FAIL
...