<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Sean Yu on Medium]]></title>
        <description><![CDATA[Stories by Sean Yu on Medium]]></description>
        <link>https://medium.com/@seanyu.dev?source=rss-72b7889bc704------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*16YWheZ_2JfH7lGd</url>
            <title>Stories by Sean Yu on Medium</title>
            <link>https://medium.com/@seanyu.dev?source=rss-72b7889bc704------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 16:29:26 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@seanyu.dev/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Distributed Tracing for Message Broker Subscribers]]></title>
            <link>https://medium.com/xendit-engineering/distributed-tracing-for-message-broker-subscribers-f6d5628be181?source=rss-72b7889bc704------2</link>
            <guid isPermaLink="false">https://medium.com/p/f6d5628be181</guid>
            <category><![CDATA[distributed-tracing]]></category>
            <category><![CDATA[sqs]]></category>
            <category><![CDATA[datadog]]></category>
            <category><![CDATA[kafka]]></category>
            <category><![CDATA[rabbitmq]]></category>
            <dc:creator><![CDATA[Sean Yu]]></dc:creator>
            <pubDate>Mon, 13 Feb 2023 03:40:23 GMT</pubDate>
            <atom:updated>2023-02-14T15:07:32.606Z</atom:updated>
            <content:encoded><![CDATA[<blockquote>This is a short tutorial on how to add end to end distributed tracing manually to your publisher subscriber. Here we use Datadog, Go or Node and RabbitMQ, but the concepts are applicable even using other distributed tracing tooling, prog language or message broker (e.g. Kafka, SQS and more)</blockquote><p><a href="https://seanyu.space/posts/distributed-tracing-for-message-broker-subscribers">Published also in my personal site</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oS9kEz_JN5OL4k6l0Iqp9w.png" /><figcaption>Debugging Together with End-to-End Distributed Tracing</figcaption></figure><h3>Problem</h3><p>Imagine you have a service that lets a user upload a picture then your service asynchronously creates the thumbnail through a worker. The architecture may look something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Eaz-rUrB5_iBGVEFCW1N7A.png" /><figcaption>Architecture</figcaption></figure><p>You have a <strong>web server to process the HTTP request,</strong> but offloads the creation of thumbnails to a <strong>worker</strong> through a message broker (e.g. Kafka, RabbitMQ, or SQS)</p><p>You want to have visibility, so you add distributed tracing to the web server, so you get this.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9swCiaIKNiee0p5rK3aW3A.png" /><figcaption>Sample Trace</figcaption></figure><p><strong>But Wait! Where is the worker that processes the thumbnails? We have no visibility for it.</strong></p><p>This is the problem we faced in Xendit. Way back we had no visibility on our RabbitMQ Queue workers/subscribers.</p><p>Ideally, we want to see the asynchronous processing in the same trace. So we can see the cause and effect of processes in different services. This enables us to debug problems like race conditions and performance bottlenecks between different processes/workers easier and faster.</p><p>So,</p><blockquote>How do we have a trace that spans to two services when using a message broker?</blockquote><h3>Solution</h3><p>Before diving into the solution implementation, we need to understand one concept in tracing called Context Propagation</p><h4>Context Propagation</h4><p>Context Propagation is the mechanism used by distributed tracing to have a trace that spans multiple services. The basic idea behind context propagation is to pass some metadata from the “source service (e.g. web server)” to the “downstream service (e.g. subscriber, queue worker, stream processing)”. The “<strong>context metadata</strong> payload” will contain a trace_id and a parentspan_id in Datadog’s case.</p><pre>// Simplified metadata in json<br>{<br>   &quot;traceID&quot;  : &quot;&lt;some 64 bit unsigned int&gt;&quot;<br>   &quot;parentID&quot; : &quot;&lt;some 64 bit unsigned int&gt;&quot;<br>}</pre><blockquote>Disclaimer: Different standards have different formats. For example, Datadog uses 64 bit unsigned int, while OpenTelemetry uses 128 bit unsigned int. There is also a w3c standard <a href="https://www.w3.org/TR/trace-context/">https://www.w3.org/TR/trace-context/</a></blockquote><p>The <strong>context metadata </strong>will be used or <strong>extracted</strong> by the downstream service before it starts its own trace. The downstream service ends up generating a span that is a child of the source service span. This process can repeat when it calls another service.</p><p>At the end of the trace, every span is then associated to a specific trace_id . Your distributed tracing provider (e.g. Datadog) or processor will process this and can now show a trace visualization.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xXXO2hAWzVBpJmnNB1DY-w.png" /><figcaption>Context Propagation</figcaption></figure><h3>HTTP Example</h3><p>To give a concrete example, if communication between service A and service B is through HTTP, then the <strong>medium</strong> to pass the <strong>context metadata</strong> is through the HTTP headers. Something like</p><pre>HTTP Headers:<br>x-datadog-trace-id: &lt;some 64 bit unsigned int&gt;<br>x-datadog-parent-id: &lt;some 64 bit unsigned int&gt;</pre><p>This can now be used by service B, with sample implementation in Golang using echo and Datadog shown below:</p><pre>// This is a simplified version of: https://github.com/DataDog/dd-trace-go/blob/649fa0c46073774aa26429999b942eeb2841b866/contrib/internal/httptrace/httptrace.go#L26<br>func StartRequestSpan(r *http.Request) (tracer.Span, context.Context) {<br><br> // Extract the context metadata from request header<br>  // Here HTTPHeadersCarrier is a helper type to help extract and inject trace context metadata<br> if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil {<br>  opts = append(opts, tracer.ChildOf(spanctx))<br> }<br> return tracer.StartSpanFromContext(r.Context(), &quot;http.request&quot;)<br><br>}<br><br>// Sample echo<br><br>func main() {<br> e := echo.New()<br> e.GET(&quot;/&quot;, func(c echo.Context) error {<br>  span, ctx := StartRequestSpan(c.Request()) // use the request span<br>    defer span.Finish() // finish span<br>  status := http.StatusOK<br>  message := &quot;ok&quot;<br>  return c.JSON(status, response{message})<br> })<br>}</pre><p>In Node using express and Datadog,</p><pre>// Simplified version: https://github.com/DataDog/dd-trace-js/blob/b9121d8fc4e12819e89c41deb12ad203983f25a3/packages/dd-trace/src/plugins/util/web.js#L250-L256<br>function traceRequestSpan(tracer, headers) {<br>  // Extract<br>  const childOf = tracer.scope().active() || tracer.extract(FORMAT_HTTP_HEADERS, headers)<br>  <br>  const span = tracer.startSpan(name, { childOf })<br>  return span<br>}<br><br><br>const app = express()<br><br>app.get(&#39;/&#39;, (req, res) =&gt; {<br> const span = traceRequestSpan(tracer, req.headers)<br> res.status(200).send()<br> span.finish()<br>})</pre><h3>Implementation</h3><p>Now, we know how context propagation works in HTTP.</p><p>We can simply apply this in a different transport medium such as a RabbitMQ Message, Kafka Record, and SQS Message.</p><p><strong>Attach it to the message’s headers.</strong></p><p>Distilling it into the basic steps:</p><ol><li>Before publishing a message in service A, get the context metadata for the trace</li><li>Attach the context metadata to the “headers” of the message (e.g. RMQ headers, Kafka Headers, SQS Headers)</li><li>Publish the message to the message broker</li><li>When service B receives/consumes the message, read the headers and check for context metadata</li><li>If there is context metadata, use context metadata to start a trace</li></ol><p>Below is how it can be done in Go and Node for AMQP</p><h3>Go</h3><p>Publisher (e.g. web/HTTP server) will have:</p><pre>// Publisher.go<br><br>import (<br> &quot;gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer&quot;<br>)<br><br>func (amqpPublisher *AMQPPublisher) PublishMessage(<br> ctx context.Context,<br> topic string,<br> headers Headers,<br> content interface{}<br>) (error) {<br> span, ctx := StartPublishSpan(ctx, headers, topic)<br>  err := amqpPublisher.Publish( // some args )<br> if err != nil {<br>  span.Finish(tracer.Error(err))<br>    return err<br> }<br> return nil<br>}<br><br>type Headers map[string]interface{}<br><br>var _ interface {<br> tracer.TextMapReader<br> tracer.TextMapWriter<br>} = (*Headers)(nil)<br><br>// Set implements TextMapWriter for Datadog Trace Extraction<br>func (headers Headers) Set(key, val string) {<br> headers[key] = val<br>}<br><br>// ForeachKey conforms to the TextMapReader for Datadog Trace Extraction<br>func (headers Headers) ForeachKey(handler func(key, val string) error) error {<br> for k, v := range headers {<br>  str, _ := v.(string)<br>  if err := handler(k, str); err != nil {<br>   return err<br>  }<br> }<br><br> return nil<br>}<br><br><br>func StartPublishSpan(<br> ctx context.Context,<br> headers Headers,<br> resourceName string,<br> opts ...tracer.StartSpanOption,<br>) (tracer.Span, context.Context) {<br> span, ok := tracer.SpanFromContext(ctx)<br> if ok {<br>  // Inject the context metadata. Headers need to &quot;implement&quot; TextMapReader TextMapWriter<br>  if err := tracer.Inject(span.Context(), headers); err != nil {<br>   log.Printf(&quot;Failed to inject span context to headers: %v&quot;, err)<br>  }<br> }<br><br> return tracer.StartSpanFromContext(ctx, &quot;amqp.publish&quot;, tracer.ResourceName(resourceName))<br>}<br><br></pre><p>Subscriber (e.g. queue worker or stream processor) will have:</p><pre>// Subscriber.go<br><br>import (<br> &quot;gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer&quot;<br>)<br><br>func StartConsumeHandlerSpan(<br> ctx context.Context,<br> headers Headers,<br> topic string,<br>) (context.Context, tracer.Span) {<br> // Same logic. Extract context metadata. &quot;Extract&quot; returns a SpanContext containing the metadata<br> spanCtx, err := ddtracer.Extract(headers)<br> if err != nil {<br>  log.Print(&quot;Failed to extract span context from headers&quot;)<br> }<br><br> opts := []ddtracer.StartSpanOption{<br>  ddtracer.ResourceName(topic),<br>  ddtracer.SpanType(ext.SpanTypeMessageConsumer),<br> }<br> if spanCtx != nil {<br>  // Use span context <br>  opts = append(opts, ddtracer.ChildOf(spanCtx))<br> }<br><br> ddspan := ddtracer.StartSpan(<br>  &quot;amqp.consumeHandler&quot;,<br>  opts...,<br> )<br><br> span := ddtracer.NewSpan(ddspan)<br> return ddtracer.ContextWithSpan(ctx, ddspan), span<br>}<br><br>func (amqpSubscriber *AMQPSubscriber) MessageHandler(<br> ctx context.Context,<br>  message Message,<br>) (error) {<br> span, ctx := StartConsumeHandlerSpan(ctx, message.Headers, message.Topic)<br>  // Insert: Processing business logic <br>  // ....<br>  // ....<br> if err != nil {<br>  span.Finish(tracer.Error(err))<br>    return err<br> }<br> return nil<br><br>}</pre><h3>Node</h3><p>Publisher (e.g. web/http server) will have:</p><pre>class Publisher {<br>   public async publish(message: Message): Promise&lt;void&gt; {<br>     const span = this.tracer.startSpan(&#39;amqp.publish&#39;)<br>     this.tracer.inject(span, &#39;text_map&#39;, message.headers)<br>     await amqplib.publish(message)<br>     span.finish()<br>    }<br>}</pre><p>Subscriber (e.g. queue worker or stream processor) will have:</p><pre>class Subscriber {<br> public async messageHandler(message: Message): Promise&lt;void&gt; {<br>  const headers = msg?.properties?.headers<br>  const childOf = this.tracer.extract(&#39;text_map&#39;, headers);<br>    await this.tracer.trace(<br>      `amqp.consumeHandler`,<br>      {<br>        ...(childOf ? { childOf } : {}),<br>        tags: {<br>          span: {<br>            kind: &#39;consumer&#39;<br>          },<br>        }<br>      },<br>      () =&gt; {<br>     // Insert: Processing business logic <br>     // ....<br>     // ....<br>      }<br>    );<br>  }<br>}</pre><p>What you end up having is this if you fully implement the above:</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FCNXpDzyUrI4%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DCNXpDzyUrI4&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FCNXpDzyUrI4%2Fhqdefault.jpg&amp;key=a19fcc184b9711e1b4764040d3dc5c07&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/751a1046897b014cbe102dd1ffad4cac/href">https://medium.com/media/751a1046897b014cbe102dd1ffad4cac/href</a></iframe><p>As you can see there is an amqp.consumeHandler that happens after the http.request. . There is also a amqp.publish span to show the publishing process. Now, you can easily debug if there is a problem in the publishing operation and most importantly in the async processing part.</p><h3>Conclusion</h3><p>Adding <a href="https://docs.datadoghq.com/tracing/trace_collection/custom_instrumentation/">Custom Instrumentation</a> (aka adding Distributed Tracing manually) to your system, isn’t that difficult if you understand <strong>context propagation</strong>, your distributed tracing provider’s lib, and especially your system components. You can add distributed tracing to any component you programmatically build.</p><p>For Xendit, having End to End Distributed Tracing even in our async processes increased engineering teams&#39; confidence in deploying reliable consumers/subscribers. We found issues before customers and fixed the root cause faster during high-pressure and difficult fires.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f6d5628be181" width="1" height="1" alt=""><hr><p><a href="https://medium.com/xendit-engineering/distributed-tracing-for-message-broker-subscribers-f6d5628be181">Distributed Tracing for Message Broker Subscribers</a> was originally published in <a href="https://medium.com/xendit-engineering">Xendit Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stop and Go — Regulating goroutines with a traffic light]]></title>
            <link>https://medium.com/xendit-engineering/stop-and-go-regulating-goroutines-with-a-traffic-light-2e416d349b1e?source=rss-72b7889bc704------2</link>
            <guid isPermaLink="false">https://medium.com/p/2e416d349b1e</guid>
            <category><![CDATA[goroutines]]></category>
            <category><![CDATA[scalability]]></category>
            <category><![CDATA[go]]></category>
            <category><![CDATA[concurrency]]></category>
            <category><![CDATA[go-channels]]></category>
            <dc:creator><![CDATA[Sean Yu]]></dc:creator>
            <pubDate>Tue, 08 Nov 2022 01:01:37 GMT</pubDate>
            <atom:updated>2023-01-29T14:15:49.276Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Stop and Go — Regulating goroutines with a traffic light</strong></h3><blockquote><em>In Go, you can control the number of concurrent goroutines running based on your needs by using a buffered channel as a semaphore</em></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*48aVAfve_SwN7Ijm3cHxJw.jpeg" /></figure><p><a href="https://seanyu.space/posts/regulating-goroutines-with-a-traffic-light">Published also in my blog</a></p><h3>Context: Batch Jobs</h3><p>In Xendit, there are a ton of cron jobs running to process batch work in a deterministic interval fashion. One of the cron job’s work is to synchronize payment statuses, successful or not, by doing an HTTP call to our bank partners. Simplifying for this purpose, the code goes like this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8dc8f1ac68fa9c09353038ca34e5a7ee/href">https://medium.com/media/8dc8f1ac68fa9c09353038ca34e5a7ee/href</a></iframe><h3>Problem: Scalability Issue</h3><p>One morning, our team got alerted that some payments were not being synchronized in time. Some of my colleagues found out that it was due to the cron job reaching its set maximum duration to run. In this case, it was reaching the set <strong>activeDeadlineSeconds</strong> for k8s CronJob resource. Digging deeper, we discovered that the delay was<strong> </strong>caused by an influx of payments as it was payday and a sudden spike of latency from the third-party service API.</p><blockquote><strong><em>In short, It was an unexpected scalability issue on our end given the higher volume and spike in latency of third party services.</em></strong></blockquote><h3>Naive Solution</h3><p>First, naive attempt was to increase the <strong>activeDeadlineSeconds</strong> / max running duration of our cron job, which would cause longer latency for payments to be synchronized, which might impact user experience.</p><p>A naive solution was to spin up as much goroutine as needed to increase scalability and maximize our server resources. It goes something like below. Just fire as much goroutine as needed 🚀</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/75a208294563a6f0968ba4d21e97411a/href">https://medium.com/media/75a208294563a6f0968ba4d21e97411a/href</a></iframe><p>The above has the following flaws:</p><ol><li>Our code above <strong>does not wait for all work/goroutines to be done</strong> before exiting the process</li><li>Our code above <strong>does not consider the load capacity of downstream services</strong>. We have no control over 3rd party’s maximum server capacity, so we have to control the load we send to them.</li></ol><h3>Guide Questions</h3><p>Let’s convert the flaws to these questions to give us a compass in implementing a solution:</p><ol><li>How do we wait for all goroutines to be finished before the process exits?</li><li>How do we limit the number of concurrent goroutines/requests running?</li></ol><h3>Question 1: How do we wait for all goroutines to be finished before the process exits?</h3><p>A quick answer to this is using wg.WaitGroup . It is a very convenient utility to <strong>“wait” given a number of “tasks” needed to be done</strong>. Let’s quickly see below:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fe12fefe0e4fc740312aaaf1e051f71e/href">https://medium.com/media/fe12fefe0e4fc740312aaaf1e051f71e/href</a></iframe><p>Done!</p><p>If you are up for a challenge, you can also use go channels to do so. 👀 See the end of the post to see how</p><h3>Question 2: Limiting # of running concurrent goroutines</h3><p>There is an abstract data type called semaphore in concurrent programming we can use. Quoting from Wikipedia:</p><blockquote>Semaphore is a variable or abstract data type used to control access to a common resource by multiple processes in a concurrent system such as a multitasking operating system.</blockquote><p>“Multiple processes” in this case are concurrently running goroutines. A semaphore can be a common resource for goroutines to refer to to behave in some desired fashion based on our needs. For this case, we can use a semaphore to limit the number of goroutines doing “active work” by controlling when a specific goroutine should do “active work”. Active work in this case is calling downstream services and doing updates to the payment</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/a388a393b3b59e14939b5c3e18257703/href">https://medium.com/media/a388a393b3b59e14939b5c3e18257703/href</a></iframe><p>Let’s say we want only 10 max concurrent requests, but we have 11 payments. We spin up a goroutine for each payment. What a semaphore needs to do is to not let the 11th goroutine do its work yet, until one goroutine finishes.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1438ca6dc0a92ec28d53ef043315d664/href">https://medium.com/media/1438ca6dc0a92ec28d53ef043315d664/href</a></iframe><p>We can use go’s <strong>buffered channel</strong> as a semaphore. We leverage a buffered channel’s characteristic of being “blocking” on the sender’s end when the channel reaches its max length or capacity. Example:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/5313d266bb16352a183b9bd00da4b0a3/href">https://medium.com/media/5313d266bb16352a183b9bd00da4b0a3/href</a></iframe><p>We can then modify our code to the below. I marked new lines of code with NEW:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/07e93745bfa3c389af177af055174e4f/href">https://medium.com/media/07e93745bfa3c389af177af055174e4f/href</a></iframe><h3>Bonus: Implementing it as a Generic Function</h3><p>To solidify our understanding of this pattern, we can try to create a “generic function” for learning purposes or for re-use if you think that’s best for your team and codebase.</p><p>Here is the code. You can run code below in <a href="https://go.dev/play/p/oOk489XFQBk">https://go.dev/play/p/oOk489XFQBk</a></p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f663dd02505675645703820ddb80e5ba/href">https://medium.com/media/f663dd02505675645703820ddb80e5ba/href</a></iframe><h3>Question 1 Challenge: Using go channels to wait for</h3><p>We can use a go channel as a medium for communication from the running goroutines to the main function. A goroutine sends a signal to the channel when it’s done processing its work. main the function waits for X signals based on the number of works before it exists.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f7aee7f4fae0dba099a21c8125b131d3/href">https://medium.com/media/f7aee7f4fae0dba099a21c8125b131d3/href</a></iframe><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2e416d349b1e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/xendit-engineering/stop-and-go-regulating-goroutines-with-a-traffic-light-2e416d349b1e">Stop and Go — Regulating goroutines with a traffic light</a> was originally published in <a href="https://medium.com/xendit-engineering">Xendit Engineering</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>