在 Golang 中测试一次条件

我有一个模块,它依赖于通过调用外部服务来填充缓存,如下所示:


func (provider *Cache) GetItem(productId string, skuId string, itemType string) (*Item, error) {


    // First, create the key we'll use to uniquely identify the item

    key := fmt.Sprintf("%s:%s", productId, skuId)


    // Now, attempt to get the concurrency control associated with the item key

    // If we couldn't find it then create one and add it to the map

    var once *sync.Once

    if entry, ok := provider.lockMap.Load(key); ok {

        once = entry.(*sync.Once)

    } else {

        once = &sync.Once{}

        provider.lockMap.Store(key, once)

    }


    // Now, use the concurrency control to attempt to request the item

    // but only once. Channel any errors that occur

    cErr := make(chan error, 1)

    once.Do(func() {


        // We didn't find the item in the cache so we'll have to get it from the partner-center

        item, err := provider.client.GetItem(productId, skuId)

        if err != nil {

            cErr <- err

            return

        }


        // Add the item to the cache

        provider.cache.Store(key, &item)

    })


    // Attempt to read an error from the channel; if we get one then return it

    // Otherwise, pull the item out of the cache. We have to use the select here because this is

    // the only way to attempt to read from a channel without it blocking

    var sku interface{}

    select {

    case err, ok := <-cErr:

        if ok {

            return nil, err

        }

    default:

        item, _ = provider.cache.Load(key)

    }


    // Now, pull out a reference to the item and return it

    return item.(*Item), nil

}

这种方法按我的预期工作。我的问题是测试;专门测试以确保该GetItem方法仅针对给定的键值调用一次。我的测试代码如下:


var _ = Describe("Item Tests", func() {


    It("GetItem - Not cached, two concurrent requests - Client called once", func() {


        // setup cache


    })

})


我遇到的问题是,有时这个测试会失败,因为请求计数是 2,而在注释行中预期它是 1。此故障不一致,我不太确定如何调试此问题。任何帮助将不胜感激。


浮云间
浏览 139回答 1
1回答

蝴蝶不菲

我认为您的测试有时会失败,因为您的缓存无法保证它只获取一次项目,而且您很幸运测试抓住了这一点。如果一个 item 不在其中,并且 2 个并发 goroutines 同时调用Cache.GetItem(),可能lockMap.Load()会在两者中报告 key 不在 map 中,两个 goroutines 都创建一个sync.Once,并且都将自己的实例存储在 map 中(显然只有一个——后者——将保留在地图中,但您的缓存不会检查这一点)。然后 2 个 goroutine 都会调用client.GetItem(),因为 2 个单独sync.Once的不提供同步。只有使用相同的sync.Once实例,才能保证传递给的函数Once.Do()只执行一次。我认为sync.Mutex避免sync.Once在这里创建和使用 2 会更容易、更合适。或者由于您已经在使用sync.Map,您可以使用以下Map.LoadOrStore()方法:创建一个sync.Once,并将其传递给Map.LoadOrStore(). 如果键已经在地图中,请使用返回的sync.Once. 如果密钥不在地图中,您sync.Once将存储在其中,因此您可以使用它。这将确保没有多个并发 goroutine 可以sync.once在其中存储多个实例。像这样的东西:var once *sync.Onceif entry, loaded := provider.lockMap.LoadOrStore(key, once); loaded {&nbsp; &nbsp; // Was already in the map, use the loaded Once&nbsp; &nbsp; once = entry.(*sync.Once)}这个解决方案仍然不完美:如果 2 个 goroutineCache.GetItem()同时调用,只有一个会尝试从客户端获取项目,但如果失败,只有这个 goroutine 会报告错误,另一个 goroutine 不会尝试获取来自客户端的项目,但将从地图加载它并且您不检查加载是否成功。你应该,如果它不在地图中,这意味着另一个并发尝试未能获得它。所以你应该报告错误(并清除sync.Once)。如您所见,它变得越来越复杂。我坚持我之前的建议:sync.Mutex在这里使用 a 会更容易。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go