282 lines
14 KiB
HTML
282 lines
14 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css"
|
|
/>
|
|
<title>haunt98 posts</title>
|
|
</head>
|
|
<style>
|
|
.markdown-body {
|
|
box-sizing: border-box;
|
|
min-width: 200px;
|
|
max-width: 980px;
|
|
margin: 0 auto;
|
|
padding: 45px;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.markdown-body {
|
|
padding: 15px;
|
|
}
|
|
}
|
|
</style>
|
|
<body class="markdown-body">
|
|
<h2>
|
|
<a href="index.html"><code>~</code></a>
|
|
</h2>
|
|
<h1>
|
|
<a
|
|
id="user-content-another-day-another-incident-02"
|
|
class="anchor"
|
|
aria-hidden="true"
|
|
href="#another-day-another-incident-02"
|
|
><span aria-hidden="true" class="octicon octicon-link"></span></a
|
|
>Another day another incident #02
|
|
</h1>
|
|
<p>Today's incident is all about Go context.</p>
|
|
<p>TLDR: context got canceled, but it shouldn't.</p>
|
|
<h2>
|
|
<a
|
|
id="user-content-the-problem"
|
|
class="anchor"
|
|
aria-hidden="true"
|
|
href="#the-problem"
|
|
><span aria-hidden="true" class="octicon octicon-link"></span></a
|
|
>The problem
|
|
</h2>
|
|
<p>Imagine a chain of APIs:</p>
|
|
<ul>
|
|
<li>Calling API A</li>
|
|
<li>Calling API B</li>
|
|
</ul>
|
|
<p>
|
|
Normally, if API A fails, API B should not be called. But what if API A is
|
|
<strong>optional</strong>, whether it successes or fails, API B should be
|
|
called anyway.
|
|
</p>
|
|
<p>My buggy code is like this:</p>
|
|
<div class="highlight highlight-source-go">
|
|
<pre><span class="pl-k">if</span> <span class="pl-s1">err</span> <span class="pl-c1">:=</span> <span class="pl-en">doA</span>(<span class="pl-s1">ctx</span>); <span class="pl-s1">err</span> <span class="pl-c1">!=</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-s1">log</span>.<span class="pl-en">Error</span>(<span class="pl-s1">err</span>)
|
|
<span class="pl-c">// Skip error</span>
|
|
}
|
|
|
|
<span class="pl-en">doB</span>(<span class="pl-s1">ctx</span>)</pre>
|
|
</div>
|
|
<p>
|
|
The problem is <code>doA</code> taking too long, so <code>ctx</code> is
|
|
canceled, and the parent of <code>ctx</code> is canceled too. So when
|
|
<code>doB</code> is called with <code>ctx</code>, it will be canceled too
|
|
(not what we want but sadly that what we got).
|
|
</p>
|
|
<p>
|
|
Example buggy code (<a
|
|
href="https://go.dev/play/p/p4S27Su16VH"
|
|
rel="nofollow"
|
|
>The Go Playground</a
|
|
>):
|
|
</p>
|
|
<div class="highlight highlight-source-go">
|
|
<pre><span class="pl-k">func</span> <span class="pl-en">main</span>() {
|
|
<span class="pl-s1">ctx</span>, <span class="pl-s1">cancel</span> <span class="pl-c1">:=</span> <span class="pl-s1">context</span>.<span class="pl-en">WithTimeout</span>(<span class="pl-s1">context</span>.<span class="pl-en">Background</span>(), <span class="pl-c1">2</span><span class="pl-c1">*</span><span class="pl-s1">time</span>.<span class="pl-c1">Second</span>)
|
|
<span class="pl-k">defer</span> <span class="pl-en">cancel</span>()
|
|
|
|
<span class="pl-en">doA</span>(<span class="pl-s1">ctx</span>)
|
|
<span class="pl-en">doB</span>(<span class="pl-s1">ctx</span>)
|
|
}
|
|
|
|
<span class="pl-k">func</span> <span class="pl-en">doA</span>(<span class="pl-s1">ctx</span> context.<span class="pl-smi">Context</span>) {
|
|
<span class="pl-s1">ctx</span>, <span class="pl-s1">ctxCancel</span> <span class="pl-c1">:=</span> <span class="pl-s1">context</span>.<span class="pl-en">WithTimeout</span>(<span class="pl-s1">ctx</span>, <span class="pl-c1">1</span><span class="pl-c1">*</span><span class="pl-s1">time</span>.<span class="pl-c1">Second</span>)
|
|
<span class="pl-k">defer</span> <span class="pl-en">ctxCancel</span>()
|
|
|
|
<span class="pl-k">select</span> {
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">time</span>.<span class="pl-en">After</span>(<span class="pl-c1">2</span> <span class="pl-c1">*</span> <span class="pl-s1">time</span>.<span class="pl-c1">Second</span>):
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doA"</span>)
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">ctx</span>.<span class="pl-en">Done</span>():
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doA"</span>, <span class="pl-s1">ctx</span>.<span class="pl-en">Err</span>())
|
|
}
|
|
}
|
|
|
|
<span class="pl-k">func</span> <span class="pl-en">doB</span>(<span class="pl-s1">ctx</span> context.<span class="pl-smi">Context</span>) {
|
|
<span class="pl-s1">ctx</span>, <span class="pl-s1">ctxCancel</span> <span class="pl-c1">:=</span> <span class="pl-s1">context</span>.<span class="pl-en">WithTimeout</span>(<span class="pl-s1">ctx</span>, <span class="pl-c1">3</span><span class="pl-c1">*</span><span class="pl-s1">time</span>.<span class="pl-c1">Second</span>)
|
|
<span class="pl-k">defer</span> <span class="pl-en">ctxCancel</span>()
|
|
|
|
<span class="pl-k">select</span> {
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">time</span>.<span class="pl-en">After</span>(<span class="pl-c1">2</span> <span class="pl-c1">*</span> <span class="pl-s1">time</span>.<span class="pl-c1">Second</span>):
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doB"</span>)
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">ctx</span>.<span class="pl-en">Done</span>():
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doB"</span>, <span class="pl-s1">ctx</span>.<span class="pl-en">Err</span>())
|
|
}
|
|
}</pre>
|
|
</div>
|
|
<p>The output is:</p>
|
|
<div class="highlight highlight-text-adblock">
|
|
<pre>
|
|
doA context deadline exceeded
|
|
doB context deadline exceeded</pre
|
|
>
|
|
</div>
|
|
<p>As you see both <code>doA</code> and <code>doB</code> are canceled.</p>
|
|
<h2>
|
|
<a
|
|
id="user-content-the-temporary-solution"
|
|
class="anchor"
|
|
aria-hidden="true"
|
|
href="#the-temporary-solution"
|
|
><span aria-hidden="true" class="octicon octicon-link"></span></a
|
|
>The (temporary) solution
|
|
</h2>
|
|
<p>
|
|
Quick Google search leads me to
|
|
<a href="https://github.com/golang/go/issues/40221"
|
|
>context: add WithoutCancel #40221</a
|
|
>
|
|
and I quote:
|
|
</p>
|
|
<blockquote>
|
|
<p>
|
|
This is useful in multiple frequently recurring and important scenarios:
|
|
</p>
|
|
<ul>
|
|
<li>
|
|
Handling of rollback/cleanup operations in the context of an event
|
|
(e.g., HTTP request) that has to continue regardless of whether the
|
|
triggering event is canceled (e.g., due to timeout or the client going
|
|
away)
|
|
</li>
|
|
<li>
|
|
Handling of long-running operations triggered by an event (e.g., HTTP
|
|
request) that terminates before the termination of the long-running
|
|
operation
|
|
</li>
|
|
</ul>
|
|
</blockquote>
|
|
<p>
|
|
So beside waiting to upgrade to Go <code>1.21</code> to use
|
|
<code>context.WithoutCancel</code>, you can use this
|
|
<a href="https://pkg.go.dev/context@master#WithoutCancel" rel="nofollow"
|
|
>workaround code</a
|
|
>:
|
|
</p>
|
|
<div class="highlight highlight-source-go">
|
|
<pre><span class="pl-k">func</span> <span class="pl-en">DisconnectContext</span>(<span class="pl-s1">parent</span> context.<span class="pl-smi">Context</span>) context.<span class="pl-smi">Context</span> {
|
|
<span class="pl-k">if</span> <span class="pl-s1">parent</span> <span class="pl-c1">==</span> <span class="pl-c1">nil</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">context</span>.<span class="pl-en">Background</span>()
|
|
}
|
|
|
|
<span class="pl-k">return</span> <span class="pl-smi">disconnectedContext</span>{
|
|
<span class="pl-c1">parent</span>: <span class="pl-s1">parent</span>,
|
|
}
|
|
}
|
|
|
|
<span class="pl-k">type</span> <span class="pl-smi">disconnectedContext</span> <span class="pl-k">struct</span> {
|
|
<span class="pl-c1">parent</span> context.<span class="pl-smi">Context</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">ctx</span> <span class="pl-smi">disconnectedContext</span>) <span class="pl-en">Deadline</span>() (<span class="pl-s1">deadline</span> time.<span class="pl-smi">Time</span>, <span class="pl-s1">ok</span> <span class="pl-smi">bool</span>) {
|
|
<span class="pl-k">return</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">ctx</span> <span class="pl-smi">disconnectedContext</span>) <span class="pl-en">Done</span>() <span class="pl-c1"><-</span><span class="pl-k">chan</span> <span class="pl-k">struct</span>{} {
|
|
<span class="pl-k">return</span> <span class="pl-c1">nil</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">ctx</span> <span class="pl-smi">disconnectedContext</span>) <span class="pl-en">Err</span>() <span class="pl-smi">error</span> {
|
|
<span class="pl-k">return</span> <span class="pl-c1">nil</span>
|
|
}
|
|
|
|
<span class="pl-k">func</span> (<span class="pl-s1">ctx</span> <span class="pl-smi">disconnectedContext</span>) <span class="pl-en">Value</span>(<span class="pl-s1">key</span> <span class="pl-smi">any</span>) <span class="pl-smi">any</span> {
|
|
<span class="pl-k">return</span> <span class="pl-s1">ctx</span>.<span class="pl-c1">parent</span>.<span class="pl-en">Value</span>(<span class="pl-s1">key</span>)
|
|
}</pre>
|
|
</div>
|
|
<p>
|
|
So the buggy code becomes (<a
|
|
href="https://go.dev/play/p/oIU-WxEJ_F3"
|
|
rel="nofollow"
|
|
>The Go Playground</a
|
|
>):
|
|
</p>
|
|
<div class="highlight highlight-source-go">
|
|
<pre><span class="pl-k">func</span> <span class="pl-en">main</span>() {
|
|
<span class="pl-s1">ctx</span>, <span class="pl-s1">cancel</span> <span class="pl-c1">:=</span> <span class="pl-s1">context</span>.<span class="pl-en">WithTimeout</span>(<span class="pl-s1">context</span>.<span class="pl-en">Background</span>(), <span class="pl-c1">2</span><span class="pl-c1">*</span><span class="pl-s1">time</span>.<span class="pl-c1">Second</span>)
|
|
<span class="pl-k">defer</span> <span class="pl-en">cancel</span>()
|
|
<span class="pl-en">doA</span>(<span class="pl-s1">ctx</span>)
|
|
<span class="pl-en">doB</span>(<span class="pl-s1">ctx</span>)
|
|
}
|
|
|
|
<span class="pl-k">func</span> <span class="pl-en">doA</span>(<span class="pl-s1">ctx</span> context.<span class="pl-smi">Context</span>) {
|
|
<span class="pl-s1">ctx</span>, <span class="pl-s1">ctxCancel</span> <span class="pl-c1">:=</span> <span class="pl-s1">context</span>.<span class="pl-en">WithTimeout</span>(<span class="pl-s1">ctx</span>, <span class="pl-c1">1</span><span class="pl-c1">*</span><span class="pl-s1">time</span>.<span class="pl-c1">Second</span>)
|
|
<span class="pl-k">defer</span> <span class="pl-en">ctxCancel</span>()
|
|
|
|
<span class="pl-k">select</span> {
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">time</span>.<span class="pl-en">After</span>(<span class="pl-c1">2</span> <span class="pl-c1">*</span> <span class="pl-s1">time</span>.<span class="pl-c1">Second</span>):
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doA"</span>)
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">ctx</span>.<span class="pl-en">Done</span>():
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doA"</span>, <span class="pl-s1">ctx</span>.<span class="pl-en">Err</span>())
|
|
}
|
|
}
|
|
|
|
<span class="pl-k">func</span> <span class="pl-en">doB</span>(<span class="pl-s1">ctx</span> context.<span class="pl-smi">Context</span>) {
|
|
<span class="pl-s1">ctx</span>, <span class="pl-s1">ctxCancel</span> <span class="pl-c1">:=</span> <span class="pl-s1">context</span>.<span class="pl-en">WithTimeout</span>(<span class="pl-en">DisconnectContext</span>(<span class="pl-s1">ctx</span>), <span class="pl-c1">3</span><span class="pl-c1">*</span><span class="pl-s1">time</span>.<span class="pl-c1">Second</span>)
|
|
<span class="pl-k">defer</span> <span class="pl-en">ctxCancel</span>()
|
|
|
|
<span class="pl-k">select</span> {
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">time</span>.<span class="pl-en">After</span>(<span class="pl-c1">2</span> <span class="pl-c1">*</span> <span class="pl-s1">time</span>.<span class="pl-c1">Second</span>):
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doB"</span>)
|
|
<span class="pl-k">case</span> <span class="pl-c1"><-</span><span class="pl-s1">ctx</span>.<span class="pl-en">Done</span>():
|
|
<span class="pl-s1">fmt</span>.<span class="pl-en">Println</span>(<span class="pl-s">"doB"</span>, <span class="pl-s1">ctx</span>.<span class="pl-en">Err</span>())
|
|
}
|
|
}</pre>
|
|
</div>
|
|
<p>The output is:</p>
|
|
<div class="highlight highlight-text-adblock">
|
|
<pre>
|
|
doA context deadline exceeded
|
|
doB</pre
|
|
>
|
|
</div>
|
|
<p>
|
|
As you see only <code>doA</code> is canceled, <code>doB</code> is done
|
|
perfectly. And that what we want in this case.
|
|
</p>
|
|
<h2>
|
|
<a
|
|
id="user-content-thanks"
|
|
class="anchor"
|
|
aria-hidden="true"
|
|
href="#thanks"
|
|
><span aria-hidden="true" class="octicon octicon-link"></span></a
|
|
>Thanks
|
|
</h2>
|
|
<ul>
|
|
<li>
|
|
<a
|
|
href="https://www.sohamkamani.com/golang/context-cancellation-and-values/"
|
|
rel="nofollow"
|
|
>Using Context in Golang - Cancellation, Timeouts and Values (With
|
|
Examples)</a
|
|
>
|
|
</li>
|
|
<li>
|
|
<a
|
|
href="https://uptrace.dev/blog/golang-context-timeout.html"
|
|
rel="nofollow"
|
|
>Go Context timeouts can be harmful</a
|
|
>
|
|
</li>
|
|
</ul>
|
|
|
|
<div>
|
|
Feel free to ask me via
|
|
<a href="mailto:hauvipapro+posts@gmail.com">email</a> or
|
|
<a rel="me" href="https://hachyderm.io/@haunguyen">Mastodon</a>. Source
|
|
code is available on
|
|
<a href="https://github.com/haunt98/posts-go">GitHub</a>
|
|
</div>
|
|
</body>
|
|
</html>
|