mirror of
https://github.com/foomo/foomo-docs.git
synced 2025-10-16 12:35:40 +00:00
85 lines
20 KiB
HTML
85 lines
20 KiB
HTML
<!doctype html>
|
||
<html lang="en" dir="ltr" class="blog-wrapper blog-post-page plugin-blog plugin-id-default" data-has-hydrated="false">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="generator" content="Docusaurus v3.0.0">
|
||
<title data-rh="true">Go race conditions testing and coverage | foomo project docs</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:url" content="https://www.foomo.org/blog/go-race-conditions-testing-and-coverage"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docusaurus_tag" content="default"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="docsearch:docusaurus_tag" content="default"><meta data-rh="true" property="og:title" content="Go race conditions testing and coverage | foomo project docs"><meta data-rh="true" name="description" content="Go has extensive support for concurrency through goroutines and channels. This feature allows programs to progress on several tasks at the same time but it requires some extra care to prevent situations where multiple goroutines might collide and lead to a panic. These are known as race conditions and they happen when a shared variable is read and written at the same time by two different routines. A typical example is a concurrent read/write of a map in memory."><meta data-rh="true" property="og:description" content="Go has extensive support for concurrency through goroutines and channels. This feature allows programs to progress on several tasks at the same time but it requires some extra care to prevent situations where multiple goroutines might collide and lead to a panic. These are known as race conditions and they happen when a shared variable is read and written at the same time by two different routines. A typical example is a concurrent read/write of a map in memory."><meta data-rh="true" property="og:type" content="article"><meta data-rh="true" property="article:published_time" content="2023-03-17T00:00:00.000Z"><meta data-rh="true" property="article:author" content="https://github.com/cvidmar,https://github.com/dreadl0ck"><meta data-rh="true" property="article:tag" content="golang,concurrency,testing"><link data-rh="true" rel="icon" href="/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://www.foomo.org/blog/go-race-conditions-testing-and-coverage"><link data-rh="true" rel="alternate" href="https://www.foomo.org/blog/go-race-conditions-testing-and-coverage" hreflang="en"><link data-rh="true" rel="alternate" href="https://www.foomo.org/blog/go-race-conditions-testing-and-coverage" hreflang="x-default"><link data-rh="true" rel="preconnect" href="https://SUATUVZDDM-dsn.algolia.net" crossorigin="anonymous"><link rel="alternate" type="application/rss+xml" href="/blog/rss.xml" title="foomo project docs RSS Feed">
|
||
<link rel="alternate" type="application/atom+xml" href="/blog/atom.xml" title="foomo project docs Atom Feed">
|
||
|
||
|
||
|
||
<link rel="search" type="application/opensearchdescription+xml" title="foomo project docs" href="/opensearch.xml"><link rel="stylesheet" href="/assets/css/styles.78fe5ce6.css">
|
||
<script src="/assets/js/runtime~main.638e5c2c.js" defer="defer"></script>
|
||
<script src="/assets/js/main.1248442c.js" defer="defer"></script>
|
||
</head>
|
||
<body class="navigation-with-keyboard">
|
||
<script>!function(){function t(t){document.documentElement.setAttribute("data-theme",t)}var e=function(){try{return new URLSearchParams(window.location.search).get("docusaurus-theme")}catch(t){}}()||function(){try{return localStorage.getItem("theme")}catch(t){}}();t(null!==e?e:"light")}(),function(){try{const c=new URLSearchParams(window.location.search).entries();for(var[t,e]of c)if(t.startsWith("docusaurus-data-")){var a=t.replace("docusaurus-data-","data-");document.documentElement.setAttribute(a,e)}}catch(t){}}()</script><div id="__docusaurus"><div role="region" aria-label="Skip to main content"><a class="skipToContent_fXgn" href="#__docusaurus_skipToContent_fallback">Skip to main content</a></div><nav aria-label="Main" class="navbar navbar--fixed-top"><div class="navbar__inner"><div class="navbar__items"><button aria-label="Toggle navigation bar" aria-expanded="false" class="navbar__toggle clean-btn" type="button"><svg width="30" height="30" viewBox="0 0 30 30" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M4 7h22M4 15h22M4 23h22"></path></svg></button><a class="navbar__brand" href="/"><b class="navbar__title text--truncate">foomo</b></a><a class="navbar__item navbar__link" href="/docs/general">General</a><a class="navbar__item navbar__link" href="/docs/frontend">Frontend</a><a class="navbar__item navbar__link" href="/docs/backend">Backend</a><a class="navbar__item navbar__link" href="/docs/devops">DevOps</a><a class="navbar__item navbar__link" href="/docs/projects">Projects</a></div><div class="navbar__items navbar__items--right"><a aria-current="page" class="navbar__item navbar__link navbar__link--active" href="/blog">Blog</a><div class="navbarSearchContainer_Bca1"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><svg width="20" height="20" class="DocSearch-Search-Icon" viewBox="0 0 20 20"><path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"></span></button></div></div></div><div role="presentation" class="navbar-sidebar__backdrop"></div></nav><div id="__docusaurus_skipToContent_fallback" class="main-wrapper mainWrapper_z2l0"><div class="container margin-vert--lg"><div class="row"><aside class="col col--3"><nav class="sidebar_re4s thin-scrollbar" aria-label="Blog recent posts navigation"><div class="sidebarItemTitle_pO2u margin-bottom--md">Recent posts</div><ul class="sidebarItemList_Yudw clean-list"><li class="sidebarItem__DBe"><a aria-current="page" class="sidebarItemLink_mo7H sidebarItemLinkActive_I1ZP" href="/blog/go-race-conditions-testing-and-coverage">Go race conditions testing and coverage</a></li><li class="sidebarItem__DBe"><a class="sidebarItemLink_mo7H" href="/blog/accuracy-of-decimal-computations">Accuracy of decimal computations</a></li><li class="sidebarItem__DBe"><a class="sidebarItemLink_mo7H" href="/blog/why-bundle-size-is-important">Why bundle size is important?</a></li><li class="sidebarItem__DBe"><a class="sidebarItemLink_mo7H" href="/blog/prometheus-cardinality-issues">Prometheus Is Out Of Memory. Again.</a></li><li class="sidebarItem__DBe"><a class="sidebarItemLink_mo7H" href="/blog/searching-for-search-engines">The never ending search a search engine 2022-01 edition</a></li></ul></nav></aside><main class="col col--7" itemscope="" itemtype="https://schema.org/Blog"><article itemprop="blogPost" itemscope="" itemtype="https://schema.org/BlogPosting"><meta itemprop="description" content="Go has extensive support for concurrency through goroutines and channels. This feature allows programs to progress on several tasks at the same time but it requires some extra care to prevent situations where multiple goroutines might collide and lead to a panic. These are known as race conditions and they happen when a shared variable is read and written at the same time by two different routines. A typical example is a concurrent read/write of a map in memory."><header><h1 class="title_f1Hy" itemprop="headline">Go race conditions testing and coverage</h1><div class="container_mt6G margin-vert--md"><time datetime="2023-03-17T00:00:00.000Z" itemprop="datePublished">March 17, 2023</time></div><div class="margin-top--md margin-bottom--sm row"><div class="col col--6 authorCol_Hf19"><div class="avatar margin-bottom--sm"><a href="https://github.com/cvidmar" target="_blank" rel="noopener noreferrer" class="avatar__photo-link"><img class="avatar__photo" src="https://github.com/cvidmar.png" alt="Cristian Vidmar" itemprop="image"></a><div class="avatar__intro" itemprop="author" itemscope="" itemtype="https://schema.org/Person"><div class="avatar__name"><a href="https://github.com/cvidmar" target="_blank" rel="noopener noreferrer" itemprop="url"><span itemprop="name">Cristian Vidmar</span></a></div><small class="avatar__subtitle" itemprop="description">Content Chef</small></div></div></div><div class="col col--6 authorCol_Hf19"><div class="avatar margin-bottom--sm"><a href="https://github.com/dreadl0ck" target="_blank" rel="noopener noreferrer" class="avatar__photo-link"><img class="avatar__photo" src="https://github.com/dreadl0ck.png" alt="Philipp Mieden" itemprop="image"></a><div class="avatar__intro" itemprop="author" itemscope="" itemtype="https://schema.org/Person"><div class="avatar__name"><a href="https://github.com/dreadl0ck" target="_blank" rel="noopener noreferrer" itemprop="url"><span itemprop="name">Philipp Mieden</span></a></div><small class="avatar__subtitle" itemprop="description">MSc</small></div></div></div></div></header><div id="__blog-post-container" class="markdown" itemprop="articleBody"><p>Go has extensive support for concurrency through goroutines and channels. This feature allows programs to progress on several tasks at the same time but it requires some extra care to prevent situations where multiple goroutines might collide and lead to a panic. These are known as race conditions and they happen when a shared variable is read and written at the same time by two different routines. A typical example is a concurrent read/write of a map in memory.</p>
|
||
<p>To illustrate an example let's consider <a href="https://github.com/foomo/gocontentful">gocontentful</a>, the code generator that creates an API to access a Contentful CMS from go. The generated API uses an in-memory cache to store a copy of a Contentful space and some extra data structures to allow go programs to access Contentful data, inspect and resolve references and so on. In a typical scenario, the client is used in a service that responds to HTTP calls and makes heavy use of concurrency because it needs to be able, at the same time, to:</p>
|
||
<ul>
|
||
<li>Read entries, assets and references from the cache</li>
|
||
<li>Update/Delete single entities and their connections with others (for example the map of parent entries)</li>
|
||
<li>Incrementally sync the content of the cache with data changes coming from Contentful</li>
|
||
<li>Rebuild the cache entirely and swap the existing one with a new copy</li>
|
||
</ul>
|
||
<p>In addition, for performance reasons, when the cache is created or rebuilt the gocontentful client spawns up to four goroutines to download chunks of data in parallel across the Internet, dynamically selecting the size the sorting of the chunks to leverage the maximum parallelism.</p>
|
||
<h2 id="detecting-race-conditions-through-unit-tests">Detecting race conditions through unit tests</h2>
|
||
<p>We experienced race conditions with the client in the past, and we fixed them to maintain it production-ready with every new version. To help with this, we included a testing framework in gocontentful that generates a sample API from a local export of a Contentful space and then runs several unit tests that, among others, test the client for concurrency safety:</p>
|
||
<p><img alt="make test" src="/assets/images/make-test-e708de4fbdbded8bf679660769b6d1af.webp" width="1570" height="756"></p>
|
||
<p>One of these unit tests spawns tens of thousands of goroutines to concurrently read, write and inspect the references of specific entries while at the same time keeps rebuilding the cache. From the screenshot above we see that no race condition is shown. Even at this concurrency level, though there's no guarantee that running</p>
|
||
<pre><code class="language-bash">go test ./...
|
||
</code></pre>
|
||
<p>will be enough to generate a collision. What we really want to do is to add a new parameter to enable the go race detector with</p>
|
||
<pre><code class="language-bash">go test -race ./...
|
||
</code></pre>
|
||
<p>(In gocontentful you can run <em>make race</em> to fire up both the API generation and the race test)</p>
|
||
<p>From the documentation at <a href="https://go.dev/blog/race-detector">https://go.dev/blog/race-detector</a>:</p>
|
||
<blockquote>
|
||
<p><em>When the -race command-line flag is set, the compiler instruments all memory accesses with code that records when and how the memory was accessed, while the runtime library watches for unsynchronized accesses to shared variables. When such “racy” behavior is detected, a warning is printed.</em></p>
|
||
</blockquote>
|
||
<p>Running this in gocontentful shows that we indeed have a potential collision condition:</p>
|
||
<p><img alt="Race condition" src="/assets/images/race-condition-5d0a166992c4af58e43708a7eaf41eb9.webp" width="1681" height="1918"></p>
|
||
<p><em>Note: After you run this test you'll want to search for "race" inside the terminal output. Make sure you enable a very long (if not infinite) scrollback or you might miss some hits.</em></p>
|
||
<p>The race detector reports the filenames and lines of code that generated the race condition. Looking at those lines in our example shows that a field of the cache (the "offline" boolean) is written protecting it with a proper mutex lock but the lock handling is missing around the read operation:</p>
|
||
<p><img alt="Read access" src="/assets/images/read-access-e74fbfc1d026ab850775c44f7511bcf0.webp" width="1489" height="402"></p>
|
||
<p><img alt="Write access" src="/assets/images/write-access-e8b23abfbe4c9a9379f8facf8e1a3c0e.webp" width="1618" height="1004"></p>
|
||
<p>The fix is very simple but in this particular case the offline flag is read and then a 2 seconds delay is started. Deferring the unlock would keep the variable locked for far too long, so we will read-lock it only for the time needed to copy its value to a local variable:</p>
|
||
<p><img alt="Fix race condition" src="/assets/images/fix-race-condition-e41a80c7dda8ce75c9414b2f2b37e1b8.webp" width="1695" height="538"></p>
|
||
<p>After fixing the issue in the generator templates and regenerating the code, the tests with the race detector run fine. In gocontentful this can be done all in one step with make race:</p>
|
||
<p><img alt="No race condition" src="/assets/images/no-race-condition-3d4eb2e0a194a1677f60d351fa3798ac.webp" width="1370" height="686"></p>
|
||
<h2 id="test-coverage">Test coverage</h2>
|
||
<p>That was nice! But how do we know if we're covering all test cases? Go has been supporting test code coverage since 1.12 through the -cover option. We can also limit the coverage to a specific package. In our case, we're only interested in the <em>testapi</em> sub-package because we want to test the generated API, not the generator itself.</p>
|
||
<pre><code class="language-bash">go test -cover -coverpkg=./test/testapi ./...
|
||
</code></pre>
|
||
<p>Let's try and run the tests with coverage:</p>
|
||
<p><img alt="Basic coverage" src="/assets/images/basic-coverage-1b4ff1807c3030afadd3d964d0dad9e3.webp" width="1930" height="290"></p>
|
||
<p>The summary shows we are only covering 22% of the code. The goal is not to cover 100%, some parts only work online calling the actual API of a real Contentful space, but we definitely have room for improvement.</p>
|
||
<p>The question is: how do we know exactly which lines of code we're covering through the test suite? Again, go test comes to the rescue through another option: <em>-coverprofile</em> lets us specify an output file that will contain references to each single line of code involved in the analysis. It is a text file, but not very readable:</p>
|
||
<pre><code>github.com/foom...tapi/gocontentfulvolibproduct.go:21.86,22.15 1 1
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:25.2,25.18 1 1
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:28.2,29.16 2 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:32.2,33.16 2 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:36.2,37.37 2 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:40.2,40.24 1 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:22.15,24.3 1 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:25.18,27.3 1 1
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:29.16,31.3 1 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:33.16,35.3 1 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:37.37,39.3 1 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:43.114,44.35 1 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:47.2,48.18 2 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:51.2,53.16 3 0
|
||
github.com/foom...tapi/gocontentfulvolibproduct.go:56.2,57.16 2 0
|
||
...
|
||
</code></pre>
|
||
<p>We can use <em>go tool</em> to convert it to a much better representation in HTML:</p>
|
||
<pre><code class="language-bash">go tool cover -html=cover.out -o cover.html
|
||
</code></pre>
|
||
<p>Opening this file in a browser reveals a lot of useful information:</p>
|
||
<p><img alt="Coverage HTML" src="/assets/images/coverage-html-31939a8031afbabb8f2d0af5c7476644.webp" width="1750" height="1778"></p>
|
||
<p>At the top left of the page there's a menu where we can select from all the files analyzed, listed along with the percentage of coverage for each one. Inside every file the lines covered by the tests are green while the red ones are not covered. In the example above we can see that the getAllAssets function is covered but it includes an <em>else</em> condition that is never met.</p>
|
||
<p>In gocontentful (starting from 1.0.18) we can generate the test API, run the tests with coverage, convert the output file and open it in the browser with a single command:</p>
|
||
<pre><code class="language-bash">make cover
|
||
</code></pre>
|
||
<p>As stated above, not necessarily 100% of the code needs to be covered by the tests, but this view in combination with the race detector gives us incredibly useful information to make the code more solid.</p></div><footer class="row docusaurus-mt-lg blogPostFooterDetailsFull_mRVl"><div class="col"><b>Tags:</b><ul class="tags_jXut padding--none margin-left--sm"><li class="tag_QGVx"><a class="tag_zVej tagRegular_sFm0" href="/blog/tags/golang">golang</a></li><li class="tag_QGVx"><a class="tag_zVej tagRegular_sFm0" href="/blog/tags/concurrency">concurrency</a></li><li class="tag_QGVx"><a class="tag_zVej tagRegular_sFm0" href="/blog/tags/testing">testing</a></li></ul></div><div class="col margin-top--sm"><a href="https://github.com/foomo/foomo-docs/tree/main/foomo/blog/2023-03-17-go-race-conditions-testing-and-coverage/index.mdx" target="_blank" rel="noopener noreferrer" class="theme-edit-this-page"><svg fill="currentColor" height="20" width="20" viewBox="0 0 40 40" class="iconEdit_Z9Sw" aria-hidden="true"><g><path d="m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z"></path></g></svg>Edit this page</a></div></footer></article><nav class="pagination-nav docusaurus-mt-lg" aria-label="Blog post page navigation"><a class="pagination-nav__link pagination-nav__link--next" href="/blog/accuracy-of-decimal-computations"><div class="pagination-nav__sublabel">Older Post</div><div class="pagination-nav__label">Accuracy of decimal computations</div></a></nav></main><div class="col col--2"><div class="tableOfContents_bqdL thin-scrollbar"><ul class="table-of-contents table-of-contents__left-border"><li><a href="#detecting-race-conditions-through-unit-tests" class="table-of-contents__link toc-highlight">Detecting race conditions through unit tests</a></li><li><a href="#test-coverage" class="table-of-contents__link toc-highlight">Test coverage</a></li></ul></div></div></div></div></div><footer class="footer"><div class="container container-fluid"><div class="row footer__links"><div class="col footer__col"><div class="footer__title">github</div><ul class="footer__items clean-list"><li class="footer__item"><a href="https://github.com/foomo" target="_blank" rel="noopener noreferrer" class="footer__link-item">https://github.com/foomo</a></li></ul></div><div class="col footer__col"><div class="footer__title">legal</div><ul class="footer__items clean-list"><li class="footer__item"><a class="footer__link-item" href="/etc/imprint">Imprint</a></li></ul></div></div><div class="footer__bottom text--center"><div class="footer__copyright">© 2024 bestbytes</div></div></div></footer></div>
|
||
</body>
|
||
</html> |