<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://rlindsgaard.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://rlindsgaard.github.io/" rel="alternate" type="text/html" /><updated>2026-09-27T05:32:35+00:00</updated><id>https://rlindsgaard.github.io/feed.xml</id><title type="html">Memory Leaks</title><subtitle>Just things that escape my mind.</subtitle><entry><title type="html">Your Mainline is not Supposed to be Pristine</title><link href="https://rlindsgaard.github.io/software%20engineering/devops/2026/09/27/your-mainline-is-not-supposed-to-be-pristine.html" rel="alternate" type="text/html" title="Your Mainline is not Supposed to be Pristine" /><published>2026-09-27T00:00:00+00:00</published><updated>2026-09-27T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/devops/2026/09/27/your-mainline-is-not-supposed-to-be-pristine</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/devops/2026/09/27/your-mainline-is-not-supposed-to-be-pristine.html"><![CDATA[<p>A common fallacy is the belief that the mainline is supposed to be pristine in a Continuous Delivery/Trunk Based Development environment – that we pursue a deployment pipeline that we should eliminate all failures before integration. Not only is this untrue, I will argue that the pursuit of this implicitly assumed requirement is even hurtful to the flow and creation of value. This does not mean we should not adequately test our code, but merely that we must learn to rely on the signals we get from the deployment pipeline.</p>

<p>Obtaining an always green deployment pipeline gets more and more difficult as you scale your development organization. One common approach is to hide changes behind feature branches – where changes can be checked before reaching the mainline. Merge Requests is a normal way to obtain this workflow and we will use the terms interchangeably.</p>

<p>This works, most of the time, but it comes with the risk of creating the wrong culture, one where people are afraid of breaking the deployment pipeline by merging into the mainline. This fear leads to larger, less frequent merges, and longer integration cycles and is one of the main arguments against feature branches in the first place [4] [5].</p>

<p>If we are not careful, then we, implicitly, create a culture where we optimize for Mean Time Between Failure (MTBF) – State of Devops [1] and subsequently DORA [2] teaches us that focusing on improving Mean Time to Recover (MTTR) is a better way to reach high performance.</p>

<p>The key metrics for measuring throughput and quality are for production environments but we can apply the same analogue thinking to the deployment pipeline as this is just a shorter feedback loop, for the commit-stage, consider tracking</p>

<ul>
  <li>Integration Frequency (i.e. how often we push or merge and as a result run the commit stage)</li>
  <li>Integration Lead Time (Time since the commit or Merge Request (MR) was created until it landed on the mainline)</li>
  <li>Mainline Recovery Time (How long it takes from a failed commit-stage job to be resolved and succeed again)</li>
  <li>Build Failure Rate (The rate between succeeding and failed commit-stage jobs)</li>
</ul>

<p>If we want to reach these goals in production, it naturally follows we need to do it at least just as fast in our development environment.</p>

<p>Ironically, code changes are not the only source of failures – and the developer making the breaking change might be unable to do something about it on their own, which only increases the fear of making changes. According to Continuous Delivery [3] there are 5 likely reasons why a pipeline might fail</p>

<ul>
  <li>There is a bug in the application code</li>
  <li>There is a bug or invalid expectation in a test or test code</li>
  <li>There is a problem with application configuration</li>
  <li>There is a problem with the deployment process</li>
  <li>There is a problem with the environment</li>
</ul>

<p>Only some might be directly influenced or caused by the developer, generally flaky tests and/or infrastructure will add onto the fear – the DevOps organization must work towards breaking these environmental issues. If a developer cannot tell “I broke it” from “infrastructure broke it”, fear persists.</p>

<h2 id="normalize-and-standardize-reversals">Normalize and Standardize Reversals</h2>
<p>Mean Time to Recover is greatly influenced by the ability to simply undo the commit - either by removing it from the mainline entirely or by reverting the commit, i.e. adding a commit that undoes the change.</p>

<p>To normalize this behavior, make sure it is well documented as part of the normal development flow. Standardization means more people throughout the organization can help recover, and makes it easier to debug when the standard procedure fails.</p>

<h2 id="lower-the-blast-radius-of-a-failed-deployment-pipeline">Lower the Blast Radius of a Failed Deployment Pipeline</h2>
<p>If you try to eliminate all failures before the mainline, you must run increasingly expensive validation: full pipeline runs, shadow environments, comprehensive test suites. This will quickly become an expensive (and likely error prone) affair as it scales horizontally with the amount of Work in Progress.</p>

<p>The real problem emerges when a failed build blocks other developers from integrating.</p>

<p>Recovery typically takes at least twice the pipeline cycle time. One cycle for getting a failure result and another for verifying the pipeline now succeeds. For a three-minute pipeline, that is six minutes and one can barely make it back from a coffee run. For a fifteen minute pipeline, this adds up to thirty minutes – that is several lunch breaks throughout the day.</p>

<p>Splitting the deployment pipeline into individual jobs where you can get feedback earlier will help you to quickly answer if you are blocking other people’s work. In Continuous Delivery this distinction is in fact made, where the fast feedback is provided by the “commit stage” whereas the slower running “acceptance stage” then figures out whether this is something that can actually be released.</p>

<p>The key is to distinguish between “fast vs slow” running tests - only check the things that are fast and help the next developer out such as</p>

<ul>
  <li>Verifying the code compiles</li>
  <li>Verifying system invariants e.g via static code analysis checks such as linting</li>
  <li>Verifying application correctness via unit tests</li>
</ul>

<p>This check should be bounded to match the team’s attention span, typically between  3-10 minutes. This could be an important metric to measure as well separate from integration lead time.</p>

<p>All the other quality checks come after as they do not provide value at this stage, deployments and deploy-script validation typically runs later as failed deployments do not block other developers - remember that a merged MR or feature branch does not mean the feature is necessarily done.</p>

<p>To protect later stages from failing prematurely we can make use of feature flags until the code is finally ready. We are thus de-coupling integration from deployments and we can safely collaborate with teams without jeopardizing the production release.</p>

<h2 id="empower-and-educate">Empower and Educate</h2>
<p>People are in general inclined to “do the right thing” – at least as long as it is easy. Continuous Delivery practices and flow optimization in general are not always intuitive. What benefits the broader (development) organization may impose constraints on individual teams. Education is a continuous necessity as there will inevitably be turnover (if not, you have other problems of stagnation to solve), it is not enough to say “we do <semantically diffused="" buzzword="">" here. The processes and rules must be restated in the organization's own language explaining what feature branching or Trunk Based Development means here, and what the expected behavior is. Never assume people read the book or even bothered googling it. A process document where the last change was a few typos three years ago does not count. Any process that does not change repeatedly is a dead process.</semantically></p>

<p>Naturally, to motivate the right behavior you also need to make people able to perform whatever operation is required, opening a ticket is not resolution, it is escalation. One side of the coin is the fast feedback (in a readable format) that can tell the developer “this was your doing” - with a clear indicator of what inadvertently broke. The other side of the coin is that they must also be given permissions and knowledge to be able to resolve it on her own accord.</p>

<h2 id="measure-and-improve">Measure and Improve</h2>
<p>Nothing speaks like a visual dashboard (sometimes called an information radiator) with clear-cut data showing the organization where we are (and where we want to be) - one of the impediments, and probably hazards is that it forces us to define with mathematical precision, and come to terms with reality, how we measure productivity and how effectively we actually work.</p>

<p>Using the adapted DORA metrics, as defined earlier in the post, my personal recommendation would be to target</p>

<h3 id="integration-frequency">Integration Frequency</h3>
<p>If the deployment pipeline is triggered on changes to the mainline we can simply increase a counter every time the build starts and select the period we wish to get the rate for e.g. daily. For periodic based triggering mechanism we need to find another mechanism such as closed MRs.</p>

<p>Continuous integration practices advocated for integration work at least once a day, where more modern day practices advocate for even more frequent integration. This is an opportune time for the organization to discuss and agree on the target rate.</p>

<h3 id="integration-lead-time">Integration Lead Time</h3>
<p>In isolation this metric is easy to game. But together with integration frequency, it becomes harder to fake - we can choose to get the MR creation time from the code repository service or, perhaps more universal, the time of each created commit if possible. Integration Lead Time should be calculated from the earliest commit timestamp in the MR/feature branch to the timestamp when that change is merged into the mainline (i.e., when the merge commit is recorded).</p>

<p>There are many reasons this value might get skewed, normally I see work paused because of priority changes (and this is one thing we want to combat) - other reasons might be vacation or sickness. In general this is a signal that people may not be checking in frequently enough. Long lead times often indicate batching or delayed integration.</p>

<h3 id="mainline-recovery-time">Mainline Recovery Time</h3>
<p>We need to track not only every build, but also the final result of the job paired with timestamps.
Tracking this as a time series we can filter out and compute the mean time difference between the first red job until the next green one.
We can also choose more direct approaches if MTTR is the only value we are interested in but we need these values to also calculate the build failure rate.</p>

<p>The target MTTR is a conversation that needs to be agreed on between relevant stakeholders, as well as when it is appropriate to roll back other people’s changes.</p>

<h3 id="build-failure-rate">Build Failure Rate</h3>
<p>While DORA’s CFR measures production failures, we track commit-stage failures as an earlier signal to catch issues before deployment.</p>

<p>Now that we track the result of each individual build we can calculate the proportion of builds that fail.</p>

<p>At this stage we are only aiming for “good enough” – fast feedback is often more valuable than exhaustive feedback at this stage.</p>

<p>Accept an initial high failure rate, say 20-30%. As the system stabilizes and flakiness in the environments or test-suites are addressed the targeted failure rate can be lowered.</p>

<p>[1] https://dora.dev/research/2016/2016-state-of-devops-report.pdf
[2] https://dora.dev/guides/dora-metrics/
[3] Continuous Delivery -  Dave Farley, Jez Humble
[4] https://youtu.be/v4Ijkq6Myfc
[5] https://a4al6a.substack.com/p/stop-using-pull-requests</p>]]></content><author><name></name></author><category term="Software Engineering" /><category term="DevOps" /><category term="Continuous Delivery" /><category term="Trunk Based Development" /><category term="DORA" /><summary type="html"><![CDATA[A common fallacy is the belief that the mainline is supposed to be pristine in a Continuous Delivery/Trunk Based Development environment – that we pursue a deployment pipeline that we should eliminate all failures before integration. Not only is this untrue, I will argue that the pursuit of this implicitly assumed requirement is even hurtful to the flow and creation of value. This does not mean we should not adequately test our code, but merely that we must learn to rely on the signals we get from the deployment pipeline.]]></summary></entry><entry><title type="html">Ship/Show/Ask - The Flowchart</title><link href="https://rlindsgaard.github.io/software%20engineering/2026/05/25/shipshowask-the-flowchart.html" rel="alternate" type="text/html" title="Ship/Show/Ask - The Flowchart" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/2026/05/25/shipshowask-the-flowchart</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/2026/05/25/shipshowask-the-flowchart.html"><![CDATA[<p>Ship/show/ask branching strategy originally devised at Thoughtworks it is explained in full at <a href="https://martinfowler.com/articles/ship-show-ask.html">Martin Fowler’s blog</a> - it is designed to combine and leverage the benefits of both Trunk Based Development (TBD) and a Merge Request (MR) based development practice.</p>

<p><a href="https://a4al6a.substack.com/p/stop-using-pull-requests">Andrea Laforgia</a> gives a much better discussion on how this strategy can be used as a stepping stone for pure TBD, providing excellent reference material along the way.</p>

<p>In short, the approach categorise changes into three scenarios</p>

<p><strong>ship</strong> - Small changes, side effect free, push changes directly to trunk. Perfect for fixes or otherwise side-effect free changes.</p>

<p><strong>show</strong> - Create a merge request and merge it when it passed checks. Perfect for feature development where you don’t need work to get stuck in the “waiting for feedback” pit.</p>

<p><strong>ask</strong> - Request and discuss feedback before going any further. Perfect for inter-team changes, newcomers to the code-base and re-designs</p>

<p>My contribution is the flowchart below combining the whole thing into a single decision tree from the perspective of an individual contributor.</p>

<p><img src="/assets/shipshowask.jpeg" alt="Ship/Show/Ask - The Flowchart v1" /></p>]]></content><author><name></name></author><category term="Software Engineering" /><summary type="html"><![CDATA[Ship/show/ask branching strategy originally devised at Thoughtworks it is explained in full at Martin Fowler’s blog - it is designed to combine and leverage the benefits of both Trunk Based Development (TBD) and a Merge Request (MR) based development practice.]]></summary></entry><entry><title type="html">Post-Deployment Validation Concepts</title><link href="https://rlindsgaard.github.io/software%20engineering/2026/03/23/post-deployment-validation-concepts.html" rel="alternate" type="text/html" title="Post-Deployment Validation Concepts" /><published>2026-03-23T00:00:00+00:00</published><updated>2026-03-23T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/2026/03/23/post-deployment-validation-concepts</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/2026/03/23/post-deployment-validation-concepts.html"><![CDATA[<h1 id="post-deployment-validation-concepts">Post-Deployment Validation Concepts</h1>

<p>Core concept definitions and caveats to be aware of when defining a strategy for verifying system behavior post-deployment.</p>

<p>I hope this text will be used for defining a good strategy, bringing everyone on a team - from junior to senior together into a discussion of setting a common baseline and understanding of distinctions and subtleties. The content is a likely candidate for a skills.md file perhaps.</p>

<p>I am purposefully leaving out any tool recommendations in order to keep the text high-level and to keep it clear of bias towards a particular stack.</p>

<h2 id="slice-and-dice">Slice and Dice</h2>

<h3 id="component-recursion">Component Recursion</h3>
<p>These definitions work for systems, and can be broken down to aggregate components and again into covering subcomponents.</p>

<p>A system can be made up of architectural quanta - i.e. groups of separately deployable units or such as a set of microservices. Such quanta make up a set of subsystems.</p>

<p>It does not make sense to talk about readiness or health without specifying what falls under our implied “what”. A component can be healthy while the overall system is not. The overall system can also be healthy whilst there are unhealthy or failing components (within some pre-defined threshold).</p>

<p>Additionally, components are often structured in layers, adding another dimension to their design which we will explore next.</p>

<h2 id="systems-are-like-onions">Systems are like Onions</h2>
<p>Today, most services run in some form of virtualised environment. As a result there are at least two operating systems under the service you want to monitor (host OS + guest OS). Each layer makes up the platform of the next one - at some point, containerization (like docker) or virtualization (like a Virtual Machine) will make the platform isolated for the component - but other subcomponents will share an identical layer. Similarly, these layers will share strengths and weaknesses, and might be monitored in the same manner.</p>

<h2 id="readiness-checks">Readiness Checks</h2>
<p>Performed once at initiation and answers the question: 
Is the service ready to serve requests in a meaningful way yet?</p>

<p>The check is very useful for blocking downstream processes that require the component to be functional before proceeding.</p>

<p>Focus is on whether the component is able to serve main functionality, such as answering http requests, the component might present itself as ready but with degraded performance (a system might be ready to perform work but slower whilst warming up a cache writing things to disk).</p>

<p>The check ought to be time-bound, and must eventually result in failure to prevent failures propagating down-stream where they are more difficult to identify.</p>

<p>On a system/deployment quantum level it makes sense that readiness takes on the value of the readiness state of the component with least ready state but that depends on the architecture. If you spawn multiple workers horizontally, you do not necessarily need all of them up.</p>

<p>The most simple “readiness” declaration is simply writing a value into a designated file as the last operation performed.</p>

<pre><code class="language-mermaid">stateDiagram-v2
    [*] --&gt; Starting
    
    Starting --&gt; Ready
    Starting --&gt; Failed
    
    Ready --&gt; [*]

    Failed --&gt; [*]
</code></pre>

<h2 id="healthchecks">Healthchecks</h2>
<p>Ongoing check of the component that takes place throughout the lifetime of the system.</p>

<p>Can be performed as a self-check (i.e. from inside the component), from an external agent, or both.</p>

<p>Focus is on the factors that sum up to whether a component is healthy or not. e.g</p>
<ul>
  <li>Are all internal services running</li>
  <li>Are resource consumption within thresholds.</li>
  <li>Did the readiness check complete</li>
</ul>

<p>It is necessary to decide if/when readiness affects health. Do components getting re-deployed, and thus not ready, affect system health? Perhaps only for critical services or for specific times of the day.</p>

<p>As described in “Systems are like Onions” it is not uncommon that we want to split health of a component into the layers that make up the component. E.g. we might wish to monitor container or virtual  machine “health” separately from the health of the webservice that runs on top of it.</p>

<p>As a good starting point, monitor the health of the layers that make up the platform externally, and expose the results of a self-check factoring in component specific behavior.</p>

<p>The right approach needs to take into consideration instrumentation strategies and tooling.</p>

<p>Now that these distinctions are in place, we are ready to dive into how these tie together when it comes to smoke testing.</p>

<h2 id="smoke-testing">Smoke Testing</h2>

<p>When deployment is complete and readiness checks report success, then it is time to do the final smoke test verifying system behavior.</p>

<p>If the readiness check or health check performs operations similar to what a user would do (e.g. hit the service from the external domain address) - then the minimal smoke test suite is merely conducting a verification that the state is as expected by issuing a GET request against that target domain adress.</p>

<p>If the checks up until now are based on self-reporting, then a successful smoke test suite should access and verify the system the same way a user would. You are basically writing an external auditor. This is also where it might be confusing, and indeed code-reuse from readiness checks is to some extent practical.</p>

<p>The more advanced suite will conduct a series of automated fake operations similar to acceptance tests to verify the system also works with the different components when put to actual work. These will mimic transaction or code-paths through the deployed stack. A simple login operation will not only check the HTTP endpoint, but possibly backend API calls and the underlying DBMS.</p>

<p>A final note is that most systems are not deployed just once. Throughout the build process they are usually deployed multiple times over in testing or staging environments. The scope, and perhaps difficulty, of smoke tests is to identify functionality that deals with verifying the actually deployment, i.e. to apply the same set of test equally throughout different deployments as they mature, such that potential errors are detected early in the process and not post-release.</p>

<h2 id="resources-and-recommended-reading">Resources and Recommended Reading</h2>

<p>The content is based on hard earned experience, lessons from seniors I have worked with, guided by these works by giants that stand out in memory</p>

<ul>
  <li>Release It, Michael T Nygaard - Architecture and Site Reliability Engineering before there was such a team</li>
  <li>Continuous Delivery, Dave Farley and Jez Humble - How to deliver quality software, fast. I think that is even the tagline of this book.</li>
  <li>Practical Monitoring, Mike Julian - More concrete definitions and low-level strategies for operations including process building.</li>
  <li>Building Evolutionary Architectures, Neal Ford, Rebecca Parsons, Patrick Kua, Pramod Sadalage - Designing and building software systems in an agile way, guided by instrumentation, data driven observations and user needs/business value to feed the cycle.</li>
</ul>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Even small teams witness terminology confusion and overloading of terms. As the number of actors grow, so does this problem.</p>

<p>What we name things is not important, what we mean by those names is.</p>

<p>Verifying the system after deployment is an important step for any product and from this post it should easy to define the validation strategy from a common baseline using smoke tests and on-going health checks.</p>]]></content><author><name></name></author><category term="Software Engineering" /><summary type="html"><![CDATA[Post-Deployment Validation Concepts]]></summary></entry><entry><title type="html">This is Fine or: How I Learned to Stop Worrying and Embrace Technical Debt</title><link href="https://rlindsgaard.github.io/software%20engineering/philosophy/2025/11/01/this-is-fine.html" rel="alternate" type="text/html" title="This is Fine or: How I Learned to Stop Worrying and Embrace Technical Debt" /><published>2025-11-01T00:00:00+00:00</published><updated>2025-11-01T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/philosophy/2025/11/01/this-is-fine</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/philosophy/2025/11/01/this-is-fine.html"><![CDATA[<p>As a developer I have always been passionate about incremental change and adamant about addressing technical debt, which I believe is deeply tied to code quality.</p>

<p>The most challenging part was reconciling my frustration towards colleagues and decision makers who did not seem to care about something I felt was of great importance – even though I worked in a team where code quality was very highly regarded and prioritized, I wanted even more I wanted everyone to strive for the same perfection as me.</p>

<p>The fact is, it is never going to be enough quality or tidiness and you will always land back on a satisfactory level of 7 (or a 3?) – not unless you can find a good metric.</p>

<p>There are many great lessons that experience, and philosophy, has taught me to harness emotions. Two of these I will expand upon in this post</p>

<ol>
  <li>The path is the goal</li>
  <li>Love your fate</li>
</ol>

<h2 id="the-path-is-the-goal">The Path is the Goal</h2>

<p>My unwavering focus on code quality and technical debt led me to explore systems design, build systems, and beyond. It has led to hands-on experience with the inner workings of tools to generate documentation and visualizations based on reflections of the code, abstract syntax trees for unconventional linting rules, and a myriad of templating and interpolation techniques.</p>

<p>This pursuit of quality has made me the engineer that I am today. I like that engineer.</p>

<p>I have also learned that the quality I sought was a golden calf. I have had the humbling experience of working on the same codebase long enough to witness the unintended consequences of my own work. One experience that stands out to me was a refactoring initiative I took on for the purpose of improving code quality – years later, working with the same code I found my earlier assumptions to be false, and my “improvements” had made the code worse. This lesson, among others, has forced me to rethink my definition of code quality.</p>

<p>Quality is not about a uniform and tidy codebase, it is about adaptability. I have found that it is better to leave working code unchanged unless you absolutely have to, regardless of how it looks.</p>

<p>Coding guidelines and static analysis are invaluable tools for preventing code rot from setting in. I would not like to work on a project without them, at least at the integration stage. But be wary, as these things can quickly devolve into bikeshedding, with teams or individuals obsessing  over the wrong bottlenecks. The road to hell is indeed paved with good intentions.</p>

<h2 id="love-your-fate">Love Your Fate</h2>
<p>This stoic phrase (or Zen-Buddhist, take your pick) encourages us to accept things as they are. Only then are you free to act.</p>

<p>Code is the way it is because work has been done – work that provided real business value. After all, that is our motivation for code quality, isn’t it?
It is okay; it is good code because it is what brought us here.
The design, the foundation, the toolstack too. It held up the codebase, it kept the business – well, in business. Perhaps instead of calling it “debt”, we should view it as battle scars.</p>

<p>Whether it is debt or not does not matter much; what matters is the next step you take. Make those steps towards internal acceptance, and practice identifying smaller increments to make. To identify the real bottlenecks, I have learned to ask myself where the real problems are, what obstacles prevent people from performing their work, and what difficulties new personnel must face. What traps do they consistently fall into?</p>

<h2 id="conclusion">Conclusion</h2>
<p>I have grown to a place where I think there is no such thing as “technical debt”, I prefer the term “unmanaged complexity” which I believe was coined by Dave Farley.</p>

<p>I have realized that management or decision makers are not to blame or be held accountable for  poor complexity management – and refactor is not the solution, that is a cop out. This is an engineering problem, a group problem and the two most effective things I can do to battle it is</p>

<ol>
  <li>Write documentation, and then write some more. Specs, tickets, commit messages–everything matters. As a senior, I know and understand the context and systems better than most. I understand the “whys” and the theory behind. That needs to be communicated – or else everyone makes up their own explanations.</li>
  <li>Spend all the time needed to help, show, discuss, review the work of junior staff. Sometimes it is skills or experience lacking. More often than not, they lack context. Even with documentation, gaining a true understanding of the system takes time.</li>
</ol>

<p>I find that most often the poor complexity management comes from well meaning people who don’t know better because of lack of context and/or experience – most often it is misunderstood attempts at making improvements that backfires into more complexity – it is a socio-technical issue and I have transgressed into treating it as such.</p>

<p>By reframing technical debt as unmanaged complexity and embracing these lessons, I have found not just peace, but also a more rewarding inner work life.</p>]]></content><author><name></name></author><category term="Software Engineering" /><category term="Philosophy" /><summary type="html"><![CDATA[As a developer I have always been passionate about incremental change and adamant about addressing technical debt, which I believe is deeply tied to code quality.]]></summary></entry><entry><title type="html">A Workflow for Tracking and Addressing Operational Defects</title><link href="https://rlindsgaard.github.io/devops/software%20engineering/2025/10/27/workflow-tracking-addressing-operational-defects.html" rel="alternate" type="text/html" title="A Workflow for Tracking and Addressing Operational Defects" /><published>2025-10-27T00:00:00+00:00</published><updated>2025-10-27T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/devops/software%20engineering/2025/10/27/workflow-tracking-addressing-operational-defects</id><content type="html" xml:base="https://rlindsgaard.github.io/devops/software%20engineering/2025/10/27/workflow-tracking-addressing-operational-defects.html"><![CDATA[<p>In an ideal world, defects are analyzed and resolved immediately, ensuring they never recur. In reality, however, resolving defects often relies on the tenacity of individual soldiers – or even plain luck – this approach leads to a culture of silos and uneven workloads. Even worse, without a structured problem-solving approach, other issues such as resource problems can mask as defects going on unnoticed or addressed indefinitely.</p>

<p>Defects refer to anything that functions unintentionally, while operational defects typically require manual intervention or tracking through automated observations. This post introduces a structured workflow for teams to collaboratively detect, track, and resolve such issues. It emphasizes structure and communication across functions and responsibilities.</p>

<p>I will argue how the data collected can be used by the team and decision-makers to prioritise fireproofing. It is not an all encompassing terminology-worthy methodology, just a starting point for any team to start working with these issues in a structured manner.</p>

<blockquote>
  <p>Imagine a case where a rogue process forgets cleaning up after itself after analyzing a file upload i.e. many files are written to the temporary directory, eventually exhausting the disk. There are load-balancing in place, for other reasons, and this particular thing happens once in a while on one out of 4 replicas. The machines are normally restarted for other reasons (it is the default first step resolution method) and is usually handled when half the fleet is down.</p>
</blockquote>

<h2 id="issue-tracking">Issue Tracking</h2>
<p>Tracking issues by registering them is a tedious task and requires the kind of overview you don’t really have. Tickets are the backbone of almost any task management workflow so there is an almost certainty this will allow you to implement the workflow with existing and known tools.</p>

<p>We need two types of tickets. I will call them “bug” and “task” but their type is not important - how they function within the team’s workflow is.</p>

<p>The bug-ticket is a collection item and must be able to aggregate task-tickets into a child/parent relationship. The bug-ticket potentially lives for a long time in the backlog.</p>

<p>The task-ticket follows normal board workflow. This is the sort of task the “expedited” lane or “unplanned work” is actually for.</p>

<p>With the overview of the structure in place, let us look at how to handle observations.</p>

<h3 id="handling-observations">Handling Observations</h3>
<p>A new task-ticket is created on each observation.</p>

<h4 id="naming">Naming</h4>
<p>Use some convention of title e.g <code class="language-plaintext highlighter-rouge">Handle: "&lt;error-message&gt; "on "&lt;instance&gt;"</code></p>

<ol>
  <li>The static prefix “Handle” becomes recognizable from glancing over summaries what class of tickets this belongs to</li>
  <li><em>error-message</em> is that nice juicy explanation of what actually happened. e.g “Full disk usage” or “Out of Memory” Most often you will find that sentence in a log or from the monitoring dashboard. This will most likely carry in some form of the bug-ticket title as well.</li>
  <li><em>instance</em> is the identifier of what machine or workload this was observed on. If you have multiple replicas you will want to know which one. e.g. “worker_fa4cd67e” or “service-db-2”.</li>
</ol>

<p>A naming convention ensures overview and readability for the human eye as the backlog grows, and helps include a minimum of relevant information.</p>

<p>The handler (or handlers, preferably) should make a best effort attempt at evaluating whether the observation is a known defect, or a new one. This classification helps determine what the right course of action is. It is also a great opportunity to communicate with your team and leverage collective knowledge “Hey we’re seeing this – has anyone observed it before?”
In the team, you should agree on a distinct course of action when no precedence can be found that ensures it can be properly found and triaged at a later point in time.</p>

<blockquote>
  <p>The team registers two tickets – one for the bug and one for the observation, linking them together. (<code class="language-plaintext highlighter-rouge">No space left on device</code> and <code class="language-plaintext highlighter-rouge">Handle: "No space left on device" on replica-2</code> respectively).</p>
</blockquote>

<h4 id="description">Description</h4>
<p>The primary objective is to collect evidence. Attach stack-traces, log outputs. Images of the monitoring visuals. You are the criminal investigator of a crime-scene - you don’t know what details are relevant so collecting comprehensive evidence is key.</p>

<p>Most important is:</p>
<ul>
  <li>Making a record of the actions taken.</li>
  <li>Recording what state you can of a system before doing any destructive operations (i.e. if you clear a folder due to full disk utilization, what is actually on there filling it up will explain something).</li>
</ul>

<blockquote>
  <p>The week after, replica-1 goes down. Same error message <code class="language-plaintext highlighter-rouge">No space left on device</code>. The team remembers the bug ticket from last week and knows it is the same thing. 
Running <code class="language-plaintext highlighter-rouge">ls -lah</code> on <code class="language-plaintext highlighter-rouge">/</code> reveals <code class="language-plaintext highlighter-rouge">/tmp</code> to be the culprit, containing a large number of files with varying size. The <code class="language-plaintext highlighter-rouge">/tmp</code> folder is wiped, restoring the service. The actions and output is added to a new ticket<code class="language-plaintext highlighter-rouge">Handle: No space left on device on replica-1"</code> which is linked back to its parent.</p>
</blockquote>

<h3 id="classification">Classification</h3>
<p>The bug-ticket represents the continued learning about; the bug in the machinery.</p>

<p>Focus is on iteratively filling in the description of the bug as we get wiser, allowing understanding to build up over time whilst keeping cognitive load low. For a first iteration, a copy/paste of the first observation ticket description will do. When we classify and triage early, we immediately start homing in on the pattern and cause of the defect.</p>

<p>From the initial task we likely know “how to identify/remedy” right from the start. As more observances are made, we might get smarter - i.e. instead of restarting the whole machine, we just have to restart this service. Start recording “next steps for investigation” (this is why you triage when handling, there might be clues as what to record or what actions to take next time around) - e.g If the disk fills up recurringly, we might want to start investigating what files or  workloads are present when it errors.</p>

<p>This structured approach will slowly reveal the root cause or appropriate mitigation of the defect to you and your team. Not only do you know the cause - you managed to track observances which mean you can extract the data of</p>

<ul>
  <li>How often does this happen?</li>
  <li>What is the impact?</li>
  <li>(When) will this happen again?</li>
</ul>

<p>These are questions you need to answer to make a case for working on a defect a priority.</p>

<blockquote>
  <p>The following week, disaster strikes again this time hitting replica 2 and 4. The week after next replica 3. Every time <code class="language-plaintext highlighter-rouge">/tmp</code> is filled up with some weirdly named files of varying size. The team can now show that there is something going on and it seems to hit the machines one by one. They manage to make the case to set aside time to investigate on the next sprint. One member of the team believes to have found the coding error causing this issue. It is all written down into a ticket linking back to the issue.</p>
</blockquote>

<p>With the root cause and patterns of the defect identified, the next step is to prevent it from recurring again.</p>

<h3 id="fireproofing-and-remediation">Fireproofing and Remediation</h3>
<p>You might think you managed to get to the root cause of an issue and fix it just to experience the same errors come popping up.</p>

<p>Addressing a defect is distinct from tracking and observing it in the wild, and separate tickets should track that progress. Luckily it goes well within this workflow and structure to track other work done relating to the bug-ticket – we can attach all of those work items alongside any observation.</p>

<blockquote>
  <p>The issue is found to indeed be a temporary file not deleted after processing an upload. This strikes a debate on handling of temporary files and one team member finds a good article upskilling everyone.</p>
</blockquote>

<p>In some cases you need to delegate work to other teams - in which case the context we are creating with this workflow becomes even more valuable. Make sure to properly fill out those tickets with relevance, business value and expectations. Also remember to respect their cadence and workflow.</p>

<blockquote>
  <p>A similar issue with temporary files elsewhere in the codebase is highlighted - this time it is not in the team`s own code but in another service - another ticket with a description of the potential issues and a description of the remediation is made, linking back to the bug. The ticket is then assigned to the colleague team. It makes it onto their next sprint without any back-and-forth.</p>
</blockquote>

<h2 id="wrapping-up">Wrapping Up</h2>
<p>There you have it, a structure to work with that provides all the data you need with the tools you probably already have at your disposal and use every day. From here, it is up to you, and your team, to make use of this workflow and resulting structure for analysis, data-driven decision making, and remediation, to tackle big or recurring problems incrementally, instead of repeatedly, together.</p>]]></content><author><name></name></author><category term="DevOps" /><category term="Software Engineering" /><summary type="html"><![CDATA[In an ideal world, defects are analyzed and resolved immediately, ensuring they never recur. In reality, however, resolving defects often relies on the tenacity of individual soldiers – or even plain luck – this approach leads to a culture of silos and uneven workloads. Even worse, without a structured problem-solving approach, other issues such as resource problems can mask as defects going on unnoticed or addressed indefinitely.]]></summary></entry><entry><title type="html">5 Tips to Maintain a Clean Commit History as an Individual Contributor</title><link href="https://rlindsgaard.github.io/software%20engineering/2025/10/19/5-tips-maintain-clean-commit-history.html" rel="alternate" type="text/html" title="5 Tips to Maintain a Clean Commit History as an Individual Contributor" /><published>2025-10-19T00:00:00+00:00</published><updated>2025-10-19T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/2025/10/19/5-tips-maintain-clean-commit-history</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/2025/10/19/5-tips-maintain-clean-commit-history.html"><![CDATA[<p>All the tips presented here is neither ground-breaking or new in any way. It is anything but, and the internet is filled with references on either topic (I took the liberty of linking to my favorites throughout the post).</p>

<p>What might be new, is my audacious attempt to provide a suggested priority or order in which to apply. I tried crystalizing the effort into concrete actionables that, in my humble opinion, yields the highest reward.</p>

<p>My intended audience is anyone I just so happen to think might benefit from reading these thoughts on the subject by linking them directly, shamelessly promoting my own thoughts - and, of course, any random developer passing by searching to hone his or her skillset.</p>

<h2 id="tip-1-rebase-early-rebase-often">Tip #1: Rebase Early, Rebase Often</h2>
<p>This tip placed at the top as it exposes you to any troubles as early as possible - merge conflicts are hell, but the larger the diff gets, the less likely you are able to resolve it and the more likely you will have wasted your time trying or applied a poor remedy.</p>

<p>Rebase your current work at least a couple of times a day, e.g. when you sit down at the start of your day and immediately after lunch. Personally I normally rebase on main prior to pushing anything remotely.</p>

<p>My favorite command (aliased I call it <code class="language-plaintext highlighter-rouge">refresh</code>) is <code class="language-plaintext highlighter-rouge">git pull --rebase --autosquash --autostash</code> this makes rebasing non-intrusive on my dirty worktree.</p>

<h2 id="tip-2-keep-commits-and-merge-requests-small">Tip #2: Keep Commits (and Merge Requests) Small</h2>
<p>Iterate functionality in the smallest way possible - stay on-point in your Merge Request (MR) and prefer to make more MRs. (I know, if review process is slow this is a pain, but then that pain needs to be addressed as a cultural/practical problem).</p>

<p>In general, you shouls consider that the more lines of code changed makes you more susceptible to merge conflicts (and your risk conflicting with the works of others).</p>

<ul>
  <li>Keep style and cleanup changes not directly related to your changes i.e. separate from feature work.</li>
  <li>Keep the number of changes and files in a commit down.</li>
  <li>Keep the number of commits in an MR down.</li>
  <li>Keep commits atomical (i.e. a particular change in the MR should be revertable without affecting other code)</li>
  <li>Write your changes so they can be integrated and work in isolation without being “activated” - i.e. add your library function and tests, then in isolation you can add the call site code.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">-p</code> when adding changes, this will allow you to review and partially add changes in a file. (note that you cannot add untracked files with this option).</li>
</ul>

<p>Kent Beck’s Tidy First is highly recommended reading, I have spent over a decade practicing good commit structure, still this book took me to a whole new level of thinking about when and how to structure my efforts.</p>

<h2 id="tip-3-use-descriptive-commit-messages">Tip #3: Use Descriptive Commit Messages</h2>

<p>In the ideal world we complete a task, switch context and then complete another one. In the real world, we switch contexts all the time - sometimes it is other work taking precedence forcing us to switch, sometimes it is time off due to weekends or vacation.</p>

<p>Pay it forward to your future self and practice being explicit in words giving a summary and your current reasoning for making a particular change.</p>

<p>Even more important is to write down what is currently <em>not</em> working or missing (so that you do not come back after an extended period of time, thinking work is complete and merge it to find out you entirely missed everything but the golden path).</p>

<p>I love pointing to <a href="https://commit.style/">https://commit.style/</a> and
<a href="https://dhwthompson.com/2019/my-favourite-git-commit">https://dhwthompson.com/2019/my-favourite-git-commit</a> as good short examples where <a href="https://cbea.ms/git-commit">https://cbea.ms/git-commit</a> covers the good commit message in depth. 
I also recommend adopting something akin to <a href="https://www.conventionalcommits.org/en/v1.0.0/">https://www.conventionalcommits.org/en/v1.0.0/</a> - not for the properties of parsing and automating generation of changelogs but for the purpose of ensuring all important pieces of information for posteritys sake are somewhat added the commit message.</p>

<p>If you think this sound like a lot of work, then then at first you maybe right - this brings me to tip #4.</p>

<h2 id="tip-4-learn-to-rewrite-branch-history">Tip #4: Learn to Rewrite Branch History</h2>

<p>Consider commits on a branch as “staged” changes - they are up for revision until they hit the main branch, in terms of content, description, and order for that matter - learn to quickly do history re-writes, the more you do it, the more second nature it becomes. Here is a selection of such re-writes I use on a daily to weekly basis.</p>

<p>See also <a href="https://about.gitlab.com/blog/keeping-git-commit-history-clean/">https://about.gitlab.com/blog/keeping-git-commit-history-clean/</a> for some more in-depth explanation of my presented scenarios. As an aside <a href="https://ohshitgit.com/">https://ohshitgit.com/</a> explains a number of ways to get you out of any pickle you are bound to get yourself into trying out these things.</p>

<h3 id="add-a-missing-filechange-to-the-last-commit">Add a missing file/change to the last commit</h3>
<p><code class="language-plaintext highlighter-rouge">git commit --amend --reuse-message=HEAD</code> will commit any changes currently staged. I alias it <code class="language-plaintext highlighter-rouge">git fix</code></p>

<h3 id="add-a-missing-filechange-to-a-previous-commit">Add a missing file/change to a previous commit.</h3>
<p>Assuming the changed is staged, use <code class="language-plaintext highlighter-rouge">git log</code> or similar to find the subject commit sha. Then <code class="language-plaintext highlighter-rouge">git commit --fixup &lt;commit-sha&gt;</code>. This will add a commit with a special message to your history and you don’t have to think of a commit message. Use <code class="language-plaintext highlighter-rouge">git rebase main --autosquash</code> to patch the target commit (or <code class="language-plaintext highlighter-rouge">git rebase -i main --autosquash</code> if you want to inspect the order first)</p>

<h3 id="i-need-this-bugfix-to-continue-my-work-but-it-is-unrelated">I need this bugfix to continue my work but it is unrelated</h3>

<p>Commit the change here and now to <code class="language-plaintext highlighter-rouge">feature-branch</code>, then checkout another branch from main and use <code class="language-plaintext highlighter-rouge">git cherry-pick feature-branch</code> (as long as the commit is the one on top cherry pick will do this) - push this fix and create an MR - continue working on feature branch and when the fix ME is merged, the bugfix change commit will automatically be removed from <code class="language-plaintext highlighter-rouge">feature-branch</code> upon rebase as it is empty.</p>

<h3 id="i-have-multiple-commits-that-when-squashed-finally-works">I Have Multiple Commits That When Squashed Finally Works</h3>

<p>Using <code class="language-plaintext highlighter-rouge">git rebase -i main</code> - I prefer the mechanism where squashes are made by <code class="language-plaintext highlighter-rouge">fixup</code> actions on subsequent commits (usually the first commit contains the description I wanted in the first place). Sometimes I <code class="language-plaintext highlighter-rouge">reword</code> the first one.</p>

<p>Sometimes this gets hairy, especially if you placed other changes in between - I then first go for multiple iterations i.e. using the rebase command over and over selecting only a subset of the changes to fix/squash each time.</p>

<p>If that doesn’t help I take on more drastic measures not covered here.</p>

<h3 id="this-change-was-placed-in-the-wrong-commit">This change was placed in the wrong commit</h3>

<p>Assuming there are no subsequent changes causing merge conflicts, <code class="language-plaintext highlighter-rouge">git rebase -i main</code> and select <code class="language-plaintext highlighter-rouge">edit</code> for the commit.</p>

<p>Rebase will now halt <code class="language-plaintext highlighter-rouge">git restore -p --staged &lt;filename&gt;</code> until the staged changes look like the commit you want. The remainding changes are left unstaged now.</p>

<p>Then <code class="language-plaintext highlighter-rouge">git rebase --continue</code>. Now the accidentally committed changes can be added to the appropriate commits.</p>

<p>Pro tip: Rebasing is just an operation that replays a set of commits onto a new base branch - you can even make new commits while performing this operation.</p>

<h2 id="tip-5-learn-the-internals-of-git">Tip #5: Learn the Internals of Git</h2>

<p>You don’t have to like working in the command line or even be able to get yourself out of any particular jam - but you need to invest time understanding the basics and the internals of Git. Most important is knowing the concepts and terminology just for the purpose of googling and prompting to get yourself out of a rot. Pro Git is available for free <a href="https://git-scm.com/book/en/v2">https://git-scm.com/book/en/v2</a> and is recommended reading for anyone working profesionally with software development.</p>

<p>In the end, nothing replaces learning the commands and their flags little by little with the help of the command line output and a manual.</p>

<h2 id="wrapping-up">Wrapping Up</h2>
<p>You don’t get good without greasing the groove. It might feel like a lot of work up front, but down the line this knowledge will save you headaches and redoing work. You are likely stuck with git as the defacto version control system for any foreseable future - and when the time comes, these lessons apply to the new tool as well. Any time invested upskilling in version control will be sure to pay itself back over and over during the course of the next decade(s) you don’t need to go the full mile instantly, just use this to consider where your next 1% improvement might be.</p>]]></content><author><name></name></author><category term="Software Engineering" /><summary type="html"><![CDATA[All the tips presented here is neither ground-breaking or new in any way. It is anything but, and the internet is filled with references on either topic (I took the liberty of linking to my favorites throughout the post).]]></summary></entry><entry><title type="html">Extrapolating the Throughput DORA Metrics Directly From Your Git Repository</title><link href="https://rlindsgaard.github.io/devops/software%20engineering/2024/02/12/throughput-dora-metrics-git.html" rel="alternate" type="text/html" title="Extrapolating the Throughput DORA Metrics Directly From Your Git Repository" /><published>2024-02-12T00:00:00+00:00</published><updated>2024-02-12T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/devops/software%20engineering/2024/02/12/throughput-dora-metrics-git</id><content type="html" xml:base="https://rlindsgaard.github.io/devops/software%20engineering/2024/02/12/throughput-dora-metrics-git.html"><![CDATA[<p>In this post I will explore a simple way to compute the DORA metrics that indicates throughput which involves no changes to the existing architecture of a VC/CI setup.</p>

<p>The purpose is to explore a method that allows for ease of experimentation working with the tools at hand to get started quickly with DORA as opposed to designing the perfect generic production setup.</p>

<p>If you need a primer on DORA metrics consider reading <a href="https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance">https://cloud.google.com/blog/products/devops-sre/using-the-four-keys-to-measure-your-devops-performance</a></p>

<p><strong>The TL;DR:</strong> Using gits built-in annotated tags to register when a release has been made, it is possible to use the “taggerdate” value og the reference. This can be used to compute the timedelta between each release to give the deployment frequency. Subsequently to obtain all commits in each release and compute the time between when the commit was made to the release time giving the lead time to release metric.</p>

<h2 id="applications-and-limitations">Applications and limitations</h2>

<p>For the example code the tag-system is expected to be used for releases only. It will probably take some customization and hardeking to get it working in a concrete scenario.The method will probably work in a simple pipeline structure as well as a fan-in pipeline structure. However the more sources you add and the more frequent changes are made, there is provably better ways to compute these values.
And of course, if you rewrite/squash history as part og merging etc, you will lose important pieces of data. Keep development in the trunk!</p>

<h2 id="creating-tags">Creating Tags</h2>
<p>When a new release is made, it is necessary to get the CI pipeline or a manual tool to post an annotated tag back to the repository.</p>

<blockquote>
  <p>$ git tag -a release-123456789 &amp;&amp; git push origin release-123456789</p>
</blockquote>

<p>Annotated tags are used as the timestamp of the tag creation time can then be used as opposed to lightweight tags that will only record the timestamp of the referenced commit. That might be ok for your use case.</p>

<h3 id="a-comment-on-security">A comment on security</h3>
<p>Beware that you do not inadvertently add a code injection vulnerability into your pipeline. Any acces from the automated pipeline to the repository should be with access to push tags only </p>

<h2 id="deployment-frequency">Deployment Frequency</h2>
<p>Deployment frequency is the time between deployments to the production system. I like to adopt the terminology of continuous delivery pipelines and use the term “release” instead of “deployment”. Given a deployment can be an internal deployment to a certain cluster etc of a release candidate. This mixed terminology has got me confused more than once.
In order to calculate the metric, we simply need to know the timestamps of each individual release and can then compute the mean of all the timedeltas between the releases.
Notice that when to record this timestamp differs whether you are doing continuous deployments or working with release candidates.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def get_releases():
    cmd = "git for-each-ref --shell --sort=taggerdate --format='%(if:equals=tag)%(objecttype)%(then){\"ref\":\"%(refname:short)\", \"dt\":%(taggerdate:format:%s)}%(end)' \"refs/tags/*\" | grep -v \"''\" | sed -e \"s/^'//\" -e \"s/'$//\""
    output = subprocess.check_output([cmd], shell=True)
    
    releases = []
    for line in output.decode('utf-8').splitlines():
        releases.append(json.loads(line))

    return releases

def calculate_deployment_frequency(releases):
    deltas = []
    for i in range(1, len(releases)):
         deltas.append(int(releases[i]['dt']) - int(releases[i-1]['dt']))
    return statistics.mean(deltas)

</code></pre></div></div>

<h2 id="lead-time-for-change">Lead Time for Change</h2>
<p>The mean time to deploy is a bit more tricky to compute and requires the known timestamps of each and any change made as well as which release it is part of. We want to calculate the distance from all of these changes to the release timestamp. 
This will give us the metric for <em>one</em> release, so we need to compute this for each changesets to each release.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def calculate_lead_time_to_release(releases):
    start = None
    deltas = []
    for release in releases:
        release['commits'] = []
        end = release['ref']

        r = f'"refs/tags/{start}".."refs/tags/{end}"'
        if start is None:
            r = f'"refs/tags/{end}"'
        
        subcmd = 'git log --pretty=\'{"ref": "%H", "ts": "%ct"}\' ' + r
        subout = subprocess.check_output([subcmd], shell=True)

        for line in subout.decode('utf-8').splitlines():
            cmt = json.loads(line)
            delta = int(release['dt']) - int(cmt['ts'])
            # Maybe bucket the commits into the release?
            # release['commits'].append(delta)
            deltas.append(delta)

        start = end
    return statistics.mean(deltas)

</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>
<p>I have shown a viable simplistic method for computing throughput metrics without a complicated setup making use of event handling and databases to get started using these metrics. The important part to me is how to obtain the relevant datapoints as they can be sliced and bucketed to any liking from here.</p>

<h2 id="complete-example-script">Complete example script</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#!/usr/bin/env python3
# Author: Ronni Elken Lindsgaard &lt;ronni.lindsgaard@gmail.com&gt;
# License: MIT

import subprocess
import json
import statistics
import argparse

units_of_time = {
    'days': 60*60*24,
    'hours': 60*60,
    'minutes': 60,
    'seconds': 1
}

def main(args):
    releases = get_releases()
    divisor = units_of_time[args.unit]
    print(f"deployment frequency: {calculate_deployment_frequency(releases)/divisor}")
    print(f"mttr: {calculate_lead_time_to_release(releases)/divisor}")

def get_releases():
    cmd = "git for-each-ref --shell --sort=taggerdate --format='%(if:equals=tag)%(objecttype)%(then){\"ref\":\"%(refname:short)\", \"dt\":%(taggerdate:format:%s)}%(end)' \"refs/tags/*\" | grep -v \"''\" | sed -e \"s/^'//\" -e \"s/'$//\""
    output = subprocess.check_output([cmd], shell=True)
    
    releases = []
    for line in output.decode('utf-8').splitlines():
        releases.append(json.loads(line))

    return releases

def calculate_deployment_frequency(releases):
    deltas = []
    for i in range(1, len(releases)):
         deltas.append(int(releases[i]['dt']) - int(releases[i-1]['dt']))
    return statistics.mean(deltas)

def calculate_lead_time_to_release(releases):
    start = None
    deltas = []
    for release in releases:
        release['commits'] = []
        end = release['ref']

        r = f'"refs/tags/{start}".."refs/tags/{end}"'
        if start is None:
            r = f'"refs/tags/{end}"'
        
        subcmd = 'git log --pretty=\'{"ref": "%H", "ts": "%ct"}\' ' + r
        subout = subprocess.check_output([subcmd], shell=True)

        for line in subout.decode('utf-8').splitlines():
            cmt = json.loads(line)
            delta = int(release['dt']) - int(cmt['ts'])
            # Maybe bucket the commits into the release?
            # release['commits'].append(delta)
            deltas.append(delta)

        start = end
    return statistics.mean(deltas)
    

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Calculate DORA throughput metrics')
    parser.add_argument(
        '-u', '--unit', help='unit of time', default='days',
        choices=['days', 'hours', 'minutes', 'seconds']
    )
    main(parser.parse_args())
</code></pre></div></div>]]></content><author><name></name></author><category term="DevOps" /><category term="Software Engineering" /><category term="DORA" /><category term="git" /><category term="metrics" /><summary type="html"><![CDATA[In this post I will explore a simple way to compute the DORA metrics that indicates throughput which involves no changes to the existing architecture of a VC/CI setup.]]></summary></entry><entry><title type="html">A Seniors Guide to Applying Design Principles</title><link href="https://rlindsgaard.github.io/software%20engineering/2022/08/06/seniors-guide-applying-design-principles.html" rel="alternate" type="text/html" title="A Seniors Guide to Applying Design Principles" /><published>2022-08-06T00:00:00+00:00</published><updated>2022-08-06T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/2022/08/06/seniors-guide-applying-design-principles</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/2022/08/06/seniors-guide-applying-design-principles.html"><![CDATA[<p>Design principles such as <em>SOLID</em> or
<em>Composition Over Inheritance</em> are abstract and can
be hard to implement in practice, especially if you try to think
them in before having any code. It takes practice working with
them and many “mistakes” before getting them right. I reckon
the difficulty lie in that it is an uncertain field and its
correctness is not verifiable via static code analysis. Furthermore,
there is usually more than one path to getting the result and
bias towards these practices.</p>

<p>This post will not be about any principle in particular, but instead
about how I apply those principle in practice, and more importantly
<em>when</em> in the process of development I apply those practices.</p>

<h2 id="growing-your-software">Growing Your Software</h2>
<p>As of late, I have been reading and watching much material that
draws similarities between software engineering and gardening, more
than architecting and building buildings. I subscribe wholeheartedly
to that analogy as that very much describes how my process looks
like. Enough of that analogy, I only ventured there to make a point:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  Running code is alive, or need to be. It needs to grow into being
  and it needs care. Once it has matured, it can live mostly by
  itself, but when we start writing the code we know only little
  of how it will look like in the end.
</code></pre></div></div>

<p>I for one have been fooled many times over by designing systems
up front in order for priorities to be shifted mid-way. To mitigate
this, I rigorously apply tracer bullets and the extreme programming
practice of quickly punching a hole. This makes me stay agile,
in the sense that changing directions is fairly easy.</p>

<p>Getting feature complete is a matter of iterative <em>refactoring</em>,
which brings me to</p>

<h2 id="another-analogy">Another Analogy</h2>
<p>When working on a greenfield project, we do not have much to
work with. It is a clean slate, but it is also a time of many
insecurities. Usually, we have some sort of idea of what we are
about to get ourselves into, we know enough to <em>sketch</em> a design
or architecture.</p>

<p>I sometimes draw, I think I am actually quite good at it using
guides and everything. That makes me draw up (get it?) this analogy.</p>

<p>The initial sketch is good, we have a general idea of the main
components that need to interact, where they will be placed - or
at least a suitable place. So, stop analyzing and get to business,
don’t worry about the principles just yet (wait, remember
<a href="https://enterprisecraftsmanship.com/posts/kiss-revisited/">KISS</a>
and <a href="https://enterprisecraftsmanship.com/posts/yagni-revisited/">YAGNI</a>
here).
Crank up the volume on your headset or whatever gets you going
and get writing some code (also don’t forget your TDD practices,
they’re going to be <em>very</em> important going forward.)</p>

<p>Oh, and <em>NO</em> <a href="https://enterprisecraftsmanship.com/posts/dry-revisited/">DRY</a>ing.
Everything should be <a href="https://dev.to/wuz/stop-trying-to-be-so-dry-instead-write-everything-twice-wet-5g33">WET</a>/copy-paste at this point.</p>

<p><img src="/assets/draw-an-owl.jpg" alt="How to draw an owl" /></p>

<h2 id="welcome-to-round-2">Welcome to round 2</h2>
<p>Ok, you nailed that first user story! You punched a hole and you
got a sunshine scenario working. It may be running on stilts,
but the tests pass. (and by the way, if you did not write a complete
end-to-end component integration test, now is the time to get
back to your test-suite) - because we are now ready to refactor
for great good – and refactoring means testing for regressions,
constantly. Component integration tests ensures that you do not
forget to update the interfaces between the already moving parts.</p>

<p>Waaait a second. Interaction between moving parts maybe calls
for some <a href="https://en.wikipedia.org/wiki/Design_by_contract">design by contract?</a></p>

<p>But first, get those changes committed, reviewed and deployed.</p>

<p>Now, you are ready for your, first no-brainer refactoring. Get
started on an interface that formalizes the structure between those
component. You got yourself some tight coupling there so that
part should maybe put into a shared library between the two components.</p>

<p>Don’t worry about having to deal with tests, you already wrote those.
You maybe need to change the in and out on your components’ unit
tests but your integration test should be steady as a rock - you’re
just checking for regressions. If you find that you are starting
to meddle with the component integration test, you’re overstepping
your boundaries just now (except maybe for some renaming of imports)</p>

<p>There is a possibility that other <em>smells</em> accumulated. Personally,
up to this point I have usually been somewhat inconsistent with naming
(it takes me some working with code and terminology before getting
it, almost, right… I never get naming right). In practice, I
start removing abbreviations in variable names and make sure all
methods are verb-phrases, all the code smells I can think of really.</p>

<p>With experience, you will know yourself well enough to know what
you usually let lying around. Might as well take care of it immediately.</p>

<p>Also, <em>DO NOT</em> DRY yet.</p>

<h2 id="round-3">Round 3</h2>

<p>Time passed, you had a breather because the system needed to get
running on the double, and then something else caught your POs
attention… And you needed to fix this bug you left lying around
in another system, and also your cat ate your commit, and…</p>

<p>Aaanyhow. At least you probably had a weekend or a chance to sit back and
think about the mess you made thus far. And your PM calls you
up saying “I got some new exiting task for you, and PO wants it
yesterday for this customer demo who will cancel anyhow”.</p>

<p>Great, you got another task, and new requirements to draw some
more lines – and maybe also start using an eraser. You
can begin to see where the code is heading (ok, you probably already
knew because the backlog accumulated like a waterfall thanks to PO/PM).
Do yourself a favour and ignore it, they have just as little idea
of what comes next. Because it is most likely going to be something
that they haven’t thought up yet.</p>

<p>But this is good. Now we start getting a glimpse of the future
of this code. Until now, we really just did some pre-germination or
some sketching. Now it’s time to take a new pencil and start
drawing some actual lines. (this analogy is really working for me!)
You can start making plans for your code. And you have a very
real idea of what you focus needs to be. It is the immediate surrounding
of the lines (of code) or components that are making it into the
changeset.</p>

<p>A word of caution, remember not to go full retard here. I have
been caught here too many times myself thinking “is this SOLID?
Ugh, which way does the composition arrow go? what do I need to
do to get it just right?” Just don’t.</p>

<p>It is a waste of your time. Or, it is not a complete waste, but your
time is better spent elsewhere so it is a waste of your employers
time and your <em>wild-ass-guesstimate</em> is closing in on its due date.
The important part is, your code is maturing, and it is
solidifying (and maybe a little bit of <a href="https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688">SOLID</a>ifying too?) which means
you also start being able to make better judgments about the
immediate future of the code, based on the lines you are actually
changing.</p>

<p>If you are anything like me, you will also start identifying the
pieces of the <em>main</em> code that really ought to be dragged out
into subroutines, split up or rename components, ordering function
arguments etc.</p>

<p>You may start DRYing now. But consider if things should not still be WET.</p>

<h2 id="rinse-repeat">Rinse Repeat</h2>
<p>This is it, it is this pattern iteration applied continuously
to use ones best judgment and applying <a href="https://www.stepsize.com/blog/how-to-be-an-effective-boy-girl-scout-engineer">the boyscout rule</a> ad-libitum.</p>

<p>Personally, I expect 2-3 major refactorings before I know where
code is supposed to be loaded and placed. Then I start working
on the details. Again, this is like when I draw, after drawing
the guides, I work on the “main” lines and features. Draw those
over and over to figure out exactly how they should be placed
between one another. And when I got this sorted, I start working
playing with the details.</p>

<p>For the code, it means that now it begins to make sense to start
 running down the checklist of your favorite principles and patterns
to apply. One by one.</p>

<p><img src="/assets/owl-2.png" alt="Drawing an owl" /></p>

<h2 id="stay-agile">Stay agile</h2>

<p>As a senior developer I have witnessed refactoring is
something developers needs to be encouraged to do (especially
if they did not write the code they are changing in the first
place and they have no ownership).  I regularly get “but it has
already been reviewed” or “Let us do it some other time” when suggesting
refactoring. I have a feeling the resistance is really due to it
being seen as an admittance that “If I refactor, it must mean I
was wrong the first time around and I don’t like admitting that”.</p>

<p>If that sounds like a familiar I hope it helps to remember that
a garden is never perfect either, it is filled with living things
requiring attention and it needs to be taken care of constantly.
Sometimes, in order to grow, plants also need a little bit of
time, or moved into or out of the shade. Sometimes they need to
be trimmed, pruned, or nurtured. Some plants just die, some gets
replaced for other reasons. In any case, the garden needs a caretaker
or it will either overgrow or rot. Likewise will code.</p>

<p><img src="/assets/harder-better-faster-stronger.jpeg" alt="Harder Better Faster Stronger" /></p>

<p>I hope this inspired you. I know design principles can
be hard to follow, not just understand, but also apply. This is written
with the intention of giving some general pointers as to how a
senior engineer goes about obtaining quality code. I am sure
I have not managed to write down this process in full, and ask
me in a year, and I would probably lay it out differently.</p>

<p>If you aim for perfect, you never get done. So just take one small
step in the right direction, with each and every commit.</p>

<h2 id="summary">Summary</h2>

<p>Design principles are great. I use them all of the time. But most
importantly, I also know when in the process of development I
should apply them.</p>

<ul>
  <li>First iteration of a project or feature, only do the ones you
got down habitually and simply “know” from experience is going
to work. Focus on the business value, not the code.</li>
  <li>Second and third iteration, start identifying where the code
is headed, where the “moving” parts supposedly are and get
them isolated. Start clearing out smells as you get more intimate
with the problem domain and its terminology.</li>
  <li>Ad-libitum, take care and clean the camp-ground - you will
know when something starts to mis-align, and <em>this</em> is where you
take a step back and refactor.</li>
</ul>

<h2 id="recommended-reading">Recommended Reading</h2>
<p>Litterature that makes the basis for my train of thought, or at
least the teaching I recollect came from</p>

<ul>
  <li><a href="https://www.goodreads.com/book/show/3735293-clean-code">Clean Code, Robert C Martin</a></li>
  <li><a href="https://www.goodreads.com/book/show/10284614-the-clean-coder">The Clean Coder, Robert C Martin</a></li>
  <li><a href="https://www.goodreads.com/book/show/387190.Test_Driven_Development">Test Drive Development, Kent Beck</a></li>
  <li><a href="https://www.goodreads.com/book/show/67833.Extreme_Programming_Explained">Extreme Programming, Kent Beck</a></li>
  <li><a href="https://www.goodreads.com/book/show/4099.The_Pragmatic_Programmer">The Pragmatic Programmer, David Thomas &amp; Andrew Hunt</a></li>
  <li><a href="https://www.goodreads.com/book/show/44936.Refactoring">Refactoring, Martin Fowler</a></li>
</ul>]]></content><author><name></name></author><category term="Software Engineering" /><summary type="html"><![CDATA[Design principles such as SOLID or Composition Over Inheritance are abstract and can be hard to implement in practice, especially if you try to think them in before having any code. It takes practice working with them and many “mistakes” before getting them right. I reckon the difficulty lie in that it is an uncertain field and its correctness is not verifiable via static code analysis. Furthermore, there is usually more than one path to getting the result and bias towards these practices.]]></summary></entry><entry><title type="html">Writing, Testing and Documenting Utility Functions</title><link href="https://rlindsgaard.github.io/software%20engineering/2022/05/14/writing-testing-and-documenting-utility-functions.html" rel="alternate" type="text/html" title="Writing, Testing and Documenting Utility Functions" /><published>2022-05-14T00:00:00+00:00</published><updated>2022-05-14T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/software%20engineering/2022/05/14/writing-testing-and-documenting-utility-functions</id><content type="html" xml:base="https://rlindsgaard.github.io/software%20engineering/2022/05/14/writing-testing-and-documenting-utility-functions.html"><![CDATA[<h1 id="writing-testing-and-documenting-utility-functions">Writing, Testing, and Documenting utility functions.</h1>

<p>To me, utility functions are small bits of code that are <a href="https://enterprisecraftsmanship.com/posts/dry-revisited/">DRYed</a> as they
are repetitive and does not relate directly to what the application I am writing
does. It is more of a short-hand than anything else and I would otherwise have
to write the exact same lines of code over and over.</p>

<p>If it relates directly to the application, I don’t consider it a utility
function but an application of the clean code “functions should do just
one thing”</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def isoformat(dt: datetime):
   return dt.strftime('%Y-%m-%dT%H:%M:%S')
</code></pre></div></div>
<p><span class="caption">An example of a utility function. This is so generic that
it is now part of the standard library</span></p>

<p>Utility functions usually “stand alone”, as they are not directly part of the
application logic, it makes sense to keep them boxed in a <code class="language-plaintext highlighter-rouge">util</code> library or
similar.</p>

<p>Even though they are defined for a particular part of the code, given the entire
project works with the same problem domain, there is a considerable possibility
it is re-usable elsewhere.</p>

<h2 id="testing">Testing</h2>

<p>Utility functions are a very good place to practice TDD - you put something
in and something comes out. Both ought to be simple and clearly defined.
Utility functions, in my book, should rarely come with side-effects.</p>

<p>No doubt the function will be tested as part of an integration test for the
application logic (right?).</p>

<p>However, from a maintenance point of view there is a possibility that the application
logic will change or be removed from making use of that function. An observant
developer would remove the utility function together with when making that change,
but since it is added as a dependency. But, if there is no tooling to detect dead
code, the likelihood of that utility function haunting the code-base after its
utility has stopped is non-negligible. This in itself adds entropy, however when
removing the application code, as a side-effect you also remove test-coverage of
the utility function. Without separate tests, you get double up on code rot –
in fact you get triple, because the knowledge of its utility is also removed.</p>

<p>Therefore, a utility function must always be tested separately from the application
logic which is the main cause for its existence.</p>

<p>Altogether, when I add a utility method, I ensure to always include a separate
unit test for that function.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def test_strftime_isoformat():
    dt = datetime(1989, 11, 09)
    assert strftime(dt) == '1989-11-09T00:00:00'
</code></pre></div></div>

<p>This conveys intent of the function to any other developer who might come by and
think about changing anything. If not discouraging change, at least the
expected interface is clear for the existing utilization of the utility function.</p>

<p>One might imagine a change</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def isoformat(dt: datetime):
    return strftime(dt)

def strftime(dt: datetime, fmt='%Y-%m-%dT%H:%M:%S'):
    return dt.strftime(fmt)
</code></pre></div></div>

<p><em>Notice how neither application logic or tests are changed.</em></p>

<h2 id="documentation">Documentation</h2>

<p>The only users of a utility library is other developers on the team. The best place to put documentation is <em>in</em> the source code.</p>

<p>This is the most likely place another developer would look for what a specific utility method does anyhow, is it not?</p>

<p>Finally, it will allow you to populate project documentation automatically.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def isoformat(dt: datetime):
    """Turn timestamp into an iso8601 string."""

    return strftime(dt)

def strftime(dt: datetime, fmt='%Y-%m-%dT%H:%M:%S'):
    """Use `fmt` to format `dt` into a string.

    Equivalent to calling `dt.strftime(fmt)`.
    """
    return dt.strftime(fmt)
</code></pre></div></div>

<p>These utility functions are so simple that when looking at the code it does not bring any value.
However, when generating documentation for the utility library you see some benefit.</p>

<p><img src="/assets/utility-functions/sphinx-with-docstrings.png" alt="screenshot-sphinx-with-docstrings" /></p>

<p>(without a docstring, by default sphinx will not show you the function in the first
place)</p>

<p>And of course, there is more to it.</p>

<h2 id="tests-are-documentation">Tests <em>are</em> Documentation</h2>

<p>Nothing shows or documents the code better than tests, and python has a neat way of finding and running tests from within the function docstring. Meet <a href="https://docs.python.org/3/library/doctest.html">doctest</a></p>

<p>You get to check that your code is correct <em>and</em> show how it works at the same time. After all, who remembers if ISO8601 has the <code class="language-plaintext highlighter-rouge">T</code> in it or not? And is <code class="language-plaintext highlighter-rouge">Z</code> required?</p>

<p>You also do your future self, and any other developer coming by, a one up by keeping the test right where the implementation is – we all have enough files open as it is, don’t we?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def isoformat(dt: datetime):
    """Turn timestamp into an iso8601 string.

    &gt;&gt;&gt; isoformat(datetime(1991, 12, 25, 16, 35))
    1991-12-25T16:35:00

    """
    return dt.strftime('%Y-%m-%dT%H:%M:%S')
</code></pre></div></div>

<p>It is way more descriptive and readable than <code class="language-plaintext highlighter-rouge">:rtype: str</code> is it not? And you get
some neat formatting in your documentation too</p>

<p><img src="/assets/utility-functions/sphinx-doctest.png" alt="sphinx-doctest" /></p>

<h2 id="summary">Summary</h2>

<ul>
  <li>
    <p>Use utility functions for code of generic character. Detach them from the
application code.</p>
  </li>
  <li>
    <p>Be sure to treat the utility library and functions as stand-alone components.</p>
  </li>
  <li>
    <p>The code in itself is of a generic character and may not show
how it is intended to be used. Make sure to leave clues.</p>
  </li>
  <li>
    <p>You can can code and test in one place, giving you documentation for free.</p>
  </li>
</ul>

<h1 id="appendix">Appendix</h1>

<p>Try out the module below with</p>

<p><code class="language-plaintext highlighter-rouge">python3 -m doctest -v util.py</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>from datetime import datetime

def isoformat(dt: datetime):
    """Turn timestamp into an iso8601 string.

    &gt;&gt;&gt; isoformat(datetime(1991, 12, 25, 16, 35))
    '1991-12-25T16:35:00'

    """
    return dt.strftime('%Y-%m-%dT%H:%M:%S')
</code></pre></div></div>]]></content><author><name></name></author><category term="Software Engineering" /><category term="python" /><category term="sphinx" /><category term="testing" /><category term="documentation" /><summary type="html"><![CDATA[Writing, Testing, and Documenting utility functions.]]></summary></entry><entry><title type="html">Writing a Simple Sequence Diagram in Graphviz DOT</title><link href="https://rlindsgaard.github.io/graphviz/2022/04/03/writing-simple-sequence-diagram-graphviz-dot.html" rel="alternate" type="text/html" title="Writing a Simple Sequence Diagram in Graphviz DOT" /><published>2022-04-03T00:00:00+00:00</published><updated>2022-04-03T00:00:00+00:00</updated><id>https://rlindsgaard.github.io/graphviz/2022/04/03/writing-simple-sequence-diagram-graphviz-dot</id><content type="html" xml:base="https://rlindsgaard.github.io/graphviz/2022/04/03/writing-simple-sequence-diagram-graphviz-dot.html"><![CDATA[<p><img src="/assets/simple-sequence-diagram.png" alt="Rendered graph" /></p>

<div class="language-dot highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">digraph</span> <span class="nv">G</span> <span class="p">{</span>

    <span class="p">{</span> <span class="c1">// Define the actor nodes in the top</span>

        <span class="c1">// Make sure they're on the same row</span>
        <span class="n">rank</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span>

        <span class="c1">// Style the actor nodes</span>
        <span class="k">node</span> <span class="o">[</span><span class="n">shape</span><span class="p">=</span><span class="nv">box</span><span class="o">]</span><span class="p">;</span>

        <span class="nv">actor1_top</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"Actor 1"</span><span class="o">]</span><span class="p">;</span>
        <span class="nv">actor2_top</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"Actor 2"</span><span class="o">]</span><span class="p">;</span>
        <span class="nv">actor3_top</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"Actor 3"</span><span class="o">]</span><span class="p">;</span>

        <span class="c1">// Tie them together in the right order</span>
        <span class="k">edge</span> <span class="o">[</span><span class="n">style</span> <span class="p">=</span> <span class="nv">invis</span><span class="o">]</span><span class="p">;</span>
        <span class="nv">actor1_top</span> <span class="o">-&gt;</span> <span class="nv">actor2_top</span> <span class="o">-&gt;</span> <span class="nv">actor3_top</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Define the actor nodes in the bottom. This is a copy paste</span>
    <span class="c1">//of the top subgraph -  s/top/bottom/g</span>
    <span class="p">{</span>
        <span class="n">rank</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span>
        <span class="k">node</span> <span class="o">[</span><span class="n">shape</span><span class="p">=</span><span class="nv">box</span><span class="o">]</span><span class="p">;</span>

        <span class="nv">actor1_bottom</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"Actor 1"</span><span class="o">]</span><span class="p">;</span>
        <span class="nv">actor2_bottom</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"Actor 2"</span><span class="o">]</span><span class="p">;</span>
        <span class="nv">actor3_bottom</span> <span class="o">[</span><span class="n">label</span><span class="p">=</span><span class="s2">"Actor 3"</span><span class="o">]</span><span class="p">;</span>

        <span class="k">edge</span> <span class="o">[</span><span class="n">style</span> <span class="p">=</span> <span class="nv">invis</span><span class="o">]</span><span class="p">;</span>
        <span class="nv">actor1_bottom</span> <span class="o">-&gt;</span> <span class="nv">actor2_bottom</span> <span class="o">-&gt;</span> <span class="nv">actor3_bottom</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Style the event nodes</span>
    <span class="k">node</span> <span class="o">[</span><span class="n">shape</span><span class="p">=</span><span class="nv">point</span><span class="o">]</span><span class="p">;</span>
    <span class="k">edge</span> <span class="o">[</span><span class="n">arrowhead</span><span class="p">=</span><span class="nv">none</span><span class="o">]</span><span class="p">;</span>

    <span class="c1">// Define the event nodes. Here they are prefixed with</span>
    <span class="c1">// the actor name.</span>
    <span class="nv">actor1_event1</span>
    <span class="nv">actor1_event3</span>

    <span class="c1">// Now we connect each of the events like pearls on a string.</span>
    <span class="nv">actor1_top</span> <span class="o">-&gt;</span>
    <span class="nv">actor1_event1</span> <span class="o">-&gt;</span>
    <span class="nv">actor1_event3</span> <span class="o">-&gt;</span>
    <span class="nv">actor1_bottom</span><span class="p">;</span>

    <span class="c1">// Repeat. The event above has a corresponding node in the</span>
    <span class="c1">// destination actor.</span>
    <span class="nv">actor2_event1</span>
    <span class="nv">actor2_event2</span>

    <span class="nv">actor2_top</span> <span class="o">-&gt;</span>
    <span class="nv">actor2_event1</span> <span class="o">-&gt;</span>
    <span class="nv">actor2_event2</span> <span class="o">-&gt;</span>
    <span class="nv">actor2_bottom</span><span class="p">;</span>

    <span class="c1">// And one more time.</span>
    <span class="nv">actor3_event2</span>
    <span class="nv">actor3_event3</span>

    <span class="nv">actor3_top</span> <span class="o">-&gt;</span>
    <span class="nv">actor3_event2</span> <span class="o">-&gt;</span>
    <span class="nv">actor3_event3</span> <span class="o">-&gt;</span>
    <span class="nv">actor3_bottom</span><span class="p">;</span>

    <span class="c1">// We connect each event src/dest. First we make sure they</span>
    <span class="c1">// are vertically aligned.</span>
    <span class="p">{</span><span class="n">rank</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span> <span class="nv">actor1_event1</span> <span class="nv">actor2_event1</span><span class="p">}</span>
    <span class="p">{</span><span class="n">rank</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span> <span class="nv">actor2_event2</span> <span class="nv">actor3_event2</span><span class="p">}</span>
    <span class="p">{</span><span class="n">ranke</span><span class="p">=</span><span class="nv">same</span><span class="p">;</span> <span class="nv">actor3_event3</span> <span class="nv">actor1_event3</span><span class="p">}</span>

    <span class="c1">// Finally, we connect the dots.</span>
    <span class="k">edge</span> <span class="o">[</span><span class="n">constraint</span><span class="p">=</span><span class="nv">false</span><span class="p">,</span> <span class="n">arrowhead</span><span class="p">=</span><span class="nv">normal</span><span class="o">]</span><span class="p">;</span>

    <span class="nv">actor1_event1</span> <span class="o">-&gt;</span> <span class="nv">actor2_event1</span> <span class="o">[</span><span class="n">xlabel</span><span class="p">=</span><span class="s2">"start"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">actor2_event2</span> <span class="o">-&gt;</span> <span class="nv">actor3_event2</span> <span class="o">[</span><span class="n">xlabel</span><span class="p">=</span><span class="s2">"do something"</span><span class="o">]</span><span class="p">;</span>
    <span class="nv">actor3_event3</span> <span class="o">-&gt;</span> <span class="nv">actor1_event3</span> <span class="o">[</span><span class="n">xlabel</span><span class="p">=</span><span class="s2">"return"</span><span class="o">]</span><span class="p">;</span>

    <span class="c1">// We use xlabel, as normal labels will skew the whole thing.</span>
<span class="p">}</span>

</code></pre></div></div>]]></content><author><name></name></author><category term="graphviz" /><summary type="html"><![CDATA[]]></summary></entry></feed>