<?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"><channel><title><![CDATA[Aleks Obukhov]]></title><description><![CDATA[My name is Aleksandr, and I am an engineer and technology enthusiast based in Berlin. I work as an engineering manager. My passion is to make people’s lives eas]]></description><link>https://blog.dclg.net</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 13:13:18 GMT</lastBuildDate><atom:link href="https://blog.dclg.net/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Importance of company culture, values and principles and example of Lingualeo]]></title><description><![CDATA[With this post, I would like to start a series of posts analyzing the way different companies define and declare their culture.
They say, "Culture eats strategy for breakfast." A well-established culture truly is a powerful tool that helps to keep th...]]></description><link>https://blog.dclg.net/importance-of-company-culture-values-and-principles-and-example-of-lingualeo</link><guid isPermaLink="true">https://blog.dclg.net/importance-of-company-culture-values-and-principles-and-example-of-lingualeo</guid><category><![CDATA[Culture]]></category><category><![CDATA[Engineering culture]]></category><category><![CDATA[values]]></category><category><![CDATA[principles]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Tue, 13 Feb 2024 00:44:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/aO_jMXTduUE/upload/0af187eaf236e7e667021cd4c5ab1f92.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With this post, I would like to start a series of posts analyzing the way different companies define and declare their culture.</p>
<p>They say, "Culture eats strategy for breakfast." A well-established culture truly is a powerful tool that helps to keep the way various challenges are addressed consistent, increases autonomy, and saves time in making decisions. I'm saying "well-established" because culture exists regardless of whether you put effort into shaping it or not. Same as if you don't have a dress code, people won't come to the office naked; if there is no guidance on company culture, they will contribute with their mindset to an overall company culture.</p>
<p>In small companies, alignment happens naturally. When everyone talks to everyone any mindset differences are surfaced and discussed early. When forming a team, people tend to find similar-minded peers. And if it happens that they don't share the same mindset and cannot align on it, they don't survive long together.</p>
<p>When a company grows, it becomes gradually harder to maintain a consistent mindset across loosely coupled parts of the bigger organization. In cases of hypergrowth, company culture can be considerably diluted as new joiners do not always have a chance to work with culture carriers and can even form their own culture in a newly created department.</p>
<p>To handle this problem, some kind of written culture manifestation is created. It is quite often called "<strong>values and principles</strong>". It may also be published in the shape of an onboarding material (e.g., <a target="_blank" href="https://www.valvesoftware.com/en/publications">Valve's Handbook for New Employees</a>) and literally can be called "manifest" (e.g., <a target="_blank" href="https://www.valvesoftware.com/en/publications">Agile Manifesto</a> - it is not a company's artifact, though it is a set of values and principles for particular software development culture).</p>
<p>Such a document aims to provide high-level guidance without detailed instructions for any possible scenario. It is also often publicly available and used as company identity material and advertising / setting expectations for the new hires. This gives an excellent opportunity to analyze and compare different formats, focus, and essences in how different companies define them.</p>
<p>The document usually (though not always) is very concise and follows a pyramid structure, starting with the highest abstraction level of "values" and building on top of that into more details, defining principles, or even specific rules. It is common to define either of those levels as a set of rules of precedence (smth. over smth.). The classic example from <a target="_blank" href="https://agilemanifesto.org/">Agile Manifesto</a>: "<em>Value individuals and interactions over processes and tools</em>".</p>
<h2 id="heading-example-of-lingualeo">Example of Lingualeo</h2>
<p>I wanted to start with this example for two reasons:</p>
<ol>
<li><p>This is the first company I've faced explicitly formulated culture</p>
</li>
<li><p>The way it was defined was very short, which makes it an easy start for such a blog post format</p>
</li>
</ol>
<p>Lingualeo Values, in their first version, were defined as three different aspects of human interaction sorted in the priority order:</p>
<ul>
<li><p>Respect</p>
</li>
<li><p>Trust</p>
</li>
<li><p>Support</p>
</li>
</ul>
<p>I remember there were more detailed documents describing it. But I don't have any at this point, so I'll do my best to explain those principles and their motivation in my own words.</p>
<h3 id="heading-respect">Respect</h3>
<p>Team members at Lingualeo base their interactions on the principle of mutual respect. We can disagree, but we care to express our disagreement in a respectful way. We also value each other's efforts in growing the product and extending its functionality.</p>
<p><strong>Why does it matter?</strong> The team was built of very young and motivated people, so we quite often went into very heated discussions about things. Without a principle of mutual respect, those discussions could easily grow into conflicts and develop into long-term tension between team members, which would be a burden that would slow us down to complete paralysis.</p>
<h3 id="heading-trust">Trust</h3>
<p>We trust each other in the way we do work and make decisions.</p>
<p><strong>Why does it matter?</strong> Trust is a foundation of delegation and autonomy. If you can't trust your peer, you would have constant overhead to cross-check and doubt the decisions before their impact can be measured.</p>
<h3 id="heading-support">Support</h3>
<p>We support each other and help to achieve goals by resolving dependencies in a timely manner.</p>
<p><strong>Why does it matter?</strong> Even in the early days, we were 2-3 teams. Each of the teams took care of the specific area of the product. Sharing a common codebase, we had dependencies, and without being able to resolve them, we couldn't advance in our own goals.</p>
<h2 id="heading-summary">Summary</h2>
<p>This is the most compact value system (company culture definition) I have ever worked with, and of course, the way we worked was not defined only by these three principles. But I like the fact that being short, it focuses on human aspects that can have a significant influence on the way people interact within an organization and the achievements of the team. The culture remained mostly unchanged for the 3.5 years I worked at that company during its growth from 10 to about 100 people, acquiring about 15 Million users worldwide.</p>
]]></content:encoded></item><item><title><![CDATA[Organizing your team's knowledge with PARA]]></title><description><![CDATA[Being an Engineering manager of a team often means assisting the team in organizing its ecosystem and tooling. I mean things like product and process metrics, CI/CD, productivity tools, and maintaining team's knowledge base and documentation.
In this...]]></description><link>https://blog.dclg.net/organizing-your-teams-knowledge-with-para</link><guid isPermaLink="true">https://blog.dclg.net/organizing-your-teams-knowledge-with-para</guid><category><![CDATA[KnowledgeManagement]]></category><category><![CDATA[team collaboration]]></category><category><![CDATA[documentation]]></category><category><![CDATA[notion]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Mon, 01 May 2023 07:07:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Oaqk7qqNh_c/upload/07078995be1446a8bd8986f7d3ae40fb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Being an Engineering manager of a team often means assisting the team in organizing its ecosystem and tooling. I mean things like product and process metrics, CI/CD, productivity tools, and maintaining team's knowledge base and documentation.</p>
<p>In this post, I would like to share how PARA method can be used to structure team's knowledge and documentation. Although it can be used with any wiki platform, we use notion, so examples and screenshots will be from there.</p>
<h2 id="heading-what-is-para-method">What is "PARA" method</h2>
<p>The PARA method is a method to organize digital life (knowledge, notes, etc.) in external storage (google drive, folders on your computer, note-keeping apps). It is authored by Tiago Forte and covered in his book "Building a Second Brain".</p>
<p>Essentially it suggests organizing your knowledge into four categories:</p>
<p><strong>P - Projects</strong>. Short-term efforts in your work or life that you’re working on now.</p>
<p><strong>A - Areas</strong>. Long-term responsibilities or ongoing activities with no set deadline.</p>
<p><strong>R - Resources.</strong> This section holds the information you would like to keep and refer to in the future.</p>
<p><strong>A - Archive.</strong> The category where you'll put items from the other three categories that have been completed or are no longer active.</p>
<h2 id="heading-how-to-apply-para-method-to-structure-team-space">How to apply PARA method to structure team space</h2>
<p>Let's use PARA categories as sections of the Team's home in the org's wiki.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682169438537/5ca2fbde-1d86-4194-9983-f5ad5e51a90a.png" alt="Screenshot of notion team home page" class="image--center mx-auto" /></p>
<h3 id="heading-projects">Projects</h3>
<p><strong>"Projects"</strong> section naturally fits as a root of your recent, current, and future projects. You can use different terminology and call them <em>initiatives</em>, <em>roadmap items</em>, or <em>epics</em>. Still, they should fit the classic project management definition: have a defined beginning and end, a specific scope of work, and a set of objectives to be achieved.</p>
<p>Projects can have a product or feature delivery as an objective or be internal ones aiming to improve processes within the team. For example, introducing DORA metrics or adopting a new tool in the team.</p>
<p>In this section, it makes sense to sort items in reverse chronological order (newest to oldest). Each subpage here is just a root holding references to relevant information, such as:</p>
<ul>
<li><p>Project meta information:</p>
<ul>
<li><p>Project definition in the project database</p>
</li>
<li><p>Project metrics and success criteria</p>
</li>
<li><p>Project roles (i.e. Stakeholder, DACI / RACI, or another framework you use)</p>
</li>
<li><p>Links to other systems (Jira epic, Miro project, Figma folder, etc.)</p>
</li>
</ul>
</li>
<li><p>Ongoing project documentation:</p>
<ul>
<li><p>Architecture decision records (ADRs)</p>
</li>
<li><p>Brainstorms or other workshops protocols</p>
</li>
<li><p>Meeting notes</p>
</li>
</ul>
</li>
<li><p>Feature or technical documentation:</p>
<ul>
<li><p>User stories</p>
</li>
<li><p>Flow / sequence / ER or other kind of diagrams</p>
</li>
<li><p>Runbooks for engineers or support specialists</p>
</li>
<li><p>Tracking schema, service catalog, etc.</p>
</li>
</ul>
</li>
</ul>
<p>Organizing all relevant documentation under one root makes it easier to find it throughout the project's lifetime.</p>
<h3 id="heading-areas">Areas</h3>
<p><strong>The "areas"</strong> section can hold projects or business domains which are not in active development but require maintenance. Not every piece of ongoing project documentation is moved here once product goes into maintenance, only some long-term ones, like features description, runbooks, key ADRs, etc., would be helpful here.</p>
<p>You don't move projects to areas once they are complete. You rather distill long-term documentation from <strong><em>projects</em></strong> to <strong><em>areas</em></strong>. In fact, areas can have a broader scope than a project. For example, an <strong><em>area</em></strong> can be "<strong>paid subscription management"</strong>, but a <strong><em>project</em></strong> belonging to this area can be "<strong>introducing a new payment provider X</strong>".</p>
<p>In addition to product domains, "areas" section can represent your engineering practices. If you perform post-mortems as part of your incident response process, it's a good idea to put them here. Your engineering or quality strategy, your productivity practices, and your team-building events - can all continuously be tracked in subpages of this section.</p>
<p>These subpages are better to be sorted alphabetically, or from most used to rarely used. If there are too many of them, it may make sense to introduce logical groups.</p>
<h3 id="heading-resources">Resources</h3>
<p>This section is perfect for the information you want to keep up to date for reference:</p>
<ul>
<li><p>Team's mission and roadmap.</p>
</li>
<li><p>Working agreements (i.e. definition of "ready" and "done", meetings structure, core working hours, kitchen duty schedule, etc.).</p>
</li>
<li><p>Contact book of your teammates.</p>
</li>
<li><p>Rituals guidelines.</p>
</li>
<li><p>Templates for communication.</p>
</li>
<li><p>Team logo</p>
</li>
</ul>
<p>This list can go, but from these examples you can see what all that items have in common: you are only interested in a current version of them. They also support your daily work in some way.</p>
<h3 id="heading-archive">Archive</h3>
<p>The number of items in other sections can grow and become hard to navigate over time. It is important to keep it clean and have only relevant information at the reach distance of your hand. Projects are finished, features are being sunset, working agreements get deprecated, and new versions of strategy are written.</p>
<p>It is a good habit to review elements linked from the main page regularly (i.e. quarterly) and move them to the archive. I would recommend having the same sections in the archive to make it easier to find archived elements by type. The archive will become hard to navigate soon too, so I would recommend organizing elements in each section by years (by the moment it was moved to the archive).</p>
<h2 id="heading-can-i-add-something-else-to-the-page">Can I add something else to the page?</h2>
<p>Of course, these four sections are not set in stone. You may want to have some information available deeper in the structure to be duplicated on the main page.</p>
<p>I'd consider putting important information in sight:</p>
<ul>
<li><p>team mission, values, moto</p>
</li>
<li><p>roadmap snapshot</p>
</li>
<li><p>members list</p>
</li>
</ul>
<p>Being there, this information will catch an eye and serve as an "information radiator" making it readily available to everyone (even if you don't look for it). Just be careful and make sure you keep it concise and do not overwhelm the page.</p>
<h2 id="heading-notion-templates">Notion templates</h2>
<p>To demonstrate the idea, I've prepared some notion templates. Feel free to clone them to your notion space and try to organize your pages with this approach.</p>
<p>You don't even need to commit and move your existing pages. You can link them (aka "page mention" in notion) inside the new structure to get the feeling of whether it works for you.</p>
<ul>
<li><p><a target="_blank" href="https://aleks-obukhov.notion.site/Santa-Crew-31663e87c2134f1d94f8436b98d3ed7a">Santa Crew - team's page template [Wide]</a></p>
</li>
<li><p><a target="_blank" href="https://aleks-obukhov.notion.site/Santa-Crew-team-s-page-template-Narrow-1ee5ff3b51cd41f1aa7f452e73494b6c">Santa Crew - team's page template [Narrow]</a></p>
</li>
<li><p><a target="_blank" href="https://aleks-obukhov.notion.site/Santa-Crew-team-s-page-template-Narrow-7daf230ec6ff4daba1fabb4bb7f928ca">Santa Crew - team's page template [Collapsable]</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Code review practice]]></title><description><![CDATA[Code review is one of the most common practices in software development. Any company I have worked for since 2012 had it as a required step of the development process. Despite its ubiquitousness, understanding of it differed from company to company a...]]></description><link>https://blog.dclg.net/code-review-practice</link><guid isPermaLink="true">https://blog.dclg.net/code-review-practice</guid><category><![CDATA[Engineering culture]]></category><category><![CDATA[code review]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Sat, 22 Apr 2023 12:54:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/KPAQpJYzH0Y/upload/2a2f87ee3381c405c7e5f305823e25f6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Code review is one of the most common practices in software development. Any company I have worked for since 2012 had it as a required step of the development process. Despite its ubiquitousness, understanding of it differed from company to company and even engineer to engineer.</p>
<p>There are more than enough articles about best code review practices, but I'd like to summarize my thoughts and experience in this post.</p>
<h2 id="heading-define-code-review">Define "code review"</h2>
<p>According to <a target="_blank" href="https://google.github.io/eng-practices/review/">Google's code review developer guide</a>, "A code review is a process where someone other than the author(s) of a piece of code examines that code."</p>
<p>I would also add to that definition some goal setting:</p>
<blockquote>
<p>Code review is a collaborative act between the author and reviewer aimed at delivering the code increment in the best quality and most suitable for further maintenance at a given time budget.</p>
</blockquote>
<p>The main output of code review is the improved code quality of the particular change, highlights of issues the author(s) of the change could have missed, and alignment on the balance of trade-offs made in the increment.</p>
<p>Before formulating review principles, let's discuss the requirements for the review request itself:</p>
<h2 id="heading-creating-a-pr">Creating a PR</h2>
<ol>
<li><p><strong>Keep Pull Requests small</strong> (ideally under 400 lines), and prefer several small PRs over one big (with sensible exceptions, i.e., for refactorings, automated code changes, etc.). Small PRs are faster to review, and reviewers are more likely to find issues (see "LGTM syndrome").</p>
</li>
<li><p><strong>Separate refactoring and actual business</strong> <strong>logic</strong> change into different PRs. Considering that refactoring can be extensive and unrelated to the business logic change, having both in one PR will make it harder to focus on each.</p>
</li>
<li><p>Before requesting the review, <strong>make sure all</strong> <strong>automated checks pass</strong> (linters, static analysis tools, automated tests, etc.).</p>
</li>
<li><p>Before requesting the review, <strong>look through the pull request yourself</strong> for obvious improvements (leftover debug code, accidental changes, etc.).</p>
</li>
<li><p><strong>Format your pull request's title and description</strong> according to guidelines, don't leave an empty PR description template.</p>
</li>
<li><p>Add <strong>comments explaining certain decisions in the code</strong> or providing context (link to documentation, discussion happened elsewhere, etc.)</p>
</li>
</ol>
<h2 id="heading-communication-style-in-code-reviews">Communication style in code reviews</h2>
<ol>
<li><p><strong>Always be respectful, friendly, and professional</strong> to your co-workers, and preserve a positive and constructive communication style.</p>
</li>
<li><p><strong>Keep</strong> <strong>your</strong> <strong>comments concise.</strong> If more than one paragraph of text is needed, leave a note but communicate details verbally (in a shot call or over the desk).</p>
</li>
<li><p><strong>Be explicit</strong> about whether the change you suggest is <strong>strongly needed</strong> or <strong>nice to have</strong>.</p>
</li>
<li><p><strong>Provide reasons</strong> for changes with possible <strong>examples</strong>, links to <strong>guidelines, or best practices</strong> when requesting a change.</p>
</li>
</ol>
<h2 id="heading-code-review-principles">Code review principles</h2>
<ol>
<li><p><strong>Technical facts, company dev guidelines, and business requirements</strong> supersede personal opinions and preferences.</p>
</li>
<li><p><strong>Respect the task scope</strong>. The "boy scout rule" is great and shall be used when there is an opportunity, but be mindful about not creating a significant delay to the task delivery. If necessary, create follow-up tasks for addressing valuable comments that can't be fixed now.</p>
</li>
<li><p><strong>Allow a variety of solutions</strong> for edge cases. To support the pace, consider handling missing cases with graceful errors. Iterate within a task or create follow-up tasks if necessary.</p>
</li>
<li><p><strong>Done is better than perfect.</strong> Delivered code has more value than infinitely polished.</p>
</li>
</ol>
<h2 id="heading-roles-of-team-members-and-outside-reviewers">Roles of team members and outside reviewers</h2>
<p>It may happen that a person from outside of the team can add their review to the code even when not requested explicitly. Generally, it is a great practice providing transparency, knowledge exchange, and innovation.</p>
<blockquote>
<p>Everyone is free to drop by the code review and add comments, but only the team members make the decision about merging.</p>
</blockquote>
<p>This principle is crucial for supporting the culture of team autonomy, ownership, and a spirit of healthy compromise. Outside reviewers often don't observe other factors in balancing trade-offs, so they cannot make an informed decision about the approval.</p>
<h2 id="heading-what-code-review-is-not">What code review is not</h2>
<p>It is also essential to understand what code review isn't.</p>
<ol>
<li><p>Code review is not an attempt to make the code absolutely perfect. Without a limiting factor to balance participants' strive for perfection, review and iterative fixes can take forever, reducing the value of such increment.</p>
</li>
<li><p>Code review is not a competition of who can write more comments on the code change or who knows more ways to implement something. It may feel natural to write many comments and get into the "I'd rather do it in another way" trap. But it is crucial to understand if implementing the comment improves the code.</p>
</li>
<li><p>Code review is not the act of gatekeeping the delivery by some authoritative person or a group. This approach, first of all, doesn't scale well. But also contradicts ownership and autonomy principles while possibly also working against innovation.</p>
</li>
</ol>
<h2 id="heading-a-small-disclaimer">A small disclaimer</h2>
<p>The principles formulated above are crafted for commercial product development. Some of them may be debatable in the case of open-source development, where effort and time to market are less pressuring (which is also debatable) factors.</p>
]]></content:encoded></item><item><title><![CDATA[Two ways to setup HTTP redirects in  Kubernetes cluster]]></title><description><![CDATA[When migrating from one domain name to another one may need to set up a redirect of types 301 (Moved Permanently) or 302 (Found) for a certain domain and its paths. If you have Kubernetes as your runtime, there are at least two ways to do that:

Depl...]]></description><link>https://blog.dclg.net/two-ways-to-setup-http-redirects-in-kubernetes-cluster</link><guid isPermaLink="true">https://blog.dclg.net/two-ways-to-setup-http-redirects-in-kubernetes-cluster</guid><category><![CDATA[k8s]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Helm]]></category><category><![CDATA[nginx]]></category><category><![CDATA[redirects]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Sat, 18 Feb 2023 23:19:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/C7B-ExXpOIE/upload/7fdf378ec80458ac5a1e6a0b90ddbbb9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When migrating from one domain name to another one may need to set up a redirect of types <strong>301 (Moved Permanently)</strong> or <strong>302 (Found</strong>) for a certain domain and its paths. If you have Kubernetes as your runtime, there are at least two ways to do that:</p>
<ul>
<li><p>Deploy nginx server as an additional service to handle redirects.</p>
</li>
<li><p>Handle redirects at your ingress controller.</p>
</li>
</ul>
<p>The first one is safer but a bit more complex, the second one utilizes the ingress controller of your cluster, so you get redirects "for free", but it comes with a penalty. Let's look at both of these ways in detail.</p>
<p>As an example, we will set up redirects from <code>old-domain.tld</code> to <code>new-domain.tld</code> for two paths:</p>
<ul>
<li><p><code>/pages/about</code> → <code>/about</code> with <code>301 Moved Permanently</code> status</p>
</li>
<li><p><code>/</code> → <code>/</code> with <code>302 Found</code> status</p>
</li>
</ul>
<h2 id="heading-additional-service-to-handle-redirects">Additional service to handle redirects</h2>
<p>Not to create all k8s resources from scratch one can use <a target="_blank" href="https://artifacthub.io/packages/helm/bitnami/nginx">Bitmani's nginx helm chart</a>.</p>
<p>Here is an example of <code>values.yaml</code> file that configures ingress to route the traffic to an old domain to the nginx container and injects a custom "server" block to implement redirect rules:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">ingress:</span>
  <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">hostname:</span> <span class="hljs-string">old-domain.tld</span>
  <span class="hljs-attr">ingressClassName:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">cert-manager.io/cluster-issuer:</span> <span class="hljs-string">"letsencrypt-prod"</span>
  <span class="hljs-attr">tls:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">serverBlock:</span> <span class="hljs-string">|-
  server {
    listen 0.0.0.0:8080;
    location /pages/about {
      return 301 https://new-domain.tld/about;
    }    
    location / {
      return 302 https://new-domain.tld/;
    }
  }</span>
</code></pre>
<p>To install the chart one should run the following commands (assuming the file above is located in the current directory and named <code>my-values.yaml</code>):</p>
<pre><code class="lang-bash">helm repo add bitnami https://charts.bitnami.com/bitnami
helm install redirector bitnami/nginx -f my-values.yaml
</code></pre>
<p>In this case, the ingress controller will proxy requests to the "redirector" instance of nginx and it will respond with appropriate redirect headers.</p>
<h2 id="heading-ingress-configuration-to-handle-redirects">Ingress configuration to handle redirects</h2>
<p>But why use an additional nginx container if you probably already have one in your cluster <strong>—</strong> your ingress controller? Is it possible to inject redirect configuration directly into it? The answer is yes, but there are several issues.</p>
<p>The only thing you need is the ingress resource looking something like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">ingress-redirector</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">nginx.org/server-snippets:</span> <span class="hljs-string">|    
      location /pages/about {
        return 301 https://new-domain.tld/about;
      }    
      location / {
        return 302 https://new-domain.tld/;
      }
</span><span class="hljs-attr">spec:</span>
  <span class="hljs-attr">ingressClassName:</span> <span class="hljs-string">nginx</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">"old-domain.com"</span>
  <span class="hljs-attr">tls:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">"old-domain.com"</span>
      <span class="hljs-attr">secretName:</span> <span class="hljs-string">old-domain-cert</span>
</code></pre>
<p>As you can see, the nginx configuration is injected via <code>nginx.org/server-snippets</code> annotation and follows nginx configuration syntax. Otherwise, ingress doesn't look any different from a regular one, except there is no need for backend service configuration as all the traffic will be handled by the ingress controller itself.</p>
<p>The "issues" I mentioned are:</p>
<ul>
<li><p>Your ingress controller has to be based on nginx (maybe other controllers also can be configured for the task but they are not covered in this blog post).</p>
</li>
<li><p><a target="_blank" href="https://docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-snippets/">Snippets configuration</a> should be enabled on the controller level. Either with <code>enable-snippets</code> command-line argument or, if you use helm to deploy your ingress controller, by setting <code>controller.enableSnippets</code> parameter to <code>true</code> in your <code>values.yaml</code> file.</p>
</li>
<li><p>Third, it introduces a certain risk: if the configuration snippet has invalid syntax it will block configuration refresh for all the services using this ingress controller in the cluster. Even other ingress configuration changes won't apply. See more in "<a target="_blank" href="https://docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-snippets/#disadvantages-of-using-snippets">Disadvantages of Using Snippets</a>".</p>
</li>
</ul>
<p>To reduce the risk of invalid configuration, you can use a public helm chart I've created: <a target="_blank" href="https://github.com/obukhov/ingress-redirector">https://github.com/obukhov/ingress-redirector</a>. Of course, it is still possible to break it with some special character in "from", "to" or "code" parameters. But at least it is harder to do it unintentionally and <code>values.yaml</code> file will look a bit more friendly:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">ingress:</span>
  <span class="hljs-attr">hosts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">old-domain.tld</span>

  <span class="hljs-attr">tls:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">secretName:</span> <span class="hljs-string">old-domain-tld-tls</span>
      <span class="hljs-attr">hosts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">old-domain.tld</span>

<span class="hljs-attr">redirectRules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span> <span class="hljs-string">/pages/about</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">https://new-domain.tld/about</span>
    <span class="hljs-attr">code:</span> <span class="hljs-number">301</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span> <span class="hljs-string">/</span>
    <span class="hljs-attr">to:</span> <span class="hljs-string">https://new-domain.tld/</span>
    <span class="hljs-attr">code:</span> <span class="hljs-number">302</span>
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>What solution to choose depends on your situation. Do you have several development teams, and such implicit dependencies between their services in a shared runtime can cause an issue? Or is it just your pet project, and you really would like to avoid deploying an extra nginx container that will require resources? Kubernetes can serve your needs anyway.</p>
]]></content:encoded></item><item><title><![CDATA[How to write peer feedback]]></title><description><![CDATA[Providing regular peer feedback is a part of healthy engineering culture. It is usually collected in a written form by a manager, aggregated, and delivered to the recipient.
This post intends to be a short cheat sheet for those who provide peer feedb...]]></description><link>https://blog.dclg.net/how-to-write-peer-feedback</link><guid isPermaLink="true">https://blog.dclg.net/how-to-write-peer-feedback</guid><category><![CDATA[Feedback]]></category><category><![CDATA[Engineering culture]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Fri, 20 Jan 2023 11:10:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/npxXWgQ33ZQ/upload/d1b3265848eb26eee927b47895270089.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Providing regular peer feedback is a part of healthy engineering culture. It is usually collected in a written form by a manager, aggregated, and delivered to the recipient.</p>
<p>This post intends to be a short cheat sheet for those who provide peer feedback (respondents). It targets people who are about to write their first peer feedback and don’t know where to start, as well as those who have done it many times and need a refresher to keep the quality high. It focuses on written regular feedback. However, some advice can still be helpful for verbal or ad-hoc feedback.</p>
<h2 id="heading-why-is-it-essential-to-give-feedback">Why is it essential to give feedback</h2>
<p>Most professionals want to become a better version of themselves. It’s hard to do relying only on self-reflection. Planning for improvement based only on your own observations is similar to Münchhausen pulling himself out of a mire by his own hair. The blind spots are nearly impossible to get rid of without external input.</p>
<h2 id="heading-why-should-i-care-about-delivering-feedback-properly">Why should I care about delivering feedback properly</h2>
<p>Properly delivered feedback has a higher chance of converting into behavior change. And this is better for several reasons:</p>
<ul>
<li><p>Interaction with the person will become more productive and satisfying for you and other teammates and for the person themself.</p>
</li>
<li><p>You will see a higher return on your time investments.</p>
</li>
<li><p>The most valuable gifts are the ones money can’t buy, or in other words, something the person cannot obtain themselves. Feedback is this kind of gift: giving feedback that helps others grow makes you a better professional as well.</p>
</li>
</ul>
<h2 id="heading-prepare-to-give-feedback">Prepare to give feedback</h2>
<p>As we write peer feedback only occasionally, it requires concentration and a mindset switch. Prepare the right environment for yourself to provide the best feedback you can:</p>
<ul>
<li><p>Allocate enough time. The time you may need can easily be over an hour, depending on how detailed your feedback is.</p>
</li>
<li><p>Eliminate all distractions (turn off notifications, go to a meeting room or a booth specifically booked for it).</p>
</li>
<li><p>Remember (or even write down) all recent interactions with the person you are giving feedback. Projects you’ve been working on together, discussions you had one-on-one or in a group, reviews, documentation, and informal conversations that stood out.</p>
</li>
</ul>
<p>Don’t try to optimize by multitasking or speeding up. Rushing the feedback may be more harmful than not giving it at all.</p>
<h2 id="heading-anatomy-of-peer-feedback">Anatomy of peer feedback</h2>
<p>The most common peer feedback structure is two contrasting blocks: what the person does well and what they can improve. It is often treated as “positive” and “negative” feedback. I would suggest thinking of those in classic retrospective categories: <strong>start</strong>, <strong>stop</strong>, <strong>continue</strong>. Where both “<strong>start”</strong> and “<strong>stop”</strong> are improvement suggestions, and “<strong>continue”</strong> is something the person does well.</p>
<h2 id="heading-shaping-your-feedback">Shaping your feedback</h2>
<p>There are several practical rules for shaping the feedback to make it more convincing and impactful.</p>
<h3 id="heading-differentiate-observations-and-interpretations">Differentiate observations and interpretations</h3>
<p>Observations are objective facts anyone can agree on. In the context of feedback, it is, for example, something the person has said or done. Interpretations are subjective. It is often an opinion you’ve formed based on observation or an emotional response to the action.</p>
<p>Compare:</p>
<ul>
<li><p>“<em>You don’t value the input of others in the conversation</em>.”</p>
</li>
<li><p>“Y<em>ou have interrupted another person in the conversation ...</em>”</p>
</li>
</ul>
<p><strong>Caution:</strong> Avoid giving opinions not supported by observations and vice versa. You don’t give them a chance to reflect on the situation without providing any observations. If you miss the interpretation, it is unclear why changing or keeping the behavior is valuable.</p>
<h3 id="heading-emphasize-interpretations">Emphasize interpretations</h3>
<p>Don’t state interpretations as facts. Word it in a way to emphasize that it is your interpretation. It makes feedback more relatable and disarms natural defensive reactions. In the end, you can’t disagree about facts, and influence on others is precisely what you want to know.</p>
<p>Compare:</p>
<ul>
<li><p>“<em>You don’t value the input of others in the conversation</em>.”</p>
</li>
<li><p>“... <em>it made me feel that you don’t value the input of others in the conversation</em>.”</p>
</li>
</ul>
<p><strong>Caution:</strong> I mentioned before that you can’t disagree about facts. If you feel the person can disagree about the fact you’ve provided, look again. Probably your “fact” has a little interpretation in it. In this case, try to separate one from another once again.</p>
<p>Compare:</p>
<ul>
<li><p>“<em>You have made a rude joke.</em>”</p>
</li>
<li><p>“<em>You have made a joke which I consider not appropriate.</em>”</p>
</li>
</ul>
<h3 id="heading-be-specific-about-your-observations">Be specific about your <strong>observations</strong></h3>
<p>Even if you think a behavior you describe is typical for the person, it is crucial to provide specific examples. One or two most recent ones are usually enough. Once again, It gives a chance for the person to reflect on those moments as well. It makes your feedback much more convincing and helps the person to see their behavior in context.</p>
<p>Compare:</p>
<ul>
<li><p>“Y<em>ou always interrupt others in a discussion.</em>”</p>
</li>
<li><p>“<em>You interrupted Mike in the refinement a week ago and Peter in the last retro</em>.”</p>
</li>
</ul>
<p><strong>Caution:</strong> If you cannot recall recent examples of the behavior you wanted to mention, it may be worth self-reflecting. Does the person still demonstrates the behavior, or does your perception trick you? Can a previously formed impression stick with you for longer than needed?</p>
<h3 id="heading-desirable-behavior">Desirable behavior</h3>
<p>The framework above is also known as “AID feedback.” AID stands for “action, impact, desirable behavior.” The last one can be pretty obvious yet worth mentioning; the first two are absolutely required.</p>
<h2 id="heading-positive-continue-feedback">Positive (“continue”) feedback</h2>
<p>It seems like positive feedback is the easiest to provide, as it doesn’t imply any tension or bad feelings when receiving it. It is crucial to offer it and deliver it properly. Without acknowledging their positive behavior, the person will not know that others value it and can stop doing it. The following questions can help to come up with positive feedback if you struggle to find it:</p>
<ul>
<li><p><em>When the person exceeded your expectations or fulfilled them exactly?</em></p>
</li>
<li><p><em>When was the last time you were thankful that you were working with the person?</em></p>
</li>
<li><p><em>What are the recent achievements of the person and their team, and their contribution to those?</em></p>
</li>
<li><p><em>What recent improvements have you noticed (either based on your previous feedback or not)?</em></p>
</li>
</ul>
<h2 id="heading-improvement-feedback">Improvement feedback</h2>
<p>Some people feel bad talking about improvements. They may feel they are complaining about others, afraid to hurt their feelings, or cause trouble. When “packaged” correctly, the feedback will not hurt, and ignoring possible improvements, can cause much more trouble in the future. To facilitate writing improvement feedback, think separately of “stop” and “start” improvement suggestions.</p>
<p>In general:</p>
<ul>
<li><p><em>Think of moments when you would do something differently if you were that person.</em></p>
</li>
<li><p><em>Think of moments when your expectations were not met (i.e., dependencies or attitude).</em></p>
</li>
</ul>
<p>Suggest stopping something:</p>
<ul>
<li><p><em>If behavior can hurt the motivation or growth of others.</em></p>
</li>
<li><p><em>If behavior can damage themselves (i.e., cause burnout).</em></p>
</li>
</ul>
<p>Suggest starting something:</p>
<ul>
<li><p><em>If you felt something was missing from their end in the most recent touch points.</em></p>
</li>
<li><p><em>If you see professional growth opportunities, you think the person should take.</em></p>
</li>
</ul>
<h2 id="heading-improve-on-giving-feedback">Improve on giving feedback</h2>
<p>People are the most valuable asset of most businesses. Maintaining a healthy feedback culture can grow that value further and give the people involved a sense of growth and accomplishment. Keep learning how to do it, practice giving feedback, do it regularly, and request input from others.</p>
]]></content:encoded></item><item><title><![CDATA[Redis Inventory: Analyzing memory usage in Redis by key pattern]]></title><description><![CDATA[This post is also available on medium where you can follow the author, clap for the post or comment it.

”Redis Inventory” is a tool I developed as an open-source project to practice some coding skills and also solve a very practical problem. It coll...]]></description><link>https://blog.dclg.net/redis-inventory-analyzing-memory-usage-in-redis-by-key-pattern</link><guid isPermaLink="true">https://blog.dclg.net/redis-inventory-analyzing-memory-usage-in-redis-by-key-pattern</guid><category><![CDATA[golang]]></category><category><![CDATA[Redis]]></category><category><![CDATA[Devops]]></category><category><![CDATA[tools]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Sat, 21 Aug 2021 11:28:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674304309172/d3bda4a8-ceeb-4965-b22d-b74a0bfbac4a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><em>This post is also available on</em> <a target="_blank" href="https://medium.com/geekculture/analyzing-memory-usage-in-redis-by-key-patterns-e36420a2c225"><em>medium</em></a> <em>where you can follow the author, clap for the post or comment it.</em></p>
</blockquote>
<p>”<a target="_blank" href="https://github.com/obukhov/redis-inventory">Redis Inventory</a>” is a tool I developed as an open-source project to practice some coding skills and also solve a very practical problem. It collects memory usage information from Redis instance (or a cluster), detects key patterns, and displays memory usage in a hierarchical way.</p>
<p>Pretty much like lots of tools helping you to clean and organize your hard drive (for example “Disk Inventory X” or Unbuntu’s “Disk Usage Analyzer”) it even visualizes usage information in a similar fashion as the latest (sunburst diagram). But there are some nuances. Let’s look closer!</p>
<h2 id="heading-so-what-is-exactly-the-problem-it-is-solving"><strong>So what is exactly the problem it is solving?</strong></h2>
<p>One can argue that, unlike hard disk, cache server is not persistent storage, so why bother analyzing its usage? Yeah, in theory, the cache is completely ephemeral and any application should be able to start and work with it in a “cold” state. But in reality, under the load, it is not always possible to flush the cache with no performance regression. Also, if there is a problem with how the app uses Redis, flushing would be only temporary mitigation as after some time the same problems will accumulate again. Sometimes, you just see general key count or memory consumption increase in Redis metrics, but it is not obvious where the problem is, so is hard to fix it in the code with no prior investigation.</p>
<p>The two most popular issues I’ve seen with cache were: cache key leak and forgetting to set TTLs or setting them too generously. Key leak happens when you accidentally add something too dynamic to the key, for example, timestamp or its hash. With TTLs you may rely on the application to delete them but miss that in some situations it will not happen and keys will stay in cache forever. Those issues are hard to track in big applications which are changing rapidly. Analysing all the code changes that can cause it with no hint for problematic keys can take days.</p>
<h2 id="heading-how-the-tool-works"><strong>How the tool works</strong></h2>
<p>To analyze memory usage the tool scans keys space and measures each individual key size with <a target="_blank" href="https://redis.io/commands/memory-usage"><code>MEMORY USAGE</code></a> command. It builds a tree of nested nodes, similar to folder structure on your disk. But how do we interpret plain string keys as a hierarchy? It is quite common to use all sorts of prefixing in cache keys, we just need to reverse it. The simplest is to use a set of “separator” characters and break the string into a tuple of string segments.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912542865/16c40774-8ff7-422c-8e8b-9f6dd27ce48f.png" alt /></p>
<p>Then treating each of these segments as a “folder” build a tree of nodes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912595676/4521edb1-a704-4fcf-8a0d-1ccc826465d1.png" alt /></p>
<p>This data structure is known as “prefix tree” or “trie” (<a target="_blank" href="https://en.wikipedia.org/wiki/Trie">https://en.wikipedia.org/wiki/Trie</a>) and there is another nice article about it I’ve used as a source when working on this project (<a target="_blank" href="https://medium.com/amboss/prefix-trees-in-action-404a6105b6d5">https://medium.com/amboss/prefix-trees-in-action-404a6105b6d5</a>). This structure is very convenient in this particular case for several reasons: it is thrifty for memory, it is easy to add keys there, and it is possible already at the stage of building it to aggregate usage data on each level. We can attach a container for aggregated metrics to each node. When adding a new key to the tree we will descend the tree and add value (memory usage or another) on each level in the path.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912622075/54bb406f-e20a-4e7f-9bbc-e07f2f5b1f8e.png" alt /></p>
<p>Except, as you may see, the “prod:” segment. But that’s ok as it does not introduce a fork in the tree and values there are inherently equal to the values of the nested “user:” segment. Once a new key will come (for example “prod:blog:1”) and the fork will be introduced we can clone the aggregator from the next nested node that has it.</p>
<h2 id="heading-memory-optimizations"><strong>Memory optimizations</strong></h2>
<p>This data structure is already compact and functional but sometimes caches may have thousands or millions of entities with the same prefix. In this case, we usually don’t care already about each individual entity id but rather want to stop at the level where they start branching.</p>
<p>For this case, <code>maxChildren</code> parameter is implemented. If the number of children of any particular node is greater than <code>maxChildren</code> value it will stop creating child nodes and just aggregate values at that level, also incrementing <code>overflowChildrenCount</code> counter so you can at least know how many keys are “lost”.</p>
<h2 id="heading-rendering"><strong>Rendering</strong></h2>
<p>Currently, there are two ways to render that data: text table and sunburst chart. The table view is displayed right in the terminal and has some formatting adjustments (see <a target="_blank" href="https://github.com/obukhov/redis-inventory/blob/master/docs/usage.md">docs for more info</a>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912652652/d9b95d34-3b67-44d6-936d-1844cd2bcdd3.gif" alt /></p>
<p>The sunburst chart is rendered on an HTML page served by a built-in web server. Hovering any segment displays additional information as a tooltip. It is also zoomable: clicking on a segment makes it a central piece and renders sub-nodes around it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912674610/82493e31-11e7-489f-90d1-af5ca059595c.gif" alt /></p>
<h2 id="heading-quality-of-life-features"><strong>“Quality of life” features</strong></h2>
<p>Here are some nice features that are not super necessary but can make your life easier while using this tool.</p>
<h3 id="heading-custom-separators"><strong>Custom separators</strong></h3>
<p>Not every application uses the colon as a separator for key segments. Other common separators are, for example, underscores, dashes, or dots. To use custom separators you can provide them as a single string in <code>--separators</code> option (i.e. <code>--separators=":_-."</code>. Don’t worry if some of them are not always used as separators: as you have seen, nodes with just one child do not introduce major overhead. One can even implement a completely custom segmentation strategy by implementing <code>Splitter</code> interface, for example, detecting numeric parts of the key and capturing them in a separate segment even if they are not padded with separator characters. This would require changing the code and building your own binary though.</p>
<h3 id="heading-throttling"><strong>Throttling</strong></h3>
<p>Another useful option is <code>--throttle</code> allowing you to specify the number of nanoseconds the tool will wait between requests to the Redis server. It may be handy if you are afraid to cause denial of service on your Redis instance.</p>
<h3 id="heading-caching-index-result"><strong>Caching index result</strong></h3>
<p>As the tool supports different display formats, you may try some of them before you will find the most suitable. When using <code>inventory</code> command the tool will re-index the database every time. It may be time-consuming and can create unnecessary additional load for the server.</p>
<p>To avoid re-indexing every time instead of running <code>inventory</code> command you can use <code>index</code> command once to index Redis DB and save the index as a temp file. After that you can run <code>display</code> command several times with various parameters, it will use cached data and will not send any queries to the Redis server anymore. Keep in mind that options like <code>maxChildren</code> influence the trie structure and cannot be changed on the display step.</p>
<h2 id="heading-open-source-infrastructure"><strong>Open-source infrastructure</strong></h2>
<p>Here is the high-level overview of the project I have worked on lately. Although the main goal (except for creating the tool I needed) was to practice my skills, I’ve learned something about OSS projects infrastructure additionally. I was quite excited to know that I’m able to use a production-grade toolchain while working on this project absolutely for free:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/">GitHub</a> - for version-controlled hosting my code and simplistic project management;</p>
</li>
<li><p><a target="_blank" href="https://travis-ci.com/">Travis CI</a> - for continuous integration and delivery (uploading artifacts in docker registry);</p>
</li>
<li><p><a target="_blank" href="https://coveralls.io/">Coveralls</a> - to keep an eye on unit test coverage;</p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/">DockerHub</a> - a public docker image registry for the dockerized version of the tool;</p>
</li>
<li><p><a target="_blank" href="https://goreportcard.com/">Go Report Card</a> - for static code analysis (7 linting tools).</p>
</li>
</ul>
<p>Not to mention open-source libraries used in the project:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/mediocregopher/radix">Radix</a> - a full-featured Redis client for Go,</p>
</li>
<li><p><a target="_blank" href="https://github.com/spf13/cobra">Cobra</a> - CLI applications framework,</p>
</li>
<li><p><a target="_blank" href="https://github.com/jedib0t/go-pretty">Go-pretty</a> - set of tools to render text tables and progress bars,</p>
</li>
<li><p>… and a couple of smaller ones.</p>
</li>
</ul>
<p>Also special thanks to <a target="_blank" href="https://www.anychart.com/">AnyChart</a> for issuing a free open-source license for the project.</p>
<p>My point here, if you want to contribute to the open-source software world, you don’t have an excuse not to do so nowadays. It is free, convenient, and the only thing you have to invest is your time and talent. So go ahead and create the tool you lack and share it with the world.</p>
]]></content:encoded></item><item><title><![CDATA[Creating Keratin helm charts]]></title><description><![CDATA[Keratin (keratin.tech) is an open-source authorization microservice implemented in golang. I wanted to play with it to evaluate if it can be useful for my hobby projects. First I tried to find an existing helm chart to deploy it but unfortunately was...]]></description><link>https://blog.dclg.net/creating-keratin-helm-charts</link><guid isPermaLink="true">https://blog.dclg.net/creating-keratin-helm-charts</guid><category><![CDATA[Helm]]></category><category><![CDATA[k8s]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Sun, 23 Feb 2020 12:35:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Sq0L3SPWLHI/upload/10bd452ed2aeb8ee92f8a53fe2022280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Keratin (<a target="_blank" href="http://keratin.tech">keratin.tech</a>) is an open-source authorization microservice implemented in golang. I wanted to play with it to evaluate if it can be useful for my hobby projects. First I tried to find an existing helm chart to deploy it but unfortunately wasn’t able to find one.</p>
<p>So I tried to create and here what I’ve got as a result: <a target="_blank" href="https://github.com/keratin/helm-charts">https://github.com/keratin/helm-charts</a>.</p>
<p>When creating a helm chart it might be a little confusing what to start with. You can start with preparing a set of k8s configuration files for deployment, service, ingress controller and after that generalize them to the templates. Another way is to start right away with the dummy chart created with <code>helm create</code> command. This is convenient as you can reuse lots of trivial code from that dummy template and customize what you need.</p>
<p>Currently, there are 2 versions of chart format (v1 and v2) supported by helm2 and helm3 respectively. But, to be honest, the difference is not that huge. First, the way you list dependencies is different (in a separate file or directly in the <code>Chart.yaml</code>). Plus the distinction between applications and libraries is a new thing supported only by the format of version 2.</p>
<p>When you complete the chart of its part you can validate it with <code>helm lint</code> or compile templates with <code>helm template</code> command. But of course, nothing would prove that your chart works better then trying it at the real cluster. A nice discovery for me was the fact that you can use <code>required</code> function when you use the variable <a target="_blank" href="https://helm.sh/docs/howto/charts_tips_and_tricks/#using-the-required-function">to force chart users to specify it</a>.</p>
<p>While developing the chart you can deploy it form the source directory (don’t forget to run <code>helm dep up</code> to install dependencies in that case) but for distribution, you need to package it and provide a public URL to download it.</p>
<p>The easiest option for open-source charts is <a target="_blank" href="https://pages.github.com/">GitHub Pages</a>. I’ve created a separate repository with GitHub Pages enabled for the master branch to host my packages charts: <a target="_blank" href="https://github.com/obukhov/keratin-helm-repo">https://github.com/obukhov/keratin-helm-repo</a>.</p>
<p>To create the package you can just run <code>helm package &lt;path&gt;</code> with the path pointing to the chart source folder. The file will be automatically named accordingly with the chart name and version. Move this file to the GitHub Pages enabled repository (in the subfolder with chart name). And finally, run the following command to generate chart index file:</p>
<pre><code class="lang-bash">helm repo index &lt;chart folder name&gt; --url &lt;base url&gt;
</code></pre>
<p>Base url for github pages follows the template:</p>
<pre><code class="lang-text">https://&lt;user&gt;.github.io/&lt;repo name&gt;/&lt;chart folder name&gt;
</code></pre>
<p>That’s it. Now the repository can be added and chart can be installed from it:</p>
<pre><code class="lang-text">helm repo add keratin https://keratin.github.io/helm-repo/charts/

helm upgrade --install &lt;release name&gt; keratin/keratin-authn-server
</code></pre>
<p>The next logical step would be to add your repository to the index of <a target="_blank" href="http://hub.helm.sh">hub.helm.sh</a> as described here: <a target="_blank" href="https://github.com/helm/hub/blob/master/Repositories.md">https://github.com/helm/hub/blob/master/Repositories.md</a> to make it easier to find by other people.</p>
]]></content:encoded></item><item><title><![CDATA[Mini Kubernetes playground for $20 per month in 10 minutes]]></title><description><![CDATA[When you decide to learn Kubernetes there is always a question of where to find a non-expensive playground with full control to experiment. It should be easy to set up and shut down and provide you full control over it, and shouldn’t cost a fortune.
...]]></description><link>https://blog.dclg.net/mini-kubernetes-playground-for-20-per-month-in-10-minutes</link><guid isPermaLink="true">https://blog.dclg.net/mini-kubernetes-playground-for-20-per-month-in-10-minutes</guid><category><![CDATA[Terraform]]></category><category><![CDATA[DigitalOcean]]></category><category><![CDATA[Infrastructure as code]]></category><category><![CDATA[Infrastructure management]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Mon, 20 Jan 2020 18:07:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/HFI_UM7RLoU/upload/723e088fe9761b607ea6125b4180197a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you decide to learn Kubernetes there is always a question of where to find a non-expensive playground with full control to experiment. It should be easy to set up and shut down and provide you full control over it, and shouldn’t cost a fortune.</p>
<p>Through trial and error, I was able to find a solution that works perfectly for me.</p>
<p>To start we will need:</p>
<ul>
<li><p>Terraform client,</p>
</li>
<li><p>Digitalocean account,</p>
</li>
<li><p>and… that’s it.</p>
</li>
</ul>
<p>I would also recommend using a version control system.</p>
<h2 id="heading-installing-terraform"><strong>Installing Terraform</strong></h2>
<p>Terraform is the tool to provision cloud providers. It is an opensource project maintained by Hashicorp. It is really easy to start using it. It supports plenty of cloud hosting providers (AWS, GCP, DigitalOcean, etc.) and SaaS services (GitHub, Datadog, sentry, etc.).</p>
<p>To install it just download a single binary file for your platform and move it to a directory included in your system’s <code>PATH</code>.</p>
<h2 id="heading-registering-digitalocean-account"><strong>Registering Digitalocean account</strong></h2>
<p>I think I don’t have to teach you how to register. You can use <a target="_blank" href="https://m.do.co/c/4e59fe5fa971">my referral link</a> to get a $100 registration bonus. That would be more than enough to experiment with Kube and decide if you want to continue using it. I obviously also will benefit from that (that’s why it is called “referral”).</p>
<p>After completing the registration process navigate to the API side menu item and create a new personal access token. Give it a memorable name (i.e. terraform) and grant with read/write permissions. Copy access token value after generation - you will need it at the next step.</p>
<h2 id="heading-start-coding"><strong>Start coding</strong></h2>
<p>Now the most exciting part - create an empty directory for your project and open the editor.</p>
<h3 id="heading-terraform-provider-configuration"><strong>Terraform provider configuration</strong></h3>
<p>Most files for Terraform have <code>.tf</code> extension. Let’s start with defining provider and access credentials for it - in this case, that would be Digitalocean. Create <a target="_blank" href="http://provider.tf"><code>provider.tf</code></a> file with the following content:</p>
<pre><code class="lang-plaintext">// provider.tf

provider "digitalocean" {
  token   = "&lt;your access token&gt;"
}
</code></pre>
<p>Is it that simple? Well, yes and no. This is the minimal provider declaration but the recommended way is to extract dynamic data like your API key to a variable, so you can change it without changing the code. Let’s make it right from the beginning and change file contents to:</p>
<pre><code class="lang-plaintext">// provider.tf

variable "do_token" {}

provider "digitalocean" {
  token   = var.do_token
}
</code></pre>
<p>The default values for the variables you can specify in a file named <code>terraform.tfvars</code>. And in this case in unlike the previous one the file name matters, so keep it.</p>
<pre><code class="lang-plaintext">// terraform.tfvars

do_token = "&lt;your access token&gt;"
</code></pre>
<h3 id="heading-describing-kubernetes-resources"><strong>Describing Kubernetes resources</strong></h3>
<p>That’s already enough to make terraform access Digitalocean API to create and destroy resources on your behalf. So let’s describe the first one to deploy the Kubernetes cluster.</p>
<pre><code class="lang-plaintext">// kube-cluster.tf

resource "digitalocean_kubernetes_cluster" "my_cluster" {
  name   = "my-cluster"
  region = "fra1"

  version = "1.16.2-do.3"

  node_pool {
    name       = "worker-pool"
    size       = "s-1vcpu-2gb"
    node_count = 2
  }
}
</code></pre>
<p>In this resource definition, you provide all necessary information for terraform to create the cluster via Digitalocean API. It is important to understand the difference between <strong>my_cluster</strong> and <strong>my-cluster</strong>: the first name exists only in terraform codebase, you can use it to refer to the attributes of the resource. The second one is the name of a cluster given in Digitalocean, you will also see it in the administrative panel. They, of course, don’t have to look alike but in most cases it makes sense.</p>
<p><strong>Region</strong> parameter defines the physical location of underlying infrastructure and <strong>version</strong> is the version of Kubernetes being deployed. The following parameters define type and quantity of nodes in the cluster.</p>
<p>You can get the list of available options for node sizes and Kubernetes versions by installing <code>doctl</code> <a target="_blank" href="https://github.com/digitalocean/doctl/blob/master/README.md#installing-doctl">command-line tool</a> and executing the following commands:</p>
<pre><code class="lang-bash">$ doctl kubernetes options versions

Slug           Kubernetes Version
1.16.2-do.3    1.16.2
1.15.5-do.3    1.15.5
1.14.8-do.3    1.14.8


$ doctl compute size list                                                                                  ]

Slug               Memory    VCPUs    Disk    Price Monthly    Price Hourly
512mb              512       1        20      5.00             0.007440
s-1vcpu-1gb        1024      1        25      5.00             0.007440
1gb                1024      1        30      10.00            0.014880
s-1vcpu-2gb        2048      1        50      10.00            0.014880
s-1vcpu-3gb        3072      1        60      15.00            0.022320
s-2vcpu-2gb        2048      2        60      15.00            0.022320
s-3vcpu-1gb        1024      3        60      15.00            0.022320
2gb                2048      2        40      20.00            0.029760
s-2vcpu-4gb        4096      2        80      20.00            0.029760
...
</code></pre>
<p>Be aware that not all droplet sizes are possible to use as Kubernetes cluster node. In fact, <code>s-1vcpu-2gb</code> is the smallest and cheapest one.</p>
<p>The number of nodes is for your consideration: a minimum of 2 nodes is required to prevent downtime during upgrades or maintenance. If you don’t mind having downtimes (as it is not a production system) you can go with one. But having one node will not provide you real experience with containers and requests being balanced to different nodes. I recommend running at least 2 nodes, which I’ve used to estimate price.</p>
<h3 id="heading-applying-changes-and-validating-the-result"><strong>Applying changes and validating the result</strong></h3>
<p>The short coding part is now over and we can proceed to the testing process. First, you have to initiate terraform project once to download the provider plugin. To do that execute <code>terraform init</code> in the project folder.</p>
<p>If there are no errors run <code>terraform apply</code> command and confirm changes by typing <code>yes</code> when prompted. It can take several minutes to complete but you will be provided with the feedback:</p>
<pre><code class="lang-bash">digitalocean_kubernetes_cluster.my_cluster: Creating...
digitalocean_kubernetes_cluster.my_cluster: Still creating... [10s elapsed]
digitalocean_kubernetes_cluster.my_cluster: Still creating... [20s elapsed]
...
digitalocean_kubernetes_cluster.my_cluster: Still creating... [6m40s elapsed]
digitalocean_kubernetes_cluster.my_cluster: Creation complete after 6m43s [id=1608898b-fdcf-411a-b8e2-0e2a7820539c]
</code></pre>
<p>The cluster is created and we can see it in the administrative panel.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676913092264/2497273d-0644-4219-a51a-94d6a4ffcec5.png" alt /></p>
<p>Digitalocean deploys some software to the cluster so you will not need to deploy it yourself. Kubernetes Dashboard is one of them. With the help of it, you can see deployed payloads, secrets, services, ingresses, etc. To access it, click on the link “Kubernetes Dashboard” in the cluster description page.</p>
<h3 id="heading-terraform-state-file"><strong>Terraform state file</strong></h3>
<p>After terraform started to communicate to Digitalocean API the new file is created in the folder <code>terraform.tfstate</code>, this file tracks state of the changes applied basically connecting your resource definitions to ids issued by Digitalocean for the created resources. These ids are used to update and delete resources.</p>
<p>To delete created cluster execute <code>terraform destroy</code> and confirm when prompted. Congrats! Now you can create and delete cluster with one command whenever you want to experiment with it.</p>
<h3 id="heading-final-touches"><strong>Final touches</strong></h3>
<p>If you want to deliver an additional 10% for your codebase you can make sure it is formatted properly. Execute <code>terraform fmt</code>. It will have no output if all your files formatted correctly and output modified file names otherwise.</p>
<p>To guarantee your declaration to work in the future you can add explicit provider version definition, this will change the provider definition to following:</p>
<pre><code class="lang-plaintext">provider "digitalocean" {
  version = "~&gt; 1.12"
  token   = var.do_token
}
</code></pre>
<p>Make sure you haven’t broken any formatting with this by running <code>fmt</code> again.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>These 18 lines of code are a small step for the developer, but it is a big step for your infrastructure marking the first steps to the infrastructure-as-code approach and making it extremely easy and fast to deploy it and clean it up with no UI interactions.</p>
]]></content:encoded></item><item><title><![CDATA[Copy 1 Million Redis Keys in 2 Minutes with Golang]]></title><description><![CDATA[Golang is a useful programming language that can solve daily problems in an efficient way. It’s easy to learn, and doesn’t require writing a lot of code to perform well.
Let’s take a look at how Golang can help in a simple and practical case involvin...]]></description><link>https://blog.dclg.net/copy-1-million-redis-keys-in-2-minutes-with-golang</link><guid isPermaLink="true">https://blog.dclg.net/copy-1-million-redis-keys-in-2-minutes-with-golang</guid><category><![CDATA[Redis]]></category><category><![CDATA[golang]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Tue, 07 Jan 2020 14:47:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/LqKhnDzSF-8/upload/ccf06cd9771fd36b1e2ce6466c81d8c4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Golang is a useful programming language that can solve daily problems in an efficient way. It’s easy to learn, and doesn’t require writing a lot of code to perform well.</p>
<p>Let’s take a look at how Golang can help in a simple and practical case involving copying large amounts of Redis keys.</p>
<p>At some point it became necessary to split our Amazon ElastiCache store into two parts — one for storing cached data, and the other for storing users’ sessions.</p>
<p>We, unfortunately, had them on the same instance previously. We also didn’t want to interrupt long-living sessions by resetting the storage.</p>
<p>Amazon ElastiCache is compatible with the Redis protocol, though with certain limitations. Redis supports the <a target="_blank" href="https://redis.io/commands/migrate">MIGRATE</a> command, allowing you to move keys matched by a pattern from one instance to another.</p>
<p>Internally it works by executing <a target="_blank" href="https://redis.io/commands/dump">DUMP</a>+<a target="_blank" href="https://redis.io/commands/dump">DEL</a> commands on the source instance and creating them in target instance using <a target="_blank" href="https://redis.io/commands/restore">RESTORE</a>. However, Amazon’s version <a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2019/10/amazon-elasticache-now-supports-online-data-migration-from-redis-on-amazon-ec2/">didn’t support this command at the time</a>.</p>
<p>Back then, my practical experience with Golang was limited. I’d only implemented projects for fun and was familiar with basic syntax and concepts like goroutine and channels. But I’d decided that was enough to make use of Golang’s strengths to solve the problem I was facing.</p>
<h2 id="heading-step-1-lets-write-some-simple-code">Step 1: Let’s Write Some Simple Code</h2>
<p>Let’s assume that Golang is fast enough to do the job. Keep in mind that <a target="_blank" href="https://redis.io/topics/benchmarks">Redis is, mostly, a single-threaded server from the point of view of commands execution</a> and implements replication with no concurrency.</p>
<h2 id="heading-preparation">Preparation</h2>
<p>I’ve picked two base libraries for this challenge:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/mediocregopher/radix">Radix</a> to connect to the Redis API, and</p>
</li>
<li><p><a target="_blank" href="https://github.com/spf13/cobra">Cobra</a> to make it easier to build the command-line interface for the tool</p>
</li>
</ul>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> cmd

<span class="hljs-keyword">import</span> (
  <span class="hljs-string">"github.com/mediocregopher/radix/v3"</span>
  <span class="hljs-string">"github.com/spf13/cobra"</span>
  <span class="hljs-string">"log"</span>
)

<span class="hljs-keyword">var</span> pattern <span class="hljs-keyword">string</span>
<span class="hljs-keyword">var</span> scanCount, report, limit <span class="hljs-keyword">int</span>

<span class="hljs-keyword">var</span> copyCmd = &amp;cobra.Command{
  Use:   <span class="hljs-string">"copy [sourceHost:port] [targetHost:port]"</span>,
  Short: <span class="hljs-string">"Copy keys from source redis instance to destination by given pattern"</span>,
  Long: <span class="hljs-string">""</span>,
  Args:  cobra.MinimumNArgs(<span class="hljs-number">2</span>),
  Run: <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(cmd *cobra.Command, args []<span class="hljs-keyword">string</span>)</span></span> {
    clientSource, err := radix.DefaultClientFunc(<span class="hljs-string">"tcp"</span>, args[<span class="hljs-number">0</span>])
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
      log.Fatal(err)
    }

    clientTarget, err := radix.DefaultClientFunc(<span class="hljs-string">"tcp"</span>, args[<span class="hljs-number">1</span>])
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
      log.Fatal(err)
    }

    <span class="hljs-comment">// ... here the copying will happen</span>
  },
}

<span class="hljs-keyword">var</span> rootCmd = &amp;cobra.Command{
  Use:   <span class="hljs-string">"go-redis-migrate"</span>,
  Short: <span class="hljs-string">"Application to migrate redis data from one instance to another"</span>,
  Long:  <span class="hljs-string">""</span>,
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
  rootCmd.AddCommand(copyCmd)

  copyCmd.Flags().StringVar(&amp;pattern, <span class="hljs-string">"pattern"</span>, <span class="hljs-string">"*"</span>, <span class="hljs-string">"Match pattern for keys"</span>)
  copyCmd.Flags().IntVar(&amp;scanCount, <span class="hljs-string">"scanCount"</span>, <span class="hljs-number">100</span>, <span class="hljs-string">"COUNT parameter for redis SCAN command"</span>)
  copyCmd.Flags().IntVar(&amp;report, <span class="hljs-string">"report"</span>, <span class="hljs-number">1000</span>, <span class="hljs-string">"After what number of keys copied to report time"</span>)
  copyCmd.Flags().IntVar(&amp;limit, <span class="hljs-string">"limit"</span>, <span class="hljs-number">0</span>, <span class="hljs-string">"After what number of keys copied to stop (0 - unlimited)"</span>)
}
</code></pre>
<p>The interface is ready, it supports the “pattern” parameter to match keys, and the “limit” parameter to define the maximum number of keys. The source and destination are provided as arguments and are also required.</p>
<h2 id="heading-the-main-loop">The Main Loop</h2>
<p>Radix supports creating a “scanner” structure that helps you iterate over keys:</p>
<pre><code class="lang-go">scanOpts := radix.ScanOpts{
  Command: <span class="hljs-string">"SCAN"</span>,
  Count:   scanCount,
}

<span class="hljs-keyword">if</span> pattern != <span class="hljs-string">"*"</span> {
  scanOpts.Pattern = pattern
}

scanner := radix.NewScanner(clientSource, scanOpts)

<span class="hljs-keyword">var</span> key <span class="hljs-keyword">string</span>
counter := <span class="hljs-number">0</span>

<span class="hljs-keyword">for</span> scanner.Next(&amp;key) {
  <span class="hljs-comment">// copy the key</span>

  counter++
}

<span class="hljs-keyword">if</span> err := scanner.Close(); err != <span class="hljs-literal">nil</span> {
  log.Fatal(err)
}

log.Printf(<span class="hljs-string">"In total %d keys copied in %s"</span>, counter, time.Since(start))
</code></pre>
<p>The loop is now ready. What’s left is to read and restore data in the target. I joined the <code>PTTL</code> and <code>DUMP</code> command to fetch <em>time to live</em> and <em>value</em> of the key in a pipeline to save execution time.</p>
<pre><code class="lang-go"><span class="hljs-keyword">var</span> value <span class="hljs-keyword">string</span>
<span class="hljs-keyword">var</span> ttl <span class="hljs-keyword">int</span>

p := radix.Pipeline(
  radix.Cmd(&amp;ttl, <span class="hljs-string">"PTTL"</span>, key),
  radix.Cmd(&amp;value, <span class="hljs-string">"DUMP"</span>, key),
)

<span class="hljs-keyword">if</span> err := clientSource.Do(p); err != <span class="hljs-literal">nil</span> {
  <span class="hljs-built_in">panic</span>(err)
}

<span class="hljs-keyword">if</span> ttl &lt; <span class="hljs-number">0</span> {
  ttl = <span class="hljs-number">0</span>
}

err = clientTarget.Do(radix.FlatCmd(<span class="hljs-literal">nil</span>, <span class="hljs-string">"RESTORE"</span>, key, ttl, value, <span class="hljs-string">"REPLACE"</span>))
<span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
  log.Fatal(err)
}
</code></pre>
<p>That’s already enough for the code to work, but adding some reporting logic would definitely improve the user experience.</p>
<p>The complete code can be found here: <a target="_blank" href="https://github.com/obukhov/go-redis-migrate/blob/v1.0/cmd/copy.go">https://github.com/obukhov/go-redis-migrate/blob/v1.0/cmd/copy.go</a></p>
<h2 id="heading-but-is-it-really-that-good">But is it really that good?</h2>
<p>Let’s run some benchmark tests by quickly spawning two Redis instances locally with Docker, and seeding the source with data (453,967 keys in total, but we only copy part of them by matching a pattern).</p>
<p>Then, we run each test three times to see the random deviation:</p>
<pre><code class="lang-text">10000 keys to copy: 17.79s 18.01s 17.98s
367610 keys to copy: 8m57.98s 8m44.98s 8m58.07s
</code></pre>
<p>That’s not bad, but let’s see if we can improve it.</p>
<h2 id="heading-step-2-utilize-concurrency">Step 2. Utilize Concurrency</h2>
<p>Let’s visualize the sequence of operations in the current implementation:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912902871/479d6d75-9f57-4032-a9cf-21fac9fa5530.png" alt /></p>
<h2 id="heading-what-can-we-do-to-improve-the-performance-here">What can we do to improve the performance here?</h2>
<p>We can clearly see the following shortcomings:</p>
<ul>
<li><p>Reading from the source and writing to the target is serialized, although it can be executed in parallel</p>
</li>
<li><p>The single-threaded nature of Redis only affects command execution, but serving data (network i/o) can also be parallelized. Depending on the value size, this can make a big difference</p>
</li>
</ul>
<p>Sending multiple concurrent requests could be a good strategy. They will block each other on processing but will better utilize the i/o. This applies to both dumping and restoring data.</p>
<p>There is one process that can’t be parallelized— scanning the database. It relies on the scanning cursor, and there’s no way to perform the scan in multiple threads.</p>
<p>Let’s split the process into three stages:</p>
<ul>
<li><p>Scanning,</p>
</li>
<li><p>Dumping data, and</p>
</li>
<li><p>Restoring data</p>
</li>
</ul>
<p>Scanned keys can be served through a channel to a set of goroutines, concurrently dumping these keys values and TTLs, and sending them through another channel to another set of goroutines. They, in turn, restore this data in the target instance.</p>
<p>Here’s an example of a visualization:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912932155/c7ad8d7a-f1e4-4641-9f0e-abd40ce5b026.png" alt /></p>
<p>The <a target="_blank" href="https://golang.org/doc/gopher/run.png">Go gopher</a> in this image was created by <a target="_blank" href="http://reneefrench.blogspot.com/">Renee French</a>, the image was composited by me.</p>
<p>Gophers running in circles are loops in the goroutines, reading from the channel, processing data, and sending the processed data to another channel.</p>
<h2 id="heading-implementation">Implementation</h2>
<p>We’ll start implementing a scanner and exporter in the same package for simplicity, starting with declaring structures:</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> KeyDump <span class="hljs-keyword">struct</span> {
  Key   <span class="hljs-keyword">string</span>
  Value <span class="hljs-keyword">string</span>
  Ttl   <span class="hljs-keyword">int</span>
}

<span class="hljs-keyword">type</span> RedisScannerOpts <span class="hljs-keyword">struct</span> {
  Pattern          <span class="hljs-keyword">string</span>
  ScanCount        <span class="hljs-keyword">int</span>
  PullRoutineCount <span class="hljs-keyword">int</span>
}

<span class="hljs-keyword">type</span> RedisScanner <span class="hljs-keyword">struct</span> {
  client      radix.Client
  options     RedisScannerOpts
  keyChannel  <span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>
  dumpChannel <span class="hljs-keyword">chan</span> KeyDump
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NewScanner</span><span class="hljs-params">(client radix.Client, options RedisScannerOpts)</span> *<span class="hljs-title">RedisScanner</span></span> {
  <span class="hljs-keyword">return</span> &amp;RedisScanner{
    client:      client,
    options:     options,
    dumpChannel: <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> KeyDump),
    keyChannel:  <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> <span class="hljs-keyword">string</span>),
  }
}
</code></pre>
<p>Two channels are declared here. The first is a plain string channel to send scanned keys from the scanner to the group of exporting goroutines. The second channel of <code>KeyDump</code>structures is for sending dumped data to the goroutines restoring data.</p>
<p>A KeyDump structure contains all the necessary information about simple Redis values: key, value, and TTL.</p>
<h2 id="heading-first-goroutines">First Goroutines</h2>
<p>The following function orchestrates goroutines for scanning and exporting data:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *redisScanner)</span> <span class="hljs-title">Start</span><span class="hljs-params">()</span></span> {
  wgPull := <span class="hljs-built_in">new</span>(sync.WaitGroup)
  wgPull.Add(s.options.PullRoutineCount)

  <span class="hljs-keyword">go</span> s.scanRoutine()
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; s.options.PullRoutineCount; i++ {
    <span class="hljs-keyword">go</span> s.exportRoutine(wgPull)
  }

  wgPull.Wait()
  <span class="hljs-built_in">close</span>(s.dumpChannel)
}
</code></pre>
<p>As you can see, it spawns one scanning routine and the number of exporting goroutines defined by the PullRoutineCount option. Pay attention to the variable named <code>wgPull</code> of type <code>WaitGroup</code>, a handy tool that makes sure our code doesn’t exit before the process is complete.</p>
<blockquote>
<p><code>WaitGroup</code> waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block excution until all goroutines have finished.</p>
</blockquote>
<p>Waitgroup is initialized through the <code>Add</code> method with the overall number of goroutines. The variable is provided to each goroutine as an argument, and when goroutine finishes its work, it calls <code>Done</code> method. <code>Add</code> increments an internal counter and<code>Done</code> decrements it. <code>Wait</code> method blocks execution until the counter reaches zero.</p>
<p>The goroutine scanner structure is similar to what we had in the first version:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *RedisScanner)</span> <span class="hljs-title">scanRoutine</span><span class="hljs-params">()</span></span> {
  <span class="hljs-keyword">var</span> key <span class="hljs-keyword">string</span>
  scanOpts := radix.ScanOpts{
    Command: <span class="hljs-string">"SCAN"</span>,
    Count:   s.options.ScanCount,
  }

  <span class="hljs-keyword">if</span> s.options.Pattern != <span class="hljs-string">"*"</span> {
    scanOpts.Pattern = s.options.Pattern
  }

  radixScanner := radix.NewScanner(s.client, scanOpts)
  <span class="hljs-keyword">for</span> radixScanner.Next(&amp;key) {
    s.keyChannel &lt;- key
  }

  <span class="hljs-built_in">close</span>(s.keyChannel)
}
</code></pre>
<p>Everything is self-explanatory, but there are a few things worth mentioning:</p>
<ul>
<li><p><code>&lt;-</code> sends data to the channel</p>
</li>
<li><p>At the very end, we close the channel to let the goroutines know that no more data is going to be sent</p>
</li>
</ul>
<p>Exporting goroutine already looks familiar to us. Terminating execution on a client error is not really a nice way to handle errors, but if the connection is reliable, it will never be a problem.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *RedisScanner)</span> <span class="hljs-title">exportRoutine</span><span class="hljs-params">(wg *sync.WaitGroup)</span></span> {
  <span class="hljs-keyword">for</span> key := <span class="hljs-keyword">range</span> s.keyChannel {
    <span class="hljs-keyword">var</span> value <span class="hljs-keyword">string</span>
    <span class="hljs-keyword">var</span> ttl <span class="hljs-keyword">int</span>

    p := radix.Pipeline(
      radix.Cmd(&amp;ttl, <span class="hljs-string">"PTTL"</span>, key),
      radix.Cmd(&amp;value, <span class="hljs-string">"DUMP"</span>, key),
    )

    <span class="hljs-keyword">if</span> err := s.client.Do(p); err != <span class="hljs-literal">nil</span> {
      log.Fatal(err)
    }

    <span class="hljs-keyword">if</span> ttl &lt; <span class="hljs-number">0</span> {
      ttl = <span class="hljs-number">0</span>
    }

    s.reporter.AddExportedCounter(<span class="hljs-number">1</span>)
    s.dumpChannel &lt;- KeyDump{
      Key:   key,
      Ttl:   ttl,
      Value: value,
    }
  }

  wg.Done()
}
</code></pre>
<p>The reading from the channel is implemented with the <code>range</code> keyword which exits the for loop automatically when the channel (<code>s.keyChannel)</code> is closed. <code>wg.Done()</code> in the last line helps to ensure that all the keys passed through <code>s.keyChannel</code> were dumped and sent through <code>s.dumpChannel</code>.</p>
<p>As you maybe know, struct fields starting with a lower-case letter are considered internal for the package, so we have to provide a getter in order to allow other packages to read the <code>dumpChannel</code> field. This is also a chance to declare return type as a channel only intended to be read from (using <code>&lt;-chan</code> instead of just <code>chan</code> type):</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *RedisScanner)</span> <span class="hljs-title">GetDumpChannel</span><span class="hljs-params">()</span> &lt;-<span class="hljs-title">chan</span> <span class="hljs-title">KeyDump</span></span> {
  <span class="hljs-keyword">return</span> s.dumpChannel
}
</code></pre>
<h2 id="heading-goroutines-to-restore-exported-data">Goroutines to Restore Exported Data</h2>
<p>Pusher can also be configured and uses WaitGroup to orchestrate goroutines:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NewRedisPusher</span><span class="hljs-params">(client radix.Client, dumpChannel &lt;-<span class="hljs-keyword">chan</span> scanner.KeyDump)</span> *<span class="hljs-title">RedisPusher</span></span> {
  <span class="hljs-keyword">return</span> &amp;RedisPusher{
    client:      client,
    dumpChannel: dumpChannel,
  }
}

<span class="hljs-keyword">type</span> RedisPusher <span class="hljs-keyword">struct</span> {
  client      radix.Client
  dumpChannel &lt;-<span class="hljs-keyword">chan</span> scanner.KeyDump
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(p *RedisPusher)</span> <span class="hljs-title">Start</span><span class="hljs-params">(wg *sync.WaitGroup, number <span class="hljs-keyword">int</span>)</span></span> {
  wg.Add(number)
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; number; i++ {
    <span class="hljs-keyword">go</span> p.pushRoutine(wg)
  }
}
</code></pre>
<p>And <code>pushRoutine</code> uses a similar practice to read from the channel and exit:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(p *RedisPusher)</span> <span class="hljs-title">pushRoutine</span><span class="hljs-params">(wg *sync.WaitGroup)</span></span> {
    <span class="hljs-keyword">for</span> dump := <span class="hljs-keyword">range</span> p.dumpChannel {
        p.reporter.AddPushedCounter(<span class="hljs-number">1</span>)
        err := p.client.Do(radix.FlatCmd(<span class="hljs-literal">nil</span>, <span class="hljs-string">"RESTORE"</span>, dump.Key, dump.Ttl, dump.Value, <span class="hljs-string">"REPLACE"</span>))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatal(err)
        }
    }

    wg.Done()
}
</code></pre>
<p>There is one important thing to note here: <code>dumpChannel</code> is closed by scanner only after all exporters exit. That guarantees no data will be lost at the very end. It’s achieved with <code>wgPull</code> and two lines in <code>RedisScanner</code>‘s <code>Start()</code> receiver:</p>
<pre><code class="lang-go">wgPull.Wait()
<span class="hljs-built_in">close</span>(s.dumpChannel)
</code></pre>
<h2 id="heading-wiring-everything-together">Wiring Everything Together</h2>
<p>Now let’s use developed packages in the Cobra command to put it all together.</p>
<p>First, extend the command definition to add more options:</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
  rootCmd.AddCommand(copyCmd)
  copyCmd.Flags().StringVar(&amp;pattern, <span class="hljs-string">"pattern"</span>, <span class="hljs-string">"*"</span>, <span class="hljs-string">"Match pattern for keys"</span>)
  copyCmd.Flags().IntVar(&amp;scanCount, <span class="hljs-string">"scanCount"</span>, <span class="hljs-number">100</span>, <span class="hljs-string">"COUNT parameter for redis SCAN command"</span>)
  copyCmd.Flags().IntVar(&amp;report, <span class="hljs-string">"report"</span>, <span class="hljs-number">1</span>, <span class="hljs-string">"Report current status every N seconds"</span>)
  copyCmd.Flags().IntVar(&amp;exportRoutines, <span class="hljs-string">"exportRoutines"</span>, <span class="hljs-number">30</span>, <span class="hljs-string">"Number of parallel export goroutines"</span>)
  copyCmd.Flags().IntVar(&amp;pushRoutines, <span class="hljs-string">"pushRoutines"</span>, <span class="hljs-number">30</span>, <span class="hljs-string">"Number of parallel push goroutines"</span>)
}
</code></pre>
<p>Then, create a scanner and pusher (and <code>WaitGroup</code> for them). Don’t forget to call <code>Wait()</code> on it, otherwise, the command will exit immediately:</p>
<pre><code class="lang-go"><span class="hljs-comment">// clientSource and clientTarget initialization</span>
redisScanner := scanner.NewScanner(
  clientSource,
  scanner.RedisScannerOpts{
        Pattern:          pattern,
        ScanCount:        scanCount,
        PullRoutineCount: exportRoutines,
    },
)

redisPusher := pusher.NewRedisPusher(clientTarget, redisScanner.GetDumpChannel())

waitingGroup := <span class="hljs-built_in">new</span>(sync.WaitGroup)

redisPusher.Start(waitingGroup, pushRoutines)
redisScanner.Start()

waitingGroup.Wait()
</code></pre>
<h2 id="heading-benchmark">Benchmark</h2>
<p>The most exciting part is to see the difference. Let’s take a look at the same test cases and compare them:</p>
<h2 id="heading-test-1">Test #1</h2>
<p>Source database: 453,967 keys.Keys to copy: 10,000 keys.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912973457/4591f080-1c3f-431f-bf2b-2181c9cb2dd7.png" alt /></p>
<h2 id="heading-test-2">Test #2</h2>
<p>Source database: 453,967 keys.Keys to copy: 367,610 keys.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676912978346/3e9b88dc-054d-41f3-b003-61a0a83c239a.png" alt /></p>
<p>Processing is <strong>three to nine times faster</strong> when testing on a local machine. The real execution on the production infrastructure took <strong>less than two minutes</strong> to copy about <strong>a million keys</strong>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This application was one of my first codebases written in Golang. It also helped to fix a real-life problem with very little development and operations time needed.</p>
<p>If you look at the full version of the code <a target="_blank" href="https://github.com/obukhov/go-redis-migrate">here</a>, you’ll see a “sidecar” goroutine that collects counters on scanned, exported, and pushed keys, and reports in with configured time intervals to <code>stdout</code>. It helps to see the progress of execution in the following format:</p>
<pre><code class="lang-text">Start copying
2021/02/14 13:11:42 Scanned: 29616 Exported: 29616 Pushed: 29616 after 1.000153648s
2021/02/14 13:11:43 Scanned: 59621 Exported: 59615 Pushed: 59615 after 2.000128223s
2021/02/14 13:11:44 Scanned: 89765 Exported: 89765 Pushed: 89765 after 3.0001194s
2021/02/14 13:11:44 Scanned: 100000 Exported: 100000 Pushed: 100000 after 3.347127281s
Finish copying
</code></pre>
<p>Do you have some examples of how Golang helped you to find a simple solution to a tricky problem? Tweet me a response with the link below.</p>
]]></content:encoded></item><item><title><![CDATA[Snakes and ladders of GraphQL on our way to the perfect API]]></title><description><![CDATA[Gave the talk “Snakes and ladders of GraphQL on our way to the perfect API” at the GraphQL Wroclaw group’s meetup.
Our team has been working for the past 18 months on porting AMBOSS functionality to GraphQL API. During this time we faced some technic...]]></description><link>https://blog.dclg.net/snakes-and-ladders-of-graphql-on-our-way-to-the-perfect-api</link><guid isPermaLink="true">https://blog.dclg.net/snakes-and-ladders-of-graphql-on-our-way-to-the-perfect-api</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[Public Speaking]]></category><dc:creator><![CDATA[Aleksandr Obukhov]]></dc:creator><pubDate>Tue, 26 Nov 2019 17:28:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674324044417/4850ca5d-5a19-4297-b797-5b7be1963304.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Gave the talk “Snakes and ladders of GraphQL on our way to the perfect API” at the GraphQL Wroclaw group’s meetup.</p>
<p>Our team has been working for the past 18 months on porting AMBOSS functionality to GraphQL API. During this time we faced some technical and organizational challenges. The talk describes the experience of this migration process, what caused questions, and how we as a team approached them.</p>
<p><a target="_blank" href="http://bit.ly/snakes-n-ladders-graphql-wroclaw">The slides can be downloaded here</a></p>
<p>The talk and slides are in English. There were five speakers in total: two from Berlin and three from Wroclaw. More details are on the <a target="_blank" href="https://www.meetup.com/GraphQL-Wroclaw/events/264614444/">event’s page at</a> <a target="_blank" href="http://meetup.com">meetup.com</a>.</p>
<h2 id="heading-recording">Recording</h2>
<iframe width="750" height="400" src="https://www.youtube.com/embed/bFN6TTe6NMk"></iframe>

<h2 id="heading-photos">Photos</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676909875971/c4bcedde-8f7a-448d-97da-8b72a1dfd467.jpeg" alt="The risk of new technology adoption photo" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676909944965/728f7a48-1587-4c7d-9158-bc4f0941819b.jpeg" alt="Errors vs Union return types" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1676909972597/17a19739-bb36-4204-99be-5acdac208623.jpeg" alt="Cover image" class="image--center mx-auto" /></p>
]]></content:encoded></item></channel></rss>